React Hooks使用及其源码

690 阅读37分钟

react-hooks 基础

什么是react-hooks?

react-hooks是react16.8以后,react新增的钩子API,目的是增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。笔者认为,react-hooks思想和初衷,也是把组件,颗粒化,单元化,形成独立的渲染环境,减少渲染次数,优化性能。

useState✅

useCallback✅

useContext✅

useEffect✅

useLayoutEffect ✅

useMemo ✅

useReducer✅

useRef✅

为什么要使用hooks

我们为什么要使用react-hooks呢,首先和传统的class声明的有状态有这显著的优点就是

1 react-hooks可以让我们的代码的逻辑性更强,可以抽离公共的方法,公共组件。

2 react-hooks思想更趋近于函数式编程。用函数声明方式代替class声明方式,虽说class也是es6构造函数语法糖,但是react-hooks写起来更有函数即组件,无疑也提高代码的开发效率(无需像class声明组件那样写声明周期,写生命周期render函数等)

3 react-hooks可能把庞大的class组件,化整为零成很多小组件,useMemo等方法让组件或者变量制定一个适合自己的独立的渲染空间,一定程度上可以提高性能,减少渲染次数。(这里特别注意的是⚠️,如果乱用hooks,不但不会提升性能,反而会影响性能,带来各种各样的想不到的问题)。

如何使用 hooks

useState

数据存储,派发更新

useState出现,使得react无状态组件能够像有状态组件一样,可以拥有自己state

useState的参数可以是一个具体的值,也可以是一个函数用于判断复杂的逻辑,函数返回作为初始值

usestate 返回一个数组,数组第一项用于读取此时的state值 ,第二项为派发数据更新组件渲染的函数,函数的参数即是需要更新的值。

useState 作为能够触发组件重新渲染的hooks,我们在使用useState的时候要特别注意的是,useState派发更新函数的执行,就会让整个function组件从头到尾执行一次,所以需要配合useMemousecallback等api配合使用,这也是为什么滥用hooks会带来负作用的原因之一了。一下代码为usestate基本应用

const DemoState = (props) => {
   /* number为此时state读取值 ,setNumber为派发更新的函数 */
   let [number, setNumber] = useState(0) /* 0为初始值 */
   return (<div>
       <span>{ number }</span>
       <button onClick={ ()=> {
         setNumber(number+1)
         console.log(number) /* 这里的number是不能够即使改变的  */
       } } ></button>
   </div>)
}

上边简单的例子说明了useState ,但是当我们在调用更新函数之后,state的值是不能即时改变的,只有当下一次上下文执行的时候,state值才随之改变。

const a =1 
const DemoState = (props) => {
   /*  useState 第一个参数如果是函数 则处理复杂的逻辑 ,返回值为初始值 */
   let [number, setNumber] = useState(()=>{
      // number
      return a===1 ? 1 : 2
   }) /* 1为初始值 */
   return (<div>
       <span>{ number }</span>
       <button onClick={ ()=>setNumber(number+1) } ></button>
   </div>)
}

useEffect

组件更新副作用钩子

如果你想在function组件中,当组件完成挂载,dom渲染完成,做一些操纵dom,请求数据,那么useEffect是一个不二选择

如果我们需要在组件初次渲染的时候请求数据,那么useEffect可以充当class组件中的 componentDidMount

但是特别注意的是:如果不给useEffect执行加入限定条件(即没有第二个参数),函数组件每一次更新都会触发effect ,那么也就说明每一次state更新,或是props的更新都会触发useEffect执行,此时的effect又充当了componentDidUpdatecomponentwillreceiveprops

所以说合理的用于useEffect就要给effect加入限定执行的条件,也就是useEffect的第二个参数

这里说是限定条件,也可以说是上一次useEffect更新收集的某些记录数据变化的记忆,在新的一轮更新,useEffect会拿出之前的记忆值和当前值做对比,如果发生了变化就执行新的一轮useEffect的副作用函数

useEffect第二个参数是一个数组,用来收集多个限制条件 。

/* 模拟数据交互 */
function getUserInfo(a){
    return new Promise((resolve)=>{
        setTimeout(()=>{ 
           resolve({
               name:a,
               age:16,
           }) 
        },500)
    })
}

const Demo = ({ a }) => {
    const [ userMessage , setUserMessage ]= useState({})
    const div= useRef()
    const [number, setNumber] = useState(0)
    
    /* 模拟事件监听处理函数 */
    const handleResize =()=>{}
    
    /* useEffect使用 ,这里如果不加限制 ,会使函数重复执行,陷入死循环*/
    useEffect(()=>{
        
        /* 请求数据 */
       getUserInfo(a).then(res=>{
           setUserMessage(res)
       })
        
       /* 操作dom  */
       console.log(div.current) /* div */
        
       /* 事件监听等 */
        window.addEventListener('resize', handleResize)
    /* 只有当props->a和state->number改变的时候 ,useEffect副作用函数重新执行 ,如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount */
    },[ a ,number ])
    
    return (<div ref={div} >
        <span>{ userMessage.name }</span>
        <span>{ userMessage.age }</span>
        <div onClick={ ()=> setNumber(1) } >{ number }</div>
    </div>)
}

如果我们需要在组件销毁的阶段,做一些取消dom监听,清除定时器等操作,那么我们可以在useEffect函数第一个参数,结尾返回一个函数,用于清除这些副作用。相当与componentWillUnmount

const Demo = ({ a }) => {
    
    /* 模拟事件监听处理函数 */
    const handleResize =()=>{}
    
    useEffect(()=>{
        
       /* 定时器 延时器等 */
       const timer = setInterval(()=>console.log(666),1000)
       
       /* 事件监听 */
       window.addEventListener('resize', handleResize)
        
       /* 此函数用于清除副作用 */
       return function(){
           clearInterval(timer) 
           window.removeEventListener('resize', handleResize)
       }
    },[ a ])
    return (<div  >
    </div>)
}

异步 async effect ?

提醒大家的是 useEffect是不能直接用 async await 语法糖的

/* 错误用法 ,effect不支持直接 async await 装饰的 */
useEffect(async ()=>{
    /* 请求数据 */
    const res = await fetch('api/data')
},[])

如果我们想要用 async,可以定义一个async函数,在effect中引用从而变相的使用async

const fetchMyAPI =async()=> {
  let response = await fetch('api/data')
}
 
useEffect(() => {
  fetchMyAPI();
}, []);

useLayoutEffect

渲染更新之前的 useEffect

useEffect 执行顺序:组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执行useEffect回调 。

