Virtual DOM 的实现原理

1、什么是虚拟DOM

  • VirtualDOM(虚拟DOM),是由普通的JS对象来描述DOM对象
  • 真实DOM成员

在这里插入图片描述
在这里插入图片描述
由此看出创建一个DOM对象的成本是非常高的

  • 使用VirtualDOM来描述真实DOM
    在这里插入图片描述
    创建虚拟DOM对象,成员非常少,也就是创建一个虚拟DOM对象比创建一个真实DOM对象的成本小很多
    虚拟DOM就是一个普通的JAVAScript的对象用来描述真实DOM

2、为什么使用虚拟DOM

为什么要使用VirtualDOM

  • 前端开发刀耕火种的时代
  • MVVM框架解决视图和状态同步问题
  • 模板引擎可以简化视图操作,没办法跟踪状态
  • 虚拟DOM跟踪状态变化
  • 参考github上 virtual-dom 的动机描述
    • 虚拟DOM可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次状态差异更新真实DOM

3、虚拟DOM的作用和虚拟DOM库

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 跨平台
    • 浏览器平台渲染DOM
    • 服务端渲染SSR(Nuxt.js/Next.js) :服务端渲染就是把虚拟DOM转换成普通的html字符串。因为虚拟DOM就是普通的js对象,所以可以对它做任意的编程处理。
    • 原生应用(Weex/ReactNative)
    • 小程序(mpvue/uni-app)等
      在这里插入图片描述
      虚拟DOM库
      -Snabbdom
    • Vue.js2.x内部使用的虚拟DOM就是改造的Snabbdom
    • 大约200SLOC(singlelineofcode)
    • 通过模块可扩展
    • 源码使用TypeScript开发
    • 最快的VirtualDOM之一
  • virtual-dom 最早的虚拟DOM开源库

4、Snabbdom 基本使用

1、创建项目

  • 步骤
    • 安装parcel
    • 配置scripts
    • 目录结构
  • 安装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 build index.html"
}
  • 目录结构
    在这里插入图片描述

2、导入 Snabbdom

Snabbdom文档

  • 看文档的意义
    • 学习任何一个库都要先看文档
    • 通过文档了解库的作用
    • 看文档中提供的示例,自己快速实现一个demo
    • 通过文档查看API的使用
  • Snabbdom文档
    • https://github.com/snabbdom/snabbdom
    • 当前版本v2.1.0
  • 安装Snabbdom
    • npm intall snabbdom@2.1.0
  • 导入Snabbdom
    • Snabbdom的两个核心函数init和h()
      • init()是一个高阶函数,返回patch()
      • h()返回虚拟节点VNode,这个函数我们在使用Vue.js的时候见过
  • 文档中导入的方式
    在这里插入图片描述
    init函数:接收一个数组用于加载snabbdom的模块
    patch函数:作用把虚拟DOM转换成真实DOM渲染到界面上
    h函数:用来创建虚拟节点
  • 实际导入的方式 防止路径·查找错误报错
    • parcel/webpack4不支持package.json中的exports字段 webpack5支持
      在这里插入图片描述

3、案例

1、体会init h patch这几个函数的使用

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

const patch = init([]) // 调用init函数返回patch函数 

// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
// let vnode = h('div#container.cls', 'Hello World')
let vnode = h('div#container.cls',{
  // 源码中有data.hook
  hook: {
    init (vnode) { // init是在创建dom之前执行的。这个函数中获取不到vnode对应的dom元素
      console.log(vnode.elm)
    },
    create (emptyNode, vnode) { // create是在创建dom元素之后执行的
      console.log(vnode.elm)
    }
  }
}, 'Hello World')
let app = document.querySelector('#app')
// 第一个参数:旧的 VNode,可以是 DOM 元素
// 第二个参数:新的 VNode
// 返回新的 VNode
let oldVnode = patch(app, vnode) // patch内部会对比这两个vnode的差异,把这个差异更新到真实DOM,并且把第二个参数也就是新的vnode返回作为下次patch的oldvnode 也就是把当前的状态保存起来

// 如果新创建的div的内容有变化。这里会重新创建一个VNode的对象
vnode = h('div#container.xxx', 'Hello Snabbdom')
patch(oldVnode, vnode) // 对比新旧vnode的差异更新到视图

