云课堂
课程地址:VirtualDOM原理解析实现
github: https://github.com/chalecao/virtualdom
适用人群
帮助前端感兴趣的同学理解Virtual DOM相关知识,需要有HTML基础和JavaScript基础知识(含ES5和ES6)即可。
课程概述
关于MVVM前端框架大家都有了解,或多或少的使用过,比如Angular,React,VUE等等。那么你是否也想自己手写一个MVVM的前端框架呢,我们从Virtual DOM入手,手把手教你写基于Virtual DOM的前端框架,在整个编写的过程中,希望大家学习更多,理解更多。
vnode与vdom
真实的DOM是网页上的文档对象模型,由一个个HTML元素节点构成的树形结构。
vnode = virtual node = node in memory
vdom = virtual document object model
我们用JS创建出来的节点就是虚拟节点,Virtual node,当然由这些虚拟节点vd构成的树形结构就称为虚拟DOM,Virtual DOM。
如何构建VirtualDOM
手工实现DOM模型构建不太合理,我们可以借助JSX的工具来完成这个转换。本节我们以rollup打包工具结合babel转换插件实现数据的抽象。具体代码配置参考: github中package.json配置和rollup.config.js
例如我们截取网页的一个元素结构放到变量里面,也就是内存里,作为vdom如下:
const vdom = (
<div id="_Q5" style="border: 1px solid red;">
<div style="text-align: center; margin: 36px auto 18px; width: 160px; line-height: 0;">
<img src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png" height="56" style="border: none; margin: 8px 0px;"></img>
hello
</div>
</div>)
这个其实是下面的这段vnode解析后的结果:
var vdom = vnode(
"div",
{ id: "_Q5", style: "border: 1px solid red;" },
vnode(
"div",
{ style: "text-align: center; margin: 36px auto 18px; width: 160px; line-height: 0;" },
vnode("img", { src: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png", height: "56", style: "border: none; margin: 8px 0px;" }),
"google"
)
);
是不是很好理解,JSX编译后会自动根据定义好的语法格式提取出元素的类型和属性和子元素,并填入vnode方法中,我们只需要实现vnode方法就可以。我们可以编写vnode方法用于构建虚拟节点的模型,编写createElement方法用于根据vnode模型创建元素。并且把vnode的子元素追加到父元素上,形成树形层级结构。
function vnode(type, props, ...children) {
return { type, props, children };
}
function createElement(node) {
if (typeof node === 'string') {
return document.createTextNode(node);
}
const $el = document.createElement(node.type);
node.children
.map(createElement)
.forEach($el.appendChild.bind($el));
return $el;
}
document.body.appendChild(createElement(vdom));
节点操作
上图展示了最简单的一层DOM的结构变化,无非也就这么几种:增加元素节点、修改节点,删除节点。
我们可以基于DOM API来实现这些基本的操作,代码如下:
function updateElement($parent, newnode, oldnode) {
var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
if (!newnode) {
$parent.removeChild($parent.childNodes[index]);
} else if (!oldnode) {
$parent.appendChild(createElement(newnode));
} else if (isChange(newnode, oldnode)) {
$parent.replaceChild(createElement(newnode), $parent.childNodes[index]);
} else if (newnode.type) {
var newL = newnode.children.length;
var oldL = oldnode.children.length;
for (var i = 0; i < newL || i < oldL; i++) {
updateElement($parent.childNodes[index], newnode.children[i], oldnode.children[i], i);
}
}
}
上面的代码中我们实际上是把diff VirtualDOM 和update vdom放在一起处理了,采用了深度优先遍历的算法,从根节点优先查到子节点,判断子节点是否变化,有变化就进行变更处理,然后再回到上级节点。
处理DOM属性和事件绑定
{
type: “div”,
props: {“style”: ”…”},
children: [
{type: “img”, props: {“src”: ”…”}
]}
上面我们抽取的vnode的模型中已经把props拿出来了,我们这里需要把这些样式设置到对应元素上就好了。我们先看下元素的属性变化有哪几种情况:
如上,元素属性可以增加可以减少,我们通过DOM API实现属性的更新操作,代码如下:
//handle props
function setProp($el, name, value) {
if (typeof value == "boolean") {
handleBoolProp($el, name, value);
} else {
$el.setAttribute(name, value);
}
}
function handleBoolProp($el, name, value) {
if (!!value) {
$el.setAttribute(name, value);
$el[name] = !!value;
} else {
$el[name] = !!value;
}
}
function removeProp($el, name, value) {
if (typeof value == "boolean") {
$el[name] = false;
}
$el.removeAttribute(name, value);
}
function updateProp($el, name, newvalue, oldValue) {
if (!newvalue) {
removeProp($el, name, oldValue);
} else if (!oldValue || newvalue != oldValue) {
setProp($el, name, newvalue);
}
}
function updateProps($el) {
var newprops = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var oldProps = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var _props = Object.assign({}, newprops, oldProps);
Object.keys(_props).forEach(function (key) {
updateProp($el, key, newprops[key], oldProps[key]);
});
}
代码比较长,但是思路很清晰的,就是更新元素的属性,中间对于一些特殊bool类型属性做了特殊处理,bool类型的属性用元素可以直接访问的,所以我们把这些布尔属性的值也挂到了元素上。然后我们在更新元素的时候就可以先更新下属性。
function updateElement($parent, newnode, oldnode, index = 0) {
if (!newnode) {
$parent.removeChild($parent.childNodes[index])
} else if (!oldnode) {
$parent.appendChild(createElement(newnode))
} else if (isChange(newnode, oldnode)) {
$parent.replaceChild(createElement(newnode),
$parent.childNodes[index])
} else if (newnode.type) {
updateProps($el, newnode.props, oldnode.props)
let newL = newnode.children.length;
let oldL = oldnode.children.length;
for (var i = 0; i < newL || i < oldL; i++) {
updateElement($parent.childNodes[index],
newnode.children[i],
oldnode.children[i],
i);
}
}
}