React_Fiber机制

大家好,我是「柒八九」

今天,又双叒叕yòu shuāng ruò zhuó开辟了一个新的领域--「前端框架」

这是继

  1. JS基础&原理
  2. JS算法
  3. 前端工程化
  4. 浏览器知识体系
  5. Css
  6. 网络通信

这些模块,又新增的知识体系。说起前端框架,大家肯定第一时间会联想到Vue/React,其实前端框架范围很广,它不应该被局限在Vue/React等主流库,还有很多在某些领域大放异彩的库和框架。例如

  • Lit
    • Google出的针对于WebComponent开发框架
  • svelte
    • RollupJs 的作者编写的「编译型框架」
    • 不再依赖Vritual-DOM进行页面构建

所以,我们在这个系列中,不仅仅会讲大家在工作中接触比较多的框架Vue/React,还有带着大家一起去探索前端其他领域比较新奇,并在后续工作中有用武之地的技术方案。

而,今天我们先简单描述一下React-Fiber的实现原理。

天不早了,我们干点正事哇。

这里给大家贴一个很早之前,画的关于Fiber的结构图。

(如果,看不清,可私聊,索要原图)

文章概要

  1. 背景介绍
  2. 从 React 元素React Element 到 Fiber 节点Fiber Node
  3. 副作用Side-effects
  4. Fiber 树的根节点
  5. 渲染算法

前言

React一个用于「构建用户界面」JavaScript 库。

❝它的「核心」「跟踪组件状态的变化」并将更新的状态投射到屏幕上。 在 React 中,我们把这个过程称为调和Reconciliation。我们调用 setState 方法,框架会检查状态state或属性props是否发生了变化,并在用户界面上重新显示一个组件。 ❞

React的文档对该机制提供了一个很好的概述:React元素的作用,生命周期方法和渲染方法,以及应用于组件子代的 diffing 算法。从渲染方法返回的不可变immutable的「React元素树」通常被称为虚拟DOMVirtual DOM 。这个术语有助于在早期向人们解释React,但它也造成了混乱,在React文档中已不再使用。在这文章中,我将坚持称它为React元素树Tree of React elements。

❝除了「React元素树」,该框架有一棵「内部实例树」(组件、DOM节点等),「用来保持状态」。 从「16版」开始,React推出了一个「新的」内部实例树的实现,以及管理它的算法,代号为Fiber。 ❞

「请注意」,这篇文章是关于 React 内部如何工作的,可能下面讲的东西,不会在实际工作中产生任何帮助。

正如「亚里士多德」把知识分为三类

  • 第一类是「经验」,会做但不知道为什么这么做是对的;
  • 第二类是知其然又知其所以然的「技术」,它来源于经验,是通过对经验的总结和归纳所形成的一般化理论;
  • 第三类是没有用的、自己为自己而存在的知识,就是科学

而我们平时在开发过程中,能够熟练使用React来构建UI,这是一种经验,而我们却很少对React内部实现原理深入了解,说明我们还未达到对React这个技术的更深层次的掌握。只有,对技术细节有一定的了解,才可以在后续的技术升级或者技术改造中游刃有余。更甚者,能够自己撸一个低级版的 UI 库。

1. 背景介绍

这里有一个简单的应用程序,该例子将贯穿整篇文章。我们有一个按钮,可以简单地增加屏幕上显示的数字。

对应的代码如下:(这里我们用Component实现)

class ClickCounter extends React.Component {
    constructor(props) {
        super(props);
        this.state = {count: 0};
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        this.setState((state) => {
            return {count: state.count + 1};
        });
    }


    render() {
        return [
            <button key="1" onClick={this.handleClick}>
              更新数字
            </button>,
            <span key="2">
              {this.state.count}
            </span>
        ]
    }
}

这是一个简单的组件,从渲染方法(render)中返回两个子元素 buttonspan。一旦你点击了按钮,组件的状态就会在处理程序中被更新。这反过来又会导致 span 元素的文本更新。

「调和」过程中,React 会执行各种操作。例如,以下是 React 在我们构建的应用中,在「第一次渲染」「状态更新后」所执行的操作。

  • 更新 ClickCounter 的状态中的 count 属性
  • 「检索和比较」 ClickCounter 的子元素和它们的props
  • 更新 span 元素的props