useLayoutEffect 执行顺序:组件更新挂载完成 -> 执行useLayoutEffect回调-> 浏览器dom 绘制完成

所以说useLayoutEffect 代码可能会阻塞浏览器的绘制 如果我们在useEffect 重新请求数据,渲染视图过程中,肯定会造成画面闪动的效果,而如果用useLayoutEffect ,回调函数的代码就会阻塞浏览器绘制,所以可定会引起画面卡顿等效果,那么具体要用 useLayoutEffect 还是 useEffect ,要看实际项目的情况,大部分的情况 useEffect 都可以满足的。

useRef

获取元素 ,缓存数据。

和传统的class组件ref一样,react-hooks 也提供获取元素方法 useRef,它有一个参数可以作为缓存数据的初始值,返回值可以被dom元素ref标记,可以获取被标记的元素节点.

const DemoUseRef = ()=>{
    const dom= useRef(null)
    const handerSubmit = ()=>{
        /*  <div >表单组件</div>  dom 节点 */
        console.log(dom.current)
    }
    return <div>
        {/* ref 标记当前dom节点 */}
        <div ref={dom} >表单组件</div>
        <button onClick={()=>handerSubmit()} >提交</button> 
    </div>
}

高阶用法 缓存数据

当然useRef还有一个很重要的作用就是缓存数据,我们知道usestate ,useReducer 是可以保存当前的数据源的

但是如果它们更新数据源的函数执行必定会带来整个组件从新执行到渲染,如果在函数组件内部声明变量,则下一次更新也会重置,如果我们想要悄悄的保存数据,而又不想触发函数的更新,那么useRef是一个很棒的选择。

const currenRef = useRef(InitialData)

useRef第一个参数可以用来初始化保存数据,这些数据可以在current属性上获取到 ,当然我们也可以通过对current赋值新的数据源。

下面我们通过react-redux源码来看看useRef的巧妙运用react-redux react-hooks发布后,用react-hooks重新了其中的Provide,connectAdvanced)核心模块,可以见得 react-hooks在限制数据更新,高阶组件上有这一定的优势,其源码大量运用useMemo来做数据判定

/* 这里用到的useRef没有一个是绑定在dom元素上的,都是做数据缓存用的 */

const lastChildProps = useRef() // react-redux 用userRef 来缓存 merge之后的 props

const lastWrapperProps = useRef(wrapperProps) //  lastWrapperProps 用 useRef 来存放组件真正的 props信息

const renderIsScheduled = useRef(false) // 储存props是否处于正在更新状态

这是react-redux中用useRef 对数据做的缓存,那么怎么做更新的呢 ,我们接下来看

//获取包装的props 
function captureWrapperProps(
  lastWrapperProps,
  lastChildProps,
  renderIsScheduled,
  wrapperProps,
  actualChildProps,
  childPropsFromStoreUpdate,
  notifyNestedSubs
) {
   //我们要捕获包装props和子props,以便稍后进行比较
  lastWrapperProps.current = wrapperProps  //子props 
  lastChildProps.current = actualChildProps //经过  merge props 之后形成的 prop
  renderIsScheduled.current = false

}

通过上面我们可以看到 ,react-redux 用重新赋值的方法,改变缓存的数据源,避免不必要的数据更新, 如果选用useState储存数据,必然促使组件重新渲染 所以采用了useRef解决了这个问题

至于react-redux源码怎么实现的,我们这里可以参考一篇文章react-redux源码解析

useContext

自由获取context

我们可以使用useContext ,来获取父级组件传递过来的context值,这个当前值就是最近的父级组件 Provider 设置的value值,useContext参数一般是由 createContext 方式引入 ,也可以父级上下文context传递 ( 参数为context )。useContext 可以代替 context.Consumer 来获取Provider中保存的value值:

父组件:

export default ()=>{
    return <div>
        <Context.Provider value={{ name:'alien' , age:18 }} >
            <DemoContext />
            <DemoContext1 />
        </Context.Provider>
    </div>
}

子组件:

/* 用useContext方式 */
const DemoContext = ()=> {
    const value:any = useContext(Context)
    /* my name is alien */
return <div> my name is { value.name }</div>
}

/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
    return <Context.Consumer>
         {/*  my name is alien  */}
        { (value)=> <div> my name is { value.name }</div> }
    </Context.Consumer>
}

useReducer

无状态组件中的redux

useReducerreact-hooks提供的能够在无状态组件中运行的类似redux的功能api,至于它到底能不能代替redux,react-redux ,我个人的看法是不能的

redux 能够复杂的逻辑中展现优势 ,而且 redux的中间件模式思想也是非常优秀了,我们可以通过中间件的方式来增强dispatch,像 redux-thunk`` redux-sage redux-action redux-promise都是比较不错的中间件,可以把同步reducer编程异步的reducer

useReducer 接受的第一个参数是一个函数,我们可以认为它就是一个reducer ,reducer的参数就是常规reducer里面的stateaction,返回改变后的state,第二个参数为state的初始值

useReducer返回一个数组,数组的第一项就是更新之后state的值 ,第二个参数是派发更新的dispatch函数

dispatch 的触发会触发组件的更新,这里能够促使组件重新的渲染的一个是useState派发更新函数,另一个就 useReducer中的dispatch

const DemoUseReducer = ()=>{
    /* number为更新后的state值,  dispatchNumbner 为当前的派发函数 */
   const [ number , dispatchNumbner ] = useReducer((state,action)=>{
       const { payload , name  } = action
       /* return的值为新的state */
       switch(name){
           case 'add':
               return state + 1
           case 'sub':
               return state - 1 
           case 'reset':
             return payload       
       }
       return state
   },0)
   return <div>
      当前值:{ number }
      { /* 派发更新 */ }
      <button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button>
      <button onClick={()=>dispatchNumbner({ name:'sub' })} >减少</button>
      <button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >赋值</button>
      { /* 把dispatch 和 state 传递给子组件  */ }
      <MyChildren  dispatch={ dispatchNumbner } State={{ number }} />
   </div>
}

当然实际业务逻辑可能更复杂的,需要我们在reducer里面做更复杂的逻辑操作。

useMemo

小而香性能优化

useMemo我认为是React设计最为精妙的hooks之一,优点就是能形成独立的渲染空间,能够使组件,变量按照约定好规则更新。渲染条件依赖于第二个参数deps

我们知道无状态组件的更新是从头到尾的更新,如果你想要从新渲染一部分视图,而不是整个组件,那么用useMemo是最佳方案,避免了不需要的更新,和不必要的上下文的执行

