学习笔记(十四)Vue Virtual DOM的实现原理

2020-12-11  本文已影响0人  彪悍de文艺青年

什么是虚拟DOM

虚拟DOM是一个普通的JavaScript对象,用来描述真实的DOM

创建虚拟DOM的开销要比创建真实DOM小很多

为什么要使用虚拟DOM?

虚拟DOM的作用和虚拟DOM库

虚拟DOM的作用

虚拟DOM库

Snabbdom的基本使用

注:以下使用的Snabbdom均为2.1.x版本,不同版本的使用方式上存在一定的差异

创建项目

导入Snabbdom

基本使用

通过两个简单的例子来演示snabbdom的基本使用方式

snabbdom中的模块

snabbdom的核心库不能处理元素的属性/样式/事件等,如果需要处理,可以使用模块

官方提供了6个常用的模块

模块的使用

// 1. 导入模块
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
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', {
    style: {
        backgroundColor: 'red'
    },
    on: {
        click: function() {
            console.log('click')
        }
    }
}, [
    h('h1', 'hello world'),
    h('p', 'hello p')
])
 
const app = document.querySelector('#app')
 
patch(app, vnode)

Snabbdom核心源码解析

h()

vnode

init(modules, domApi)

patch(oldVNode, newVNode)

比较新旧vnode的变化,并将新节点中变化的内容渲染到真实DOM,最终返回新节点作为下一次处理的旧节点

patch执行的整体过程

return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    // 保存新插入节点的队列,用于触发钩子函数
    const insertedVnodeQueue: VNodeQueue = []
    // 执行模块的所有 pre 钩子函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
 
    // 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm
    if (!isVnode(oldVnode)) { // 通过对象是否包含 sel 属性判断
      // 把 DOM 元素转换成空的 VNode
      // 调用 vnode 构造函数创建 VNode
      oldVnode = emptyNodeAt(oldVnode)
    }
 
    // 判断新旧节点是否相同
    // 判断新旧vnode的key与sel是否相同
    if (sameVnode(oldVnode, vnode)) {
      // 找节点的差异并更新 DOM
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      // 如果新旧节点不同,vnode创建对应的 DOM
      // 获取当前的 DOM 元素
      elm = oldVnode.elm!
      // 获取 DOM 元素的父节点
      parent = api.parentNode(elm) as Node
 
      // 创建 vnode 对应的 DOM 元素,并触发 init/create 钩子函数
      createElm(vnode, insertedVnodeQueue)
 
      if (parent !== null) {
        // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        // 移除老节点
        removeVnodes(parent, [oldVnode], 0, 0)
      }
    }
 
    // 执行用户设置的 insert 钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    // 执行用户设置的 post 钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    // 返回 vnode
    return vnode
  }

createElm(vnode, insertedVnodeQueue)

创建 vnode 对应的 DOM 元素,并触发 init/create 钩子函数

function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any
    let data = vnode.data
    if (data !== undefined) {
      // 执行用户传入的 init 钩子函数
      const init = data.hook?.init
      if (isDef(init)) {
        init(vnode)
        // 用户传入的 init 函数可能修改 vnode 的 data
        // 需要重新赋值 data
        data = vnode.data
      }
    }
    // 把 vnode 转换成真实 DOM 对象 (但并没有渲染到页面)
    const children = vnode.children
    const sel = vnode.sel
    if (sel === '!') {
      // 创建注释节点
      if (isUndef(vnode.text)) {
        vnode.text = ''
      }
      vnode.elm = api.createComment(vnode.text!)
    } else if (sel !== undefined) {
      // Parse selector
      // 解析选择器     
      const hashIdx = sel.indexOf('#')
      const dotIdx = sel.indexOf('.', hashIdx)
      const hash = hashIdx > 0 ? hashIdx : sel.length
      const dot = dotIdx > 0 ? dotIdx : sel.length
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag) // 创建带命名空间的 DOM 元素
        : api.createElement(tag) // 创建普通的 DOM 元素
      // 设置元素 id
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
      // 设置元素 class
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
      // 执行模块中的 create 钩子函数
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
      // 如果 vnode 中有子节点,递归调用 createElm 创建子 vnode 对应的 DOM 元素,并追加到 DOM 树
      if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i]
          if (ch != null) {
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
          }
        }
      } else if (is.primitive(vnode.text)) {
        // 如果 vnode 的 text 是 string/number, 创建文本节点并追加到 DOM 树
        api.appendChild(elm, api.createTextNode(vnode.text))
      }
      const hook = vnode.data!.hook
      if (isDef(hook)) {
        // 执行传入的 create 钩子函数
        hook.create?.(emptyNode, vnode)
        if (hook.insert) {
          // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
          insertedVnodeQueue.push(vnode)
        }
      }
    } else {
      // 如果选择器为空,创建文本节点
      vnode.elm = api.createTextNode(vnode.text!)
    }
    // 返回新创建的 DOM
    return vnode.elm
  }
image-20201211002029821

removeVnodes(parentElm, vnodes, start, end)

用于批量移除节点

