企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[TOC] # Hook 简单来说 Hook 拥抱了函数式编程,Fiber 架构从底层优化了 React 的性能。 使用 Hook ```js import React, { useState, useEffect } from 'react'; // 自定义hook function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); // 将 setState 的统一操作抽离为一个个的函数 function handleStatusChange(status) { setIsOnline(status.isOnline); } // useEffect 简化了事件监听器的添加与移除的书写 useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); return isOnline; } // 使用自定义hook function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); } ``` 不使用 Hook ```js class XXX extend Component { state = { isOnline: false } handleStatusChange (xxx) { this.setState({ isOnline: xxx }) } componentDidMount =()=> { ChatAPI.subscribeToFriendStatus(friendID, this.handleStatusChange); } componentWillUnmount = () => { ChatAPI.unsubscribeFromFriendStatus(friendID, this.handleStatusChange); } render () { XXXXXXX } } ``` ## state hook(让函数组件拥有 state) ```js import React, { useState } from 'react'; function Example() { // 声明一个叫 "count" 的 state 变量 const [count, setCount] = useState(0); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } ``` 等价的 Class 示例: ```js class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); } } ``` `const [count, setCount] = useState(0);` 这条语句到底做了哪些事情? 我们声明了一个叫`count`的 state 变量,然后把它设为`0`。React 会在重复渲染时记住它当前的值,并且提供最新的值给我们的函数。我们可以通过调用`setCount`来更新当前的`count`。 下面具体分析下`useState()`方法 1、调用 useState 方法的时候做了什么?它定义一个 “state 变量”。我们的变量叫`count`, 这是一种在函数调用时保存变量的方式 ——`useState`是一种新方法,它与 class 里面的`this.state`提供的功能完全相同。一般来说,在函数退出后变量就就会”消失”,而 state 中的变量会被 React 保留。 2、`useState`需要哪些参数?`useState()`方法里面唯一的参数就是初始 state。不同于 class 的是,我们可以按照需要使用数字或字符串对其进行赋值,而不一定是对象。在示例中,只需使用数字来记录用户点击次数,所以我们传了`0`作为变量的初始 state。(如果我们想要在 state 中存储两个不同的变量,只需调用`useState()`两次即可。) 3、`useState`方法的返回值是什么?返回值为:当前 state 以及更新 state 的函数。这就是我们写`const [count, setCount] = useState()`的原因。这与 class 里面`this.state.count`和`this.setState`类似,唯一区别就是你需要成对的获取它们。 ### 读取与更新 state 读取 state: 当我们想在 class 中显示当前的 count,我们读取`this.state.count`: ~~~ <p>You clicked {this.state.count} times</p> ~~~ 在函数中,我们可以直接用`count`: ~~~ <p>You clicked {count} times</p> ~~~ 更新 state: 在 class 中,我们需要调用`this.setState()`来更新`count`值: ~~~ <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> ~~~ 在函数中,我们已经有了`setCount`和`count`变量,所以我们不需要`this`: ~~~ <button onClick={() => setCount(count + 1)}> Click me </button> ~~~ ## Effect Hook(让函数组件拥有生命周期) 你可以把`useEffect`Hook 看做`componentDidMount`,`componentDidUpdate`和`componentWillUnmount`这三个函数的组合。 <br/> *Effect Hook* 可以让你在函数组件中执行副作用操作,在 React 组件中有两种常见副作用操作:需要清除的和不需要清除的,所以对应的 Effect Hook 也分为无需清除的 effect 和需要清除的 effect。 <br/> 默认情况下,effect 将在每轮渲染结束后执行,但也可以选择让其在只有某些值改变的时候才执行。 ### 无需清除的 effect 有时候,我们只想**在 React 更新 DOM 之后运行一些额外的代码**。比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作。因为我们在执行完这些操作之后,就可以忽略他们了。让我们对比一下使用 class 和 Hook 都是怎么实现这些副作用的。 ```js // 使用 class class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } // 我们基本上都希望在 React 更新 DOM 之后才执行我们的操作 componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; } render() { return ( <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> ); } } ``` 在这个 class 中,我们需要在两个生命周期函数中编写重复的代码。 这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。从概念上说,我们希望它在每次渲染之后执行 —— 但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。 ```js // 使用 hook import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); // 通过使用这个 Hook,你可以告诉 React 组件需要在渲染后执行某些操作。 // React 会保存你传递的函数(我们将它称之为 “effect”),并且在执行 DOM 更新之后调用它。 useEffect(() => { document.title = `You clicked ${count} times`; // 可以直接访问 count state 变量或其他 prop,它们保存在函数作用域中 }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); } ``` ### 需要清除的 effect 之前,我们研究了如何使用不需要清除的副作用,还有一些副作用是需要清除的。例如**订阅外部数据源**。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!现在让我们来比较一下如何用 Class 和 Hook 来实现。(常见的如手动添加事件处理程序,需要在组件销毁之前移除) ```js // 使用 class class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); } // 与 componentDidMount 逻辑相对应的,我们必须这样拆分代码~~~ import React, { useState, useEffect } from 'react'; function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // Specify how to clean up after this effect: return function cleanup() { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; } ``` ### effect 的条件执行 默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。 <br/> 然而,在某些场景下这么做可能会矫枉过正。比如,在上一章节的订阅示例中,我们不需要在每次组件更新时都创建新的订阅,而是仅需要在`source`prop 改变时重新创建。 <br/> 要实现这一点,可以给`useEffect`传递第二个参数,它是 effect 所依赖的值数组。更新后的示例如下: ``` useEffect( () => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source], ); ``` 此时,只有当`props.source`改变后才会重新创建订阅。 如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组(`[]`)作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循输入数组的工作方式。 [关于依赖列表是否为空的注意事项](https://react.docschina.org/docs/hooks-faq.html#is-it-safe-to-omit-functions-from-the-list-of-dependencies) ## 自定义 Hook [https://zh-hans.reactjs.org/docs/hooks-custom.html#gatsby-focus-wrapper](https://zh-hans.reactjs.org/docs/hooks-custom.html#gatsby-focus-wrapper) - 将组件逻辑提取到可重用的函数中(使组件可以共享某一重复的逻辑) 例如,将一个获取鼠标位置的状态逻辑写成自定义 Hook: ``` import React, { useState, useEffect } from 'react' // 自定义 Hook 是一个函数,其名称必须以 "use" 开头(约定) const useMousePosition = () => { const [ positions, setPositions ] = useState({x: 0, y: 0}) useEffect(() => { const updateMouse = (event) => { setPositions({ x: event.clientX, y: event.clientY }) } document.addEventListener('mousemove', updateMouse) return () => { document.removeEventListener('mousemove', updateMouse) } }) return positions } export default useMousePosition ``` 在组件中使用自定义 Hook 也很简单: ``` function App() { const position = useMousePosition() return ( <div className="App"> <header className="App-header"> <h1>{position.x}</h1> </header> </div> ) } ``` 在两个组件中使用相同的 Hook 不会共享 state。自定义 Hook 是一种重用状态逻辑的机制(例如设置为订阅并存储当前值),所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。 # Fiber ## Fiber 架构解决了什么问题 在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象。请看以下例子: ![](https://img.kancloud.cn/9a/f4/9af4c676d8771189740c052dac66c489_550x280.gif =400x) 其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用`setState`更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI。整个过程是一气呵成,不能被打断的。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。 针对这一问题,React 团队从框架层面对 web 页面的运行机制做了优化,得到很好的效果。 ![](https://img.kancloud.cn/70/6c/706ceaa233acb9d0d3b713ccf7948da7_550x280.gif =400x) ## 实现浅析 React 框架内部的运作可以分为 3 层: * Virtual DOM 层,描述页面长什么样。 * Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。 * Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。 这次改动最大的当属 Reconciler 层了,React 团队也给它起了个新的名字,叫`Fiber Reconciler`。这就引入另一个关键词:Fiber。 先看一下`stack-reconciler`下的 React 是怎么工作的。代码中创建(或更新)一些元素, React 会根据这些元素创建(或更新)Virtual DOM,然后 React 根据更新前后 Virtual DOM 的区别,去修改真正的 DOM。注意,**在 stack reconciler 下,DOM 的更新是同步的,也就是说,在 Virtual DOM 的比对过程中,发现一个 Instance 有更新,会立即执行 DOM 操作**。 ![](https://img.kancloud.cn/9d/d9/9dd907076cd23c005f57ae1a1aad358e_1133x639.png =400x) 而`Fiber Reconciler`下,操作是可以分成很多小部分,并且可以被中断的,所以同步操作 DOM 可能会导致 fiber-tree 与实际 DOM 的不同步。对于每个节点来说,其不光存储了对应元素的基本信息,还要保存一些用于任务调度的信息。因此,fiber 仅仅是一个对象,表征 reconciliation 阶段所能拆分的最小工作单元,和上图中的 react instance一一对应。通过`stateNode`属性管理 Instance 自身的特性。通过`child`和`sibling`表征当前工作单元的下一个工作单元,`return`表示处理完成后返回结果所要合并的目标,通常指向父节点。整个结构是一个链表树。每个工作单元(fiber)执行完成后,都会查看是否还继续拥有主线程时间片,如果有继续下一个,如果没有则先处理其他高优先级事务,等主线程空闲下来继续执行。 Fiber 就是一种数据结构,它可以用一个纯 JS 对象来表示: ```js const fiber = { stateNode: {}, // 管理 Instance 自身的特性 child: {}, // 表征当前工作单元的下一工作单元 sibling: {}, // 表征当前工作单元的下一工作单元 return: {}, // 表示处理完成后返回结果所要合并的目标,通常指向父节点 } ``` ### 举个例子 当前页面包含一个列表,通过该列表渲染出一个 button 和一组 Item,Item 中包含一个 div,其中的内容为数字。通过点击 button,可以使列表中的所有数字进行平方。另外有一个按钮,点击可以调节字体大小。 ![](https://img.kancloud.cn/b6/7d/b67d7f9ffd717781b31aea6a99ed89b7_678x673.png =200x) 页面渲染完成后,就会初始化生成一个`fiber-tree`,这一过程与初始化 Virtual DOM Tree 类似。 ![](https://img.kancloud.cn/70/7a/707ad710a5ea7ee428f4738ad9df1821_451x817.png =250x) 同时,React 还会维护一个`workInProgressTree`,`workInProgressTree`用于计算更新,完成 reconciliation 过程。 ![](https://img.kancloud.cn/b3/1f/b31f5c759e308e6b1706c1ab80855f47_653x844.png =300x) 用户点击平方按钮后,利用各个元素平方后的 list 调用 setState,React 会把当前的更新送入 list 组件对应的`update queue`中。但是 React 并不会立即执行对比并修改 DOM 的操作。而是交给 scheduler 去处理。 scheduler 会根据当前主线程的使用情况去处理这次 update。为了实现这种特性,使用了`requestIdelCallback`API。对于不支持这个 API 的浏览器,react 会加上 pollyfill。 总的来讲,通常,客户端线程执行任务时会以帧的形式划分,大部分设备控制在 30-60 帧是不会影响用户体验;在两个执行帧之间,主线程通常会有一小段空闲时间,`requestIdleCallback`可以在这个 **空闲期(Idle Period)** 调用 **空闲期回调(Idle Callback)**,执行一些任务 ![](https://img.kancloud.cn/32/5d/325d0263851b3180b058effdee674b37_737x139.png) 1、低优先级任务由`requestIdleCallback`处理; 2、高优先级任务,如动画相关的由`requestAnimationFrame`处理; 3、`requestIdleCallback`可以在多个空闲期调用空闲期回调,执行任务; 4、`requestIdleCallback`方法提供 deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞 UI 渲染而导致掉帧; ![](https://img.kancloud.cn/6d/4e/6d4e202604ed0584204e6c5a48ef3ceb_1048x786.png =450x) 整个过程,简单来说,先通过`requestIdleCallback`获得可用的时间片,然后检查节点的`update queue`看是否需要更新,每处理完一个节点都会检查时间片是否用完,如果没用完,根据其保存的下一个工作单元的信息处理下一个节点。详细过程见第四个参考链接。 # 参考资料 [React Hook 探究](https://www.jianshu.com/p/d6e2bd342476) [官方文档](https://react.docschina.org/docs/hooks-reference.html) [React Fiber 原理](https://segmentfault.com/a/1190000018250127?utm_source=tag-newest) [https://juejin.im/post/5ab7b3a2f265da2378403e57#heading-2](https://juejin.im/post/5ab7b3a2f265da2378403e57#heading-2) [https://usehooks.com/](https://usehooks.com/)