在介绍useMemo之前,我们先来说一说memo, 我们知道class声明的组件可以用componentShouldUpdate来限制更新次数,那么memo就是无状态组件的ShouldUpdate , 而我们今天要讲的useMemo就是更为细小的ShouldUpdate单元

先来看看memo ,memo的作用结合了pureComponent纯组件和 componentShouldUpdate功能,会对传进来的props进行一次对比,然后根据第二个函数返回值来进一步判断哪些props需要更新。

/* memo包裹的组件,就给该组件加了限制更新的条件,是否更新取决于memo第二个参数返回的boolean值, */
const DemoMemo = connect(state => ({ goodList: state.goodList }))(
    memo(
        ({ goodList, dispatch }) => {
            return (
                <Select>
                    {goodList.map((item, index) => (
                        <Option key={index + 'asd' + item.itemId} value={item.itemId}>
                            {item.itemName}
                        </Option>
                    ))}
                </Select>
            );
            /* 判断之前的goodList 和新的goodList 是否相等,如果相等,则不更新此组件 这样就可以制定属于自己的渲染约定 ,让组件只有满足预定的下才重新渲染 */
        },
        (pre, next) => is(pre.goodList, next.goodList)
    )
);

useMemo的应用理念和memo差不多,都是判定是否满足当前的限定条件来决定是否执行useMemocallback函数,而useMemo的第二个参数是一个deps数组,数组里的参数变化决定了useMemo是否更新回调函数,useMemo返回值就是经过判定更新的结果。

它可以应用在元素上,应用在组件上,也可以应用在上下文当中。如果有一个循环的list元素,那么useMemo会是一个不二选择,接下来我们一起探寻一下useMemo的优点

useMemo可以减少不必要的循环,减少不必要的渲染

useMemo(
    () => (
      <Modal
        width={'70%'}
        visible={listshow}
        footer={[
          <Button key="back">取消</Button>,
          <Button key="submit" type="primary"> 确定 </Button>
        ]}
      >
        {/* 减少了PatentTable组件的渲染 */}
        <PatentTable
          getList={getList}
          selectList={selectList}
          cacheSelectList={cacheSelectList}
          setCacheSelectList={setCacheSelectList}
        />
      </Modal>
    ),
    [listshow, cacheSelectList]
  );

useMemo让函数在某个依赖项改变的时候才运行,这可以避免很多不必要的开销(这里要注意⚠️⚠️⚠️的是如果被useMemo包裹起来的上下文,形成一个独立的闭包,会缓存之前的state值,如果没有加相关的更新条件,是获取不到更新之后的state的值的,如下边👇⬇️)

const DemoUseMemo=()=>{
    const [ number ,setNumber ] = useState(0)
    const newLog = useMemo(()=>{
        const log =()=>{
            /* 点击span之后 打印出来的number 不是实时更新的number值 */
            console.log(number)
        }
        return log
      /* [] 没有 number */  
    },[])
    return <div>
        <div onClick={()=>newLog()} >打印</div>
        <span onClick={ ()=> setNumber( number + 1 )  } >增加</span>
    </div>
}

useMemo很不错,react-reduxreact-hooks重写后运用了大量的useMemo情景,我为大家分析一处。

react-redux通过判断 redux store的改变来获取与之对应的state

 const previousState = useMemo(() => store.getState(), [store])

感兴趣的同学可以去看源码。

讲到这里,如果我们应用useMemo根据依赖项合理的颗粒化我们的组件,能起到很棒的优化组件的作用。

Redux

以简单的实现加减法举例子,store目录结构如下:

image-20210902180955084

常量

定义加减两个常量并导出

// src/store/utils.js
export const Count = {
    ADD: 'COUNT_ADD',
    REDUCE: 'COUNT_REDUCE',
}

reducer

创建reducer.js文件,根据触发的action修改state

// src/store/reducer/count.js

import { Count } from "store/utils";

// reducer
const defaultState = { num: 0 };
const reducer = (state = defaultState, action) => {
    switch (action.type) {
        case Count.REDUCE:
            return { ...state, num: state.num - 1 };
        case Count.ADD:
            return { ...state, num: state.num + 1 };
        default:
            return state;
    }
};
export default reducer

action

创建action,这里简单封装了createActions函数,用于创建action。然后创建了加、减两个action

// src/store/action/count.js
import { Count } from "store/utils";
/**
*@desc 创建action的函数
*@param type [string] action类型
*@param payload [any] dispatch传过来的参数
*/
const createActions = (type, payload) => ({ type, payload })

/**
*@desc 创建增减的action
*@param payload [any] dispatch传过来的参数
*/
export const reduceAction = (payload) => createActions(Count.REDUCE, payload)
export const addAction = (payload) => createActions(Count.ADD, payload)

注册store

使用combineReducers合并reducer

// src/store/index.js

import { createStore, combineReducers } from 'redux';
import count from "./reducer/count";

export default createStore(combineReducers({
    count
}));

将store注入react

// src/index.tsx

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./store";
import { Provider } from "react-redux";
// 当store改变时打印改变的值
const listener = () => {
    console.log('store', store.getState());
}
store.subscribe(listener) // 可以监听store变化
ReactDOM.render(
    <React.StrictMode>
    <Provider store={store}>
    <App />
    </Provider>
</React.StrictMode>,
document.getElementById("root")
);

useDispatch

使用这个 hook 能得到 redux storedispatch 方法引用,通常用于“手动” dispatch action

// src/count/index.jsx

import React from 'react';
import { useDispatch, useSelector } from 'react-redux'
import { addAction, reduceAction } from "store/action/count"; // action
function Count(props) {
    const dispatch = useDispatch() // 在函数组件使用dispatch
    const num = useSelector(state => state.count.num); // 在函数组件拿到state
    return (
        <div>
            <button onClick={() => dispatch(reduceAction())}>-</button>
            <button>{num}</button>
            <button onClick={() => dispatch(addAction())}>+</button>

        </div>
    );
}

export default Count;

总结

react-hooks的诞生,也不是说它能够完全代替class声明的组件,对于业务比较复杂的组件,class组件还是首选,只不过我们可以把class组件内部拆解成funciton组件,根据业务需求,哪些负责逻辑交互,哪些需要动态渲染,然后配合usememo等api,让性能提升起来。react-hooks使用也有一些限制条件,比如说不能放在流程控制语句中,执行上下文也有一定的要求。总体来说,react-hooks还是很不错的,值得大家去学习和探索。

React-hooks 源码解析

前言