「调和」过程中还有其他操作,如调用生命周期方法或更新ref「所有这些操作在 Fiber 架构中都被统称为」 工作Work。「工作的类型通常取决于React元素的类型」。例如,对于一个类组件React 需要创建一个实例,而对于一个函数组件,它不需要这样做。

如你所知,我们在 React 中有许多种类的元素。

  • 类组件(React.Component)
  • 函数组件
  • 宿主组件(DOM节点)
  • Portals (将子节点渲染成存在于父组件的DOM层次之外的DOM节点)

React 元素的类型是由 createElement 函数的第一个参数定义的」。这个函数一般在render方法中使用,用于创建一个元素。而在React开发中,我们一般都使用JSX语法来定义元素(而JSXcreateElement的语法糖),「JSX 标签的第一部分决定了React元素的类型」。例如,

  • 以大写字母开头表示JSX标签是指一个「React组件」 <ClickCounter>
  • 以小写字母开头表示「宿主组件」或者自定义组件」 <button>/<p-test>

关于JSX,可以参考官网的,它有详细的解释。

在我们开始探索Fiber算法之前,首先让我们熟悉一下React内部使用的数据结构

2. 从 React 元素React Element 到 Fiber 节点Fiber Node

React 中的「每个组件都是一个UI表示」

这里是我们的 ClickCounter 组件的模板。

<button key="1" onClick={this.onClick}>
  更新数字
</button>
<span key="2">
  {this.state.count}
</span>

React 元素React Element

❝一旦模板通过JSX编译器JSX compiler,你最终会得到一堆「React元素」「这就是真正从 React 组件的渲染方法中返回的东西,而不是HTML」。 ❞

如果不需要使用 JSX语法,ClickCounter 组件的渲染方法可以重写如下方式。

class ClickCounter {
    ...
    render() {
        return [
            React.createElement(
                'button',
                {
                    key: '1',
                    onClick: this.onClick
                },
                '更新数字'
            ),
            React.createElement(
                'span',
                {
                    key: '2'
                },
                this.state.count
            )
        ]
    }
}

render方法中对 React.createElement调用「创建」这样的两个数据结构

[
    {
        $$typeof: Symbol(react.element),
        type: 'button',
        key: "1",
        props: {
            children: '更新数字',
            onClick: () => { ... }
        }
    },
    {
        $$typeof: Symbol(react.element),
        type: 'span',
        key: "2",
        props: {
            children: 0
        }
    }
]

你可以看到 React 给这些对象添加$$typeof属性,可以「标识它们是React元素」。然后还有「描述元素的属性 typekeyprops,这些值取自你传递给React.createElement函数内容

ClickCounter「React元素」没有任何propskey

{
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ClickCounter
}

Fiber 节点Fiber Node

❝在「调和过程」中,从render方法返回的「每个React元素的数据」都被合并到Fiber节点的树中。 与React元素不同,fiber「不会在每次渲染时重新创建」。这些是可变的数据结构mutable data structures,持有组件状态和 DOM信息 ❞

我们之前介绍过,根据「React元素的类型,React需要执行不同的操作」。在我们的示例应用程序中

  • 对于类组件ClickCounter,它调用生命周期方法和渲染方法
  • 而对于span宿主组件(DOM节点),它执行DOM变异。

因此,「每个React元素都被转换为相应类型的Fiber节点」,描述需要完成的工作。

❝可以把fiber看作是一个「数据结构」,它代表了一些要做的工作,或者说,一个工作单位」Fiber的架构还提供了一种方便的方式来「跟踪、安排、暂停和中止」工作。 ❞

一个「React元素」第一次被转换成一个「Fiber节点」时,React 使用该元素的数据在 createFiberFromTypeAndProps 函数中创建一个fiber。在随后的更新中,React「重用」Fiber节点,只是「使用来自相应 React元素 的数据更新必要的属性。如果相应的React元素不再从渲染方法中返回,React可能还需要根据关键props在层次结构中移动节点或删除它。

因为「React为每个React元素创建了一个fiber节点」,由于我们有一个由元素组成的element 树,所以我们也将有一个fiber节点组成的fiber树。在我们的示例应用程序中,它看起来像这样。

