威尼斯人开户

源码阅读分析,原理领会

4 4月 , 2019  

原标题:snabbdom 源码阅读分析

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

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

第二知道VNode对象

1个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: 当前节点的文本,壹般文本节点或注释节点会有该属性
  • elm: 当前虚拟节点对应的诚实的dom节点
  • key: 节点的key属性,用于作为节点的标识,有利于patch的优化

例如,定义2个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 二.x 版本的 Virtual DOM 部分也是依据 snabbdom
举办改动的。snabbdom 那一个库核心代码只有 200 多行,十一分适合想要深刻驾驭Virtual DOM 落成的读者读书。如若你没听大人讲过
snabbdom,可以先看看官方文书档案。

正文将对于Vue 2.⑤.3版本中运用的Virtual Dom举办辨析。

正文将对此Vue 二.5.3本子中选择的Virtual Dom实行分析。

patch

本身对patch的通晓就是对剧情早已改成的节点开始展览改动的长河

当model中的响应式的数额爆发了变更,那么些响应式的多少所保障的dep数组便会调用dep.notify()方法成功有着注重遍历执行的工作,那在那之中就包涵了视图的换代即updateComponent方法。

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

成功视图的立异工作实际就是调用了vm._update方法,那一个法子接收的率先个参数是刚生成的Vnode(vm._render()会转移1个新的Vnode)
vm._update方法首要调用了vm._patch_()
方法,那也是1体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)方法去创制2个新的节点。而当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: 当前节点的文件,1般文本节点或注释节点会有该属性
  • 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的起第2节点oldStartVnode和最终节点oldEndVnode是或不是留存,假设不设有,则oldCh的序幕节点向后运动一人,末尾节点向前移动一人。

万1存在,则每一轮diff都开展相比较如下相比较:

  1. sameVnode(oldStartVnode, newStartVnode)
    看清老节点的初节点和新节点的初节点是不是是同一种类,借使是,则对它们七个拓展patchVnode(patch过程).八个节点初节点分别向后移动壹个人。
  2. 如果1不满足,sameVnode(oldEndVnode, newEndVnode)
    判定老节点的尾节点和新节点的尾节点是或不是是同1品种,要是是,则对它们四个拓展patchVnode(patch进度).多少个节点尾节点分别向前移动1位。
  3. 比方2也不满意,则sameVnode(oldStartVnode, newEndVnode)
    看清老节点的初节点和新节点的尾节点是不是是同一连串,如若是,则对它们八个拓展patchVnode(patch进度).老节点的初节点向后移动一人,新节点尾节点向前移动一人。
  4. 要是三也不满足,则sameVnode(oldEndVnode, newStartVnode)
    判定老节点的尾节点和新节点的初节点是不是是同1品种,借使是,则对它们三个进行patchVnode(patch进程).老节点的尾节点向前挪动一人,新节点初节点向后活动1人。
    伍.如若以上都不满足,则开立异的dom节点,newCh的startVnode被添加到oldStartVnode的前头,同时newStartIndex后移一个人;

用图来讲述正是

威尼斯人开户 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的3个基类,通过VNode构造函数生成的VNnode实例可为如下几类:

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

2.VNode的分类


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

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

diff遍历的长河如下: (节点属性中带key的场地)

前4步还和地点的一致
第六步:若是前四步都不满足,则率先建立oldCh key和index索引的对应关系。

  • 假定newStartVnode上存在那些key,然则oldKeyToIdx中不设有
    则开革新的dom节点,newCh的startVnode被添加到oldStartVnode的前面,同时newStartIndex后移一位;
  • 1经找到与newStartVnode key一致的oldVnode
    则先将那多少个节点实行patchVnode(patch进度),然后将newStartVnode移到oldStartVnode的前面,并在oldCh中除去与newStartVnode
    key一致的oldVnode,然后新节点初节点向后移动1人。再拓展遍历。

用图来描述正是

威尼斯人开户 6

第一轮diff

威尼斯人开户 7

第二轮diff

威尼斯人开户 8

第三轮diff

威尼斯人开户 9

第四轮diff

威尼斯人开户 10

第五轮diff

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

snabbdom 是 Virtual DOM 的一种完毕,所以以前,你必要先知道怎么是
Virtual DOM。通俗的说,Virtual DOM 正是3个 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)
      }
    }
  }
}

三.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

