VirtualDOM 简易实现

Vuejs 和 Reactjs 都用到了虚拟DOM,来实现数据绑定和 DOM 的自动更新,此处做了一个简单的实现,方便学习基本的工作原理;

1
2
3
4
5
6
7
8
9
10
11
12
13
const exampleButton = {
tag: "button",
properties: {
class: "primary",
disabled: true,
onClick: doSomething,
},
children: [] // 虚拟节点列表
}

const exampleText = {
text: "Hello"
}
1
2
3
4
5
6
7
function h(tag, properties, children) {
return { tag, properties, children };
}

function text(content) {
return { text: content }
}
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
function diffOne(l, r) {
const isText = l.text !== undefined;
// 若是文本,直接替换
if (isText) {
return l.text !== r.text
? { replace: r }
: { noop: true }
}
// 若 tag 不同,直接替代
if (l.tag !== r.tag) {
return { replace: r };
}
// 检查需要删除的属性
const remove = [];
for (const prop in l.properties) {
if (r.properties[prop] === undefined) {
remove.push(prop);
}
}
// 检查新增的属性
const set = {};
for (const prop in r.properties) {
if (r.properties[prop] !== l.properties[prop]) {
set[prop] = r.properties[prop];
}
}
const children = diffList(l.chilren, r.children);
return { modify: { remove, set, children } };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function diffList(ls, rs) {
const length = Math.max(ls.length, rs.length);
return Array.from({ length }).map((_, i) => {
if (ls[i] === undefined) {
return { create: rs[i] }
} else {
if (rs[i] === undefined) {
return { remove: true }
} else {
return diffOne(ls[i], rs[i])
}
}
})
}
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
function apply(el, enqueue, childrenDiff) {
const children = Array.from(el.childNodes);
childrenDiff.forEach((diff, i) => {
const action = Object.keys(diff)[0];
switch(action) {
case "remove": {
children[i].remove();
break;
}
case "modify": {
modify(children[i], enqueue, diff.modify);
}
case "create": {
const child = create(enqueue, diff.create);
el.appendChild(child);
break;
}
case "replace": {
const child = create(diff.replace);
children[i].replacewith(child);
break;
}
case "noop": {
break;
}
}
})
}
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
element["_ui"] = { listeners: { click: doSomething }}

// 事件监听函数, 所有事件都归集到同一个函数进行处理
function listener(event) {
const el = event.currentTarget;
const handler = el._ui.listeners[event.type];
const enqueue = el._ui.enqueue;
const msg = handler(event);
if (msg !== undefined) {
enqueue(msg)
}
}

// 给 el 添加事件监听函数
function setListener(el, event, handle) {
if (el._ui.listeners[event] === undefined) {
el.addEventListener(event, listener);
}
el._ui.listeners[event] = handle;
}

// 获得监听的事件名称
function eventName(str) {
if (str.indexOf("on") === 0) {
return str.slice(2).toLowerCase();
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const props = new Set([ "autoplay", "checked", "checked", "contentEditable", "controls",
"default", "hidden", "loop", "selected", "spellcheck", "value", "id", "title",
"accessKey", "dir", "dropzone", "lang", "src", "alt", "preload", "poster",
"kind", "label", "srclang", "sandbox", "srcdoc", "type", "value", "accept",
"placeholder", "acceptCharset", "action", "autocomplete", "enctype", "method",
"name", "pattern", "htmlFor", "max", "min", "step", "wrap", "useMap", "shape",
"coords", "align", "cite", "href", "target", "download", "download",
"hreflang", "ping", "start", "headers", "scope", "span" ]);

function setProperty(prop, value, el) {
if (props.has(prop)) {
el[prop] = value;
} else {
el.setAttribute(prop, value);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function create(enqueue, vnode) {
if (vnode.text !== undefined) {
const el = document.createTextNode(vnode.text);
return el;
}
const el = document.createElement(vnode.tag);
el._ui = { listeners: {}, enqueue };
// 有些 properties 是真的 prop, 有些则是事件监听函数,所以需要区别对待
for (const prop in vnode.properties) {
const event = eventName(prop);
const value = vnode.properties[prop];
if (event !== null) {
setListener(el, event, value);
} else {
setProperty(prop, value, el);
}
}
for (const childNode of vnode.children) {
const child = create(enqueue, childNode);
el.appendChild(child);
}
return el;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function modify(el, enqueue, diff) {
for (const prop of diff.remove) {
const event = eventName(prop);
if (event === null) {
el.removeAttribute(prop);
} else {
el._ui.listeners[event] = undefined;
el.removeEventListener(event, listener);
}
}
for (const prop in diff.set) {
const value = diff.set[prop];
const event = eventName[prop];
if (event !== null) {
setListener(el, event, value);
} else {
setProperty(prop, value, el);
}
};
apply(el, enqueue, diff.children);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 应用示例
function view(state) {
return [
h("p", {}, [ text(`counter: ${state.counter}`)])
];
}

function update(state, msg) {
return { counter: state.counter + msg }
}

const initialState = { counter: 0 };
const root = document.querySelector(".my-application");

const { enqueue } = init(root, initialState, update, view);

setInterval(() => enqueue(1), 1000);
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
function init(root, initialState, update, view) {
let state = initialState;
let nodes = [];
let queue = [];

function enqueue(msg) {
queue.push(msg);
}

function draw() {
let newNodes = view(state);
apply(root, enqueue, diffList(nodes, newNodes));
nodes = newNodes;
}

function updateState() {
if (queue.length > 0) {
let msgs = queue;
queue = [];
for (msg of msgs) {
state = update(state, msg, enqueue);
}
draw();
}
window.requestAnimationFrame(updateState);
}
draw();
updateState();
return { enqueue };
}
1
2
3
4
5
const button = h(
"button",
{ onClick: () => 1 },
[ text("increase counter")],
)

VirtualDOM 简易实现
https://ccw1078.github.io/2024/06/22/VirtualDom 简易实现/
作者
ccw
发布于
2024年6月22日
许可协议