谈谈你对虚拟DOM原理的理解

Posted 前端进阶之旅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谈谈你对虚拟DOM原理的理解相关的知识,希望对你有一定的参考价值。

什么是 Virtual DOM

  1. Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual 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,aut ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown ,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup, onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend ,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl eave,onselectstart,onselectionchange,onanimationend,onanimationiteration ,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click ,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at tributes,shadowRoot,assignedSlot,innerhtml,outerHTML,scrollTop,scrollLef t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight ,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme ntTiming,previousElementSibling,nextElementSibling,children,firstElement Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture ,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames ,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute ,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_ NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION _NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace ,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo veEventListener,dispatchEvent
  1. 可以使用 Virtual DOM 来描述真实 DOM
{
  sel"div",
  data: {},
  childrenundefined,
  text"Hello Virtual DOM",
  elmundefined,
  keyundefined
}

为什么使用 Virtual DOM

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

虚拟 DOM 的作用

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

Virtual DOM 库

  • Snabbdom
    • Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
    • 通过模块可扩展
    • 源码使用 TypeScript 开发
    • 最快的 Virtual DOM 之一
  • virtual-dom

Snabbdom 基本使用

创建项目

# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建 package.json yarn init -y
# 本地安装 parcel
yarn add parcel-bundler

配置 package.jsonscripts

"scripts": {
"dev""parcel index.html --open""build""parcel build index.html"
}

创建目录结构

谈谈你对虚拟DOM原理的理解
yarn add snabbdom
import{init,h,thunk}from'snabbdom'

snabbdom 的核心仅提供最基本的功能,只导出了三个函数 init()h()thunk()

  • init() 是一个高阶函数,返回 patch()
  • h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
new Vue({
  router,
  store,
  renderh => h(App) 
}).$mount('#app')
  • thunk() 是一种优化策略,可以在处理不可变数据时使用

注意:导入时候不能使用 import snabbdom from 'snabbdom'。原因:node_modules/src/snabbdom.ts 末尾导出使用的语法是 export 导出 API,没有使用 export default 导出默认输出

谈谈你对虚拟DOM原理的理解

基本使用

例子1

import { h, init } from 'snabbdom'

// 1. hello world
// 参数:数组,模块
// 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
let patch = init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h('div#container.cls', { 
  hook: {
    init (vnode) {
      console.log(vnode.elm)
    },
    create (emptyVnode, vnode) {
      console.log(vnode.elm)
    }
  }
}, 'Hello World')

let app = document.querySelector('#app')
// 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数:VNode
// 返回值:VNde
let oldVnode = patch(app, vnode)

// 假设的时刻
vnode = h('div''Hello Snabbdom')

patch(oldVnode, vnode)

例子2

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

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

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

例子3 debug-patchVnode

import { h, init } from 'snabbdom'

let patch = init([])

// 首次渲染
let vnode = h('div''Hello World')
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

// patchVnode 的执行过程
vnode = h('div''Hello Snabbdom')
patch(oldVnode, vnode)

例子4 debug-updateChildren

import { h, init } from 'snabbdom'

let patch = init([])

