React系列(六)---|React系列(六)--- 从HOC再到HOOKS

系列文章 React系列(一)-- 2013起源 OSCON - React Architecture by vjeux
React系列(二)-- React基本语法实现思路
React系列(三)-- Jsx, 合成事件与Refs
React系列(四)--- virtualdom diff算法实现分析
React系列(五)--- 从Mixin到HOC
React系列(六)--- 从HOC再到HOOKS
在线调试 React在线运行网址: https://codesandbox.io/s/blue...
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
动机 Hook 解决了我们五年来编写和维护成千上万的组件时遇到的各种各样看起来不相关的问题。
在组件之间复用状态逻辑很难
如果你使用过 React 一段时间,你也许会熟悉一些解决此类问题的方案,
比如 render props高阶组件。但是这类方案需要重新组织你的组件结构,这可能会很麻烦,使你的代码难以理解。
如果你在 React DevTools 中观察过 React 应用,你会发现由 providers,consumers,高阶组件,render props 等其他抽象层组成的组件会形成“嵌套地狱”。
你可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使你在无需修改组件结构的情况下复用状态逻辑。
复杂组件变得难以理解
我们经常维护一些组件,组件起初很简单,但是逐渐会被状态逻辑和副作用充斥。每个生命周期常常包含一些不相关的逻辑。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
难以理解的 class
你必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。
class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况。
Hook 使你在非 class 的情况下可以使用更多的 React 特性。
HOOKS规范 在顶层调用HOOKS
不要在循环,条件,或者内嵌函数中调用.这都是为了保证你的代码在每次组件render的时候会按照相同的顺序执行HOOKS,而这也是能够让React在多个useState和useEffect执行中正确保存数据的原因
只在React函数调用HOOKS

  • React函数组件调用
  • 从自定义HOOKS中调用
可以确保你源码中组件的所有有状态逻辑都是清晰可见的.
State Hook
const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
setState 函数用于更新 state。它接收一个新的 state 值并将组件的一次重新渲染加入队列。
惰性初始 state
initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略。如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用:
const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; });

跳过 state 更新
调用 State Hook 的更新函数并传入当前的 state 时,React 将跳过子组件的渲染及 effect 的执行。(React 使用 Object.is 比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
示例
import React, { useState } from 'react'; function Example() { // 声明一个叫 “count” 的 state 变量。 const [count, setCount] = useState(0); return (You clicked {count} times
); }export default Example;

等价于下面Class写法
import React from 'react'; class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0, }; }render() { return (You clicked {this.state.count} times
); } }export default Example;

从上面可以看出useState实际上就是在state里声明一个变量并且初始化了一个值而且提供一个可以改变对应state的函数.因为在纯函数中没有this.state.count的这种用法,所以直接使用count替代,上面的count就是声明的变量,setCount就是改变变量的方法.
需要注意的一点是useStatethis.state有点不同,它只有在组件第一次render才会创建状态,之后每次都只会返回当前的值.
如果改变需要根据之前的数据变化,可以通过函数接收旧数据,例如
setCount(prevCount => prevCount + 1)

如果是想声明多个state的时候,就需要使用多次useState
function ExampleWithManyStates() { // Declare multiple state variables! const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); }

或者通过组合对象一次合并多个数据
Effect Hook
useEffect(didUpdate);

执行有副作用的函数,你可以把 useEffect Hooks 视作 componentDidMountcomponentDidUpdatecomponentWillUnmount 的结合,useEffect 会在浏览器绘制后延迟执行,但会保证在任何新的渲染前执行,React 将在组件更新前刷新上一轮渲染的 effect。
在函数组件主体内(这里指在 React 渲染阶段)改变 DOM、添加订阅、设置定时器、记录日志以及执行其他包含副作用的操作都是不被允许的,因为这可能会产生莫名其妙的 bug 并破坏 UI 的一致性。
React 组件中的 side effects 大致可以分为两种
不需要清理 有时我们想要在 React 更新过 DOM 之后执行一些额外的操作。比如网络请求、手动更新 DOM 、以及打印日志都是常见的不需要清理的 effects
import React from 'react'; class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0, }; }componentDidMount() { console.log(`componentDidMount: You clicked ${this.state.count} times`); }componentDidUpdate() { console.log(`componentDidUpdate: You clicked ${this.state.count} times`); }render() { return (You clicked {this.state.count} times
); } }export default Example;

componentDidMount: You clicked 0 times
// 点击按钮
componentDidUpdate: You clicked 1 times
但是如果我们换成HOOKS的写法
import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { // 初始化默认输出: You clicked 0 times // 后续点击才会输出 console.log(`You clicked ${count} times`); }); return (You clicked {count} times
); }export default Example;

