You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
importReactfrom"react";importReactDOMfrom"react-dom";importCounterfrom"./Counter";classHomeextendsReact.Component{constructor(props){super(props);this.state={step: 0};this.handleClick=this.handleClick.bind(this);}handleClick(){this.setState((state)=>{return{step: state.step+1};},()=>{console.log("this.setState callback");});}staticgetDerivedStateFromProps(props,state){console.log("getDerivedStateFromProps======");returnnull;}getSnapshotBeforeUpdate(prevProps,prevState){constbtn=document.getElementById("btn");constscrollHeight=btn.scrollHeight;console.log("get snapshot before update...",scrollHeight);returnscrollHeight;}componentDidUpdate(prevProps,prevState,snapshot){console.log("component did update...",snapshot);}componentDidMount(){console.log("component did mount......");}componentWillUnmount(){console.log("component will unmount...");}UNSAFE_componentWillMount(){console.log("component will mount...");}UNSAFE_componentWillReceiveProps(nextProps){console.log("component will receive props...",nextProps);}UNSAFE_componentWillUpdate(nextProps,nextState){console.log("component will update....",nextProps,nextState);}shouldComponentUpdate(){console.log("should component update");returntrue;}render(){return[(!this.state.step||this.state.step%3)&&(<Counterstep={this.state.step}/>),<buttonid="btn"key="2"onClick={this.handleClick}>
类组件按钮:{this.state.step}</button>,];}}ReactDOM.render(<Home/>,document.getElementById("root"));
functioncompleteUnitOfWork(unitOfWork){letcompletedWork=unitOfWork;do{constcurrent=completedWork.alternate;constreturnFiber=completedWork.return;letnext;// 完成此fiber对应的真实DOM节点创建和属性赋值的功能next=completeWork(current,completedWork,subtreeRenderLanes);// 开始构建副作用列表。if(returnFiber!==null){if(returnFiber.firstEffect===null){returnFiber.firstEffect=completedWork.firstEffect;}if(completedWork.lastEffect!==null){if(returnFiber.lastEffect!==null){returnFiber.lastEffect.nextEffect=completedWork.firstEffect;}returnFiber.lastEffect=completedWork.lastEffect;}constflags=completedWork.flags;if(flags>PerformedWork){if(returnFiber.lastEffect!==null){returnFiber.lastEffect.nextEffect=completedWork;}else{returnFiber.firstEffect=completedWork;}returnFiber.lastEffect=completedWork;}}constsiblingFiber=completedWork.sibling;if(siblingFiber!==null){// If there is more work to do in this returnFiber, do that next.workInProgress=siblingFiber;return;}completedWork=returnFiber;workInProgress=completedWork;}while(completedWork!==null);}
深入概述 ReactDOM.render 初次渲染 以及 setState 手动触发状态更新主流程
调试 DEMO
答应我,在阅读本篇文章时,用以下 Demo,在本篇文章介绍的各个函数入口处打断点,边阅读边 debug。书读百遍,真的不如动手一遍。
新建 index.jsx 文件:
新建 Counter.jsx 文件:
一、前置知识
在阅读本文时,假设你已经有一些 fiber 的基础知识。
1.1 容器 root 节点
我们传递给
ReactDOM.render(element, root)
的第二个参数root
1.2 fiber 类型
fiber
节点的类型通过fiber.tag
标识,称为React work tag
。我们重点关注以下几个类型:React 对于文本节点的处理分两种场景:单一节点和多节点。比如:
记住这几个 fiber 类型,会贯穿整篇文章。在整个 react 渲染阶段,react 基于 fiber.tag 执行不同的操作。因此你会看到大量的基于 fiber.tag 的 switch 语句。
1.3 副作用
副作用通过
fiber.flags
标记。对于不同的 fiber 类型,副作用含义不同在 render 阶段,react 会找出有副作用的 fiber 节点,并自底向上构建单向的
副作用链表
1.4 React 渲染流程
React 渲染主要分为两个阶段:
render
阶段 和commit
阶段。1.4.1 render 阶段
render
阶段支持异步并发渲染,可中断。分为 beginWork 以及 completeUnitOfWork 两个子阶段:render 阶段的结果是一个副作用链表以及一棵 finishedWork 树。
1.4.2 commit 阶段
commit 阶段是同步的,一旦开始就不能再中断。这个阶段遍历副作用链表并执行真实的 DOM 操作。commit 阶段分为
commitBeforeMutationEffects
、commitMutationEffects
以及commitLayoutEffects
三个子阶段。每个子阶段都是一个 while 循环。同时,每个子阶段都是从头开始遍历副作用链表!!!__reactProps$md9gs3r7129
属性,这个属性存的是 fiber 节点的 props 值。这个属性很重要,主要是更新 dom 上的 onClick 等合成事件。由于事件委托在容器 root 上,因此在事件委托时,需要通过 dom 节点获取最新的 onClick 等事件二、ReactDOM.render 初次渲染
初次渲染的入口。初次渲染主要逻辑在
createRootImpl
以及updateContainer
这两个函数中,React 在初次渲染不会追踪副作用,主要工作:root._reactRootContainer._internalRoot
属性访问。本篇文章中我们重点关注
_internalRoot
中的两个属性:current
和finishedWork
。current
保存的是当前页面对应的 fiber 树。finishedWork
保存的是 render 阶段完成,commit 阶段开始前,构建完成但是还没更新到页面的 fiber 树。等commit
阶段完成后,finishedWork
树就变成了current
树三、调度更新
3.1 scheduleUpdateOnFiber
更新的入口。不管是初次渲染,还是后续我们通过 this.setState 或者 useState 等手动触发状态更新,都会走
scheduleUpdateOnFiber
方法开始调度更新。scheduleUpdateOnFiber
会从当前 fiber 开始往上找到 HostRootFiber,然后从 HostRootFiber 开始更新。这里又分为同步更新以及批量更新。同步更新直接走
performSyncWorkOnRoot
方法。批量更新走ensureRootIsScheduled
方法调度。ensureRootIsScheduled
方法里面会根据环境判断是该走同步更新,即performSyncWorkOnRoot
还是批量更新,即performConcurrentWorkOnRoot
本篇文章中,我们只需要关注同步更新,即
performSyncWorkOnRoot
的流程。3.2 performSyncWorkOnRoot
performSyncWorkOnRoot
的 render 阶段是同步的。在这里,React 将渲染过程拆分成了两个子阶段:四、render 阶段
4.1 renderRootSync
render 阶段从
renderRootSync
函数开始。主要逻辑在prepareFreshStack
以及workLoopSync
方法。workInProgress 代表正在工作的 fiber 节点。对于每一个 fiber 节点,都会执行 performUnitOfWork。
4.2 performUnitOfWork
对于每一个 fiber 节点,首先调用
beginWork
协调子节点,如果beginWork
返回null
,说明当前 fiber 节点已经没有子节点,工作可以完成了,调用completeUnitOfWork
完成工作。4.3 beginWork
beginWork
函数自身就是一个简单的基于fiber.tag
的 switch 语句,这个阶段的逻辑主要在各个分支函数中。beginWork
最主要的工作:4.3.1 HostRootFiber beginWork:updateHostRoot
updateHostRoot
函数执行完,由于 HostRootFiber 没有副作用,因此 HostRootFiber.flags 依然是 04.3.2 类组件 beginWork: updateClassComponent
类组件的更新分为初次渲染以及更新两种情况。
constructClassInstance
以及mountClassInstance
两个函数中this.setState
方法的更新器instance._reactInternals
找到当前的fiber
节点,并开始调度更新。getDerivedStateFromProps
静态生命周期方法componentWillMount
生命周期方法updateClassInstance
函数中,按顺序执行以下操作:最后,调用
finishClassComponent
开始协调子元素4.3.3 函数组件 beginWork:mountIndeterminateComponent & updateFunctionComponent
函数组件在第一次渲染时,会走
IndeterminateComponent
分支,执行mountIndeterminateComponent
方法当执行完成
renderWithHooks
方法后,此时 fiber 类型已经确定,因此需要修改workInProgress.tag
函数组件在更新阶段,会走
FunctionComponent
分支,执行updateFunctionComponent
方法。不管是初次渲染还是更新阶段,都会走
renderWithHooks
方法,这是函数组件执行的主要逻辑。React 提供了各种 hook 给我们在函数组件中使用,但是这些 hook 在初次渲染和更新阶段的行为又有点不同,为了屏蔽这些行为,React 在renderWithHooks
中会判断,如果是初次渲染,则使用HooksDispatcherOnMount
,如果是更新阶段,则使用HooksDispatcherOnUpdate
。HooksDispatcherOnMount
和HooksDispatcherOnUpdate
提供的 API 一模一样,只是实现有细微差别。4.3.4 原生的 HTML 标签 beginWork:updateHostComponent
调用
shouldSetTextContent
判断是否需要为新的 react element 子节点创建 fiber 节点。如果新的 react element,即 nextChildren 是一个字符串或者数字,则说明 nextChildren 不需要创建 fiber 节点。比如:如果是多节点的情况,比如:
4.3.5 HostText 文本节点 beginWork:updateHostText
HostText
节点在beginWork
阶段几乎不做任何处理,因此这个节点可以直接完成了。4.4 completeUnitOfWork
当一个 fiber 节点没有子节点,或者子节点仅仅是单一的字符串或者数字时,说明这个 fiber 节点当前的
beginWork
已经完成,可以进入completeUnitOfWork
完成工作。completeUnitOfWork
主要工作如下:completeWork
。创建真实的 DOM 节点,属性赋值等。completeWork
函数也是一个基于fiber.tag
的 switch 语句可以看出,对于函数组件和类组件,
completeWork
几乎没有工作。主要的工作集中在HostRoot
、HostComponent
、HostText
4.4.1 原生的 HTML 标签渲染 completeUnitOfWork
对于
HostComponent
,则需要区分第一次渲染以及更新阶段。注意这里的第一次渲染是指这个 DOM 元素第一次渲染。而不是我们的页面第一次渲染。HostComponent
第一次渲染__reactFiber$uqibbgdk1tp
属性, 同时将 newProps 挂载到 DOM 上的__reactProps$uqibbgdk1tp
属性。所以我们可以看到,浏览器上的每个 DOM 都会有至少两个自定义的属性:__reactProps$uqibbgdk1tp
和__reactFiber$uqibbgdk1tp
。这两个属性名称$
后面的是一串随机字符串appendAllChildren
中,调用parent.appendChild(child)
将 fiber 的 child(对应的真实 dom) 添加到当前的 DOM 上。finalizeInitialChildren
方法中,给真实的 DOM 设置属性,比如 style,id 等。这里有一点需要注意,
appendAllChildren
要区分两个场景:单一节点以及多节点。我们知道在
beginWork:updateHostComponent
中,如果HostComponent
只有一个新的子节点并且是字符串或者数字,那么则不会为新的子节点创建对应的 fiber 节点,比如:HostComponent
第一次渲染的逻辑主要集中在createInstance
、appendAllChildren
、finalizeInitialChildren
三个函数中。从这个过程也可以看出,是有对真实的 dom 进行操作的。HostComponent
更新阶段,主要逻辑在updateHostComponent
函数中:prepareUpdate
比较 oldProps 和 newProps 的差异。如果属性发生了变更,则将变更的属性的键值对存入数组updatePayload
中。updatePayload
复制给 workInProgress.updateQueueonClick
变了,其他属性没有变化,React 在diffProperties
时会特意将updatePayload
赋值一个空数组。方便在 commit 阶段重新挂载__reactProps$
属性4.4.2 HostText completeUnitOfWork
类似于
HostComponent
,HostText
也需要区分第一次渲染以及更新阶段。在第一次渲染阶段,只需要直接调用
document.createTextNode(text)
创建文本 DOM 节点,初次之外没有其他操作。在更新阶段,判断是否需要更新 fiber.flags,除此之外没有其他操作。
4.4.3 HostRoot completeUnitOfWork
updateHostContainer
方法其实就是一个空函数,HostRoot
在这个过程几乎没有操作。当执行到这里的时候,render
阶段已经完成,进入commit
阶段。注意,在初次渲染的过程中,React 不需要追踪副作用,同时在 render 阶段就操作真实的 DOM!!!!!!。当
HostRoot
的completeUnitOfWork
执行完成时,我们实际上已经得到一棵真实的 DOM 树,存储在内存中,还没挂载到容器 root 上五、commit 阶段
render
阶段完成后,我们得到一个副作用链表,以及一棵 finishedWork 树。commit
阶段从commitRoot
函数开始。主要逻辑在commitRootImpl
函数中5.1 commitRootImpl
commit 阶段分成三个子阶段:
第一阶段:commitBeforeMutationEffects。DOM 变更前
第二阶段:commitMutationEffects。DOM 变更,操作真实的 DOM 节点。注意这个阶段是
卸载
相关的生命周期方法执行时机useLayoutEffect
的清除函数
componentWillUnmount
生命周期方法useEffect
的清除函数
添加进异步队列,异步执行。第三阶段:commitLayoutEffects。DOM 变更后
useLayoutEffect
监听函数,同步执行useEffect
监听函数放入异步队列,异步执行componentDidMount
生命周期方法,同步执行componentDidUpdate
生命周期方法,同步执行this.setState(arg, callback)
中的callback
回调,同步执行每一个子阶段都是一个 while 循环,从头开始遍历副作用链表。
5.2 commitBeforeMutationEffects
这个函数主要是在 DOM 变更前执行,主要逻辑如下:
5.3 commitMutationEffects
这个函数操作 DOM,主要有三个方法:
parentNode.appendChild(child);
或者container.insertBefore(child, beforeChild)
插入 DOM 节点useLayoutEffect
的清除函数
,这个函数对于类组件没有任何操作卸载
相关的生命周期方法useLayoutEffect
的清除函数
,这是同步执行的useEffect
的清除函数
添加进异步刷新队列,这是异步执行的componentWillUnmount
生命周期方法5.4 commitLayoutEffects
当执行到这个函数,此时
useLayoutEffect
的清除函数已经全部执行完成。componentDidMount
生命周期方法,同步执行componentDidUpdate
生命周期方法,同步执行this.setState(arg, callback)
中的callback
回调,同步执行useLayoutEffect
监听函数,同步执行useEffect
监听函数放入异步队列,异步执行The text was updated successfully, but these errors were encountered: