Vue - Virtual DOM的实现原理

一、什么是 Virtual DOM

  • **Virtual DOM(虚拟 DOM),**是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM

我们来看看真实的DOM成员

  let element = document.querySelector('#app') 
    let s = '' 
    for (var key in element) { 
    	s+= key + ',' 
    }
    console.log(s)
    // 打印结果
    // align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,autocapitalize,contentEditable,isContentEditable,inputMode,offsetP .......Listener,dispatchEvent,removeEventListener,

可以使用 Virtual DOM 来描述真实 DOM,示例

{
  sel: "div",
  data: {},
  children: undefined, 
  text: "Hello Virtual DOM", 
  elm: undefined, 
  key: undefined
}

创建虚拟DOM的成本要比 创建真实DOM的成本小很多

二、为什么使用Virtual DOM

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升
  • 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM 出现了
  • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述DOM, Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM

三、虚拟DOM的作用

  • 维护视图和状态的关系
  • 跨平台
  • 首次渲染的性能下降,复杂视图情况下提升渲染性能
  • 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等

在这里插入图片描述

四、Virtual DOM库

源码地址:https://github.com/snabbdom/snabbdom

  • Snabbdom
    • Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
    • 大约 200 SLOC(single line of code)
    • 通过模块可扩展
    • 源码使用 TypeScript 开发
    • 最快的 Virtual DOM 之一
  • virtual-dom
    1、导入Snabbom
import {h,thunk,init} from 'snabbdom'
  • Snabbdom 的核心仅提供最基本的功能,只导出了三个函数 init()、h()、thunk()
  • init() 是一个高阶函数,返回 patch();
  • h() 返回虚拟节点 VNode;
new Vue({
	router,
	store,
	render: h => h(App)
}).$mount('#app')
  • thunk() 是一种优化策略,可以在处理不可变数据时使用
    2、代码
    先创建一个Hellow World
<div id="app"></div>
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

const patch = init([])

// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls',{
  hook: {
    init (vnode) {
      console.log(vnode.elm)
    },
    create (emptyNode, vnode) {
      console.log(vnode.elm)
    }
  }
}, 'Hello World')
let app = document.querySelector('#app')
// 第一个参数:旧的 VNode,可以是 DOM 元素
// 第二个参数:新的 VNode
// 返回新的 VNode
let oldVnode = patch(app, vnode)

vnode = h('div#container.xxx', 'Hello Snabbdom')
patch(oldVnode, vnode)

接下来我们假设要从服务器获取新的数据并更新到页面元素

//导入snabbdom
// import snabbdom from 'snabbdom'
// console.log(snabbdom)
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

//参数:数组,模块
//返回值:patch函数,作用:对比两个vnode的差异,并更新到真实的DOM
let patch = init([]) // init函数初始化patch函数
//第一个参数:标签+选择器,
//第二个参数:如果是字符串,就是标签中的内容
let vnode = h("div#app","helloworld")

let app = document.querySelector("#app")
//第一个参数:可以使DOM元素,内部会把DOM元素转化为VNode
//第二个参数:VNode
//返回值:VNode
let oldValue = patch(app,vnode)

//假设的时刻
newVnode = h("div","hello Snabbdom")
patch(oldValue,newVnode )

再来一个例子,2秒后创建dom元素,清空页面

// 2. div中放置子元素 h1,p
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

let 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(() => {
  newVnode = h('div#container', [
    h('h1', 'Hello World'),
    h('p', 'Hello P')
  ])
  patch(oldVnode, newVnode)

  // 清空页面元素  错误做法
  // patch(oldVnode, null)
  patch(oldVnode, h('!')) // 正确做法
}, 2000);

五、模块

Snabbdom的核心库并不能处理元素的属性、样式、事件等,如果需要处理的话,可以使用模块。
常用模块,这里官网提供了6个模块

  • attributes
    (1)、设置DOM元素的属性,使用setAttributes()
    (2)、处理布尔类型的属性
  • props
    (1)、和attributes模块相似,设置DOM元素属性element[attr] = value
    (2)、不处理布尔类型的属性
  • class
    (1)、切换类样式
    (2)、注意:给元素设置类样式是通过sel选择器
  • dataset
    (1)、设置data-*的自定义属性
  • evevtlisteners
    (1)、注册和移除事件
  • style
    (1)、设置行内样式,支持动画
    (2)、delayed/remove/destroy