You clicked 0 times
// 点击按钮
You clicked 1 times
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 比较常见的就类似挂载的时候监听事件或者开启定时器,卸载的时候就移除.
import React from 'react'; class Example extends React.Component { constructor(props) { super(props); }componentDidMount() { document.addEventListener('click', this.clickFunc, false); }componentWillUnmount() { document.removeEventListener('click', this.clickFunc); }clickFunc(e) { // doSomethings }render() { return ; } }export default Example;

换成HOOKS写法类似,只是会返回新的函数
import React, { useEffect } from 'react'; function Example() { useEffect(() => { document.addEventListener('click', clickFunc, false); return () => { document.removeEventListener('click', clickFunc); }; }); function clickFunc(e) { // doSomethings }return ; }export default Example;

我们为什么在 effect 中返回一个函数
这是一种可选的清理机制。每个 effect 都可以返回一个用来在晚些时候清理它的函数。这让我们让添加和移除订阅的逻辑彼此靠近。它们是同一个 effect 的一部分!
React 究竟在什么时候清理 effect?
React 在每次组件 unmount 的时候执行清理。然而,正如我们之前了解的那样,effect 会在每次 render 时运行,而不是仅仅运行一次。这也就是为什么 React 也会在执行下一个 effect 之前,上一个 effect 就已被清除。
我们可以修改一下代码看看effect的运行机制
import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { console.log('addEventListener: ' + count); document.addEventListener('click', clickFunc, false); return () => { console.log('removeEventListener: ' + count); document.removeEventListener('click', clickFunc); }; }); function clickFunc(e) { setCount(count + 1); }return ; }export default Example;

addEventListener: 0
// 点击按钮
removeEventListener: 0
addEventListener: 1
// 点击按钮
removeEventListener: 1
addEventListener: 2
可以看到上面代码在每次更新都是重新监听,想要避免这种情况不在useEffect里return函数即可
进阶使用 有时候我们可能有多套逻辑写在不同的生命周期里,如果换成HOOKS写法的话我们可以按功能划分使用多个,React将会按照指定的顺序应用每个effect。
import React, { useState, useEffect } from "react"; function Example() { const [count, setCount] = useState(0); useEffect(() => { console.log(`You clicked ${count} times`); }); useEffect(() => { console.log('addEventListener'); document.addEventListener("click", clickFunc, false); return () => { console.log('removeEventListener'); document.removeEventListener("click", clickFunc); }; }); function clickFunc(e) { setCount(count + 1); }return (You clicked {count} times
); }export default Example;

You clicked 0 times
addEventListener
// 点击按钮
removeEventListener
You clicked 1 times
addEventListener
// 点击按钮
removeEventListener
You clicked 2 times
addEventListener
为什么Effects会在每次更新后执行
如果你们以前使用class的话可能会有疑惑,为什么不是在卸载阶段执行一次.从官网解释代码看
componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); }

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

但是如果我们换成HOOKS的写法就不会有这种bug
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; });

这是因为HOOKS会在应用下一个effects之前清除前一个effects,此行为默认情况下确保一致性,并防止由于缺少更新逻辑而在类组件中常见的错误
通过跳过effects提升性能
就在上面我们知道每次render都会触发effects机制可能会有性能方面的问题,在class的写法里我们可以通过componentDidUpdate做选择是否更新
componentDidUpdate(prevProps, prevState) { if (prevState.count !== this.state.count) { document.title = `You clicked ${this.state.count} times`; } }

而在useEffect里我们可以通过传递一组数据给它作为第二参数,如果在下次执行的时候该数据没有发生变化的话React会跳过当次应用
import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { console.log('addEventListener: ' + count); document.addEventListener('click', clickFunc, false); return () => { console.log('removeEventListener: ' + count); document.removeEventListener('click', clickFunc); }; }, [count]); function clickFunc(e) {}return ; }export default Example;

所以上面提到的bug案例可以通过这个方式做解决
useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }, [props.friend.id]); // Only re-subscribe if props.friend.id changes

注意 如果你想使用这种优化方式,请确保数组中包含了所有外部作用域中会发生变化且在 effect 中使用的变量,否则你的代码会一直引用上一次render的旧数据.
如果你想要effects只在挂载和卸载时各清理一次的话,可以传递一个空数组作为第二参数.相当于告诉React你的effects不依赖于任何的props或者state,所以没必要重复执行.
effect 的执行时机
componentDidMountcomponentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
然而,并非所有 effect 都可以被延迟执行。例如,在浏览器执行下一次绘制前,用户可见的 DOM 变更就必须同步执行,这样用户才不会感觉到视觉上的不一致。(概念上类似于被动监听事件和主动监听事件的区别。)React 为此提供了一个额外的 useLayoutEffect Hook 来处理这类 effect。它和 useEffect 的结构相同,区别只是调用时机不同。
useLayoutEffect 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
提示
如果你使用服务端渲染,请记住,无论 useLayoutEffect 还是 useEffect 都无法在 Javascript 代码加载完成之前执行。这就是为什么在服务端渲染组件中引入 useLayoutEffect 代码时会触发 React 告警。解决这个问题,需要将代码逻辑移至 useEffect 中(如果首次渲染不需要这段逻辑的情况下),或是将该组件延迟到客户端渲染完成后再显示(如果直到 useLayoutEffect 执行之前 HTML 都显示错乱的情况下)。
若要从服务端渲染的 HTML 中排除依赖布局 effect 的组件,可以通过使用 showChild && 进行条件渲染,并使用 useEffect(() => { setShowChild(true); }, []) 延迟展示组件。这样,在客户端渲染完成之前,UI 就不会像之前那样显示错乱了。
useMemo
const memoizedValue = https://www.it610.com/article/useMemo( () => { doSomething(a, b); }, [a, b], );

