1. 背景
react 从18 开始完全的加入了并行更新的功能,这个是一个重大的升级。react作为前端用到最多以及最熟练的工具,有必要了解其原理以及背后设计的思想。可以在做业务的层面,更深一层的掌握react。对写项目也是有帮助的,当我们深入了解react之后,相信
你写项目的时候会更加谨慎以及更加清楚代码是在做什么。
2.同步更新
先从同步更新开始,了解了同步更新后面的并行以及ssr 都会很好理解
let buttonRef = React.createRef(); let buttonRef11 = React.createRef(); // jsx 代码 function Parent() { const [sb, setSb] = React.useState(1); return ( <div ref={buttonRef} onClick={event => { setSb(pre => pre + 1); }}> {sb} <Child3 /> </div> ); } function Child3(props) { return <h3 ref={buttonRef11}>sdfsf</h3>; } root.render(<Parent />);
// jsx 编译成js 之后的代码
function Parent() { var _React$useState5 = React.useState(1), sb = _React$useState5[0], setSb = _React$useState5[1] return /*#__PURE__*/ React.createElement( "div", { ref: buttonRef, onClick: function (event) { setSb(function (pre) { return pre + 1 }) }, __source: { fileName: _jsxFileName, lineNumber: 180, columnNumber: 9 }, }, sb /*#__PURE__*/, React.createElement(Child3, { __source: { fileName: _jsxFileName, lineNumber: 186, columnNumber: 11 }, }) ) } function Child3(props) { return /*#__PURE__*/ React.createElement( "h3", { ref: buttonRef11, __source: { fileName: _jsxFileName, lineNumber: 160, columnNumber: 14 }, }, "sdfsf" ) } root.render( /*#__PURE__*/ React.createElement(Parent, { __source: { fileName: _jsxFileName, lineNumber: 205, columnNumber: 19 }, }) )
从上面两幅 图可以看到 jsx → js 之后原来的jsx 代码 都成了 React.createElement api 的调用。这就是纯js 代码。这样很好解释了一种写法(下图code)为什么能工作。jsx 语法只是 React.createElement 的语法糖,
const jsxCode = (<div>div content</div>) return jsxCode //============ var jsxCode = /*#__PURE__*/ React.createElement( "div", { __source: { fileName: _jsxFileName, lineNumber: 180, columnNumber: 24 } }, "div content" ) return jsxCode
2.1 React.createElement 做了什么
通过调试代码进入createElement
注意:react 内部在development (dev)和 production 下走的是两套代码,dev模式下的函数会做很多规则校验,代码内大量 if(DEV_){} 模式的代码,这种代码在webpack等打包工具编译 production 模式的时候,会被删除。
同时react 为了兼容性问题很多代码都有 xx.new.js 和 xx.old.js 两份代码,现在的react 18 都是 new.js 格式的文件
调用逻辑顺序是 下表格式
| 函数名称 | 功能 |
|---|---|
| createElementWithValidation(type, props, children) | 校验type 类型是否是function 或者 react 要求的类型 |
| createElement(type, config, children) | 校验props 内 是否有 key 和 ref ,对这些属性重新设置defineProperty ,添加警告信息和做对应的校验,同时从传入的arguments 内获取 正确的children |
| const ReactElement = function(type, key, ref, self, source, owner, props) | 这里是createElement 代码运行的逻辑 ,返回 createElement 的内容 , typeof 就是react 认识的type 类型在后面会用到const element = { // typeof 是 react 元素 === Symbol.for('react.element') // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Symbol.for('react.element'); // Built-in properties that belong on the element type: type, key: key, ref: ref, props: props, // Record the component responsible for creating this element. _owner: owner, } |
总的来说 createElement 除去校验,就做了一件事情返回一个 element json,里面包括类型 props 等信息。
2.2 render 函数做了什么
react 18 对 同步更新的render 还是支持的
render 函数属于 react-dom package 和 createElement (react)不是一个 package
简单介绍,后面的代码也会有很多校验的地方,这些大多是 dev 模式才会有的代码,所有直接讲解 逻辑部分,校验代码不讲了。
render 函数调用栈
| 函数名称 | 功能 |
|---|---|
| function render( element, // root App 组件 typeof === function container, // id="root" html 容器 ,ReactDom.render(,root) callback) | 一般render() 会有两个参数第一个是 element 从createElement 返回第二个是html 容器(container)第三个是一个callback 不经常使用 |
| function legacyRenderSubtreeIntoContainer( parentComponent, // null children, // container, // html rootforceHydrate, // false , ssr === falsecallback) | 这里需要注意因为是sync render ,所以 forceHydrate (ssr 使用)=== false , 后面代码混杂了不少ssr 的逻辑代码以及 并行更新的逻辑代码,我们这里只考虑sync 的情况react 会在 html 容器内插入很多属性比如 container._reactRootContainer ,判断是否已经被react 使用过的 root 容器刚开始进来肯定是没有被使用的,所以接下来会创建 React Root (FiberRoot) |
| function legacyCreateRootFromDOMContainer( container, // root div html initialChildren, // {props , type , key , ref , ....}parentComponent, // init render nullcallback, isHydrationContainer) | 1. 首先进来,react 会删除 html 容器下的其他 html |
-
创建 Root fiber (createFiberRoot)
-
挂载root fiber 到容器上: container._reactRootContainer = root.
-
在容器上监听所有的html 事件 (listenToAllSupportedEvents)
-
同步更新Task flushSync (flushSync) | | 在上面的逻辑内会创建Fiber Root 和 FiberHost 在 createFiberRoot 完成 | 1. 创建FiberRootNode , 称为 root
-
创建 FiberNode(tag === HostRoot) , 称为 fiberHost
-
连接上面创建的两个fiber,root.current = fiberHost , fiberHost.stateNode = root (注意stateNode属性,很重要)
-
在fiberHost 内插入更新队列,同时更新 记忆状态fiberHost.updateQueue = {...}fiberHost.memoizedState = {...}这两个属性也很重要这里创建完成之后就返回到上面的函数,作为fiberRoot | | listenToAllSupportedEvents | 在container 监听所有的 html event , react 的事件内容是假的,是react 内部模拟的。通过顶层container 或者 document 监听到事件之后在内容fiber 创建的 html tree内,模拟 html 事件传播过程 :1. 捕获
-
target
-
冒泡 | | flushSync | 设置一些前置参数,然后执行参数 的回调 (这里的参数回调是:updateContainer)下面开始到另一个package 了 : react-reconciler (react 调和器 ,翻译很不顺畅) | | function updateContainer( element, // {props, ...} container, // FiberRoot parentComponent, callback) | 这个时候 root 已经创建完成,同时还有参数element 1. 获取当前时间: performance.now()
-
请求更新Lane,因为是同步更新返回的都是SyncLane (0b00001 === 1) :注意这里有react 的特色了二进制bit ,后面会有大量的这种变量,这个其实是掩码格式,可以在一个变量内保存多种数据,这种可以节省变量,同时多数据很清晰,还有对应的数据操作方法可以扩展
-
创建update 数据 包括了 element ,然后挂载更新数据(enqueueUpdate) | | function enqueueUpdate( fiber, update, lane) | 还记得上面的 updateQueue 吗,这里会把包含 element的 更新,创建一个循环列表,插入到上面的 updateQueue.shared.pending 内,作用就是将数据连接到 updateQueue链表指针也是react 特色,这种数据结构有很多优势,后面的hooks 都是这种结构数据准备好之后开始插入更新队列,执行更新 | | function scheduleUpdateOnFiber( fiber, // init === FiberHost lane, eventTime) | 1. 从 当前更新的fiber ,一层迭代到顶层fiber 用bit 或( | ),向上冒泡 当前Lane 的值,代码大致是下面的逻辑,然后返回fiber root向上冒泡属性之后,只要比较root的childLane 就能知道对应的子集是否有lane 需要更新const node = sourceFiberconst lane = node.laneconst parent = sourceFiber.return // ( react 用 return 标记parent) ,同时还会处理另外一个 fiber ,react是双缓冲 alternaterwhile(parent != null){ parent.childLanes = parent.childLanes | lane if(node.tag === HostRoot(fiber host)){ return node.stateNode; // 返回root fiber } node = parent parent = parent.return}2. 在root 上标记更新属性 同时记录 更新Lane 对应的 int 值 所在的 更新时间root.pendingLanes |= laneroot.eventTimes = [.....,laneInt,....] = [...,eventTime,.....] ,同步更新laneInt === 03. 计划发布更新 ensureRootIsScheduled | | function ensureRootIsScheduled(root, currentTime) | 1. 这里就体现了Lane 为什么需要用bit 来表示下面这bit 可以看到有的位数是1,如果用1 表示一个更新,那么这下面就有三个更新,如果按照位数的越低表示优先级,那么最右边的1就是最高的优先级0x000100010001 所以这个函数就有这么一个功能从 root.pendingLanes 里面拿出优先级最高的那个lane 作为本次更新的Lane因为是同步更新所有很简单 这个Lane 就是 1 2. 将同步任务 performSyncWorkOnRoot 压入执行堆栈,同时发布一个微任务 在 这个任务内会刷新这个执行堆栈执行里面的方法大致逻辑是任务入栈:syncQueue = [performSyncWorkOnRoot]发布微任务: scheduleMicrotask(() => { flushSyncCallbacks(); //刷新任务堆栈})刷新堆栈: for(i = 0 ; i < syncQueue.length; i ++) { callback = syncQueue[i] callback()}3. 记录当前的更新优先级 root.callbackPriority = 1 (同步更新),这里就是批处理的关键,如果多次入栈前面的逻辑会比较这个值,只会执行一次更新操作注意:因为是init mount , 所以这个任务会马上执行,通过下面的逻辑 | | 返回到上面的 flushSync 最后的代码 | 1. 执行 flushSyncCallbacks,所有上面的微任务执行的时候其实任务已经刷新了,没有任务可以执行 |
Ok, 上面就是一段逻辑,render 函数是怎么从element到 react 任务的逻辑的,下面逻辑和上面的可以分开,因为如果不是init mount 下面的会在微任务内执行
2.3 react 怎么将 element 到 html ,以及hooks 是怎么执行的
主要的逻辑都在 performSyncWorkOnRoot 执行,包括内容
-
遍历 (深度优先,然后广度) element 创建 fiber (beginwork)
-
回溯遍历 fiber 创建 html ,然后连接 (completeWork) ,循环 1 步骤,直到遍历了所有的 function 组件以及对应的 html
-
完成上面之后,校验 bit 是否有更新执行 effects 逻辑 ,这个是commit 过程
-
判断是否有Effect , 将flushPassiveEffects 压入 Task,会在下一个js 时间循环的时候执行 ,flushPassiveEffects ,深度优先,然后广度 遍历fiber ,执行清理删除的fiber的内存,执行useEffect create 和 destory
-
执行comitBeforeMutationEffects ,深度优先,然后广度 遍历fiber ,校验 bit 是否有对应的修改,这里执行 HostRoot init ,删除 fiber ,和 visible fiber ,对应html event :beforeblur
-
执行commitMutationEffects : 深度优先,然后广度 遍历fiber,执行 ref 的detach ,断开 parent 和 child fiber 的连接指针,更新 html 内容
-
执行 commitLayoutEffects ,执行 useLayoutEffect 逻辑,连接ref
-
如果pendingLane 内还有其他的更新Lane 循环上面的 ensureRootIsScheduled 步骤,同步更新这个步骤是不会执行的。
深度优先,然后广度 遍历
- work
| renderRootSync | 准备环境 |
| prepareFreshStack | 1. 从root 对应的current(HostFiber),复制一份相同属性的fiber 作为 工作fiber ,workInProgress fiber (fiber.alternate),这个其实就是 fiber 对应的另一个fiber 也就是react 的双缓冲,也就是内存中运行的 fiber 其实是挂载html fiber 的一个copy ,在完成 work之后,会交换 workInProgressfiber(alternate) 和 currentFiber ,设置这个 alternate 等于workInProgress 全局变量, 注意 react 内用了大量的全局变量来做context,而且会 export 这种变量2. 设置一些全局的变量 lane |
| workLoopSync | 这个就是上图的执行函数,通过while 判断 workInProgress 是否 对于null 循环执行while(workInProgress !== null){ performUnitOfWork(workInProgress)} |
| performUnitOfWork | 这里执行 深度优先,然后广度 的逻辑,需要参考上面workLoop的那个while 循环 , 代码如下:while(workInProgress !== null){ current = fiber.alternate next = beginWork(current , fiber , SyncLane) // 同步更新都是SyncLane , next 返回的是深度遍历的fiber if( next === null){ // 深度遍历结束,开始回溯,回溯到能够广度遍历为止 completeUnitOfWork(fiber) } else { //还能够继续深度遍历,那么会执行while 循环 workInProgress = next; }}注意:上面修改的fiber 永远都是 alternate 对应的fiber |
| beginWork | 下表格 |
beginWork 会对fiber 的类型做不同的处理
- beginWork 对于 mount 和 update 会执行不一样的逻辑,如果不是host 那么第一次mount 的时候该fiber 的另一个 备用fiber 肯定是null
- 如果是update ,这个时候会比较 fiber 的memoizedProps(上一次的props) 和 pendingProps (最新的props)是否相等,或者 对应的fiber 是否 有更新lane 判断是否需要 执行copy 还是 new copy fiber 属性实现 diff 算法
| fiber 类型 (通过tag ) | |
|---|---|
| HostFiber | root下对应的主fiber,这个是一开始react 就会创建的fiber ,对应 root.current === hostfiber1.将很多环境变量压入变量堆栈:当前的fiber,当前的container ,当前的context 等2. 从updateQueue 内拿到更新数据,其实就是element (还记得吗,就是render的那个App)3. reconcileChildren 调解children 其实就是创建children 对应的fiber,到这里应该理解了 App function 或者其他 function 组件都对应一个 fiber4. 这里有一个细节 因为是 host fiber ,对应的 alternate 是已经在上面 创建了,所以会执行 reconcileChildFibers ,而不是mount 操作,这个很重要。后面的fiber 在init 的时候 对应的alternate 是 null 会执行 mount 操作,最后的效果是 这些fiber 上面不会有 Placement 标志,不会在commit 阶段执行对应的更新,只需要在 host fiber 上执行一次 html 连接就行了5. 通过element 的$$typeof 创建对应的fiber reconcileSingleElement ,这里是一个对应typeof 的switch 。总的来说就几种类型,单个的 children ,数组类型,还有lazy 等类型,这里简单只说明 单个的,其他的都差不多6. reconcileSingleElement 因为是当个的, 第一次进来不会执行删除操作,如果不是第一次进来 会判断这个 fiber 的current 之前是不是有多一个 兄弟fiber ,如果有执行删除操作(标记为删除,这里不会执行真正的删除)7. 如果是fragment 把这个 fragment 的children 给 创建fiber函数,不是就用当前的 element8.createFiberFromElement , createFiberFromTypeAndProps这里会判断 element 的type 如果是 string (div 等html)对应fiber类型 HostComponent ,如果是Function这里还不好判断是什么类型是 IndeterminateComponent ,会在 执行这个fiber 的时候修改为 class 还是 Function,这里还有其他的类型简单就不说了9. createFiber(fiberTag, pendingProps, key, mode) 用属性创建fiber ,这个返回的就是 对应的next10. 连接 parent fiber 和 next fiber , fiber.return = newFiber11. 因为是host fiber 这里会在 新创建的fiber 设置更新 bit , newFiber.flags != Placement12.返回之后继续 beginWork 循环 |
| HostComponent | 对应 容器类型的 html fiber,所以就是说每个html 标签都会创建一个 fiber,所有在完成代码的时候能少嵌套最好,可以减少内存的使用量,因为fiber 是双缓冲的1. 执行 mountIndeterminateComponent 因为上面创建的fiber 还不知道上面类型 |
-
判断 type 是否有render 函数:对应class 组件,因为都是function组件这里不会执行
-
执行 renderWithHooks 开始执行 function 组件
-
收集上一次 hook执行的所有名称,这里就是为什么react知道hooks 的数量是否正确的地方
-
判断是否是第一次渲染设置全局 hooks 方法(useState,等),第一次渲染和后面的更新 useState 等hook 不是一份代码,第一次渲染使用 HooksDispatcherOnMountInDEV
-
执行 function 组件代码,组件代码是最上面的parent 有一个 hook 这里只说 useState 其他的都简单
-
执行 useState
-
获取全局hooks 变量 HooksDispatcherOnMountInDEV,执行 HooksDispatcherOnMountInDEV.useState(initVal)
-
将当前hook名称入栈,后面更新的时候好判断hook是不是少了还是多了
-
将HooksDispatcherOnMountInDEV全局变量修改为错误的会抛出异常的Hook ,这样你就不能再hook内在执行hook了,这样会抛异常
-
将init val 放入 当前fiber 的memoizedState 内,创建更新queue , 返回 [memoizedState, dispatch.bind(null , currentfiber , queue)]
-
然后你再代码内执行的setState 就是执行 dispatch
-
从 Component 返回,判断返回的是否有render 就能知道是class 还是Function 类型,因为这里是做function 所以这个fiber tag 修改为 FunctionComponent=================== 更新的时候setState 怎么执行 ===============================1. 假设现在执行了setState ,对应 dispatch ,已经bind 了 fiber 和queue
-
先requestUpdateLane 获取这次的更新lane ,同步永远是 SyncLane === 1
-
创建update:包括这次的lane 和 action (setState(action) )
-
插入这次的更新到queue ,这里是为什么多次调用setState 都能一次执行完成的逻辑
-
如果第一次更新queue.pending = update ,update = update.next
-
如果调用了两次update.next = newUpdate , newUpdate.next = update 构成循环指针
-
scheduleUpdateOnFiber ,发布更新 | | HostText | 这个是对底层的fiber ,对应 html 的text 类型,到这里 就不会再返回next了,next === null,接下来就会执行 completeWork |
CompleteWork
这里是深度遍历的回溯,如果在回溯过程中发现当前fiber 还有 sibling fiber (对应创建的时候就是创建数组fiber 类型)没有 执行,会回到上面的循环继续深度遍历,
否则返回到上一级,继续 CompleteWork
| fiber 类型 (通过tag ) | |
|---|---|
| HostText | 1. 对压栈的操作做弹出操作,获取正确的上下文 |
-
用弹出的value ,其中包括container html ,创建 text 对应的 html 标签。同时缓冲数据到html 标签包括fiber 和props (text 没有但是HostComponent 可以保存)
-
保存到fiber , fiber.stateNode = html
-
冒泡属性,这里很重要,在执行更新的是否react 怎么知道很深的一个 fiber 有对应的操作需要执行的,因为fiber在这里会将子集的flags 合并到subTreeFlags 以及lanes 合并到childLanes 包括对应的sigling那么这样没迭代一个fiber 他的这些属性都会保存到对应的 parent ,一次迭代到最顶层。所有最顶层只需要判断一下root 的subTreeFlags和 childLanes 是否有对应标准,然后循环深度优先,广度 遍历fiber 直到 有这个值的fiber 就行了
-
return null ,那么会遍历到parent fiber 执行 completeWork | | HostComponent | 1. 退栈
-
创建对应的html,然后挂载到fiber
-
挂载 子集 html 到新创建的html 实现连接
-
flag 是否有Ref ,掩码操作
-
冒泡属性 | | FunctionComponent | 函数组件没有html ,只需向上冒泡属性 | | HostRoot | 1. 退栈
-
标签flag |= Snapsshot ,后面用这个flag将所有的html 插入到container
-
冒泡属性 |
Commit
commit 主要是在fiber tree 已经创建完成,接下来执行对应的 Effect操作 ,包括 Ref 连接,useLayoutEffect ,发送 useEffect Task 执行 , 删除fiber 等操作
- 先计算 剩余lane ,从 childLanes 获取 ,然后更新 fiber的pendingLanes 如果还有lane 没有完成会发现下一次迭代任务,但是Sync的情况到这不会有剩余的Lane ,同时reset 一开始设置的 eventTimes 和 过期时间等
- 比较HostFiber 的 subtreeFlags 和 flags 是否有 passive 效果 ,这里需要主机这两个属性都是冒泡得到的代码的是子集的效果,然后 passive 不是一个效果而是多个效果合成的,因为是bit 可以有多个bit 组合,
- 如果有passive 会发送一个任务,该任务会 flushPassiveEffects 其实也是useEffect 执行的逻辑,这里发送的是任务,不是微任务。所有不会阻塞浏览器。任务是这次react 并行的关键后面会详细介绍
- 类似上面步骤判断 subtreeFlags 和 flags 是否有 某些操作
- 如果有执行操作:commitBeforeMutationEffects , commitMutationEffects ,commitLayoutEffects
| effect | |
|---|---|
| commitBeforeMutationEffects | 1. 深度优先,广度 遍历fiber tree ,是否有删除,是否 找到冒泡的fiber flag的那个标签,如果找到执行对应逻辑 |
-
删除操作:对于需要删除的fiber 发送 beforeblur 事件
-
遍历到 对应的flag fiber 执行 complete ,只对HostFiber 有操作,删除container下的html | | commitMutationEffects | 1. 深度优先,广度 遍历fiber tree
-
对于删除的fiber 断开 ref 连接,从parent html 删除fiber 对应的html ,执行删除function 的useLayoutEffect destory ,多个 destory 会组成一个链表,只需要循序链表调用即可
-
执行PlaceMent 操作,如果fiber 有插入操作在之类会插入到对应的html,因为init的时候没有所有不会执行
-
对于Component,如果是update 会执行更新props 包括style 等 ,断开ref。如果是Text 执行text 更新。Function ,对顶层的Function的flag 有Placement 标签这里会将之前的顶层html 插入到 container ,然后完成了fiber tree → 到 html tree | | commitLayoutEffects | 1. 深度优先,广度 遍历fiber tree ,
-
执行useLayoutEffect create 方法 | | flushPassiveEffects | 刷新在同步更新的时候会在任务执行1. 需要判断之前的操作是否有effect passive : rootWithPendingPassiveEffects !== null
-
深度优先,广度 遍历fiber tree
-
对有删除操作的fiber ,执行 useEffect destory ,回收fiber 内存,reset 属性 ,删除之前挂载在html 上的fiber 属性,detach ref ,执行useEffect destory
-
执行 useEffect create |
对于有Effect passive 的,会设置 rootWithPendingPassiveEffects ,会在 flushPassiveEffects 任务使用
2.4 react 任务调度
react 的任务调度在package scheduler ,这里执行任务的分发,也是 并行能够执行关键