简述vue中的虚拟dom及diff算法

50 篇文章 3 订阅
33 篇文章 0 订阅

为什么使用虚拟DOM

手动操作DOM比较麻烦,而且需考虑兼容性,但是随项目复杂度,dom操作更复杂

为了简化DOM的复杂操作出现mvvm框架,解决视图-状态的同步问题

为简化视图操作可使用模板引擎,但是模板没解决跟踪状态变化的问题,所以使用Virtual dom Virtual

Dom好处是状态改变不需要更新DOM,只需要虚拟树描述,Virtual DOM内部将有效更新dom

什么是虚拟DOM

Virtual DOM 就是 普通的JS对象,以对象的结构描述DOM结构
因为不是真实DOM节点,所以叫做虚拟DOM
真实DOM结构

let element = document.querySelector("#app");
let s = ''
for(let key in element){
    s += key + ','
}
console.log(s) 
// 可以看到一个真实dom节点具有非常多的开销
// __vue__,align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,autocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,offsetTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,oncopy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onchange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondragend,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchange,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypress,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup,onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,onresize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongotpointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointerup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerleave,onselectstart,onselectionchange,dataset,nonce,tabIndex,click,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,namespaceURI,prefix,localName,tagName,id,className,classList,slot,part,attributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLeft,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,previousElementSibling,nextElementSibling,children,firstElementChild,lastElementChild,childElementCount,onfullscreenchange,onfullscreenerror,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttributeNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAttributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElementsByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentElement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClientRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scrollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,remove,prepend,append,querySelector,querySelectorAll,requestFullscreen,webkitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestinationInsertionPoints,elementTiming,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMMENT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION_NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMENT_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAINED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseURI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstChild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChildNodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocumentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace,insertBefore,appendChild,replaceChild,removeChild,addEventListener,removeEventListener,dispatchEvent,

Virtual DOM

{
    sel:'div',
    data:{},
    Children: undefined,
    text: 'virtual dom',
    elm: undefined,
    key: undefined
}
// 虚拟dom相对开销节省很多

Virtual DOM作用和Virtual DOM库

作用

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

Virtual DOM库
Snabbdom

vue2.0 内部使用的Virtual DOM改造的Snabbdom
大约200 SLOC
通过模块扩展
源码使用TS开发
最快的Virtual Dom之一

Snabbdom 基本使用

创建项目----打包工具使用parcel
mkdir snabbdom-demo  
yarn init -y
yarn add parcel-bundler
导入Snabbdom   
yarn add snabbdom

修改package.json

"scripts": {
    "dev": "parcel index.html  --open",
    "build": "parcel build index.html"
  },
   "exports": {
    "./init": "./build/package/init.js",
    "./h": "./build/package/h.js",
    "./helpers/attachto": "./build/package/helpers/attachto.js",
    "./hooks": "./build/package/hooks.js",
    "./htmldomapi": "./build/package/htmldomapi.js",
    "./is": "./build/package/is.js",
    "./jsx": "./build/package/jsx.js",
    "./modules/attributes": "./build/package/modules/attributes.js",
    "./modules/class": "./build/package/modules/class.js",
    "./modules/dataset": "./build/package/modules/dataset.js",
    "./modules/eventlisteners": "./build/package/modules/eventlisteners.js",
    "./modules/hero": "./build/package/modules/hero.js",
    "./modules/module": "./build/package/modules/module.js",
    "./modules/props": "./build/package/modules/props.js",
    "./modules/style": "./build/package/modules/style.js",
    "./thunk": "./build/package/thunk.js",
    "./tovnode": "./build/package/tovnode.js",
    "./vnode": "./build/package/vnode.js"
  }

使用
创建对应文件index.html , src/basicuage.js

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

Snabbdom核心只提供最基本的功能,只导出 init(),h(),thunk()
init()是一个高阶函数,返回patch()
h()返回虚拟节点VNode,这个函数我们在使用Vue.js的时候见过
new Vue({
router,
store,
render: h=>h(App) })

thunk()是一种优化策略,可以出来不可变数据时使用
import { h } from ‘snabbdom/build/package/h’
import { init } from ‘snabbdom/build/package/init’

