一 前言
本篇文章次要从react-hooks
起源,原理,源码角度,开始分析react-hooks
运行机制和外部原理,置信这篇文章过后,对于面试的时候那些hooks
问题,也就迎刃而解了。理论react-hooks
也并没有那么难以了解,听起来很cool
,理论就是函数组件解决没有state
,生命周期,逻辑不能复用的一种技术计划。
Hook 是 React 16.8 的新增个性。它能够让你在不编写 class 的状况下应用 state 以及其余的 React 个性。
老规矩,️️️咱们带着疑难开始明天的探讨(能答复上几个,本人能够尝试一下,把握水平):
- 1 在无状态组件每一次函数上下文执行的时候,
react
用什么形式记录了hooks
的状态? - 2 多个
react-hooks
用什么来记录每一个hooks
的程序的 ? 换个问法!为什么不能条件语句中,申明hooks
?hooks
申明为什么在组件的最顶部? - 3
function
函数组件中的useState
,和class
类组件setState
有什么区别? - 4
react
是怎么捕捉到hooks
的执行上下文,是在函数组件外部的? - 5
useEffect
,useMemo
中,为什么useRef
不须要依赖注入,就能拜访到最新的扭转值? - 6
useMemo
是怎么对值做缓存的?如何利用它优化性能? - 7 为什么两次传入
useState
的值雷同,函数组件不更新? - ...
纲要.jpg
如果你认真读完这篇文章,这些问题全会迎刃而解。
function组件和class组件实质的区别
在解释react-hooks
原理的之前,咱们要加深了解一下, 函数组件和类组件到底有什么区别,废话不多说,咱们先看 两个代码片段。
class Index extends React.Component<any,any>{ 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> }}
打印后果?
再来看看函数组件中:
function Index(){ const [ num ,setNumber ] = React.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的面纱
1 当咱们引入hooks时候产生了什么?
咱们从引入 hooks
开始,以useState
为例子,当咱们从我的项目中这么写:
import { useState } from 'react'
于是乎咱们去找useState
,看看它到底是哪路神仙?
react/src/ReactHooks.js
useState
export function useState(initialState){ const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState);}
useState()
的执行等于 dispatcher.useState(initialState)
这外面引入了一个dispatcher
,咱们看一下resolveDispatcher
做了些什么?
resolveDispatcher
function resolveDispatcher() { const dispatcher = ReactCurrentDispatcher.current return dispatcher}
ReactCurrentDispatcher
react/src/ReactCurrentDispatcher.jsconst ReactCurrentDispatcher = { current: null,};
咱们看到ReactCurrentDispatcher.current
初始化的时候为null
,而后就没任何下文了。咱们暂且只能把ReactCurrentDispatcher
记下来。看看ReactCurrentDispatcher
什么时候用到的 ?
2 动工造物,从无状态组件的函数执行说起
想要彻底弄明确hooks
,就要从其本源开始,上述咱们在引入hooks
的时候,最初以一个ReactCurrentDispatcher
草草收尾,线索全副断了,所以接下来咱们只能从函数组件执行开始。
renderWithHooks 执行函数
对于function
组件是什么时候执行的呢?
react-reconciler/src/ReactFiberBeginWork.js
function
组件初始化:
renderWithHooks( null, // current Fiber workInProgress, // workInProgress Fiber Component, // 函数组件自身 props, // props context, // 上下文 renderExpirationTime,// 渲染 ExpirationTime);
对于初始化是没有current
树的,之后实现一次组件更新后,会把以后workInProgress
树赋值给current
树。
function
组件更新:
renderWithHooks( current, workInProgress, render, nextProps, context, renderExpirationTime,);
咱们从上边能够看进去,renderWithHooks
函数作用是调用function
组件函数的次要函数。咱们重点看看renderWithHooks
做了些什么?
renderWithHooks react-reconciler/src/ReactFiberHooks.js
export function renderWithHooks( current, workInProgress, Component, props, secondArg, nextRenderExpirationTime,) { renderExpirationTime = nextRenderExpirationTime; currentlyRenderingFiber = workInProgress; workInProgress.memoizedState = null; workInProgress.updateQueue = null; workInProgress.expirationTime = NoWork; ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; let children = Component(props, secondArg); if (workInProgress.expirationTime === renderExpirationTime) { // ....这里的逻辑咱们先放一放 } ReactCurrentDispatcher.current = ContextOnlyDispatcher; renderExpirationTime = NoWork; currentlyRenderingFiber = null; currentHook = null workInProgressHook = null; didScheduleRenderPhaseUpdate = false; return children;}
所有的函数组件执行,都是在这里办法中,首先咱们应该明确几个感怀,这对于后续咱们了解useState
是很有帮忙的。
current fiber树
: 当实现一次渲染之后,会产生一个current
树,current
会在commit
阶段替换成实在的Dom
树。
workInProgress fiber树
: 行将和谐渲染的 fiber
树。再一次新的组件更新过程中,会从current
复制一份作为workInProgress
,更新结束后,将以后的workInProgress
树赋值给current
树。
workInProgress.memoizedState
: 在class
组件中,memoizedState
寄存state
信息,在function
组件中,这里能够提前透漏一下,memoizedState
在一次和谐渲染过程中,以链表的模式寄存hooks
信息。
workInProgress.expirationTime
: react
用不同的expirationTime
,来确定更新的优先级。
currentHook
: 能够了解 current
树上的指向的以后调度的 hooks
节点。
workInProgressHook
: 能够了解 workInProgress
树上指向的以后调度的 hooks
节点。
renderWithHooks
函数次要作用:
首先先置空行将和谐渲染的workInProgress
树的memoizedState
和updateQueue
,为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks
信息挂载到这两个属性上,而后在组件commit
阶段,将workInProgress
树替换成current
树,替换实在的DOM
元素节点。并在current
树保留hooks
信息。
而后依据以后函数组件是否是第一次渲染,赋予ReactCurrentDispatcher.current
不同的hooks
,终于和下面讲到的ReactCurrentDispatcher
分割到一起。对于第一次渲染组件,那么用的是HooksDispatcherOnMount
hooks对象。 对于渲染后,须要更新的函数组件,则是HooksDispatcherOnUpdate
对象,那么两个不同就是通过current
树上是否memoizedState
(hook信息)来判断的。如果current
不存在,证实是第一次渲染函数组件。
接下来,调用Component(props, secondArg);
执行咱们的函数组件,咱们的函数组件在这里真正的被执行了,而后,咱们写的hooks
被顺次执行,把hooks
信息顺次保留到workInProgress
树上。 至于它是怎么保留的,咱们马上会讲到。
接下来,也很重要,将ContextOnlyDispatcher
赋值给 ReactCurrentDispatcher.current
,因为js
是单线程的,也就是说咱们没有在函数组件中,调用的hooks
,都是ContextOnlyDispatcher
对象上hooks
,咱们看看ContextOnlyDispatcher
hooks,到底是什么。
const ContextOnlyDispatcher = { useState:throwInvalidHookError}function throwInvalidHookError() { invariant( false, '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://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.', );}
原来如此,react-hooks
就是通过这种函数组件执行赋值不同的hooks
对象形式,判断在hooks
执行是否在函数组件外部,捕捉并抛出异样的。
最初,从新置空一些变量比方currentHook
,currentlyRenderingFiber
,workInProgressHook
等。
3 不同的hooks
对象
上述讲到在函数第一次渲染组件和更新组件别离调用不同的hooks
对象,咱们当初就来看看HooksDispatcherOnMount
和 HooksDispatcherOnUpdate
。
第一次渲染(我这里只展现了罕用的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
,本文的第二局部和第三局部,将重点两者的分割。
咱们用流程图来形容整个过程:
17AC0A26-745A-4FD8-B91B-7CADB717234C.jpg
三 hooks初始化,咱们写的hooks会变成什么样子
本文将重点围绕四个中重点hooks
开展,别离是负责组件更新的useState
,负责执行副作用useEffect
,负责保留数据的useRef
,负责缓存优化的useMemo
, 至于useCallback
,useReducer
,useLayoutEffect
原理和那四个重点hooks
比拟相近,就不一一解释了。
咱们先写一个组件,并且用到上述四个次要hooks
:
请记住如下代码片段,前面解说将以如下代码段开展
import React , { useEffect , useState , useRef , useMemo } from 'react'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
最终会变成什么?
1 mountWorkInProgressHook
在组件初始化的时候,每一次hooks
执行,如useState()
,useRef()
,都会调用mountWorkInProgressHook
,mountWorkInProgressHook
到底做了写什么,让咱们一起来剖析一下:
react-reconciler/src/ReactFiberHooks.js \-> mountWorkInProgressHookfunction mountWorkInProgressHook() { const hook: Hook = { memoizedState: null, // useState中 保留 state信息 | useEffect 中 保留着 effect 对象 | useMemo 中 保留的是缓存的值和deps | useRef中保留的是ref 对象 baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // 例子中的第一个`hooks`-> useState(0) 走的就是这样。 currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { workInProgressHook = workInProgressHook.next = hook; } return workInProgressHook;}
mountWorkInProgressHook
这个函数做的事件很简略,首先每次执行一个hooks
函数,都产生一个hook
对象,外面保留了以后hook
信息,而后将每个hooks
以链表模式串联起来,并赋值给workInProgress
的memoizedState
。也就证实了上述所说的,函数组件用memoizedState
寄存hooks
链表。
至于hook
对象中都保留了那些信息?我这里先别离介绍一下 :
memoizedState: useState中
保留 state
信息 | useEffect
中 保留着 effect
对象 | useMemo
中 保留的是缓存的值和 deps
| useRef
中保留的是 ref
对象。
baseQueue : usestate
和useReducer
中 保留最新的更新队列。
baseState : usestate
和useReducer
中,一次更新中 ,产生的最新state
值。
queue : 保留待更新队列 pendingQueue
,更新函数 dispatch
等信息。
next: 指向下一个 hooks
对象。
那么当咱们函数组件执行之后,四个hooks
和workInProgress
将是如图的关系。
shunxu.jpg
晓得每个hooks
关系之后,咱们应该了解了,为什么不能条件语句中,申明hooks
。
咱们用一幅图示意如果在条件语句中申明会呈现什么状况产生。
如果咱们将上述demo
其中的一个 useRef
放入条件语句中,
let curRef = null if(isFisrt){ curRef = useRef(null) }
hoo11.jpg
因为一旦在条件语句中申明hooks
,在下一次函数组件更新,hooks
链表构造,将会被毁坏,current
树的memoizedState
缓存hooks
信息,和以后workInProgress
不统一,如果波及到读取state
等操作,就会产生异样。
上述介绍了 hooks
通过什么来证实唯一性的,答案 ,通过hooks
链表程序。和为什么不能在条件语句中,申明hooks
,接下来咱们依照四个方向,别离介绍初始化的时候产生了什么?
2 初始化useState -> mountState
mountState
function mountState( initialState){ const hook = mountWorkInProgressHook(); if (typeof initialState === 'function') { // 如果 useState 第一个参数为函数,执行函数失去state initialState = initialState(); } hook.memoizedState = hook.baseState = initialState; const queue = (hook.queue = { pending: null, // 带更新的 dispatch: null, // 负责更新函数 lastRenderedReducer: basicStateReducer, //用于失去最新的 state , lastRenderedState: initialState, // 最初一次失去的 state }); const dispatch = (queue.dispatch = (dispatchAction.bind( // 负责更新的函数 null, currentlyRenderingFiber, queue, ))) return [hook.memoizedState, dispatch];}
mountState
到底做了些什么,首先会失去初始化的state
,将它赋值给mountWorkInProgressHook
产生的hook
对象的 memoizedState
和baseState
属性,而后创立一个queue
对象,外面保留了负责更新的信息。
这里先说一下,在无状态组件中,useState
和useReducer
触发函数更新的办法都是dispatchAction
,useState
,能够看成一个简化版的useReducer
,至于dispatchAction
怎么更新state
,更新组件的,咱们接着往下钻研dispatchAction
。
在钻研之前 咱们先要弄明确dispatchAction
是什么?
function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A,)const [ number , setNumber ] = useState(0)
dispatchAction
就是 setNumber
, dispatchAction
第一个参数和第二个参数,曾经被bind
给改成currentlyRenderingFiber
和 queue
,咱们传入的参数是第三个参数action
dispatchAction 无状态组件更新机制
作为更新的次要函数,咱们一下来钻研一下,我把 dispatchAction
精简,精简,再精简,
function dispatchAction(fiber, queue, action) { // 计算 expirationTime 过程略过。 /* 创立一个update */ const update= { expirationTime, suspenseConfig, action, eagerReducer: null, eagerState: null, next: null, } /* 把创立的update */ 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)) { didScheduleRenderPhaseUpdate = true; update.expirationTime = renderExpirationTime; currentlyRenderingFiber.expirationTime = renderExpirationTime; } else { /* 以后函数组件对应fiber没有处于和谐渲染阶段 ,那么获取最新state , 执行更新 */ if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) { const lastRenderedReducer = queue.lastRenderedReducer; if (lastRenderedReducer !== null) { let prevDispatcher; try { const currentState = queue.lastRenderedState; /* 上一次的state */ const eagerState = lastRenderedReducer(currentState, action); /**/ update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (is(eagerState, currentState)) { return } } } } scheduleUpdateOnFiber(fiber, expirationTime); }}
无论是类组件调用setState
,还是函数组件的dispatchAction
,都会产生一个 update
对象,外面记录了此次更新的信息,而后将此update
放入待更新的pending
队列中,dispatchAction
第二步就是判断以后函数组件的fiber
对象是否处于渲染阶段,如果处于渲染阶段,那么不须要咱们在更新以后函数组件,只须要更新一下以后update
的expirationTime
即可。
如果以后fiber
没有处于更新阶段。那么通过调用lastRenderedReducer
获取最新的state
,和上一次的currentState
,进行浅比拟,如果相等,那么就退出,这就证实了为什么useState
,两次值相等的时候,组件不渲染的起因了,这个机制和Component
模式下的setState
有肯定的区别。
如果两次state
不相等,那么调用scheduleUpdateOnFiber
调度渲染以后fiber
,scheduleUpdateOnFiber
是react
渲染更新的次要函数。
咱们把初始化mountState
*和*无状态组件更新机制讲明确了,接下来看一下其余的hooks初始化做了些什么操作?
3 初始化useEffect -> mountEffect
上述讲到了无状态组件中fiber
对象memoizedState
保留以后的hooks
造成的链表。那么updateQueue
保留了什么信息呢,咱们会在接下来摸索useEffect
过程中找到答案。 当咱们调用useEffect
的时候,在组件第一次渲染的时候会调用mountEffect
办法,这个办法到底做了些什么?
mountEffect
function mountEffect( create, deps,) { const hook = mountWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = pushEffect( HookHasEffect | hookEffectTag, create, // useEffect 第一次参数,就是副作用函数 undefined, nextDeps, // useEffect 第二次参数,deps );}
每个hooks
初始化都会创立一个hook
对象,而后将hook的memoizedState
保留以后effect hook
信息。
有两个memoizedState
大家千万别混同了,我这里再情谊提醒一遍
workInProgress / current
树上的memoizedState
保留的是以后函数组件每个hooks
造成的链表。- 每个
hooks
上的memoizedState
保留了以后hooks
信息,不同品种的hooks
的memoizedState
内容不同。上述的办法最初执行了一个pushEffect
,咱们一起看看pushEffect
做了些什么?
pushEffect 创立effect对象,挂载updateQueue
function pushEffect(tag, create, destroy, deps) { const effect = { tag, create, destroy, deps, next: null, }; let componentUpdateQueue = currentlyRenderingFiber.updateQueue if (componentUpdateQueue === null) { // 如果是第一个 useEffect 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
,就是workInProgress
的updateQueue
。而后将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
树执行更改。
4 初始化useMemo -> mountMemo
不晓得大家是否把 useMemo
设想的过于简单了,理论相比其余 useState
, useEffect
等,它的逻辑理论简略的很。
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
记录下来,赋值给以后hook
的memoizedState
。整体上并没有简单的逻辑。
5 初始化useRef -> mountRef
对于useRef
初始化解决,仿佛更是简略,咱们一起来看一下:
function mountRef(initialValue) { const hook = mountWorkInProgressHook(); const ref = {current: initialValue}; hook.memoizedState = ref; return ref;}
mountRef
初始化很简略, 创立一个ref对象, 对象的current
属性来保留初始化的值,最初用memoizedState
保留ref
,实现整个操作。
6 mounted 阶段 hooks 总结
咱们来总结一下初始化阶段,react-hooks
做的事件,在一个函数组件第一次渲染执行上下文过程中,每个react-hooks
执行,都会产生一个hook
对象,并造成链表构造,绑定在workInProgress
的memoizedState
属性上,而后react-hooks
上的状态,绑定在以后hooks
对象的memoizedState
属性上。对于effect
副作用钩子,会绑定在workInProgress.updateQueue
上,等到commit
阶段,dom
树构建实现,在执行每个 effect
副作用钩子。
四 hooks更新阶段
上述介绍了第一次渲染函数组件,react-hooks
初始化都做些什么,接下来,咱们剖析一下,
对于更新阶段,阐明上一次 workInProgress
树曾经赋值给了 current
树。寄存hooks
信息的memoizedState
,此时曾经存在current
树上,react
对于hooks
的解决逻辑和fiber
树逻辑相似。
对于一次函数组件更新,当再次执行hooks
函数的时候,比方 useState(0)
,首先要从current
的hooks
中找到与以后workInProgressHook
,对应的currentHooks
,而后复制一份currentHooks
给workInProgressHook
,接下来hooks
函数执行的时候,把最新的状态更新到workInProgressHook
,保障hooks
状态不失落。
所以函数组件每次更新,每一次react-hooks
函数执行,都须要有一个函数去做下面的操作,这个函数就是updateWorkInProgressHook
,咱们接下来一起看这个updateWorkInProgressHook
。
1 updateWorkInProgressHook
function updateWorkInProgressHook() { let nextCurrentHook; if (currentHook === null) { /* 如果 currentHook = null 证实它是第一个hooks */ const current = currentlyRenderingFiber.alternate; if (current !== null) { nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } else { /* 不是第一个hooks,那么指向下一个 hooks */ nextCurrentHook = currentHook.next; } let nextWorkInProgressHook if (workInProgressHook === null) { //第一次执行hooks // 这里应该留神一下,当函数组件更新也是调用 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; const newHook = { //创立一个新的hook memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null, }; if (workInProgressHook === null) { // 如果是第一个hooks currentlyRenderingFiber.memoizedState = workInProgressHook = newHook; } else { // 从新更新 hook workInProgressHook = workInProgressHook.next = newHook; } } return workInProgressHook;}
这一段的逻辑大抵是这样的:
- 首先如果是第一次执行
hooks
函数,那么从current
树上取出memoizedState
,也就是旧的hooks
。 - 而后申明变量
nextWorkInProgressHook
,这里应该值得注意,失常状况下,一次renderWithHooks
执行,workInProgress
上的memoizedState
会被置空,hooks
函数程序执行,nextWorkInProgressHook
应该始终为null
,那么什么状况下nextWorkInProgressHook
不为null
,也就是当一次renderWithHooks
执行过程中,执行了屡次函数组件,也就是在renderWithHooks
中这段逻辑。
if (workInProgress.expirationTime === renderExpirationTime) { // ....这里的逻辑咱们先放一放 }
这外面的逻辑,理论就是断定,如果以后函数组件执行后,以后函数组件的还是处于渲染优先级,阐明函数组件又有了新的更新工作,那么循坏执行函数组件。这就造成了上述的,nextWorkInProgressHook
不为 null
的状况。
- 最初复制
current
的hooks
,把它赋值给workInProgressHook
,用于更新新的一轮hooks
状态。
接下来咱们看一下四个品种的hooks
,在一次组件更新中,别离做了那些操作。
2 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
。构造如下图:
setState.jpg
接下来会把以后useState
或是useReduer
对应的hooks
上的baseState
和baseQueue
更新到最新的状态。会循环baseQueue
的update
,复制一份update
,更新 expirationTime
,对于有足够优先级的update
(上述三个setNumber
产生的update
都具备足够的优先级),咱们要获取最新的state
状态。,会一次执行useState
上的每一个action
。失去最新的state
。
更新state
sset1.jpg
这里有会有两个疑难️:
- 问题一:这里不是执行最初一个
action
不就能够了嘛?
答案: 起因很简略,下面说了 useState
逻辑和useReducer
差不多。如果第一个参数是一个函数,会援用上一次 update
产生的 state
, 所以须要循环调用,每一个update
的reducer
,如果setNumber(2)
是这种状况,那么只用更新值,如果是setNumber(state=>state+1)
,那么传入上一次的 state
失去最新state
。
- 问题二:什么状况下会有优先级有余的状况(
updateExpirationTime < renderExpirationTime
)?
答案: 这种状况,个别会产生在,当咱们调用setNumber
时候,调用scheduleUpdateOnFiber
渲染以后组件时,又产生了一次新的更新,所以把最终执行reducer
更新state
工作交给下一次更新。
3 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
函数。
4 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
怎么用,到底什么场景用,用了会不会起到副作用,通过对源码原理解析,我能够明确的说,基本上能够放心使用,说白了就是能够定制化缓存,存值取值而已。
5 updateRef
function updateRef(initialValue){ const hook = updateWorkInProgressHook() return hook.memoizedState}
函数组件更新useRef做的事件更简略,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次,hook.memoizedState
内存中都指向了一个对象,所以解释了useEffect
,useMemo
中,为什么useRef
不须要依赖注入,就能拜访到最新的扭转值。
一次点击事件更新
91A72028-3A38-4491-9375-0895F420B7CD.jpg
五 总结
下面咱们从函数组件初始化,到函数组件更新渲染,两个维度合成解说了react-hooks
原理,把握了react-hooks
原理和外部运行机制,有助于咱们在工作中,更好的应用react-hooks
。