原标题:snabbdom 源码阅读分析

DOM“天生就慢”,所在此在此以前端各大框架都提供了对DOM操作举行优化的办法,Angular中的是脏值检查,React首先提议了Virtual
Dom,Vue二.0也进入了Virtual Dom,与React类似。

DOM“天生就慢”,所以前端各大框架都提供了对DOM操作进行优化的格局,Angular中的是脏值检查,React首先提议了Virtual
Dom,Vue二.0也到场了Virtual Dom,与React类似。

率先知道VNode对象

2个VNode的实例对象涵盖了以下属性,参见源码src/vdom/vnode.js

constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }

内部多少个比较根本的属性:

  • tag: 当前节点的标签名
  • data:
    当前节点的数额对象,具体包含哪些字段能够参见vue源码types/vnode.d.ts中对VNodeData的定义
  • children: 数组类型,包含了现阶段节点的子节点
  • text: 当前节点的文件,1般文本节点或注释节点会有该属性
  • elm: 当前虚拟节点对应的实事求是的dom节点
  • key: 节点的key属性,用于作为节点的标识,有利于patch的优化

譬如说,定义多个vnode,它的数据结构是:

    {
        tag: 'div'
        data: {
            id: 'app',
            class: 'page-box'
        },
        children: [
            {
                tag: 'p',
                text: 'this is demo'
            }
        ]
    }

由此自然的渲染函数,最终渲染出的实际的dom结构就是:

   <div id="app" class="page-box">
       <p>this is demo</p>
   </div>

VNode对象是JS用对象模拟的DOM节点,通过渲染这几个指标即可渲染成1棵dom树。

随着 React Vue 等框架的盛行,Virtual DOM 也更是火,snabbdom
是里面壹种实现,而且 Vue 2.x 版本的 Virtual DOM 部分也是基于 snabbdom
进行修改的。snabbdom 这几个库核心代码唯有 200 多行,极度适合想要深切摸底
Virtual DOM 实现的读者阅读。倘若您没听别人讲过
snabbdom,能够先看看官方文书档案。

正文将对此Vue 2.五.叁本子中选拔的Virtual Dom进行分析。

本文将对于Vue 二.伍.3本子中选取的Virtual Dom举办解析。

patch

自笔者对patch的精通正是对剧情已经转移的节点开展改动的经过

当model中的响应式的数额发生了变通,这么些响应式的多少所保障的dep数组便会调用dep.notify()方法成功有着注重遍历执行的干活,那当中就包涵了视图的立异即updateComponent方法。

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

成功视图的立异工作实际正是调用了vm._update方法,这些法子接收的率先个参数是刚生成的Vnode(vm._render()会转移三个新的Vnode)
vm._update方法主要调用了vm._patch_()
方法,那也是一体virtaul-dom个中最为基本的主意,首要形成了prevVnode和vnode的diff进度并依据必要操作的vdom节点打patch,最毕生成新的实在dom节点并成功视图的立异工作。

   function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
        // 当oldVnode不存在时
        if (isUndef(oldVnode)) {
            // 创建新的节点
            createElm(vnode, insertedVnodeQueue, parentElm, refElm)
        } else {
            const isRealElement = isDef(oldVnode.nodeType)
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // patch existing root node
            patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } 
        }
    }

在当oldVnode不设有的时候,今年是root节点开始化的进程,因此调用了createElm(vnode,
insertedVnodeQueue, parentElm,
refElm)方法去创立一个新的节点。而当oldVnode是vnode且sameVnode(oldVnode,
vnode)一个节点的中坚品质相同,那么就进去了3个节点的patch以及diff进度。
(在对oldVnode和vnode类型判断中有个sameVnode方法,那个艺术决定了是不是要求对oldVnode和vnode实行diff及patch的进度。若是二个vnode的骨干品质存在差异的情形,那么就会平素跳过diff的历程,进而依据vnode新建二个实事求是的dom,同时删除老的dom节点)

function sameVnode (a, b) {
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  )
}

patch进程主要调用了patchVnode(src/core/vdom/patch.js)方法开始展览的:

if (isDef(data) && isPatchable(vnode)) {
      // cbs保存了hooks钩子函数: 'create', 'activate', 'update', 'remove', 'destroy'
      // 取出cbs保存的update钩子函数,依次调用,更新attrs/style/class/events/directives/refs等属性
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }

