秋招复习计划-Vue知识点1

  |  

前言

为了帮助学友复习,所以我先整理一下虚拟DOM及其递归创建,以后我的博客就不只是为自己写了,要减少错别字,减轻学友的阅读负担。学友加油,冲呀!


39



为什么要使用虚拟DOM

在JS从输入url到页面展示的复习中,我们知道了浏览器解析渲染真实DOM的流程。如果使用原生JS或JQ操作DOM,浏览器会从构建DOM树开始从头到尾执行一遍流程。如果在一次操作中,我们需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道后面还有9次更新请求,因此会马上进行重构并解析渲染DOM的流程。后面9次也是如此。即使计算机硬件一直在迭代更新,操作DOM的代价仍旧是昂贵的,频繁操作会出现页面卡顿,影响用户体验。因此虚拟DOM的被设计了出来,用来解决浏览器性能问题。

我们将真实DOM抽象为虚拟DOM,然后通过新旧虚拟DOM这两个对象的差异(Diff算法),生成补丁对象patch,将其应用到实际的DOM,完成更新。

虚拟DOM3

这就是所谓的Virtual DOM算法。它包括几个步骤:

  • 用JavaScript对象结构表示DOM树的结构;然后用这个树构建一个正真的DOM树,插入到文档当中
  • 当状态变更的时候,重新构造一棵新的对象树,然后用新的树和旧的树进行比较,记录两颗树间的差异
  • 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图更新

虚拟DOM1

虚拟DOM2

Virtual DOM本质上就是在JS和DOM之间做一个缓存。类比CPU和硬盘,硬盘读写很慢,所以我们在硬盘和CPU之间加一个内存,同理DOM操作很慢,所以我们在DOM和JS之间加一个Virtual DOM。CPU(JS)只操作内存(Virtual DOM),最后的时候将变更写入硬盘(DOM)

创建虚拟DOM对象

假设我们现在有这么一个DOM结构

1
2
3
4
5
<ul id='list'>
<li class='item'>Item 1</li>
<li class='item'>Item 2</li>
<li class='item'>Item 3</li>
</ul>

我们如何用JS对象来模拟这个DOM结构呢。首先分析我们需要记录的信息类型有:节点类型、节点属性、子节点。那么我们创建一个类型

1
2
3
4
5
6
7
8
9
10
class Element {
constructor(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
}
module.exports = function (tagName, props, children) {
return new Element(tagName, props, children);
}

我们很简单的就将DOM节点用JS对象模拟出来了,并把它打包,好在后面引用。

下面我们就用这个节点类型来创建虚拟DOM

1
2
3
4
5
6
let el = require('./Element');
let ul = el('ul', {id: 'list'}, [
el('li', {class: 'item'}, ['Item 1']),
el('li', {class: 'item'}, ['Item 2']),
el('li', {class: 'item'}, ['Item 3'])
])

这就是虚拟DOM的递归创建。来看一下运行结果

创建虚拟DOM

虚拟DOM的render

之前我们已经能将一个DOM结构抽象成虚拟DOM了,那么我们如何将虚拟DOM的结构编程真实的DOM结构,并且可以添加到页面中呢。这里我们为Element类型添加一个原型方法render

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Element {
constructor(tagName, props, children) {
this.tagName = tagName;
this.props = props;
this.children = children;
}
render() {
let el = document.createElement(this.tagName);
for(let propName in this.props) {
el.setAttribute(propName,this.props[propName]);
}
this.children.forEach(children => {
let childElement = (children instanceof Element) ? children.render() : document.createTextNode(children); //这里也是一个递归调用创建子节点
el.appendChild(childElement);
})
return el;
}
}

我们尝试用这个方式来创建真实的DOM树,并把他添加到页面中

创建DOM

Diff算法

比较两颗DOM树的差异是Virtual DOM算法最核心的部分。简单的来说就是新旧虚拟DOM进行比较,如果有差异就以新的为准,然后再插入真实的DOM中,重新渲染

diff

需要注意的是:diff的比较只会在同级进行,不会跨级进行

传统的Diff算法一直存在,但是它的时间复杂度是O(n^3),意思是当你对虚拟DOM进行10次修改,Diff算法要进行1000次比较。

于是为了进行优化,将O(n^3)复杂度的问题转换为O(n)的问题,虚拟DOM做出了大胆的决策:

