Virtual DOM虚拟DOM

556 阅读6分钟

这是我参与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的基本使用

  • 创建项目

    • 步骤:
      • 安装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>
// 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:注册事件
  • 模块的使用步骤
    1. 导入需要的模块
    2. init中注册模块
    3. 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源码

  • 源码目录

-   .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,判断子节点是否有变化