Hook 是 React 16.8 的新增特性。

它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

目的是增加代码的可复用性,逻辑性,弥补无状态组件没有生命周期,没有数据管理状态state的缺陷。

🤔️🤔️🤔️我们带着疑问开始今天的探讨(能回答上几个,自己可以尝试一下,掌握程度):

  1. 在无状态组件每一次函数上下文执行的时候,react用什么方式记录了hooks的状态?
  2. 为什么不能条件语句中,声明hooks? hooks声明为什么在组件的最顶部?
  3. function函数组件中的useState,和 class类组件 setState有什么区别?
  4. react 是怎么捕获到hooks的执行上下文,是在函数组件内部的?
  5. useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值?
  6. useMemo是怎么对值做缓存的?如何应用它优化性能?
  7. 为什么两次传入useState的值相同,函数组件不更新?

image-20211216101214705

Fiber 树

  • Fiber 是一种数据结构,React目前的做法是使用链表,每个VirtualDOM节点内部表示一个Fiber
let virtualDOM = (
    <div key="A">
        <div key="B1">B1</div>
        <div key="B2">B2</div>
    </div>
)

image-20211223191037293

  • 当React渲染完成后会产生一个 current fiber 树
  • RootFiber是根节点,根fiber。
  • workInProgress fiber 树在render阶段,会基于current树创建新的workInProgress fiber树,更新完成后会把workInProgress fiber树赋值给 current fiber树,下次更新会复制current树给workInProgress,更新完成后再赋值给current,依次循环。
  • workInProgress fiber树的每个节点会有一个altermate指针指向current树对应的fiber节点

Fiber树架构

  • Fiber是一个执行单元,每次执行完一个执行单元,React就会检查现在还剩多少时间,如果没有时间就将控制权让出去

image-20211224102332215

循环链表

循环链表是另一种形式的链式存储结构,它的特点是表中最后一个节点的指针指向头节点,整个链表形成一个环。

我们一起来看一下以下代码,以及示例图:

function dispatchAction(queue, action) {
    const update = { action, next: null } // 创建一个update对象
    const pending = queue.pending
    if (pending === null) {
        update.next = update // 让自己和自己构建成一个循环链表 
    } else {
        update.next = pending.next // 将上一个update的next放在当前update的next里面
        pending.next = update // 把最新的update放在上个update的后面
    }
    queue.pending = update
}
// 队列
let queue = { pending: null }
dispatchAction(queue, 'action1')
dispatchAction(queue, 'action2')
dispatchAction(queue, 'action3')
console.log('queue', queue);

image-20211227151600784

我们将其每一项单独打印出来看一下

const pendingQueue = queue.pending
if (pendingQueue !== null) {
    let first = pendingQueue.next
    let update = first
    do {
        console.log(update)
        update = update.next
    } while (update !== first)
}

image-20211227154524744

action1.next-->action2.next-->action3.next-->action1.next

可以得出三个结论:

  1. pending永远指向最后一个更新
  2. pengding.next永远指向第一个更新
  3. 更新顺序永远不变

function VS class 组件

在解释react-hooks原理的之前,我们要加深理解一下, 函数组件和类组件到底有什么区别,废话不多说,我们先看两个代码片段。

import { Component } from "react";
export default class Index extends Component {
    constructor(props) {
        super(props)
        this.state = {
            number: 0
        }
    }
    handerClick = () => {
        for (let i = 0; i < 5; i++) {
            setTimeout(() => {
                this.setState({ number: this.state.number + 1 })
                console.log(this.state.number)
            }, 1000)
        }
    }

    render() {
        return <div>
            <button onClick={this.handerClick} >num++</button>
        </div>
    }
}

打印结果?

再来看看函数组件中:

import { useState } from "react";
export default function Index() {
    const [num, setNumber] = useState(0)
    const handerClick = () => {
        for (let i = 0; i < 5; i++) {
            setTimeout(() => {
                setNumber(num + 1)
                console.log(num)
            }, 1000)
        }
    }
    return <button onClick={handerClick} >{num}</button>
}

打印结果?

------------公布答案-------------

在第一个🌰子打印结果: 1 2 3 4 5

在第二个🌰子打印结果: 0 0 0 0 0

这个问题实际很蒙人,我们来一起分析一下,第一个类组件中,由于执行setState没有在react正常的函数执行上下文上执行,而是setTimeout中执行的,批量更新条件被破坏。所以可以直接获取到变化后的state

但是在无状态组件中,似乎没有生效。原因很简单,在class状态中,通过一个实例化的class,去维护组件中的各种状态;

但是在function组件中,没有一个状态去保存这些信息,每一次函数上下文执行,所有变量,常量都重新声明,执行完毕,再被垃圾机制回收。

所以如上,无论setTimeout执行多少次,都是在当前函数上下文执行,此时num = 0不会变,之后setNumber执行,函数组件重新执行之后,num才变化。

所以, 对于class组件,我们只需要实例化一次,实例中保存了组件的state等状态。对于每一次更新只需要调用render方法就可以。

但是在function组件中,每一次更新都是一次新的函数执行,为了保存一些状态,执行一些副作用钩子,react-hooks应运而生,去帮助记录组件的状态,处理一些额外的副作用。

揭开hooks的面纱

当我们引入hooks时候发生了什么?

我们从引入 hooks开始,以useState为例子,当我们从项目中这么写:

import { useState } from 'react'

于是乎我们去找useState,看看它到底是哪路神仙?

useState

// packages/react/src/ReactHooks.js:79

export function useState(initialState){
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

useState() 的执行等于执行了 dispatcher.useState(initialState)resolveDispatcher创建了dispatcher,我们看一下resolveDispatcher做了些什么?

resolveDispatcher

// packages/react/src/ReactHooks.js:23
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current
  return dispatcher
}

ReactCurrentDispatcher

// packages/react/src/ReactCurrentDispatcher.js
const ReactCurrentDispatcher = {
  current: null,
};

我们看到ReactCurrentDispatcher.current初始化的时候为null,然后就没任何下文了。

我们暂且只能把**ReactCurrentDispatcher**记下来。看看ReactCurrentDispatcher什么时候用到的 ?

无状态组件的函数执行

想要彻底弄明白hooks,就要从其根源开始,上述我们在引入hooks的时候,最后以一个ReactCurrentDispatcher草草收尾,线索全部断了,所以接下来我们只能从函数组件执行开始。

renderWithHooks 执行函数

对于function组件是什么时候执行的呢?

function组件初始化:

mountIndeterminateComponent函数中首次执行renderWithHooks函数