所有的「Fiber节点」都是通过childsiblingreturn属性构建成「链表」连接起来的。

Current Tree 和 workInProgress Tree

「在第一次渲染之后,React 最终会有一个 Fiber 树,它反映了用来渲染 UI 的应用程序的状态」。这个树通常被称为当前树Current Tree。 当React开始「状态更新」时,它建立了一个所谓的workInProgress 树workInProgress Tree,反映了「未来」将被刷新到屏幕上的状态。 ❞

「所有的工作都在workInProgress树fiber 上进行」。当React穿过current树时,「对于每个现有的fiber节点,它创建一个备用节点,构成 workInProgress树」。这个节点是「使用render方法返回的React元素的数据创建」的。一旦更新处理完毕,所有相关的工作都完成了,React 就会有一个备用的树,准备刷新到屏幕上。「一旦这个workInProgress树被渲染到屏幕上,它就成为current树」

React 的核心原则之一是「一致性」React 总是「一次性地更新DOM--它不会显示部分结果」workInProgress树作为一个用户不可见的草稿draft,这样 React 可以「先处理所有的组件,然后将它们的变化刷新到屏幕上」

在源代码中,你会看到很多函数current树WorkInProgress树获取fiber节点。下面是一个这样的函数的签名。

function updateHostComponent(
            current, 
            workInProgress, 
            ) {...}

❝每个fiber节点通过alternate属性保存着对「另一棵树」上的对应节点的引用。 current树一个节点指向workInProgress树的节点,反之亦然。 ❞

3. 副作用Side-effects

可以把React中的「组件看作是一个使用state和props来计算UI表现的函数

一个操作,如「DOM的突变」调用生命周期方法,都应该被视为一个「副作用」,或者简单地说,是一个效果effect。

❝从React组件中执行过「数据获取「事件订阅「手动改变DOM」。我们称这些操作为 "副作用"(或简称 "效果"),因为它们会影响其他组件,而且不能在渲染过程中进行。 ❞

你可以看到大多数stateprops的更新都会导致副作用的产生。由于「应用效果是一种工作类型」fiber节点是一种方便的机制,除了更新之外,还可以跟踪效果「每个fiber节点都可以有与之相关的效果。它们被编码在 effectTag 字段中」

所以「Fiber中的效果基本上定义了更新处理后需要对实例进行的工作」

  • 对于宿主组件(DOM元素),工作包括添加更新删除元素。
  • 对于类组件,React 可能需要「更新Refs」调用 componentDidMountcomponentDidUpdate 生命周期方法

效果清单Effects list

React处理更新的速度非常快,为了达到这种性能水平,它采用了一些有趣的技术。其中之一是「建立一个带有效果fiber节点的线性列表」,以便快速迭代。「迭代线性列表要比树形快得多」,而且不需要在没有副作用的节点上花费时间。

这个列表的目的是标记有DOM更新或其他与之相关的效果的节点」。这个「列表是 workInProgress 树一个子集」,并且使用 nextEffect 属性链接,而不是currentworkInProgress 树中使用的 child 属性

Dan Abramov对效果清单做了一个比喻。「把 React 应用想象成一棵圣诞树,用 "圣诞灯 "把所有有效果的节点绑在一起」。为了形象化这一点,让我们想象有下面的fiber节点树,并且做一些操作,c2「插入」到DOM中,d2c1「改变了属性b2 「触发了一个生命周期方法效果列表将它们联系在一起,这样React就可以在以后跳过其他节点。

从上图中可以看到带有效果的节点是如何连接在一起的。当访问这些节点时,React 使用 firstEffect 指针来计算「列表的开始位置」,用 nextEffect将拥有效果的节点连接起来。所以上图可以表示为这样的一个线性列表。

4. Fiber 树的根节点

❝每个React应用程序都有一个或多个DOM元素,作为容器。 ❞

在我们的例子中,它是ID为容器的div元素。

const domContainer = document.querySelector('#container');
ReactDOM.render(
            React.createElement(ClickCounter), 
            domContainer
        );

「React为每个容器创建一个fiber-root对象」。你可以使用DOM元素的引用来访问它。

const fiberRoot = query('#container')
                  ._reactRootContainer
                  ._internalRoot

