从Mixin到HOC再到HOOKS(四)

原创 Lin_Grady 教程 框架/库 53阅读 22 天前 举报

React系列

React简单模拟语法(一)
Jsx, 合成事件与Refs(二)
virtualdom diff算法实现分析(三)
从Mixin到HOC再到HOOKS(四)
createElement, ReactElement与Component部分源码解析(五)

Mixins(已废弃)

这是React初期提供的一种组合方案,通过引入一个公用组件,然后可以应用公用组件的一些生命周期操作或者定义方法,达到抽离公用代码提供不同模块使用的目的.

曾经的官方文档demo如下

但是Mixins只能应用在createClass的创建方式,在后来的class写法中已经被废弃了.原因在于:

  1. mixin引入了隐式依赖关系
  2. 不同mixins之间可能会有先后顺序甚至代码冲突覆盖的问题
  3. mixin代码会导致滚雪球式的复杂性
    详细介绍mixin危害性文章可直接查阅Mixins Considered Harmful

高阶组件(Higher-order component)

HOC是一种React的进阶使用方法,大概原理就是接收一个组件然后返回一个新的继承组件,继承方式分两种

属性代理(Props Proxy)

最基本的实现方式

从代码可以看出属性代理方式其实就是接受一个 WrappedComponent 组件作为参数传入,并返回一个继承了 React.Component 组件的类,且在该类的 render() 方法中返回被传入的 WrappedComponent 组件

抽离state && 操作props

demo代码可以参考这里
有种常见的情况是用来做双向绑定

demo代码可以参考这里

获取被继承refs实例

因为这是一个被HOC包装过的新组件,所以想要在HOC里面获取新组件的ref需要用些特殊方式,但是不管哪种,都需要在组件挂载之后才能获取到.并且不能在无状态组件(函数类型组件)上使用 ref 属性,因为无状态组件没有实例。

通过父元素传递方法获取

demo代码可以参考这里

通过高阶组件当中间层

相比较上一方式,需要在高阶组件提供设置赋值函数,并且需要一个props属性做标记

demo代码可以参考这里

forwardRef

React.forwardRef 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。这种技术并不常见,但在以下两种场景中特别有用:

  • 转发 refs 到 DOM 组件
  • 在高阶组件中转发 refs

以下是对上述示例发生情况的逐步解释:

  1. 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={ref}>
  3. React 传递 ref 给 fowardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

劫持渲染

最简单的例子莫过于loading组件了

当然也能用于布局上嵌套在其他元素输出
demo代码可以参考这里

反向继承(Inheritance Inversion)

最简单的demo代码

在这里WrappedComponent成了被继承的那一方,从而可以在高阶组件中获取到传递组件的所有相关实例

获取继承组件实例

demo代码可以参考这里

修改props和劫持渲染

再讲解demo之前先科普React的一个方法

以 element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。新的子元素将取代现有的子元素,而来自原始元素的 key 和 ref 将被保留。
React.cloneElement() 几乎等同于:

但是,这也保留了组件的 ref。这意味着当通过 ref 获取子节点时,你将不会意外地从你祖先节点上窃取它。相同的 ref 将添加到克隆后的新元素中。

相比属性继承来说,反向继承修改props会比较复杂一点

demo代码可以参考这里

为什么需要用到cloneElement方法?

因为render函数内实际上是调用React.creatElement产生的React元素,尽管我们可以拿到这个方法但是无法修改它.可以用getOwnPropertyDescriptors查看它的配置项

所以用cloneElement创建新的元素替代

相比较属性继承来说,后者只能条件性选择是否渲染WrappedComponent,但是前者可以更加细粒度劫持渲染元素,可以获取到 state,props,组件生命周期(component lifecycle)钩子,以及渲染方法(render),但是依旧不能保证WrappedComponent里的子组件是否渲染,也无法劫持.