// packages/react-reconciler/src/ReactFiberBeginWork.js:1647

renderWithHooks(
    null,                // current Fiber
    workInProgress,      // workInProgress Fiber
    Component,           // 函数组件本身
    props,               // props
    context,             // 上下文
    renderLanes,
);

对于初始化是没有current树的,之后完成一次组件更新后,会把当前workInProgress树赋值给current树。

function组件更新:

// packages/react-reconciler/src/ReactFiberBeginWork.js:382
renderWithHooks(
    current,
    workInProgress,
    render,
    nextProps,
    ref,
    renderLanes,
);

我们从上边可以看出来,renderWithHooks函数作用是调用function组件函数的主要函数。我们重点看看renderWithHooks做了些什么?

// packages/react-reconciler/src/ReactFiberHooks.new.js:366
export function renderWithHooks(
 current,
 workInProgress,
 Component,
 props,
 secondArg,
 nextRenderLanes,
) {
    renderLanes = nextRenderLanes;
    //将workInProgress的引用赋值给 current Fiber
    currentlyRenderingFiber = workInProgress;

    // 置空即将调和渲染的workInProgress树的memoizedState和updateQueue
    workInProgress.memoizedState = null;
    workInProgress.updateQueue = null;

    // 根据当前函数组件是否是第一次渲染,赋予ReactCurrentDispatcher.current不同的hooks
    ReactCurrentDispatcher.current =
        current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
    : HooksDispatcherOnUpdate;

    // 调用Component执行函数组件
    let children = Component(props, secondArg);
    
	// 没有在函数组件中调用的hooks
    ReactCurrentDispatcher.current = ContextOnlyDispatcher;

    //置空一些变量
    currentHook = null  
    workInProgressHook = null; 

    return children;
}

所有的函数组件执行,都是在这里方法中,首先我们应该明白几个概念,这对于后续我们理解useState是很有帮助的。

current fiber树: 当完成一次渲染之后,会产生一个current树,current会替换成真实的Dom树。

workInProgress fiber树: 即将调和渲染的 fiber 树。在一次新的组件更新过程中,会从current复制一份作为workInProgress,更新完毕后,将当前的workInProgress树赋值给current树。

workInProgress.memoizedState: 在class组件中,memoizedState存放state信息,在function组件中,memoizedState在一次调和渲染过程中,以链表的形式存放hooks信息。

currentHook : 可以理解为 current树上的指向当前调度的 hooks节点。

workInProgressHook : 可以理解为 workInProgress树上指向当前调度的 hooks节点。

renderWithHooks函数主要作用:

  1. 首先置空即将调和渲染的workInProgress树的memoizedStateupdateQueue,为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks信息挂载到这两个属性上,然后在组件commit阶段(dom操作前中后),将workInProgress树替换成current树,替换真实的DOM元素节点。并在current树保存hooks信息。

  2. 然后根据当前函数组件是否是第一次渲染,赋予ReactCurrentDispatcher.current不同的hooks,终于和上面讲到的ReactCurrentDispatcher联系到一起。对于第一次渲染组件,那么用的是HooksDispatcherOnMount hooks对象。 对于渲染后,需要更新的函数组件,则是HooksDispatcherOnUpdate对象,那么两个不同就是通过current树上是否memoizedState(hook信息)来判断的。如果current不存在,证明是第一次渲染函数组件。

  3. 接下来,调用Component(props, secondArg);执行我们的函数组件,我们的函数组件在这里真正的被执行了,然后,我们写的hooks被依次执行,把hooks信息依次保存到workInProgress树上。 至于它是怎么保存的,我们马上会讲到。

  4. 接下来,也很重要,将ContextOnlyDispatcher赋值给 ReactCurrentDispatcher.current,由于js是单线程的,也就是说我们没有在函数组件中调用的hooks,都是ContextOnlyDispatcher对象上hooks

我们看看ContextOnlyDispatcherhooks,到底是什么。

// packages/react-reconciler/src/ReactFiberHooks.new.js:2373
const ContextOnlyDispatcher = {
    useState:throwInvalidHookError
}
function throwInvalidHookError() {
  throw new Error(
    'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
    ' one of the following reasons:\n' +
    '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
    '2. You might be breaking the Rules of Hooks\n' +
    '3. You might have more than one copy of React in the same app\n' +
    'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
  );
}

原来如此,react-hooks就是通过这种函数组件执行赋值不同的hooks对象方式,判断在hooks执行是否在函数组件内部,捕获并抛出异常的。

最后,重新置空一些变量比如currentHook,workInProgressHook等。

不同的hooks对象

上述讲到在函数第一次渲染组件和更新组件分别调用不同的hooks对象,我们现在就来看看HooksDispatcherOnMountHooksDispatcherOnUpdate

第一次渲染(我这里只展示了常用的hooks):

const HooksDispatcherOnMount = {
  useCallback: mountCallback,
  useEffect: mountEffect,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
};

更新组件:

const HooksDispatcherOnUpdate = {
  useCallback: updateCallback,
  useEffect: updateEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState
};

看来对于第一次渲染组件,和更新组件,react-hooks采用了两套Api

我们用流程图来描述整个过程:

image-20211220152044595

hooks初始化

我们写的hooks会变成什么样子?

我们将重点围绕四个重点hooks展开,分别是负责组件更新的useState,负责执行副作用useEffect ,负责保存数据的useRef,负责缓存优化的useMemo, 至于useCallback,useReducer,useLayoutEffect原理和那四个重点hooks比较相近,就不一一解释了。

我们先写一个组件,并且用到上述四个主要hooks

请记住如下代码片段,后面讲解将以如下代码段展开

import React , { useEffect , useState , useRef , useMemo  } from 'react'
'
export default function Index(){
    const [ number , setNumber ] = useState(0)
    const DivDemo = useMemo(() => <div> hello , i am useMemo </div>,[])
    const curRef  = useRef(null)
    useEffect(()=>{
        console.log(curRef.current)
    },[])
    return <div ref={ curRef } >
        hello,world { number } 
        { DivDemo }
        <button onClick={() => setNumber(number+1) } >number++</button>
    </div>
}

接下来我们一起研究一下我们上述写的四个hooks最终会变成什么?

mountWorkInProgressHook

在组件初始化的时候,每一次hooks执行,如useState(),useRef(),都会调用mountWorkInProgressHook,那到底做了些什么,让我们一起来分析一下:

