实现Vue的Diff算法

  • npx server

目录结构

1
2
3
4
5
6
7
8
├── index.html
└── src
├── Element.js
├── doPatch.js
├── domDiff.js
├── index.js
├── patchTypes.js
└── virtualDom.js

index.html

1
2
3
4
<body>
<div id='app'></div>
<script type='module' src='./src/index.js'></script>
</body>

src/Element.js

1
2
3
4
5
6
7
8
9
class Element {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}

export default Element

src/doPatch.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
import {
ATTR,
TEXT,
REPLACE,
REMOVE
} from './patchTypes.js';
import { setAttrs, render } from './virtualDom.js';
import Element from "./Element.js";
let finalPatches = {},//储存patches
rnIndex = 0;//真实节点index
function doPatch(rDom, patches) {
finalPatches = patches;
rNodeWalk(rDom)
}
// 处理真实节点
function rNodeWalk(rNode) {
const rnPath = finalPatches[rnIndex++],
childNodes = rNode.childNodes;
//子节点为类数组
[...childNodes].map((childrenNode) => {
rNodeWalk(childrenNode)
})
// 判断是否需要更新节点
if (rnPath) {
patchAction(rNode, rnPath);//当前节点所对应的需要更新的内容
}
}
function patchAction(rNode, rnPath) {
rnPath.map(path => {
switch (path.type) {
case ATTR:
for (let key in path.attrs) {
const value = path.attrs[key];
// 之前有的,现在有的->添加更新
if (value) {
setAttrs(rNode, key, value)
} else {
// 之前有的,现在没了 ->删除
rNode.removeAttribute(key)
}
}
break
case TEXT:
// 设置节点文本内容
rNode.textContent = path.text
break
case REPLACE:
// 判断是否被Element构造出来的,不是那会是文本节点,是的话直接给render转换成真实节点
const newNode = (path.newNode instanceof Element)
? render(path.newNode)
: document.createTextNode(path.newNode);
// 替换真实节点
rNode.parentNode.replaceChild(newNode, rNode);
break
case REMOVE:
// 找到父亲杀儿子
rNode.parentNode.removeChild(rNode);
break
default:
break
}
})
}
export default doPatch;

src/domDiff.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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import {
ATTR,
TEXT,
REPLACE,
REMOVE
} from './patchTypes.js'

let patches = {};
let vnIndex = 0;

function domDiff(oldVDom, newVDom) {
let index = 0;
vNodeWalk(oldVDom, newVDom, index)
return patches;
}

function vNodeWalk(oldNode, newNode, index) {
let vnPatch = [];
if (!newNode) {
// 被删除
vnPatch.push({
type: REMOVE,
index
})
} else if (typeof oldNode === 'string' && typeof newNode === 'string') {
//两个节点都是文本节点 比较文本内容
if (oldNode !== newNode) {
// 更新
vnPatch.push({
type: TEXT,
text: newNode
})
}
} else if (oldNode.type === newNode.type) {
// 组件名一样
// 对比props
const attrPath = attrsWalk(oldNode.props, newNode.props);
// 有差异
if (Object.keys(attrPath).length > 0) {
vnPatch.push({
type: ATTR,
attrs: attrPath
})
}
childrenWalk(oldNode.children, newNode.children)
} else {
// 替换
vnPatch.push({
type: REPLACE,
newNode
})
}
if (vnPatch.length > 0) {
patches[index] = vnPatch;
}
}
// 对比属性异同
function attrsWalk(oldAttrs, newAttrs) {
let attrPath = {};
for (let key in oldAttrs) {
// 修改
if (oldAttrs[key] !== newAttrs[key]) {
attrPath[key] = newAttrs[key]
}
}
for (let key in newAttrs) {
// 添加
if (!oldAttrs.hasOwnProperty(key)) {
attrPath[key] = newAttrs[key]
}
}
return attrPath;
}
// 对比儿子异同
function childrenWalk(oldChildren, newChildren) {
oldChildren.map((children, index) => {
vNodeWalk(children, newChildren[index], ++vnIndex)
})
}
export default domDiff;

src/index.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
66
67
68
69
70
71
72
73
import { createElement, render, renderDOM } from './virtualDom.js'
import domDiff from './domDiff.js'
import doPatch from './doPatch.js'

const vDomOld = createElement('ul', {
class: 'list1',
style: 'width: 300px;height: 300px; background-color: orange',
}, [
createElement('li', {
class: 'item',
'data-index': 0
}, [
createElement('p', { class: 'text' }, ['第1个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 1
}, [
createElement('p', { class: 'text' }, ['第2个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 2
}, [
createElement('p', { class: 'text' }, ['第3个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 3
}, [
createElement('p', { class: 'text' }, ['第4个列表项'])
])
]
)

const rDom = render(vDomOld)
renderDOM(rDom, document.getElementById('app'))

var vDomNew = createElement('ul', {
class: 'list2',
style: 'width: 300px;height: 300px; background-color: orange',
}, [
createElement('li', {
class: 'item',
'data-index': 0
}, [
createElement('p', { class: 'text' }, ['第1个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 1
}, [
createElement('p', { class: 'text' }, ['第2个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 2
}, [
createElement('p', { class: 'text' }, ['第3个列表项'])
]),
createElement('li', {
class: 'item',
'data-index': 3
}, [
createElement('h1', { class: 'text' }, ['越难越爱', '---'])
])
])

const patches = domDiff(vDomOld, vDomNew)
console.log(patches);
doPatch(rDom, patches)


src/patchTypes.js

1
2
3
4
5
6
7
8
9
10
11
const ATTR = 'ATTR';
const TEXT = 'TEXT';
const REPLACE = 'REPLACE';
const REMOVE = 'REMOVE';
export {
ATTR,
TEXT,
REPLACE,
REMOVE
}

src/virtualDom.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
import Element from "./Element.js";

function createElement(type, props, children) {
return new Element(type, props, children);
}

function setAttrs(node, prop, value) {
switch (prop) {
case 'value':
if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
node.value = value
} else {
node.setAttribute(prop, value)
}
break
case 'style':
node.style.cssText = value
break
default:
node.setAttribute(prop, value)
}
}

function render(vDom) {
const { type, props, children } = vDom
const el = document.createElement(type)

for (let key in props) {
setAttrs(el, key, props[key])
}

children.map((c) => {
c = c instanceof Element ? render(c) : document.createTextNode(c)
el.appendChild(c)
})

return el
}

function renderDOM(rDom, rootEl) {
rootEl.appendChild(rDom)
}

export {
createElement,
render,
setAttrs,
renderDOM
}

仓库地址