注意

  • 静态属性失效
    因为高阶组件返回的已经不是原组件了,所以原组件的静态属性方法已经无法获取,除非你主动将它们拷贝到返回组件中
  • 渲染机制
    因为高阶组件返回的是新组件,里面的唯一标志也会变化,所以不建议在render里面也调用高阶组件,这会导致其每次都重新卸载再渲染,即使它可能长得一样.
    所以建议高阶组件都是无副作用的纯函数,即相同输入永远都是相同输出,不允许任何有可变因素.
  • 嵌套过深
    在原组件中如果包裹层级过多会产生类似回调地狱的烦恼,难以调试,可阅读性糟糕
  • 遵守规则
    如果没有规范情况下,也可能造成代码冲突覆盖的局面

HOOKS

Hooks是React v16.7.0-alpha中加入的新特性。它可以让你在class以外使用state和其他React特性。

Hooks是可以让你与React状态以及函数式组件的生命周期特性“挂钩”的函数。钩子是为了让你抛弃类使用React的,所以它不能在类中运行,但是可以用在纯函数中,这就解决了一直以来可能因为需要用到生命周期或者react状态的时候,你不得不将原本的纯函数代码整个替换成Class写法的烦恼.

Hooks也分两种

State Hook

能够让你在不使用Class的情况下使用state和其他的React功能

useState

等价于下面Class写法

demo代码可以参考这里
从上面可以看出useState实际上就是在state里声明一个变量并且初始化了一个值而且提供一个可以改变对应state的函数.因为在纯函数中没有this.state.count的这种用法,所以直接使用count替代
上面的count就是声明的变量,setCount就是改变变量的方法.
需要注意的一点是useState和this.state有点不同,它只有在组件第一次render才会创建状态,之后每次都只会返回当前的值.

赋值初始值的时候如果需要经过某些逻辑处理才能得到的话,可以通过函数传递,例如

如果改变需要根据之前的数据变化,可以通过函数接收旧数据,例如

如果是想声明多个state的时候,就需要使用多次useState

或者通过组合对象一次合并多个数据

Effect Hook

执行有副作用的函数,你可以把 useEffect Hooks 视作 componentDidMountcomponentDidUpdatecomponentWillUnmount 的结合,useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行。React 将在组件更新前刷新上一轮渲染的 effect。React 组件中的 side effects 大致可以分为两种

不需要清理

有时我们想要在 React 更新过 DOM 之后执行一些额外的操作。比如网络请求、手动更新 DOM 、以及打印日志都是常见的不需要清理的 effects

如上所示,如果放在render的话在挂载前也会触发,但是为了避免这个问题我们不得不在两个生命周期写同样的代码.但是如果我们换成HOOKS的写法

demo代码可以参考这里

useEffect 做了什么? 通过这个 Hook,React 知道你想要这个组件在每次 render 之后做些事情。React 会记录下你传给 useEffect 的这个方法,然后在进行了 DOM 更新之后调用这个方法。但我们同样也可以进行数据获取或是调用其它必要的 API。

为什么 useEffect 在组件内部调用?useEffect 放在一个组件内部,可以让我们在 effect 中,即可获得对 count state(或其它 props)的访问,而不是使用一个特殊的 API 去获取它。

useEffect 是不是在每次 render 之后都会调用? 默认情况下,它会在第一次 render 之后的每次 update 后运行。React 保证每次运行 effects 之前 DOM 已经更新了。

使用上还有哪些区别? 不像 componentDidMount 或者 componentDidUpdateuseEffect 中使用的 effect 并不会阻滞浏览器渲染页面。我们也提供了一个单独的 useLayoutEffect来达成这同步调用的效果。它的 API 和 useEffect 是相同的。

需要清理的 Effect

比较常见的就类似挂载的时候监听事件或者开启定时器,卸载的时候就移除.

换成HOOKS写法类似,只是会返回新的函数

demo代码可以参考这里

