Virtual DOM

这次我们的学习目标有三:
1.了解什么是虚拟DOM,以及虚拟DOM的作用
2.Snabbdom的基本使用
3.Snabbdom的源码解析

什么是Virtual DOM

  • Virtual DOM就是虚拟DOM,是由普通的JS对象来描述DOM对象
  • 使用Virtual DOM来描述真实的DOM


为什么要使用Virtual DOM?

  • DOM的操作本身是性能会出现问题,操作比较复杂的
  • MVVM框架解决视图和状态同步问题
  • 模板引擎可以简化视图操作,没办法跟踪状态
    (无法得知当前页面变化之前的状态)
  • 虚拟DOM能够跟踪状态变化
  • 参考github上virtual-dom的动机描述
    • 虚拟DOM可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次状态差异更新真实DOM

实际案例

传统DOM操作方式:

https://codesandbox.io/s/jquery-demo-yr65q

虚拟DOM操作方式:

https://obk5t.csb.app/

通过实例我们就可以轻易区分出两者的不同之处

Virtual DOM的作用

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

虚拟DOM库

  • Snabbdom
    • Vue.js 2.x内部使用的虚拟DOM,就是改造的Snabbdom
    • 大约200 SLOC
    • 通过模块可拓展
    • 源码使用TS开发
    • 最快的Virtual DOM之一
  • virtual-dom

Snabbdom基本使用方式

  • 安装parcel
  • 配置scripts
  • 目录结构


Snabbdom 文档

https://github.com/snabbdom/snabbdom

当前版本:v2.1.0
官方文档中文翻译:

https://github.com/coconilu/Blog/issues/152

根据文档所述:

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

console.log(init)
console.log(h)

我们引入两个核心功能,这里要注意一下,因为webpack版本问题,我们按照官方文档那样引入的话会出现加载错误的问题,所以我们应该按照如上代码依次查询路径导入

打印一下init和h方法,可以看到我们主要使用的方法的具体内容

基本使用:

  • 主要用到了init函数和h函数
  • h函数有两个参数,第一个参数是新定义的标签(包含class和id),第二个参数是新的内容(传入字符串)
  • 要获取挂载的元素,通过init函数得到patch函数,第一次声明patch,init内部得是空数组
  • patch有两个参数,第一个是挂载的元素dom,第二个是通过h函数创建的vnode
    详细来看就是这样的:
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'

// 1.通过h函数创建VNode
let vNode = h('div#box.container', '新内容')

// 获取挂载元素
const dom = document.querySelector('#app')

// 2.通过init函数得到patch函数
const patch = init([])

// 3.通过patch将VNode渲染到DOM
let oldVnode = patch(dom, vNode)

// 4.创建新的Vnode,更新给oldVnode
vNode = h('p#text.abc', '这是p标签')
patch(oldVnode, vNode)

包含子节点

跟基本使用大体上是一样的,但是有一点不同,h函数的第二个参数,如果是数组的话表示子节点列表

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

const patch = init([])

// 创建包含子节点的VNode
// h的参数二为子节点列表,内部就应该传入vNode
let vNode = h('div#container', [
  h('h1', '标题1'),
  h('p', '内容1')
])

// 获取挂载元素
const dom = document.querySelector('#app')

// 渲染vNode
patch(dom, vNode)
效果图

函数参数为!时表示清空

模块使用

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

模板的作用

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

官方提供的模块

  • attributes
  • props
  • dataset
  • class
  • style
  • eventlisteners

模块使用步骤

  • 导入需要的模块
  • init()中注册模块
  • h函数的第二个参数处使用模块
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'

// 1.导入模块(注意拼写,导入的名称不要写错)
import { styleModule } from 'snabbdom/build/modules/style'
import { eventListenersModule } from 'snabbdom/build/modules/eventlisteners'

console.log(styleModule);
console.log(eventListenersModule)
// 2.注册模块(为patch函数添加模块对应的能力)
const patch = init([
    styleModule,
    eventListenersModule
])

// 3.使用模块
let vNode = h('div#box', {
    style: {
        backgroundColor: 'green',
        height: '200px',
        width: '200px'
    }
}, [
    h('h1#title', {
      style: {
          color: '#fff'
      },
      on: {
          click () {
              console.log('点击了h1标签')
          }
      }
    }, '这是标题内容'),
    h('p', '这是内容文本')
])

const dom = document.getElementById('app')
patch(dom, vNode)
效果图

Snabbdom源码解析

我们该怎么看源码呢?

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

Snabbdom的核心

  • init()设置模块,创建patch函数
  • 使用h函数创建JS对象(VNode)描述真实DOM
  • patch()比较新旧两个VNode
  • 把变化的内容更新到真实的DOM树

源码

  • 地址:

https://github.com/snabbdom/snabbdom

  • 克隆代码

git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git

h函数

  • 作用:创建VNode对象
  • Vue的h函数


函数重载

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

参数类型

h函数的重载

内部本身只做判断

patch整理过程分析

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

init

钩子函数与init构造函数

patch

patch函数

createElm

在patch函数中使用到的createElm



patchVnode

updateChildren

  function updateChildren (parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx: KeyToIndexMap | undefined
    let idxInOld: number
    let elmToMove: VNode
    let before: any

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx]
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          elmToMove = oldCh[idxInOld]
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined as any
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
      } else {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
      }
    }
  }

注:后续会专门挨着挨着解析源码的作用

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

推荐阅读更多精彩内容