履新真实dom节点的data属性,也正是对dom节点举办了预处理的操作
接下来:

    ...
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children
    const ch = vnode.children
    // 如果vnode没有文本节点
    if (isUndef(vnode.text)) {
      // 如果oldVnode的children属性存在且vnode的属性也存在
      if (isDef(oldCh) && isDef(ch)) {
        // updateChildren,对子节点进行diff
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        // 如果oldVnode的text存在,那么首先清空text的内容
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // 然后将vnode的children添加进去
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        // 删除elm下的oldchildren
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        // oldVnode有子节点,而vnode没有,那么就清空这个节点
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      // 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
      nodeOps.setTextContent(elm, vnode.text)
    }

其一patch的进度又分为三种情形:
1.当vnode的text为空,即不是文件节点时。

  • 如果oldVnode和新节点vnode都有子节点。
    则调用updateChildren( ),对子节点开始展览diff
  • 借使只有新节点vnode有子节点
    则判断oldVnode是或不是是文本节点,就算是文件节点,则率先清空真实节点的text的剧情。然后把新节点的children添加到elm中。
  • 只要唯有oldVnode有子节点时
    则调用removeVnodes()删除elm下的oldVnode的children。
  • 如果oldVnode和新节点vnode都不曾子舆节点,且oldVnode是文本节点
    则清空真实节点的text的始末。

2.当vnode的text存在,便是文本节点时
则设置真实节点的text内容为vnode的text内容。

缘何选拔 snabbdom

updataChildren是Diff算法的为主,所以本文对updataChildren实行了图像和文字的辨析。

updataChildren是Diff算法的主干,所以本文对updataChildren实行了图像和文字的分析。

diff过程

自家对diff的知道正是遍历两棵差别的虚拟树,如若中间一部分节点分裂,则展开patch。

上个函数的updateChildren(src/core/vdom/patch.js)方法正是diff进度,它也是整套diff经过中最珍视的环节:

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    // 为oldCh和newCh分别建立索引,为之后遍历的依据
    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, idxInOld, elmToMove, refElm

    // 直到oldCh或者newCh被遍历完后跳出循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } 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)
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 插入到老的开始节点的前面
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        // 如果以上条件都不满足,那么这个时候开始比较key值,首先建立key和index索引的对应关系
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : null
        // 如果idxInOld不存在
        // 1. newStartVnode上存在这个key,但是oldKeyToIdx中不存在
        // 2. newStartVnode上并没有设置key属性
        if (isUndef(idxInOld)) { // New element
          // 创建新的dom节点
          // 插入到oldStartVnode.elm前面
          // 参见createElm方法
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          newStartVnode = newCh[++newStartIdx]
        } else {
          elmToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !elmToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )

          // 将找到的key一致的oldVnode再和newStartVnode进行diff
          if (sameVnode(elmToMove, newStartVnode)) {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
            oldCh[idxInOld] = undefined
            // 移动node节点
            canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          } else {
            // same key but different element. treat as new element
            // 创建新的dom节点
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx]
          }
        }
      }
    }
    // 如果最后遍历的oldStartIdx大于oldEndIdx的话
    if (oldStartIdx > oldEndIdx) {        // 如果是老的vdom先被遍历完
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      // 添加newVnode中剩余的节点到parentElm中
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) { // 如果是新的vdom先被遍历完,则删除oldVnode里面所有的节点
      // 删除剩余的节点
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

代码中,oldStartIdx,oldEndIdx是遍历oldCh(oldVnode的子节点)的索引
newStartIdx,newEndIdx是遍历newCh(vnode的子节点)的索引

  • 骨干代码唯有 200 行,丰硕的测试用例
  • 无敌的插件系统、hook 系统
  • vue 使用了 snabbdom,读懂 snabbdom 对通晓 vue 的落到实处有帮忙

1.VNode对象


二个VNode的实例包涵了以下属性,那有的代码在src/core/vdom/vnode.js里

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  functionalContext: Component | void; // real context vm for functional nodes
  functionalOptions: ?ComponentOptions; // for SSR caching
  functionalScopeId: ?string; // functioanl scope id support
  • tag: 当前节点的标签名
  • data:
    当前节点的多少对象,具体包括如何字段可以参照vue源码types/vnode.d.ts中对VNodeData的概念
  • children: 数组类型,包涵了眼下节点的子节点
  • text: 当前节点的文本,一般文本节点或注释节点会有该属性
  • elm: 当前虚拟节点对应的真正的dom节点
  • ns: 节点的namespace
  • context: 编写翻译作用域
  • functionalContext: 函数化组件的功能域
  • key: 节点的key属性,用于作为节点的标识,有利于patch的优化
  • componentOptions: 成立组件实例时会用到的选项新闻
  • child: 当前节点对应的零件实例
  • parent: 组件的占位节点
  • raw: raw html
  • isStatic: 静态节点的标识
  • isRootInsert: 是还是不是作为根节点插入,被
  • isComment: 当前节点是或不是是注释节点
  • isCloned: 当前节点是还是不是为克隆节点
  • isOnce: 当前节点是不是有v-once指令

1.VNode对象


三个VNode的实例包蕴了以下属性,那某些代码在src/core/vdom/vnode.js里

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  functionalContext: Component | void; // real context vm for functional nodes
  functionalOptions: ?ComponentOptions; // for SSR caching
  functionalScopeId: ?string; // functioanl scope id support
  • tag: 当前节点的标签名
  • data:
    当前节点的多寡对象,具体包涵哪些字段能够参考vue源码types/vnode.d.ts中对VNodeData的概念
  • children: 数组类型,包括了当前节点的子节点
  • text: 当前节点的文书,一般文本节点或注释节点会有该属性
  • elm: 当前虚拟节点对应的忠实的dom节点
  • ns: 节点的namespace
  • context: 编写翻译成效域
  • functionalContext: 函数化组件的作用域
  • key: 节点的key属性,用于作为节点的标识,有利于patch的优化
  • componentOptions: 成立组件实例时会用到的选项新闻
  • child: 当前节点对应的机件实例
  • parent: 组件的占位节点
  • raw: raw html
  • isStatic: 静态节点的标识
  • isRootInsert: 是不是作为根节点插入,被
  • isComment: 当前节点是不是是注释节点
  • isCloned: 当前节点是或不是为克隆节点
  • isOnce: 当前节点是还是不是有v-once指令

diff遍历的进度如下: (节点属性中不带key的地方)

遍历完的规则就是oldCh或许newCh的startIndex >= endIndex
率先先判断oldCh的起第6节点oldStartVnode和最终节点oldEndVnode是还是不是存在,如若不设有,则oldCh的序幕节点向后运动壹人,末尾节点向前移动一个人。

假诺存在,则每一轮diff都开始展览比较如下相比:

  1. sameVnode(oldStartVnode, newStartVnode)
    判定老节点的初节点和新节点的初节点是还是不是是同一品种,假如是,则对它们三个拓展patchVnode(patch进程).三个节点初节点分别向后移动一人。
  2. 如果1不满足,sameVnode(oldEndVnode, newEndVnode)
    看清老节点的尾节点和新节点的尾节点是不是是同一系列,假若是,则对它们七个拓展patchVnode(patch进度).七个节点尾节点分别向前移动一个人。
  3. 借使2也不满足,则sameVnode(oldStartVnode, newEndVnode)
    判定老节点的初节点和新节点的尾节点是还是不是是同1品种,假若是,则对它们三个举行patchVnode(patch进程).老节点的初节点向后运动1人,新节点尾节点向前挪动1位。
  4. 如果叁也不满意,则sameVnode(oldEndVnode, newStartVnode)
    判断老节点的尾节点和新节点的初节点是不是是同壹类型,借使是,则对它们多个开展patchVnode(patch过程).老节点的尾节点向前移动1人,新节点初节点向后移动一个人。
    5.如若之上都不满意,则创建新的dom节点,newCh的startVnode被添加到oldStartVnode的先头,同时newStartIndex后移1人;

用图来叙述正是

金沙网址 1

第一轮diff

金沙网址 2

第二轮diff

金沙网址 3

第三轮diff

金沙网址 4

第四轮diff

金沙网址 5

第五轮diff

遍历的长河甘休后,newStartIdx >
newEndIdx,表明此时oldCh存在多余的节点,那么最后就供给将oldCh的结余节点从parentElm中去除。
倘诺oldStartIdx >
oldEndIdx,表达此时newCh存在多余的节点,那么最后就需求将newCh的结余节点添加到parentElm中。

什么是 Virtual DOM

2.VNode的分类


VNode能够清楚为VueVirtual
Dom的二个基类,通过VNode构造函数生成的VNnode实例可为如下几类:

  • EmptyVNode: 未有内容的诠释节点
  • TextVNode: 文本节点
  • ElementVNode: 普通成分节点
  • ComponentVNode: 组件节点
  • CloneVNode:
    克隆节点,能够是以上任意档次的节点,唯1的分别在于isCloned属性为true

2.VNode的分类


VNode能够知晓为VueVirtual
Dom的贰个基类,通过VNode构造函数生成的VNnode实例可为如下几类:

  • EmptyVNode: 未有内容的诠释节点
  • TextVNode: 文本节点
  • ElementVNode: 普通成分节点
  • 【金沙网址】源码阅读分析,原理领会。ComponentVNode: 组件节点
  • CloneVNode:
    克隆节点,能够是以上任意档次的节点,唯1的界别在于isCloned属性为true

diff遍历的历程如下: (节点属性中带key的事态)

前四步还和地点的同一
第六步:要是前四步都不满足,则第7%立oldCh key和index索引的相应关系。

  • 假诺newStartVnode上存在那一个key,可是oldKeyToIdx中不设有
    则创制新的dom节点,newCh的startVnode被添加到oldStartVnode的先头,同时newStartIndex后移一人;
  • 若果找到与newStartVnode key一致的oldVnode
    则先将那四个节点开始展览patchVnode(patch进程),然后将newStartVnode移到oldStartVnode的眼下,并在oldCh中去除与newStartVnode
    key一致的oldVnode,然后新节点初节点向后运动一个人。再开始展览遍历。

用图来叙述便是

金沙网址 6

第一轮diff

金沙网址 7

第二轮diff

金沙网址 8

第三轮diff

金沙网址 9

第四轮diff

金沙网址 10

第五轮diff

最后,由于newStartIndex>newEndIndex,所以newCh结余的节点会被添加到parentElm中

snabbdom 是 Virtual DOM 的一种达成,所以以前,你要求先明了怎么着是
Virtual DOM。通俗的说,Virtual DOM 就是贰个 js 对象,它是屏气凝神 DOM
的望梅止渴,只保留部分得力的音信,更轻量地描述 DOM 树的布局。 比如在
snabbdom 中,是那般来定义三个 VNode 的:

三.Create-Element源码分析


那部分代码在src/core/vdom/create-element.js里,小编就直接粘代码加上自己的注明了

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode {
  // 兼容不传data的情况
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 如果alwaysNormalize是true
  // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  // 调用_createElement创建虚拟节点
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode {

  /**
   * 如果存在data.__ob__,说明data是被Observer观察的数据
   * 不能用作虚拟节点的data
   * 需要抛出警告,并返回一个空节点
   *
   * 被监控的data不能被用作vnode渲染的数据的原因是:
   * data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
   */
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // 当组件的is属性被设置为一个falsy的值
    // Vue将不会知道要把这个组件渲染成什么
    // 所以渲染一个空节点
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // key为非原始值警告
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    warn(
      'Avoid using non-primitive value as key, ' +
      'use string/number value instead.',
      context
    )
  }
  // 作用域插槽
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 根据normalizationType的值,选择不同的处理方法
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // 如果标签名是字符串类型
  if (typeof tag === 'string') {
    let Ctor
    // 获取标签的命名空间
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 如果是保留标签
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      // 就创建这样一个vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
      // 如果不是保留字标签,尝试从vm的components上查找是否有这个标签的定义
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // 如果找到,就创建虚拟组件节点
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      // 兜底方案,创建一个正常的vnode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // 当tag不是字符串的时候,我们认为tag是组件的构造类
    // 所以直接创建
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (isDef(vnode)) {
    // 应用命名空间
    if (ns) applyNS(vnode, ns)
    return vnode
  } else {
    // 返回一个空节点
    return createEmptyVNode()
  }
}

function applyNS (vnode, ns, force) {
  vnode.ns = ns
  if (vnode.tag === 'foreignObject') {
    // use default namespace inside foreignObject
    ns = undefined
    force = true
  }
  if (isDef(vnode.children)) {
    for (let i = 0, l = vnode.children.length; i < l; i++) {
      const child = vnode.children[i]
      if (isDef(child.tag) && (isUndef(child.ns) || isTrue(force))) {
        applyNS(child, ns, force)
      }
    }
  }
}

3.Create-Element源码分析


那有的代码在src/core/vdom/create-element.js里,作者就一直粘代码加上本身的笺注了

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode {
  // 兼容不传data的情况
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 如果alwaysNormalize是true
  // 那么normalizationType应该设置为常量ALWAYS_NORMALIZE的值
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  // 调用_createElement创建虚拟节点
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode {

  /**
   * 如果存在data.__ob__,说明data是被Observer观察的数据
   * 不能用作虚拟节点的data
   * 需要抛出警告,并返回一个空节点
   *
   * 被监控的data不能被用作vnode渲染的数据的原因是:
   * data在vnode渲染过程中可能会被改变,这样会触发监控,导致不符合预期的操作
   */
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // 当组件的is属性被设置为一个falsy的值
    // Vue将不会知道要把这个组件渲染成什么
    // 所以渲染一个空节点
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // key为非原始值警告
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    warn(
      'Avoid using non-primitive value as key, ' +
      'use string/number value instead.',
      context
    )
  }
  // 作用域插槽
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 根据normalizationType的值,选择不同的处理方法
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // 如果标签名是字符串类型
  if (typeof tag === 'string') {
    let Ctor
    // 获取标签的命名空间
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // 如果是保留标签
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      // 就创建这样一个vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
      // 如果不是保留字标签,尝试从vm的components上查找是否有这个标签的定义
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // 如果找到,就创建虚拟组件节点
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      // 兜底方案,创建一个正常的vnode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // 当tag不是字符串的时候,我们认为tag是组件的构造类
    // 所以直接创建
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (isDef(vnode)) {
    // 应用命名空间
    if (ns) applyNS(vnode, ns)
    return vnode
  } else {
    // 返回一个空节点
    return createEmptyVNode()
  }
}

function applyNS (vnode, ns, force) {
  vnode.ns = ns
  if (vnode.tag === 'foreignObject') {
    // use default namespace inside foreignObject
    ns = undefined
    force = true
  }
  if (isDef(vnode.children)) {
    for (let i = 0, l = vnode.children.length; i < l; i++) {
      const child = vnode.children[i]
      if (isDef(child.tag) && (isUndef(child.ns) || isTrue(force))) {
        applyNS(child, ns, force)
      }
    }
  }
}

总结

Virtual DOM 算法首要是落到实处地点八个概念:VNode,diff,patch
总计下来正是

一. 透过结构VNode创设虚拟DOM

二. 通过编造DOM营造真正的DOM

3. 生成新的虚构DOM

四. 比较两棵虚拟DOM树的不一致.从根节点开端比较,diff进度

5. 在真正的DOM成分上利用变更,patch

内部patch的进度中际遇八个节点有子节点,则对其子节点举办diff。
而diff的长河又会调用patch。

参照链接:
新浪:怎样晓得虚拟DOM?
Vue原理分析之Virtual
Dom
Vue 2.0 的 virtual-dom
实现简析

export interface VNode { sel: string | undefined; data: VNodeData |
undefined; children: Array<VNode | string> | undefined; elm: Node
| undefined; 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?: Array<any>; // for
thunks [key: string]: any; // for any other 3rd party module} 复制代码

4.Patch原理


patch函数的定义在src/core/vdom/patch.js中,patch逻辑相比简单,就不粘代码了

patch函数接收陆个参数:

  • oldVnode: 旧的虚拟节点或旧的实在dom节点
  • vnode: 新的虚构节点
  • hydrating: 是或不是要跟真是dom混合
  • removeOnly: 特殊flag,用于
  • parentElm: 父节点
  • refElm: 新节点将插入到refElm从前

4.Patch原理


patch函数的定义在src/core/vdom/patch.js中,patch逻辑相比不难,就不粘代码了

patch函数接收七个参数:

  • oldVnode: 旧的虚构节点或旧的真实dom节点
  • vnode: 新的杜撰节点
  • hydrating: 是还是不是要跟真是dom混合
  • removeOnly: 特殊flag,用于
  • parentElm: 父节点
  • refElm: 新节点将插入到refElm此前

从上面包车型客车概念大家得以看出,大家得以用 js 对象来讲述 dom
结构,那大家是否足以对八个情景下的 js
对象开始展览相比较,记录出它们的差异,然后把它使用到确实的 dom
树上呢?答案是足以的,那正是 diff 算法,算法的为主步骤如下:

patch的逻辑是:

  1. if
    vnode不存在但是oldVnode存在,表达来意是要绝迹老节点,那么就调用invokeDestroyHook(oldVnode)来进行销
  2. if
    oldVnode不设有然则vnode存在,表达来意是要创立新节点,那么就调用createElm来成立新节点
  3. else 当vnode和oldVnode都设有时

    • if oldVnode和vnode是同1个节点,就调用patchVnode来拓展patch
    • 当vnode和oldVnode不是同多个节点时,假设oldVnode是真性dom节点或hydrating设置为true,需求用hydrate函数将虚拟dom和真是dom进行映射,然后将oldVnode设置为对应的虚构dom,找到oldVnode.elm的父节点,依据vnode创设3个实事求是dom节点并插入到该父节点中oldVnode.elm的地方

patch的逻辑是:

  1. if
    vnode不设有不过oldVnode存在,表明来意是要销毁老节点,那么就调用invokeDestroyHook(oldVnode)来进展销
  2. if
    oldVnode不设有可是vnode存在,表达来意是要成立新节点,那么就调用createElm来创立新节点
  3. else 当vnode和oldVnode都设有时

    • if oldVnode和vnode是同二个节点,就调用patchVnode来进展patch
    • 当vnode和oldVnode不是同二个节点时,若是oldVnode是忠实dom节点或hydrating设置为true,必要用hydrate函数将虚拟dom和真是dom举办映射,然后将oldVnode设置为对应的虚构dom,找到oldVnode.elm的父节点,依据vnode创设1个实事求是dom节点并插入到该父节点中oldVnode.elm的义务
  • 用 js 对象来描述 dom 树结构,然后用这些 js 对象来创造1棵真正的 dom
    树,插入到文书档案中
  • 当状态更新时,将新的 js 对象和旧的 js
    对象开始展览相比,获得三个目的时期的差异
  • 将出入应用到真正的 dom 上

patchVnode的逻辑是:

  1. 设若oldVnode跟vnode完全一致,那么不必要做别的业务
  2. 假定oldVnode跟vnode都是静态节点,且全体同等的key,当vnode是仿造节点可能v-once指令控制的节点时,只要求把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其余操作
  3. 否则,假如vnode不是文本节点或注释节点

    • 只要oldVnode和vnode都有子节点,且二方的子节点不完全1致,就实施updateChildren
    • 设若唯有oldVnode有子节点,那就把这个节点都剔除
    • 假使唯有vnode有子节点,那就创办那一个子节点
    • 假设oldVnode和vnode都未有子节点,不过oldVnode是文件节点或注释节点,就把vnode.elm的文书设置为空字符串
  4. 一旦vnode是文本节点或注释节点,不过vnode.text !=
    oldVnode.text时,只需求更新vnode.elm的公文内容就足以

代码如下:

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 如果新旧节点一致,什么都不做
    if (oldVnode === vnode) {
      return
    }

    // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
    const elm = vnode.elm = oldVnode.elm

    // 异步占位符
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    // 如果新旧都是静态节点,并且具有相同的key
    // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
    // 也不用再有其他操作
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 如果vnode不是文本节点或者注释节点
    if (isUndef(vnode.text)) {
      // 并且都有子节点
      if (isDef(oldCh) && isDef(ch)) {
        // 并且子节点不完全一致,则调用updateChildren
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

        // 如果只有新的vnode有子节点
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // elm已经引用了老的dom节点,在老的dom节点上添加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

        // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)

        // 如果老节点是文本节点
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }

      // 如果新vnode和老vnode是文本节点或注释节点
      // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

patchVnode的逻辑是:

  1. 就算oldVnode跟vnode完全一致,那么不须要做其余业务
  2. 即使oldVnode跟vnode都以静态节点,且持有同等的key,当vnode是仿造节点只怕v-once指令控制的节点时,只需求把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有别的操作
  3. 不然,假诺vnode不是文本节点或注释节点

    • 假设oldVnode和vnode都有子节点,且2方的子节点不完全一致,就执行updateChildren
    • 若是唯有oldVnode有子节点,那就把这一个节点都剔除
    • 假定只有vnode有子节点,那就创办这个子节点
    • 假如oldVnode和vnode都未曾子舆节点,然而oldVnode是文本节点或注释节点,就把vnode.elm的文件设置为空字符串
  4. 一经vnode是文本节点或注释节点,不过vnode.text !=
    oldVnode.text时,只须要更新vnode.elm的文本内容就足以

代码如下:

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    // 如果新旧节点一致,什么都不做
    if (oldVnode === vnode) {
      return
    }

    // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
    const elm = vnode.elm = oldVnode.elm

    // 异步占位符
    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    // 如果新旧都是静态节点,并且具有相同的key
    // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
    // 也不用再有其他操作
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    // 如果vnode不是文本节点或者注释节点
    if (isUndef(vnode.text)) {
      // 并且都有子节点
      if (isDef(oldCh) && isDef(ch)) {
        // 并且子节点不完全一致,则调用updateChildren
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

        // 如果只有新的vnode有子节点
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        // elm已经引用了老的dom节点,在老的dom节点上添加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

        // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)

        // 如果老节点是文本节点
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }

      // 如果新vnode和老vnode是文本节点或注释节点
      // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

接下去大家来分析那1体经过的落到实处。

5.updataChildren原理


5.updataChildren原理


源码分析

updateChildren的逻辑是:

  1. 独家获取oldVnode和vnode的firstChild、lastChild,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode
  2. 若果oldStartVnode和newStartVnode是同壹节点,调用patchVnode实行patch,然后将oldStartVnode和newStartVnode都安装为下二个子节点,重复上述流程
    金沙网址 11
  3. 要是oldEndVnode和newEndVnode是同1节点,调用patchVnode实行patch,然后将oldEndVnode和newEndVnode都设置为上叁个子节点,重复上述流程
    金沙网址 12
  4. 假若oldStartVnode和newEndVnode是同壹节点,调用patchVnode举办patch,若是removeOnly是false,那么能够把oldStartVnode.elm移动到oldEndVnode.elm之后,然后把oldStartVnode设置为下二个节点,newEndVnode设置为上1个节点,重复上述流程
    金沙网址 13
  5. 假如newStartVnode和oldEndVnode是同1节点,调用patchVnode进行patch,假使removeOnly是false,那么能够把oldEndVnode.elm移动到oldStartVnode.elm从前,然后把newStartVnode设置为下三个节点,oldEndVnode设置为上1个节点,重复上述流程
    金沙网址 14
  6. 借使上述都不般配,就尝试在oldChildren中寻找跟newStartVnode具有同样key的节点,借使找不到平等key的节点,表达newStartVnode是3个新节点,就制造3个,然后把newStartVnode设置为下一个节点
  7. 假若上一步找到了跟newStartVnode相同key的节点,那么通过其它属性的相比较来判断那些节点是或不是是同一个节点,要是是,就调用patchVnode进行patch,假设removeOnly是false,就把newStartVnode.elm插入到oldStartVnode.elm在此以前,把newStartVnode设置为下贰个节点,重复上述流程
    金沙网址 15
  8. 要是在oldChildren中未有寻找到newStartVnode的同一节点,那就创建七个新节点,把newStartVnode设置为下1个节点,重复上述流程
  9. 若果oldStartVnode跟oldEndVnode重合了,并且newStartVnode跟newEndVnode也重合了,那么些轮回就得了了

现实代码如下:

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 旧头索引
    let newStartIdx = 0 // 新头索引
    let oldEndIdx = oldCh.length - 1 // 旧尾索引
    let newEndIdx = newCh.length - 1 // 新尾索引
    let oldStartVnode = oldCh[0] // oldVnode的第一个child
    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
    let newStartVnode = newCh[0] // newVnode的第一个child
    let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果oldVnode的第一个child不存在
      if (isUndef(oldStartVnode)) {
        // oldStart索引右移
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

      // 如果oldVnode的最后一个child不存在
      } else if (isUndef(oldEndVnode)) {
        // oldEnd索引左移
        oldEndVnode = oldCh[--oldEndIdx]

      // oldStartVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // patch oldStartVnode和newStartVnode, 索引左移,继续循环
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

      // oldEndVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // patch oldEndVnode和newEndVnode,索引右移,继续循环
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

      // oldStartVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // patch oldStartVnode和newEndVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // oldStart索引右移,newEnd索引左移
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]

      // 如果oldEndVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // patch oldEndVnode和newStartVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // oldEnd索引左移,newStart索引右移
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]

      // 如果都不匹配
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        // 如果未找到,说明newStartVnode是一个新的节点
        if (isUndef(idxInOld)) { // New element
          // 创建一个新Vnode
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

        // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
        } else {
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }

          // 比较两个具有相同的key的新节点是否是同一个节点
          //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // patch vnodeToMove和newStartVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 清除
            oldCh[idxInOld] = undefined
            // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
            // 移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)

          // 如果key相同,但是节点不相同,则创建一个新的节点
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          }
        }

        // 右移
        newStartVnode = newCh[++newStartIdx]
      }
    }

updateChildren的逻辑是:

  1. 分别获取oldVnode和vnode的firstChild、lastChild,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode
  2. 如果oldStartVnode和newStartVnode是同1节点,调用patchVnode进行patch,然后将oldStartVnode和newStartVnode都安装为下3个子节点,重复上述流程
    金沙网址 16
  3. 1经oldEndVnode和newEndVnode是同1节点,调用patchVnode举办patch,然后将oldEndVnode和newEndVnode都设置为上三个子节点,重复上述流程
    金沙网址 17
  4. 设若oldStartVnode和newEndVnode是同一节点,调用patchVnode进行patch,假使removeOnly是false,那么能够把oldStartVnode.elm移动到oldEndVnode.elm之后,然后把oldStartVnode设置为下三个节点,newEndVnode设置为上二个节点,重复上述流程
    金沙网址 18
  5. 比方newStartVnode和oldEndVnode是同1节点,调用patchVnode进行patch,假若removeOnly是false,那么能够把oldEndVnode.elm移动到oldStartVnode.elm从前,然后把newStartVnode设置为下1个节点,oldEndVnode设置为上2个节点,重复上述流程
    金沙网址 19
  6. 要是上述都不匹配,就尝试在oldChildren中寻觅跟newStartVnode具有同等key的节点,即使找不到均等key的节点,表达newStartVnode是一个新节点,就创办三个,然后把newStartVnode设置为下二个节点
  7. 比方上一步找到了跟newStartVnode相同key的节点,那么通过任何质量的可比来判断那1个节点是或不是是同3个节点,假若是,就调用patchVnode举行patch,若是removeOnly是false,就把newStartVnode.elm插入到oldStartVnode.elm在此以前,把newStartVnode设置为下1个节点,重复上述流程
    金沙网址 20
  8. 假如在oldChildren中一向不检索到newStartVnode的同一节点,那就创办三个新节点,把newStartVnode设置为下一个节点,重复上述流程
  9. 固然oldStartVnode跟oldEndVnode重合了,并且newStartVnode跟newEndVnode也重合了,那个轮回就结束了

切实代码如下:

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0 // 旧头索引
    let newStartIdx = 0 // 新头索引
    let oldEndIdx = oldCh.length - 1 // 旧尾索引
    let newEndIdx = newCh.length - 1 // 新尾索引
    let oldStartVnode = oldCh[0] // oldVnode的第一个child
    let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
    let newStartVnode = newCh[0] // newVnode的第一个child
    let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果oldVnode的第一个child不存在
      if (isUndef(oldStartVnode)) {
        // oldStart索引右移
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

      // 如果oldVnode的最后一个child不存在
      } else if (isUndef(oldEndVnode)) {
        // oldEnd索引左移
        oldEndVnode = oldCh[--oldEndIdx]

      // oldStartVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        // patch oldStartVnode和newStartVnode, 索引左移,继续循环
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]

      // oldEndVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // patch oldEndVnode和newEndVnode,索引右移,继续循环
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]

      // oldStartVnode和newEndVnode是同一个节点
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        // patch oldStartVnode和newEndVnode
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // oldStart索引右移,newEnd索引左移
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]

      // 如果oldEndVnode和newStartVnode是同一个节点
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        // patch oldEndVnode和newStartVnode
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
        // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        // oldEnd索引左移,newStart索引右移
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]

      // 如果都不匹配
      } else {
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

        // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

        // 如果未找到,说明newStartVnode是一个新的节点
        if (isUndef(idxInOld)) { // New element
          // 创建一个新Vnode
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

        // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
        } else {
          vnodeToMove = oldCh[idxInOld]
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
            warn(
              'It seems there are duplicate keys that is causing an update error. ' +
              'Make sure each v-for item has a unique key.'
            )
          }

          // 比较两个具有相同的key的新节点是否是同一个节点
          //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
          if (sameVnode(vnodeToMove, newStartVnode)) {
            // patch vnodeToMove和newStartVnode
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
            // 清除
            oldCh[idxInOld] = undefined
            // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
            // 移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)

          // 如果key相同,但是节点不相同,则创建一个新的节点
          } else {
            // same key but different element. treat as new element
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
          }
        }

        // 右移
        newStartVnode = newCh[++newStartIdx]
      }
    }