1、从let oldVnode = patch(app, vnode)打断点 f5刷新 调试patch函数
2、按f11单步执行(一行一行代码来执行)
3、定义了一些内部成员
4、遍历cbs找到所有模块的pre钩子函数,然后执行这些钩子函数。因为init的时候没有传入任何模块,所以这里没有pre钩子函数
5、isVnode函数 判断oldVnode是否是vnode类型的对象 发现是dom对象
6、继续执行emptyNodeAt函数 真实dom转换成虚拟节点vnode 内部: 处理id与 classname 又调用vnode函数把真实dom转换成vnode对象。这边创建 vnode的时候 传入的第一个参数先获取到dom元素的tagname并转换为小写,然后在拼接处理好的id和类选择器,然后设置后续的几个参数,最后把当前传入的dom元素作为新创建的vnode的elm
7、进入vnode函数
在这里插入图片描述
先去获取data中的key属性也就是vnode的唯一值,作为创建VNode对象的最后一个参数。当前没有传入,所以此时是undefined
vnode函数最后是把传入的这些数据,组合成一个js对象返回。返回的就是我们想要的vnod类型的对象
8、sameVnode判断新旧vnode节点是否是相同的vnode节点(key、sel)。如果相同则不会再创建dom元素,而是比较两个节点的差异,然后把差异更新到dom元素的内容上来。
进入该函数发现新旧vnode都是undefined没有设置值 。oldVnode.sel是div#app 新的vnode.sel是div#container.cls 不相同 所以不是相同的节点
9、因为不相同所以不会执行patchVnode函数。
创建vnode节点对应的dom元素,然后渲染到界面上。并且会把oldVnode对应的dom元素从界面上移除
执行:
首先获取oldVnode对应的dom元素
获取elm的父元素,因为接下里要把新节点对应的dom元素插入到这个父元素中
老节点的dom元素和父元素都获取到之后
9、调用createElm创建新节点对应的dom元素
f10跳过方法或者函数继续往后执行
新节点对应的dom元素创建好之后,要把这个dom元素插入到parent中
判断parent是否为null,不会null执行插入操作
此时界面上可以看到更新后的内容
在这里插入图片描述
然后移除oldVnode对应的dom元素
在这里插入图片描述
此时页面渲染工作就完成了
10、再往后就是触发用户传入的insert钩子函数以及模块中的post钩子函数。因为本案例没有传入任何的钩子函数。所以会跳过下面的这些循环。
11、最后返回vnode作为下次处理的oldvnode
这是patch函数首次执行的调试的过程。

2、h函数创建一个div,可以创建div里面的子元素

import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

const patch = init([])

let vnode = h('div#container', [
  h('h1', 'Hello Snabbdom'),
  h('p', '这是一个p')
])

let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

setTimeout(() => {
  // vnode = h('div#container', [
  //   h('h1', 'Hello World'),
  //   h('p', 'Hello P')
  // ])
  // patch(oldVnode, vnode)

  // 清除div中的内容
  patch(oldVnode, h('!')) // h('!') 创建空的注释节点
}, 2000);

5、Snabbdom中的模块

模块
•模块的作用
•官方提供的模块
•模块的使用步骤

1、模块的作用

  • Snabbdom的核心库并不能处理DOM元素的属性/样式/事件等,可以通过注册Snabbdom默认提供的模块来实现
  • Snabbdom中的模块可以用来扩展Snabbdom的功能
  • Snabbdom中的模块的实现是通过注册全局的钩子函数来实现的

2、官方提供的模块

  • attributes 设置vnode对应的DOM元素的属性,内部使用的是DOM元素的标准方法setAttribute来实现的。这个模块内部会对DOM元素的不类型的属性(如:selected,checked等)做判断
  • props 类似 attributes 都是用来设置DOM对象的属性。不同的是props模块内部设置DOM对象的属性是通过对象点属性的方式来设置的。另外它内部不会去处理布尔类型的属性
  • dataset 用来处理html5中提供的data-这样的自定义属性
  • class 用来切换类样式
  • style 用来设置行类样式,并且通过这个模块可以很容易设置过渡动画,它内部还注册了transitionEnd这个事件
  • eventlisteners 用来注册和移除事件

3、模块的使用步骤

  • 导入需要的模块
  • init()中注册模块
  • h()函数的第二个参数处使用模块 第二个参数可以设置成对象,这个对象就是设置模块中所需要的数据。这里可以设置DOM的属性、行内样式、事件等
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('别点我,疼')
}

let app = document.querySelector('#app')
patch(app, vnode)

6、Snabbdom 源码解析

如何学习源码
•宏观了解
•带着目标看源码
•看源码的过程要不求甚解
•调试
•参考资料

Snabbdom的核心

  • init()设置模块,创建patch()函数
  • 使用h()函数创建JavaScript对象(VNode)描述真实DOM
  • patch()比较新旧两个Vnode
  • 把变化的内容更新到真实DOM树
    Snabbdom源码
    • 源码地址
      • https://github.com/snabbdom/snabbdom
      • 当前版本:v2.1.0
    • 克隆代码
    • git clone -b v2.1.0 --depth=1
    • https://github.com/snabbdom/snabbdom.git

