[译] Virtual Dom 和 Diff 算法在 React 中是如何工做的?

译者:Kite
做者:Gethyl George Kurian
原文连接:medium.com/@gethylgeor…react

文章已通过时,基于 react v16 的将会近期发出,此文仅供参考 git

我曾经尝试去深层而清晰地去理解 Virtual-DOM 的工做原理,也一直在寻找能够更详细地解释其工做细节的资料。github

因为在我大量搜索的资料中没有获取到一点有用的资料,我最终决定探究 reactreact-dom 的源码来更好地理解它们的工做原理。算法

可是在咱们开始以前,你有思考过为何咱们不直接渲染DOM的更新吗?app

接下来的一节中,我将介绍 DOM 是如何建立的,以及让你了解为何 React 一开始就建立了 Virtual-DOMdom

DOM 是如何建立的

(图片来自 Mozilla - https://developer.mozilla.org/en-US/docs/Introduction_to_Layout_in_Mozilla)函数

我不会说太多关于 DOM 是如何建立且是如何绘制到屏幕上的,但能够查阅这里这里去理解将整个 HTML 转换成 DOM 以及绘制到屏幕的步骤。性能

由于 DOM 是一个树形结构,每次DOM 中的某些部分发生变化时,虽然这些变化 已经至关地快了,但它改变的元素不得不通过回流的步骤,且它的子节点不得不被重绘,所以,若是项目中越多的节点须要经历回流/重绘,你的应用就会表现得越慢。ui

什么是 Virtual-DOM ? 它尝试去最小化回流/重绘步骤,从而在大型且复杂的项目中获得更好的性能。this

接下来一节中将会解释更多有关于Virtual-DOM 如何工做的细节。

理解 Virtual-DOM

既然你已经了解了 DOM 是如何构建的,那如今就让咱们去更多地了解一下 Virtual-DOM吧。

在这里,我会先用一个小型的 app 去解释 virtual dom 是如何工做的,这样,你能够容易地去看到它的工做过程。

我不会深刻到最初渲染的工做细节,仅关注从新渲染时所发生的事情,这将帮助你去理解 virtual domdiff 算法是如何工做的,一旦你理解了这个过程,理解初始的渲染就变得很简单:)。

能够在这个git repo 上找到这个 app 的源码。这个简单的计算器界面长这样:

除了 Main.jsCalculator.js以外,在这个 repo 中的其余文件均可以不用关心。

// Calculator.js
import React from "react"
import ReactDOM from "react-dom"

export default class Calculator extends React.Component{
	constructor(props) {
		super(props);
		this.state = {output: ""};
	}

	render(){
		let IntegerA,IntegerB,IntegerC;
		

		return(
			<div className="container"> <h2>using React</h2> <div>Input 1: <input type="text" placeholder="Input 1" ref="input1"></input> </div> <div>Input 2 : <input type="text" placeholder="Input 2" ref="input2"></input> </div> <div> <button id="add" onClick={ () => { IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value) IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value) IntegerC = IntegerA+IntegerB this.setState({output:IntegerC}) } }>Add</button> <button id="subtract" onClick={ () => { IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value) IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value) IntegerC = IntegerA-IntegerB this.setState({output:IntegerC}) } }>Subtract</button> </div> <div> <hr/> <h2>Output: {this.state.output}</h2> </div> </div>
		);
	}
}
复制代码
// Main.js
import React from "react";
import Calculator from "./Calculator"

export default class Layout extends React.Component{
	render(){	

		return(
			<div> <h1>Basic Calculator</h1> <Calculator/> </div>
		);
	}
}
复制代码

初始加载时产生的 DOM 长这样:

(初始渲染后的 DOM)

下面是 React 内部构建的上述 DOM 树的结构:

如今添加两个数字并点击「Add」按钮去更深刻的理解

为了去理解 Diff 算法是如何工做及reconciliation 如何调度 virtual-dom 到真实的DOM 的,在这个计算器中,我将输入 100 和 50 并点击「Add」按钮,期待输出 150:

输入1: 100
输入2: 50

输出: 150
复制代码

那么,当你按下「Add」按钮时,发生了什么?

在咱们的例子中,当点击了「Add」按钮,咱们 set 了一个包含有输出值 150 的 state:

// Calculator.js
 <button id="add" onClick={() => {
        IntegerA = parseInt(ReactDOM.findDOMNode(this.refs.input1).value);
        IntegerB = parseInt(ReactDOM.findDOMNode(this.refs.input2).value);
        IntegerC = IntegerA+IntegerB;
        this.setState({output:IntegerC});
      }}>Add</button>
复制代码

标记组件

(注: 将发生变化的组件)

首先,让咱们理解第一步,一个组件是如何被标记的:

  1. 全部的 DOM 事件监听器都被包裹在 React 自定义的事件监听器中,所以,当点击「Add」按钮时,这个点击事件被发送到 react 的事件监听器,从而执行上面代码中你所看到的匿名函数

  2. 在匿名函数中,咱们调取 this.setState 方法获得了一个新的 state 值。

  3. 这个 setState() 方法将如如下几行代码同样,依次标记组件。