率先从1个简单的事例入手,一步一步分析任何代码的执行进度,上面是官方的1个简短示例:

6.具体的Diff分析


不设key,newCh和oldCh只会实行头尾两端的相互比较,设key后,除了头尾两端的相比外,还会从用key生成的对象oldKeyToIdx中检索匹配的节点,所以为节点设置key能够更敏捷的施用dom。

diff的遍历进度中,只假使对dom实行的操作都调用api.insertBefore,api.insertBefore只是原生insertBefore的粗略封装。
相比分为二种,1种是有vnode.key的,一种是尚未的。但那二种相比较对真实dom的操作是同壹的。

对此与sameVnode(oldStartVnode,
newStartVnode)和sameVnode(oldEndVnode,newEndVnode)为true的图景,不要求对dom举办运动。

小结遍历进程,有三种dom操作:上述图中都有

  1. 当oldStartVnode,newEndVnode值得相比,表明oldStartVnode.el跑到oldEndVnode.el的后面了。
  2. 当oldEndVnode,newStartVnode值得比较,oldEndVnode.el跑到了oldStartVnode.el的前边,准确的说应该是oldEndVnode.el需求活动到oldStartVnode.el的前头”。
  3. newCh中的节点oldCh里未有, 将新节点插入到oldStartVnode.el的近期

