内容简介:译者: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简介、三种工作模式、十种调度算法)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web性能权威指南
Ilya Grigorik / 李松峰 / 人民邮电出版社 / 2013-9 / 69
本书是谷歌公司高性能团队核心成员的权威之作,堪称实战经验与规范解读完美结合的产物。本书目标是涵盖Web 开发者技术体系中应该掌握的所有网络及性能优化知识。全书以性能优化为主线,从TCP、UDP 和TLS 协议讲起,解释了如何针对这几种协议和基础设施来优化应用。然后深入探讨了无线和移动网络的工作机制。最后,揭示了HTTP 协议的底层细节,同时详细介绍了HTTP 2.0、 XHR、SSE、WebSoc......一起来看看 《Web性能权威指南》 这本书的介绍吧!