1、h函数

核心就是处理参数 并且调用VNode函数创建一个VNode对象返回
h函数介绍

  • 作用:创建VNode对象
  • Vue中的h函数
new Vue({
	router,
	store,
	render: h => h(App)
}).$mount('#app')
  • h函数最早见于hyperscript,使用JavaScript创建超文本

函数重载

  • 参数个数或参数类型不同的函数
  • JavaScript中没有重载的概念
  • TypeScript中有重载,不过重载的实现还是通过代码调整参数
    函数

函数重载-参数个数
在这里插入图片描述
函数重载-参数类型
在这里插入图片描述

2、常用快捷键

1、定位:光标–> F12 返回: Alt + <-
2、定位:Ctrl+鼠标左键 返回: Alt + <-
前进到刚刚的位置 Alt + ->

3、VNode

VNode对象用来描述真实DOM,创建VNode的时候可以根据需要传递相应应的参数

4、patch整体过程分析

patch整体过程分析

  • patch(oldVnode,newVnode)
  • 把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧VNode是否相同节点(节点的key和sel相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text不同,直接更新文本内容
  • 如果新的VNode有children,判断子节点是否有变化

5、init

返回patch函数(oldvnode, vnode)

6、patch

patch内部会对比这两个vnode的差异,把这个差异更新到真实DOM,并且把第二个参数也就是新的vnode返回作为下次patch的oldvnode 也就是把当前的状态保存起来

7、调试 patch 函数

8、createElm

9、调试createElm

10、removeVnodes和addVnodes

11、patchVnode

在这里插入图片描述

12、updateChildren整体分析

13、Diff算法

  • 虚拟DOM中的Diff算法
    • 查找两颗树每一个节点的差异
      在这里插入图片描述
  • Snbbdom根据DOM的特点对传统的diff算法做了优化
    • DOM操作时候很少会跨级别操作节点
    • 只比较同级别的节点
      在这里插入图片描述

执行过程

  • 在对开始和结束节点比较的时候,总共有四种情
    • oldStartVnode/newStartVnode(旧开始节点/新开始节点)
    • oldEndVnode/newEndVnode(旧结束节点/新结束节点)
    • oldStartVnode/oldEndVnode(旧开始节点/新结束节点)
    • oldEndVnode/newStartVnode(旧结束节点/新开始节点)

执行过程
在这里插入图片描述

开始和结束节点

  • 如果新旧开始节点是sameVnode(key和sel相同)
  • 调用patchVnode()对比和更新节点
  • 把旧开始和新开始索引往后移动oldStartIdx++/oldEndIdx++
    在这里插入图片描述

旧开始节点/新结束节点

  • 调用patchVnode()对比和更新节点
  • 把oldStartVnode对应的DOM元素,移动到右边,更新索引
    在这里插入图片描述

旧结束节点/新开始节点

  • 调用patchVnode()对比和更新节点
  • 把oldEndVnode对应的DOM元素,移动到左边,更新索引
    在这里插入图片描述

非上述四种情况
在这里插入图片描述

非上述四种情况

  • 遍历新节点,使用newStartNode的key在老节点数组中找相同节点
  • 如果没有找到,说明newStartNode是新节点
    • 创建新节点对应的DOM元素,插入到DOM树中
  • 如果找到了
    • 判断新节点和找到的老节点的sel选择器是否相同
    • 如果不相同,说明节点被修改了
      • 重新创建对应的DOM元素,插入到DOM树中
    • 如果相同,把elmToMove对应的DOM元素,移动到左边

循环结束

  • 当老节点的所有子节点先遍历完(oldStartIdx>oldEndIdx),循环结束
  • 新节点的所有子节点先遍历完(newStartIdx>newEndIdx),循环结束

oldStartIdx>oldEndIdx

  • 如果老节点的数组先遍历完(oldStartIdx>oldEndIdx)
    • 说明新节点有剩余,把剩余节点批量插入到右边
      在这里插入图片描述

newStartIdx>newEndIdx

  • 如果新节点的数组先遍历完(newStartIdx>newEndIdx)
    • 说明老节点有剩余,把剩余节点批量删除
      在这里插入图片描述

14、updateChildren

15、调试updateChildren

16、调试带key的情况

节点对比过程

在这里插入图片描述

17、Key的意义

用来在diff算法中比较vnode是否是相同节点。如果不设置key会最大程度重用当前dom元素,但是重用dom元素有的时候会有问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值