内容简介:译者:Kite作者:Gethyl George Kurian原文链接:
译者:Kite
作者:Gethyl George Kurian
原文链接: medium.com/@gethylgeor…
我曾经尝试去深层而清晰地去理解 Virtual-DOM 的工作原理,也一直在寻找可以更详细地解释其工作细节的资料。
由于在我大量搜索的资料中没有获取到一点有用的资料,我最终决定探究 react 和 react-dom 的源码来更好地理解它们的工作原理。
但是在我们开始之前,你有思考过为什么我们不直接渲染 DOM 的更新吗?
接下来的一节中,我将介绍 DOM 是如何创建的,以及让你了解为什么 React 一开始就创建了 Virtual-DOM
DOM 是如何创建的
(图片来自 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 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 树的结构:
现在添加两个数字并点击「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>
复制代码
标记组件
(注: 将发生变化的组件)
首先,让我们理解第一步,一个组件是如何被标记的:
-
所有的
DOM事件监听器都被包裹在React自定义的事件监听器中,因此,当点击「Add」按钮时,这个点击事件被发送到 react 的事件监听器,从而执行上面代码中你所看到的匿名函数 -
在匿名函数中,我们调取
this.setState方法得到了一个新的 state 值。 -
这个
setState()方法将如以下几行代码一样,依次标记组件。
// ReactUpdates.js - enqueueUpdate(component) function dirtyComponents.push(component); 复制代码
你是否在思考为什么 react 不直接标记这个 button, 而是标记整个组件?好了,这是因为你用了 this.setState() 来调取 setState 方法,而这个 this 指向的就是这个 Calculator 组件
- 所以现在,我们的 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 )
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 反向传播算法如何工作
- AES 加密算法工作原理
- 工作职位推荐系统的算法与架构
- 实际工作中用不上数据结构和算法吗?
- 干货!推荐算法工程师学习路线及工作指南
- LVS负载均衡(LVS简介、三种工作模式、十种调度算法)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
编写可读代码的艺术
Boswell, D.、Foucher, T. / 尹哲、郑秀雯 / 机械工业出版社 / 2012-7-10 / 59.00元
细节决定成败,思路清晰、言简意赅的代码让程序员一目了然;而格式凌乱、拖沓冗长的代码让程序员一头雾水。除了可以正确运行以外,优秀的代码必须具备良好的可读性,编写的代码要使其他人能在最短的时间内理解才行。本书旨在强调代码对人的友好性和可读性。 本书关注编码的细节,总结了很多提高代码可读性的小技巧,看似都微不足道,但是对于整个软件系统的开发而言,它们与宏观的架构决策、设计思想、指导原则同样重要。编......一起来看看 《编写可读代码的艺术》 这本书的介绍吧!