三. 生成新的虚拟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 叁rd 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创设叁个真真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不是同3个节点时,要是oldVnode是真心诚意dom节点或hydrating设置为true,须求用hydrate函数将虚拟dom和真是dom实行映射,然后将oldVnode设置为相应的虚构dom,找到oldVnode.elm的父节点,依据vnode成立三个实事求是dom节点并插入到该父节点中oldVnode.elm的职位
  • 用 js 对象来叙述 dom 树结构,然后用那个 js 对象来制造一棵真正的 dom
    树,插入到文书档案中
  • 当状态更新时,将新的 js 对象和旧的 js
    对象进行比较,得到三个对象之间的差别
  • 将差别应用到实在的 dom 上

patchVnode的逻辑是:

  1. 要是oldVnode跟vnode完全1致,那么不须求做任何事情
  2. 若是oldVnode跟vnode都是静态节点,且独具相同的key,当vnode是仿制节点只怕v-once指令控制的节点时,只供给把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其余操作
  3. 再不,假设vnode不是文件节点或注释节点

    • 1经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)
    }
  }

patchVnode的逻辑是:

  1. 假若oldVnode跟vnode完全一致,那么不要求做别的工作
  2. 假使oldVnode跟vnode都以静态节点,且富有同样的key,当vnode是克隆节点也许v-once指令控制的节点时,只需求把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其余操作
  3. 否则,若是vnode不是文本节点或注释节点

    • 倘诺oldVnode和vnode都有子节点,且二方的子节点不完全一致,就进行updateChildren
    • 假诺只有oldVnode有子节点,那就把这一个节点都剔除
    • 假定只有vnode有子节点,那就创办这个子节点
    • 假设oldVnode和vnode都不曾参节点,但是oldVnode是文件节点或注释节点,就把vnode.elm的公文设置为空字符串
  4. 1经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)
    }
  }

接下去我们来分析这总体经过的贯彻。

5.updataChildren原理


5.updataChildren原理


源码分析

updateChildren的逻辑是:

  1. 分别获取oldVnode和vnode的firstChild、lastChild,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode
  2. 借使oldStartVnode和newStartVnode是同一节点,调用patchVnode举行patch,然后将oldStartVnode和newStartVnode都安装为下3个子节点,重复上述流程
    威尼斯人开户 11
  3. 假诺oldEndVnode和newEndVnode是同壹节点,调用patchVnode实行patch,然后将oldEndVnode和newEndVnode都安装为上三个子节点,重复上述流程
    威尼斯人开户 12
  4. 一旦oldStartVnode和newEndVnode是同1节点,调用patchVnode举行patch,借使removeOnly是false,那么能够把oldStartVnode.elm移动到oldEndVnode.elm之后,然后把oldStartVnode设置为下1个节点,newEndVnode设置为上1个节点,重复上述流程
    威尼斯人开户 13
  5. 借使newStartVnode和oldEndVnode是同一节点,调用patchVnode进行patch,要是removeOnly是false,那么能够把oldEndVnode.elm移动到oldStartVnode.elm以前,然后把newStartVnode设置为下二个节点,oldEndVnode设置为上1个节点,重复上述流程
    威尼斯人开户 14
  6. 万壹以上都不合营,就尝试在oldChildren中找寻跟newStartVnode具有同样key的节点,假设找不到同样key的节点,表达newStartVnode是3个新节点,就创制1个,然后把newStartVnode设置为下2个节点
  7. 如若上一步找到了跟newStartVnode相同key的节点,那么通过别的属性的相比较来判定那二个节点是还是不是是同三个节点,假如是,就调用patchVnode举办patch,如若removeOnly是false,就把newStartVnode.elm插入到oldStartVnode.elm在此以前,把newStartVnode设置为下2个节点,重复上述流程
    威尼斯人开户 15
  8. 万1在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]
      }
    }