// 首次渲染
let vnode = h('ul', [
  h('li''首页'),
  h('li''视频'),
  h('li''微博')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

// updateChildren 的执行过程
vnode = h('ul', [
  h('li''首页'),
  h('li''微博'),
  h('li''视频')
])
patch(oldVnode, vnode)

例子5 debug-updateChildren-key

import { h, init } from 'snabbdom'

let patch = init([])

// 首次渲染
let vnode = h('ul', [
  h('li', { key'a' }, '首页'),
  h('li', { key'b' }, '视频'),
  h('li', { key'c' }, '微博')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

// updateChildren 的执行过程
vnode = h('ul', [
  h('li', { key'a' }, '首页'),
  h('li', { key'c' }, '微博'),
  h('li', { key'b' }, '视频')
])
patch(oldVnode, vnode)

模块

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

常用模块

官方提供了 6 个模块

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

模块使用

模块使用步骤:

  • 导入需要的模块
  • init() 中注册模块
  • 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移
import { init, h } from 'snabbdom'
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
// 2. 注册模块
let patch = init([
  style,
  eventlisteners
])
// 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
let vnode = h('div', {
  style: {
    backgroundColor'red'
  },
  on: {
    click: eventHandler
  }
}, [
  h('h1''Hello Snabbdom'),
  h('p''这是p标签')
])

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

let app = document.querySelector('#app')

let oldVnode = patch(app, vnode)


vnode = h('div''hello')
patch(oldVnode, vnode)

Snabbdom 源码解析

Snabbdom 的核心

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

Snabbdom 源码

src 目录结构

谈谈你对虚拟DOM原理的理解

h 函数

  • h() 函数介绍: 在使用 Vue 的时候见过 h() 函数
new Vue({
  router,
  store,
  renderh => h(App) 
}).$mount('#app')
  • h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本
  • Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode

函数重载

  • 概念
  • 参数个数或类型不同的函数
  • JavaScript 中没有重载的概念
  • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
  • 重载的示意
function add (a, b
  console.log(a + b)
}
function add (a, b, c{
  console.log(a + b + c) 
}
add(12)
add(123)

源码位置:src/h.ts

// h函数的重载
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 
{
  let data: VNodeData = {}
  let children: any
  let text: any
  let 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
    } else if (c && c.sel) {
      children = [c]
    }
  } else if (b !== undefined && b !== null) {
    // 处理两个参数的情况
    if (is.array(b)) {
      children = b
      // 如果 b 是字符串或者数字
    } else if (is.primitive(b)) {
      text = b
      // 如果 b 是 VNode
    } 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(undefinedundefinedundefined, 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)
};

// 导出模块
export default h;

VNode

一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNodechildren 就是 Virtual DOM

源码位置: src/vnode.ts

export interface VNodeData {
  props?: Props
  attrs?: Attrs
  class?: Classes
  style?: VNodeStyle
  dataset?: Dataset
  on?: On
  hero?: Hero
  attachData?: AttachData
  hook?: Hooks
  key?: Key
  ns?: string // for SVGs
  fn?: () 
=> VNode // for thunks
  args?: any[] // for thunks
  is?: string // for custom elements v1
  [key: string]: any // for any other 3rd party module
}

export interface VNode {
  // 选择器
  sel: string | undefined
  // 节点数据:属性/样式/事件等
  data: VNodeData | undefined
  // 子节点,和 text 只能互斥
  childrenArray<VNode | string> | undefined
  // 记录 vnode 对应的真实 DOM
  elm: Node | undefined
  // 节点中的内容,和 children 只能互斥
  text: string | undefined
  // 优化用
  key: Key | undefined
}

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined
): VNode 
{
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

snabbdom

  • patch(oldVnode, newVnode)
  • 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧 VNode 是否相同节点(节点的 keysel 相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnodetext 不同,直接更 新文本内容
  • 如果新的 VNodechildren,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
  • diff 过程只进行同层级比较
谈谈你对虚拟DOM原理的理解

init

  • 功能: init(modules, domApi),返回 patch()函数(高阶函数)
  • 为什么要使用高阶函数?
    • 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如: modules/domApi/cbs
    • 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而 不需要重新创建
  • init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中

源码位置:src/init.ts

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI{
  let i: number
  let j: number
  const cbs: ModuleHooks = {
    create: [],
    update: [],
    remove: [],
    destroy: [],
    pre: [],
    post: []
  }

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi

  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]]
      if (hook !== undefined) {
        (cbs[hooks[i]] as any[]).push(hook)
      }
    }
  }
  ...

  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode)
    }

    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      elm = oldVnode.elm!
      parent = api.parentNode(elm) as Node

      createElm(vnode, insertedVnodeQueue)

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        removeVnodes(parent, [oldVnode], 00)
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    return vnode
  }
}

patch

  • 功能:
    • 传入新旧 VNode,对比差异,把差异渲染到 DOM
    • 返回新的 VNode,作为下一次 patch()oldVnode
  • 执行过程:
    • DOM 元素转换成 oldVnode
    • 调用 createElm()vnode 转换为真实 DOM,记录到 vnode.elm
    • 把刚创建的 DOM 元素插入到 parent
    • 移除老节点
    • 触发用户设置的 create 钩子函数
    • 调用 patchVnode(),找节点的差异并更新 DOM
    • 首先执行模块中的钩子函数 pre
    • 如果 oldVnodevnode 相同( keysel 相同)
    • 如果 oldVnodeDOM 元素

源码位置:src/snabbdom.ts

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)) {
    // 把 DOM 元素转换成空的 VNode
    oldVnode = emptyNodeAt(oldVnode)
  }
  // 如果新旧节点是相同节点(key 和 sel 相同)
  if (sameVnode(oldVnode, vnode)) {
    // 找节点的差异并更新 DOM
    patchVnode(oldVnode, vnode, insertedVnodeQueue)
  } else {
    // 如果新旧节点不同,vnode 创建对应的 DOM
    // 获取当前的 DOM 元素
    elm = oldVnode.elm!
    parent = api.parentNode(elm) as Node
    // 触发 init/create 钩子函数,创建 DOM
    createElm(vnode, insertedVnodeQueue)

    if (parent !== null) {
      // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
      api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
      // 移除老节点
      removeVnodes(parent, [oldVnode], 00)
    }
  }
  // 执行用户设置的 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

  • 功能:

    • createElm(vnode, insertedVnodeQueue),返回创建的 DOM 元素
    • 创建 vnode 对应的 DOM 元素
  • 执行过程:

  • 首先触发用户设置的 init 钩子函数

  • 如果选择器是!,创建评论节点

  • 如果选择器为空,创建文本节点

  • 如果选择器不为空

    • 解析选择器,设置标签的 idclass 属性
    • 执行模块的 create 钩子函数
    • 如果 vnodechildren,创建子 vnode 对应的 DOM,追加到 DOM
    • 如果 vnodetext 值是 string/number,创建文本节点并追击到 DOM
    • 执行用户设置的 create 钩子函数
    • 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中
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)
      data = vnode.data
    }
  }
  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
    // 如果选择器不为空
    // 解析选择器
    // 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(0Math.min(hash, dot)) : sel
    const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
      ? api.createElementNS(i, tag, data)
      : api.createElement(tag, data)
    if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
    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 中有子节点,创建子 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
}