在竣事作时间,分为三种意况:

  1. oldStartIdx >
    oldEndIdx,能够认为oldCh先遍历完。当然也有望newCh此时也恰恰达成了遍历,统一都归为此类。此时newStartIdx和newEndIdx之间的vnode是增创的,调用addVnodes,把他们全数插进before的前面,before很多时候是为null的。addVnodes调用的是insertBefore操作dom节点,大家看看insertBefore的文书档案:parentElement.insertBefore(newElement,
    referenceElement)
    设若referenceElement为null则newElement将被插入到子节点的最终。假若newElement已经在DOM树中,newElement首先会从DOM树中移除。所以before为null,newElement将被插入到子节点的终极。
  2. newStartIdx >
    newEndIdx,能够认为newCh先遍历完。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里早就不设有了,调用removeVnodes将它们从dom里删除

6.具体的Diff分析


不设key,newCh和oldCh只会进展头尾两端的互相比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中摸索匹配的节点,所以为节点设置key能够更敏捷的施用dom。

diff的遍历进度中,只假诺对dom进行的操作都调用api.insertBefore,api.insertBefore只是原生insertBefore的简练封装。
相比较分为二种,一种是有vnode.key的,1种是尚未的。但那三种比较对实事求是dom的操作是平等的。

对此与sameVnode(oldStartVnode,
newStartVnode)和sameVnode(oldEndVnode,newEndVnode)为true的意况,不供给对dom实行活动。

