本文是根据尚硅谷前端系列对DOM以及diff算法的讲解而做的笔记,中间也参考了其他的博客
1、什么是虚拟Dom
就是用javaScript对象描述DOM的层次结构,DOM中的一切属性都在虚拟DOM中有对应的属性。
{ "sel": "a", "data": { props: { href: 'http://www.baidu.com' } }, "text": "百度" }
2、什么是真实Dom
<a href="http://www.baidu.com">百度a>
3、什么是diff
diff主要作用域虚拟DOM,当要进行更新的时候,新旧DOM会进行比较,这个时候就用到了diff算法,当两个DOM的标签名不一样的时候,diff会暴力替换。但是当DOM标签名相同的时候,这个时候就需要精细化比较,算出如何最小量更新,最后反应到真正的DOM上去。
snabbdom 是著名的虚拟 DOM 库,是 diff 算法的鼻祖,Vue 源码就是借鉴了 snabbdom
安装步骤:
必须安装webpack@5
(这点很重要,不然代码跑不起来。)const path = require('path');
module.exports = {
// 入口文件
entry: './src/index.js',
// 出口文件
output: {
// 虚拟打包路径,就是说文件夹不会真正生成,而是会在8080端口虚拟生成
publicPath: "xixi",
// 打包出来的文件名
filename: 'bundle.js'
},
devServer: {
// 端口号
port: 8080,
// 静态资源文件夹
contentBase: 'www'
}
};
snabbdom官方index.js例子复制下来,放到src/index.js中。
这里要把代码里面用到click函数的地方,用空函数替换一下。因为此时www/index.html页面并没有定义这些方法。{
"scripts": {
"dev": "webpack-dev-server",
}
}
npm run dev
h函数用来产生虚拟节点(vnode),h函数接收的第一个参数是标签名。
比如说这样调用h函数:
h('a', { props: { href: 'https://www.baidu.com' } }, '百度');
得到的是这样的虚拟节点:
{sel: 'a', data: {props:{href:'https://baidu.com'}}, children: undefined, text: '百度', elm: undefined}
{
children:undefined//孩子节点
data:{}//节点上带的属性
elm:undefined// 表示这个虚拟结点还没有上树,这个一般会放DOM元素
key:undefined //唯一标识符
sel:"div"//标签属性
text:"我是一个盒子"//文本信息
}
在手写h函数之前呢,先看一下它在整体的流程中所处的位置。下面是一个小的Demo
,写在了src/index.js
里面。
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
// 创建虚拟节点
var myVnode1 = h('a', { props: { href: 'https://www.baidu.com' } }, '百度');
// const myVnode2 = h('div', {}, '我是一个盒子');
// const myVnode3 = h('ul', [
// h('li', "西瓜"),
// h('li', "香蕉"),
// h('li', "火龙果"),
// h('li', "桃子")
// ])
//让虚拟节点上树
console.log(myVnode1)
const container = document.getElementById('container');
patch(container, myVnode1);
上树经历了一下几步:
在src中创建一个文件夹,在此文件夹内创建h.js文件。这里编写的是一个低配版的h函数。这个函数必须要接收3
个参数。与真正的h函数相比,只是重载能力弱而已,真正的h函数会考虑多种情况。
代码如下:
import vnode from './vnode'
export default function (sel, data, c) {
// vnode(sel, data, c)
// 检查参数的个数
if (arguments.length != 3)
throw new Error('对不起,h函数必须传入3个参数');
// 检查参数c的类型
if (typeof c == 'string' || typeof c == 'number') {
// 说明现在调用h函数是形态1
return vnode(sel, data, undefined, c, undefined);
} else if (Array.isArray(c)) {
// 说明现在调用h函数是形态2
let children = [];
// for循环值是收集一下获得c的子类就可以了
for (let i = 0; i < c.length; i++) {
// 检查c[i]必须是一个对象,如果不满足
if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel')))
throw new Error('传入的数组参数中有项不是h函数');
// 此时只需要收集好就可以了
children.push(c[i])
}
return vnode(sel, data, children, undefined, undefined);
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
// 说明现在调用h函数的是形态3
// 此时说明就包含了一个子节点,因为不是数组形式
let children = [];
children.push(c)
return vnode(sel, data, children, undefined, undefined);
} else {
throw new Error("传入错误")
}
}
聪明你的你一定能看懂这个代码的!
vnode.js代码如下:
export default function (sel, data, children, text, elm) {
return {
sel, data, children, text, elm
};
}
vnode返回的的样子是:
{sel: 'a', data: {props:{href:'https://baidu.com'}}, children: undefined, text: '百度', elm: undefined}
这里h.js函数则是考虑到了更多的情况发生,但是返回的数据仍然是这个样子的。
在src/index.js中进行调用:
import h from './mysnabbdom/h.js'
var myVode1 = h('div', {}, [
h('p', {}, '哈哈'),
h('p', {}, '嘿嘿'),
h('p', {}, '嘻嘻'),
h('p', {}, '吼吼'),
]);
console.log(myVode1);
这里要补充
虚拟节点的属性里面有key这个值,这个值太重要了。key
是节点的唯一标识符,告诉diff算法,在更改前后他们是否是同一个DOM节点。
举例说明:在src/index.js中写如下代码
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');
// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
const vnode1 = h('ul', {}, [
h('li', { }, 'A'),
h('li', { }, 'B'),
h('li', { }, 'C'),
h('li', { }, 'D'),
])
patch(container, vnode1)
const vnode2 = h('ul', {}, [
h('li', { }, 'A'),
h('li', { }, 'B'),
h('li', { }, 'C'),
h('li', { }, 'D'),
h('li', { }, 'E'),
])
// 点击按钮,将vnode1变成vnode2
btn.onclick = function () {
patch(vnode1, vnode2);
}
最后一个例子的代码进行了一个小改动,就是吧vnode2中的E提到了A的前面。
当给每个li都添加key属性会怎么样呢?
const vnode1 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
])
const vnode2 = h('ul', {}, [
h('li', { key: 'E' }, 'E'),
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
])
答:选择器相同且Key相同。
首先要现在src/index.js中调用patch.js
import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch'
// const myVnode1 = h('h1', {}, '你好');
const myVnode1 = h('ul', {}, [
h('li', {}, 'A'),
h('li', {}, 'B'),
h('li', {}, 'C'),
h('li', {}, 'D'),
]);
const container = document.getElementById('container');
// 上树,如何上树呢,将在patch.js中进行编写
patch(container, myVnode1);
在src下创建文件夹,在文件夹下创建patch.js
。下面是他的代码:
import vnode from './vnode.js'
import createElement from './createElement'
export default function (oldVnode, newVnode) {
// 判断传入的第一个参数,是DOM节点还是虚拟节点?
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// 传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
}
// 判断oldVnode和newVnode是不是同一个节点,是同一个节点
if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
// 那么就是精细比较
} else {
// 如果说oldVnode和newVnode不是同一个节点
let newVnodeDOM = createElement(newVnode)
// 在老节点的父节点的子节点里面插入新节点
if (oldVnode.elm.parentNode != undefined && newVnodeDOM != undefined) {
oldVnode.elm.parentNode.insertBefore(newVnodeDOM, oldVnode.elm)
}
console.log(oldVnode);
// 在老节点的父节点的子节点里面删除老节点
oldVnode.elm.parentNode.removeChild(oldVnode.elm);
}
}
判断是DOM节点还是虚拟节点,要是DOM就要用到vnode了,包装它
判断新旧节点是不是同一个节点,刚才说到的判断是否是同一个节点,要比较俩值。
是就进行精细比较
不是就是粗暴的删除,添加
对新节点创建真实DOM节点,放到elm属性里面去。这一步在createElement.js中完成。
// 真正创建节点,将vnode创建为DOM,插入到pivot这个元素之前
export default function createElement(vnode) {
console.log('目的是把虚拟节点', vnode, '真正变为DOM');
let domNode = document.createElement(vnode.sel);
// 是有子节点还是文本?
if (vnode.text != '' && (vnode.children == undefined || vnode.children.length == 0)) {
// 内部是文字
domNode.innerText = vnode.text;
// 将节点上树,因为是同一个节点嘛,所以只需要插入到标杆节点上就可以了
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 它内部是子节点,就要递归创建节点
for (let i = 0; i < vnode.children.length; i++) {
let ch = vnode.children[i];
let chDOM = createElement(ch);
domNode.appendChild(chDOM);
}
}
// 补充elm属性
vnode.elm = domNode;
// 返回elm,elm属性时一个纯DOM对象
return vnode.elm;
}
这一步用到了递归,但是也是for循环遍历递归,就递归了一层,不算难理解。
返回创建好的vnode虚拟节点,然后再老节点的父元素的中插入新的节点。
删除旧的节点。
页面就可以展示出来啦。