function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number
      let rm: () => void
      const ch = vnodes[startIdx]
      // 判断 vnode 是否有值     
      if (ch != null) {
        // 判断 sel 是否有值
        // 有值为元素节点,否则为文本节点
        if (isDef(ch.sel)) {
          // 执行用户定义的 destory 钩子函数(包含子节点)
          invokeDestroyHook(ch)
          listeners = cbs.remove.length + 1
          // 创建删除的回调函数
          // 通过listeners计数判断,最终当listeners为0时才会真正执行
          rm = createRmCb(ch.elm!, listeners)
          // 执行用户设置模块的 remove 钩子函数
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
          const removeHook = ch?.data?.hook?.remove
          // 判断是否存在用户定义的 remove 钩子函数
          // 存在则先执行用户定义的钩子函数
          // 不存在则直接执行删除元素的方法
          if (isDef(removeHook)) {
            removeHook(ch, rm)
          } else {
            rm()
          }
        } else { // Text node
          // 文本节点,直接移除
          api.removeChild(parentElm, ch.elm!)
        }
      }
    }
  }

addVnodes(parentElm, before, vnodes, start, end, insertedVnodeQueue)

用于批量添加节点

function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    // 遍历 vnodes
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (ch != null) {
        // 通过 createElm 将 vnode 转换为真实的 DOM,并插入指定的元素之前
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
      }
    }
  }

patchVnode(oldVnode, vnode, insertedVnodeQueue)

用于比较新老 vnode 之间的差异,并更新 DOM

image-20201211183117085
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    const hook = vnode.data?.hook
    // 执行用户设置的 prepatch 钩子函数
    hook?.prepatch?.(oldVnode, vnode)
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    // 如果新老 vnode 相同,则返回
    if (oldVnode === vnode) return

    if (vnode.data !== undefined) {
      // 执行模块的 update 钩子函数
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 执行用户设置的 update 钩子函数
      vnode.data.hook?.update?.(oldVnode, vnode)
    }
    // 如果 vnode.text 未定义
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新旧 vnode 都存在子节点,且子节点不相同
        // 调用 updateChildren 更新子节点
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      } else if (isDef(ch)) {
        // 新 vnode 存在子节点,老 vnode 不存在子节点
        // 如果老 vnode.text 存在,则清空 DOM 元素的 textContent
        if (isDef(oldVnode.text)) api.setTextContent(elm, '')
        // 批量添加新 vnode 中的子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 老 vnode 存在子节点,新 vnode 不存在子节点
        // 批量删除老 vnode 下的子节点
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // 新老 vnode 都不存在子节点
        // 老 vnode.text 存在,则清空 DOM 元素的 textContent
        api.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 老 vnode.text 与新 vnode.text 不相同
      // 老 vnode 存在子节点,则批量移除老 vnode 的所有子节点
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      }
      // 将 DOM 元素的 textContent 设置成新 vnode.text
      api.setTextContent(elm, vnode.text!)
    }
    // 执行用户设置的 postpatch 钩子函数
    hook?.postpatch?.(oldVnode, vnode)
  }

updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue)

比较新老 vnode 的子节点,并更新,是diff算法的核心

模块源码

snabbdom为了保证核心代码的精简,将处理元素属性、样式、事件等工作,放到了模块中

模块可以按需引入

模块实现的核心基于Hooks

以 attributes 模块为例

function updateAttrs (oldVnode: VNode, vnode: VNode): void {
  var key: string
  var elm: Element = vnode.elm as Element
  var oldAttrs = (oldVnode.data as VNodeData).attrs
  var attrs = (vnode.data as VNodeData).attrs

  // 新老节点没有属性,直接返回
  if (!oldAttrs && !attrs) return
  // 新老节点属性相同,直接返回
  if (oldAttrs === attrs) return
  oldAttrs = oldAttrs || {}
  attrs = attrs || {}

  // update modified attributes, add new attributes
  // 遍历新元素的属性
  for (key in attrs) {
    const cur = attrs[key]
    const old = oldAttrs[key]
    // 新老节点属性不同时
    if (old !== cur) {
      // 处理布尔类型
      if (cur === true) {
        elm.setAttribute(key, '')
      } else if (cur === false) {
        elm.removeAttribute(key)
      } else {
        if (key.charCodeAt(0) !== xChar) {
          elm.setAttribute(key, cur as any)
        } else if (key.charCodeAt(3) === colonChar) {
          // Assume xml namespace
          elm.setAttributeNS(xmlNS, key, cur as any)
        } else if (key.charCodeAt(5) === colonChar) {
          // Assume xlink namespace
          elm.setAttributeNS(xlinkNS, key, cur as any)
        } else {
          elm.setAttribute(key, cur as any)
        }
      }
    }
  }
  // remove removed attributes
  // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
  // the other option is to remove all attributes with value == undefined
  // 遍历老元素的属性,判断在新元素中是否存在,没有则移除
  for (key in oldAttrs) {
    if (!(key in attrs)) {
      elm.removeAttribute(key)
    }
  }
}

// 使用 create update 两个 hook
export const attributesModule: Module = { create: updateAttrs, update: updateAttrs }

上一篇 下一篇

猜你喜欢

热点阅读