// packages/react-reconciler/src/ReactFiberHooks.new.js:628
function mountWorkInProgressHook() {
    const hook: Hook = {
        // useState中 保存 state信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和deps | useRef中保存的是ref 对象
        memoizedState: null, 
        // usestate和useReducer中,一次更新中 ,产生的最新state值。
        baseState: null, 
        // usestate和useReducer中 保存最新的更新队列。
        baseQueue: null,
        // 保存待更新队列 pendingQueue ,更新函数 dispatch 等信息。
        queue: null,
        // 指向下一个 hooks对象。
        next: null,
    };
    // 例子中的第一个hooks-> useState(0) 走的就是这里。
    if (workInProgressHook === null) { 
        // currentlyRenderingFiber即为workInProgress current树
        currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
    } else {
        // 将自己的next指向自己,形成一个循环链表
        workInProgressHook = workInProgressHook.next = hook;
    }
    return workInProgressHook;
}

mountWorkInProgressHook这个函数做的事情很简单,首先每次执行一个hooks函数,都产生一个hook对象,里面保存了当前hook信息,然后将每个hooks以链表形式串联起来,并赋值给workInProgressmemoizedState(注意:这里虽然赋值给的currentlyRenderingFiber,但是前面我们提到,他是workInProgress的引用)。也就回答了我们的第1个提问,函数组件用memoizedState存放hooks链表。

至于hook对象中都保留了那些信息?我这里先分别介绍一下 :

memoizedStateuseState中 保存 state 信息 | useEffect 中 保存着 effect 对象 | useMemo 中 保存的是缓存的值和 depsuseRef 中保存的是 ref 对象。

baseQueue : usestateuseReducer中 保存最新的更新队列。

baseStateusestateuseReducer一次更新中 ,产生的最新state值。

queue : 保存待更新队列 pendingQueue ,更新函数 dispatch 等信息。

next: 指向下一个 hooks对象。

那么当我们函数组件执行之后,四个hooksworkInProgress将是如图的关系。

image-20211220153739311

知道每个hooks关系之后,我们应该理解了,为什么不能条件语句中,声明hooks

我们用一幅图表示如果在条件语句中声明会出现什么情况发生。

如果我们将上述demo其中的一个 useRef 放入条件语句中,

 let curRef  = null
 if(isFisrt){
  curRef = useRef(null)
 }

image-20211220153804223

为什么hooks不能放在条件语句中:因为一旦在条件语句中声明hooks,在下一次函数组件更新,hooks链表结构,将会被破坏,current树的memoizedState缓存hooks信息,和当前workInProgress不一致,如果涉及到读取state等操作,就会发生异常。

上述介绍了 hooks通过什么来证明唯一性的,答案 ,通过hooks链表顺序。和为什么不能在条件语句中,声明hooks,接下来我们按照四个方向,分别介绍初始化的时候发生了什么?

初始化useState -> mountState

// packages/react-reconciler/src/ReactFiberHooks.new.js:1497
function mountState(initialState) {
  const hook = mountWorkInProgressHook();
    // 如果 useState 第一个参数为函数,执行函数得到state
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue  = {
    pending: null, // 待更新的
    dispatch: null, // 负责更新函数
    lastRenderedReducer: basicStateReducer, //用于得到最新的 state ,
    lastRenderedState: initialState // 最后一次得到的 state
  };
    hook.queue=queue

  const dispatch = (queue.dispatch = dispatchAction.bind(
    // 负责更新的函数
    null,
    currentlyRenderingFiber,
    queue
  ));
  return [hook.memoizedState, dispatch];
}

mountState到底做了些什么:

首先会得到初始化的state,将它赋值给mountWorkInProgressHook产生的hook对象的 memoizedStatebaseState属性

然后创建一个queue对象,里面保存了负责更新的信息。

这里先说一下,在无状态组件中,useStateuseReducer触发函数更新的方法都是dispatchActionuseState可以看成一个简化版的useReducer,至于dispatchAction怎么更新state,更新组件的,我们接着往下研究dispatchAction

在研究之前 我们先要弄明白**dispatchAction是什么?**

function dispatchAction( fiber, queue, action )
const [ number , setNumber ] = useState(0)

dispatchAction 就是 setNumber , dispatchAction 第一个参数和第二个参数,已经被bind给改成currentlyRenderingFiberqueue,我们传入的参数是第三个参数action

dispatchAction 无状态组件更新机制

作为更新的主要函数,我们一下来研究一下,我把 dispatchAction 精简,精简,再精简,

// packages/react-reconciler/src/ReactFiberHooks.new.js:2173
function dispatchAction(fiber, queue, action) {
  /* 创建一个update,记录了此次更新的信息 */
  const update= {
      lane,
      action,
      hasEagerState: false,
      eagerState: null,
      next: (null: any),
  }

  const pending = queue.pending;
    // 证明第一次更新
  if (pending === null) {  
    update.next = update;
  } else { // 不是第一次更新
    update.next = pending.next;
    pending.next = update;
  }
  
  queue.pending = update;
  const alternate = fiber.alternate;
  
  /* 判断当前是否在渲染阶段 */
  if ( fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else { 
      // 当前函数组件对应fiber没有处于调和渲染阶段 ,那么获取最新state , 执行更新
    if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        try {
           // 上一次的state
          const currentState = queue.lastRenderedState;
            // 最新的state
          const eagerState = lastRenderedReducer(currentState, action); 
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
            // 两次值对比,没有改变则不更新
          if (objectIs(eagerState, currentState)) { 
            return
          }
        } 
      }
    }
    scheduleUpdateOnFiber(fiber, lane, eventTime)
  }
}

无论是类组件调用setState,还是函数组件的dispatchAction ,都会产生一个 update对象,里面记录了此次更新的信息,然后将此update放入待更新的pending队列中

dispatchAction第二步就是判断当前函数组件的fiber对象是否处于渲染阶段,如果处于渲染阶段,那么不需要我们再更新当前函数组件,只需要这之前的更新一下当前updateeventTime即可。

如果当前fiber没有处于更新阶段。那么通过调用lastRenderedReducer获取最新的state,和上一次的currentState,进行浅比较,如果相等,那么就退出,这就证实了为什么useState,两次值相等的时候,组件不渲染的原因了。

如果两次state不相等,那么调用scheduleUpdateOnFiber调度渲染当前fiber(scheduleUpdateOnFiber是react渲染更新的主要函数)。

我们把初始化mountState和无状态组件更新机制讲明白了,接下来看一下其他的hooks初始化做了些什么操作?

初始化 useEffect -> mountEffect