// ReactUpdates.js - enqueueUpdate(component) function
dirtyComponents.push(component);
复制代码

你是否在思考为何 react 不直接标记这个 button, 而是标记整个组件?好了,这是由于你用了this.setState() 来调取 setState 方法,而这个 this 指向的就是这个 Calculator 组件

  1. 因此如今,咱们的 Calculator 组件被标记了,让咱们看看接下来又将发生什么。

遍历组件的生命周期

很好!如今这个组件被标记了,那么接下来会发生什么呢?接下来是更新 virtual dom,而后使用diff 算法作 reconciliation 并更新真实的 DOM

在咱们进行下一步以前,熟悉组件生命周期的不一样之处是很是重要的

如下是咱们的 Calculator 组件在 react 中的样子:

Calculator Wrapper

如下是这个组件被更新的步骤:

  1. 这是经过 react 运行批量更新而更新的;

  2. 在批量更新中,它会检查是否组件被标记,而后开始更新。

//ReactUpdates.js
var flushBatchedUpdates = function () {
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
复制代码
  1. 接下来,它会检查是否存在必须更新的待处理状态或是否发出了forceUpdate
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
复制代码

在咱们的例子中,您能够看到 this._pendingStateQueue 在具备新输出状态的计算器包装器里

  1. 首先,它会检查咱们是否使用了componentWillReceiveProps(),若是咱们使用了,则容许使用收到的 props 更新 state

  2. 接下来,react 会检查咱们在组件里是否使用了 shouldComponentUpdate() ,若是咱们使用了,咱们能够检查一个组件是否须要根据它的 stateprops 的改变而从新渲染。

当你知道不须要从新渲染组件时,请使用此方案,从而提升性能

  1. 接下来的步骤依次是 componentWillUpdate(), render(), 最后是 componentDidUpdate()

从第 4,5 和 6 步, 咱们只使用 render()

  1. 如今,让咱们深刻看看 render() 期间发生了什么?

渲染便是 Virtual-DOM 比较差别并从新构建

渲染组件 - 更新 Virtual-DOM, 运行diff 算法并更新到真实的DOM

在咱们的例子中,全部在这个组件里的元素都会在 Virtual-DOM 中被从新构建

它会检查相邻已渲染的元素是否具备相同的类型和键,而后协调这个类型与键匹配的组件。

var prevRenderedElement = this._renderedComponent._currentElement;
 //Calculator.render() method is called and the element is build.
 var nextRenderedElement = this._instance.render(); 
复制代码

有一个重要的点就是这里是调用组件 render 方法的地方。好比,Calculator.render()

这个 reconciliation 过程一般采用如下步骤:

组件的 render 方法 - 更新Virtual DOM,运行 diff 算法,最后更新 DOM

红色虚线意味着全部的reconciliation 步骤都将在下一个子节点及子节点中的子节点里重复。

上述的流程图总结了 Virtual DOM 是如何更新实际 DOM 的。

我可能在知情或不知情的状况下错过了几个步骤,但此图表涵盖了大部分关键步骤。

所以,你能够在咱们的示例中看到这个reconciliation是如何像如下这样进行运做的:

我先跳过前一个<div>reconciliation ,引导你看看 DOM 变成 Output:150 的更新步骤,

  • Reconciliation 从这个组件的类名为 "container" 的<div> 开始
  • 它的孩子是一个包含了输出的<div>, 所以,react 将从这个子节点开始reconciliation
  • 如今这个子节点拥有了子节点 <hr><h2>
  • 因此 react 将为 <hr> 执行reconciliation
  • 接下来,它将从 <h2>reconciliation 开始,由于它有本身的子节点,即输出和 state 的输出,它将开始对这两个进行reconciliation
  • 第一个输出文本通过了reconciliation,由于它没有任何变化,因此 DOM 没有什么须要改变。
  • 接下来,来自 state 的输出通过reconciliation,由于咱们如今有了一个新值,即 150,react 会更新真实的 DOM。 ...

真实 DOM 的渲染

咱们的例子中,在 reconciliation 期间,只有输出字段有以下所示的更改和在开发人员控制台出现绘制闪烁。

仅重绘输出

以及在真实 DOM上更新的组件树

结论

结论虽然这个例子很是简单,但它可让你基本了解react 内部所发生的事情。

我没有选择更复杂的应用程序是由于绘制整个组件树真的很烦人。:-|

reconciliation 过程就是 React

  • 比较前一个的内部实例与下一个内部实例
  • 更新内部实例 Virtual DOM(JavaScript 对象) 中的组件树结构。
  • 仅更新存在实际变化的节点及其子节点的真实 DOM

(注: 做者文中的 react 版本是 v15.4.1)

相关文章
相关标签/搜索