notes 笔记notes 笔记
Home
Article
Category
Tag
Timeline
Home
Article
Category
Tag
Timeline
  • build

    • 环境变量
  • browse

    • 2fa
    • sse
    • token
  • database

    • mongodb

      • start
    • mysql

      • curd
      • 安装
      • join
      • 多对多
      • 性能优化
      • 表设计
      • 常见问题
  • docker

    • github-actions

      • local
      • prod
    • docker-compose
    • index
    • podman
  • file

    • 文件下载
    • 原生 node.js下载文件
  • git

    • index
    • multiple-github-accounts
    • auto-commit
    • pat
    • rebase
  • linux

    • grep
    • index
    • ssh
    • vim
    • windows
  • network

    • best-proxy-way
    • git-via-https
    • github-push-fail
  • nginx

    • acme.sh
    • cache
    • https
    • index
    • safe
  • node

    • fnm
    • tool
  • obsidian

    • ish
    • start
  • react

    • index
    • set-state
  • summary

    • index
  • vue

    • index
    • typescript
  • libs
  • open-source
  • 概念篇
    • React 的核心思想
    • 渲染器
    • reconcilers
    • Stack reconciler
    • Fiber reconciler
    • Fiber 架构
      • 什么是 fiber?
      • 两个阶段的拆分
        • Reconciliation 阶段
        • Commit 阶段
    • 类组件
      • 生命周期
      • setState
      • forceUpdate
      • static defaultProps
      • static displayName
    • 函数式组件
    • 事件处理
      • 合成事件
      • 事件触发顺序
      • 事件处理函数
      • 向事件处理程序传递参数
    • key
    • ref
      • 何时使用 ref
      • Refs 与函数组件
      • 将 DOM Refs 暴露给父组件
    • context
      • Context.Constomer 和 函数式组件
      • 动态 context
      • 在嵌套组件中更新 Context
      • 消费多个 Context
      • 意外渲染
    • 高阶组件
      • 不要改变原始组件
      • 将不相关的 props 传递给被包裹的组件
      • 最大化可组合性
      • 包装显示名称以便轻松调试
      • 不要在 render 方法中使用 HOC
      • 务必复制静态方法
      • Refs 不会被传递
    • render props
      • 对比高阶组件
      • render prop 优化

概念篇

React 的核心思想

内存中维护一颗虚拟DOM树,数据变化时(setState),自动更新虚拟 DOM,得到一颗新树,然后 Diff 新老虚拟 DOM 树,找到有变化的部分,得到一个 Change(Patch),将这个 Patch 加入队列,最终批量更新这些 Patch 到 DOM 中。

渲染器

React 最初只是服务于 DOM,但是这之后被改编成也能同时支持原生平台的 React Native。因此,在 React 内部机制中引入了“渲染器”这个概念。

渲染器用于管理一棵 React 树,使其根据底层平台进行不同的调用。

reconcilers

即便 React DOM 和 React Native 渲染器的区别很大,但也需要共享一些逻辑。特别是协调算法需要尽可能相似,这样可以让声明式渲染,自定义组件,state,生命周期方法和 refs 等特性,保持跨平台工作一致。

为了解决这个问题,不同的渲染器彼此共享一些代码。我们称 React 的这一部分为 “reconciler”。当处理类似于 setState() 这样的更新时,reconciler 会调用树中组件上的 render(),然后决定是否进行挂载,更新或是卸载操作。

Reconciler 没有单独的包,因为他们暂时没有公共 API。相反,它们被如 React DOM 和 React Native 的渲染器排除在外。

Stack reconciler

“stack” reconciler 是 React 15 及更早的解决方案,已经停止使用。实现说明

Fiber reconciler

fiber reconciler 是一个新尝试,致力于解决 stack reconciler 中固有的问题,同时解决一些历史遗留问题。Fiber 从 React 16 开始变成了默认的 reconciler。 它的主要目标是:

  • 能够把可中断的任务切片处理。
  • 能够调整优先级,重置并复用任务。
  • 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
  • 能够在 render() 中返回多个元素。
  • 更好地支持错误边界。 虽然这已经在 React 16 中启用了,但是 async 特性还没有默认开启。 React Fiber 架构、深入了解 react 中的新 reconciliation 算法

Fiber 架构