「这个fiber-root是React保存对fiber树的引用的地方」。它被存储在fiber-rootcurrent属性中。

const hostRootFiberNode = fiberRoot.current

fiber树以一种特殊类型的fiber节点开始,它就是 HostRoot。它是在内部创建的,作为最上面的组件的父节点。通过 stateNode 属性,可以从 HostRoot fiber节点访问到 FiberRoot

fiberRoot.current.stateNode === fiberRoot; // true

你可以通过fiberRoot访问最上面的 HostRoot fiber节点来访问fiber树

你可以从一个组件实例中获得一个单独的fiber节点

compInstance._reactInternalFiber

Fiber-Node的数据结构

现在让我们来看看为 ClickCounter 组件创建的fiber节点的结构。

{
    stateNode: new ClickCounter,
    type: ClickCounter,
    alternate: null,
    key: null,
    updateQueue: null,
    memoizedState: {count: 0},
    pendingProps: {},
    memoizedProps: {},
    tag: 1,
    effectTag: 0,
    nextEffect: null
}

span DOM 元素的fiber节点的结构。

{
    stateNode: new HTMLSpanElement,
    type: "span",
    alternate: null,
    key: "2",
    updateQueue: null,
    memoizedState: null,
    pendingProps: {children: 0},
    memoizedProps: {children: 0},
    tag: 5,
    effectTag: 0,
    nextEffect: null
}

stateNode

❝保存对与fiber节点相关的组件、DOM节点或其他React元素类型的类实例「引用」

这个属性是用来保存与 fiber 相关的「本地状态」

type

❝定义了与该fiber相关的函数或类」

  • 对于类组件,它指向「构造函数
  • 对于DOM元素,它指定了「HTML标签

「使用这个字段来了解一个fiber节点与什么元素有关」

tag

「定义了fiber的类型」。 它在调和算法中被用来确定需要做什么工作。 ❞

如前所述,「工作根据React元素的类型而不同」函数 createFiberFromTypeAndProps一个React元素映射到相应的fiber节点类型。

在上面的实例中,ClickCounter 组件的属性标签是 1,表示 ClassComponent,对于 span 元素,它是 5,表示 HostComponent

updateQueue

「状态更新、回调和DOM更新的队列」

memoizedState

「用于创建输出的fiber的state 当处理更新时,它反映了「当前屏幕上」呈现的状态。 ❞

memoizedProps

❝在「上一次渲染过程」中用于创建输出fiberprops。 ❞

pendingProps

❝从React元素的「新数据」中更新的props,需要应用于子组件或DOM元素。 ❞

key

❝用于在一组子item「唯一标识」子项的字段。 ❞

以帮助React弄清哪些item已经改变,已经从列表中添加删除

5. 渲染算法

❝React的工作主要分两个阶段进行:渲染Render和提交Commit。 ❞

render阶段,React 通过 setStateReact.render对预定的组件进行更新,并找出UI中需要更新的内容

  • 如果是「初次渲染」Reactrender方法返回的每个元素创建一个「新的」fiber节点
  • 在接下来的「更新中」,现有 React元素fiber「重新使用和更新」

该阶段的结果是「一棵标有副作用的fiber节点树」。这些效果描述了在接下来的「提交阶段」要做的工作。在commit阶段,React 遍历标有效果fiber树,并将效果应用于实例。它遍历effect列表,执行DOM更新和其他用户可见的变化。

重要的是,render阶段的工作可以「异步进行」React 可以根据「可用的时间」来处理一个或多个fiber节点,然后停下来,把「已经完成的工作储存起来,并将处理fiber的操作」暂停yield。然后从上次离开的地方继续。但有时,可能需要丢弃已完成的工作并从头开始。针对在这个阶段执行的工作的暂停操作「不会导致任何用户可见的UI变化」,如DOM更新。相比之下,接下来的「提交阶段总是同步的」。这是因为在这个阶段进行的工作会导致用户可见的变化,例如DOM更新。这就是为什么React需要一次性完成这些工作。

调用生命周期的方法是React执行的一种工作类型。有些方法是在render阶段调用的,有些是在commit阶段调用的。下面是在「render阶段工作时调用的生命周期的列表」

  • [UNSAFE_]componentwillMount (废弃)
  • [UNSAFE_]componentwillReceiveProps (废弃)
  • static getDerivedStateFromProps
  • shouldComponentUpdate
  • [UNSAFE_]componentwillUpdate (废弃)
  • render