// 1.写一个hello world
let patch = init([])
// 第一个参数:标签+选择器
// 第二个参数: 如果是字符串就是标签里的内容
let vnode = h('div#container.cls',"HELLO world")

let app = document.querySelector("#app")
// 第一个参数:可以是dom元素,内部会把Dom转成VNode
// 第二个参数:VNode
// 返回值:VNode
let oldVnode = patch(app,vnode)
// 2. div 中放置子元素h1,p
import { h } from 'snabbdom/build/package/h'
import { init } from 'snabbdom/build/package/init'

let patch = init([])

let vnode = h('div#container',[
    h('h1','hello Snabbdom'),
    h('p','p Snabbdom'),
])

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

let oldValue = patch(app,vnode)
// 下方是模拟下远程加载数据,并清除,可以注释
setTimeout(()=>{
    vnode = h('div#container',[
        h('h1','hello world'),
        h('p','hello p'),
    ])
    patch(oldValue,vnode)

    // 清空元素
    // patch(oldValue,vnode)
    patch(oldValue,h('!'))
},1000)

模块

Snabbdom的核心并不能处理元素的属性、样式、事件。如果需要处理,可使用模块

常用模块

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

使用

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)

Vue中的虚拟DOM

vue 中借鉴使用了 开源的虚拟dom库 snabbdom
所以直接解析snabbdom,效果相同

学习过程

宏观了解 有目标准确阅读
不求甚解,代码分支逻辑很复杂,主要是主干部分
调试,自己模拟一个

Snabbdom的核心

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

Snabbdom 源码

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

h函数
介绍

vue时见过h()函数,并且vue增强了h函数,可以创建组件
new Vue({
router,
store,
render: h=>h(App) })

h函数最早是hyperscript,使用JS创建超文本
Snabbdom中的h函数不是创建超文本,而是Vnode

函数重载

概念
参数个数或类型不同的函数
JavaScript中没有重载的概念
Typescript中有重载,不过重载还是通过代码调整参数
示例
function(a,b){console.log(a+b)}
function(a,b,c){console.log(a+b+c)}
add(1,2)
add(1,2,3)

源码 src/h.ts

部分概念
最先导入各种模块,并且设置了相对应的类型
h 函数定义了4种重载形式,
h函数先进行处理第3个参数
并使用addNS方法处理svg标签,将所有子元素递归调用
h函数支持两种方式的导出,export和 export default h函数
核心的调用vnode函数返回虚拟节点

h.ts

import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'

export type VNodes = VNode[]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement<T> = T | T[]
export type VNodeChildren = ArrayOrElement<VNodeChildElement>

function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
  data.ns = 'http://www.w3.org/2000/svg'
  if (sel !== 'foreignObject' && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      // 循环判断所有的子元素,给所有的子元素递归调用addNS
      const childData = children[i].data
      if (childData !== undefined) {
        addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
      }
    }
  }
}

// 函数的重载
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) {
    // 处理两个参数的情况
    // 如果 b 是数组
    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(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)
  }
  return vnode(sel, data, children, text, undefined)
};

// 导出模块
export default h;

VNode 实现

利用JavaScript对象结构描述真实DOM
最后返回结构 : { sel, data, children, text, elm, key }

import { Hooks } from './hooks'
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'
export type Key = string | number

// ts interface接口  约束所有的VNode都拥有这些属性
export interface VNode {
  // 选择器
  sel: string | undefined
  // 节点数据:属性、样式、事件
  data: VNodeData | undefined
  // 子节点,和text只能互斥
  children: Array<VNode | string> | undefined
  // 记录vnode对应的真实dom
  elm: Node | undefined
  // 节点在的内容和 children 只能互斥
  text: string | undefined
  // 优化使用
  key: Key | undefined
}

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
  [key: string]: any // for any other 3rd party module
}

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
  // js 返回了一个对象,这个对象描述了对应的节点
  return { sel, data, children, text, elm, key }
}

