当创建一个元素比如 div,有以下几项内容需要实现: HTML element、Element、GlobalEventHandler。简单的说,就是插入一个 Dom 元素的时候,这个元素上本身或者继承很多属性如 width、height、offsetHeight、style、title,另外还需要注册这个元素的诸多方法,比如 onfocus、onclick 等等。 这还只是一个元素,如果元素比较多的时候,还涉及到嵌套,那么元素的属性和方法等等就会很多,效率很低。
另外,Flux 虽然说的是单向的 Data Flow(redux 也是),但是实际上就是单向的 Observer,Store->View->Action->Store(箭头是数据流向,实现上可以理解为 View 监听 Store,View 直接 trigger action,然后 Store 监听 Action)。
2.2 实现
那么 react 如何实现呢? 最简单的方法就是当数据变化时,我直接把原先的 DOM 卸载,然后把最新数据的 DOM 替换上去。 但是,虚拟 DOM 哪去了? 这样做的效率显然是极低的。
所以虚拟 DOM 就来救场了。
那么虚拟 DOM 和 DOM 之间的关系是什么呢?
首先,Virtual DOM 并没有完全实现 DOM,即虚拟 DOM 和真正地 DOM 是不一样的,Virtual DOM 最主要的还是保留了 Element 之间的层次关系和一些基本属性。因为真实 DOM 实在是太复杂,一个空的 Element 都复杂得能让你崩溃,并且几乎所有内容我根本不关心好吗。所以 Virtual DOM 里每一个 Element 实际上只有几个属性,即最重要的,最为有用的,并且没有那么多乱七八糟的引用,比如一些注册的属性和函数啊,这些都是默认的,创建虚拟 DOM 进行 diff 的过程中大家都一致,是不需要进行比对的。所以哪怕是直接把 Virtual DOM 删了,根据新传进来的数据重新创建一个新的 Virtual DOM 出来都非常非常非常快。(每一个 component 的 render 函数就是在做这个事情,给新的 virtual dom 提供 input)。
所以,引入了 Virtual DOM 之后,React 是这么干的:你给我一个数据,我根据这个数据生成一个全新的 Virtual DOM,然后跟我上一次生成的 Virtual DOM 去 diff,得到一个 Patch,然后把这个 Patch 打到浏览器的 DOM 上去。完事。并且这里的 patch 显然不是完整的虚拟 DOM,而是新的虚拟 DOM 和上一次的虚拟 DOM 经过 diff 后的差异化的部分。
这里你可以做一些小实验,去破坏 VirtualDom1 == DOM1 这个假设(手动在 DOM 里删除一些 Element,这时候 VirtualDom 里的 Element 没有被删除,所以两边不一样了)。 然后给新的数据,你会发现生成的界面就不是你想要的那个界面了。
最后,回到为什么 Virtual Dom 快这个问题上。 其实是由于每次生成 virtual dom 很快,diff 生成 patch 也比较快,而在对 DOM 进行 patch 的时候,虽然 DOM 的变更比较慢,但是 React 能够根据 Patch 的内容,优化一部分 DOM 操作,比如之前的那个例子。
重点就在最后,哪怕是我生成了 virtual dom(需要耗费时间),哪怕是我跑了 diff(还需要花时间),但是我根据 patch 简化了那些 DOM 操作省下来的时间依然很可观(这个就是时间差的问题了,即节省下来的时间 > 生成 virtual dom 的时间 + diff 时间)。所以总体上来说,还是比较快。
简单发散一下思路,如果哪一天,DOM 本身的操作已经非常非常非常快了,并且我们手动对于 DOM 的操作都是精心设计优化过后的,那么加上了 VirtualDom 还会快吗? 当然不行了,毕竟你多做了这么多额外的工作。
用 JavaScript 对象来表示 DOM 树的结构; 然后用这个树构建一个真正的 DOM 树,插入到文档中。
当状态变更的时候,重新构造一个新的对象树,然后用这个新的树和旧的树作对比,记录两个树的差异。
把 2 所记录的差异应用在步骤一所构建的真正的 DOM 树上,视图就更新了。
Virtual DOM 的本质就是在 JS 和 DOM 之间做一个缓存,可以类比 CPU 和硬盘,既然硬盘这么慢,我们就也在他们之间添加一个缓存; 既然 DOM 这么慢,我们就可以在 JS 和 DOM 之间添加一个缓存。 CPU(JS)只操作内存(虚拟 DOM),最后的时候在把变更写入硬盘(DOM)。
算法实现
1、 用 JavaScript 对象模拟 DOM 树
用 JavaScript 对象来模拟一个 DOM 节点并不难,你只需要记录他的节点类型(tagName)、属性(props)、子节点(children)。
element.js
1 2 3 4 5 6 7 8
function Element(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; } module.exports = function (tagName, props, children) { return new Element(tagName, props, children); }
Element.prototype.render = function () { // 根据tagName创建一个真实的元素 var el = document.createElement(this.tagName); // 得到这个元素的属性对象,方便我们遍历。 var props = this.props;
for (var propName in props) { // 获取到这个元素值 var propValue = props[propName];
var ul = new Element('ul', {id: 'list'}, [ new Element('li', {class: 'item'}, ['item1']), new Element('li', {class: 'item'}, ['item2']), new Element('li', {class: 'item'}, ['item3']) ]);
Element.prototype.render = function () { // 根据tagName创建一个真实的元素 var el = document.createElement(this.tagName); // 得到这个元素的属性对象,方便我们遍历。 var props = this.props;
for (var propName in props) { // 获取到这个元素值 var propValue = props[propName];
var ulRoot = ul.render(); document.body.appendChild(ulRoot); </script> </body> </html>
上面的这段代码,就可以渲染出下面的结果了:
2、比较两颗虚拟 DOM 树的差异
比较两颗 DOM 树的差异是 Virtual DOM 算法中最为核心的部分,这也就是所谓的 Virtual DOM 的 diff 算法。 两个树的完全的 diff 算法是一个时间复杂度为 O(n3) 的问题。 但是在前端中,你会很少跨层地移动 DOM 元素,所以真实的 DOM 算法会对同一个层级的元素进行对比。
上图中,div 只会和同一层级的 div 对比,第二层级的只会和第二层级对比。 这样算法复杂度就可以达到 O(n)。
那么如果我们把 div 的子节点重新排序下了呢? 比如 p、ul、div 的顺序换成了 div、p、ul,那么这个该怎么对比呢? 如果按照同级进行顺序对比的话,他们就会被替换掉,如 p 和 div 的 tagName 不同,p 就会被 div 所代替,最终,三个节点就都会被替换,这样 DOM 开销就会非常大,而实际上是不需要替换节点的,只需要移动就可以了, 我们只需要知道怎么去移动。这里牵扯到了两个列表的对比算法,如下。
(3)列表对比算法
假设现在可以用英文字母唯一地标识每一个子节点:
旧的节点顺序:
1
a b c d e f g h i
现在对节点进行了删除、插入、移动的操作。新增 j 节点,删除 e 节点,移动 h 节点:
新的节点顺序:
1
a b c h d f g i j
现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。这个问题抽象出来其实是字符串的最小编辑距离问题(Edition Distance),最常见的解决算法是 Levenshtein Distance,通过动态规划求解,时间复杂度为 O(M * N)。但是我们并不需要真的达到最小的操作,我们只需要优化一些比较常见的移动情况,牺牲一定 DOM 操作,让算法时间复杂度达到线性的(O(max(M, N))。具体算法细节比较多,这里不累述,有兴趣可以参考代码。
我们能够获取到某个父节点的子节点的操作,就可以记录下来:
1 2 3 4
patches[0] = [{ type: REORDER, moves: [{remove or insert}, {remove or insert}, ...] }]
但是要注意的是,因为 tagName 是可重复的,不能用这个来进行对比。所以需要给子节点加上唯一标识 key,列表对比的时候,使用 key 进行对比,这样才能复用老的 DOM 树上的节点。
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches) }
function dfsWalk (node, walker, patches) { var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异
var len = node.childNodes ? node.childNodes.length : 0 for (var i = 0; i < len; i++) { // 深度遍历子节点 var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) }
if (currentPatches) { applyPatches(node, currentPatches) // 对当前节点进行DOM操作 } }
捕获到异常:{message: "Uncaught ReferenceError: Jartto is not defined", source: "http://127.0.0.1:8001/", lineno: 36, colno: 5, error: ReferenceError: Jartto is not defined at setTimeout (http://127.0.0.1:8001/:36:5)}
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service logErrorToMyService(error, info); } render() { if (this.state.hasError) { // You can render any custom fallback UI return
//标记法 function fn(m, n) { var count = ""; for (i = m; i <= n; i++) {//第一次循环 var flage = true;//设一个标记 for (j = 2; j < i; j++) { if (i % j === 0) {//第二次循环 flage = false;//不满足条件改变标记, break;//跳出循环 } } if (flage) {//满足条件,也就是为true时 count += i + "," } } return count } console.log(fn(100, 200)) //调用函数,输出值
//计数法 function fn(m, n) { var sun = ""; for (i = m; i <= n; i++) {//第一次循环 var count = 0;//初值为零 for (j = 2; j < i; j++) {//第二次循环 if (i % j === 0) {//不满足条件加1 count+=1 } } if (count === 0) { sun += i + "," } } return sun } console.log(fn(100, 200))