上述讲到了无状态组件中fiber对象memoizedState保存当前的hooks形成的链表。那么updateQueue保存了什么信息呢,我们会在接下来探索useEffect过程中找到答案。 当我们调用useEffect的时候,在组件第一次渲染的时候会调用mountEffect方法,这个方法到底做了些什么?

mountEffect

// packages/react-reconciler/src/ReactFiberHooks.new.js:1698
function mountEffect( create, deps) {
  const hook = mountWorkInProgressHook(); // 创建hook对象
  const nextDeps = deps === undefined ? null : deps;
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag, 
    create, // useEffect 第一次参数,就是副作用函数
    undefined,
    nextDeps, // useEffect 第二次参数,deps
  );
}

创建hook对象,然后将effect hook的信息保存在hookmemoizedState里面。

有两个memoizedState大家千万别混淆了,我这里再友情提示一遍

  • workInProgress / current 树上的 memoizedState 保存的是当前函数组件每个hooks形成的链表。

  • 每个hooks上的memoizedState 保存了当前hooks信息,不同种类的hooksmemoizedState内容不同。

上述的方法最后执行了一个pushEffect,我们一起看看pushEffect做了些什么?

pushEffect

pushEffect 创建effect对象,挂载updateQueue

// packages/react-reconciler/src/ReactFiberHooks.new.js:1537
function pushEffect(tag, create, destroy, deps) {
  const effect = {
    tag,
    create,
    destroy,
    deps,
    next: null,
  };
  let componentUpdateQueue = currentlyRenderingFiber.updateQueue
  // 如果是第一个 useEffect
  if (componentUpdateQueue === null) {
    componentUpdateQueue = {  lastEffect: null  }
    currentlyRenderingFiber.updateQueue = componentUpdateQueue
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {  
      // 存在多个effect
    const lastEffect = componentUpdateQueue.lastEffect;
    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      const firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      componentUpdateQueue.lastEffect = effect;
    }
  }
  return effect;
}

这一段实际很简单,首先创建一个 effect ,判断组件如果第一次渲染,那么创建 componentUpdateQueue ,就是workInProgressupdateQueue。然后将effect放入updateQueue中。

假设我们在一个函数组件中这么写:

useEffect(()=>{
    console.log(1)
},[ props.a ])
useEffect(()=>{
    console.log(2)
},[])
useEffect(()=>{
    console.log(3)
},[])

最后workInProgress.updateQueue会以这样的形式保存:

7B8889E7-05B3-4BC4-870A-0D4C1CDF6981.jpg

拓展:effectList

effect list 可以理解为是一个存储 effectTag 副作用列表容器。

它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect ,和最后一个节点 lastEffect

React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每一个有副作用的 fiber 筛选出来,最后构建生成一个只带副作用的 effect list 链表。

commit 阶段,React 拿到 effect list 数据后,通过遍历 effect list,并根据每一个 effect 节点的 effectTag 类型,执行每个effect,从而对相应的 DOM 树执行更改。

初始化useMemo -> mountMemo

不知道大家是否把 useMemo 想象的过于复杂了,实际相比其他 useState , useEffect等,它的逻辑实际简单的很。