我们为什么在 effect 中返回一个函数 这是一种可选的清理机制。每个 effect 都可以返回一个用来在晚些时候清理它的函数。这让我们让添加和移除订阅的逻辑彼此靠近。它们是同一个 effect 的一部分!

React 究竟在什么时候清理 effect? React 在每次组件 unmount 的时候执行清理。然而,正如我们之前了解的那样,effect 会在每次 render 时运行,而不是仅仅运行一次。这也就是为什么 React 会在执行下一个 effect 之前,上一个 effect 就已被清除。

我们可以修改一下代码看看effect的运行机制

demo代码可以参考这里
可以看到上面代码在每次更新都是重新监听,想要避免这种情况可以往下继续看.

进阶使用

有时候我们可能有多套逻辑写在不同的生命周期里,如果换成HOOKS写法的话我们可以按功能划分使用多个,React将会按照指定的顺序应用每个effect。

demo代码可以参考这里

为什么Effects会在每次更新后执行

如果你们以前使用class的话可能会有疑惑,为什么不是在卸载阶段执行一次.从官网解释代码看

它在挂载阶段监听,移除阶段移除监听,每次触发就根据this.props.friend.id做出对应处理.但是这里有个隐藏的bug就是当移除阶段的时候获取的this.props.friend.id可能是旧的数据,引起的问题就是卸载时候会使用错误的id而导致内存泄漏或崩溃,所以在class的时候一般都会在componentDidUpdate 做处理

但是如果我们换成HOOKS的写法就不会有这种bug

这是因为HOOKS会在应用下一个effects之前清除前一个effects,此行为默认情况下确保一致性,并防止由于缺少更新逻辑而在类组件中常见的错误

通过跳过effects提升性能

就在上面我们知道每次render都会触发effects机制可能会有性能方面的问题,在class的写法里我们可以通过componentDidUpdate做选择是否更新

而在useEffect里我们可以通过传递一组数据给它作为第二参数,如果在下次执行的时候该数据没有发生变化的话React会跳过当次应用

所以上面提到的bug案例可以通过这个方式做解决

注意

如果你想使用这种优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会一直引用上一次render的旧数据.

如果你想要effects只在挂载和卸载时各清理一次的话,可以传递一个空数组作为第二参数.相当于告诉React你的effects不依赖于任何的props或者state,所以没必要重复执行.

useCallback

返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useReducer

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

demo代码可以参考这里
从语法上你们会看到还有一个init的入参,是用来做惰性初始化,将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg),这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利

demo代码可以参考这里

useMemo

返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo。
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。

demo代码可以参考这里
本质上,useRef 就像是可以在其 .current 属性中保存一个可变值的“盒子”。
你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以 <div ref={myRef} /> 形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。
这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

HOOKS规范

在顶层调用HOOKS

不要在循环,条件,或者内嵌函数中调用.这都是为了保证你的代码在每次组件render的时候会按照相同的顺序执行HOOKS,而这也是能够让React在多个useState和useEffect执行中正确保存数据的原因

只在React函数调用HOOKS

  • React函数组件调用
  • 从自定义HOOKS中调用

可以确保你源码中组件的所有有状态逻辑都是清晰可见的.

自定义HOOKS

我们可以将相关逻辑抽取出来

我必须以“use”开头为自定义钩子命名吗? 这项公约非常重要。如果没有它,我们就不能自动检查钩子是否违反了规则,因为我们无法判断某个函数是否包含对钩子的调用。

使用相同钩子的两个组件是否共享状态? 不。自定义钩子是一种重用有状态逻辑的机制(例如设置订阅并记住当前值),但是每次使用自定义钩子时,其中的所有状态和效果都是完全隔离的。

自定义钩子如何获得隔离状态? 对钩子的每个调用都处于隔离状态。从React的角度来看,我们的组件只调用useStateuseEffect

问题

Hook 会替代 render props 和高阶组件吗?