正如你所看到的,从16.3版本开始,一些在渲染阶段执行的传统生命周期方法标记UNSAFE。它们现在在文档中被称为「遗留生命周期」。它们将在未来的16.x版本中被废弃。

我们来简单解释下,为什么会有生命周期会被遗弃。

由于render阶段不会产生像DOM更新那样的副作用,React可以异步处理组件的更新(甚至有可能在多个线程中进行)。然而,标有 UNSAFE 的生命周期经常被滥用。开发者倾向于将有副作用的代码在这方法中,这可能会「给新的异步渲染方法带来问题」

下面是在commit阶段执行的生命周期方法的列表。

  • getSnapshotBeforeUpdate
  • componentDidMount
  • componentDidUpdate
  • componentwillUnmount

因为这些方法「同步提交阶段执行」,它们可能包含副作用并触及DOM。

这里我们贴一个针对react-16.4+版本的类组件的生命周期方法

Render 阶段

「调和算法」总是使用 renderRoot 函数从最上面的 HostRoot fiber节点开始。然而,React会跳过已经处理过的fiber节点,直到「找到工作未完成的节点」。 ❞

例如,如果你在组件树的深处调用 setState,React会从顶部开始,但迅速跳过父节点,直到它到达调用了setState方法的组件。

workLoop 主要流程

「所有」fiber节点都在 workLoop 中被处理 ❞

下面是该循环的同步部分的实现。

function workLoop(isYieldy) {
  if (!isYieldy) {
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performunitOfWork(nextUnitOfWork);
    }
  } else {...}
}

在上面的代码中,nextUnitOfWork 持有对来自 workInProgress 树fiber节点的引用,该节点有一些工作要做。当 React 遍历 Fiber 树时,它「使用这个变量来了解是否还有其他未完成工作的 Fiber 节点」。处理current fiber后,该变量将包含对树中下一个fiber节点的引用或为空。

有 4 个主要函数用于遍历树并启动或完成工作:

  1. performunitOfWork
  2. beginWork
  3. completeUnitOfWork
  4. completeWork

为了演示如何使用它们,请查看以下遍历fiber树的动图。每个函数都接收一个fiber节点并对其处理,当 React 沿着树向下移动时,您可以看到当前活动的fiber节点发生了变化。「它先完成孩子节点的处理,再转向其父节点」

❝请注意,「垂直连接」表示兄弟节点,而「水平连接」表示子节点, 例如 b1 没有孩子,而 b2一个孩子 c1。 ❞

可以将begin视为「进入」组件,将complete视为退出组件。

我们简单的分析「开始阶段的函数 performunitOfWorkbeginWork

function performunitOfWork(workInProgress) {
    let next = beginWork(workInProgress);
    if (next === null) {
        next = completeUnitOfWork(workInProgress);
    }
    return next;
}

function beginWork(workInProgress) {
    console.log('work performed for ' + workInProgress.name);
    return workInProgress.child;
}

performunitOfWork 函数workInProgress 树接收一个fiber节点,并通过调用 beginWork 函数开始工作。该函数将启动针对fiber的相关处理操作。

函数 beginWork 总是返回一个指向循环中要处理的「下一个子节点的指针或 null。」

  • 如果有下一个孩子,它将被分配给 workLoop 函数中的变量 nextUnitOfWork
  • 如果没有子节点,React 知道它到达了「分支的末尾」,因此它可以完成当前节点。

「节点完成后,需要为兄弟姐妹执行处理,然后回溯到父节点」。这些操作是在 completeUnitOfWork 函数中完成的:

function completeUnitOfWork(workInProgress) {
    while (true) {
        let returnFiber = workInProgress.return;
        let siblingFiber = workInProgress.sibling;

        nextUnitOfWork = completeWork(workInProgress);

        if (siblingFiber !== null) {
            // 如果存在兄弟节点,将其返回并对其处理
            return siblingFiber;
        } else if (returnFiber !== null) {
            // 兄弟节点不存在,父节点存在,返回父节点
            workInProgress = returnFiber;
            continue;
        } else {
            // 到达该分支的尾端
            return null;
        }
    }
}

