手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法(三)

Virtual DOM 的实现原理

  • 了解什么是虚拟DOM,以及虚拟DOM的作用
  • Snabbdom的基本使用
  • Snabbdom的源码解析
一、什么是虚拟DOM ----Virtual DOM
  • 虚拟DOM是由普通的JS对象来描述DOM对象


    image.png
二、 为什么要使用Virtual DOM
  • 前端开发初期,MVVM框架解决视图和状态同步问题
  • 模板引擎可以简化视图操作,没办法跟踪状态
  • 虚拟DOM跟踪状态变化
  • 参考GitHub上Virtual-dom的动机描述
    • 虚拟DOM可以维护程序上的状态,跟踪上一次的状态
    • 通过比较前后两次状态差异更新真实DOM

虚拟DOM用来维护视图和状态的关系

三、虚拟DOM的作用和虚拟DOM库
  • 虚拟DOM的作用

    • 维护视图的状态和关系
    • 复杂视图情况下提升渲染性能
    • 跨平台
      • 浏览器平台渲染DOM
      • 服务端渲染SSR(Nuxt.js/Next.js)
      • 原生应用(Weex/React Native)
      • 小程序(mpvue/uni-app)等
  • 虚拟DOM库

    • Snabbdom
    • Vue.js 2.x 内部使用的虚拟DOM就是改造的Snabbdom
    • 大约200 SLOC(Single line of code)
    • 通过模块可扩展
    • 源码使用TypeScript 开发
    • 最快的Virtual Dom 之一
    • virtual-dom
四、 Snabbdom 的基本使用
  • 1、基本步骤:
    • 初始化项目目录并安装Parcel
//创建项目目录
md snabbdom-demo
//进入项目目录
cd snabbdom-demo 
//创建package.json
npm init -y
//本地安装parcel
npm install parcel-bundler -D

  • 配置package.json中的scripts
"scripts:"{
  "dev":"parcel index.html --open",
  "build":"parcel build index.html"
}
  • 创建目录结构
    • 根目录创建index.html,引入src目录中的文件
    • 在src中创建js文件来导入使用的snabbdom进行编码
image.png
image.png
image.png
//导入snabbdom
 npm install snabbdom@2.1.0


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

const patch = init([])
  • 3、案例1
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'

const patch = init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls', 'hello world')
let app = document.querySelector('#app')
// patch函数中的第一个参数:旧的VNode,也可以是DOM元素
// 第二个参数:新的VNode
// 返回新的VNode
let oldVnode = patch(app, vnode)

vnode = h('div#container.xxx','hello Snabbdom')
patch(oldVnode,vnode)
image.png

image.png
  • 4、案例2
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', '这是一个段落'),
])

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中的内容  h('!')-->生成空的节点
  patch(oldVnode, h('!'))
}, 2000)

五、 Snabbdom 模块的使用
  • 模块的作用
    • Snabbdom 的核心库并不能处理DOM元素的属性、样式、事件等,可以通过注册Snabbdom默认提供的模块来实现
    • Snabbdom 中的模块可以用来扩展Snabbdom的功能
    • Snabbdom中的模块的实现是通过注册全局的钩子来实现的
  • 官方提供的模块
    • attributes 设置元素属性 会处理布尔类型的属性
    • props 设置元素属性 不会处理布尔类型的属性
    • dataset 处理html5中的data-的自定义属性
    • class 用来切换类样式
    • style 用来设置行内样式,可以很容易设置过度动画
    • eventlisteners 用来注册和移除事件
  • 模块的使用步骤
    • 导入需要的模块
    • init()中注册模块
    • h()函数中的第二个参数处使用模块
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: { background: 'red' } }, 'hello World'),
  h('p', { on: { click: eventHandler } }, 'hello p'),
])

function eventHandler() {
  console.log('点击了')
}

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

六、 Snabbdom 源码解析
npm install
 
npm run build  

查看
七、 h() 函数
  • h函数介绍
    • 作用:创建vNode 对象
    • vue中的h函数
    • h 函数最早见于hyperscript,使用JavaScript创建超文本
//vue中的h函数
new Vue({
  router,
  store,
  render:h => h(App)
}).$mount('#app)
  • 函数重载
    • 概念:参数个数或参数类型不同的函数,重载的概念和参数相关,和返回值无关
    • JavaScript 中没有重载的概念
    • TypeScript中有重载,不过重载的实现还是通过代码调整参数
//函数重载--参数个数
function add(a:number,b:number){
  console.log(a+b);
}
function add(a:number,b:number,c:number){
  console.log(a+b+c);
}
add(1,2)
add(1,2,3)
//函数重载--参数类型
function add(a:number,b:number){
  console.log(a+b);
}
function add(a:number,b:string){
  console.log(a+b);
}
add(1,2)
add(1,'2')

// 函数的重载
export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode {
  var data: VNodeData = {}
  var children: any
  var text: any
  var i: number
  // 处理参数,实现重载的机制
  if (c !== undefined) {
    // 处理三个参数的情况
    // sel data children/text
    if (b !== null) {
      data = b
    }
    if (is.array(c)) {
      children = c
      // 如果c是字符串或者数字
    } else if (is.primitive(c)) {
      text = c
      // 如果c 是VNode
    } else if (c && c.sel) {
      children = [c]
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b
    } else if (is.primitive(b)) {
      text = b
    } else if (b && b.sel) {
      children = [b]
    } else { data = b }
  }
  if (children !== undefined) {
    // 处理children中的原始值(string/number)
    for (i = 0; i < children.length; ++i) {
      // 如果child 是string/number,创建文本节点
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
    }
  }
  if (
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    // 如果是svg,添加命名空间
    addNS(data, children, sel)
  }
  // 返回VNode
  return vnode(sel, data, children, text, undefined)
};

八、 快捷键

快速定位
Alt + ←向左方向键
ctrl+鼠标左键

九、 VNode
image.png
十、 Patch 整体过程分析
  • patch(oldVnode,newVnode)
  • 把新节点中变化的内容渲染到真实的DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧VNode是否相同节点(节点的key和sel相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode 的text不同,直接更新文本内容
  • 如果有新的VNode有children,判断子节点是否有变化
十一、patchVnode
image.png
十二、 Diff 算法
  • 虚拟DOM中的Differences算法

    • 查找两棵树每一个节点的差异


      image.png
  • Snabbdom 根据DOM的特点对传统的diff算法做了优化

    • DOM操作时候很少会跨级别操作节点
    • 只比较同级别的节点


      image.png
  • 对比子节点的具体过程,在对开始和结束节点比较的时候,共有四种情况

    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
    • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
    • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
image.png
  • 开始和结束节点

    • 如果新旧开始节点是sameVnode(key和sel相同)
      • 调用patchVnode()对比和更新节点
      • 把旧开始和新开始索引往后移动 oldStartIdx ++ / newStartIdx ++


        image.png
  • 旧开始节点 / 新结束节点

    • 调用patchVnode()对比和更新节点
    • 把oldStartVnode对应的DOM元素,移动到右边,更新索引


      image.png
  • 旧结束节点 / 新开始节点

    • 调用patchVnode()对比和更新节点
    • 把oldStartVnode对应的DOM元素,移动到左边,更新索引


      image.png
  • 非上述四种情况


    image.png
  • 循环结束

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

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


        image.png
  • newStartIdx >newEndIdx

    • 如果新节点的数组先遍历完(newStartIdx > newEndIdx)
      • 说明老节点有剩余,把剩余节点批量删除


        image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容