通常,render props 和高阶组件只渲染一个子节点。我们认为让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderItem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。

生命周期方法要如何对应到 Hook?

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。
  • getDerivedStateFromProps:改为在渲染时安排一次更新。
  • shouldComponentUpdate:详见 React.memo.
  • render:这是函数组件体本身。
  • componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表达所有这些的组合。
  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。

我可以只在更新时运行 effect 吗?

这是个比较罕见的使用场景。如果你需要的话,你可以 使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染,然后在你的 effect 中检查这个标识。

如何获取上一轮的 props 或 state?

目前,你可以通过ref来手动实现:

有类似 forceUpdate 的东西吗?

如果前后两次的值相同,useState 和 useReducer Hook 都会放弃更新。原地修改 state 并调用 setState 不会引起重新渲染。
通常,你不应该在 React 中修改本地 state。然而,作为一条出路,你可以用一个增长的计数器来在 state 没变的时候依然强制一次重新渲染:

我该如何测量 DOM 节点?

要想测量一个 DOM 节点的位置或是尺寸,你可以使用 callback ref。每当 ref 被附加到另一个节点,React 就会调用 callback。

demo代码可以参考这里
使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果。

注意到我们传递了 [] 作为 useCallback 的依赖列表。这确保了 ref callback 不会在再次渲染时改变,因此 React 不会在非必要的时候调用它。

我该如何实现 shouldComponentUpdate?

你可以用 React.memo 包裹一个组件来对它的 props 进行浅比较:

React.memo 等效于 PureComponent,但它只比较 props。(你也可以通过第二个参数指定一个自定义的比较函数来比较新旧 props。如果函数返回 true,就会跳过更新。)

React.memo 不比较 state,因为没有单一的 state 对象可供比较。但你也可以让子节点变为纯组件,或者 用useMemo优化每一个具体的子节点。

如何惰性创建昂贵的对象?

第一个常见的使用场景是当创建初始 state 很昂贵时,为避免重新创建被忽略的初始 state,我们可以传一个函数给 useState,React 只会在首次渲染时调用这个函数

你或许也会偶尔想要避免重新创建 useRef() 的初始值。useRef 不会像 useState 那样接受一个特殊的函数重载。相反,你可以编写你自己的函数来创建并将其设为惰性的:

Hook 会因为在渲染时创建函数而变慢吗?

不会。在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
除此之外,可以认为 Hook 的设计在某些方面更加高效:

  • Hook 避免了 class 需要的额外开支,像是创建类实例和在构造函数中绑定事件处理器的成本。
  • 符合语言习惯的代码在使用 Hook 时不需要很深的组件树嵌套。这个现象在使用高阶组件、render props、和 context 的代码库中非常普遍。组件树小了,React 的工作量也随之减少。

传统上认为,在 React 中使用内联函数对性能的影响,与每次渲染都传递新的回调会如何破坏子组件的 shouldComponentUpdate 优化有关。Hook 从三个方面解决了这个问题。

  • useCallback Hook 允许你在重新渲染之间保持对相同的回调引用以使得 shouldComponentUpdate 继续工作:
  • useMemo Hook 使控制具体子节点何时更新变得更容易,减少了对纯组件的需要。
  • 最后,useReducer Hook 减少了对深层传递回调的需要,就如下面解释的那样。

如何避免向下传递回调?

在大型的组件树中,我们推荐的替代方案是通过 contextuseReducer 往下传一个 dispatch 函数:

TodosApp 内部组件树里的任何子节点都可以使用 dispatch 函数来向上传递 actions

总而言之,从维护的角度来这样看更加方便(不用不断转发回调),同时也避免了回调的问题。像这样向下传递 dispatch 是处理深度更新的推荐模式。

React 是如何把对 Hook 的调用和组件联系起来的?

React 保持对当先渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

评论 ( 0 )
最新评论
暂无评论

赶紧努力消灭 0 回复