返回一个 memoized 值。
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。(可以理解成Vue的computed API)
记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo
如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。
你可以把 useMemo 作为性能优化的手段,但不要把它当成语义上的保证。将来,React 可能会选择“遗忘”以前的一些 memoized 值,并在下次渲染时重新计算它们,比如为离屏组件释放内存。先编写在没有 useMemo 的情况下也可以执行的代码 —— 之后再在你的代码中添加 useMemo,以达到优化性能的目的。
注意
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
useCallback
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );

返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。
注意
依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。未来编译器会更加智能,届时自动创建数组将成为可能。
useReducer
const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(类似Redux的工作方式)
在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
import React, { useReducer } from "react"; const initialState = {count: 0}; function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } }function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( <> Count: {state.count} ); }export default Counter;

【React系列(六)---|React系列(六)--- 从HOC再到HOOKS】注意
React 会确保 dispatch 函数的标识是稳定的,并且不会在组件重新渲染时改变。这就是为什么可以安全地从 useEffectuseCallback 的依赖列表中省略 dispatch
指定初始 state 将初始 state 作为第二个参数传入 useReducer 是最简单的方法:
const [state, dispatch] = useReducer(reducer, { count: initialCount });

注意
React 不使用 state = initialState 这一由 Redux 推广开来的参数约定。有时候初始值依赖于 props,因此需要在调用 Hook 时指定。如果你特别喜欢上述的参数约定,可以通过调用 useReducer(reducer, undefined, reducer) 来模拟 Redux 的行为,但我们不鼓励你这么做。
惰性初始化 从语法上你们会看到还有一个init的入参,是用来做惰性初始化,将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)
这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利
import React, { useReducer } from "react"; function init(initialCount) { return {count: initialCount}; }function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); default: throw new Error(); } }function Counter({initialCount = 0}) { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} ); }export default Counter;

跳过 dispatch 如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行。(React 使用 Object.is 比较算法 来比较 state。)
需要注意的是,React 可能仍需要在跳过渲染前再次渲染该组件。不过由于 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必担心。如果你在渲染期间执行了高开销的计算,则可以使用 useMemo 来进行优化。
useContext
const value = https://www.it610.com/article/useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 value prop 决定。
当组件上层最近的 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。
调用了 useContext 的组件总会在 context 值变化时重新渲染。如果重渲染组件的开销较大,你可以通过使用 memoization来优化。
示例 举个例子,在上面的useReducer代码中,我们通过一个context做中转:
import React, { useContext, useReducer } from "react"; const initialState = { count: 0 }; // context对象 const stateContext = React.createContext(); function reducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; default: throw new Error(); } }// 数据提供中心 function ContextProvider(props) { const [state, dispatch] = useReducer(reducer, initialState); return ({props.children}); }// 数据接收组件 function Counter() { const { state, dispatch } = useContext(stateContext); return ( <> Count: {state.count} ); }// 形成关系 const App = () => { return ( ); }; export default App;

useRef
const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。
import React, { useRef } from "react"; function Example() { const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素 inputEl.current.focus(); }; return ( ); }export default Example;

你应该熟悉 ref 这一种访问 DOM 的主要方式。如果你将 ref 对象以形式传入组件,则无论该节点如何改变,React 都会将 ref 对象的 .current 属性设置为相应的 DOM 节点。
然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值。
这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。
请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef一起使用:
import React, { useRef, useImperativeHandle, forwardRef } from "react"; const FancyButton = forwardRef((props, ref) => { const inputRef = useRef(); useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); }, })); return ; }); function Example() { const inputRef = useRef(); return ( ); }export default Example;

在上述的示例中,React 会将 元素的 ref 作为第二个参数传递给 React.forwardRef 函数中的渲染函数。该渲染函数会将 ref 传递给 元素。
因此,当 React 附加了 ref 属性之后,ref.current 将直接指向 ; }
总而言之,从维护的角度来这样看更加方便(不用不断转发回调),同时也避免了回调的问题。像这样向下传递 dispatch 是处理深度更新的推荐模式。
React 是如何把对 Hook 的调用和组件联系起来的?
React 保持对当先渲染中的组件的追踪。多亏了 Hook 规范,我们得知 Hook 只会在 React 组件中被调用(或自定义 Hook —— 同样只会在 React 组件中被调用)。
每个组件内部都有一个「记忆单元格」列表。它们只不过是我们用来存储一些数据的 JavaScript 对象。当你用 useState() 调用一个 Hook 的时候,它会读取当前的单元格(或在首次渲染时将其初始化),然后把指针移动到下一个。这就是多个 useState() 调用会得到各自独立的本地 state 的原因。

    推荐阅读