  • 两个不同类型的元素会产生不同的树
  • 对于同一层级的一组节点,它们可以通过唯一的key进行区分

基于这两个前提策略对Diff算法进行了优化(这里我也没看懂为什么,先写下来,以后再说)

在比较之后,可能出现四种情况:

  • 此节点是否被移除——>移除旧节点,可能添加新节点
  • 属性是否被改变——>就属性修改为新属性
  • 文本内容是否被改变——>旧内容改为新内容
  • 节点类型改变——> 移除原节点,替换为新节点

Diff.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
let utils = require('./utils'); //自己写的工具包

let keyIndex = 0; //子节点序号
function diff(oldTree, newTree) {
let patches = {} //补丁对象
keyIndex = 0;
let index = 0; //节点序号
walk(oldTree, newTree, index, patches);
return patches;
}

function walk(oldNode, newNode, index, patches) {
let currentPatches = []; //记录这个节点的所有变化
if(!newNode) {
//如果新节点不存在,说明此节点删除,记录变化类型和节点序号
currentPatches.push({type: utils.REMOVE, index});
} else if (utils.isString(oldNode) && utils.isString(newNode)) {
if(oldNode != newNode) {
//如果新旧节点都为文本,但是不相等,说明文本节点改变,记录变化类型和新文本内容
currentPatches.push({type: utils.TEXT, content: newNode});
}
} else if(oldNode.tagName == newNode.tagName) {
//新旧节点类型相同
//判断新旧节点的属性是否相同
let propsPatch = diffAttr(oldNode.props, newNode.props);
if(Object.keys(propsPatch).length > 0) {
//如果属性不同,记录变化类型和返回的属性补丁
currentPatches.push({type: utils.PROPS, props: propsPatch});
}
//当前节点判断完毕,开始比较两个节点的子节点
diffChildren(oldNode.children, newNode.children, index, patches, currentPatches);
} else {
//之前的情况都不满足,判断新节点要完全替代旧节点
currentPatches.push({type: utils.REPLACE, node: newNode})
}
if(currentPatches.length > 0) {
//如果当前节点有变化,将结果压入总补丁
patches[index] = currentPatches;
}
}

//子节点比较函数
function diffChildren(oldChildren, newChildren, index, patches, currentPatches) {
oldChildren.forEach((child, idx) => {
walk(child, newChildren[idex], ++keyIndex, patches)
});
}

//属性比较
function diffAttr(oldAttrs, newAttrs) {
let attrsPatch = {};
for(let attr in oldAttrs) {
if(oldAttrs[attr] != newAttrs[attr]) {
//如果旧属性的值和新属性的值不想等,有两种情况,值改变了,属性被删除了
attrsPatch[attr] = newAttrs[attr];
}
for(let attr in newAttrs) {
if(!oldAttrs.hasOwnProperty(attr)) {
//添加新属性
attrsPatch[attr] = newAttrs[attr];
}
}
}
return attrsPatch;
}

其中有个需要注意的是新旧虚拟dom比较的时候,是先同层比较,同层比较完再看是否有子节点,有则需要继续比较下去,直到没有子节点。所以也是一种递归

diff2

patch.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let keyIndex = 0;
let utils = require('./utils');
let allPatches;//这里就是完整的补丁包
function patch(root, patches) {
allPatches = patches;
walk(root);
}
function walk(node) {
let currentPatches = allPatches[keyIndex++];
(node.childNodes || []).forEach(child => walk(child));
if (currentPatches) {
doPatch(node, currentPatches);
}
}
function doPatch(node, currentPatches) {
currentPatches.forEach(patch => {
switch (patch.type) {
case utils.ATTRS:
for (let attr in patch.attrs) {
let value = patch.attrs[attr];
if (value) {
utils.setAttr(node, attr, value);
} else {
node.removeAttribute(attr);
}
}
break;
case utils.TEXT:
node.textContent = patch.content;
break;
case utils.REPLACE:
let newNode = (patch.node instanceof Element) ? path.node.render() : document.createTextNode(path.node);
node.parentNode.replaceChild(newNode, node);
break;
case utils.REMOVE:
node.parentNode.removeChild(node);
break;
}
});
}

打补丁的文件,根据Diff.js生成的补丁,修改真实DOM

文章目录
  1. 1. 为什么要使用虚拟DOM
  2. 2. 创建虚拟DOM对象
  3. 3. 虚拟DOM的render
  4. 4. Diff算法
    1. 4.1. Diff.js
    2. 4.2. patch.js
|