一文读懂React组件渲染核心原理

引言

相信大家对 React 都已经非常熟悉了,像 React,Vue 这样的现代前端框架已经是我们日常开发离不开的工具了,这篇文章主要是从源码的角度剖析 React 的核心渲染原理。我们将从用户编写的组件代码开始,一步一步分析 React 是如何将它们变成真实 DOM ,这个过程主要可以分成两个阶段:render 阶段和 commit 阶段。文章的核心内容也正是对这两个阶段的分析。

专业从事成都网站制作、网站设计、外贸网站建设,高端网站制作设计,小程序开发,网站推广的成都做网站的公司。优秀技术团队竭力真诚服务,采用HTML5+CSS3前端渲染技术,自适应网站建设,让网站在手机、平板、PC、微信下都能呈现。建站过程建立专项小组,与您实时在线互动,随时提供解决方案,畅聊想法和感受。

一、前置知识

声明式渲染

『声明式渲染』,顾名思义,就是让使用者只需要「声明或描述」我需要渲染的东西是什么,然后就把具体的渲染工作交给机器去做,与之相对的是『命令式渲染』。

『命令式渲染』则是由用户去一步一步地命令机器下一步该怎么做。

举个简单的例子:

如果我们需要在网页上渲染一个有三个节点的列表,命令式的做法是手动操作 dom,首先创建一个容器节点,再利用循环每次先创建一个新节点,填充内容,然后将新节点新增到容器节点下,最后再将容器节点新增到 body 标签下:

 
 
 
 
  1. const list = [1,2,3];
  2. const container = document.createElement('div');
  3. for (let i = 0; i < list.length; i ++) {
  4.     const newDom = document.createElement('div');
  5.     newDom.innerHTML = list[i];
  6.     container.appendChild(newDom);
  7. }
  8. document.body.appendChild(container);