patchVnode

  • 功能:
    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • 对比 oldVnodevnode 的差异,把差异渲染到 DOM
  • 执行过程:
    • 如果老节点有子节点,全部移除
    • 设置 DOM 元素的 textContentvnode.text
    • 如果 oldVnode.childrenvnode.children 都有值
    • 如果 vnode.children 有值, oldVnode.children 无值
    • 如果 oldVnode.children 有值, vnode.children 无值
    • 如果 oldVnode.text 有值
    • 调用 updateChildren()
    • 使用 diff 算法对比子节点,更新子节点
    • 清空 DOM 元素
    • 调用 addVnodes() ,批量添加子节点
    • 调用 removeVnodes() ,批量移除子节点
    • 清空 DOM 元素的内容
    • 首先执行模块的 create钩子函数
    • 然后执行用户设置的 create 钩子函数
    • 首先执行用户设置的 prepatch 钩子函数
    • 执行 create 钩子函数
    • 如果 vnode.text 未定义
    • 如果设置了 vnode.text 并且和和 oldVnode.text 不等
    • 最后执行用户设置的 postpatch 钩子函数
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)) {
    // 如果新老节点都有 children
    if (isDef(oldCh) && isDef(ch)) {
      // 使用 diff 算法对比子节点,更新子节点
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
    } else if (isDef(ch)) {
      // 如果新节点有 children,老节点没有 children
      // 如果老节点有text,清空dom 元素的内容
      if (isDef(oldVnode.text)) api.setTextContent(elm, '')
      // 批量添加子节点
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      // 如果老节点有children,新节点没有children
      // 批量移除子节点
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // 如果老节点有 text,清空 DOM 元素
      api.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    // 如果没有设置 vnode.text
    if (isDef(oldCh)) {
      // 如果老节点有 children,移除
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    }
    // 设置 DOM 元素的 textContent 为 vnode.text
    api.setTextContent(elm, vnode.text!)
  }
  // 最后执行用户设置的 postpatch 钩子函数
  hook?.postpatch?.(oldVnode, vnode)
}

updateChildren

  • 功能:
    • diff 算法的核心,对比新旧节点的 children,更新 DOM
  • 执行过程:
  • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二课树的每一个节点比 较,但是这样的时间复杂度为 O(n^3)
  • DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
  • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复 杂度为 O(n)
谈谈你对虚拟DOM原理的理解
  • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍 历的过程中移动索引
  • 在对开始和结束节点比较的时候,总共有四种情况
    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
    • oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
    • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
谈谈你对虚拟DOM原理的理解
  • 开始节点和结束节点比较,这两种情况类似
    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
  • 如果 oldStartVnodenewStartVnodesameVnode ( keysel 相同)
    • 调用 patchVnode() 对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
谈谈你对虚拟DOM原理的理解
  • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
    • 更新索引
    • 调用 patchVnode() 对比和更新节点
    • oldStartVnode 对应的 DOM 元素,移动到右边
谈谈你对虚拟DOM原理的理解
  • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
    • 调用 patchVnode() 对比和更新节点
    • oldEndVnode 对应的 DOM 元素,移动到左边
    • 更新索引
谈谈你对虚拟DOM原理的理解
  • 如果不是以上四种情况
    • 判断新节点和找到的老节点的 sel 选择器是否相同
    • 如果不相同,说明节点被修改了
    • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
    • 重新创建对应的 DOM 元素,插入到 DOM 树中
    • 创建新节点对应的 DOM 元素,插入到 DOM 树中
    • 遍历新节点,使用 newStartNodekey 在老节点数组中找相同节点
    • 如果没有找到,说明 newStartNode 是新节点
    • 如果找到了
谈谈你对虚拟DOM原理的理解
  • 循环结束
    • 当老节点的所有子节点先遍历完 ( oldStartIdx > oldEndIdx),循环结束
    • 新节点的所有子节点先遍历完 ( newStartIdx > newEndIdx),循环结束
  • 如果老节点的数组先遍历完( oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
谈谈你对虚拟DOM原理的理解
  • 如果新节点的数组先遍历完( newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批 量删除

end


1. 如果觉得文章帮到您「点赞、在看、分享,让更多人也能看到这篇文章

以上是关于谈谈你对虚拟DOM原理的理解的主要内容,如果未能解决你的问题,请参考以下文章

灵魂拷问第6篇:谈谈你对重绘和回流的理解

谈谈你对可重入锁和不可重入锁的理解区别及原理

谈谈虚拟dom在Vue和React中的应用

面试官:谈谈虚拟dom在Vue和React中的应用

java面试-谈谈你对volatile的理解

谈谈对虚拟DOM的理解