Patch 的整体过程 – VNode渲染真实DOM
snabbdom

  1. patch(oldValue,newVnode),patch具有新旧节点两个属性

  2. patch俗称打补丁,是将新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
    下方是patch具体的渲染过程

  3. 对比新旧VNode是否相同节点(节点的key和sel相同,sel选择器,key是追踪变化优化使用)

  4. 如果不是相同节点,删除之前的内容,重新渲染

  5. 如果是相同节点,在判断新的VNode是否有text,如果有并且和oldVnode的text不同,直接更新文本内容

  6. 如果新的VNode有children,判断自己诶单是否有变化,判断子节点的过程使用的就是diff算法 (diff相当于大家来找茬,找出具体那块有变化的过程)

  7. diff过程只进行同层级比较(diff不会将父节点同后代比较,只会是第一层比较,第二层比较,第三层比较)
    在这里插入图片描述

init函数会返回patch函数,所以先看init
init

开始处理一些引用模块
声明一些类型
下方是一些辅助函数
// 定义的对应钩子 const hooks: Array = [‘create’, ‘update’, ‘remove’, ‘destroy’, ‘pre’, ‘post’]
以数组形式定义的生命周期钩子函数,在下方的init函数内部存储到cbs内部
最后export function init 暴露init函数,init内部return 返回patch函数

patch

patch函数是snabbdom的核心函数,
功能是对比两个VNode的差异,把VNode的差异渲染到真实DOM,返回新的VNode
执行过程
定义insertedVnodeQueue,是插入节点的队列,是为了触发节点上的钩子函数
触发了所有模块的pre钩子函数,pre是处理节点之前处理的第一个钩子函数
下方是进行新旧节点,VNode的差异并更新
elm = oldVnode.elm! 语法是ts的,保证这个节点不为空
进行判断,如果父节点不为空,渲染新节点,删除老节点
下方是执行对应的insert 钩子函数,确定insertedVnodeQueue是有值的,调用对应的hook里的insert方法
执行对应的post钩子函数
返回vnode节点

createElm – 创建对应的元素,但是并未渲染到界面上

执行过程

  1. 执行模块对应的init函数
  2. 将vnode转换为对应的DOM对象,根据选择器sel生成不同类型节点
    hook.create?.(emptyNode, vnode) ts语法,如果create有值,直接调用函数生成节点,没值不调用函数
  3. 返回新创建的DOM
    在这里插入图片描述

patchVnode – 对比新旧节点,更新差异
在这里插入图片描述

updateChildren --diff算法核心

功能
diff算法核心,对比新旧节点的chidren,更新DOM
执行过程

  • 要对比两棵树的差异,可以取第一个每棵树的每一个节点依次和第二个树的每一个节点比较,但是这样的时间复杂度O(n∧3)
  • 在DOM操作的时候我们很少会把一个父节点移动/更新到某一个子节点
  • 因此只需要找同级的子节点依次比较,然后再找下一级别的节点比较,这样算法的复杂度为O(n)
    在这里插入图片描述
  • 在进行同级别节点比较时候,首先会对新老节点数组的开始和结尾设置标记索引,遍历过程移动索引
  • 在对开始和结束节点比较时候,总共会有四种情况
    oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)
    oldEndVnode / newEndVnode(旧结束节点 / 新结束节点)
    oldStartVnode / oldEndVnode(旧开始节点 / 新结束节点)
    oldEndVnode / newStartVnode(旧结束节点 / 新开始节点)

updateChildren 执行过程

  1. 定义一些变量,开始索引,等等
  2. while循环部分,查找索引的变化,进行对比索引的元素
  3. 循环结束,进行判断,进行收尾工作