React Fiber是对React核心算法的不断重新实现。这是React团队经过两年多研究的结晶。 它的标题功能是增量渲染:将渲染工作分成多个块并将其分布到多个帧中的能力。

什么是 fiber?

fiber 的主要目标是使React能够利用调度的优势。具体来说,我们需要能够:

  • 暂停工作,稍后再回来。
  • 为不同类型的工作分配优先级。
  • 重用以前完成的工作。
  • 如果不再需要,则中止工作。 为了做到这一点,我们首先需要一种将工作分解成多个单元的方法。从某种意义上讲,这就是 fiber。fiber 代表工作单元。

但是仅仅是分解为单元也无法做到中断任务,因为函数调用栈就是这样,每个函数为一个工作,每个工作被称为堆栈帧,它会一直工作,直到堆栈为空,无法中断。

所以我们需要一种增量渲染的调度,那么就需要重新实现一个堆栈帧的调度,这个堆栈帧可以按照自己的调度算法执行他们。另外由于这些堆栈是可以自己控制的,所以可以加入并发或者错误边界等功能。

因此 Fiber 就是重新实现的堆栈帧,本质上 Fiber 也可以理解为是一个虚拟的堆栈帧,将可中断的任务拆分成多个子任务,通过按照优先级来自由调度子任务,分段更新,从而将之前的同步渲染改为异步渲染。

所以我们可以说 Fiber 是一种数据结构(堆栈帧),也可以说是一种解决可中断的调用任务的一种解决方案,它的特性就是时间分片(time slicing)和暂停(supense)。

两个阶段的拆分

每次渲染有两个阶段:Reconciliation(协调阶段) 和 Commit(提交阶段).

Reconciliation 阶段

可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为 副作用(Effect)。以下生命周期钩子会在协调阶段被调用:

  • [UNSAFE_]componentWillMount(弃用)
  • [UNSAFE_]componentWillReceiveProps(弃用)
  • getDerivedStateFromProps
  • shouldComponentUpdate
  • [UNSAFE_]componentWillUpdate(弃用)
  • render

在协调阶段如果时间片用完,React 就会选择让出控制权。因为协调阶段执行的工作不会导致任何用户可见的变更,所以在这个阶段让出控制权不会有什么问题。

需要注意的是:因为协调阶段可能被中断、恢复,甚至重做,React 协调阶段的生命周期钩子可能会被调用多次!, 例如 componentWillMount 可能会被调用两次。

因此建议 协调阶段的生命周期钩子不要包含副作用。索性 React 就废弃了这部分可能包含副作用的生命周期方法,例如componentWillMount、componentWillUpdate。

Commit 阶段

将上一个阶段计算出来的需要处理的**副作用(Effects)**一次性执行,将 Diff 的结果反映到真实 DOM 的过程。这个阶段必须同步执行,不能被打断. 这些生命周期钩子在提交阶段被执行:

  • getSnapshotBeforeUpdate 严格来说,这个是在进入 commit 阶段前调用
  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

提交阶段必须同步执行,不能中断的吧。因为我们要正确地处理各种副作用,包括 DOM 变更、还有在componentDidMount中发起的异步请求、useEffect 中定义的副作用。因为有副作用,所以必须保证按照次序只调用一次,况且会有用户可以察觉到的变更, 不容差池。

注意:

  • reconciler 是调和器,是一个名词,可以说是 React 工作的一个模块,协调模块;
  • reconcile 是调和器调和的动作,是一个动词;
  • reconciliation 只是 reconcile 过程的第一个阶段。

类组件

生命周期

声明周期方法

  • constructor
  • static getDerivedStateFromProps
  • render
  • componentDidMount
  • shouldComponentUpdate
  • getSnapshotBeforeUpdate
  • componentDidUpdate
  • componentWillUnmount

setState

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。

setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。

除非 shouldComponentUpdate() 返回 false,否则 setState() 将始终执行重新渲染操作。

如需基于之前的 state 来设置当前的 state,updater 可以传递函数

this.setState((state) => {
    return {quantity: state.quantity + 1}
})

注意以下几点:

  • 不要直接修改 state
  • state 的更新可能是异步的
  • state 的更新会被合并

forceUpdate

强制让组件重新渲染,一般不要使用,作为最后的手段

static defaultProps

一般封装组件用,设置组件 props 的默认值

static displayName

用于调试工具显示的组件名,一般不需要管它。但是在高阶组件、Context.Provider 中需要手动处理。

