译者:Kite
做者:Gethyl George Kurian
原文连接:medium.com/@gethylgeor…react
文章已通过时,基于 react v16 的将会近期发出,此文仅供参考 git
我曾经尝试去深层而清晰地去理解 Virtual-DOM
的工做原理,也一直在寻找能够更详细地解释其工做细节的资料。github
因为在我大量搜索的资料中没有获取到一点有用的资料,我最终决定探究 react
和 react-dom
的源码来更好地理解它们的工做原理。算法
可是在咱们开始以前,你有思考过为何咱们不直接渲染
DOM
的更新吗?app
接下来的一节中,我将介绍 DOM
是如何建立的,以及让你了解为何 React
一开始就建立了 Virtual-DOM
dom
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 dom
与diff
算法是如何工做的,一旦你理解了这个过程,理解初始的渲染就变得很简单:)。
能够在这个git repo 上找到这个 app 的源码。这个简单的计算器界面长这样:
除了 Main.js
和 Calculator.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 树的结构:
为了去理解 Diff
算法是如何工做及reconciliation
如何调度 virtual-dom
到真实的DOM
的,在这个计算器中,我将输入 100 和 50 并点击「Add」按钮,期待输出 150:
输入1: 100
输入2: 50
输出: 150
复制代码
在咱们的例子中,当点击了「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>
复制代码
(注: 将发生变化的组件)
首先,让咱们理解第一步,一个组件是如何被标记的:
全部的 DOM
事件监听器都被包裹在 React
自定义的事件监听器中,所以,当点击「Add」按钮时,这个点击事件被发送到 react 的事件监听器,从而执行上面代码中你所看到的匿名函数
在匿名函数中,咱们调取 this.setState
方法获得了一个新的 state 值。
这个 setState()
方法将如如下几行代码同样,依次标记组件。
// ReactUpdates.js - enqueueUpdate(component) function
dirtyComponents.push(component);
复制代码
你是否在思考为何 react 不直接标记这个 button, 而是标记整个组件?好了,这是由于你用了
this.setState()
来调取setState
方法,而这个 this 指向的就是这个 Calculator 组件
很好!如今这个组件被标记了,那么接下来会发生什么呢?接下来是更新 virtual dom
,而后使用diff
算法作 reconciliation
并更新真实的 DOM
在咱们进行下一步以前,熟悉组件生命周期的不一样之处是很是重要的
如下是咱们的 Calculator 组件在 react
中的样子:
Calculator Wrapper
如下是这个组件被更新的步骤:
这是经过 react
运行批量更新而更新的;
在批量更新中,它会检查是否组件被标记,而后开始更新。
//ReactUpdates.js
var flushBatchedUpdates = function () {
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
复制代码
forceUpdate
。if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
复制代码
在咱们的例子中,您能够看到 this._pendingStateQueue
在具备新输出状态的计算器包装器里
首先,它会检查咱们是否使用了componentWillReceiveProps()
,若是咱们使用了,则容许使用收到的 props
更新 state
。
接下来,react
会检查咱们在组件里是否使用了 shouldComponentUpdate()
,若是咱们使用了,咱们能够检查一个组件是否须要根据它的 state
或 props
的改变而从新渲染。
当你知道不须要从新渲染组件时,请使用此方案,从而提升性能
componentWillUpdate()
, render()
, 最后是 componentDidUpdate()
从第 4,5 和 6 步, 咱们只使用
render()
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
)