计算遍历进程,有三种dom操作:上述图中都有

  1. 当oldStartVnode,newEndVnode值得比较,表达oldStartVnode.el跑到oldEndVnode.el的前面了。
  2. 当oldEndVnode,newStartVnode值得相比较,oldEndVnode.el跑到了oldStartVnode.el的日前,准确的说应该是oldEndVnode.el要求活动到oldStartVnode.el的日前”。
  3. newCh中的节点oldCh里未有, 将新节点插入到oldStartVnode.el的前方

在收尾时,分为三种景况:

  1. oldStartIdx >
    oldEndIdx,可以认为oldCh先遍历完。当然也有望newCh此时也恰恰完结了遍历,统一都归为此类。此时newStartIdx和newEndIdx之间的vnode是新增的,调用addVnodes,把她们一切插进before的后边,before很多时候是为null的。addVnodes调用的是insertBefore操作dom节点,大家看看insertBefore的文书档案:parentElement.insertBefore(newElement,
    referenceElement)
    固然referenceElement为null则newElement将被插入到子节点的最后。要是newElement已经在DOM树中,newElement首先会从DOM树中移除。所以before为null,newElement将被插入到子节点的末段。
  2. newStartIdx >
    newEndIdx,能够认为newCh先遍历完。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除

varsnabbdom = require( ‘snabbdom’); varpatch = snabbdom.init([ // Init
patch function with chosen modulesrequire(
‘snabbdom/modules/class’).default, // makes it easy to toggle
classesrequire( ‘snabbdom/modules/props’).default, // for setting
properties on DOM elementsrequire( ‘snabbdom/modules/style’).default, //
handles styling on elements with support for animationsrequire(
‘snabbdom/modules/eventlisteners’).default // attaches event
listeners]); varh = require( ‘snabbdom/h’).default; // helper function
for creating vnodesvarcontainer = document.getElementById( ‘container’);
varvnode = h( ‘div#container.two.classes’, { on: { click: someFn } },
[ h( ‘span’, { style: { fontWeight: ‘bold’} }, ‘This is bold’), ‘ and
this is just normal text’, h( ‘a’, { props: { href: ‘/foo’} }, “I’ll
take you places!”)]); // Patch into empty DOM element – this modifies
the DOM as a side effectpatch(container, vnode); varnewVnode = h(
‘div#container.two.classes’, { on: { click: anotherEventHandler } }, [
h( ‘span’, { style: { fontWeight: ‘normal’, fontStyle: ‘italic’} },
‘This is now italic type’), ‘ and this is still just normal text’, h(
‘a’, { props: { href: ‘/bar’} }, “I’ll take you places!”)]); // Second
`patch` invocationpatch(vnode, newVnode); // Snabbdom efficiently
updates the old view to the new state复制代码

先是 snabbdom 模块提供三个 init 方法,它接受叁个数组,数组中是各个module,那样的宏图使得那些库更具增添性,我们也能够达成团结的
module,而且能够依据本身的急需引入相应的 module,比如即便不需求写入
class,那您能够一贯把 class 的模块移除。 调用 init 方法会重临3个 patch
函数,这一个函数接受八个参数,第三个是旧的 vnode 节点恐怕 dom
节点,第二个参数是新的 vnode 节点,调用 patch 函数会对 dom
实行翻新。vnode
能够经过应用h函数来变化。使用起来杰出不难,那也是本文接下去要分析的剧情。