约定命名:

  • 高阶组件,例如:WithAuth(Home)
  • Context,例如:MyDisplayName.Provider, MyDisplayName.Consumer
  • React.forwardRef,例如:ForwardRef(myFunction)。

函数式组件

没有生命周期的组件,函数内容就相当于是 render 方法。

利用 hooks api 可以方便的在函数组件中复用逻辑。

事件处理

React 元素的事件处理和 DOM 原生事件有些区别:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
  • 阻止默认行为需要显示调用 e.preventDefault(),而不是返回 false。

合成事件

在上面,e 是一个合成事件,称为 SyntheticEvent,它将被传递给事件处理函数。它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。需要使用浏览器的底层事件时,使用 e.nativeEvent 属性来获取即可。

事件触发顺序

React 利用了事件委托机制,将所有事件绑定到了 document 之上。当一个元素同时绑定原生事件和 react 事件时,原生事件先执行,冒泡到 document 上再执行 react 事件。

事件处理函数

事件处理函数中的 this 需要自行处理,一般使用箭头函数 或者 bind 来正确绑定 this。

class LoginButton extends React.Component {
 constructor(props) {
   super(props)
   // 在构造器中使用 bind 绑定
   this.handleClickBinded = this.handleClick.bind(this)
 }
 // 这里的 this 是 undefined
 handleClick() {
   console.log('this is:', this)
 }
 // 直接使用箭头函数定义 handleClick
 handleClick = () => {
   console.log('this is:', this)
 }

 render() {
   return (
     <button onClick={this.handleClick}>
       Click me
     </button>
   )
 }
}

一般不推荐在 JSX 中绑定 this,每次渲染都会创建不同的回调函数。如果该回调函数作为 prop 传入子组件时,会导致子组件额外的重新渲染。

class LoginButton extends React.Component {
 handleClick() {
   console.log(this)
 }

 render() {
   // 不可取,每次渲染会创建不同的回调函数
   return (
     <button onClick={() => this.handleClick()}>
       Click me
     </button>
   )
 }
}

向事件处理程序传递参数

在循环中,通常我们会为事件处理函数传递额外的参数。

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

除了使用箭头函数和 bind 函数传递参数之外,还可以使用 data-attributes 传递参数。

deleteRow(e) {
 console.log(e.target.dataset.id)
}
<button onClick={this.deleteRow} data-id={id}>Delete Row</button>

key

key 是在创建元素数组时,需要用到的一个特殊字符串属性。key 帮助 React 识别出被修改、添加或删除的 item。应当给数组内的每个元素都设定 key,以使元素具有固定身份标识。react 会在渲染中复用相同 key 属性的标签。

ref

React 支持一个特殊的、可以附加到任何组件上的 ref 属性。此属性可以是一个由 React.createRef() 或者 React.useRef() 创建的对象,也可以是一个回调函数。当 ref 属性是一个回调函数时,此函数会(根据元素的类型)接收底层 DOM 元素或 class 实例作为其参数。这能够让你直接访问 DOM 元素或组件实例。

ref 的值根据节点的类型而有所不同:

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 不能在函数组件上使用 ref 属性,因为他们没有实例。

何时使用 ref

  • 管理焦点,文本选择或媒体播放。
  • 触发强制动画。
  • 集成第三方 DOM 库。

避免使用 refs 来做任何可以通过声明式实现来完成的事情。

例如,避免在 Modal 组件里暴露 open() 和 close() 方法,最好传递 visible 属性。

Refs 与函数组件

虽然不能在函数组件上使用 ref 属性,但是函数组件内部可以使用 ref 属性的,只要它指向一个 DOM 元素或 class 组件。

将 DOM Refs 暴露给父组件

有些情况我们需要在父组件引用子组件内的 dom,有两种方法:

  • 使用不同于 ref 名字的键代替
  • 使用 React.forwardRef() 进行 ref 转发

第一种,一般使用诸如 inputRef 之类的名字代替 ref

function CustomTextInput(props) {
 return (
   <div>
     <input ref={props.inputRef} />
   </div>
 )
}
class Parent extends React.Component {
 constructor(props) {
   super(props)
   this.inputElement = React.createRef()
 }
 render() {
   return (
     <CustomTextInput inputRef={this.inputElement} />
   )
 }
}

第二种,使用 React.forwardRef()(16.3+ 版本)