源码部分

 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)) {
        // 1. 比较老开始节点 及 新开始节点
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 2. 比较老结束节点,新结束节点
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
        // Vnode moved right
        // 3. 比较老开始节点 及 新结束节点
        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
        // 4. 比较老结束节点 及 新开始节点
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } 
      // 开始节点和结束节点都不相同
      // 使用 newStartNode 的 key 在老节点数组中查找相同节点
      // 先设置 记录 key 和 index 的对象
      
      else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }
        // 遍历 newStartVnode,从老的节点找相同 key 的 oldVnode 的索引
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        // 如果是新的Vnode
        if (isUndef(idxInOld)) { // New element
          // 如果没找到,newStartNode 是新的节点
          //  创建元素插入 DOM树
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
          elmToMove = oldCh[idxInOld]
          if (elmToMove.sel !== newStartVnode.sel) {
            // 如果新旧节点的选择器不同
            // 创建新开始节点对应的DOM元素,插入到DOM树中
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
            // 如果相同, patchVnode()
            //  把 elmToMove 对应的DOM元素,移动到左边
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined as any
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        // 重新给newStartVnode 赋值,指向下一个新节点
        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)
      }
    }
  }

完整代码

import { Module } from './modules/module'
import { vnode, VNode } from './vnode'
import * as is from './is'
import { htmlDomApi, DOMAPI } from './htmldomapi'

type NonUndefined<T> = T extends undefined ? never : T

function isUndef (s: any): boolean {
  return s === undefined
}
function isDef<A> (s: A): s is NonUndefined<A> {
  return s !== undefined
}

type VNodeQueue = VNode[]

const emptyNode = vnode('', {}, [], undefined, undefined)

function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}

function isVnode (vnode: any): vnode is VNode {
  return vnode.sel !== undefined
}

type KeyToIndexMap = {[key: string]: number}

type ArraysOf<T> = {
  [K in keyof T]: Array<T[K]>;
}

type ModuleHooks = ArraysOf<Required<Module>>

function createKeyToOldIdx (children: VNode[], beginIdx: number, endIdx: number): KeyToIndexMap {
  const map: KeyToIndexMap = {}
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key
    if (key !== undefined) {
      map[key] = i
    }
  }
  return map
}
// 定义的对应钩子
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']

