这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
Virtual DOM
- Virtual DOM(虚拟DOM),是由普通的JS对象来描述DOM对象
- 真实DOM成员,先打印出所有的DOM成员
let element = document.querySelector('#app')
let s = ''
for(let key in element){
s += key + ','
}
console.log(s)
- 一个DOM的成员是非常多的,创建一个真实DOM成本非常高
- 虚拟DOM
{
sel:'div',
data:{},
children:undefined,
text:'hello world',
elm:undefined,
key:undefined
}
- 创建成本比真实DOM低很多
为什么要使用Virtual DOM
- 既需要操作DOM,也需要操作数据
- MVVM框架解决视图和状态同步问题
- 模版引擎可以简化视图操作,没办法跟踪状态,只好把页面元素全部删除,再全部创建。
- 虚拟DOM跟踪状态变化
- github上virtual-dom的动机描述
- 虚拟DOM可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态差异更新真实的DOM
虚拟DOM的作用
-
维护视图和状态的关系
-
复杂视图情况下提升渲染性能
-
跨平台
- 浏览器平台渲染DOM
- 服务端渲染SSR(Nuxt.js/Next.js)
- 原生应用(Weex/React Native)
- 小程序(mpvue/uni-app)等
-
虚拟DOM开源库
- Snabbdom
- Vue.js2.x内部使用的虚拟DOM就是改造的Snabbdom
- 大约200 SLOC (single line of code)
- 通过模块可扩展
- 源码使用TypeScript
- 最快Virtual DOM之一
- Virtual-dom
- 最早的虚拟DOM开源库之一
- Snabbdom
Snabbdom的基本使用
-
创建项目
- 步骤:
- 安装parcel
- 步骤:
// 创建项目目录
md snabbdom-demo
// 进入项目目录
cd snabbdom-demo
// 创建package.json
npm init -y
// 本地安装parcel
npm install parcel-bundler -D
- 配置scripts
"scripts":{
"dev":"parcel index.html --open",
"build":"parcel index.html"
}
- 目录结构
index.html
package.json
src
01-basicusage.js
- index.html
<!DOCTYPE html>
<html lang='cn'>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, inital-sacle=1.0">
<meta http-equiv="X-UA-Compatiable" content="ie=edge">
<title>Vue</title>
</head>
<body>
<div id="app"></div>
<script src="./src/01-basicuage.js"></script>
</body>
</html>
- Snabbdom文档
- Snabbdom文档
- github.com/snabbdom/sn…
- 当前版本v2.1.0,npm install snabbdom@2.1.0
- Snabbdom文档
// 01-basicusage.js
import { init } from 'snabbdom/init'
import { h } from 'snabbdom/h'
// 注意导入的路径和打包工具的版本,是否支持package.json中的exports
// 不支持的话需要全路径:'snabbdom/build/package/init','snabbdom/build/package/h
const patch = init([])
// h函数的第一个参数:标签+选择器,第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls','Hello world')
let app = document.querySelector('#app')
// 第一个参数,旧的 VNode, 可以是DOM元素
// 第二个参数:新的VNode
// 返回新的VNode
let oldVNode = patch(app, vnode)
vnode = h('div#container.xxx','Hello Snabbdom')
// 对比新旧的node,进行更新
patch(oldValue, vnode)
- demo
// 02-basicusage.js
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
const patch = init([])
// 因为有子元素,所以第二个参数是一个数组,因为子元素也是vnode,所以也需要用h函数创建
let vnode = h('div#container',[
h('h1','Hello')
h('p','p标签')
])
let app = document.querySelector('#app')
let oldVNode = patch(app, vnode)
// 测试vnode更新
setTimeout(() => {
vnode = h('div#container',[
h('h1','Hello World')
h('p','Hello p标签')
])
patch(oldValue, vnode)
// 清除div中的内容,'!'空的注释节点
patch(oldVnode, h('!'))
}, 2000)
Snabbdom的模块
- 模块的作用
- Snabbdom的核心库并不能处理DOM元素的属性/样式/事件,只能处理vnode,可以通过注册Snabbdom默认提供的模块来实现
- Snabbdom中的模块可以用来扩展Snabbdom的功能
- Snabbdom中的模块的实现是通过注册全局的钩子函数来实现的
- 官方提供的模块
- attributes:设置vnode属性,内部DOM标准方法通过setAttribute来实现的,内部会处理布尔类型的属性
- props:通过对象点的方式去设置属性,并不会判断布尔类型的属性
- dataset:用来处理HTML5中的data-自定义属性
- class:不是用来设置类样式,是用来切换类样式,设置类样式可以通过h函数的一个参数来实现
- style:设置行内样式
- eventlistensers:注册事件
- 模块的使用步骤
- 导入需要的模块
- init中注册模块
- h的第二个参数使用函数
// 03-modules.js
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
// 1.导入模块
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
// 2.注册模块
const patch = init([
styleModule,
eventListenersModule
])
// 3.使用h()函数的第二个参数,传入模块中使用的数据
let vnode = h('div', [
h('h1', { style:{ backgroundColor: 'red' } },'Hello World'),
h('p', { on: { click: eventHandler }}, 'Hello p标签')
])
function eventHandler() {
console.log('eventHandler')
}
let app = document.querySelector('#app')
let oldVNode = patch(app, vnode)
Snabbdom源码解析
-
Snabbdom的核心
-
init()设置模块,创建patch函数
-
使用h()函数创建JavaScript对象(VNode)描述真实的DOM
-
patch()比较新旧两个Vnode
-
把变化的内容更新到真实DOM树
-
-
Snabbdom源码
-
源码地址:github.com/snabbdom/sn… (版本:v2.1.0)
-
克隆代码:git clone -b v2.1.0 --depth=1 github.com/snabbdom/sn…
-
-
源码目录
- .vscode: 编辑器的配置文件
- examples:官方示例
- perf: 性能测试
- src:源码
- 剩下的都是配置文件,不用关注
-
examples:
-
里面有四个示例,其中有两个用svg结尾的是示例,
-
hero:演示自定义模块
-
reorder-animation:演示带过渡动画的列表
-
查看其中的某个例子
-
目前目录中是不存在build这个目录,需要安装依赖并进行编译
-
npm i
-
npm run compile
-
编译结束,目录会生产一个build目录
-
通过open with live server打开一个外部浏览器运行
-
src目录
-
package:源码
-
helpers
- attachto.ts:定义了AttachData的数据结构
-
modules
-
六个官方模块,和一个hero示例中的自定义模块
-
modules.ts:定义所有使用的钩子函数
-
-
h.ts:定义h函数,用来创建vnode
-
hooks.ts:定义生命周期中所有的钩子函数
-
htmldomapi.ts:dom元素的包装,创建元素,删除元素等等
-
init.ts:定义init函数,加载模块和api,并返回patch函数
-
jsx-golbal.ts:jsx的类型声明文件
-
jsx.ts:处理jsx的
-
thunk.ts:处理复杂视图的优化
-
tovnode.ts:提供函数可以把node转化为vnode,patch函数第一个可以是node,就需要转化
-
vnode.ts:定义vnode数据结构
-
剩下的是src中的配置文件
-
-
-
test:单元测试
h函数
- 作用:创建VNode对象
- Vue中的h函数
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
-
h函数最早见于hyperscript,使用JavaScript创建超文本
-
函数的重载
-
参数个数或参数类型不同的函数,和参数相关,和返回值无关
-
Javascript中没有重载的概念
-
Typescript中有重载,不过实现还是通过代码调整参数
-
VNode
patch过程
-
patch(oldVnode, newVnode)
-
把新节点中变化的内容渲染到真实的DOM,最后返回新节点作为下一次处理的旧节点。
-
对比新旧VNode,是否相同节点(节点的key和sel相同)
-
如果不是相同的节点,删除之前的内容,重新渲染
-
如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text相同,直接更新文本内容
-
如果新的VNode有children,判断子节点是否有变化