init 函数 exportinterfaceModule { pre: PreHook; create: CreateHook;
update: UpdateHook; destroy: DestroyHook; remove: RemoveHook; post:
PostHook;} exportfunctioninit(modules:
Array<Partial<Module>>, domApi?: DOMAPI) { // cbs 用于采集
module 中的 hookleti: number, j: number, cbs = {} asModuleHooks;
constapi: DOMAPI = domApi !== undefined? domApi : htmlDomApi; // 收集
module 中的 hookfor(i = 0; i < hooks.length; ++i) { cbs[hooks[i]]
= []; for(j = 0; j < modules.length; ++j) { consthook =
modules[j][hooks[i]]; if(hook !== undefined) { (cbs[hooks[i]]
asArray< any>).push(hook); } } } functionemptyNodeAt(elm: Element)
{ // …} functioncreate君越mCb(childElm: Node, listeners: number) { //
…} // 创制真正的 dom 节点functioncreateElm(vnode: VNode,
insertedVnodeQueue: VNodeQueue): Node{ // …}
functionaddVnodes(parentElm: Node, before: Node | null, vnodes:
Array<VNode>, startIdx: number, endIdx: number,
insertedVnodeQueue: VNodeQueue ) { // …} // 调用 destory hook//
假使存在 children 递归调用functioninvokeDestroyHook(vnode: VNode) { //
…} functionremoveVnodes(parentElm: Node, vnodes: Array<VNode>,
startIdx: number, endIdx: number): void{ // …}
functionupdateChildren(parentElm: Node, oldCh: Array<VNode>,
newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) { // …}
functionpatchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:
VNodeQueue) { // …} returnfunctionpatch(oldVnode: VNode | Element,
vnode: VNode): VNode{ // …};} 复制代码

地点是 init
方法的有的源码,为了阅读方便,权且先把一些措施的实际完成给注释掉,等实用到的时候再具体分析。
通过参数能够知道,这里有接受五个 modules 数组,其余有3个可选的参数
domApi,假诺没传递会采纳浏览器卯月 dom 相关的
api,具体能够看那里,那样的宏图也很有便宜,它能够让用户自定义平台相关的
api,比如能够看看weex 的连带兑现 。首先那里会对 module 中的 hook
举行采访,保存到 cbs
中。然后定义了种种函数,这里能够先不管,接着正是重临三个 patch
函数了,那里也先不分析它的切实逻辑。那样 init 就寿终正寝了。

h 函数

听大人讲例子的流水生产线,接下去看看h方法的实现

exportfunctionh(sel: string): VNode; exportfunctionh(sel: string, data:
VNodeData): VNode; exportfunctionh(sel: string, children:
VNodeChildren): VNode; exportfunctionh(sel: string, data: VNodeData,
children: VNodeChildren): VNode; exportfunctionh(sel: any, b?: any, c?:
any): VNode{ vardata: VNodeData = {}, children: any, text: any, i:
number; // 参数格式化if(c !== undefined) { data = b; if(is.array(c)) {
children = c; } elseif(is.primitive(c)) { text = c; } elseif(c && c.sel)
{ children = [c]; } } elseif(b !== undefined) { if(is.array(b)) {
children = b; } elseif(is.primitive(b)) { text = b; } elseif(b && b.sel)
{ children = [b]; } else{ data = b; } } // 借使存在 children,将不是
vnode 的项转成 vnodeif(children !== undefined) { for(i = 0; i <
children.length; ++i) { if(is.primitive(children[i])) children[i] =
vnode( undefined, undefined, undefined, children[i], undefined); } }
// svg 成分添加 namespaceif(sel[ 0] === ‘s’&& sel[ 1] === ‘v’&&
sel[ 2] === ‘g’&& (sel.length === 3|| sel[ 3] === ‘.’|| sel[ 3]
=== ‘#’)) { addNS(data, children, sel); } // 返回 vnodereturnvnode(sel,
data, children, text, undefined);} functionaddNS(data: any, children:
VNodes | undefined, sel: string| undefined): void{ data.ns =
”; if(sel !== ‘foreignObject’&& children !==
undefined) { for( leti = 0; i < children.length; ++i) { letchildData
= children[i].data; if(childData !== undefined) { addNS(childData,
(children[i] asVNode).children asVNodes, children[i].sel); } } }}
exportfunctionvnode(sel: string| undefined, data: any| undefined,
children: Array<VNode | string> | undefined, text: string|
undefined, elm: Element | Text | undefined): VNode{ letkey = data ===
undefined? undefined: data.key; return{ sel: sel, data: data, children:
children, text: text, elm: elm, key: key };} 复制代码

因为 h
函数后多少个参数是可选的,而且有种种传递格局,所以那边首先会对参数进行格式化,然后对
children 属性做拍卖,将或然不是 vnode 的项转成 vnode,假设是 svg
成分,会做三个非同小可处理,最后回来3个 vnode 对象。

patch 函数

patch 函数是 snabbdom 的着力,调用 init 会重临这一个函数,用来做 dom
相关的立异,接下去看看它的实际完毕。

functionpatch(oldVnode: VNode | Element, vnode: VNode): VNode{ leti:
number, elm: Node, parent: Node; constinsertedVnodeQueue: VNodeQueue =
[]; // 调用 module 中的 pre hookfor(i = 0; i < cbs.pre.length; ++i)
cbs.pre[i](); // 若是传入的是 Element 转成空的
vnodeif(!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); } //
sameVnode 时 (sel 和 key相同) 调用 patchVnodeif(sameVnode(oldVnode,
vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else{ elm =
oldVnode.elm asNode; parent = api.parentNode(elm); // 创造新的 dom 节点
vnode.elmcreateElm(vnode, insertedVnodeQueue); if(parent 金沙网址,!== null) { //
插入 domapi.insertBefore(parent, vnode.elm asNode,
api.nextSibling(elm)); // 移除旧 domremoveVnodes(parent, [oldVnode],
0, 0); } } // 调用成分上的 insert hook,注意 insert hook 在 module
上不辅助for(i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data asVNodeData).hook asHooks).insert
asany)(insertedVnodeQueue[i]); } // 调用 module post hookfor(i = 0; i
< cbs.post.length; ++i) cbs.post[i](); returnvnode;}
functionemptyNodeAt(elm: Element) { constid = elm.id ? ‘#’+ elm.id :
”; constc = elm.className ? ‘.’+ elm.className.split( ‘ ‘).join( ‘.’) :
”; returnvnode(api.tagName(elm).toLowerCase() + id + c, {}, [],
undefined, elm);} // key 和 selector 相同functionsameVnode(vnode一:
VNode, vnode二: VNode): boolean{ returnvnode一.key === vnode2.key &&
vnode一.sel === vnode2.sel;} 复制代码

第3会调用 module 的 pre
hook,你只怕会有可疑,为啥向来不调用来自各样要素的 pre
hook,那是因为成分上不帮助 pre hook,也有一对 hook 不协理在 module
中,具体能够查阅那里的文档。然后会咬定传入的第二个参数是还是不是为 vnode
类型,倘若不是,会调用 emptyNodeAt 然后将其转换来一个 vnode,emptyNodeAt
的切切实实贯彻也很简短,注意那里只是保留了 class 和 style,那些和 toVnode
的落实多少分歧,因为此处并不需求保存很多新闻,比如 prop attribute
等。接着调用 sameVnode 来判定是或不是为同1的 vnode
节点,具体达成也很不难,那里只是一口咬住不放了 key 和 sel
是还是不是一致。借使同样,调用 patchVnode,要是不等同,会调用 createElm
来创设一个新的 dom 节点,然后壹旦存在父节点,便将其插入到 dom
上,然后移除旧的 dom 节点来形成换代。最终调用成分上的 insert hook 和
module 上的 post hook。 那里的重点是 patchVnode 和 createElm
函数,大家先看 createElm 函数,看看是怎么来创制 dom 节点的。

createElm 函数 // 成立真正的 dom 节点functioncreateElm(vnode: VNode,
insertedVnodeQueue: VNodeQueue): Node{ leti: any, data = vnode.data; //
调用成分的 init hookif(data !== undefined) { if(isDef(i = data.hook) &&
isDef(i = i.init)) { i(vnode); data = vnode.data; } } letchildren =
vnode.children, sel = vnode.sel; // 注释节点if(sel === ‘!’) {
if(isUndef(vnode.text)) { vnode.text = ”; } // 创立注释节点vnode.elm =
api.createComment(vnode.text asstring); } elseif(sel !== undefined) { //
Parse selectorconsthashIdx = sel.indexOf( ‘#’); constdotIdx =
sel.indexOf( ‘.’, hashIdx); consthash = hashIdx > 0? hashIdx :
sel.length; constdot = dotIdx > 0? dotIdx : sel.length; consttag =
hashIdx !== -1|| dotIdx !== -1? sel.slice( 0, Math.min(hash, dot)) :
sel; constelm = vnode.elm = isDef(data) && isDef(i = (data
asVNodeData).ns) ? api.NS(i, tag) : api.(tag); if(hash < dot)
elm.setAttribute( ‘id’, sel.slice(hash + 1, dot)); if(dotIdx > 0)
elm.setAttribute( ‘class’, sel.slice(dot + 1).replace( /./g, ‘ ‘)); //
调用 module 中的 create hookfor(i = 0; i < cbs.create.length; ++i)
cbs.create[i](emptyNode, vnode); // 挂载子节点if(is.array(children)) {
for(i = 0; i < children.length; ++i) { constch = children[i]; if(ch
!= null) { api.(elm, createElm(ch asVNode, insertedVnodeQueue)); } } }
elseif(is.primitive(vnode.text)) { api.(elm,
api.createTextNode(vnode.text)); } i = (vnode.data asVNodeData).hook; //
Reuse variable// 调用 vnode 上的 hookif(isDef(i)) { // 调用 create
hookif(i.create) i.create(emptyNode, vnode); // insert hook 存款和储蓄起来 等
dom 插入后才会调用,那里用个数组来保存能防止调用时再一次对 vnode
树做遍历if(i.insert) insertedVnodeQueue.push(vnode); } } else{ //
文本节点vnode.elm = api.createTextNode(vnode.text asstring); }
returnvnode.elm;} 复制代码

