首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
71 阅读
2
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
52 阅读
3
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
42 阅读
4
如何在 Clash for Windows 上配置服务
40 阅读
5
如何在 IOS Shadowrocket 上配置服务
40 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,061
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1061
篇与
的结果
2024-10-20
React18 源码解析之 beginWork 的操作
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 我们在上一篇文章 React18 源码解析之虚拟 DOM 转为 fiber 树 中只是简单地了解了下 beginWork() 的操作,通过 beginWork()可以将当前 fiber 节点里的 element 转为 fiber 节点。这篇文章我们会详细讲解下,element 转为 fiber 节点的具体实现。beginWork() 源码位置:packages/react-reconciler/src/ReactFiberBeginWork.old.js。 1. 基本操作 # 不同类型的组件,转换成 fiber 节点的过程是不一样的。比如普通的 jsx 结构(即 html 标签类型的)需要通过递归一步步创建;而函数组件类型的,则需要执行该函数,才能得到内部的 jsx 结构,然后再递归转换;类组件类型的,则需要初始化出一个实例,然后调用其内部的 render()方法,才能得到相应的 jsx 结构。beginWork()函数就是根据不同的节点类型(如函数组件、类组件、html 标签、树的根节点等),调用不同的函数,来得到下一个将要处理的 jsx 结构(即 element),然后再将得到的 element 结构解析成 fiber 节点。后续再通过这个新的 fiber 节点,递归后续的 jsx,直到全部遍历完。我们第一次调用时,unitOfWork(即 workInProgress)最初指向的就是树的根节点,这个根节点的类型tag是:HostRoot。以下不同的 fiber 节点属性,会调用不同的方式来得到将要处理的 jsx: HostRoot 类型的,即树的根节点类型的,会把 workInProgress.updateQueue.shared.pending 对应的环形链表中 element 结构,放到 workInProgress.updateQueue.firstBaseUpdate 里,等待后续的执行; FunctionComponent 类型,即函数组件的,会执行这个函数,返回的结果就是 element 结构; ClassComponent 类型的,即类组件的,会得到这个类的实例,然后执行 render()方法,返回的结构就是 element 结构; HostComponent 类型的,即 html 标签类型的,通过children属性,即可得到; 上面不同类型的 fiber 节点都得到了 element 结构,但将 element 转为 fiber 节点时,调用的方式也不一样,如转为文本节点、普通 div 节点、element 为数组转为系列节点、或者 elemen 转为 FunctionComponent 类型的节点等等。beginWork()处理完当前 fiber 节点的 element 结构后,就会到一个这个 element 对应的新的 fiber 节点(若 element 是数组的话,则得到的是 fiber 链表结构的头节点),workInProgress 再指向到这个新的 fiber 节点(workInProgress = next),继续处理。若没有子节点了,workInProgress 就会指向其兄弟元素;若所有的兄弟元素也都处理完了,就返回到其父级节点,查看父级是否有兄弟节点。 2. 判断 workInProgress 是否可以提前退出 # 这里进行了一些简单的判断,判断前后两个 fiber 节点是否有发生变化,若没有变化时,在后续的操作中可以提前结束,或者称之为"剪枝",是一种优化的手段。更具体的流程图可以查看这个: 判断 workInProgress 是否可以提前退出。若 props 没有任何变化,且没有其他的任何更新时,可以提前退出当前的流程,进入到函数 attemptEarlyBailoutIfNoScheduledUpdate()。不过在我们初始渲染阶段,通过 checkScheduledUpdateOrContext() 得到 hasScheduledUpdateOrContext 是 true,但 current.flags & ForceUpdateForLegacySuspense 又为 NoFlags:/** * 判断current的lanes和renderLanes是否有重合,若有则需要更新 * 初始render时,current.lanes和renderLanes是一样的,则返回true */ const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(current, renderLanes); // true (current.flags & ForceUpdateForLegacySuspense) !== NoFlags; // false 因此并不会进入到提前结束的流程(想想也不可能,刚开始构建,怎么就立刻结束呢?),didReceiveUpdate 得到的结果为 false。然后就进入到switch-case阶段了,根据当前 fiber 的不同类型,来调用不同的方法。 3. 根据 fiber 节点的类型进行不同的操作 # 我们在上面也说了,React 中 fiber 节点的类型很多,不过我们主要关注其中的 4 种类型: HostRoot 类型的,即树的根节点类型的; FunctionComponent 类型,即函数组件的; ClassComponent 类型的,即类组件; HostComponent 类型的,即 html 标签类型; workInProgress 初始时指向的是树的根节点,该节点的类型 tag 为HostRoot。从这里开始构建这棵 fiber 树。下面的几个操作,都是为了得到当前 fiber 节点中的 element。大致的流程: 3.1 HostRoot # 当节点类型为 HostRoot 时,会进入到这个分支中,然后执行函数 updateHostRoot()。updateHostRoot(current, workInProgress, renderLanes); 3.1.1 复制 updateQueue 中的属性函数 cloneUpdateQueue # 在函数 updateHostRoot() 中,cloneUpdateQueue()是将 current.updateQueue 中的数据给到 workInProgress.updateQueue:/** * 将current中updateQueue属性中的字段给到workInProgress * @param current * @param workInProgress */ export function cloneUpdateQueue(current: Fiber, workInProgress: Fiber): void { // Clone the update queue from current. Unless it's already a clone. // 将current节点中的update链表克隆给到workInProgress,除非已经克隆过了 const queue: UpdateQueue = (workInProgress.updateQueue: any); const currentQueue: UpdateQueue = (current.updateQueue: any); if (queue === currentQueue) { const clone: UpdateQueue = { baseState: currentQueue.baseState, firstBaseUpdate: currentQueue.firstBaseUpdate, lastBaseUpdate: currentQueue.lastBaseUpdate, shared: currentQueue.shared, effects: currentQueue.effects, }; workInProgress.updateQueue = clone; } } 这里直接在函数内部进行了,并没有返回数据。在 React 中很多地方都是这样,这是用到了 js 中的 对象引用 的特性,即对于数组和 object 类型这两种数据结构而言,当多个变量指向同一个地址时,改变其中变量的值,其他变量的值也会同步更新。因此,在 cloneUpdateQueue() 修改了 workInProgress 的 updateQueue 属性,其实也相应地修改了外部的 fiber 节点。 3.1.2 processUpdateQueue # 函数 processUpdateQueue() 相对来说,功能复杂一些。功能主要是操作 workInProgress 中的 updateQueue 属性,将其中将要进行的更新队列拿出来,串联执行,得到最终的一个结果。在初始 render()阶段,workInProgress.updateQueue.shared.pending 中只有一个 update 节点,这个节点中存放着一个 element 结构,通过一系列的运算后,就可以得到这个 element 结构,然后将其放到了 workInProgress.updateQueue.baseState 中。源码比较长,可以直接 点击链接 去 GitHub 上查看。关于 processUpdateQueue() 函数的详细解读,可以参考这篇文章React18 源码解析之 processUpdateQueue 的执行。我们这里就不展开了。这里要知道的是执行该方法后,初始的 element 结构,已经存放在了 workInProgress.memoizedState 中了。const nextState: RootState = workInProgress.memoizedState; // 若前后两次的element没有变化,则提前退出,直接复用之前的节点 // 而初始时,prevChildren为null,nextChildren为将要更新的element,肯定不相等 if (nextChildren === prevChildren) { return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } /** * nextChildren 为将要转为fiber节点的element结构, * 将得到的fiber结构给到 workInProgress.child */ reconcileChildren(current, workInProgress, nextChildren, renderLanes); 关于函数 reconcileChildren() 如何将 element 转为 fiber 结构,可以参考第 4 节。如上面所说,本第 3 节的内容,都只是根据不同的类型的组件,通过不同的方式获取到 element 结构。具体怎么转换,是在函数 reconcileChildren() 中。 3.2 FunctionComponent # 当节点类型为 FunctionComponent 时,会进入到这个分支中,然后执行函数 updateFunctionComponent()。若 workInProgress 为函数组件,只有执行这个函数,才能得到内部的 jsx。而这个实体函数就放在属性type中。函数组件会涉及到 hooks 的使用,这里我们暂时会直接跳过,不讲解 hooks。const Component = workInProgress.type; // 函数组件时,type即该函数,可以直接执行type() 函数组件的主体就放在属性 type 中,后续执行该 type() 即可。 3.2.1 updateFunctionComponent # 对函数组件进行处理。function updateFunctionComponent(current, workInProgress, Component, nextProps: any, renderLanes) { let nextChildren = renderWithHooks(current, workInProgress, Component, nextProps, context, renderLanes); /** * 若current不为空,且 didReceiveUpdate 为false时, * 执行 bailoutHooks */ if (current !== null && !didReceiveUpdate) { bailoutHooks(current, workInProgress, renderLanes); /** * 优化的工作路径 —— bailout https://juejin.cn/post/7017702556629467167#heading-17 * React 引入了树遍历算法中的常用优化手段 —— “剪枝”,在 React 中又被称作 bailout 。 * 通过 bailout ,某些与本次更新毫无关系的 Fiber 树路径将被直接省略掉;当然, * “省略”并不是直接将这部分 Fiber 节点丢弃,而是直接复用被“省略”的 Fiber 子树的根节点; * 这种“复用”方式,是会保留被“省略”的 Fiber 子树的所有 Fiber 节点的。 */ return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } 可以看到该方法的最后,也是调用了函数 reconcileChildren()。这里的 nextChildren 是通过执行 renderWithHooks() 函数得到的。通过函数名字也可以看出来,在执行函数组件时,需要考虑 hooks 的挂载和执行。不过本篇文章里,我们仅考虑如何获取到函数组件中的 jsx 结构,暂不考虑 hooks 相关的特性。nextChildren = renderWithHooks(current, workInProgress, Component, nextProps, context, renderLanes); 3.2.2 renderWithHooks # 这里我们精简下 renderWithHooks() 中的操作:export function renderWithHooks( current: Fiber | null, workInProgress: Fiber, Component: (p: Props, arg: SecondArg) => any, props: Props, secondArg: SecondArg, nextRenderLanes: Lanes, ): any { renderLanes = nextRenderLanes; currentlyRenderingFiber = workInProgress; // 当前Function Component对应的fiber节点 // 根据是否是初始化挂载,来决定是初始化hook,还是更新hook // 将初始化或更新hook的方法给到 ReactCurrentDispatcher.current 上, // 稍后函数组件拿到的hooks,都是从 ReactCurrentDispatcher.current 中拿到的 ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; /** * 执行 Function Component,将我们写的jsx通过babel编译为element结构,并返回 */ let children = Component(props, secondArg); return children; } 核心的操作就是children = Component(props, secondArg),通过执行该函数,得到内部的 element 结构,即 children,然后返回到 updateFunctionComponent(),再传递给 reconcileChildren() 进行处理。目前只是了解了 element 转为 fiber 的过程,上面的精简版已经够用了。若想了解 hooks 是如何挂载和执行的,可以跳转去:React18 源码解析之 hooks 的挂载。 3.3 ClassComponent # 当节点类型为 ClassComponent 时,会进入到这个分支中,然后执行函数 updateClassComponent()。不过现在函数组件是 React 的趋势,我们不会太深入类组件的各个环节。workInProgress 对应的是类组件时,workInProgress.stateNode 中应当存储的是该类组件的实例。在初始 render()阶段,workInProgress.stateNode 为空,需要调用函数 constructClassInstance() 来创建实例。我们先熟悉下类组件的编写:class App extends React.Component { state = { count: 0, }; handleClick() { this.setState({ count: this.state.count + 1 }); } render() { return ( {this.state.count} click me ); } } 若要渲染该组件,则需要初始化该类的实例,然后调用 render()方法才可以。 3.3.1 constructClassInstance # 该函数主要是用来创建 workInProgress 这个 fiber 节点对应的类组件的实例,同时将创建出来的实例和 workInProgress 节点进行互相绑定。/** * 创建workInProgress对应的类组件的实例,同时将实例和fiber节点进行互相绑定 * @param {Fiber} workInProgress 当前fiber节点 * @param {any} ctor 类组件,可以:new ctor() * @param props * @returns {*} instance 实例 */ function constructClassInstance(workInProgress: Fiber, ctor: any, props: any): any { // 初始化出类的实例,源码这里ctor用了全部小写的方式,不过感觉用Ctor这种可能会更好一些 let instance = new ctor(props, context); // 获取到类组件中的state,放到workInProgress中的memoizedState字段中 const state = (workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null); /** * 将workInProgress和类的实例进行互相绑定 * instance.updater = workInProgress; * workInProgress.stateNode = instance; */ adoptClassInstance(workInProgress, instance); return instance; } 这里只是创建出来了一个实例而已,并没有执行内部任何的方法。创建成功后,我们就可以直接从 workInProgress.stateNode 拿到这个类的实例了,然后再执行其内部的一些生命周期方法和 render()等。 3.3.2 mountClassInstance # 再回到 updateClassComponent(),接着就会执行 mountClassInstance()。这里面会执行一些调用 render()之前的方法和生命周期,如 getDerivedStateFromProps、componentWillMount 等。 componentDidMount 是渲染完成后才会执行的方法,因此这里并不会执行该生命周期。 我们使用函数 constructClassInstance(),保证了后续从 workInProgress.stateNode 中获取实例时,一定是存在的。// 执行渲染之前的一些生命周期函数 function mountClassInstance(workInProgress: Fiber, ctor: any, newProps: any, renderLanes: Lanes): void { const instance = workInProgress.stateNode; // 获取到类组件的实例 instance.props = newProps; instance.state = workInProgress.memoizedState; // 类组件的state instance.refs = emptyRefsObject; // 给类组件对应的fiber节点,初始化一个更新链表: fiber.updateQueue initializeUpdateQueue(workInProgress); const contextType = ctor.contextType; if (typeof contextType === 'object' && contextType !== null) { instance.context = readContext(contextType); } else if (disableLegacyContext) { instance.context = emptyContextObject; } else { const unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); instance.context = getMaskedContext(workInProgress, unmaskedContext); } // 没懂,为什么这里又重新赋值一次? instance.state = workInProgress.memoizedState; /** * https://zh-hans.reactjs.org/docs/react-component.html#static-getderivedstatefromprops * getDerivedStateFromProps 是一个静态方法,会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。 * 它应返回一个对象来更新 state,如果返回 null 则不更新任何内容。 */ const getDerivedStateFromProps = ctor.getDerivedStateFromProps; if (typeof getDerivedStateFromProps === 'function') { applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps); instance.state = workInProgress.memoizedState; } // In order to support react-lifecycles-compat polyfilled components, // Unsafe lifecycles should not be invoked for components using the new APIs. if ( typeof ctor.getDerivedStateFromProps !== 'function' && typeof instance.getSnapshotBeforeUpdate !== 'function' && (typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function') ) { /** * 当 componentWillMount 和 UNSAFE_componentWillMount 已定义时,执行这俩 */ callComponentWillMount(workInProgress, instance); // If we had additional state updates during this life-cycle, let's // process them now. // 执行当前fiber节点的更新链表中的update,不过初始化时,update为空,不需要更新 processUpdateQueue(workInProgress, newProps, instance, renderLanes); instance.state = workInProgress.memoizedState; // 得到最新的state } /** * 我猜的哈: componentDidMount 并不会像上面的方法那样直接执行,而是采用lanes模型来调度 */ if (typeof instance.componentDidMount === 'function') { let fiberFlags: Flags = Update; if (enableSuspenseLayoutEffectSemantics) { fiberFlags |= LayoutStatic; } workInProgress.flags |= fiberFlags; } } 3.3.3 finishClassComponent # 我们再次回到 updateClassComponent() 中,这时就流转到 finishClassComponent() 中了。这里面会调用 render()方法获取到 jsx(即 element 结构),然后调用 reconcileChildren() 将 element 转为 fiber 结构。/** * finishClassComponent()执行render()方法得到element, * 然后调用 reconcileChildren() 得到 workInProgress.child,并返回 * 注意:这里面并没有执行 componentDidMount() 这些生命周期 * @param current * @param workInProgress * @param Component * @param shouldUpdate * @param hasContext * @param renderLanes * @returns {Fiber} */ function finishClassComponent(current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderLanes: Lanes,) { const instance = workInProgress.stateNode; // 类组件的实例 // 类组件,就调用 render() 方法获取 jsx 对应的 element 结构 nextChildren = instance.render(); // 获取到 element 结构后,调用函数 reconcileChildren() 将其转为 workInProgress.child reconcileChildren(current, workInProgress, nextChildren, renderLanes); // Memoize state using the values we just used to render. // render()之后重新存储state的值 workInProgress.memoizedState = instance.state; return workInProgress.child; } 到这里,类组件中的 element 已转为 fiber 节点。 3.4 HostComponent # 当节点类型为 HostComponent 时,说明当前 fiber 节点是原生 html 标签,会进入到这个分支中,然后执行函数 updateHostComponent()。原生 HTML 标签对应的 fiber 节点,获取 element 时就简单很多。直接从 props 中获取 children 属性即可,唯一要注意的就是对文本节点的处理,不过这里我没看懂。/** * 处理html标签的element结构 * @param current * @param workInProgress * @param renderLanes * @returns {Fiber} */ function updateHostComponent(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) { const type = workInProgress.type; // 当前节点的类型, const nextProps = workInProgress.pendingProps; // props,如className, id, children等 const prevProps = current !== null ? current.memoizedProps : null; let nextChildren = nextProps.children; // 判断接下来是否要设置文本了,不过没看懂,若接下来是文本节点,为什么要把 nextChildren 设置为null? // 而且在接下来的 updateHostText() 中,什么也没干 const isDirectTextChild = shouldSetTextContent(type, nextProps); if (isDirectTextChild) { // 若接下要转换的是文本节点,则 // We special case a direct text child of a host node. This is a common // case. We won't handle it as a reified child. We will instead handle // this in the host environment that also has access to this prop. That // avoids allocating another HostText fiber and traversing it. nextChildren = null; } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) { // If we're switching from a direct text child to a normal child, or to // empty, we need to schedule the text content to be reset. workInProgress.flags |= ContentReset; } markRef(current, workInProgress); /** * 对除文本类型之外的其他类型,转为fiber节点 */ reconcileChildren(current, workInProgress, nextChildren, renderLanes); return workInProgress.child; } 这里还得保留一个疑问,目前没看懂对文本类型的处理,接下来是文本节点,为什么要把 nextChildren 设置为 null?而且在接下来的 updateHostText() 中,什么也没干。那么哪个地方处理这个文本内容了。 3.5 IndeterminateComponent # 有同学在 FunctionComponent 中打点时,发现第一次渲染时,各种函数组件并没有进入到那个逻辑里。其实函数类型的组件都进入到 IndeterminateComponent 的类型中了,即不确定类型的组件。为什么用 function 编写的组件,还是"不确定类型"呢?比如以下的几种方式:// function中 return 带有 render() 的obj function App() { return { render() { return function render; }, }; } // render() 在 函数 App() 的prototype上 function App() {} App.prototype.render = () => { return function prototype render; }; // 继承React.Component function App() { return { componentDidMount() { console.log('componentDidMount'); }, render() { return function render; }, }; } App.prototype = React.Component.prototype; // 或 new React.Component() // function 中直接return一个jsx function App() { return function jsx; } 上面的这几种方式,都是用 function 来实现的,但最终的效果是不一样的,不过 React 都是支持的(有的已经不推荐了)。个人猜测,这是因为在 js 中,class 也是可以用 function 来模拟的,有的开发者喜欢用 function 来实现 class。React 为了支持多种书写方式,就得有更多的判断。前面的两种方式,虽然也可以正常运行和输出,但测试环境中,会在控制台输出错误警告,告知开发者用其他的方式来代替,如: 使用 class 继承自 React.Component 来实现,class App extends React.Component {}; 仍使用 function,不过可以用 App.prototype = React.Component.prototype 来完善; 不要使用箭头函数来实现,因为 React 内部会使用new来创建实例; React 内部是怎么判断当前组件,是归到类组件里,还是归到函数组件里呢?// 初始化不确定类型的组件 function mountIndeterminateComponent(_current, // 好奇怪,这里为什么要用下划线开头 workInProgress, Component, renderLanes,) { let value = renderWithHooks(null, workInProgress, Component, props, context, renderLanes); if ( !disableModulePatternComponents && typeof value === 'object' && value !== null && typeof value.render === 'function' && value.$$typeof === undefined ) { // 类组件 workInProgress.tag = ClassComponent; } else { // 函数组件 workInProgress.tag = FunctionComponent; } } 判断执行该函数后的结果 value 是什么类型,若 value 是 Object 类型,且有 render()方法,且没有 $$typeof 属性,表示 value 肯定不是 element 结构,而有 render()方法的对象,则我们认为当前的 workInProgress 是类组件;否则 value 是 element 结构,则认为 workInProgres 是函数组件。 4. 总结 # 我们主要学习了函数 beginWork() 的功能,根据当前 fiber 的类型,用不同的方法获取到下一个应当构建为 fiber 节点的 element。稍后我们会讲解如何把 jsx 转为 fiber 节点。
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 processUpdateQueue 的执行
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 fiber 节点上可能会存在一些在本次调度时需要执行的任务,而且还可能存在上次调度时,优先级不够挪到当前调度的任务。 这些任务如何执行呢? 如何将当前任务和上次的任务进行拼接? 如何筛查出当前调度中优先级低的任务? 这些操作全都是函数 processUpdateQueue() 完成的,源码的位置:packages/react-reconciler/src/ReactUpdateQueue.old.js。我们在之前的 React18 源码解析之 beginWork 的操作 中稍微涉及到了点 processUpdateQueue() 的内容,但并没有展开讲解,这里我们详细说明下。 1. 几个属性的含义 # 我们在讲解任务的执行之前,先明确几个属性的含义,方便我们理解。 1.1 updateQueue 的结构 # 这是 HostFiber 中 updateQueue 的基本结构:export type UpdateQueue = {| baseState: State, // 本次更新前该Fiber节点的state,此后的计算是基于该state计算更新后的state firstBaseUpdate: Update | null, // 上次渲染时遗留下来的低优先级任务会组成一个链表,该字段指向到该链表的头节点 lastBaseUpdate: Update | null, // 该字段指向到该链表的尾节点 shared: SharedQueue, // 本次渲染时要执行的任务,会存放在shared.pending中,这里是环形链表,更新时,会将其拆开,链接到 lastBaseUpdate 的后面 effects: Array | null, // 存放 update.callback 不为null的update |}; 主要涉及到两个链表: 上次渲染时优先级不够的任务链表:每次调度时都会判断当前任务是否有足够的优先级来执行,若优先级不够,则重新存储到链表中,用于下次渲染时重新调度,而 firstBaseUpdate 和 lastBaseUpdate 就是低优先级任务的头指针和尾指针;若 firstBaseUpdate 为 null,说明这可能是第一次渲染,或者上次所有的任务的优先级都足够,全部执行了; 本次要执行的任务:本次渲染时新增的任务,会放到 shared.pending 中,这是一个环形链表,调度前,会将其拆成单向链表,拼接到刚才的链表的后面; 1.2 update 结构 # updateQueue 链表中的每个节点,都是一个 update 结构:const update: Update = { eventTime, // 当前操作的时间 lane, // 优先级 tag: UpdateState, // 执行的操作 /** * 对上一个状态prevState进行操作, * 1. 若payload是函数,则执行它:partialState = payload(prevState);否则 partialState = payload; * 2. 若 partialState 为null,则直接返回; * 3. partialState 与 prevState 进行合并:assign({}, prevState, partialState); */ payload: null, callback: null, next: null, // next指针 }; 接下来是一些对该链表的操作。 2. 初始化链表 initializeUpdateQueue # 该方法就是初始化 fiber 中的 updateQueue 结构,将 fiber 中的初始值fiber.memoizedState给到这个链表的 baseState 中:/** * 初始化一个UpdateQueue,并将 updateQueue 给了 fiber * updateQueue队列是fiber更新时要执行的内容 * @param fiber */ export function initializeUpdateQueue(fiber: Fiber): void { const queue: UpdateQueue = { baseState: fiber.memoizedState, // 前一次更新计算得出的状态,比如:创建时是声明的初始值 state,更新时是最后得到的 state(除去因优先级不够导致被忽略的 Update) firstBaseUpdate: null, // 更新阶段中由于优先级不够导致被忽略的第一个 Update 对象 lastBaseUpdate: null, // 更新阶段中由于优先级不够导致被忽略的最后一个 Update 对象 shared: { pending: null, // 更新操作的循环链表,所有的更新操作都暂时放到这里 interleaved: null, lanes: NoLanes, }, effects: null, }; fiber.updateQueue = queue; } 执行该方法 initializeUpdateQueue(fiber) 后,fiber 节点上就有了 updateQueue 属性了。 3. 添加 update 操作 # 函数 enqueueUpdate(update) 就是用来向链表 fiber.updateQueue.shared.pending 中添加 update 节点的。这个链表是一个循环链表,而且指针指向到该链表的最后一个节点。为什么要做成环形链表? 做成环形链表可以只需要利用一个指针,便能找到最后一个进入的节点和第一个进入的节点; 更加方便地找到最后一个 Update 对象,同时插入新的 Update 对象也非常方便; 如果使用普通的线性链表,就需要同时记录第一个和最后一个节点的位置,维护成本相对较高; /** * 将update节点添加到fiber的updateQueue.shared.pending中 * @param fiber * @param update * @param lane */ export function enqueueUpdate(fiber: Fiber, update: Update, lane: Lane) { const updateQueue = fiber.updateQueue; if (updateQueue === null) { // 只有在fiber已经被卸载了才会出现 // Only occurs if the fiber has been unmounted. return; } const sharedQueue: SharedQueue = (updateQueue: any).shared; if (isInterleavedUpdate(fiber, lane)) { // 省略 } else { const pending = sharedQueue.pending; if (pending === null) { // This is the first update. Create a circular list. /** * 当pending为null时,说明链表中还没有节点,update为第1个节点, * 自己指向自己,最后面的pending指向到update */ update.next = update; } else { /** * 当已经存在节点时,pending指向的是最后一个节点,pending.next是指向的第一个节点, * update.next = pending.next:即update的next指向到了第一个节点, * pending.next = update:即最后一个节点pending的next指针指向到了update节点, * 这样update就进入到链表中了,此时update是链表的最后一个节点了, * 然后下面的 sharedQueue.pending 再指向到租后一个 update 节点 */ update.next = pending.next; pending.next = update; } sharedQueue.pending = update; } } 在插入节点维护一个环形链表时,上操作可能比较绕,需要多理解理解。 4. processUpdateQueue # 这是一个相对来说比较复杂的操作,要考虑任务的优先级和状态的存储。export function processUpdateQueue(workInProgress: Fiber, props: any, instance: any, renderLanes: Lanes): void { // This is always non-null on a ClassComponent or HostRoot // 在 HostRoot和 ClassComponent的fiber节点中,updateQueue不可能为null const queue: UpdateQueue = (workInProgress.updateQueue: any); hasForceUpdate = false; /** * queue.shared.pending本身是一个环形链表,即使有一个节点,也会形成环形链表, * 而且 queue.shared.pending 指向的是环形链表的最后一个节点,这里将其断开形成单向链表 * 单向链表的头指针存放到 firstBaseUpdate 中,最后一个节点则存放到 lastBaseUpdate 中 */ let firstBaseUpdate = queue.firstBaseUpdate; // 更新链表的开始节点 let lastBaseUpdate = queue.lastBaseUpdate; // 更新链表的最后的那个节点 // 检测是否存在将要进行的更新,若存在,则将其拼接到 lastBaseUpdate 的后面,并清空刚才的链表 let pendingQueue = queue.shared.pending; if (pendingQueue !== null) { queue.shared.pending = null; /** * 若pending queue 是一个环形链表,则将第一个和最后一个节点断开, * 环形链表默认指向的是最后一个节点,因此 pendingQueue 指向的就是最后一个节点, * pendingQueue.next(lastPendingUpdate.next)就是第一个节点了 */ const lastPendingUpdate = pendingQueue; // 环形链表的最后一个节点 const firstPendingUpdate = lastPendingUpdate.next; // 环形链表的第一个节点 lastPendingUpdate.next = null; // 最后一个节点与第一个节点断开 /** * 将 pendingQueue 拼接到 更新链表 queue.firstBaseUpdate 的后面 * 1. 更新链表的最后那个节点为空,说明当前更新链表为空,把要更新的首节点 firstPendingUpdate 给到 firstBaseUpdate即可; * 2. 若更新链表的尾节点不为空,则将要更新的首节点 firstPendingUpdate 拼接到 lastBaseUpdate 的后面; * 3. 拼接完毕后,lastBaseUpdate 指向到新的更新链表最后的那个节点; */ // Append pending updates to base queue if (lastBaseUpdate === null) { firstBaseUpdate = firstPendingUpdate; } else { lastBaseUpdate.next = firstPendingUpdate; } lastBaseUpdate = lastPendingUpdate; /** * 若workInProgress对应的在current的那个fiber节点,其更新队列的最后那个节点与当前的最后那个节点不一样, * 则我们将上面「将要更新」的链表的头指针和尾指针给到current节点的更新队列中, * 拼接方式与上面的一样 */ const current = workInProgress.alternate; if (current !== null) { // This is always non-null on a ClassComponent or HostRoot const currentQueue: UpdateQueue = (current.updateQueue: any); const currentLastBaseUpdate = currentQueue.lastBaseUpdate; // 若current更新链表的最后那个节点与当前将要更新的链表的最后那个节点不一样 // 则,把将要更新的链表也拼接到current中 if (currentLastBaseUpdate !== lastBaseUpdate) { if (currentLastBaseUpdate === null) { currentQueue.firstBaseUpdate = firstPendingUpdate; } else { currentLastBaseUpdate.next = firstPendingUpdate; } currentQueue.lastBaseUpdate = lastPendingUpdate; } } } /** * 进行到这里,render()初始更新时,放在 queue.shared.pending 中的update节点(里面存放着element结构), * 就已经放到 queue.firstBaseUpdate 里了, * 因此 firstBaseUpdate 里肯定存放了一个 update 节点,一定不为空,进入到 if 的逻辑中 */ // These values may change as we process the queue. if (firstBaseUpdate !== null) { // Iterate through the list of updates to compute the result. // 迭代更新列表以计算结果 /** * newState 先拿到上次的数据,然后执行 firstBaseUpdate 链表中所有的 update, * 再存储每轮的结果,最后将其给到 workInProgress.memoizedState * 默认值: * { * cache: {controller: AbortController, data: Map(0), refCount: 1} * element: null * isDehydrated: false * pendingSuspenseBoundaries: null * transitions: null * } */ let newState = queue.baseState; // TODO: Don't need to accumulate this. Instead, we can remove renderLanes // from the original lanes. let newLanes = NoLanes; /** * 下次渲染时的初始值 * 1. 若存在低优先级的任务,则该 newBaseState 为第一个低优先级任务之前计算后的值; * 2. 若不存在低优先级的任务,则 newBaseState 为执行完所有任务后得到的值; */ let newBaseState = null; /** * 下面的两个指针用来存放低优先级的更新链表, * 即 firstBaseUpdate 链表中,可能会存在一些优先级不够的update, * 若存在低优先级的update,则将其拼接到 newFirstBaseUpdate 里, * 同时,既然存在低优先级的任务,为了保证整个更新的完整性,也会将已经执行update后的结果,也放到这个新链表中, * 这里存在一个问题,若低优先级任务是中间才出现的,怎么办呢? * 解决方案:将执行到当前update前的state设置为新链表的初始值:newBaseState = newState; */ let newFirstBaseUpdate = null; // 新的更新链表的头指针 let newLastBaseUpdate = null; // 新的更新链表的尾指针 let update = firstBaseUpdate; // 从第1个节点开始执行 do { const updateLane = update.lane; const updateEventTime = update.eventTime; /** * 判断 updateLane 是否是 renderLanes 的子集, * if 这里有个取反的符号,导致理解起来可能有点困难,实际上: * 1. 若 update 的 lane (又名 updateLane) 是 renderLanes 的子集,则执行该update; * 2. 若不是其子集,则将其放到心的队列中,等待下次的执行; */ if (!isSubsetOfLanes(renderLanes, updateLane)) { // Priority is insufficient. Skip this update. If this is the first // skipped update, the previous update/state is the new base // update/state. /** * 若当前 update 的操作的优先级不够。跳过此更新。 * 将该update放到新的队列中,为了保证链式操作的连续性,下面else逻辑中已经可以执行的update,也放到这个队列中, * 这里还有一个问题,从第一个低优先级的任务到最后都已经存储起来了,那新的初始状态是什么呢? * 新的初始状态就是当前跳过的update节点时的那个状态。新的初始状态,只有在第一个跳过任务时才需要设置。 * 例如我们初始状态是0,有10个update的操作,第0个update的操作是+0,第1个update的操作是+1,第2个update的操作是+2,依次类推; * 若第4个update是一个低优先级的操作,其他的都是正常的优先级。 * 那么将第4个update放到新的链表进行存储时,此时要存储的初始值就是执行当前节点前的值,是6(state+0+1+2+3) * 后续的update即使当前已经执行过了,也是要放到新的链表中的,否则更新就会乱掉。 * 下次渲染时,就是以初始state为6,+4的那个update开始,重新判断优先级 */ const clone: Update = { eventTime: updateEventTime, lane: updateLane, tag: update.tag, payload: update.payload, callback: update.callback, next: null, }; // 拼接低优先级的任务 if (newLastBaseUpdate === null) { // 还没有节点,这clone就是头结点 // 并将此时的 newState 放到新的 newBaseState中 newFirstBaseUpdate = newLastBaseUpdate = clone; newBaseState = newState; } else { // 已经有节点了,直接向后拼接 newLastBaseUpdate = newLastBaseUpdate.next = clone; } // Update the remaining priority in the queue. newLanes = mergeLanes(newLanes, updateLane); } else { // This update does have sufficient priority. // 此更新具有足够的优先级 // 初始render()时会走这里 if (newLastBaseUpdate !== null) { /** * 若存储低优先级的更新链表不为空,则为了操作的完整性,即使当前update会执行, * 也将当前的update节点也拼接到后面, * 但初始render()渲染时,newLastBaseUpdate为空,走不到 if 这里 */ const clone: Update = { eventTime: updateEventTime, // This update is going to be committed so we never want uncommit // it. Using NoLane works because 0 is a subset of all bitmasks, so // this will never be skipped by the check above. /** * 翻译:这次update将要被提交更新,因此后续我们不希望取消这个提交。 * 使用 NoLane 这个是可行的,因为0是任何掩码的子集, * 所以上面 if 的检测`isSubsetOfLanes(renderLanes, updateLane)`,永远都会为真, * 该update永远不会被作为低优先级进行跳过,每次都会执行 */ lane: NoLane, tag: update.tag, payload: update.payload, callback: update.callback, next: null, }; // 拼接到低优先级链表的后面 newLastBaseUpdate = newLastBaseUpdate.next = clone; } // Process this update. /** * render()时 newState 的默认值: * { * cache: {controller: AbortController, data: Map(0), refCount: 1} * element: null * isDehydrated: false * pendingSuspenseBoundaries: null * transitions: null * } * 执行 getStateFromUpdate() 后,则会将 update 中的 element 给到 newState 中 */ newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance); const callback = update.callback; if ( callback !== null && // If the update was already committed, we should not queue its // callback again. update.lane !== NoLane ) { workInProgress.flags |= Callback; const effects = queue.effects; if (effects === null) { queue.effects = [update]; } else { effects.push(update); } } } update = update.next; // 初始render()时,只有一个update节点,next为null,直接break,跳出循环 if (update === null) { /** * 在上面将 queue.shared.pending 放到firstBaseUpdate时, * queue.shared.pending就已经重置为null了 * @type {Update|null|*} */ pendingQueue = queue.shared.pending; if (pendingQueue === null) { break; } else { // An update was scheduled from inside a reducer. Add the new // pending updates to the end of the list and keep processing. /** * 猜的,在优先级调度过程中,又有了新的更新到来,则此时再拼接到更新队列的后面,接着循环处理 */ const lastPendingUpdate = pendingQueue; // Intentionally unsound. Pending updates form a circular list, but we // unravel them when transferring them to the base queue. const firstPendingUpdate = ((lastPendingUpdate.next: any): Update); lastPendingUpdate.next = null; update = firstPendingUpdate; queue.lastBaseUpdate = lastPendingUpdate; queue.shared.pending = null; } } } while (true); if (newLastBaseUpdate === null) { // 若没有任意的低优先级的任务呢,则将一串的update执行后的结果,就是新的 baseState, // 若有低优先级的任务,则已经在上面设置过 newBaseState 了,就不能在这里设置了 newBaseState = newState; } queue.baseState = ((newBaseState: any): State); // 下次更新时,要使用的初始值 queue.firstBaseUpdate = newFirstBaseUpdate; queue.lastBaseUpdate = newLastBaseUpdate; /** * 经过上面的操作,queue(即 workInProgress.updateQueue )为: * baseState: { element: element结构, isDehydrated: false } * effects: null, * firstBaseUpdate: null, * lastBaseUpdate: null, * shared: { pending: null, interleaved: null, lanes: 0 } */ // workInProgress.updateQueue的数据结构: https://mat1.gtimg.com/qqcdn/tupload/1659687672451.png // Interleaved updates are stored on a separate queue. We aren't going to // process them during this render, but we do need to track which lanes // are remaining. const lastInterleaved = queue.shared.interleaved; if (lastInterleaved !== null) { let interleaved = lastInterleaved; do { newLanes = mergeLanes(newLanes, interleaved.lane); interleaved = ((interleaved: any).next: Update); } while (interleaved !== lastInterleaved); } else if (firstBaseUpdate === null) { // `queue.lanes` is used for entangling transitions. We can set it back to // zero once the queue is empty. queue.shared.lanes = NoLanes; } // Set the remaining expiration time to be whatever is remaining in the queue. // This should be fine because the only two other things that contribute to // expiration time are props and context. We're already in the middle of the // begin phase by the time we start processing the queue, so we've already // dealt with the props. Context in components that specify // shouldComponentUpdate is tricky; but we'll have to account for // that regardless. markSkippedUpdateLanes(newLanes); workInProgress.lanes = newLanes; workInProgress.memoizedState = newState; // 存储本次最新的结果 } } 我们再总结梳理下函数 processUpdateQueue() 里的操作: 将当前将要进行的更新 shared.pending 的环形链表,拆开拼接到到 lastBaseUpdate 的后面; 执行 firstBaseUpdate 链表的操作时,若当前 update 对应的任务的优先级符合要求,则执行;若优先级较低,则存储执行到当前节点的状态,做为下次渲染时的初始值,和接下来所有的 update 节点; 将执行所有操作后得到的 newState 重新给到 workInProgress.memoizedState;然后存储刚才淘汰下来的低优先级任务的链表,以便下次更新; 我们在上一篇文章 React18 源码解析之 beginWork 的操作 中,树的根节点是 HostRoot 类型,会调用 processUpdateQueue() 函数。我们在了解其内部的调度后,就更加清晰了。初始时,workInProgress.updateQueue.shared.pending 中只有一个 update 节点,这个节点中存放着一个 element 结构。 初始的 baseState 为 { element: null },我们暂时忽略其他属性; 把 shared.pending 中的 update 节点放到 firstBaseUpdate 的链表中; 任务优先级的调度,我们在初始 render()阶段时,所有任务的优先级都是 DefaultLane,即不会跳过任何一个任务; 所有的 update 都执行完毕后,会再执行一条:workInProgress.memoizedState = newState; // workInProgress.memoizedState = { element }; 执行 processUpdateQueue() 完毕后,workInProgress 节点的 memoizedState 属性上,就已经挂载 element 结构了。 5. 对上一个状态 prevState 进行操作 # 函数 getStateFromUpdate(),可以调用 update 节点中的 payload ,对上一状态 prevState 进行处理。根据 update.tag 也是区分了几种情况: ReplaceState:直接舍弃掉旧状态,返回更新后的新状态; UpdateState:新状态和旧状态的数据合并后再返回; ForceUpdate:只修改 hasForceUpdate 为 true,返回的还是旧状态; function getStateFromUpdate( workInProgress: Fiber, queue: UpdateQueue, update: Update, prevState: State, nextProps: any, instance: any, ): any { /** * 可以看到下面也是区分了几种情况 * 1. ReplaceState:舍弃掉旧状态,直接用新状态替换到旧状态; * 2. UpdateState:新状态和旧状态的数据合并后再返回; * 3. ForceUpdate:只修改 hasForceUpdate 为true,不过返回的还是旧状态; */ switch (update.tag) { case ReplaceState: { const payload = update.payload; if (typeof payload === 'function') { // Updater function // 若payload是function,则将prevState作为参数传入,执行payload() // 直接返回该函数执行后的结果(不再与之前的数据进行合并) const nextState = payload.call(instance, prevState, nextProps); return nextState; } // 若不是function类型,则传入什么,返回什么 // State object return payload; } case CaptureUpdate: { workInProgress.flags = (workInProgress.flags & ~ShouldCapture) | DidCapture; } // Intentional fallthrough case UpdateState: { const payload = update.payload; let partialState; // 用于存储计算后的新state结果,方便最后进行assign合并处理 if (typeof payload === 'function') { // Updater function // 若payload是function,则将prevState作为参数传入,执行payload() partialState = payload.call(instance, prevState, nextProps); } else { // Partial state object // 若 payload 是变量,则直接赋值 partialState = payload; } if (partialState === null || partialState === undefined) { // Null and undefined are treated as no-ops. // 若得到的结果是null或undefined,则返回之前的数据 return prevState; } // Merge the partial state and the previous state. // 与之前的state数据进行合并 return assign({}, prevState, partialState); } case ForceUpdate: { hasForceUpdate = true; return prevState; } } return prevState; } update.payload的类型不一样,执行的操作也不一样: payload 为 function 类型:执行该函数 payload(prevState),然后再处理后续的结果; payload 为 其他类型:我们认为是新的状态,直接使用; 6. 总结 # 我们主要学习了 fiber 节点中关于链表任务的调度和执行,后续涉及到 hooks 时,也会有类似的操作。
2024年10月20日
6 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 reconcileChildren 生成 fiber 的过程
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 React 中维护着两棵 fiber 树,一棵是正在展示的 UI 对应的那棵树,我们称为 current 树;另一棵通过更新,将要构建的树。那么在更新的过程中,是通过怎么样的对比过程,来决定是复用之前的节点,还是创建新的节点。 1. reconcileChildren # 函数 reconcileChildren() 是一个入口函数,这里会根据 current 的 fiber 节点的状态,分化为 mountChildFibers() 和 reconcileChildFibers()。/** * 调和,创建或更新fiber树 * 若current的fiber节点为null,调用 mountChildFibers 初始化 * 若current不为空,说明要得到一棵新的fiber树,执行 reconcileChildFibers() 方法 * @param current 当前树中的fiber节点,可能为空 * @param workInProgress 将要构建树的fiber节点 * @param nextChildren 将要构建为fiber节点的element结构 * @param renderLanes 当前的渲染优先级 */ export function reconcileChildren(current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes) { if (current === null) { /** * mount阶段,这是一个还未渲染的全新组件,我们不用通过对比最小副作用来更新它的子节点。 * 直接转换nextChildren即可,不用标记哪些节点需要删除等等 */ workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes); } else { /** * 若current不为null,则需要进行的工作: * 1. 判断之前的fiber节点是否可以复用; * 2. 若不能复用,则需要标记删除等; */ workInProgress.child = reconcileChildFibers( workInProgress, /** * 因为我们要构建的是workInProgress的子节点,这里也传入current的子节点, * 方便后续的对比和复用 */ current.child, nextChildren, renderLanes, ); } } 再看下这两个函数的区别:export const reconcileChildFibers = ChildReconciler(true); // 需要收集副作用 export const mountChildFibers = ChildReconciler(false); // 不用追踪副作用 这两个函数都是 ChildReconciler() 生成,只是参数不一样。可见这两个函数就区别在是否要追踪 fiber 节点的副作用。 2. ChildReconciler # ChildReconciler(shouldTrackSideEffects) 只有一个参数,并返回的是一个函数。/** * 子元素协调器,即把当前fiber节点中的element结构转为fiber节点 * @param {boolean} shouldTrackSideEffects 是否要追踪副作用,即我们本来打算复用之前的fiber节点,但又复用不了,需要给该fiber节点打上标记,后续操作该节点 * @returns {function(Fiber, (Fiber|null), *, Lanes): *} 返回可以将element转为fiber的函数 */ function ChildReconciler(shouldTrackSideEffects) { // 暂时省略其他代码 function reconcileChildFibers(returnFiber: Fiber, // 当前 Fiber 节点,即 workInProgress currentFirstChild: Fiber | null, // current 树上对应的当前 Fiber 节点的第一个子 Fiber 节点,mount 时为 null,主要是为了是否能复用之前的节点 newChild: any, // returnFiber中的element结构,用来构建returnFiber的子节点 lanes: Lanes, // 优先级相关): Fiber | null { // 省略 return deleteRemainingChildren(returnFiber, currentFirstChild); } return reconcileChildFibers; } 当 current 对应的 fiber 节点为 null 时,那它就没有子节点,也无所谓复用和删除的说法,直接按照 workInProgress 里的 element 构建新的 fiber 节点即可,这时,是不用收集副作用的。若 current 对应的 fiber 节点不为 null 时,那么就把 current 的子节点拿过来,看看是否有能复用的节点,有能复用的节点就直接复用;不能复用的,比如类型发生了改变的(div 标签变成了 p 标签),新结构里已经没有该 fiber 节点了等等,都是要打上标记,后续在 commit 阶段进行处理。 3. reconcileChildFibers # 函数 reconcileChildFibers() 不做实际的操作,仅是根据 element 的类型,调用不同的方法来处理,相当于一个路由分发。/** * 将returnFiber节点(即当前的workInProgress对应的节点)里的element结构转为fiber结构 * @param returnFiber 当前的workInProgress对应的fiber节点 * @param currentFirstChild current 树上对应的当前 Fiber 节点的第一个子 Fiber 节点,可能为null * @param newChild returnFiber中的element结构,用来构建returnFiber的子节点 * @param lanes * @returns {Fiber|*} */ function reconcileChildFibers(returnFiber: Fiber, // 当前 Fiber 节点,即 workInProgress currentFirstChild: Fiber | null, newChild: any, lanes: Lanes, // 优先级相关): Fiber | null { // 是否是顶层的没有key的fragment组件 const isUnkeyedTopLevelFragment = typeof newChild === 'object' && newChild !== null && newChild.type === REACT_FRAGMENT_TYPE && newChild.key === null; // 若是顶层的fragment组件,则直接使用其children if (isUnkeyedTopLevelFragment) { newChild = newChild.props.children; } // Handle object types // 判断该节点的类型 if (typeof newChild === 'object' && newChild !== null) { /** * newChild是Object,再具体判断 newChild 的具体类型。 * 1. 是普通React的函数组件、类组件、html标签等 * 2. portal类型; * 3. lazy类型; * 4. newChild 是一个数组,即 workInProgress 节点下有并排多个结构,这时 newChild 就是一个数组 * 5. 其他迭代类型,我暂时也不确定这哪种? */ switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: // 一般的React组件,如或等 return placeSingleChild( // 调度单体element结构的元素 reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes), ); case REACT_PORTAL_TYPE: return placeSingleChild(reconcileSinglePortal(returnFiber, currentFirstChild, newChild, lanes)); case REACT_LAZY_TYPE: const payload = newChild._payload; const init = newChild._init; // TODO: This function is supposed to be non-recursive. return reconcileChildFibers(returnFiber, currentFirstChild, init(payload), lanes); } if (isArray(newChild)) { // 若 newChild 是个数组 return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes); } if (getIteratorFn(newChild)) { return reconcileChildrenIterator(returnFiber, currentFirstChild, newChild, lanes); } throwOnInvalidObjectType(returnFiber, newChild); } if ((typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number') { // 文本节点 return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild, lanes)); } // Remaining cases are all treated as empty. // 若没有匹配到任何类型,说明当前newChild无法转为fiber节点,如boolean类型,null等是无法转为fiber节点的 // deleteRemainingChildren()的作用是删除 returnFiber 节点下,第2个参数传入的fiber节点,及后续所有的兄弟节点 // 如 a->b->c->d-e,假如我们第2个参数传入的是c,则删除c及后续的d、e等兄弟节点, // 而这里,第2个参数传入的是 currentFirstChild,则意味着删除returnFiber节点下所有的子节点 // 为什么要删除呢?这是因为,为了保证前后两棵树是一致的,若jsx在workInProgress所在树中,无法转为fiber节点, // 说明 returnFiber 下所有的fiber节点均无法复用 return deleteRemainingChildren(returnFiber, currentFirstChild); } 我们先来看下源码 reconcileChildFibers() 中都判断了 newChild 的哪些类型: 是否是顶层的 fragment 元素,如在执行 render()时,用的是 fragment 标签( 或 )包裹,则表示该元素顶级的 fragment 组件,这里直接使用其 children; 合法的 ReactElement,如通过 createElement、creatPortal 等创建创建的元素,只是 $$typeof 不一样;这里也把 lazy type 归类到了这里; 普通数组,每一项都是合法的其他元素,如一个 div 标签下有并列的 span 标签、函数组件、纯文本等,这些在 jsx 转换过程中,会形成数组; Iterator,跟数组类似,只是遍历方式不同; string 或 number 类型:如(abc)里的 abc 即为字符串类型的文本(外层的div节点依然是html标签,在React中会把div标签和abc分成两部分进行处理); 函数 reconcileChildFibers() 只处理 workInProgress 节点里的 element 结构,无论 element 是一个节点,还是一组节点,只会把这一层的节点都进行转换。若 element 中对应的只有一个 fiber 节点,那就返回这个节点,若是一组数据,则会形成一个 fiber 单向链表,然后返回这个链表的头节点。源码的注释里也明确说了,reconcileChildFibers()不是递归函数,他只处理当前层级的数据。如果还有印象的话,我们在之前讲解的函数performUnitOfWork(),他本身就是一个连续递归的操作。整个流程的控制权在那里。function performUnitOfWork(unitOfWork: Fiber): void { const current = unitOfWork.alternate; let next; // 若 next 是fiber节点,则 workInProgress 指向到该新的fiber节点,继续处理其内部的jsx // 若 next 为 null,说明当前所有的结构均已处理完毕,completeUnitOfWork()判断是去处理兄弟节点,还是返回到上级节点 next = beginWork(current, unitOfWork, subtreeRenderLanes); unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // unitOfWork已经是最内层的节点了,没有子节点了 // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork); } else { workInProgress = next; } } 这里我们主要讲解一般的 React 类型 REACT_ELEMENT_TYPE,数组类型和普通文本类型的 element 的构建。reconcileChildFibers()函数的整体思想就是复用,复用,复用,能复用之前 fiber 节点的,绝不创建新的 fiber 节点。只不过,之前的 fiber 节点是否可以复用,复用哪个 fiber 节点,情况比较复杂,接下来我们一一讲解下。 4. 单体 element 结构的元素 reconcileSingleElement # 若 element 中只对应一个元素,且是普通 React 的函数组件、类组件、html 标签等类型,那我们调用 reconcileSingleElement() 来处理。 判断是否可以复用之前的节点,复用节点的标准是 key 一样、类型一样,任意一个不一样,都无法复用; 新要构建的节点是只有一个节点,但之前不一定只有一个节点,比如之前是多个 li 标签,新 element 中只有一个 li 标签; 若无法复用之前的节点,那之前的节点也没留着的必要了,则将之前的节点删除,创建一个新的节点。这里我们说的复用节点,指的是复用current.alternate的那个节点,因为在没有任何更新时,两棵 fiber 树是一一对应的。在产生更新后,可能就会存在对应不上的情况,因此才有了下面的各种 diff 对比环节。 4.1 对比判断是否有可复用的节点 # 在对比过程中,采用了循环的方式,我们知道同一 fiber 节点下,所有同一级别的子 fiber 节点是横向单向链表,串联起来的。而且,虽然新节点是单个节点,但却无法保证之前的节点也是单个节点,因此这里从第 1 个子 fiber 节点开始,查找第一个 key 和节点类型都一样的节点。若找到,进行复用;若找不到,则删除之前所有的子节点,创建新的 fiber 节点。/** * 单个普通ReactElement的构建 * @param returnFiber * @param currentFirstChild * @param element * @param lanes * @returns {Fiber} */ function reconcileSingleElement(returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, lanes: Lanes,): Fiber { // element是workInProgress中的,表示正在构建中的 const key = element.key; // child: 当前正在对比的child,初始时是第1个子节点 let child = currentFirstChild; // 新节点是单个节点,但无法保证之前的节点也是单个节点, // 这里用循环查找第一个 key和节点类型都一样的节点,进行复用 while (child !== null) { // TODO: If key === null and child.key === null, then this only applies to // the first item in the list. // 比较 key 值是否有变化,这是复用 Fiber 节点的先决条件 // 若找到 key 一样的节点,即使 key 都为 null,那也是节点一样 // 注意 key 为 null 我们也认为是相等,因为单个节点没有 key 也是正常的 if (child.key === key) { const elementType = element.type; if (elementType === REACT_FRAGMENT_TYPE) { // 复用之前的fiber节点,整体在下面 } // Didn't match. // 若key一样,但节点类型没有匹配上,无法直接复用,则直接删除该节点和其兄弟节点,停止循环, // 开始走while后面的创建新fiber节点的逻辑 deleteRemainingChildren(returnFiber, child); break; } else { // 若key不一样,不能复用,标记删除当前单个child节点 deleteChild(returnFiber, child); } child = child.sibling; // 指针指向下一个sibling节点,检测是否可以复用 } // 上面的一通循环没找到可以复用的节点,则接下来直接创建一个新的fiber节点 if (element.type === REACT_FRAGMENT_TYPE) { // 若新节点的类型是 REACT_FRAGMENT_TYPE,则调用 createFiberFromFragment() 方法创建fiber节点 // createFiberFromFragment() 也是调用的createFiber(),第1个参数指定fragment类型 // 然后再调用 new FiberNode() 创建一个fiber节点实例 const created = createFiberFromFragment(element.props.children, returnFiber.mode, lanes, element.key); created.return = returnFiber; // 新节点的return指向到父级节点 // 额外的,fragment元素没有ref return created; } else { // 若新节点是其他类型,如普通的html元素、函数组件、类组件等,则会调用 createFiberFromElement() // 这里面再接着调用 createFiberFromTypeAndProps(),然后判断element的type是哪种类型 // 然后再调用对应的create方法创建fiber节点 // 有心的同学可能已经发现,这里用了一个else,但实际上if中已经有return了,这里就用不到else了,可以去提pr了! const created = createFiberFromElement(element, returnFiber.mode, lanes); created.ref = coerceRef(returnFiber, currentFirstChild, element); // 处理ref created.return = returnFiber; return created; } } 如何复用之前的 fiber 节点?我们知道 fragment 标签 没有什么意义,仅仅是为了聚合内容,而且 fragment 标签也是可以设置 key 的。fragment 标签与其他标签是不一样的,因此这里单独进行了处理:// 将要构建的是 fragment 类型,然后在之前的节点里找到一个 fragment 类型的 if (child.tag === Fragment) { /** * deleteRemainingChildren(returnFiber, fiber); // 删除当前fiber及后续所有的兄弟节点 */ deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用的fiber节点,从下一个节点开始全部删除 /** * useFiber是将当前可以复用的节点和属性传入,然后复制合并到workInProgress上 * @type {Fiber} */ const existing = useFiber(child, element.props.children); // 该节点是fragment类型,则复用其children existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点 // 多说一句,fragment类型的fiber没有ref属性,这里不用处理 return existing; } else { // 其他类型,如REACT_ELEMENT_TYPE, REACT_LAZY_TYPE等 if ( child.elementType === elementType || // Keep this check inline so it only runs on the false path: (__DEV__ ? isCompatibleFamilyForHotReloading(child, element) : false) || // Lazy types should reconcile their resolved type. // We need to do this after the Hot Reloading check above, // because hot reloading has different semantics than prod because // it doesn't resuspend. So we can't let the call below suspend. (typeof elementType === 'object' && elementType !== null && elementType.$$typeof === REACT_LAZY_TYPE && resolveLazy(elementType) === child.type) ) { /** * deleteRemainingChildren(returnFiber, fiber); // 删除当前fiber及后续所有的兄弟节点 */ deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用的fiber节点,从下一个节点开始全部删除 const existing = useFiber(child, element.props); // 复用child节点和element.props属性 existing.ref = coerceRef(returnFiber, child, element); // 处理ref existing.return = returnFiber; // 重置新Fiber节点的return指针,指向当前Fiber节点 return existing; } } 这里可能会有人有疑问,deleteRemainingChildren() 只删除后续的节点,那前面的节点怎么办呢?前面的节点已经在 while 循环中的 else 逻辑里,把匹配不上的节点标记为删除了。从这里也能看到,我们在 React 组件的状态变更时,尽量不要修改元素的标签类型,否则当前元素对应的 fiber 节点及所有的子节点都会被丢弃,然后重新创建。如// 原来的 function App() { return ( ); } // 经 useState() 修改后的 function App() { return ( ); } 虽然只是外层的 div 标签变成了 section 标签,内部的都没有变化,但 React 在进行对比时,还是认为没有匹配上,然后把 div 对应的 fiber 节点及所有的子节点都删除了,重新从 section 标签开始构建新的 fiber 节点。 4.2 复用之前的节点 # 若在循环的过程中,找到了可复用的 fiber 节点。deleteRemainingChildren(returnFiber, child.sibling); // 已找到可复用的child节点,从下一个节点开始全部删除 const existing = useFiber(child, element.props); // 复用匹配到的child节点,并使用element中新的props属性 existing.ref = coerceRef(returnFiber, child, element); // 处理ref existing.return = returnFiber; // 复用的Fiber节点的return指针,指向当前Fiber节点 在已经找到可以复用的 child 节点后,child 节点后续的节点就都可以删除了。我们再看下 useFiber() 中是如何复用 child 这个节点的:function useFiber(fiber: Fiber, pendingProps: mixed): Fiber { // We currently set sibling to null and index to 0 here because it is easy // to forget to do before returning it. E.g. for the single child case. // 将新的 fiber 节点的 index 设置为0,sibling 设置为null, // 因为目前我们还不知道这个节点用来干什么,比如他可能用于单节点的 case 中 const clone = createWorkInProgress(fiber, pendingProps); clone.index = 0; clone.sibling = null; return clone; } /** * 说是复用current节点,其实是复用 current.alternate 的那个节点, * 因为 current 和 workInProgress 两个节点是通过 alternate 属性互相指向的 * @param current * @param pendingProps * @returns {Fiber} */ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { /** * 翻译:我们使用双缓冲池技术,因为我们知道我们最多只需要两个版本的树。 * 我们可以汇集其他未使用的节点,进行自由的重用。 * 这是惰性创建的,以避免为从不更新的对象分配额外的对象。 * 它还允许我们在需要时回收额外的内存 */ // 若 workInProgress 为 null,则直接创建一个新的fiber节点 workInProgress = createFiber( current.tag, pendingProps, // 传入最新的props current.key, current.mode, ); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; // workInProgress 和 current通过 alternate 属性互相进行指向 workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; // 设置新的props // Needed because Blocks store data on type. workInProgress.type = current.type; // We already have an alternate. // Reset the effect tag. workInProgress.flags = NoFlags; // The effects are no longer valid. workInProgress.subtreeFlags = NoFlags; workInProgress.deletions = null; } // Reset all effects except static ones. // Static effects are not specific to a render. workInProgress.flags = current.flags & StaticMask; workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; // Clone the dependencies object. This is mutated during the render phase, so // it cannot be shared with the current fiber. const currentDependencies = current.dependencies; workInProgress.dependencies = currentDependencies === null ? null : { lanes: currentDependencies.lanes, firstContext: currentDependencies.firstContext, }; // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; return workInProgress; } 整个 React 应用中,我们维护着两棵树,其实每棵树没啥差别,FiberRootNode 节点中的 current 指针指向到哪棵树,就展示那棵树。只不过是我们把当前正在展示的那棵树叫做 current,将要构建的那个叫做 workInProgress。这两棵树中互相的两个节点,通过 alternate 属性进行互相的指向。 4.3 普通 React 类型 element 转为 fiber # 将单个普通 React 类型的 element 转为 fiber 节点,是 createFiberFromElement(),其又调用了 createFiberFromTypeAndProps()。这里将其进行了细致的划分,如 类组件 ClassComponent,普通 html 标签 HostComponent,strictMode 等export function createFiberFromTypeAndProps(type: any, // React$ElementType,element的类型 key: null | string, pendingProps: any, owner: null | Fiber, mode: TypeOfMode, lanes: Lanes,): Fiber { let fiberTag = IndeterminateComponent; // 我们还不知道当前fiber是什么类型 // The resolved type is set if we know what the final type will be. I.e. it's not lazy. // 如果我们知道最终类型type将是什么,则设置解析的类型。 let resolvedType = type; if (typeof type === 'function') { // 当前是函数组件或类组件 if (shouldConstruct(type)) { // 类组件 fiberTag = ClassComponent; } else { // 还是不明确是什么类型的组件,啥也没干 } } else if (typeof type === 'string') { // type是普通的html标签,如div, p, span等 fiberTag = HostComponent; } else { // 其他类型,如fragment, strictMode等,暂时省略 } // 通过上面的判断,得到 fiber 的类型后,则调用 createFiber() 函数,生成 fiber 节点 const fiber = createFiber(fiberTag, pendingProps, key, mode); fiber.elementType = type; // fiber中的 elmentType 与 element 中的 type 一样, fiber.type = resolvedType; // 测试环境会做一些处理,正式环境与 elementType 属性一样,type 为 REACT_LAZY_TYPE,resolveType 为 null fiber.lanes = lanes; return fiber; } 我们在之前讲解函数 beginWork() 时,当 fiber 节点没明确类型时,判断过 fiber 节点的类型,那时候是执行 fiber 节点里的 function,根据返回值来判断的。这里就不能再执行 element 中的函数了,否则会造成多次执行。如当用函数来实现一个类组件时:function App() {} App.prototype = React.Component.prototype; // React.Component Component.prototype.isReactComponent = {}; 可以看到,只要函数的 prototype 上有 isReactComponent 属性,他就肯定是类组件。但若没有这个属性,也不一定就会是函数组件,还得通过执行后的结果来判断(就是之前 beginWork()里的步骤了)。React 源码中,采用了shouldConstruct(type)来判断。/** * 判断用函数实现的组件是否是类组件 * @param Component * @returns {boolean} */ function shouldConstruct(Component: Function) { /** * 类组件都是要继承 React.Component 的,而 React.Component 的 prototype 上有一个 isReactComponent 属性,值为{} * 文件地址在: https://github.com/wenzi0github/react/blob/1cf8fdc47b360c1f1a079209fc4d49026fafd8a4/packages/react/src/ReactBaseClasses.js#L30 * 因此只要判断 Component.prototype 上是否有 isReactComponent 属性,即可判断出当前是类组件还是函数组件 */ const prototype = Component.prototype; return !!(prototype && prototype.isReactComponent); } 我们再来汇总梳理下类型的判断: 若 element.type 是函数,则再通过 shouldConstruct() 判断,若明确类型是类组件,则 fiberTag 为 ClassComponent;若不是类组件,则还是认为他是未知组件的类型 IndeterminateComponent,后续再通过执行的结果判断; 若 element.type 是字符串,则认为是 html 标签的类型 HostComponent; 若是其他的类型,如 REACT_FRAGMENT_TYPE, REACT_SUSPENSE_TYPE 等,则单独调用对应的方法创建 fiber 节点; 若是前两种类型的,则会调用 createFiber() 创建新的 fiber 节点:/** * 通过上面的判断,得到fiber的类型后,则调用createFiber()函数,生成fiber节点。 * createFiber()内再执行 `new FiberNode()` 来初始化出一个fiber节点。 */ const fiber = createFiber(fiberTag, pendingProps, key, mode); fiber.elementType = type; // fiber中的elmentType与element中的type一样, fiber.type = resolvedType; // 测试环境会做一些处理,正式环境与elementType属性一样,type为 REACT_LAZY_TYPE,resolveType为null fiber.lanes = lanes; 无论是复用之前的节点,还是新创建的 fiber 节点,到这里,我们总归是把 element 结构转成了 fiber 节点。 5. 处理单个的文本节点 reconcileSingleTextNode # 文本节点处理起来相对来说比较简单,它本身就是一个字符串或者数字,没有 key,没有 type。this is text in p tag 1234567in spanabcdef 如上面的样例中,this is text in p tag就是单个的文本节点,但第 2 个 p 节点中,虽然123456, abcdef也是文本节点,不过这并不是单独的文本节点,而是与 span 标签组成了一个数组(span 标签里的in span也是单个的文本节点):这种情况,我们会在第 6 节中进行说明,这里我们只处理单独的文本节点(第 1 行 p 标签里的文本)。// 调度文本节点 function reconcileSingleTextNode(returnFiber: Fiber, currentFirstChild: Fiber | null, textContent: string, lanes: Lanes,): Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. // 这里不再判断文本节点的key,因为文本节点就来没有key,也没有兄弟节点 if (currentFirstChild !== null && currentFirstChild.tag === HostText) { // We already have an existing node so let's just update it and delete // the rest. // 若当前第1个子节点就是文本节点,则直接删除后续的兄弟节点 deleteRemainingChildren(returnFiber, currentFirstChild.sibling); const existing = useFiber(currentFirstChild, textContent); // 复用这个文本的fiber节点,重新赋值新的文本 existing.return = returnFiber; return existing; } // The existing first child is not a text node so we need to create one // and delete the existing ones. // 若不存在子节点,或者第1个子节点不是文本节点,直接将当前所有的节点都删除,然后创建出新的文本fiber节点 deleteRemainingChildren(returnFiber, currentFirstChild); const created = createFiberFromText(textContent, returnFiber.mode, lanes); created.return = returnFiber; return created; } 这里我们要理解文本 fiber 节点的两个特性: 文本节点没有 key,无法通过 key 来进行对比; 文本节点只有一个节点,没有兄弟节点;若新节点在只有一个文本节点时,之前的树中有多个 fiber 节点,除第 1 个节点决定是否复用外,其他的可以全部删除; 6. 处理并列多个元素 reconcileChildrenArray # 当前将要构建的 element 是一个数组,即并列多个节点要进行处理。这种情况要比之前处理单个节点复杂的多,因为可能会存在末尾新增、中间插入、删除、节点移动等情况,比如要考虑的情况有: 新列表和旧列表都是顺序排布的,但新列表更长,这里在新旧对比完成后,还得接着新建新增的节点; 新列表和旧列表都是顺序排布的,但新列表更短,这里在新旧对比完成后,还得删除旧列表中多余的节点; 新列表中节点的顺序发生了变化,那就不能按照顺序一一对比了; 在 fiber 结构中,并列的元素会形成单向链表,而且也没有双指针。在 fiber 链表和 element 数组进行对比时,只能从头节点开始比较: 同一个位置(索引相同),保持不变或复用的可能性比较大; newChildren 遍历完了,说明 oldFiber 链表的节点有剩余,需要删除; oldFiber 所在链表遍历完了,新数组 newChildren 可能还有剩余,直接创建新节点; 无法按照顺序一一比较,可能发生了节点的移动,这里把旧 fiber 节点存入到 map 中; 其实还存在一种特殊的情况,oldFiber.index > newIdx,旧 fiber 节点的索引比当前索引 newIdx 大,说明之前的 element 有存在无法转为 fiber 的元素。fiber 节点上的 index 索引值,是来自于数组中的下标,若这个下标对应的 jsx 元素无法转为 fiber 节点,则会造成该下标索引值的空缺。假如下标为 1 的 jsx 元素为 null,那么转成的 fiber 链表的索引值会变成:0->2->3。新数组在遍历时,是从 0 开始,按照顺序遍历的,同时旧 fiber 链表也跟着往后自动,当新索引 newIdx 到 1 时,oldFiber 移动到下一个节点的索引就是 2 了。然后机会出现 oldFiber.index > newIdx 的情况。当出现这种情况时,我们直接把 oldFiber 节点设置为了 null,然后在执行 updateSlot() 时创建出新的 fiber 节点。等待 newIdx 与 oldFiber.index 相等时,再进行相同位置的比较。reconcileChildrenArray 的流程图,也可以直接查看在线链接:接下来我们分步骤讲解一下。 6.1 相同索引位置对比 # 同一个位置(索引相同),保持不变或复用的可能性比较大。不过也只能说可能性比较大,在实际开发中什么情况都会存在,我们先以最简单的方式来处理。let resultingFirstChild: Fiber | null = null; // 新构建出来的fiber链表的头节点 let previousNewFiber: Fiber | null = null; // 新构建出来链表的最后那个fiber节点,用于构建整个链表 let oldFiber = currentFirstChild; // 旧链表的节点,刚开始指向到第1个节点 let lastPlacedIndex = 0; // 表示当前已经新建的 Fiber 的 index 的最大值,用于判断是插入操作,还是移动操作等 let newIdx = 0; // 表示遍历 newChildren 的索引指针 let nextOldFiber = null; // 下次循环要处理的fiber节点 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { if (oldFiber.index > newIdx) { /** * oldIndex 大于 newIndex,那么需要旧的 fiber 等待新的 fiber,一直等到位置相同。 * 那当前的 newChildren[newIdx] 则直接创建新的fiber节点 * 当 oldFiber.index > newIdx 时,说明旧 element 对应的newIdx的位置的 fiber 为null,这时将 oldFiber 设置为null, * 然后调用 updateSlot() 时,就不再考虑复用的问题了,直接创建新的节点。 * 下一个旧的fiber还是当前的节点,等待 newIdx 索引相等的那个 child */ nextOldFiber = oldFiber; oldFiber = null; } else { // 旧 fiber 的索引和n ewChildren 的索引匹配上了,获取 oldFiber 的下一个兄弟节点 nextOldFiber = oldFiber.sibling; } /** * 将旧节点和将要转换的 element 传进去, * 1. 若 key 对应上 * 1.1 若 type 对应上,则复用之前的节点; * 1.2 若 type 对应不上,则直接创建新的fiber节点; * 2. 若 key 对应不上,无法复用,返回 null; * 3. 若 oldFiber 为null,则直接创建新的fiber节点; * @type {Fiber} * updateSlot() 具体如何实现,我们稍后讲解 */ const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], lanes); if (newFiber === null) { /** * 新fiber节点为 null,退出循环。 * 不过这里为null的原因有很多,比如: * 1. newChildren[newIdx] 本身就是无法转为fiber的类型,如null, boolean, undefined等; * 2. oldFiber 和 newChildren[newIdx] 的 key 没有匹配上; */ if (oldFiber === null) { oldFiber = nextOldFiber; } break; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null) { // We matched the slot, but we didn't reuse the existing fiber, so we // need to delete the existing child. // 若旧fiber节点存在,但新节点并没有复用该节点,则将该旧节点删除 deleteChild(returnFiber, oldFiber); } } /** * 此方法是一种顺序优化手段,lastPlacedIndex 一直在更新,初始为 0, * 表示访问过的节点在旧集合中最右的位置(即最大的位置)。 */ lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); /** * resultingFirstChild:新fiber链表的头节点 * previousNewFiber:用于拼接整个链表 */ if (previousNewFiber === null) { // 若整个链表为空,则头指针指向到newFiber resultingFirstChild = newFiber; } else { // 若链表不为空,则将newFiber放到链表的后面 previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; // 指向到当前节点,方便下次拼接 oldFiber = nextOldFiber; // 下一个旧fiber节点 } 我们在循环中,尽量地去通过索引 index 和 key 等标识,来复用旧 fiber 节点。无法复用的,就创建出新的 fiber 节点。同时,结束循环或者跳出循环的条件有多种,在循环之后,还要做出一些额外的判断。 6.2 新节点遍历完毕 # 若经过上面的循环后,新节点已全部创建完毕,这说明可能经过了删除操作,新节点的数量更少,这里我们直接把剩下的旧节点删除了就行。// 新索引 newIdx 跟newChildren的长度一样,说明新数组已遍历完毕 // 老数组后面可能有剩余的,需要删除 if (newIdx === newChildren.length) { // 删除旧链表中剩余的节点 deleteRemainingChildren(returnFiber, oldFiber); // 返回新链表的头节点指针 return resultingFirstChild; } 后续已不需要其他的操作了,直接返回新链表的头节点指针即可。 6.3 旧 fiber 节点遍历完毕 # 若经过上面的循环后,旧 fiber 节点已遍历完毕,但 newChildren 中可能还有剩余的元素没有转为 fiber 节点,但现在旧 fiber 节点已全部都复用完了,这里直接创建新的 fiber 节点即可。// 若旧数据中所有的节点都复用了,说明新数组可能还有剩余 if (oldFiber === null) { // 这里已经没有旧的fiber节点可以复用了,然后我们就选择直接创建的方式 for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild(returnFiber, newChildren[newIdx], lanes); if (newFiber === null) { continue; } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 接着上面的链表往后拼接 if (previousNewFiber === null) { // 记录起始的第1个节点 resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } // 返回新链表的头节点指针 return resultingFirstChild; } 到这里,目前简单的对数组进行增、删节点的对比还是比较简单,接下来就是移动的情况是如何进行复用的呢? 6.4 节点位置发生了移动 # 若节点的位置发生了变动,虽然在旧节点链表中也存在这个节点,但若按顺序对比时,确实不方便找到这个节点。因此可以把这些旧节点放到 Map 中,然后根据 key 或者 index 获取。/** * 将 currentFirstChild 和后续所有的兄弟节点放到map中,方便查找 * 若该 fiber 节点有 key,则使用该 key 作为 map 的 key;否则使用隐性的index作为map的key * @param {Fiber} returnFiber 要存储的节点的父级节点,但这个参数没用到 * @param {Fiber} currentFirstChild 要存储的链表的头节点指针 * @returns {Map} 返回存储所有节点的map对象 */ function mapRemainingChildren(returnFiber: Fiber, currentFirstChild: Fiber): Map { /** * 将剩余所有的子节点都存放到 map 中,方便可以通过 key 快速查找该fiber节点 * 若该 fiber 节点有 key,则使用该key作为map的key;否则使用隐性的index作为map的key */ const existingChildren: Map = new Map(); let existingChild = currentFirstChild; while (existingChild !== null) { if (existingChild.key !== null) { existingChildren.set(existingChild.key, existingChild); } else { existingChildren.set(existingChild.index, existingChild); } existingChild = existingChild.sibling; } return existingChildren; } 把所有的旧 fiber 节点存储到 Map 中后,就接着循环新数组 newChildren,然后从 map 中获取到对应的旧 fiber 节点(也可能不存在),再创建出新的节点。for (; newIdx < newChildren.length; newIdx++) { // 从 map 中查找是否存在可以复用的fiber节点,然后生成新的fiber节点 const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes); if (newFiber !== null) { // 这里只处理 newFiber 不为null的情况 if (shouldTrackSideEffects) { // 若需要记录副作用 if (newFiber.alternate !== null) { /** * newFiber.alternate指向到current,若current不为空,说明复用了该fiber节点, * 这里我们要在 map 中删除,因为后面会把 map 中剩余未复用的节点删除掉的, * 所以这里我们要及时把已复用的节点从 map 中剔除掉 */ existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); // 接着之前的链表进行拼接 if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } if (shouldTrackSideEffects) { // 将 map 中没有复用的 fiber 节点添加到删除的副作用队列中,等待删除 existingChildren.forEach(child => deleteChild(returnFiber, child)); } // 返回新链表的头节点指针 return resultingFirstChild; 到这里,我们新数组 newChildren 中所有的 element 结构,都已转为 fiber 节点。 7. 几个关于 fiber 的工具函数 # 我们在上面探讨前后 diff 对比时,涉及到了多个对 fiber 处理的工具函数,但都跳过去了,这里我们挑几个稍微讲解下。我们在 diff 阶段涉及到所有对 fiber 的增删等操作,都只是打上标记而已,并不是立刻进行处理的,是要等到 commit 阶段才会处理。 7.1 删除单个节点 deleteChild # 删除单一某个 fiber 节点,这里会将该节点,存储到其父级 fiber 节点的 deletions 中。/** * 将returnFiber子元素中,需要删除的fiber节点放到deletions的副作用数组中 * 该方法只删除一个节点 * 当前diff时不会立即删除,而是在更新时,才会将该数组中的fiber节点进行删除 * @param returnFiber * @param childToDelete */ function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void { if (!shouldTrackSideEffects) { // 不需要收集副作用时,直接返回,不进行任何操作 return; } const deletions = returnFiber.deletions; if (deletions === null) { // 若副作用数组为空,则创建一个 returnFiber.deletions = [childToDelete]; returnFiber.flags |= ChildDeletion; } else { // 否则直接推入 deletions.push(childToDelete); } } 7.2 批量删除多个节点 deleteRemainingChildren # 跟上面的 deleteChild 很像,但这个函数会把从某个节点开始到结尾所有的 fiber 节点标记为删除状态。/** * 删除 returnFiber 的子元素中,currentFirstChild及后续所有的兄弟元素 * 即把 currentFirstChild 及其兄弟元素,都放到 returnFiber 的 deletions 的副作用数组中,等待删除 * 这是一个批量删除节点的方法 * @param returnFiber 要删除节点的父级节点 * @param currentFirstChild 当前要删除节点的起始节点 * @returns {null} */ function deleteRemainingChildren(returnFiber: Fiber, currentFirstChild: Fiber | null): null { if (!shouldTrackSideEffects) { // 不需要收集副作用时,直接返回,不进行任何操作 return null; } /** * 从 currentFirstChild 节点开始,把当前及后续所有的节点,通过 deleteChild() 方法标记为删除状态 * @type {Fiber} */ let childToDelete = currentFirstChild; while (childToDelete !== null) { deleteChild(returnFiber, childToDelete); childToDelete = childToDelete.sibling; } return null; } 7.3 复用 fiber 节点 useFiber # 在没有任何更新时,React 中的两棵 fiber 树是一一对应的。不过当产生更新后,前后两棵 fiber 树就不一样了。若当前要根据 element 生成一个 fiber 节点,目前有 2 种情况: current 节点不存在或 current.alternate 不存在,说明该 element 是新增的,直接新建即可; current.alternate 存在,则可以直接复用; 在 useFiber() 中,会调用 createWorkInProgress() 来尝试复用 workInProgress 节点,生成新的 fiber 节点:export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { // We use a double buffering pooling technique because we know that we'll // only ever need at most two versions of a tree. We pool the "other" unused // node that we're free to reuse. This is lazily created to avoid allocating // extra objects for things that are never updated. It also allow us to // reclaim the extra memory if needed. /** * 翻译:我们使用双缓冲池技术,因为我们知道我们最多只需要两个版本的树。 * 我们可以汇集其他未使用的节点,进行自由的重用。 * 这是惰性创建的,以避免为从不更新的对象分配额外的对象。 * 它还允许我们在需要时回收额外的内存 */ workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode); workInProgress.elementType = current.elementType; workInProgress.type = current.type; workInProgress.stateNode = current.stateNode; /** * workInProgress是新创建出来的,要和current建立联系 * workInProgress 和 current通过 alternate 属性互相进行指向 */ workInProgress.alternate = current; current.alternate = workInProgress; } else { workInProgress.pendingProps = pendingProps; // Needed because Blocks store data on type. workInProgress.type = current.type; // We already have an alternate. // Reset the effect tag. workInProgress.flags = NoFlags; // The effects are no longer valid. workInProgress.subtreeFlags = NoFlags; workInProgress.deletions = null; } /** * 以下语句,复用current中的特性 */ // Reset all effects except static ones. // Static effects are not specific to a render. workInProgress.flags = current.flags & StaticMask; workInProgress.childLanes = current.childLanes; workInProgress.lanes = current.lanes; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; workInProgress.memoizedState = current.memoizedState; workInProgress.updateQueue = current.updateQueue; // These will be overridden during the parent's reconciliation workInProgress.sibling = current.sibling; workInProgress.index = current.index; workInProgress.ref = current.ref; return workInProgress; } 在 createWorkInProgress() 中,若 workInProgress(current.alternate) 不存在,则新创建一个,然后与 current 建立关联;若 workInProgress 已存在,则直接复用该节点,并将 current 中的特性给到这个 workInProgress 节点。不过在 reconcileChildFibers() 中的 useFiber() 里,复用节点时,暂时还不知道它将来的使用情况,有可能只是做为单个 fiber 节点使用,因此把 index 和 sibling 进行了重置。/** * 复用fiber节点的alternate,生成一个新的fiber节点 * 若alternate为空,则创建; * 若不为空,则直接复用,并将传入的fiber属性和pendingProps的属性给到alternate上 * @param fiber * @param pendingProps * @returns {Fiber} */ function useFiber(fiber: Fiber, pendingProps: mixed): Fiber { const clone = createWorkInProgress(fiber, pendingProps); // 重置以下两个属性 clone.index = 0; clone.sibling = null; return clone; } 无论我们是复用,还是新创建的 fiber 节点,目前并不知道它将来怎么使用,所 7.4 updateSlot # updateSlot()和 createChild()两个方法很像,但两者最大的区别就在于:是否要复用 oldFiber 节点。 updateSlot() 会尽量复用 oldFiber 节点,若 oldFiber 的 key 和 element 的 key 对应不上,则直接返回 null,否则复用创建; createChild() 则不考虑复用的问题,直接用 element 新建出新的 fiber 节点; 里面要稍微注意的一点:若当前单个结构是一个数组类型,则会先创建一个 fragment 类型的 fiber 节点,然后再递归创建内部的结构。如:function App() { const list = ['Jack', 'Tom', 'Jerry']; return ( {list.map(username => ( {username} ))} Emma Mia ); } 代码中,数组 list.map()后得到的是一个数组结构,在 React 内构建 fiber 节点时,并不会把数组中的这几个 li 标签,和下面的 2 个 li 标签合到一起。实际会变成这样:function App() { const list = ['Jack', 'Tom', 'Jerry']; return ( Jack Tom Jerry Emma Mia ); } 具体的实现:/** * 创建或更新element结构 newChild 为fiber节点 * 若oldFiber不为空,且newChild与oldFiber的key能对得上,则复用旧fiber节点 * 否则,创建一个新的fiber节点 * 该updateSlot方法与createChild方法很像,但createChild只有创建新fiber节点的功能 * 而该updateSlot()方法则可以根据oldFiber,来决定是复用之前的fiber节点,还是新创建节点 * @param returnFiber * @param oldFiber * @param newChild * @param lanes * @returns {Fiber|null} */ function updateSlot(returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, lanes: Lanes): Fiber | null { // 若key相等,则更新fiber节点;否则直接返回null const key = oldFiber !== null ? oldFiber.key : null; if ((typeof newChild === 'string' && newChild !== '') || typeof newChild === 'number') { // 文本节点本身是没有key的,若旧fiber节点有key,则说明无法复用 if (key !== null) { return null; } // 若旧fiber没有key,即使他不是文本节点,我们也尝试复用 return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes); } if (typeof newChild === 'object' && newChild !== null) { // 若是一些ReactElement类型的,则判断key是否相等;相等则复用;不相等则返回null switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { if (newChild.key === key) { // key一样才更新 return updateElement(returnFiber, oldFiber, newChild, lanes); } else { // key不一样,则直接返回null return null; } } // 其他类型暂时省略 } if (isArray(newChild) || getIteratorFn(newChild)) { // 当前是数组或其他迭代类型,本身是没有key的,若oldFiber有key,则无法复用 if (key !== null) { return null; } // 若 newChild 是数组或者迭代类型,则更新为fragment类型 return updateFragment(returnFiber, oldFiber, newChild, lanes, null); } } // 其他类型不进行处理,直接返回null return null; } updateSlot() 着重在复用上,只有前后两个 key 匹配上,才会继续后续的流程,否则直接返回 null。 8. 总结 # 我们主要了解了不同类型的 element 转成 fiber 节点的过程,如文本类型的,React 普通类型的 ,数组类型的,等等。尤其是在数组对比时,涉及到每个元素的新增、删除、移动等操作,对比起来要复杂一些。可以看到,当组件产生比较大的变更时,React 需要做更多的动作,来构建出新的 fiber 树,因此我们在开发过程中,若从性能优化的角度考虑,尤其要注意的是: 节点不要产生大量的越级操作:因为 React 是只进行同层节点的对比,若同一个位置的子节点产生了比较大的变动,则只会舍弃掉之前的 fiber 节点,从而执行创建新 fiber 节点的操作;React 并不会把之前的 fiber 节点移动到另一个位置;相应的,之前的 jsx 节点移动到另一个位置后,在进行前后对比后,同样会执行更多的创建操作; 不修改节点的 key 和 type 类型,如使用随机数做为列表的 key,或从 div 标签改成 p 标签等操作,在 diff 对比过程中,都会直接舍弃掉之前的 fiber 节点及所有的子节点(即使子节点没有变动),然后重新创建出新的 fiber 节点;
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 placeChild 的执行
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 在 React fiber 对比的过程中,有用到 placeChild() 函数,这个函数是做什么的呢?此方法是一种顺序优化手段,lastPlacedIndex 一直在更新,初始为 0,表示访问过的节点在旧集合中最右的位置(即最大的位置)。如果新集合中当前访问的节点比 lastPlacedIndex 大,说明当前访问节点在旧集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作。只有当访问的节点比 lastPlacedIndex 小时,才需要进行移动操作。lastPlaceIndex 表示当前复用到的旧 fiber 的最大索引,初始时为 0。比如第一个新 fiber 节点复用的是最后一个旧 fiber 节点,那 lastPlaceIndex 就是最后那个旧 fiber 节点的索引值。 1. 样例 1 # return !flag ? [0, 1, 2] : [0, 2, 2]; 过程描述: lastPlaceIndex 初始时为 0; 新节点 key1,Map 集合中存在 key1 则取出复用,key1 老节点的 oldIndex 为 1,不满足 oldIndex < lastPlacedIndex,返回 oldIndex,并且赋值给 lastPlacedIndex 值更新为 1。 新节点 key2,Map 集合中存在 key2 则取出复用,key2 老节点的 oldIndex 为 2,不满足 oldIndex < lastPlacedIndex,返回 oldIndex,并且赋值给 lastPlacedIndex 值更新为 2。 新节点 key0,Map 集合中存在 key0 则取出复用,key0 老节点的 oldIndex 为 0,满足 oldIndex < lastPlacedIndex,则将 key0 标记为插入,返回 lastPlacedIndex。 2. 样例 2 # return !flag ? [ 0, 1, 2, 2 ] : [ 1, 0, 3, 2 ]; 过程描述: lastPlaceIndex 初始时为 0; 新节点 key1,Map 集合中存在 key1 则取出复用,key1 老节点的 oldIndex 为 1,不满足 oldIndex < lastPlacedIndex,返回 oldIndex,并且赋值给 lastPlacedIndex 值更新为 1。 新节点 key0,Map 集合中存在 key0 则取出复用,key0 老节点的 oldIndex 为 0,满足 oldIndex < lastPlacedIndex,则将 key0 标记为插入,返回 lastPlacedIndex。 新节点 key3,Map 集合中存在 key3 则取出复用,key3 老节点的 oldIndex 为 3,不满足 oldIndex < lastPlacedIndex,返回 oldIndex,并且赋值给 lastPlacedIndex 值更新为 3。 新节点 key2,Map 集合中存在 key2 则取出复用,key2 老节点的 oldIndex 为 2,满足 oldIndex < lastPlacedIndex,则将 key2 标记为插入,返回 lastPlacedIndex。 3. 样例 3: # return !flag ? [ 0, 1, 2, 2 ] : [ 1, 5, 3, 0 ]; 过程描述: lastPlaceIndex 初始时为 0; 新节点 key1,Map 集合中存在 key1 则取出复用,key1 老节点的 oldIndex 为 1,不满足 oldIndex < lastPlacedIndex,返回 oldIndex,并且赋值给 lastPlacedIndex 值更新为 1。 新节点 key5,Map 集合中不存在 key5 新建节点,不满足 current !== null,则将 key5 标记为插入,返回 lastPlacedIndex。 新节点 key3,Map 集合中存在 key3 则取出复用,key3 老节点的 oldIndex 为 3,不满足 oldIndex < lastPlacedIndex,返回 oldIndex,并且赋值给 lastPlacedIndex 值更新为 3。 新节点 key0,Map 集合中存在 key0 则取出复用,key0 老节点的 oldIndex 为 0,满足 oldIndex < lastPlacedIndex,则将 key0 标记为插入,返回 lastPlacedIndex。 剩余节点 key2 通过 existingChildren 遍历删除,被复用过的节点因为从 map 集合中已经移除了,所以这里的删除只是为被复用的。 4. 样例 4(性能差的一种情况) # return !flag ? [0, 1, 2] : [2, 0, 1]; 过程同上,但是这种操作会使得顺序优化算法失去效果,除了最后一个节点没有 effect,其他节点都会被执行插入操作,所以尽量避免将最后一个节点更新到第一个节点的位置操作。参考文章: React Diff
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 key 的作用
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 在阅读过 React18 源码解析之 reconcileChildren 生成 fiber 的过程 文章后,我们就会知道,属性 key 相当于某元素的唯一标识,当组件中产生需要 diff 对比元素时,首先要对比的就是属性 key。React 中并不知道哪个节点产生了修改、新增或者其他变动,都是 key 和 type 来对比前后两个节点才会知道。 当 key 不一样时(如相同索引的位置,current 的 key 是 abc,而 element 中的 key 是 def),或者找不到相应的 key(如 element 中的 key 是 def,但原 current 树中没有该 key),都会把之前的 fiber 节点删除掉,重新创建新的 fiber 节点。那就会引申出几个问题: 最好不要使用随机数做为 key; 最好不要使用数组的下标做为 key; 1. 为什么不能用随机数做 key? # 我们在之前的文章讲解过 updateSlot() 方法,在 diff 对比过程中,当 key 或 type 不一样时,都会直接舍弃掉之前的 fiber 节点及所有的子节点(即使子节点没有变动),然后重新创建出新的 fiber 节点。通过随机数设定的 key,则会产生无序性,可能会导致所有的 key 都匹配不上,然后舍弃掉之前所有构建出来的 fiber 节点,再重新创建新的节点。点击查看样例:React 中不同的 key 产生的影响。 key 的类型选择“随机数”; 可以任意点击“最前新增”或“后面新增”两个按钮,添加元素; 在输入框中输入任意字符,目前输入框是非受控组件; 再次点击任意两个按钮,观察效果; 我们在输入框中输入任意的字符,然后再新增 item 时,输入框中的数据就会被情况。这是因为,React 中前后两个 key 进行对比时,没有匹配上,然后就会丢弃之前的 fiber 节点,重新创建。同时 input 目前是非受控组件,所有的数据在重新创建后都会丢失。我们稍微了解下什么是受控组件和非受控组件: 受控组件:只能通过 React 修改数据或状态的组件,就是受控组件; 非受控组件:与受控组件相反,如 input, textarea, select 等组件,用户也可以控制展示的数据,这就是非受控组件; 我们可以通过一定的方式将非受控组件,改为受控组件,如:const App = () => { const [value, setValue] = useState(''); const handleInput = event => { setValue(event.target.value); }; return ; }; 监听输入框 input 的 onInput 事件,通过 React 来修改 input 的值。 2. 为什么不要使用数组的下标做为 key? # 数组下标相对随机数来说,比较稳定一些。但数组下标对应的组件并不是一成不变的,只要在数组的前面或者中间插入元素时,该下标对应的元素就发生变化。例如数组 list,初始时是['abc'],那么下标 0 对应的就是字符串 abc 元素。然后在最前面插入数字 123,变成[123, 'abc'],此时下标 0 对应的就是数组 123 了。虽然 key 没变,但对应的元素已经发生变化了。点击查看样例:React 中不同的 key 产生的影响。 key 的类型选择“数组下标”; 点击“最前新增”按钮,添加元素; 在任意输入框中输入任意字符(这里我们可以选择最上面的那个),目前输入框是非受控组件; 再次点击“最前新增”按钮,观察效果; 可以看到,本来带有内容的输入框应该跟着一起向下移动的,可是他却一直在最上面。因为 key 没有变化,input 节点就会复用上一次渲染的。 3. 为什么是最好不要使用? # 为什么是“最好不要”使用,而不是坚决不能使用。因为有些情况下是可以用随机数和下标做为 key 的。比如只有则初始时渲染一次,后续不再更新列表,只是对某个具体元素进行更新或事件的处理等。参考文章: 列表 & Key
2024年10月20日
3 阅读
0 评论
0 点赞
1
...
52
53
54
...
213