const CustomTextInput = React.forwardRef((props, ref) => (
 <div>
   <input ref={ref} />
 </div>
))
class Parent extends React.Component {
 constructor(props) {
   super(props)
   this.inputElement = React.createRef()
 }
 render() {
   return (
     <CustomTextInput ref={this.inputElement} />
   )
 }
}

context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。 在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:UI 尺寸,UI 主题,locale),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

使用 context, 可以避免通过中间元素传递 props:

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light')
class App extends React.Component {
 render() {
   // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
   // 无论多深,任何组件都能读取这个值。
   // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
   return (
     <ThemeContext.Provider value="dark">
       <Toolbar />
     </ThemeContext.Provider>
   )
 }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar() {
 return (
   <div>
     <ThemedButton />
   </div>
 )
}

class ThemedButton extends React.Component {
 // 指定 contextType 读取当前的 theme context。
 // React 会往上找到最近的 theme Provider,然后使用它的值。
 // 在这个例子中,当前的 theme 值为 “dark”。
 static contextType = ThemeContext // 新语法
 render() {
   return <Button theme={this.context} />
 }
}
ThemedButton.contextType = ThemeContext // 旧语法

基本流程:

  • 使用 React.createContext(defaultValue) 创建一个 context
  • 使用 Context.Provider 生产组件提供 value 属性
  • 在需要 value 的组件中,将 Context 对象赋值给 class 组件的静态属性 contextType,然后可以通过 this.context 访问到 Context 上的那个值。

Context.Constomer 和 函数式组件

函数式组件没有静态属性 contextType,使用 Context.Constomer 消费组件代替。

<MyContext.Consumer>
  {value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>

这里,React 组件也可以订阅到 context 变更。这能让你在函数式组件中完成订阅 context。

这需要函数作为子元素(function as a child)这种做法。这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。如果没有对应的 Provider,value 参数等同于传递给 createContext() 的 defaultValue。

动态 context

动态 context 的 value 是变化的,下面是动态的例子:

const themes = {
 light: {
   foreground: '#000000',
   background: '#eeeeee'
 },
 dark: {
   foreground: '#ffffff',
   background: '#222222'
 }
}

const ThemeContext = React.createContext(themes.dark/* 默认值 */)

class ThemedButton extends React.Component {
 render() {
   const props = this.props
   const theme = this.context
   return (
     <button
       {...props}
       style={{backgroundColor: theme.background}}
     />
   )
 }
}
ThemedButton.contextType = ThemeContext

// 一个使用 ThemedButton 的中间组件
function Toolbar(props) {
 return (
   <ThemedButton onClick={props.changeTheme}>
     Change Theme
   </ThemedButton>
 )
}

class App extends React.Component {
 constructor(props) {
   super(props)
   this.state = {
     theme: themes.light
   }

   this.toggleTheme = () => {
     this.setState(state => ({
       theme:
         state.theme === themes.dark
           ? themes.light
           : themes.dark
     }))
   }
 }

 render() {
   // 在 ThemeProvider 内部的 ThemedButton 按钮组件使用 state 中的 theme 值,
   // 而外部的组件使用默认的 theme 值
   return (
     <Page>
       <ThemeContext.Provider value={this.state.theme}>
         <Toolbar changeTheme={this.toggleTheme} />
       </ThemeContext.Provider>
       <Section>
         <ThemedButton />
       </Section>
     </Page>
   )
 }
}

在嵌套组件中更新 Context

从一个在组件树中嵌套很深的组件中更新 context 是很常见的。可以通过 context 传递一个函数,使得 consumers 组件更新 context:

// 确保传递给 createContext 的默认值数据结构是调用的组件(consumers)所能匹配的!
const ThemeContext = React.createContext({
 theme: themes.dark,
 toggleTheme: () => {}
})

function ThemeTogglerButton() {
 // Theme Toggler 按钮不仅仅只获取 theme 值,它也从 context 中获取到一个 toggleTheme 函数
 return (
   <ThemeContext.Consumer>
     {({theme, toggleTheme}) => (
       <button
         onClick={toggleTheme}
         style={{backgroundColor: theme.background}}
       >
         Toggle Theme
       </button>
     )}
   </ThemeContext.Consumer>
 )
}

function Content() {
 return (
   <div>
     <ThemeTogglerButton />
   </div>
 )
}

class App extends React.Component {
 constructor(props) {
   super(props)
   this.toggleTheme = () => {
     this.setState(state => ({
       theme:
         state.theme === themes.dark
           ? themes.light
           : themes.dark
     }))
   }

   // State 也包含了更新函数,因此它会被传递进 context provider。
   this.state = {
     theme: themes.light,
     toggleTheme: this.toggleTheme
   }
 }

 render() {
   // 整个 state 都被传递进 provider
   return (
     <ThemeContext.Provider value={this.state}>
       <Content />
     </ThemeContext.Provider>
   )
 }
}

区别于上节的写法,toggleTheme 分别通过 props 和 context 传递。

消费多个 Context

context 可以嵌套多个使用。

// Theme context
const ThemeContext = React.createContext('light')

// 用户登录 context
const UserContext = React.createContext({
  name: 'Guest'
})

class App extends React.Component {
  render() {
    const {signedInUser, theme} = this.props

    // 提供初始 context 值的 App 组件
    return (
      <ThemeContext.Provider value={theme}>
        <UserContext.Provider value={signedInUser}>
          <Layout />
        </UserContext.Provider>
      </ThemeContext.Provider>
    )
  }
}

function Layout() {
  return (
    <div>
      <Sidebar />
      <Content />
    </div>
  )
}

// 一个组件可能会消费多个 context
function Content() {
  return (
    <ThemeContext.Consumer>
      {theme => (
        <UserContext.Consumer>
          {user => (
            <ProfilePage user={user} theme={theme} />
          )}
        </UserContext.Consumer>
      )}
    </ThemeContext.Consumer>
  )
}

意外渲染

当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。 当传递对象给 value 时,provider 的父组件进行重渲染可能会在 consumers 组件中触发意外的渲染。

class App extends React.Component {
 render() {
   // value 属性总是被赋值为新的对象
   return (
     <MyContext.Provider value={{something: 'something'}}>
       <Toolbar />
     </MyContext.Provider>
   )
 }
}

为了防止这种情况,将 value 状态提升到父节点的 state 里:

class App extends React.Component {
 constructor(props) {
   super(props)
   this.state = {
     value: {something: 'something'}
   }
 }

 render() {
   return (
     <Provider value={this.state.value}>
       <Toolbar />
     </Provider>
   )
 }
}

高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。高阶组件是参数为组件,返回值为新组件的函数。

// 此函数接收一个组件...
function withSubscription(WrappedComponent, selectData) {
 // ...并返回另一个组件...
 return class extends React.Component {
   constructor(props) {
     super(props)
     this.handleChange = this.handleChange.bind(this)
     this.state = {
       data: selectData(dataSource, props)
     }
   }

   componentDidMount() {
     // ...负责订阅相关的操作...
     dataSource.addChangeListener(this.handleChange)
   }

   componentWillUnmount() {
     dataSource.removeChangeListener(this.handleChange)
   }

   handleChange() {
     this.setState({
       data: selectData(dataSource, this.props)
     })
   }

   render() {
     // ... 并使用新数据渲染被包装的组件!
     // 请注意,我们可能还会传递其他属性
     return <WrappedComponent data={this.state.data} {...this.props} />
   }
 }
}

不要改变原始组件

不要试图在 HOC 中修改组件原型(或以其他方式改变它)。

function logProps(InputComponent) {
 InputComponent.prototype.componentDidUpdate = function(prevProps) {
   console.log('Current props: ', this.props)
   console.log('Previous props: ', prevProps)
 }
 return InputComponent
}

// 每次调用 logProps 时,增强组件都会有 log 输出。
const EnhancedComponent = logProps(InputComponent)

上面的做法导致传入组件的 componentDidUpdate 生命周期被污染,只能覆盖原来的重新写入新的逻辑,对于函数式组件也无法使用。

将不相关的 props 传递给被包裹的组件

HOC 应该透传与自身无关的 props。

render() {
 // 过滤掉非此 HOC 额外的 props,且不要进行透传
 const { extraProp, ...restProps } = this.props

 // 将 props 注入到被包装的组件中。
 // 通常为 state 的值或者实例方法。
 const injectedProp = someStateOrInstanceMethod

 // 将 props 传递给被包装组件
 return (
   <WrappedComponent
     injectedProp={injectedProp}
     {...restProps}
   />
 )
}

最大化可组合性

为了增加 HOC 的组合性,有必要保证它们的签名一致。 React Redux 的 connect 函数就是最好的例子:

// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(commentListSelector, commentListActions)
// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList)

像 connect 函数返回的单参数 HOC 具有签名 Component => Component。 输出类型与输入类型相同的函数很容易组合在一起。

// 常规写法
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))