// packages/react-reconciler/src/ReactFiberHooks.new.js:1895
function mountMemo(nextCreate,deps){
  const hook = mountWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

初始化useMemo,就是创建一个hook,然后执行useMemo的第一个参数,得到需要缓存的值,然后将值和deps记录下来,赋值给当前hookmemoizedState。整体上并没有复杂的逻辑。

初始化useRef -> mountRef

对于useRef初始化处理,似乎更是简单,我们一起来看一下:

function mountRef(initialValue) {
  const hook = mountWorkInProgressHook();
  const ref = {current: initialValue};
  hook.memoizedState = ref;
  return ref;
}

mountRef初始化很简单, 创建一个ref对象, 对象的current 属性来保存初始化的值,最后用memoizedState保存ref,完成整个操作。

mounted 阶段 hooks 总结

我们来总结一下初始化阶段,react-hooks做的事情:

在一个函数组件第一次渲染执行上下文过程中,每个react-hooks执行,都会产生一个hook对象,并形成链表结构,绑定在workInProgressmemoizedState属性上,然后react-hooks上的状态,绑定在当前hooks对象的memoizedState属性上。

对于effect副作用钩子,会绑定在workInProgress.updateQueue上,等到commit阶段,dom树构建完成,再执行每个 effect 副作用钩子。

hooks更新阶段

上述介绍了第一次渲染函数组件,react-hooks初始化都做些什么,接下来,我们分析一下,

对于更新阶段,说明上一次 workInProgress 树已经赋值给了 current 树。

存放hooks信息的memoizedState,此时已经存在current树上,react对于hooks的处理逻辑和fiber树逻辑类似。

对于一次函数组件更新,当再次执行hooks函数的时候,比如 useState(0) ,首先要从currenthooks中找到与当前workInProgressHook,对应的currentHooks,然后复制一份currentHooksworkInProgressHook,接下来hooks函数执行的时候,把最新的状态更新到workInProgressHook,保证hooks状态不丢失。

所以函数组件每次更新,每一次react-hooks函数执行,都需要有一个函数去做上面的操作,这个函数就是updateWorkInProgressHook,我们接下来一起看这个updateWorkInProgressHook

image-20211224171221192

updateWorkInProgressHook

// packages/react-reconciler/src/ReactFiberHooks.new.js:649
function updateWorkInProgressHook() {
  let nextCurrentHook;
    // 如果 currentHook = null 证明它是第一个hooks 
  if (currentHook === null) {  
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
        // 从current树上取出memoizedState
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else { 
      // 不是第一个hooks,那么指向下一个 hooks 
    nextCurrentHook = currentHook.next;
  }
  let nextWorkInProgressHook
  //第一次执行hooks
  if (workInProgressHook === null) {  
    // 这里应该注意一下,当函数组件更新也是调用 renderWithHooks ,memoizedState属性是置空的
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else { 
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) { 
      /* 这个情况说明 renderWithHooks 执行 过程发生多次函数组件的执行 ,我们暂时先不考虑 */
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    invariant(
      nextCurrentHook !== null,
      'Rendered more hooks than during the previous render.',
    );
    currentHook = nextCurrentHook;
      //创建一个新的hook
    const newHook = { 
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
      // 如果是第一个hooks
    if (workInProgressHook === null) { 
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else { 
        // 重新更新 hook
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

这一段的逻辑大致是这样的:

  1. 首先如果是第一次执行hooks函数,那么从current树上取出memoizedState ,也就是旧的hooks

  2. 然后声明变量nextWorkInProgressHook,这里应该值得注意,正常情况下,一次renderWithHooks执行,workInProgress上的memoizedState会被置空,hooks函数顺序执行,nextWorkInProgressHook应该一直为null,那么什么情况下nextWorkInProgressHook不为null,也就是当一次renderWithHooks执行过程中,执行了多次函数组件,也就是在renderWithHooks中这段逻辑。

  if (workInProgress.expirationTime === renderExpirationTime) { 
       // ....这里的逻辑我们先放一放
  }

这里面的逻辑,实际就是判定,如果当前函数组件执行后,当前函数组件还是处于渲染优先级,说明函数组件又有了新的更新任务,那么循坏执行函数组件。这就造成了上述的,nextWorkInProgressHook不为 null 的情况。

  1. 最后复制currenthooks,把它赋值给workInProgressHook,用于更新新的一轮hooks状态。

接下来我们看一下四个种类的hooks,在一次组件更新中,分别做了哪些操作。

updateState

useState

function updateReducer( reducer, initialArg, init){
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const current = currentHook;
  let baseQueue = current.baseQueue;
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
     // 这里省略... 第一步:将 pending  queue 合并到 basequeue
  }
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;
    do {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) { //优先级不足
        const clone  = {
          expirationTime: update.expirationTime,
          ...
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
      } else {  //此更新确实具有足够的优先级。
        if (newBaseQueueLast !== null) {
          const clone= {
            expirationTime: Sync, 
             ...
          };
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        /* 得到新的 state */
        newState = reducer(newState, action);
      }
      update = update.next;
    } while (update !== null && update !== first);
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = newBaseQueueFirst;
    }
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;
    queue.lastRenderedState = newState;
  }
  const dispatch = queue.dispatch
  return [hook.memoizedState, dispatch];
}

这一段看起来很复杂,让我们慢慢吃透,首先将上一次更新的pending queue 合并到 basequeue,为什么要这么做,比如我们在一次点击事件中这么写

function Index(){
   const [ number ,setNumber ] = useState(0)
   const handerClick = ()=>{
    //    setNumber(1)
    //    setNumber(2)
    //    setNumber(3)
       setNumber(state=>state+1)
       // 获取上次 state = 1 
       setNumber(state=>state+1)
       // 获取上次 state = 2
       setNumber(state=>state+1)
   }
   console.log(number) // 3 
   return <div>
       <div>{ number }</div>
       <button onClick={ ()=> handerClick() } >点击</button>
   </div>
}

点击按钮, 打印 3

三次setNumber产生的update会暂且放入pending queue,在下一次函数组件执行时候,三次 update被合并到 baseQueue。结构如下图:

image-20211221183145355

接下来会把当前useState或是useReduer对应的hooks上的baseStatebaseQueue更新到最新的状态。会循环baseQueueupdate,复制一份update,更新 expirationTime,对于有足够优先级的update(上述三个setNumber产生的update都具有足够的优先级),我们要获取最新的state状态。,会一次执行useState上的每一个action。得到最新的state

更新state

image-20211221183400674

这里有会有两个疑问🤔️:

  • 问题一:这里不是执行最后一个action不就可以了嘛?

原因很简单,上面说了 useState逻辑和useReducer差不多。如果第一个参数是一个函数,会引用上一次 update产生的 state, 所以需要循环调用,每一个updatereducer,如果setNumber(2)是这种情况,那么只用更新值,如果是setNumber(state=>state+1),那么传入上一次的 state 得到最新state

  • 问题二:什么情况下会有优先级不足的情况(updateExpirationTime < renderExpirationTime)?

这种情况,一般会发生在,当我们调用setNumber时候,调用scheduleUpdateOnFiber渲染当前组件时,又产生了一次新的更新,所以把最终执行reducer更新state任务交给下一次更新。

updateEffect

function updateEffect(create, deps): void {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  let destroy = undefined;
  if (currentHook !== null) {
    const prevEffect = currentHook.memoizedState;
    destroy = prevEffect.destroy;
    if (nextDeps !== null) {
      const prevDeps = prevEffect.deps;
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        pushEffect(hookEffectTag, create, destroy, nextDeps);
        return;
      }
    }
  }
  currentlyRenderingFiber.effectTag |= fiberEffectTag
  hook.memoizedState = pushEffect(
    HookHasEffect | hookEffectTag,
    create,
    destroy,
    nextDeps,
  );
}

useEffect 做的事很简单,判断两次deps 相等,如果相等说明此次更新不需要执行,则直接调用 pushEffect,这里注意 effect的标签,hookEffectTag,如果不相等,那么更新 effect ,并且赋值给hook.memoizedState,这里标签是 HookHasEffect | hookEffectTag,然后在commit阶段,react会通过标签来判断,是否执行当前的 effect 函数。

updateMemo

function updateMemo( nextCreate, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps; // 新的 deps 值
  const prevState = hook.memoizedState; 
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1]; // 之前保存的 deps 值
      if (areHookInputsEqual(nextDeps, prevDeps)) { //判断两次 deps 值
        return prevState[0];
      }
    }
  }
  const nextValue = nextCreate();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

在组件更新过程中,我们执行useMemo函数,做的事情实际很简单,就是判断两次 deps是否相等,如果不相等,证明依赖项发生改变,那么执行 useMemo的第一个函数,得到新的值,然后重新赋值给hook.memoizedState,如果相等 证明没有依赖项改变,那么直接获取缓存的值。

不过这里有一点,值得注意,nextCreate()执行,如果里面引用了usestate等信息,变量会被引用,无法被垃圾回收机制回收,就是闭包原理,那么访问的属性有可能不是最新的值,所以需要把引用的值,添加到依赖项 dep 数组中。每一次dep改变,重新执行,就不会出现问题了。

温馨小提示: 有很多人说 useMemo怎么用,到底什么场景用,用了会不会起到反作用,通过对源码原理解析,我可以明确的说,基本上可以放心使用,说白了就是可以定制化缓存,存值取值而已。

updateRef

function updateRef(initialValue){
  const hook = updateWorkInProgressHook()
  return hook.memoizedState
}

函数组件更新useRef做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState内存中都指向了一个对象,所以解释了useEffect,useMemo 中,为什么useRef不需要依赖注入,就能访问到最新的改变值。

一次点击事件更新

image-20211221184330188

总结

上面我们从函数组件初始化,到函数组件更新渲染,两个维度分解讲解了react-hooks原理,掌握了react-hooks原理和内部运行机制,有助于我们在工作中,更好的使用react-hooks