那边的逻辑也很清楚,首先会调用成分的 init hook,接着那里会存在两种状态:

  • 比方当前成分是注释节点,会调用 createComment
    来创设三个诠释节点,然后挂载到 vnode.elm
  • 若果不存在选拔器,只是单纯的文本,调用 createTextNode
    来创建文本,然后挂载到 vnode.elm
  • 假定期存款在选用器,会对这么些选拔器做分析,得到 tag、id 和
    class,然后调用 或 NS 来生成节点,并挂载到 vnode.elm。接着调用
    module 上的 create hook,如若存在 children,遍历全体子节点并递归调用
    createElm 创造 dom,通过 挂载到当前的 elm 上,不设有 children 但存在
    text,便选拔 createTextNode 来成立文本。最终调用调用成分上的 create
    hook和封存存在 insert hook 的 vnode,因为 insert hook 要求等 dom
    真正挂载到 document
    上才会调用,那里用个数组来保存能够幸免真正须求调用时需求对 vnode
    树做遍历。

随之大家来看望 snabbdom 是怎么着做 vnode 的 diff 的,那有的是 Virtual DOM
的主导。

patchVnode 函数

其1函数做的政工是对传播的五个 vnode 做 diff,若是存在创新,将其报告到
dom 上。

functionpatchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:
VNodeQueue) { leti: any, hook: any; // 调用 prepatch hookif(isDef((i =
vnode.data)) && isDef((hook = i.hook)) && isDef((i = hook.prepatch))) {
i(oldVnode, vnode); } constelm = (vnode.elm = oldVnode.elm asNode);
letoldCh = oldVnode.children; letch = vnode.children; if(oldVnode ===
vnode) return; if(vnode.data !== undefined) { // 调用 module 上的 update
hookfor(i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,
vnode); i = vnode.data.hook; // 调用 vnode 上的 update hookif(isDef(i)
&& isDef((i = i.update))) i(oldVnode, vnode); } if(isUndef(vnode.text))
{ if(isDef(oldCh) && isDef(ch)) { // 新旧节点均存在
children,且不雷同时,对 children 进行 diffif(oldCh !== ch)
updateChildren(elm, oldCh asArray<VNode>, ch asArray<VNode>,
insertedVnodeQueue); } elseif(isDef(ch)) { // 旧节点不设有 children
新节点有 children// 旧节点存在 text 置空if(isDef(oldVnode.text))
api.setTextContent(elm, ”); // 参预新的 vnodeaddVnodes(elm, null, ch
asArray<VNode>, 0, (ch asArray<VNode>).length – 1,
insertedVnodeQueue); } elseif(isDef(oldCh)) { // 新节点不设有 children
旧节点存在 children 移除旧节点的 childrenremoveVnodes(elm, oldCh
asArray<VNode>, 0, (oldCh asArray<VNode>).length – 一); }
elseif(isDef(oldVnode.text)) { // 旧节点存在 text
置空api.setTextContent(elm, ”); } } elseif(oldVnode.text !==
vnode.text) { // 更新 textapi.setTextContent(elm, vnode.text asstring);
} // 调用 postpatch hookif(isDef(hook) && isDef((i = hook.postpatch))) {
i(oldVnode, vnode); }} 复制代码

第一调用 vnode 上的 prepatch hook,要是当前的多少个 vnode
完全相同,直接重回。接着调用 module 和 vnode 上的 update
hook。然后会分为以下二种处境做拍卖:

  • 均存在 children 且不一致等,调用 updateChildren
  • 新 vnode 存在 children,旧 vnode 不存在 children,固然旧 vnode 存在
    text 先清空,然后调用 addVnodes
  • 新 vnode 不存在 children,旧 vnode 存在 children,调用 removeVnodes
    移除 children
  • 均不设有 children,新 vnode 不存在 text,移除旧 vnode 的 text
  • 均存在 text,更新 text

最终调用 postpatch hook。整个经过很鲜明,大家须要关切的是 updateChildren
addVnodesremoveVnodes。

updateChildren functionupdateChildren(parentElm: Node, oldCh:
Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue:
VNodeQueue) { letoldStartIdx = 0, newStartIdx = 0; letoldEndIdx =
oldCh.length – 1; letoldStartVnode = oldCh[ 0]; letoldEndVnode =
oldCh[oldEndIdx]; letnewEndIdx = newCh.length – 1; letnewStartVnode =
newCh[ 0]; letnewEndVnode = newCh[newEndIdx]; letoldKeyToIdx: any;
letidxInOld: number; letelmToMove: VNode; letbefore: any; // 遍历 oldCh
newCh,对节点开始展览相比较和更新// 每轮相比较最多处理三个节点,算法复杂度
O(n)while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 假如展开相比较的 五个节点中存在空节点,为空的节点下标向中档推进,继续下个循环if(oldStartVnode
== null) { oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have
been moved left} elseif(oldEndVnode == null) { oldEndVnode =
oldCh[–oldEndIdx]; } elseif(newStartVnode == null) { newStartVnode =
newCh[++newStartIdx]; } elseif(newEndVnode == null) { newEndVnode =
newCh[–newEndIdx]; // 新旧起第陆节点相同,直接调用 patchVnode
进行翻新,下标向中档推进} elseif(sameVnode(oldStartVnode,
newStartVnode)) { patchVnode(oldStartVnode, newStartVnode,
insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx]; // 新旧截止节点相同,逻辑同上}
elseif(sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode,
newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[–oldEndIdx];
newEndVnode = newCh[–newEndIdx]; //
旧开首节点等于新的节点节点,表明节点向右移动了,调用 patchVnode
进行翻新} elseif(sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved
rightpatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); //
旧最先节点等于新的终结节点,表达节点向右移动了//
具体活动到哪,因为新节点处于末尾,所以添加到旧停止节点(会随着
updateChildren 左移)的前面// 注意那里必要活动
dom,因为节点右移了,而为啥是插入 oldEndVnode 的后面呢?//
能够分为多少个情景来理解:// 壹. 当循环刚开首,下标都还从未挪动,这移动到
oldEndVnode 的末端就相当于是最前面,是创造的// 二.
巡回已经实施过一部分了,因为每一回相比较停止后,下标都会向中档靠拢,而且每一遍都会处理多个节点,//
那时下标左右两边已经处理到位,能够把下标开头到结束区域当成是未曾开端循环的四个完整,//
所以插入到 oldEndVnode
前面是有理的(在当下巡回来说,也也正是是最终边,同
1)api.insertBefore(parentElm, oldStartVnode.elm asNode,
api.nextSibling(oldEndVnode.elm asNode)); oldStartVnode =
oldCh[++oldStartIdx]; newEndVnode = newCh[–newEndIdx]; //
旧的终止节点等于新的起头节点,表明节点是向左移动了,逻辑同上}
elseif(sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved
leftpatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm asNode, oldStartVnode.elm
asNode); oldEndVnode = oldCh[–oldEndIdx]; newStartVnode =
newCh[++newStartIdx]; // 如若以上 4 种处境都不般配,恐怕存在上边 二种意况// 壹. 以此节点是新创立的// 贰.
这些节点在原本的岗位是高居中间的(oldStartIdx 和 endStartIdx之间)}
else{ // 假若 oldKeyToIdx 不存在,成立 key 到 index 的映射//
而且也设有各样细微的优化,只会创立二次,并且已经成功的一对不要求映射if(oldKeyToIdx
=== undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx,
oldEndIdx); } // 拿到在 oldCh 下对应的下标idxInOld =
oldKeyToIdx[newStartVnode.key asstring]; //
假如下标不设有,表明这些节点是新成立的if(isUndef(idxInOld)) { // New
element// 插入到 oldStartVnode
的前边(对于如今轮回来说,约等于最前面)api.insertBefore(parentElm,
createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm asNode);
newStartVnode = newCh[++newStartIdx]; } else{ // 即便是曾经存在的节点
找到需求活动地方的节点elmToMove = oldCh[idxInOld]; // 尽管 key
相同了,然而 seletor 不壹致,须求调用 createElm 来成立新的 dom
节点if(elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode,
insertedVnodeQueue), oldStartVnode.elm asNode); } else{ // 不然调用
patchVnode 对旧 vnode 做革新patchVnode(elmToMove, newStartVnode,
insertedVnodeQueue); // 在 oldCh 上将当前早就处理的 vnode
置空,等下次循环到那些下标的时候一贯跳过oldCh[idxInOld] =
undefinedasany; // 插入到 oldStartVnode
的前方(对于当下循环来说,也正是最前方)api.insertBefore(parentElm,
elmToMove.elm asNode, oldStartVnode.elm asNode); } newStartVnode =
newCh[++newStartIdx]; } } } // 循环甘休后,大概会存在二种情形// 壹.
oldCh 早就整整处理到位,而 newCh
还有新的节点,要求对结余的各种项都创制新的 domif(oldStartIdx <=
oldEndIdx || newStartIdx <= newEndIdx) { if(oldStartIdx >
oldEndIdx) { before = newCh[newEndIdx + 1] == null? null:
newCh[newEndIdx + 1].elm; addVnodes(parentElm, before, newCh,
newStartIdx, newEndIdx, insertedVnodeQueue); // 二. newCh
已经整整甩卖完了,而 oldCh 还有旧的节点,要求将剩下的节点移除} else{
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }} 复制代码