而声明式的做法应该是:

 
 
 
 
  1. const list = [1,2,3];
  2. const container = document.createElement('div');
  3. const Demo = () =>
  4. (
  5.     {list.map((item) => 
    {item}
    )}
)
  • ReactDom.render(, container);
  • 可以看到在这个例子中,声明式写法以 HTML 语法直接告诉机器,我需要的视图应该是长这个样子,然后具体的 DOM 操作全部交由机器去完成。开发者只需要专注于业务逻辑的实现。

    这便是声明式渲染。

    声明式渲染是现代前端框架的比较普遍的设计思路。

    JSX 和 ReactElement

    相信大家最初学 React 的时候都有这样的疑问,为什么我们能够以类似 HTML 的语法编写组件,这个东西又是怎么转换成 JavaScript 语法的?答案就是 Babel。根据官网介绍,这种语法被称为 JSX,是一个 JavaScript 的语法扩展。能够被 Babel 编译成 React.createElement 方法。举个例子:

    通过查阅源码我们可以看到 「React.createElement」 方法。

     
     
     
     
    1. export function createElement(type, config, children) {
    2.   let propName;
    3.   // Reserved names are extracted
    4.   const props = {};
    5.   let key = null;
    6.   let ref = null;
    7.   let self = null;
    8.   let source = null;
    9.   ...
    10.   return ReactElement(
    11.     type,
    12.     key,
    13.     ref,
    14.     self,
    15.     source,
    16.     ReactCurrentOwner.current,
    17.     props,
    18.   );
    19. }
    20. const ReactElement = function(type, key, ref, self, source, owner, props) {
    21.   const element = {
    22.     // This tag allows us to uniquely identify this as a React Element
    23.     $typeof: REACT_ELEMENT_TYPE,
    24.     // Built-in properties that belong on the element
    25.     type: type,
    26.     key: key,
    27.     ref: ref,
    28.     props: props,
    29.     // Record the component responsible for creating this element.
    30.     _owner: owner,
    31.   };
    32.   ...
    33.   return element;
    34. }

    可以看到 React 是使用了 element 这种结构来代表一个节点,里面就只有简单的 6 个字段。我们可以看个实际的例子,下面 Count 组件对应的 element 数据结构:

     
     
     
     
    1. function Count({count, onCountClick}) {
    2.   return  { onCountClick()}}>
    3.   count: {count}
    4.   
  • }
  • 可以看到,element 结构只能反映出 jsx 节点的层级结构,而组件里的各种状态或者返回 jsx 等都是不会记录在 element 中。

    目前我们知道,我们编写的 jsx 会首先被处理成 element 结构。

    jsx -> element

    那 React 又是如何处理 element 的,如刚刚说的,element 里包含的信息太少,只靠 element 显然是不足以映射到所有真实 DOM 的,因此我们还需要更精细的结构。

    Fiber 树结构

    Fiber 这个单词相信大家多多少少都有听过,它是在 React 16 被引入,关于 Fiber 如何实现任务调度在这篇文章不会涉及,但是 Fiber 的引入不仅仅带来了任务调度方面的能力,整个 React 实现架构也因此重构了一遍,而我们之前经常提到的虚拟 DOM 树在新的 React 架构下被称为 Fiber 树,上面提到的每个 element 都有一个所属的 Fiber。

    首先我们先看看源码中 Fiber 的构造函数:

     
     
     
     
    1. function FiberNode(
    2.   tag: WorkTag,
    3.   pendingProps: mixed,
    4.   key: null | string,
    5.   mode: TypeOfMode,
    6. ) {
    7.   // Instance
    8.   this.tag = tag;            // 标识节点类型,例如函数组件、类组件、普通标签等
    9.   this.key = key;
    10.   this.elementType = null;  // 标识具体 jsx 标签名
    11.   this.type = null;        // 类似 elementType
    12.   this.stateNode = null;  // 对应的真实 DOM 节点
    13.   // Fiber
    14.   this.return = null;    // 父节点
    15.   this.child = null;     // 第一个子节点
    16.   this.sibling = null;   // 第一个兄弟节点
    17.   this.index = 0;
    18.   this.ref = null;
    19.   this.pendingProps = pendingProps;  // 传入的 props
    20.   this.memoizedProps = null;    
    21.   this.updateQueue = null;   // 状态更新相关
    22.   this.memoizedState = null;
    23.   this.dependencies = null;
    24.   this.mode = mode;
    25.   // Effects
    26.   this.flags = NoFlags;
    27.   this.subtreeFlags = NoFlags;
    28.   this.deletions = null;
    29.   this.lanes = NoLanes;
    30.   this.childLanes = NoLanes;
    31.   this.alternate = null;
    32.   ...
    33. }

    可以看到 Fiber 节点中的属性很多,其中不仅仅包含了 element 相关的实例信息,还包含了组成 Fiber 树所需的一些“指针”,组件内部的状态(memorizedState),用于操作真实 DOM 的副作用(effects)等等。

    我们以上面的 Count 组件为例看一下它对应的 Fiber 结构:

    这里我们先主要介绍一下与形成 Fiber 树相关的三个属性:child, sibling 和 return。他们分别指向 Fiber 的第一个子 Fiber,下一个兄弟 Fiber 和父 Fiber。

    以下面的 jsx 代码为例:

     
     
     
     
    1.      
    2. // App.jsx    
    3.     
    4.       
    5.         
    6.         

    7.           text
    8.         

    9.         
    10.       
    11.     
  •     
  • // Count.jsx
  • 最终形成的 Fiber 树结构为:

    总结一下,我们编写的 jsx 首先会形成 element ,然后在 render 过程中每个 element 都会生成对应的 Fiber,最终形成 Fiber 树。

    jsx -> element -> Fiber

    下面我们正式介绍一下 render 的过程,看看 Fiber 是如何生成并形成 Fiber 树的。

    二、渲染(render)过程

    核心流程

    通常 React 运行时会有两个 Fiber 树,一个是根据当前最新组件状态构建出来的,另一个则是上一次构建出来的 Fiber 树,当然如果是首次渲染就没有上一次的 Fiber 树,这时就只有一个了。简单来说,render 过程就是 React 「对比旧 Fiber 树和新的 element」 然后「为新的 element 生成新 Fiber 树」的一个过程。

    从源码中看,React 的整个核心流程开始于 「performSyncWorkOnRoot」 函数,在这个函数里会先后调用 「renderRootSync」 函数和 「commitRoot」 函数,它们两个就是分别就是我们上面提到的 render 和 commit 过程。来看 renderRootSync 函数,在 「renderRootSync」 函数里会先调用 「prepareFreshStack」 ,从函数名字我们不难猜出它主要就是为接下来的工作做前置准备,初始化一些变量例如 workInProgress(当前正在处理的 Fiber 节点) 等,接着会调用 「workLoopSync」 函数。(这里仅讨论传统模式,concurrent 模式留给 Fiber 任务调度分享),而在 「workLoopSync」 完成之后,「renderRootSync」 也基本上完成了,接下来就会调用 commitRoot 进入 commit 阶段。

    因此整个 render 过程的重点在 「workLoopSync」 中,从 「workLoopSync」 简单的函数定义里我们可以看到,这里用了一个循环来不断调用 「performUnitOfWork」 方法,直到 workInProgress 为 null。

     
     
     
     
    1. function workLoopSync() {
    2.   // Already timed out, so perform work without checking if we need to yield.
    3.   while (workInProgress !== null) {
    4.     performUnitOfWork(workInProgress);
    5.   }
    6. }

    而 「performUnitOfWork」 函数做的事情也很简单,简单来说就是为传进来的 workInProgress 生成下一个 Fiber 节点然后赋值给 workInProgress。通过不断的循环调用 「performUnitOfWork」,直到把所有的 Fiber 都生成出来并连接成 Fiber 树为止。

    现在我们来看 「performUnitOfWork」 具体是如何生成 Fiber 节点的。

    前面介绍 Fiber 结构的时候说过,Fiber 是 React 16 引入用于任务调度提升用户体验的,而在此之前,render 过程是递归实现的,显然递归是没有办法中断的,因此 React 需要使用循环来模拟递归过程。

    「performUnitOfWork」 正是使用了 「beginWork」 和 「completeUnitOfWork」 来分别模拟这个“递”和“归”的过程。

    render 过程是深度优先的遍历,「beginWork」 函数则会为遍历到的每个 Fiber 节点生成他的所有子 Fiber 并返回第一个子 Fiber ,这个子 Fiber 将赋值给 workInProgress,在下一轮循环继续处理,直到遍历到叶子节点,这时候就需要“归”了。

    「completeUnitOfWork」 就会为叶子节点做一些处理,然后把叶子节点的兄弟节点赋值给 workInProgress 继续“递”操作,如果连兄弟节点也没有的话,就会往上处理父节点。

    同样以上面的 Fiber 树例子来看,其中的 Fiber 节点处理顺序应该如下:

    beginWork

    在介绍概览的时候说过,React 通常会同时存在两个 Fiber 树,一个是当前视图对应的,一个则是根据最新状态正在构建中的。这两棵树的节点一一对应,我们用 current 来代表前者,我们不难发现,当首次渲染的时候,current 必然指向 null。实际上在代码中也确实都是通过这个来判断当前是首次渲染还是更新。

    「beginWork」 的目的很简单:

    在 「beginWork」 执行中,首先会判断当前是否是首次渲染。

     
     
     
     
    1. switch (workInProgress.tag) {
    2.     case FunctionComponent: {
    3.       ...
    4.     }
    5.     case ClassComponent: {
    6.       ...
    7.     }
    8.     case HostRoot: {
    9.       ...
    10.     }
    11.     case HostComponent: {
    12.       ...
    13.     }
    14.     ...
    15.   }
     
     
     
     
    1. if (current !== null) {
    2.     // 这里处理一些依赖
    3.     if (
    4.       enableLazyContextPropagation &&
    5.       !includesSomeLane(renderLanes, updateLanes)
    6.     ) {
    7.       const dependencies = current.dependencies;
    8.       if (dependencies !== null && checkIfContextChanged(dependencies)) {
    9.         updateLanes = mergeLanes(updateLanes, renderLanes);
    10.       }
    11.     }
    12.     const oldProps = current.memoizedProps;
    13.     const newProps = workInProgress.pendingProps;
    14.     if (
    15.       oldProps !== newProps ||
    16.       hasLegacyContextChanged() ||
    17.       // Force a re-render if the implementation changed due to hot reload:
    18.       (__DEV__ ? workInProgress.type !== current.type : false)
    19.     ) {
    20.       // 如果 props 或者 context 变了
    21.       didReceiveUpdate = true;
    22.     } else if (!includesSomeLane(renderLanes, updateLanes)) {
    23.       didReceiveUpdate = false;
    24.       // 走到这里则说明符合优化条件
    25.       switch (workInProgress.tag) {
    26.         case HostRoot:
    27.           ...
    28.           break;
    29.         case HostComponent:
    30.           ...
    31.           break;
    32.         case ClassComponent: {
    33.           ...
    34.           break;
    35.         }
    36.         case HostPortal:
    37.           ...
    38.           break;
    39.         case ContextProvider: {
    40.           ...
    41.           break;
    42.         }
    43.         ...
    44.         
    45.       }
    46.       return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    47.     } else {
    48.       ...
    49.       didReceiveUpdate = false;
    50.     }
    51.   } else {
    52.     didReceiveUpdate = false;
    53.   }

    更新优化策略应用

    开发过程中我们常常希望利用 React 非首次渲染的优化策略来提升性能,如下代码,B 组件是个纯展示组件且内部没有依赖任何 Demo 组件的数据,因此有些同学可能会想当然认为当 Demo 重新渲染时这个 B 组件是符合 React 优化条件的。但结果是,每次 Demo 重新渲染都会导致 B 组件重新渲染。每次渲染时 B 组件的 props 看似没发生变化,但由于 Demo 重新执行后会生成全新的 B 组件(下面会介绍),所以新旧 B 组件的 props 肯定也是不同的。

     
     
     
     
    1. function App() {
    2.     return 
    3. }
    4. function Demo() {
    5.     const [v, setV] = useState();
    6.     return (
    7.         
    8.             
    9.             
    10.           
    11.     );
    12. }

    那有什么办法可以保持住 B 组件不变吗,答案是肯定的,我们可以把 B 组件放到 Demo 组件外层,这样一来,B 组件是在 App 组件中生成并作为 props 传入 Demo 的,因为不管 Demo 组件状态怎么变化都不会影响到 App 组件,因此 App 和 B 组件就只会在首次渲染时会执行一遍,也就是说 Demo 获取到的 props.children 的引用一直都是指向同一个对象,这样一来 B 组件的 props 也就不会变化了。

     
     
     
     
    1. function App() {
    2.     return 
    3.         
    4.     
    5. }
    6. function Demo(props) {
    7.     const [v, setV] = useState();
    8.     return (
    9.         
    10.             
    11.            {props.children}
    12.           
    13.     );
    14. }

    更新当前节点

    通过上面的解析我们知道,当不走优化逻辑时 「beginWork」 使用大量的 switch...case 来分别处理不同类型的组件,下来我们以我们熟悉的 Function Component 为例。

    「核心就是通过调用函数组件,得到组件的返回的 element。」

    类似地,对于类组件,则是调用组件实例的 render 方法得到 element。

    而对于我们普通的组件,例如

     则是直接取 props.children 即可。

     
     
     
     
    1. function updateFunctionComponent(
    2.   current,
    3.   workInProgress,
    4.   Component,
    5.   nextProps: any,
    6.   renderLanes,
    7. ) {
    8.   let context;
    9.   if (!disableLegacyContext) {
    10.     const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
    11.     context = getMaskedContext(workInProgress, unmaskedContext);
    12.   }
    13.   let nextChildren;
    14.   prepareToReadContext(workInProgress, renderLanes);
    15.   // 执行组件函数获取返回的 element
    16.   nextChildren = renderWithHooks(
    17.     current,
    18.     workInProgress,
    19.     Component,
    20.     nextProps,
    21.     context,
    22.     renderLanes,
    23.   );
    24.   
    25.   // React DevTools reads this flag.
    26.   workInProgress.flags |= PerformedWork;
    27.   reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    28.   return workInProgress.child;
    29. }

    得到组件返回的 element(s) 之后,下一步就是为他们生成 Fiber,我们查看源码可以看到,不论是函数组件或是类组件或是普通组件,最后返回的 element(s) 都会作为参数传入到 「reconcileChildren」 中。

    介绍 「reconcileChildren」 之前我们先用一张图总结一下 「beginWork」 的大致流程:

    生成子节点

    经过上一步得到 workInProgress 的 children 之后,接下来需要为这些 children element 生成 Fiber ,这就是 「reconcileChildFibers」 函数做的事情,这也是我们经常提到的 diff 的过程。

    这个函数里主要分两种情况处理,如果是 newChild(即 children element)是 object 类型,则进入单节点 diff 过程(「reconcileSingleElement」),如果是数组类型,则进入多节点 diff 过程(「reconcileChildrenArray」)。

     
     
     
     
    1. function reconcileChildFibers(
    2.     returnFiber: Fiber,
    3.     currentFirstChild: Fiber | null,
    4.     newChild: any,
    5.     lanes: Lanes,
    6.   ): Fiber | null {
    7.     if (typeof newChild === 'object' && newChild !== null) {
    8.       switch (newChild.$typeof) {
    9.         case REACT_ELEMENT_TYPE:
    10.           return placeSingleChild(
    11.             reconcileSingleElement(
    12.               returnFiber,
    13.               currentFirstChild,
    14.               newChild,
    15.               lanes,
    16.             ),
    17.           );
    18.         ...
    19.       }
    20.       if (isArray(newChild)) {
    21.         return reconcileChildrenArray(
    22.           returnFiber,
    23.           currentFirstChild,
    24.           newChild,
    25.           lanes,
    26.         );
    27.       }
    28.       throwOnInvalidObjectType(returnFiber, newChild);
    29.     }
    30.     
    31. }   

    单节点diff

     
     
     
     
    1. function reconcileSingleElement(
    2.     returnFiber: Fiber,
    3.     currentFirstChild: Fiber | null,
    4.     element: ReactElement,
    5.     lanes: Lanes,
    6.   ): Fiber {
    7.     const key = element.key;
    8.     let child = currentFirstChild;
    9.     while (child !== null) {
    10.     
    11.       // 首先比较 key 是否相同
    12.       if (child.key === key) {
    13.         const elementType = element.type;
    14.         ...
    15.            // 然后比较 elementType 是否相同
    16.           if (child.elementType === elementType) {
    17.             deleteRemainingChildren(returnFiber, child.sibling);
    18.             const existing = useFiber(child, element.props);
    19.             existing.ref = coerceRef(returnFiber, child, element);
    20.             existing.return = returnFiber;
    21.             return existing;
    22.           }
    23.         
    24.         // Didn't match.
    25.         deleteRemainingChildren(returnFiber, child);
    26.         break;
    27.       } else {
    28.         deleteChild(returnFiber, child);
    29.       }
    30.       // 遍历兄弟节点,看能不能找到 key 相同的节点
    31.       child = child.sibling;
    32.     }
    33.     if (element.type === REACT_FRAGMENT_TYPE) {
    34.       const created = createFiberFromFragment(
    35.         element.props.children,
    36.         returnFiber.mode,
    37.         lanes,
    38.         element.key,
    39.       );
    40.       created.return = returnFiber;
    41.       return created;
    42.     } else {
    43.       const created = createFiberFromElement(element, returnFiber.mode, lanes);
    44.       created.ref = coerceRef(returnFiber, currentFirstChild, element);
    45.       created.return = returnFiber;
    46.       return created;
    47.     }
    48.   }
    49.   

    本着尽可能复用旧节点的原则,在单节点 diff 在这里,我们会遍历旧节点,对每个遍历到的节点会做一下两个判断:

    延伸下来有三种情况:

    多节点diff

     
     
     
     
    1. function reconcileChildrenArray(
    2.     returnFiber: Fiber,
    3.     currentFirstChild: Fiber | null,
    4.     newChildren: Array<*>,
    5.     lanes: Lanes,
    6. ) {
    7.     let resultingFirstChild: Fiber | null = null;
    8.     let previousNewFiber: Fiber | null = null;
    9.     let oldFiber = currentFirstChild;
    10.     let lastPlacedIndex = 0;
    11.     let newIdx = 0;
    12.     let nextOldFiber = null;
    13.     for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    14.         const newFiber = updateSlot(
    15.         returnFiber,
    16.         oldFiber,
    17.         newChildren[newIdx],
    18.         lanes,
    19.         );
    20.         if (newFiber === null) {
    21.           break;
    22.         }
    23.           lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    24.           if (previousNewFiber === null) {
    25.             resultingFirstChild = newFiber;
    26.           } else {
    27.             previousNewFiber.sibling = newFiber;
    28.           }
    29.           previousNewFiber = newFiber;
    30.           oldFiber = nextOldFiber;
    31.     }
    32.     if (newIdx === newChildren.length) {
    33.         ...
    34.     }
    35.     if (oldFiber === null) {
    36.         ...
    37.     }
    38.     for (; newIdx < newChildren.length; newIdx++) {
    39.         ...
    40.     }
    41.     return resultingFirstChild;
    42. }
    43. function updateSlot(
    44.     returnFiber: Fiber,
    45.     oldFiber: Fiber | null,
    46.     newChild: any,
    47.     lanes: Lanes,
    48.   ): Fiber | null {
    49.     const key = oldFiber !== null ? oldFiber.key : null;
    50.     ...
    51.     if (newChild.key === key) {
    52.       return updateElement(returnFiber, oldFiber, newChild, lanes);
    53.     } else {
    54.       return null;
    55.     }
    56. }

    从源码我们可以看到,在 「reconcileChildrenArray」 中,出现了两个循环。

    第一轮循环中逻辑如下:

    可以看到第一轮循环只要碰到新旧的 key 不一样时就会跳出循环,换句话说,第一轮循环里做的事情都是基于 key 相同,主要就是「更新」的工作。

    跳出循环后,要先执行两个判断:

    如果以上两种情况都不是,则进入第二轮循环。

    在执行第二轮循环之前,先把剩下的旧节点和他们对应的 key 或者 index 做成映射,方便查找。

    第二轮循环沿用了第一轮循环的 newIdx 变量,说明第二轮循环是在第一轮循环结束的地方开始再次遍历剩下的 newChildren。

     
     
     
     
    1.   const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    2.   for (; newIdx < newChildren.length; newIdx++) {
    3.     const newFiber = updateFromMap(
    4.       existingChildren,
    5.       returnFiber,
    6.       newIdx,
    7.       newChildren[newIdx],
    8.       lanes,
    9.     );
    10.     if (newFiber !== null) {
    11.       if (shouldTrackSideEffects) {
    12.         if (newFiber.alternate !== null) {
    13.           existingChildren.delete(
    14.             newFiber.key === null ? newIdx : newFiber.key,
    15.           );
    16.         }
    17.       }
    18.       lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    19.       if (previousNewFiber === null) {
    20.         resultingFirstChild = newFiber;
    21.       } else {
    22.         previousNewFiber.sibling = newFiber;
    23.       }
    24.       previousNewFiber = newFiber;
    25.     }
    26.   }
    27.   
    28.   
    29.   function placeChild(
    30.   newFiber: Fiber,
    31.   lastPlacedIndex: number,
    32.   newIndex: number,
    33. ): number {
    34.   newFiber.index = newIndex;
    35.   if (!shouldTrackSideEffects) {
    36.     // Noop.
    37.     return lastPlacedIndex;
    38.   }
    39.   const current = newFiber.alternate;
    40.   if (current !== null) {
    41.     const oldIndex = current.index;
    42.     if (oldIndex < lastPlacedIndex) {
    43.       // This is a move.
    44.       newFiber.flags |= Placement;
    45.       return lastPlacedIndex;
    46.     } else {
    47.       // This item can stay in place.
    48.       return oldIndex;
    49.     }
    50.   } else {
    51.     // This is an insertion.
    52.     newFiber.flags |= Placement;
    53.     return lastPlacedIndex;
    54.   }
    55. }

    第二轮循环主要调用了 「updateFromMap」 来处理节点,在这里需要用 newChild 的 key 去 existingChildren 中找对应的 Fiber。

    不管是复用还是新增,「updateFromMap」 都会返回一个 newFiber,然后我们需要为这个 newFiber 更新一下它的位置(index),但是仅仅更新这个 Fiber 的 index 还不够,因为这个 Fiber 有可能是复用的,如果是复用的就意味着它已经有对应的真实 DOM 节点了,我们还需要复用它的真实 DOM,因此需要对应更新这个 Fiber 的 flag,但是真的需要对每个 Fiber 都去设置 flag 吗,我们举个例子:

     
     
     
     
    1. // 旧
    2. []
    3. // 新
    4. []

    如果按照我们刚刚说的做法,这里的 a, b, c 都会被打上 flag,这样一来,在 commit 阶段,这三个 DOM 都会被移动,可是我们知道,这里显然只需要移动一个节点即可,退一万步说我们移动两个节点也比移动所有节点要来的聪明。

    其实在这个问题上主要就是我们得区分一下到底哪个节点才是移动了的,这就需要一个参照点,我们要保证在参照点左边都是已经排好顺序了的。而这个参照点就是 lastPlacedIndex。有了它,我们在遍历 newChildren 的时候可能会出现下面两种情况:

    我们举一个例子:

           
     
     
     
    1. // 旧
    2. []
    3. // 新
    4. []

    lastPlacedIndex 初始值为 0,

    首先处理

    网站栏目:一文读懂React组件渲染核心原理
    分享地址:http://www.stwzsj.com/qtweb/news1/16151.html

    网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

    广告

    声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联

    猜你还喜欢下面的内容

    自适应网站知识

    同城分类信息