// 你可以编写自己的组合工具函数,也可以使用第三方 lodash, ramda 的工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
 // 这些都是单参数的 HOC
 withRouter,
 connect(commentSelector)
 // ...更多的 HOC
)
const EnhancedComponent = enhance(WrappedComponent)

如果使用装饰器,代码可以更加简洁。

包装显示名称以便轻松调试

HOC 创建的容器组件会显示在 React Developer Tools 调试工具中。如果不处理,那么容器组件将显示 Anonymous 。

最好的做法是主动命名,表明是 HOC 组件。

function withSubscription(WrappedComponent) {
 class WithSubscription extends React.Component {/* ... */}
 // With 开头的 HOC 名字(被包装组件名字)
 WithSubscription.displayName = `WithSubscription(${getDisplayName(WrappedComponent)})`
 return WithSubscription
}

function getDisplayName(WrappedComponent) {
 return WrappedComponent.displayName || WrappedComponent.name || 'Component'
}

不要在 render 方法中使用 HOC

每次调用 render 方法都将返回全新的组件,React 的 diff 算法会将上一次与当前组件视为不相等,导致重新渲染整个组件子树。

从功能来说,重新渲染组件导致该组件及子组件的状态重置,这是不应该的。

render() {
 // 每次调用 render 函数都会创建一个新的 EnhancedComponent
 // EnhancedComponent1 !== EnhancedComponent2
 const EnhancedComponent = enhance(MyComponent)
 // 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!
 return <EnhancedComponent />
}

