【尚硅谷】Vue源码解析之虚拟DOM和diff算法
【Vue源码】图解 diff算法 与 虚拟DOM-snabbdom-最小量更新原理解析-手写源码-updateChildren]
文章目录
- 2. snabbdom 简介 及 准备工作
- 2.1 简介
- 2.2 搭建初始环境
- 1. 安装snabbdom
- 2. 安装webpack5并配置
- 3. 复制官方demo Example
- 3. h函数的介绍与使用
- 3.1 介绍
- 3.2 使用h函数 创建虚拟节点
- 3.3 使用patch函数 将虚拟节点上DOM树
- 3.4 h函数嵌套使用,得到虚拟DOM树(重要)
- 4. 手写h函数
- 5. 手写diff算法准备
- 5.1 diff算法原理
- 5.2 手写diff预备
- 5.2.1 源码中如何定义“同一个节点”
- 5.2.2 源码中创建子节点,需要递归
- 6. 手写diff——首次上DOM树patch(container, myVnode1)
- 6.0 DOM 预备知识
- 6.0.1 Node.insertBefore()
- 6.0.2 Node.appendChild()
- 6.0.3 Element.tagName
- 6.0.4 Node.removeChild
- 6.0.5 document.createElement
- 6.1 patch.js
2. snabbdom 简介 及 准备工作
2.1 简介
snabbdom(瑞典语,“速度”)是著名的虚拟DOM库,是diff算法的鼻祖
Vue源码借鉴了snabbdom
源码使用TypeScript写的https://github.com/snabbdom/snabbdom
从npm下载的是build出来的JavaScript版本
-D 是开发dev版本的依赖 -S是项目真正依赖
2.2 搭建初始环境
1. 安装snabbdom
npm init
npm install -D snabbdom
下载好的源码在node_modules
需要读取一些init等文件,使用webpack5,不能是webpack4(读取的地址不对)。接下来安装webpack5
2. 安装webpack5并配置
cnpm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
配置webpack5
根目录下创建webpack.config.js
cnpm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
“dev”: “webpack-dev-server” npm run dev实际跑的是npm run webpack-dev-server会读取webpack.config.js文件
webpack.config.js
配置文件
module.exports = {
// webpack5 不用配置mode
// 入口
entry: "./src/index.js",
// 出口
output: {
// 虚拟打包路径,文件夹不会真正生成,而是在8080端口虚拟生成
publicPath: "xuni",
// 打包出来的文件名
filename: "bundle.js",
},
// 配置webpack-dev-server
devServer: {
// 静态根目录
contentBase: 'www',
// 端口号
port: 8080,
},
};
3. 复制官方demo Example
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
根目录创建www文件夹,内部有index.html(默认访问该文件)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<script src="/xuni/bundle.js"></script>
</body>
</html>
npm run dev跑起来
3. h函数的介绍与使用
3.1 介绍
diff算法发生在 虚拟DOM上,新旧虚拟DOM的比较
h 函数产生虚拟节点
虚拟节点vnode的属性
{
children: undefined// 子元素 数组
data: {} // 属性、样式、key
elm: undefined // 对应的真正的dom节点(对象),undefined表示节点还没有上dom树
key: // 唯一标识
sel: "" // 选择器
text: "" // 文本内容
}
3.2 使用h函数 创建虚拟节点
3.3 使用patch函数 将虚拟节点上DOM树
src/index.js
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
var myVnode1 = h('a', {
props: {
href: "http://www.baidu.com",
target: '_blank',
}
}, '百度')
// const myVnode2 = h('div', '我是盒子' )
const myVnode3 = h('div', {class:{'box': true}}, '我是盒子')
console.log(myVnode1)
// 将虚拟节点渲染成真实节点 需要使用patch函数
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");
patch(container, myVnode1)
console.log(myVnode1)的输出
3.4 h函数嵌套使用,得到虚拟DOM树(重要)
const myVnode4 = h('ul', [
h('li', '苹果1'),
h('li', '苹果2'),
h('li', '苹果3'),
h('li', '苹果4'),
])
console.log(myVnode1)
// 将虚拟节点渲染成真实节点 需要使用patch函数
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");
patch(container, myVnode4)
const myVnode4 = h('ul', [
h('li', '苹果1'),
h('li', [
h('div', [
h('p', "香蕉皮"),
h('p', "苹果皮")
])
]),
h('li', h('span', '西瓜')),
h('li', '番茄'),
])
4. 手写h函数
参考ts版本的h函数,手写Js版本
看h函数源码,h最后调用vnode函数
h函数有很多形式,下面只是部分的形式。我们将写三个参数的h函数,进行基本的学习,参数1:标签 参数2 {} 参数三[]或者文字
在自己的src下创建如下文件,参考源码写h函数
源码中vnode函数是将接收的参数整合成一个对象,返回。我们自己手写vnode也是如此
src/mysnabbdom/vnode.js
export default function (sel, data, children, text, elm) {
// sel, data, children, text, elm 参考function vnode
return {sel, data, children, text, elm}
}
h函数源码中,对传入的参数进行判断,最后调用vnode函数
src\mysnabbdom\h.js
import vnode from "./vnode";
// 编写一个低配版的h函数,这个函数必须接受3个参数,缺一不可
/*
h('div', {}, '文字')
h('div', {}, [])
h('div', {}, h())
*/
export default function (sel, data, c) {
// 检查参数个数
if (arguments.length != 3) {
throw new Error("低配版的h函数必须三个参数")
}
// 检查参数c的类型
if (typeof c == 'string' || typeof c == 'number') {
//调用这种版本 h('div', {}, '文字')
return vnode(sel, data,undefined, c, undefined)
} else if (Array.isArray(c)) {
let children = []
// h('div', {}, []) ,[]内部是h函数,h函数返回的是对象,需要对数组每一项判断
for (let i = 0; i < c.length; ++i) {
if (!(typeof c[i] == "object" && c[i].hasOwnProperty('sel'))) {
throw new Error("传入的数组参数中有项不是h函数")
}
// 这里需不要执行c[i]因为c[i]已经执行过了,在[h()]数组内已经执行了,此时的c[i]是执行后的结果
// 收集children
children.push(c[i])
}
// 循环结束,children收集完毕
return vnode(sel, data, children, undefined, undefined)
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
// h('div', {}, h()) 内部的h返回的是对象并且有sel属性
let children = [c]
return vnode(sel,data,children, undefined, undefined)
} else {
throw new Error("传入的第三个参数不正确")
}
}
在src\index.js
调用自己得h函数,其他函数调用官方的,进行渲染
// 导入自己的h函数
import h from './mysnabbdom/h'
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
} from "snabbdom";
let b = h('li', { }, [
h('li', { }, "xx1"),
h('div', { }, "xx2")
])
// 将虚拟节点渲染成真实节点 需要使用patch函数
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");
patch(container, b)
5. 手写diff算法准备
5.1 diff算法原理
最小量更新,key很关键。key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。
问题: 如何定义是同一个虚拟节点
答:选择器相同且key相同
只进行同层比较,不会进行跨层比较。即使是同一片 虚拟节点,但是跨层了,diff就是暴力删除旧的,然后插入新的
在这里插入图片描述
// 导入自己的h函数
import h from './mysnabbdom/h'
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
} from "snabbdom";
let vnode1 = h('ul', {}, [
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
])
let vnode2 = h('li', {}, [
h('session', { key: 'SS' },
[
h('li', { key: 'A' }, 'A'),
h('li', { key: 'B' }, 'B'),
h('li', { key: 'C' }, 'C'),
h('li', { key: 'D' }, 'D'),
h('li', { key: 'E' }, 'E'),
])
])
// 将虚拟节点渲染成真实节点 需要使用patch函数
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");
patch(container, vnode1)
const btn = document.getElementById("btn");
btn.addEventListener("click", function () {
// 点击按钮,将vnode1变成vnode2
// patch 意思是修补 ,对虚拟节点vnode1、vnode2对比执行最小量更新
patch(vnode1, vnode2)
})
更改顺序时,我的是部分更新,老师的是全部都没有更新
5.2 手写diff预备
5.2.1 源码中如何定义“同一个节点”
最新的版本是比较:选择器sel相同, key相同, data相同, text相同
老版本的比较: 选择器sel相同, key相同
5.2.2 源码中创建子节点,需要递归
6. 手写diff——首次上DOM树patch(container, myVnode1)
6.0 DOM 预备知识
6.0.1 Node.insertBefore()
var insertNode = parentNode.insertBefore(newNode, referenceNode);
insertedNode :被插入节点(newNode)
parentNode :新插入节点的父节点
newNode :用于插入的节点
referenceNode :newNode 将要插在这个节点之前
在当前节点下增加一个子节点 Node,并使该子节点位于参考节点的前面。
6.0.2 Node.appendChild()
element.appendChild(aChild)
将一个节点附加到指定父节点的子节点列表的末尾处。
如果将被插入的节点已经存在于当前文档的文档树中,那么 appendChild() 只会将它从原先的位置移动到新的位置(不需要事先移除要移动的节点)。
6.0.3 Element.tagName
返回当前元素的标签名
elementName = element.tagName
elementName 是一个字符串,包含了element元素的标签名.
在HTML文档中, tagName会返回其大写形式
6.0.4 Node.removeChild
从DOM中删除一个子节点。返回删除的节点
let oldChild = node.removeChild(child);
//OR
element.removeChild(child);
child 是要移除的那个子节点.
node 是child的父节点.
oldChild保存对删除的子节点的引用. oldChild === child.
6.0.5 document.createElement
var element = document.createElement(tagName[, options]);
tagName:指定要创建元素类型的字符串, 创建元素时的 nodeName 使用 tagName 的值为初始化,该方法不允许使用限定名称(如:“html:a”),在 HTML 文档上调用 createElement() 方法创建元素之前会将tagName 转化成小写,在 Firefox、Opera 和 Chrome 内核中,createElement(null) 等同于 createElement(“null”)
返回 新建的元素(Element)
6.1 patch.js