模块使用

  • 使用模块步骤
    (1)、导入需要的模块
    (2)、init()中注册模块
    (3)、使用h()函数创建Vnode的时候,可以把第二个参数设置为对象,其他参数往后移
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)

六、h函数

1、h()函数介绍

  • 在使用Vue的时候h函数的使用
new Vue({
   router,
   store,
   render:h=>h(App)
}).$mount("#app");

  • h()函数最早见于hyperscript(网页脚本语言),使用JavaScript创建超文本
  • Snabbdom中的h()函数不是用来创建超文本,而是创建VNode

2、函数重载

  • 概念

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

function add(a,b) {
	console.log(a+b)
}

function add(a,b,c) {
	console.log()
}

add(1,2) // 调用第一个函数
add(1,2,3) // 调用第二个函数

七、patch的整体过程

  1. patch(oldValue,newValue)
  2. 找补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
  3. 对比新旧VNode是否是相同节点(节点的key和sel相同)
  4. 如果不是相同节点,删除之前的内容,重新渲染
  5. 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text不同,直接更新文本内容
  6. 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
  7. diff过程只进行同层级比较(我们更新DOM的时候,很少会把父节点更新到子节点上或把子节点更新到父节点,基本都是同级别的操作,比如:排序或添加移除元素)
    在这里插入图片描述

八、createElm的整体执行过程

在这里插入图片描述

九、updateChildren整体分析

  1. 功能:diff算法的核心,对比新旧节点的children,更新DOM

  2. 执行过程:
    (1)、在dom操作的时候我们很少会把一个父节点移动或更新到某一个子节点,因此只需要找同级别的子节点一次比较,然后再找下一级别的节点进行比较。
    (2)、在进行同级别比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
    在这里插入图片描述
    (3)、在对开始和结束节点比较的时候总共有四种情况

    1. oldStartVnode/newStartVnode(旧开始节点/新开始节点)
    2. oldEndVnode/newEndVnode(旧开始节点/新结束节点)
    3. oldStartVnode/oldEndVnode(旧开始节点/新结束节点)
    4. oldEndVnode/newStartVnode(旧结束节点/新开始节点)

在这里插入图片描述

(4)、开始节点和结束节点的比较,情况类似

  1. oldStartVnode/newStartVnode(旧开始节点/新开始节点)
  2. oldStartVnode/newStartVnode(旧结束节点/新结束节点)

(5)、如果oldStartVnode和newStartVnode是sameVnode(key和sel相同)

  1. 调用patchVnode()对比和更新节点
  2. 把旧开始和新开始索引向后移动 oldStartIdx++/oldEndIdx++
    在这里插入图片描述

(6)、如果oldStartVnode/newEndVnode(旧开始节点/新结束节点)相同
1. 调用patchVnode()对比和更新节点
2. 把oldStartVnode对应的DOM元素,移动到右边
3. 更新索引
在这里插入图片描述

(7)、oldEndVnode/newStartVnode(旧结束节点/新开始节点)相同

    1. 调用patchVnode()对比和更新节点
    2. 把oldEndVnode对应的DOM元素,移动到左边
    3. 更新索引

(8)、循环结束

  1. 当老节点的所有子节点先遍历完(oldStartldx>oldEndldx),循环结束
  2. 新节点的所有子节点先遍历完(newStartldx>newEndldx),循环结束

(9)、如果老节点的数组先遍历完(oldStartldx>oldEndldx),说明新节点有剩余,把剩余节点批量插入到右边
在这里插入图片描述
(10)、如果新节点的数组先遍历完(newStartldx>newEndldx),说明老节点有剩余,会把剩余节点批量删除
在这里插入图片描述

十、NO Virtual DOM

  • svelte
  • https://svelte.dev/
  • https://github.com/sveltejs/svelte
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值