// modules 处理模块的样式,属性,事件等 ,    domApi 是返回具体的dom操作,没传入就是默认的dom操作api  
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number
  let j: number
  const cbs: ModuleHooks = { // cbs 存放钩子函数
    create: [],
    update: [],
    remove: [],
    destroy: [],
    pre: [],
    post: []
  }
  // 初始化转换虚拟节点的 api
  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
  // 把传入的所有模块的钩子函数,同意存储到 cbs
  // 最终构建的 cbs 对象的形式 cbs = {create:[fn1,fn2],upfate:[],....}
  for (i = 0; i < hooks.length; ++i) {
    // cbs.create = [],cubs.update = [] ....
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      // modules 传入的模块数组
      // 获取模块中的hook 函数
      // hook = modules[0][create]....
      const hook = modules[j][hooks[i]]
      if (hook !== undefined) {
        // 把获取到的hook函数放入到 cbs 对应的钩子函数数组中
        (cbs[hooks[i]] as any[]).push(hook)
      }
    }
  }

  function emptyNodeAt (elm: Element) {
    const id = elm.id ? '#' + elm.id : ''
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
    return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
  }

  function createRmCb (childElm: Node, listeners: number) {
    // 返回删除元素的回调函数
    return function rmCb () {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm) as Node
        api.removeChild(parent, childElm)
      }
    }
  }
  // 将Vnode 转换成dom元素,但是未渲染到界面上
  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钩子,将虚拟节点赋值
        init(vnode)
        data = vnode.data
      }
    }
    // 把 vnode 转换成真实 DOM 对象 (没有渲染到界面)
    const children = vnode.children
    const sel = vnode.sel
    if (sel === '!') { 
      // 如果 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) // 带有命名空间,创建svg标签
        : api.createElement(tag)  // 创建普通节点
      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) {
            // 如果 elm的子节点不为空,递归调用
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
          }
        }
      }
      // 如果 vnode 的 text 值是string/number,创建文本节点并追加到 dom 树 
      else if (is.primitive(vnode.text)) {
        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
  }

  function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx]
      if (ch != null) {
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
      }
    }
  }

  function invokeDestroyHook (vnode: VNode) {
    const data = vnode.data
    if (data !== undefined) {
      data?.hook?.destroy?.(vnode)
      for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
      if (vnode.children !== undefined) {
        for (let j = 0; j < vnode.children.length; ++j) {
          const child = vnode.children[j]
          if (child != null && typeof child !== 'string') {
            invokeDestroyHook(child)
          }
        }
      }
    }
  }

  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]
      if (ch != null) {
        // 如果 sel 有值
        if (isDef(ch.sel)) {
          // 执行 destroy 钩子函数 (会执行所有子节点 destory 钩子函数)
          invokeDestroyHook(ch)
          listeners = cbs.remove.length + 1
          // 创建删除的回调函数
          rm = createRmCb(ch.elm!, listeners)
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
          // 执行用户设置的 remove 钩子函数
          const removeHook = ch?.data?.hook?.remove
          if (isDef(removeHook)) {
            removeHook(ch, rm)
          } else {
            // 如果没有用户钩子函数,直接调用删除元素的方法
            rm()
          }
        } else { // Text node  如果是文本节点,直接调用删除元素的方法
          api.removeChild(parentElm, ch.elm!)
        }
      }
    }
  }

  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)) {
        // 1. 比较老开始节点 及 新开始节点
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 2. 比较老结束节点,新结束节点
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { 
        // Vnode moved right
        // 3. 比较老开始节点 及 新结束节点
        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
        // 4. 比较老结束节点 及 新开始节点
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } 
      // 开始节点和结束节点都不相同
      // 使用 newStartNode 的 key 在老节点数组中查找相同节点
      // 先设置 记录 key 和 index 的对象
      
      else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        }
        // 遍历 newStartVnode,从老的节点找相同 key 的 oldVnode 的索引
        idxInOld = oldKeyToIdx[newStartVnode.key as string]
        // 如果是新的Vnode
        if (isUndef(idxInOld)) { // New element
          // 如果没找到,newStartNode 是新的节点
          //  创建元素插入 DOM树
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
        } else {
          // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
          elmToMove = oldCh[idxInOld]
          if (elmToMove.sel !== newStartVnode.sel) {
            // 如果新旧节点的选择器不同
            // 创建新开始节点对应的DOM元素,插入到DOM树中
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
          } else {
            // 如果相同, patchVnode()
            //  把 elmToMove 对应的DOM元素,移动到左边
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined as any
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
          }
        }
        // 重新给newStartVnode 赋值,指向下一个新节点
        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)
      }
    }
  }

  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    // 获取用户传入的钩子函数
    const hook = vnode.data?.hook
    // 首先执行用户设置的perpatch 钩子函数
    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) { // 如果新节点的data属性不为空
      // 执行所有模块的 upfate 钩子函数
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)// 执行模块的update
      vnode.data.hook?.update?.(oldVnode, vnode) // 执行用户传入的update
    }
    // 如果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)
  }
  // init 内部返回 patch 函数,把vnode渲染从真实dom,并返回vnode
  // patch采用高阶函数,访问到modules,domApi 形成闭包,全局一直可以访问
  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]()
    // 如果oldValue 不是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! // elm节点不为空
      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!.ho
      ok!.insert!(insertedVnodeQueue[i])
    }
    // 执行模块的 post 钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    // 返回 vnode
    return vnode
  }
}

虚拟DOM 的执行过程

  1. 先执行模块对应的pre钩子函数
  2. 判断旧节点是不是VNode,如果不是,创建VNode
  3. 比较新旧节点是不是相同节点,
  4. 如果是相同节点(选择器sel和key相同), 找到并更新差异DOM
  5. 如果不同,获取当前元素,创建对应的VNode,如果父节点不为空,插入新节点,并将老元素从节点上移除,执行init/create钩子函数,

为什么使用key高效

  1. diff算法是同级比较,比较当前标签上的key还有它当前的标签名,如果key和标签名都一样时只是做了一个移动的操作,不会重新创建元素和删除元素。
  2. 没有key的时候默认使用的是“就地复用”策略。如果数据项的顺序被改变,Vue不是移动Dom元素来匹配数据项的改变,而是简单复用原来位置的每个元素。如果删除第一个元素,在进行比较时发现标签一样值不一样时,就会复用之前的位置,将新值直接放到该位置,以此类推,最后多出一个就会把最后一个删除掉。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值