笔记来源:拉勾教育 - 大前端就业集训营
文章内容:学习过程中的笔记、感悟、和经验
Virtual DOM
课程目标
- 了解什么是虚拟虚拟DOM,以及虚拟DOM的作用
- snabbdom库的基本使用
- snabbdom源码解析
Virtual DOM - 虚拟DOM
虚拟DOM:由普通的js对象描述DOM对,减小Dom操作的性能开销
// 使用virtual Dom来描述真实DOM,表示真实对象内部的一些核心属性
{
sel: 'div',
data: {},
children: undefined,
text: 'hello Vitual Dom',
elm: undefined,
key: undefined
}
为什么使用Virtual DOM
- Dom书写复杂,操作起来消耗性能
- MVVM框架解决视图和状态同步问题
- 模版引擎可以简化视图操作,但没办法跟踪状态
- 虚拟DOM可以跟踪状态变化
可以参考gitHub上的Vittual-Dom
- 虚拟DOM可维护程序的状态,跟踪上一次的状态
- 通过对比前后两次状态差异更新真实DOM
虚拟DOM作用
- 维护视图和状态的关系
- 在视图复杂的情况下提升渲染性能
- 跨平台:浏览器、服务端、原生应用、小程序等
虚拟DOM库
- Vue2.xx使用的虚拟DOM就是基于snabbdom改造的
- 核心功能行数大约200行
- 通过模块可扩展:使用核心功能以外的功能可通过模块机制添加
- 源码使用TypeScript开发
- 最快的virtual dom之一
snabbdom基本使用
安装parcel打包工具
使用webpack也可以,但是parcel提供一个配置好的环境,方便打包
- 初始化项目生成package.json:
npm i init -y - 安装parcel作为开发依赖:
npm i parcel-bundler -D
配置scripts
根目录新建入口文件index.html,引入需要的js文件
package.json 配置scripts字段,添加两条命令
"scripts": {
// 使用parcel,index.html是入口文件,自动打开浏览器
"dev": "parcel index.html --open",
// 打包,入口文件为index.html
"build": "parcel build index.html"
}
学习任何一个库都要先看文档,通过文档了解库的作用,看文档中提供的示例,自己快速创建一个demo,通过文档查看API使用
安装snabbdom:npm i snabbdom
// 文档demo解析,我自己的理解,但是渲染机制还不清楚
// 引入功能
import {
// init 和 h 都是snabbdom的核心功能
init,
h,
// 下面四个都是第三方功能
classModule,
propsModule,
styleModule,
eventListenersModule,
} from "snabbdom"
// 初始化snabbdom,并将功能注入进去(注意:初始化必须写,哪怕不需要注入第三方功能)
const patch = init([classModule, propsModule, styleModule, eventListenersModule])
// 获取节点
const container = document.getElementById("container");
// 使用 h 函数创建一个节点 - div元素 - id为container - 类名为 two和classes
const vnode = h("div#container.two.classes", {
// 事件对象,在里面添加要实现的事件
on: {
// 点击事件
click: someFn
}
}, [
// 这里面书写子节点
h("span", {
// 样式
style: {
// 书写样式
fontWeight: "bold"
}
}, "旧文本"),// 最后可以书写内容文本
// 也可以直接书写内容文本
"旧的",
// 再创建一个节点
h("a", {
// 属性设置
props: {
// 设置指向地址
href: "/foo"
}
}, "旧文本"),
])
// 进行第一次比对渲染
patch(container, vnode);
// 再进行创建新节点
const newVnode = h(
"div#container.two.classes", {
on: {
click: anotherEventHandler
}
},
[
h(
"span", {
style: {
fontWeight: "normal",
fontStyle: "italic"
}
},
"新文本"
),
" 新的",
h("a", {
props: {
href: "/bar"
}
}, "新文本"),
]
)
// 再进行比对渲染
patch(vnode, newVnode)
// 两个事件函数
function someFn() {
console.log(111)
}
function anotherEventHandler() {
console.log(222)
}
基本使用
// 引入功能:init 和 h 都是snabbdom的核心功能
import { init, h } from "snabbdom"
// 创建虚拟节点
let vnode = h('div#app.content', '我是虚拟节点内容')
// 获取当前存在的节点
const app = document.getElementById('app')
// 初始化 snabbdom,必须写,就算 [] 为空也要写
const patch = init([])
// 进行比对更新节点,这里使用 oldnode 接收一下此次状态,以便下次进行比对
let oldnode = patch(app, vnode)
// 重新设置虚拟节点
vnode = h('p.text', '我是新的虚拟节点内容')
// 再次进行比对渲染,同样存储这次的状态
oldnode = patch(oldnode, vnode)
注意:如果init和h引入出现问题可能需要修改引入路径,我这里按照最新的版本使用没有发现问题
包含子节点
// 引入功能:init 和 h 都是snabbdom的核心功能
import { init, h, vnode } from "snabbdom"
// 获取当前存在的节点
const app = document.getElementById('app')
// 初始化 snabbdom,必须写,就算 [] 为空也要写
const patch = init([])
// 创建虚拟节点
let vNode = h('div#box', [
// 把参数2设置为数组可以在里面书写子节点
// 使用 h 函数创建虚拟节点
h('h1', '标题'),
h('p', '内容'),
'文本可以直接传入,不需要使用h函数'
])
// 进行虚拟节点渲染
let oldNode = patch(app, vNode)
// 特殊写法,设置为注释绩点可实现清空
patch(vNode, h('!'))
模块使用
模块作用
- snabbdom核心功能无法处理DOM的属性、样式、事件等
- 使用snabbdom中默认提供的模块来实现,模块可以扩充snabbdom功能
- 模块的实现是利用注册在全局的生命周期钩子函数来实现的
官方提供以下模块
- attribures:进行固有属性处理
- props:和上面一样,但是无法处理布尔类型的属性
- dataset:用于处理data-开头的自定义属性
- class:进行类切换,但是本身核心功能就已经有了,所以一般不用
- style:进行样式处理
- eventlisteners:进行事件处理
使用步骤
- 导入需要的模块
- init中注册模块
- h函数第二个参数使用模块
// 引入功能:init 和 h 都是snabbdom的核心功能
// 引入模块的时候要注意引入名称不要写错
import { init, h, classModule ,propsModule ,styleModule ,eventListenersModule } from "snabbdom"
// 获取当前存在的节点
const app = document.getElementById('app')
// 在初始化的时候注入模块
const patch = init([classModule ,propsModule ,styleModule ,eventListenersModule])
// 创建虚拟节点
let vNode = h('div#box', {
// 参数2的位置使用对象进行模块使用
// 使用style模块
style: {
backgroundColor: 'red',
// 注意这里添加的是行内样式,需要写单位
width: '200px',
height: '200px'
},
// 使用事件模块
on: {
// 添加click方法
click: c
}
}, [
// 子节点
h('h1', '标题'),
h('p', '内容'),
'文本可以直接传入,不需要使用h函数'
])
// 进行虚拟节点渲染
let oldNode = patch(app, vNode)
// 事件函数
function c() {
console.log(1111)
}
Snabbdom源码解析
学习源码
- 宏观了解
- 带着目标看源码
- 看源码的过程要不求甚解
- 调试
- 参考资料
核心功能
- init()设置模块,创建patch()函数
- 使用h()函数创建js对象(vNode)描述真实DOM节点
- patch()比较新旧两个vNode
- 把变化的内容更新到真实DOM树
源码克隆
使用官方githyb源码地址 - 课程使用版本v2.1.0 - 我使用的v3.0.1
-
克隆代码:
git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git -
命令解析:克隆 + v2.1.0版本 + 最后一次提交 + 地址
-
结构分析:
- .vscode目录:编辑器配置(不需要关注)
- examples目录:官方提供的示例demo
- perf目录:性能测试(不需要关注)
- src目录:源码文件
- 剩下一些配置文件不需要关注
-
安装依赖:
npm i -
打包:
npm run compile- 这里打包命令是compile,和vue不一样,打包后的vuild目录就是我们在其他项目安装依赖的时候使用的依赖了 -
运行
examples/reorder-animation/index.html查看效果,使用liveserver运行,不要使用文件直接打开,我们可能会发现点击排序、删除按钮都无效,这是因为这个示例里面的代码没有跟随版本更新而更新,需要我们手动修改一下 -
可以尝试修改
examples/reorder-animation/index.js里面的代码,找到里面的所有click事件,将click: [remove, movie]修改为click () { remove(movie) }的形式
h()函数
用来创建vNode - 虚拟DOM对象
Vue中使用h函数是在Vue实例中添加了render选项,在render选项中使用了h函数,简单来说就是Vue中依赖reader选项内部使用h函数做虚拟DOM操作
new Vue({
router,
store,
// 使用h函数进行DOM操作
render: h => h(App)
}).$mount('#app')
函数重载:参数个数或者参数类型不同的同名函数,在调用的时候根据传入的参数不同而执行不同的代码
js中没有重载的概念,但是type script中有,不过重载的实现还需要代码调整参数
h函数中利用ts函数重载,在内部通过判断传入的参数数量、类型不同而最终返回一个vnode的函数调用,真正的虚拟节点创建是由vnode实现的
vnode函数
函数内部只不过规定了虚拟DOM节点内部的一些属性
patch整体过程分析
语法:patch(旧节点,新节点)
- 把新节点的比那花内容渲染到真是节点,最后返回新节点作为下一次比对的旧节点
- 对比新旧节点是否相同,利用节点的节点唯一标识(key)和节点类型(sel)是否相同判断
- 如果不是相同节点(sel不同)删除之前内容,重新渲染
- 如果是相同节点
- 对比新节点是否有text,如果有和旧节点进行比对,不同直接跟新文本内容
- 如果新节点有子节点,判断子节点的变化
init()函数
init用于创建patch,patch函数作为init的返回值
init函数中封装了一些生命周期函数,patch作为返回值形成的闭包可以使用这些生命周期返回值,并且init接受一个数组接收其他函数功能
patch函数
进行新旧节点比对
creatElm
当新旧节点不同
根据传入的新虚拟节点以及内部子节点创建DOM元素并且返回
patchvnode
当新旧节点相同的时候负责找出差异并渲染
updattChildren整体分析
新旧虚拟节点内部同时存在子节点,进行对比
Diff算法
虚拟DOM的Diff算法:查找两棵树之间节点的差异
Snabbdom根据DOM的特点对传统的diff算法进行了优化
- Dom操作时很少会跨级别操作节点
- 只对比同级别节点
我听懵了。算了。 人间不值得