全副进程简单的话,对五个数组举行比较,找到同样的局地开始展览复用,并更新。整个逻辑恐怕看起来某些懵,能够组合上面那几个例子通晓下:

  1. 只要旧节点顺序为[A, B, C, D],新节点为[B, A, C, D, E]

金沙网址 21

  1. 首先轮比较:开首终结节点两两并不等于,于是看 newStartVnode
    在旧节点中是或不是存在,最后找到了在其次个任务,调用 patchVnode
    实行翻新,将 oldCh[1] 至空,将 dom 插入到 oldStartVnode
    后面,newStartIdx 向中档移动,状态更新如下

金沙网址 22

  1. 其次轮比较:oldStartVnode 和 newStartVnode 相等,直接patchVnode,newStartIdx 和 oldStartIdx 向中档移动,状态更新如下

金沙网址 23

  1. 其三轮车相比较:oldStartVnode 为空,oldStartIdx
    向中档移动,进入下轮相比,状态更新如下

金沙网址 24

  1. 第伍轮比较:oldStartVnode 和 newStartVnode 相等,直接patchVnode,newStartIdx 和 oldStartIdx 向中档移动,状态更新如下

金沙网址 25

  1. oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和
    oldStartIdx 向中档移动,状态更新如下

金沙网址 26

  1. oldStartIdx 已经超(英文名:jīng chāo)越
    oldEndIdx,循环截至,由于是旧节点先结束循环而且还有没处理的新节点,调用
    addVnodes 处理剩下的新节点

addVnodes 和 removeVnodes 函数 functionaddVnodes(parentElm: Node,
before: Node | null, vnodes: Array<VNode>, startIdx: number,
endIdx: number, insertedVnodeQueue: VNodeQueue) { for(; startIdx <=
endIdx; ++startIdx) { constch = vnodes[startIdx]; if(ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
} }} functionremoveVnodes(parentElm: Node, vnodes: Array<VNode>,
startIdx: number, endIdx: number): void{ for(; startIdx <= endIdx;
++startIdx) { leti: any, listeners: number, rm: ()=> void, ch =
vnodes[startIdx]; if(ch != null) { if(isDef(ch.sel)) { // 调用 destory
hookinvokeDestroyHook(ch); // 总括需求调用 removecallback 的次数
唯有全体调用了才会移除 domlisteners = cbs.remove.length + 一; rm =
createHighlandermCb(ch.elm asNode, listeners); // 调用 module 中是 remove hook
for(i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); //
调用 vnode 的 remove hook if(isDef(i = ch.data) && isDef(i = i.hook) &&
isDef(i = i.remove)) { i(ch, rm); } else{ rm(); } } else{ // Text
nodeapi.removeChild(parentElm, ch.elm asNode); } } }} // 调用 destory
hook // 假如存在 children 递归调用 functioninvokeDestroyHook(vnode:
VNode) { leti: any, j: number, data = vnode.data; if(data !== undefined)
{ if(isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode); for(i = 0;
i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
if(vnode.children !== undefined) { for(j = 0; j <
vnode.children.length; ++j) { i = vnode.children[j]; if(i != null&&
typeofi !== “string”) { invokeDestroyHook(i); } } } }} // 只有当有着的
remove hook 都调用了 remove callback 才会移除 dom
functioncreate科雷傲mCb(childElm: Node, listeners: number) { return
functionrmCb() { if(–listeners === 0) { constparent =
api.parentNode(childElm); api.removeChild(parent, childElm); } };} 复制代码

那七个函数首要用来添加 vnode 和移除 vnode,代码逻辑基本都能看懂。

thunk 函数

1般我们的利用是基于 js 状态来更新的,比如上面这么些事例

functionrenderNumber(num) { returnh( ‘span’, num);} 复制代码

此间代表1旦 num 未有更改的话,那对 vnode 举行 patch 正是未有意义的,
对于那种景观,snabbdom 提供了1种优化手段,也正是thunk,该函数同样再次来到一个 vnode 节点,不过在 patchVnode
起头时,会对参数实行3次相比较,借使一致,将身故相比较,那几个有点类似于 React
的 pureComponent,pureComponent 的完结上会做1次浅相比 shadowEqual,结合
immutable 数据开始展览利用成效更为。下边包车型大巴事例能够成为那样。

functionrenderNumber(num) { returnh( ‘span’, num);} functionrender(num)
{ returnthunk( ‘div’, renderNumber, [num]);} varvnode =
patch(container, render( 一)) // 由于num 相同,renderNumber
不会进行patch(vnode, render( 一)) 复制代码

它的求实完结如下:

exportinterface ThunkFn { (sel: string, fn: Function, args:
Array<any>): Thunk; (sel: string, key: any, fn: Function, args:
Array<any>): Thunk;} // 使用 h 函数重回 vnode,为其添加 init 和
prepatch 钩子exportconstthunk = functionthunk(sel: string, key?: any,
fn?: any, args?: any): VNode{ if(args === undefined) { args = fn; fn =
key; key = undefined; } returnh(sel, { key: key, hook: { init: init,
prepatch: prepatch}, fn: fn, args: args });} asThunkFn; // 将 vnode
上的数据拷贝到 thunk 上,在 patchVnode 中会议及展览开判断,即使相同会终结
patchVnode// 并将 thunk 的 fn 和 args 属性保存到 vnode 上,在 prepatch
时供给开始展览比较functioncopyToThunk(vnode: VNode, thunk: VNode): void{
thunk.elm = vnode.elm; (vnode.data asVNodeData).fn = (thunk.data
asVNodeData).fn; (vnode.data asVNodeData).args = (thunk.data
asVNodeData).args; thunk.data = vnode.data; thunk.children =
vnode.children; thunk.text = vnode.text; thunk.elm = vnode.elm;}
functioninit(thunk: VNode): void{ constcur = thunk.data asVNodeData;
constvnode = (cur.fn asany).apply( undefined, cur.args);
copyToThunk(vnode, thunk);} functionprepatch(oldVnode: VNode, thunk:
VNode): void{ leti: number, old = oldVnode.data asVNodeData, cur =
thunk.data asVNodeData; constoldArgs = old.args, args = cur.args;
if(old.fn !== cur.fn || (oldArgs asany).length !== (args asany).length)
{ // 如若 fn 分化或 args 长度区别,表达发生了变化,调用 fn 生成新的
vnode 并重回copyToThunk((cur.fn asany).apply( undefined, args), thunk);
return; } for(i = 0; i < (args asany).length; ++i) { if((oldArgs
asany)[i] !== (args asany)[i]) { //
假若每一个参数发生变化,逻辑同上copyToThunk((cur.fn asany).apply(
undefined, args), thunk); return; } } copyToThunk(oldVnode, thunk);}
复制代码

能够回顾下 patchVnode 的兑现,在 prepatch 后,会对 vnode
的数目做相比,比如当 children 相同、text 相同都会终结 patchVnode。

结语

到此处 snabbdom 的中坚源码已经阅读完毕,剩下的还有一部分放置的
module,有趣味的能够自动阅读。重回乐乎,查看更多

小编:

相关文章

网站地图xml地图