务必复制静态方法

有时在 React 组件上定义静态方法很有用。例如,Relay 容器暴露了一个静态方法 getFragment 以方便组合 GraphQL 片段。

但是,当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。

目前有 3 种主流的解决方法:

  • 手动拷贝
function enhance(WrappedComponent) {
 class Enhance extends React.Component {/*...*/}
 // 必须准确知道应该拷贝哪些方法 :(
 Enhance.staticMethod = WrappedComponent.staticMethod
 return Enhance
}

需要一个个列举出来,必须特别清楚有哪些方法以及需要哪些方法。

  • 使用 hoist-non-react-statics 插件拷贝所有静态方法
import hoistNonReactStatic from 'hoist-non-react-statics'
function enhance(WrappedComponent) {
  class Enhance extends React.Component {/*...*/}
  hoistNonReactStatic(Enhance, WrappedComponent)
  return Enhance
}

比较方便,缺点是引入了额外的插件。

  • 单独使用
MyComponent.someFunction = someFunction
export default MyComponent

// 单独导出该方法
export { someFunction }

// 在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js'

在第三方库中经常见到。

Refs 不会被传递

尽管高阶组件的约定是将所有的 props 传递给被包装组件,但是 refs 是不会被传递的,事实上, ref 并不是一个 prop,和 key 一样,它由 React 专门处理。

如果对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。

这个问题可以通过 React.forwardRef (React 16.3中新增)来解决。在 React.forwardRef 之前,这个问题,我们可以通过给容器组件添加 forwardedRef (prop的名字自行确定,不过不能是 ref 或者是 key)。

使用 React.forwardRef:

function logProps(Component) {
 class LogProps extends React.Component {
   componentDidUpdate(prevProps) {
     console.log('old props:', prevProps)
     console.log('new props:', this.props)
   }

   render() {
     const {forwardedRef, ...rest} = this.props

     // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
     return <Component ref={forwardedRef} {...rest} />
   }
 }

 // 注意 React.forwardRef 回调的第二个参数 “ref”。
 // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
 // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
 return React.forwardRef((props, ref) => {
   return <LogProps {...props} forwardedRef={ref} />
 })
}

class FancyButton extends React.Component {
 focus() {
   // ...
 }
}

const FancyButtonLogged = logProps(FancyButton)

class DemoComponent extends React.Component {
 constructor(props) {
   super(props)
   this.buttonElement = React.createRef()
 }
 // ref 可以引用到真正的 FancyButton 组件,可以使用 this.buttonElement.current.focus() 调用 focus 方法
 render() {
   <FancyButtonLogged ref={this.buttonElement} />
 }
}