function completeWork(workInProgress) {
    console.log('work completed for ' + workInProgress.name);
    return null;
}

代码中可以看到该函数一个「很大的 while 循环」。当 workInProgress 节点没有子节点时React 会进入此函数。在完成current fiber的工作后,它会检查是否有兄弟姐妹。如果找到,React 退出函数「返回指向兄弟的指针」。它将被分配给 nextUnitOfWork 变量,React 将从这个兄弟节点开始执行分支的工作。重要的是要理解,「此时 React 只完成了前面的兄弟姐妹的工作」。它还没有完成父节点的工作。「只有从子节点开始的所有分支都完成后,它才能执行回溯操作并完成父节点的工作」

代码实现中可以看出,performunitOfWorkcompleteUnitOfWork 都主要用于「迭代」,而「主要操作发生在 beginWorkcompleteWork 函数中」


Commit 阶段

该阶段从函数 completeRoot 开始。这是 React 更新 DOM 并调用「变动前后」生命周期方法的地方。

React 进入这个阶段时,它「有 2 棵树」

  • 「第一个树」代表当前在屏幕上呈现的状态。
  • 「第二个树」是在「render阶段」构建了一个备用树alternate tree。 它在源代码中称为 finishedWorkworkInProgress,表示「需要」在屏幕上反映的状态。 该备用树通过child指针和sibling指针进行各个节点的连接。

还有一个效果列表」——来自finishedWork树的节点「子集」,通过 nextEffect 指针链接。请记住,效果列表是render阶段的结果」。渲染的重点是确定哪些节点需要插入、更新或删除,哪些组件需要调用其生命周期方法。这就是效果列表告诉我们。「它正是在commit阶段需要处理的节点集」

commit阶段运行的主要函数commitRoot。基本上,它执行以下操作:

调用方法 getSnapshotBeforeUpdate 之后,React 将提交树中的所有副作用。它「分两次完成」

  • 「第一遍」执行所有 DOM 插入、更新、删除ref 卸载。 然后 ReactfinishedWork 树分配给 FiberRoot,并将 workInProgress 树标记current 树
  • 「第二遍中」React 调用所有生命周期方法ref 回调。

以下是运行上述步骤的函数的要点:

function commitRoot(root, finishedWork) {
    commitBeforeMutationLifecycles()
    commitAllHostEffects();
    root.current = finishedWork;
    commitAllLifeCycles();
}

❝这些「子函数中的每一个都实现了一个循环」,该循环遍历效果列表并检查效果的类型。当它找到与函数目的相关的效果时,它会应用它。 ❞

突变前的生命周期

下面是遍历效果树并检查节点是否具有Snapshot效果代码

function commitBeforeMutationLifecycles() {
    while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;
        if (effectTag & Snapshot) {
            const current = nextEffect.alternate;
            commitBeforeMutationLifeCycles(current, nextEffect);
        }
        nextEffect = nextEffect.nextEffect;
    }
}

对于类组件,此效果意味着调用 getSnapshotBeforeUpdate 生命周期方法

DOM更新

commitAllHostEffectsReact 执行 DOM 更新的函数。该函数基本上定义了需要对节点执行的操作类型并执行它:

function commitAllHostEffects() {
    switch (primaryEffectTag) {
        case Placement: {
            commitPlacement(nextEffect);
            ...
        }
        case PlacementAndUpdate: {
            commitPlacement(nextEffect);
            commitWork(current, nextEffect);
            ...
        }
        case Update: {
            commitWork(current, nextEffect);
            ...
        }
        case Deletion: {
            commitDeletion(nextEffect);
            ...
        }
    }
}

突变后的生命周期方法

commitAllLifecyclesReact 调用「所有剩余生命周期方法 componentDidUpdatecomponentDidMount函数

后记

分享是一种态度」

参考资料:

  • React-Fiber
  • React官网
  • 拉钩教育-React教

相关文章

一、前言 在组件方面react和Vue一样的,核心思想玩的就是组件...
前言: 前段时间学习完react后,刚好就接到公司一个react项目...
前言: 最近收到组长通知我们项目组后面新开的项目准备统一技...
react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom...