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

栏目: 编程工具 · 发布时间: 5年前

内容简介:译者:Kite作者:Gethyl George Kurian原文链接:

译者:Kite

作者:Gethyl George Kurian

原文链接: medium.com/@gethylgeor…

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

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

但是在我们开始之前,你有思考过为什么我们不直接渲染 DOM 的更新吗?

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

DOM 是如何创建的

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

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

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

因为 DOM 是一个树形结构,每次 DOM 中的某些部分发生变化时,虽然这些变化 已经相当地快了,但它改变的元素不得不经过 回流 的步骤,且它的子节点不得不被 重绘 ,因此,如果项目中越多的节点需要经历 回流/重绘 ,你的应用就会表现得越慢。

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

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

理解 Virtual-DOM

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

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

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

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

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

除了 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 长这样:

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

(初始渲染后的 DOM)

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

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

现在添加两个数字并点击「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>
复制代码

标记组件

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

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

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

  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 中的样子:

[译] Virtual Dom 和 Diff 算法在 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);
复制代码
[译] Virtual Dom 和 Diff 算法在 React 中是如何工作的?

在我们的例子中,您可以看到 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 中被重新构建

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

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

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

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

这个 reconciliation 过程通常采用以下步骤:

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

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

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

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

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

因此,你可以在我们的示例中看到这个 reconciliation 是如何像以下这样进行运作的:

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

我先跳过前一个 <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 期间,只有输出字段有如下所示的更改和在开发人员控制台出现绘制闪烁。

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

仅重绘输出

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

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

结论

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

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

reconciliation 过程就是 React

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

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Web性能权威指南

Web性能权威指南

Ilya Grigorik / 李松峰 / 人民邮电出版社 / 2013-9 / 69

本书是谷歌公司高性能团队核心成员的权威之作,堪称实战经验与规范解读完美结合的产物。本书目标是涵盖Web 开发者技术体系中应该掌握的所有网络及性能优化知识。全书以性能优化为主线,从TCP、UDP 和TLS 协议讲起,解释了如何针对这几种协议和基础设施来优化应用。然后深入探讨了无线和移动网络的工作机制。最后,揭示了HTTP 协议的底层细节,同时详细介绍了HTTP 2.0、 XHR、SSE、WebSoc......一起来看看 《Web性能权威指南》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具