本系列主要整理前端面试中需要掌握的知识点。本节介绍虚拟DOM与diff算法详解。
知识主要从尚硅谷中学习,并且参考了大神YK菌的博客。
npm install -S snabbdom
注意:视频中老师使用snabbdom是2.1.0版本,其中有exports字段,但是截至2022年6月27日,snabbdom的最新版本是3.5.1,直接下载3.5.1就可以,不要下载2.1.0版本,后面和webpack会不兼容报错(亲测!!!血泪!!!)
npm install
npm install -S snabbdom
cnpm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
,(npm不能安装webpack-cli@3);module.exports = {
// webpack5 不用配置mode
// 入口
entry: "./src/index.js",
// 出口
output: {
// 虚拟打包路径,文件夹不会真正生成,而是在8080端口虚拟生成
publicPath: "xuni",
// 打包出来的文件名
filename: "bundle.js",
},
// 配置webpack-dev-server
devServer: {
// 静态根目录
contentBase: 'www',
// 端口号
port: 8080,
},
};
//src.index.js
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);
const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: function () { } } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
const newVnode = h(
"div#container.two.classes",
{ on: { click: function () { } } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
const container = document.getElementById("container")
//创建虚拟节点
const myVnode1 = h('ul', {}, [
h('li', {},'A'),
h('li', {},'B'),
h('li', {},'C'),
h('li', {},'D'),
])
//节点上树,更新DOM
patch(container, myVnode1)
export default function (sel,data,children,text,elm) {
return {sel,data,children,text,elm}
}
import vnode from './vnode.js'
// 傻瓜h函数,只能接受3个参数,正常中间的{}为空是可以省略的,但我们就直接写死三个参数,方便分析
export default function (sel, data, c) {
if (arguments.length != 3) {
throw new Error('对不起,h函数必须传入3个参数');
}
// 最后一个参数是文本或者数字,就是text内容
if (typeof c == 'string' || typeof c == 'number') {
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
// 数组就是里面有好多h函数,遍历即可
let children = [];
for (let i = 0; i < c.length; 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函数,因为h函数返回的是对象,包裹成数组,再执行即可
let children = [c];
return vnode(sel,data,children,undefined,undefined)
} else {
throw new Error('第三个参数类型不对')
}
}
import h from './mysnabbdom/h'
var myVnode1 = h('div', {}, h('div', {}, '文字'),)
console.log(myVnode1);
import vnode from "./vnode";
import createElement from "./createElement";
// patch函数的功能是判断新节点和老节点是否是同一个节点,如果不是,分别进行精细比较和暴力插入,至于创建节点的事情就交给了createElement做
// oldVnode:老的虚拟节点
// newVnode:新的虚拟节点
export default function (oldVnode, newVnode) {
// 如果传入的老节点不是虚拟节点,就要转换为虚拟节点
if (oldVnode.sel == '' || oldVnode.sel == undefined) {
// oldVnode = h(oldVnode.tagName.toLowerCase(), {},[])
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {},[],undefined,oldVnode)
}
if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
console.log('是同一个节点');
} else {
console.log('不是同一个节点,暴力插入新的、删除旧的');
let newVnodeElm = createElement(newVnode);
if (oldVnode.elm.parentNode && newVnodeElm) {
oldVnode.elm.parentNode.insertBefore(newVnodeElm,oldVnode.elm)
}
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
export default function createElement(vnode) {
let domNode = document.createElement(vnode.sel)
if (vnode.text != "" && (vnode.children == undefined || vnode.length == 0)) {
// 内部是文字
domNode.innerText = vnode.text;
vnode.elm = domNode;
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
//内部是子节点,就要递归创建节点
console.log('内部是子节点,就要递归创建节点');
for (let i = 0; i < vnode.children.length; i++){
let ch = vnode.children[i]
console.log(ch);
let chDOM = createElement(ch);
domNode.appendChild(chDOM);
vnode.elm = domNode;
}
}
return vnode.elm
}
if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
console.log('是同一个节点');
patchVnode(oldVnode,newVnode)
}
import createElement from './createElement'
export default function patchVnode(oldVnode, newVnode) {
// 新旧vnode完全相同,一样一样的
if (oldVnode === newVnode) return;
if (newVnode.text != undefined && (newVnode.children === undefined || newVnode.children.length == 0)) {
if (newVnode.text !== oldVnode.text) {
// 直接让新text写入老elm中即可
oldVnode.elm.innerText = newVnode.text;
}
} else {
// 判断oldVnode有没有children属性
if (oldVnode.children != undefined && oldVnode.children.length > 0) {
} else {
oldVnode.elm.innerHTML = "";
for (let ch of newVnode.children) {
let chDOM = createElement(ch);
oldVnode.elm.appendChild(chDOM)
}
}
}
}
四种命中查找:
①新前与旧前
②新后与旧后
③新后与旧前(此种发生后,涉及到移动节点,新后指向的节点,移动到旧后之后)
④新前与旧后(此种发生后,涉及到移动节点,新前指向的节点,移动到旧前之前)
命中一种就不再进行命中判断了,如果都没有命中,就需要用循环来寻找,移动到旧前之前。
这部分可以看大神的整理【Vue源码】图解 diff算法 与 虚拟DOM-snabbdom-最小量更新原理解析-手写源码-updateChildren