16.3之前,没有 React.forwardRef:

function logProps(Component) {
 return class LogProps extends React.Component {
   componentDidUpdate(prevProps) {
     console.log('old props:', prevProps)
     console.log('new props:', this.props)
   }

   render() {
     const {forwardedRef, ...rest} = this.props

     // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
     return <Component ref={forwardedRef} {...rest} />
   }
 }
}

class FancyButton extends React.Component {
 focus() {
   // ...
 }
}

const FancyButtonLogged = logProps(FancyButton)

class DemoComponent extends React.Component {
 constructor(props) {
   super(props)
   this.buttonElement = React.createRef()
 }
 // 通过 forwardedRef prop 来引用到 FancyButton 组件
 render() {
   <FancyButtonLogged forwardedRef={this.buttonElement} />
 }
}

render props

一种在 React 组件之间使用一个值为函数的 prop 共享代码的简单技术。

在父组件有个 render 的 prop,然后 render 是一个回调函数,父组件通过调用 render 方法,把父组件里面的数据(一般是 state )带出来让业务组件使用,然后,这个回调函数返回一个 React 元素,然后渲染在父组件里面。

下面使用 render prop 封装了一个 ToggleModal 组件,用于 Modal 的显示隐藏:

class ToggleModal extends React.Component {
 state = {
   visible: this.props.initialVisible
 }

 toggle = () => {
   this.setState({
     visible: !this.state.visible
   })
 }

 render() {
   return this.props.render(this.state.visible, this.toggle)
 }
}

class DemoModal extends React.Component {
 render() {
   return (
     <ToggleModal
       initialVisible={false}
       render={(visible, toggle) => (
         <Button type="primary" onClick={toggle}>Show Modal</Button>
         <Modal
           title="Demo Modal"
           visible={visible}
           onOk={toggle}
           onCancel={toggle}
         >
           <div>Content...</div>
         </Modal>
       )}
     />
   )
 }
}

render prop 是因为模式才被称为 render prop,不一定要用名为 render 的 prop 来使用这种模式。任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为 render prop。

对于上面的例子使用 children prop:

class ToggleModal extends React.Component {
 state = {
   visible: this.props.initialVisible
 }

 toggle = () => {
   this.setState({
     visible: !this.state.visible
   })
 }

 render() {
   return this.props.children(this.state.visible, this.toggle)
 }
}

class DemoModal extends React.Component {
 // children 不需要显示添加到 Modal 的属性中,直接放在其内部
 render() {
   return (
     <ToggleModal
       initialVisible={false}
     >
       {(visible, toggle) => (
         <Button type="primary" onClick={toggle}>Show Modal</Button>
         <Modal
           title="Demo Modal"
           visible={visible}
           onOk={toggle}
           onCancel={toggle}
         >
           <div>Content...</div>
         </Modal>
       )}
     </ToggleModal>
   )
 }
}

对比高阶组件

能用高阶组件实现的,render props 都可以都可以代替。

高阶组件,本质上是将组件的逻辑提升到了容器组件里面,然后通过 props 传递给当前组件。

render props 是将名为 render 的 prop 作为函数调用,将 state 通过回调参数传递给当前组件。

两者可以结合使用:

// 如果你出于某种原因真的想要 HOC,那么你可以轻松实现
// 使用具有 render prop 的普通组件创建一个
function withToggle(Component) {
 return class extends React.Component {
   render() {
     return (
       <ToggleModal render={(visible, toggle) => (
         <Component {...this.props} visible={visible} toggle={toggle} />
       )}/>
     )
   }
 }
}

render prop 优化

因为 render prop 是一个函数,所以每次组件 render 都会生成新的 render prop 函数,这将导致子组件的不必要渲染。

一般将 prop 声明为实例方法:

class DemoModal extends React.Component {
 renderModal(visible, toggle) {
   return (
     <Button type="primary" onClick={toggle}>Show Modal</Button>
     <Modal
       title="Demo Modal"
       visible={visible}
       onOk={toggle}
       onCancel={toggle}
     >
       <div>Content...</div>
     </Modal>
   )
 }
 render() {
   return (
     <ToggleModal
       initialVisible={false}
       render={this.renderModal}
     />
   )
 }
}
最近更新:: 2025/10/19 23:44
Contributors: qyhever
Next
set-state