updateChildren的逻辑是:

  1. 分别获取oldVnode和vnode的firstChild、lastChild,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode
  2. 比方oldStartVnode和newStartVnode是同一节点,调用patchVnode进行patch,然后将oldStartVnode和newStartVnode都设置为下1个子节点,重复上述流程
    威尼斯人开户 16
  3. 只要oldEndVnode和newEndVnode是同一节点,调用patchVnode举办patch,然后将oldEndVnode和newEndVnode都安装为上二个子节点,重复上述流程
    威尼斯人开户 17
  4. 固然oldStartVnode和newEndVnode是同1节点,调用patchVnode实行patch,如若removeOnly是false,那么能够把oldStartVnode.elm移动到oldEndVnode.elm之后,然后把oldStartVnode设置为下二个节点,newEndVnode设置为上2个节点,重复上述流程
    威尼斯人开户 18
  5. 万一newStartVnode和oldEndVnode是同一节点,调用patchVnode实行patch,假如removeOnly是false,那么能够把oldEndVnode.elm移动到oldStartVnode.elm以前,然后把newStartVnode设置为下二个节点,oldEndVnode设置为上多少个节点,重复上述流程
    威尼斯人开户 19
  6. 设若上述都不合营,就尝试在oldChildren中寻找跟newStartVnode具有同样key的节点,如若找不到均等key的节点,表明newStartVnode是3个新节点,就创建贰个,然后把newStartVnode设置为下3个节点
  7. 万一上一步找到了跟newStartVnode相同key的节点,那么通过任何质量的比较来判断那3个节点是或不是是同多少个节点,若是是,就调用patchVnode举行patch,如若removeOnly是false,就把newStartVnode.elm插入到oldStartVnode.elm在此以前,把newStartVnode设置为下多少个节点,重复上述流程
    威尼斯人开户 20
  8. 如果在oldChildren中尚无检索到newStartVnode的同一节点,那就创办1个新节点,把newStartVnode设置为下3个节点,重复上述流程
  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个不难示例:

6.具体的Diff分析


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

diff的遍历进度中,只若是对dom实行的操作都调用api.insertBefore,api.insertBefore只是原生insertBefore的简短封装。
比较分为三种,1种是有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此时也恰好完成了遍历,统1都归为此类。此时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的粗略封装。
比较分为三种,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里删除

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 模块提供1个 init 方法,它接受二个数组,数组中是各样module,那样的筹划使得这一个库更具扩大性,大家也得以兑现本身的
module,而且能够根据自个儿的内需引入相应的 module,比如借使不需求写入
class,那你能够直接把 class 的模块移除。 调用 init 方法会再次回到八个 patch
函数,那个函数接受三个参数,第七个是旧的 vnode 节点或许 dom
节点,第2个参数是新的 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宝马X伍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 数组,别的有一个可选的参数
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
成分,会做多个分裂平常处理,最终回来二个 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 === vnode二.key &&
vnode一.sel === vnode二.sel;} 复制代码

第三会调用 module 的 pre
hook,你大概会有思疑,为啥一直不调用来自各类要素的 pre
hook,那是因为成分上不补助 pre hook,也有一对 hook 不扶助在 module
中,具体能够查阅那里的文档。然后会咬定传入的率先个参数是或不是为 vnode
类型,就算不是,会调用 emptyNodeAt 然后将其转换来叁个 vnode,emptyNodeAt
的求实达成也很简短,注意那里只是保留了 class 和 style,这一个和 toVnode
的落实多少差异,因为此处并不要求保存很多音信,比如 prop attribute
等。接着调用 sameVnode 来判定是或不是为同壹的 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 的,这1部分是 Virtual DOM
的宗旨。

patchVnode 函数

以此函数做的作业是对传播的七个 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 – 一,
insertedVnodeQueue); } elseif(isDef(oldCh)) { // 新节点不设有 children
旧节点存在 children 移除旧节点的 childrenremoveVnodes(elm, oldCh
asArray<VNode>, 0, (oldCh asArray<VNode>).length – 1); }
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 的前面就也就是是最前面,是有理的// 二.
巡回已经实施过1有个别了,因为每一次比较结束后,下标都会向中档靠拢,而且每一回都会处理多少个节点,//
那时下标左右两边已经处理到位,能够把下标开端到甘休区域当成是从未有过起首循环的2个整机,//
所以插入到 oldEndVnode
前面是意料之中的(在时下巡回来说,也相当于是最终面,同
一)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 的映射//
而且也设有各类细微的优化,只会创立三次,并且1度到位的有些不须求映射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部分开始展览复用,并更新。整个逻辑恐怕看起来有点懵,能够构成上边那些例子精晓下:

  1. 万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 =
create奥迪Q7mCb(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
functioncreateSportagemCb(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);} 复制代码

此地球表面示假若 num 未有改动的话,那对 vnode 进行 patch 便是从未意义的,
对于那种气象,snabbdom 提供了一种优化手段,也等于thunk,该函数同样重返1个 vnode 节点,可是在 patchVnode
开端时,会对参数进行三次相比,尽管壹致,将告竣相比,那一个有点类似于 React
的 pureComponent,pureComponent 的得以实现上会做三遍浅相比 shadowEqual,结合
immutable 数据进行应用成效越来越。下面的事例能够成为那样。

functionrenderNumber(num) { returnh( ‘span’, num);} functionrender(num)
{ returnthunk( ‘div’, renderNumber, [num]);} varvnode =
patch(container, render( 1)) // 由于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地图