snabbdom
简介
- snabbdom:是瑞典语单词,单词原意为“速度”。是著名的虚拟 DOM 库,是 diff 算法的鼻祖,Vue 源码就是借鉴了 snabbdom
- 官方 Git:https://github.com/snabbdom/snabbdom
安装
- 在 git 上的 snabbdom 源码是用 TypeScript 写的,git 上并不提供编译好的 JavaScript 版本
- 如果要直接使用 build 出来的 JavaScript 版的 snabbdom 库,可以从 npm 上下载:npm i -S snabbdom
- 安装步骤:新建文件夹study-snabbdom =》在终端打开study-snabbdom文件夹 =》npm初始化:
npm init =》一路回车 =》安装snabbdom:npm i -S snabbdom 【终端不要关】 - 安装好了之后 打开node_modules\snabbdom文件夹 里面的src中放着TS代码 build文件夹中放着JS代码
测试环境搭建
- snabbdom 库是 DOM 库,当然不能在 nodejs 环境运行,所以我们需要搭建 webpack 和 webpack-dev-server 开发环境,好消息是不需要安装任何loader
- 这里需要注意,必须安装最新版 webpack@5,不能安装 webpack@4,这是因为 webpack@4 没有读取身份证(package.json)中 exports 的能力,建议大家使用这样的版本:
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
(在终端继续输入以上代码安装测试环境)
- 在study-snabbdom文件夹中新建webpack.config.js文件 参考webpack官网 将以下代码书写到webpack.config.js文件中
// https://webpack.docschina.org/
const path = require('path')
module.exports = {
// 入口
entry: './src/index.js',
// 出口
output: {
// 虚拟打包路径,就是说文件夹不会真正生成,而是在 8080 端口虚拟生成,不会真正的物理生成
publicPath: 'xuni',
// 打包出来的文件名 不会真正的物理生成 而是虚拟生成在端口里
filename: 'bundle.js'
},
devServer: {
// 端口号
port: 8080,
// 静态资源文件夹
contentBase: 'www'
}
}
- study-snabbdom文件夹中新建src文件夹 在该文件夹下新建index.js文件
study-snabbdom文件夹中新建www文件夹 在该文件夹下新建index.html文件 将package.json文件夹中的"scripts"的内容改为:
scripts": {
"dev": "webpack-dev-server"
},
- 跑通snabbdom官方git首页的demo程序,即证明调试环境已经搭建成功 src/index.js 中的内容改为snabbdom官方git首页的Example中的内容:
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
classModule,
propsModule,
styleModule,
eventListenersModule,
]);
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(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!"),
]
);
patch(vnode, newVnode);
var container = document.getElementById('container') 这行代码表明页面也定要有一个id为container的div 将www/index.html文件内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>静态资源文件夹</title>
</head>
<body>
<div id="container"></div>
<!-- 引入打包文件-->
<script src="xuni/bundle.js"></script>
</body>
</html>
- 在终端输入:
npm run dev 进行打包 - 访问:http://localhost:8080/ 和 http://127.0.0.1:8080/xuni/bundle.js, 可以看到 www/index.html 和 xuni/bundle.js 文件的内容
虚拟 DOM 和 h 函数
虚拟 DOM
- 虚拟DOM:用 JavaScript 对象描述 DOM 的层次结构。DOM 中的一切属性都在虚拟 DOM 中有对应的属性。
<div class="box">
<h3>我是一个标题</h3>
<ul>
<li>牛奶</li>
<li>咖啡</li>
<li>可乐</li>
</ul>
</div>
{
"sel": "div",
"data": {
"class": { "box": true }
},
"children": [
{
"sel": "h3",
"data": {},
"text": "我是一个标题"
},
{
"sel": "ul",
"data": {},
"children": [
{ "sel": "li", "data": {}, "text": "牛奶" },
{ "sel": "li", "data": {}, "text": "咖啡" },
{ "sel": "li", "data": {}, "text": "可乐" }
]
}
]
}
- diff 是发生在虚拟 DOM 上的:新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真实的 DOM 上
也就是说 新虚拟 DOM 和老虚拟 DOM 进行 diff算法比较 找出不同的地方 然后对真实的DOM进行局部操作
{
"sel": "div",
"data": {
"class": { "box": true }
},
"children": [
{
"sel": "h3",
"data": {},
"text": "我是一个标题"
},
{
"sel": "ul",
"data": {},
"children": [
{ "sel": "li", "data": {}, "text": "牛奶" },
{ "sel": "li", "data": {}, "text": "咖啡" },
{ "sel": "li", "data": {}, "text": "可乐" }
]
}
]
}
{
"sel": "div",
"data": {
"class": { "box": true }
},
"children": [
{
"sel": "h3",
"data": {},
"text": "我是一个标题"
},
{
"sel": "span",
"data": {},
"text": "我是一个新的span"
},
{
"sel": "ul",
"data": {},
"children": [
{ "sel": "li", "data": {}, "text": "牛奶" },
{ "sel": "li", "data": {}, "text": "咖啡" },
{ "sel": "li", "data": {}, "text": "可乐" },
{ "sel": "li", "data": {}, "text": "雪碧" }
]
}
]
}
- DOM 如何变为虚拟 DOM,属于模板编译原理范畴,本次课不研究
h函数
- h 函数用来产生虚拟节点(vnode)
比如这样调用 h 函数:h('a', { props: { href: 'http://www.atguigu.com' } }, '尚硅谷') 将得到这样的虚拟节点:{ "sel": "a", "data": { "props": { "href": "http://www.atguigu.com" } }, "text": "尚硅谷" } 它表示的真正的 DOM 节点:<a href="http://www.atguigu.com">尚硅谷</a>
h函数的第一个参数是字符串类型的 传入标签名字 会被添加到虚拟DOM的sel属性中 第二个参数是个对象 传入标签的属性 属性也是一个对象 第二个参数会被添加到虚拟DOM的data属性中 可省略 第三个参数是字符串 传入标签的内容 会被添加到虚拟DOM的text属性中 也可以是h函数或者h函数构成的数组 传递子元素
- 虚拟节点有哪些属性
{
children: undefined,
data: {},
elm: undefined,
key: undefined,
sel: 'div',
text: '我是一个盒子'
}
基本使用
将src/index.js中的代码改成:
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
classModule,
propsModule,
styleModule,
eventListenersModule,
]);
const myVNode1 = h(
'a',
{ props: { href: 'http://www.atguigu.com', target: '_blank' } },
'尚硅谷'
)
const myVNode2 = h('div', { class: { box: true } }, '我是一个盒子')
const container = document.getElementById('container')
patch(container, myVNode2)
嵌套使用
- h函数用来产生虚拟节点 所以可以嵌套使用 h 函数:
h('ul', {}, [
h('li', {}, '牛奶'),
h('li', {}, '咖啡'),
h('li', {}, '可乐')
])
将得到这样的虚拟 DOM 树:
{
"sel": "ul",
"data": {},
"children": [
{ "sel": "li", "data": {}, "text": "牛奶" },
{ "sel": "li", "data": {}, "text": "咖啡" },
{ "sel": "li", "data": {}, "text": "可乐" }
]
}
- 第二个参数是标签属性 没有可以省略 第三个参数是标签内容(h函数开头已经展示)或标签子元素 如果元素只有一个子元素 那么不用写数组[] 如果子元素大于1个 需要用数组[]包起来
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
classModule,
propsModule,
styleModule,
eventListenersModule,
]);
const myVNode3 = h('ul', [
h('li', {}, '苹果'),
h('li', '西瓜'),
h('li', [h('div', [h('p', '嘻嘻'), h('p', '哈哈')])]),
h('li', h('p', '火龙果'))
])
const container = document.getElementById('container')
patch(container, myVNode3)
h函数源码
import { vnode, VNode, VNodeData } from "./vnode";
import * as is from "./is";
export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;
function addNS(
data: any,
children: VNodes | undefined,
sel: string | undefined
): void {
data.ns = "http://www.w3.org/2000/svg";
if (sel !== "foreignObject" && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
const childData = children[i].data;
if (childData !== undefined) {
addNS(childData, children[i].children as VNodes, children[i].sel);
}
}
}
}
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
sel: string,
data: VNodeData | null,
children: VNodeChildren
): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
let data: VNodeData = {};
let children: any;
let text: any;
let i: number;
if (c !== undefined) {
if (b !== null) {
data = b;
}
if (is.array(c)) {
children = c;
} else if (is.primitive(c)) {
text = c;
} else if (c && c.sel) {
children = [c];
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b;
} else if (is.primitive(b)) {
text = b;
} else if (b && b.sel) {
children = [b];
} else {
data = b;
}
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i]))
children[i] = vnode(
undefined,
undefined,
undefined,
children[i],
undefined
);
}
}
if (
sel[0] === "s" &&
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")
) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined);
}
- 返回一个vnode函数创造出一个虚拟节点 vnode函数源码:
export function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
): VNode {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
vnode函数就是把传入的参数返回成一个对象
手写h函数
- 假设h函数必须传入3个参数 本文不考虑h函数参数省略情况
- 将原来的src/index.js改名为index-demo.js 新建index.js文件、mysnabbdom文件夹 在mysnabbdom文件夹下新建h.js和vnode.js文件
- vnode.js中的代码:
export default function (sel, data, children, text, elm) {
return {
sel,
data,
children,
text,
elm
}
}
- mysnabbdom/h.js中我们自己写的源码:
import vnode from './vnode'
export default function (sel, data, c) {
if (arguments.length !== 3)
throw new Error('对不起,h函数必须传入3个参数,我们是低配版h函数')
if (typeof c === 'string' || typeof c === 'number') {
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
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')) {
let children = [c]
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error('传入的第三个参数类型不对')
}
}
5.在src/index.js中:
import h from './mysnabbdom/h'
const myVNode1 = h('div', {}, [
h('p', {}, '哈哈'),
h('p', {}, '嘻嘻'),
h('p', {}, '呵呵'),
h('p', {}, [h('span', {}, 'aa'), h('span', {}, 'bb')]),
h('p', {}, h('span', {}, 'A'))
])
const myVNode2 = h('ul', {}, [
h('li', {}, '牛奶'),
h('li', {}, '咖啡'),
h('li', {}, [h('div', {}, [h('p', {}, '可口可乐'), h('p', {}, '百事可乐')])]),
h('li', {}, h('p', {}, '雪碧'))
])
console.log(myVNode1, myVNode2)
diff 算法
|