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
组件从头到尾执行一次,所以需要配合useMemo
,usecallback
等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又充当了componentDidUpdate
和componentwillreceiveprops
所以说合理的用于
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
useReducer
是react-hooks
提供的能够在无状态组件中运行的类似redux
的功能api
,至于它到底能不能代替redux,react-redux
,我个人的看法是不能的
redux
能够复杂的逻辑中展现优势 ,而且 redux
的中间件模式思想也是非常优秀了,我们可以通过中间件的方式来增强dispatch
,像 redux-thunk`` redux-sage
redux-action
redux-promise
都是比较不错的中间件,可以把同步reducer
编程异步的reducer
。
useReducer
接受的第一个参数是一个函数,我们可以认为它就是一个reducer
,reducer
的参数就是常规reducer
里面的state
和action
,返回改变后的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
差不多,都是判定是否满足当前的限定条件来决定是否执行useMemo
的callback
函数,而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-redux
用react-hooks
重写后运用了大量的useMemo
情景,我为大家分析一处。
react-redux
通过判断 redux store
的改变来获取与之对应的state
const previousState = useMemo(() => store.getState(), [store])
感兴趣的同学可以去看源码。
讲到这里,如果我们应用useMemo根据依赖项合理的颗粒化我们的组件,能起到很棒的优化组件的作用。
Redux
以简单的实现加减法举例子,store目录结构如下:
常量
定义加减两个常量并导出
// 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 store
的 dispatch
方法引用,通常用于“手动” 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的缺陷。
🤔️🤔️🤔️我们带着疑问开始今天的探讨(能回答上几个,自己可以尝试一下,掌握程度):
- 在无状态组件每一次函数上下文执行的时候,
react
用什么方式记录了hooks
的状态?- 为什么不能条件语句中,声明
hooks
?hooks
声明为什么在组件的最顶部?function
函数组件中的useState
,和class
类组件setState
有什么区别?react
是怎么捕获到hooks
的执行上下文,是在函数组件内部的?useEffect
,useMemo
中,为什么useRef
不需要依赖注入,就能访问到最新的改变值?useMemo
是怎么对值做缓存的?如何应用它优化性能?- 为什么两次传入
useState
的值相同,函数组件不更新?
Fiber 树
- Fiber 是一种数据结构,React目前的做法是使用链表,每个VirtualDOM节点内部表示一个Fiber
let virtualDOM = (
<div key="A">
<div key="B1">B1</div>
<div key="B2">B2</div>
</div>
)
- 当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就会检查现在还剩多少时间,如果没有时间就将控制权让出去
循环链表
循环链表是另一种形式的链式存储结构,它的特点是表中最后一个节点的指针指向头节点,整个链表形成一个环。
我们一起来看一下以下代码,以及示例图:
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);
我们将其每一项单独打印出来看一下
const pendingQueue = queue.pending
if (pendingQueue !== null) {
let first = pendingQueue.next
let update = first
do {
console.log(update)
update = update.next
} while (update !== first)
}
action1.next-->action2.next-->action3.next-->action1.next
可以得出三个结论:
pending
永远指向最后一个更新pengding.next
永远指向第一个更新- 更新顺序永远不变
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
函数主要作用:
-
首先置空即将调和渲染的
workInProgress
树的memoizedState
和updateQueue
,为什么这么做,因为在接下来的函数组件执行过程中,要把新的hooks
信息挂载到这两个属性上,然后在组件commit阶段(dom操作前中后),将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,到底是什么。
// 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
对象,我们现在就来看看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
我们用流程图来描述整个过程:
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
以链表形式串联起来,并赋值给workInProgress
的memoizedState
(注意:这里虽然赋值给的currentlyRenderingFiber,但是前面我们提到,他是workInProgress的引用)。也就回答了我们的第1个提问,函数组件用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
将是如图的关系。
知道每个hooks
关系之后,我们应该理解了,为什么不能条件语句中,声明hooks
。
我们用一幅图表示如果在条件语句中声明会出现什么情况发生。
如果我们将上述demo
其中的一个 useRef
放入条件语句中,
let curRef = null
if(isFisrt){
curRef = useRef(null)
}
为什么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
对象的memoizedState
和baseState
属性然后创建一个
queue
对象,里面保存了负责更新的信息。
这里先说一下,在无状态组件中,useState
和useReducer
触发函数更新的方法都是dispatchAction
。useState
可以看成一个简化版的useReducer
,至于dispatchAction
怎么更新state
,更新组件的,我们接着往下研究dispatchAction
。
在研究之前 我们先要弄明白**dispatchAction
是什么?**
function dispatchAction( fiber, queue, action )
const [ number , setNumber ] = useState(0)
dispatchAction
就是 setNumber
, dispatchAction
第一个参数和第二个参数,已经被bind
给改成currentlyRenderingFiber
和 queue
,我们传入的参数是第三个参数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
对象是否处于渲染阶段,如果处于渲染阶段,那么不需要我们再更新当前函数组件,只需要这之前的更新一下当前update
的eventTime
即可。
如果当前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
的信息保存在hook
的memoizedState
里面。
有两个memoizedState
大家千万别混淆了,我这里再友情提示一遍
workInProgress / current
树上的memoizedState
保存的是当前函数组件每个hooks
形成的链表。每个
hooks
上的memoizedState
保存了当前hooks
信息,不同种类的hooks
的memoizedState
内容不同。
上述的方法最后执行了一个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
,就是workInProgress
的updateQueue
。然后将effect
放入updateQueue
中。
假设我们在一个函数组件中这么写:
useEffect(()=>{
console.log(1)
},[ props.a ])
useEffect(()=>{
console.log(2)
},[])
useEffect(()=>{
console.log(3)
},[])
最后workInProgress.updateQueue
会以这样的形式保存:
拓展: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
记录下来,赋值给当前hook
的memoizedState
。整体上并没有复杂的逻辑。
初始化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
对象,并形成链表结构,绑定在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
。
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;
}
这一段的逻辑大致是这样的:
-
首先如果是第一次执行
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
,在一次组件更新中,分别做了哪些操作。
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
。结构如下图:
接下来会把当前useState
或是useReduer
对应的hooks
上的baseState
和baseQueue
更新到最新的状态。会循环baseQueue
的update
,复制一份update
,更新 expirationTime
,对于有足够优先级的update
(上述三个setNumber
产生的update
都具有足够的优先级),我们要获取最新的state
状态。,会一次执行useState
上的每一个action
。得到最新的state
。
更新state
这里有会有两个疑问🤔️:
- 问题一:这里不是执行最后一个
action
不就可以了嘛?
原因很简单,上面说了
useState
逻辑和useReducer
差不多。如果第一个参数是一个函数,会引用上一次update
产生的state
, 所以需要循环调用,每一个update
的reducer
,如果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
不需要依赖注入,就能访问到最新的改变值。
一次点击事件更新
总结
上面我们从函数组件初始化,到函数组件更新渲染,两个维度分解讲解了react-hooks
原理,掌握了react-hooks
原理和内部运行机制,有助于我们在工作中,更好的使用react-hooks
。