澳门威尼斯赌场


化学家于今无法解释,那扇门可以通往太空

苹果其实想说不,苹果能够因而将成品营造工作转移到United States来防止关税

源码阅读分析,原理理解

原标题:snabbdom 源码阅读分析

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

DOM“天生就慢”,所此前端各大框架都提供了对DOM操作举办优化的不贰秘诀,Angular中的是脏值检查,React首先提议了Virtual
Dom,Vue贰.0也插足了Virtual Dom,与React类似。

首先知道VNode对象

二个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节点,通过渲染这个指标即可渲染成一棵dom树。

乘机 React Vue 等框架的风行,Virtual DOM 也进一步火,snabbdom
是里面壹种达成,而且 Vue 2.x 版本的 Virtual DOM 部分也是遵照 snabbdom
实行修改的。snabbdom 那一个库主旨代码唯有 200 多行,卓殊适合想要深切通晓Virtual DOM 完毕的读者阅读。假如你没听大人讲过
snabbdom,能够先看看官方文书档案。

本文将对于Vue 二.5.三本子中采纳的Virtual Dom进行解析。

正文将对此Vue 二.5.叁版本中动用的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)方法去创建1个新的节点。而当oldVnode是vnode且sameVnode(oldVnode,
vnode)二个节点的着力属性相同,那么就进来了一个节点的patch以及diff进程。
(在对oldVnode和vnode类型判断中有个sameVnode方法,那一个措施决定了是或不是须求对oldVnode和vnode进行diff及patch的经过。要是3个vnode的着力属性存在不均等的情状,那么就会直接跳过diff的长河,进而根据vnode新建1个真实的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的胚胎节点oldStartVnode和末段节点oldEndVnode是否留存,假诺不设有,则oldCh的初阶节点向后移动一人,末尾节点向前移动一位。

如果存在,则每1轮diff都进展比较如下相比:

  1. sameVnode(oldStartVnode, newStartVnode)
    判定老节点的初节点和新节点的初节点是还是不是是同1品种,要是是,则对它们七个开始展览patchVnode(patch进程).多少个节点初节点分别向后活动一人。
  2. 如果1不满足,sameVnode(oldEndVnode, newEndVnode)
    判断老节点的尾节点和新节点的尾节点是否是同壹类型,假使是,则对它们七个开始展览patchVnode(patch进度).三个节点尾节点分别向前挪动一人。
  3. 假诺二也不满意,则sameVnode(oldStartVnode, newEndVnode)
    判定老节点的初节点和新节点的尾节点是或不是是同1品种,假若是,则对它们八个拓展patchVnode(patch进程).老节点的初节点向后移动一个人,新节点尾节点向前挪动壹个人。
  4. 比方叁也不满足,则sameVnode(oldEndVnode, newStartVnode)
    判断老节点的尾节点和新节点的初节点是不是是同1类型,纵然是,则对它们三个进行patchVnode(patch进度).老节点的尾节点向前移动一个人,新节点初节点向后活动一个人。
    5.假如以上都不满意,则创设新的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的一个基类,通过VNode构造函数生成的VNnode实例可为如下几类:

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

2.VNode的分类


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

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

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

前4步还和方面包车型地铁一模1样
第5步:如若前4步都不满足,则率先建立oldCh key和index索引的照应关系。

  • 只要newStartVnode上设有这一个key,可是oldKeyToIdx中不设有
    则开立异的dom节点,newCh的startVnode被添加到oldStartVnode的先头,同时newStartIndex后移一人;
  • 假如找到与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 的1种达成,所以在此之前,你须要先清楚什么样是
Virtual DOM。通俗的说,Virtual DOM 就是一个 js 对象,它是实在 DOM
的悬空,只保留部分灵光的音讯,更轻量地叙述 DOM 树的构造。 比如在
snabbdom 中,是如此来定义1个 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

3. 生成新的虚构DOM

四. 相比较两棵虚拟DOM树的不相同.从根节点开端相比较,diff进程

伍. 在真正的DOM成分上运用变更,patch

其中patch的经过中遇见七个节点有子节点,则对其子节点实行diff。
而diff的进度又会调用patch。

参照链接:
乐乎:怎么样明白虚拟DOM?
Vue原理分析之Virtual
Dom
Vue 二.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是同贰个节点,就调用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不是同一个节点时,即使oldVnode是一心一意dom节点或hydrating设置为true,必要用hydrate函数将虚拟dom和真是dom进行映射,然后将oldVnode设置为对应的杜撰dom,找到oldVnode.elm的父节点,依据vnode创制3个实际dom节点并插入到该父节点中oldVnode.elm的地方
  • 用 js 对象来讲述 dom 树结构,然后用那几个 js 对象来创立一棵真正的 dom
    树,插入到文书档案中
  • 当状态更新时,将新的 js 对象和旧的 js
    对象举行比较,获得八个目的之间的歧异
  • 将差距应用到确实的 dom 上

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. 设若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完全1致,那么不供给做别的工作
  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. 假使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都设置为下三个子节点,重复上述流程
    澳门威尼斯赌场官网 11
  3. 比方oldEndVnode和newEndVnode是同1节点,调用patchVnode举办patch,然后将oldEndVnode和newEndVnode都安装为上八个子节点,重复上述流程
    澳门威尼斯赌场官网 12
  4. 要是oldStartVnode和newEndVnode是同壹节点,调用patchVnode举办patch,要是removeOnly是false,那么可以把oldStartVnode.elm移动到oldEndVnode.elm之后,然后把oldStartVnode设置为下1个节点,newEndVnode设置为上一个节点,重复上述流程
    澳门威尼斯赌场官网 13
  5. 借使newStartVnode和oldEndVnode是同1节点,调用patchVnode进行patch,假使removeOnly是false,那么能够把oldEndVnode.elm移动到oldStartVnode.elm在此以前,然后把newStartVnode设置为下一个节点,oldEndVnode设置为上3个节点,重复上述流程
    澳门威尼斯赌场官网 14
  6. 只要上述都不相称,就尝试在oldChildren中追寻跟newStartVnode具有相同key的节点,假使找不到均等key的节点,表达newStartVnode是2个新节点,就创办3个,然后把newStartVnode设置为下一个节点
  7. 倘若上一步找到了跟newStartVnode相同key的节点,那么通过任何质量的可比来判断那二个节点是不是是同2个节点,假使是,就调用patchVnode实行patch,如果removeOnly是false,就把newStartVnode.elm插入到oldStartVnode.elm在此之前,把newStartVnode设置为下三个节点,重复上述流程
    澳门威尼斯赌场官网 15
  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]
      }
    }

updateChildren的逻辑是:

  1. 分别获取oldVnode和vnode的firstChild、lastChild,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode
  2. 只要oldStartVnode和newStartVnode是同一节点,调用patchVnode举行patch,然后将oldStartVnode和newStartVnode都设置为下3个子节点,重复上述流程
    澳门威尼斯赌场官网 16
  3. 假诺oldEndVnode和newEndVnode是同1节点,调用patchVnode进行patch,然后将oldEndVnode和newEndVnode都安装为上多个子节点,重复上述流程
    澳门威尼斯赌场官网 17
  4. 就算oldStartVnode和newEndVnode是同一节点,调用patchVnode进行patch,假若removeOnly是false,那么能够把oldStartVnode.elm移动到oldEndVnode.elm之后,然后把oldStartVnode设置为下1个节点,newEndVnode设置为上3个节点,重复上述流程
    澳门威尼斯赌场官网 18
  5. 如果newStartVnode和oldEndVnode是同1节点,调用patchVnode进行patch,假诺removeOnly是false,那么能够把oldEndVnode.elm移动到oldStartVnode.elm从前,然后把newStartVnode设置为下三个节点,oldEndVnode设置为上三个节点,重复上述流程
    澳门威尼斯赌场官网 19
  6. 假若以上都差异盟,就尝试在oldChildren中追寻跟newStartVnode具有同样key的节点,假若找不到同样key的节点,表明newStartVnode是三个新节点,就创建叁个,然后把newStartVnode设置为下一个节点
  7. 假设上一步找到了跟newStartVnode相同key的节点,那么通过其余属性的相比较来判定那三个节点是不是是同二个节点,假诺是,就调用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]
      }
    }

第2从二个粗略的例证入手,一步一步分析任何代码的执行过程,上边是法定的二个大致示例:

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的粗略封装。
相比分为二种,一种是有vnode.key的,壹种是绝非的。但这三种比较对真实dom的操作是同样的。

对此与sameVnode(oldStartVnode,
newStartVnode)和sameVnode(oldEndVnode,newEndVnode)为true的情况,不须要对dom进行活动。

总计遍历进程,有3种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,把她们1切插进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 方法会再次来到三个 patch
函数,这么些函数接受八个参数,第贰个是旧的 vnode 节点恐怕 dom
节点,第3个参数是新的 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 数组,别的有三个可选的参数
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{ returnvnode1.key === vnode2.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
来创设3个新的 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 – 一); }
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 的末尾呢?//
能够分成三个状态来精通:// 1. 当循环刚开首,下标都还未曾运动,那移动到
oldEndVnode 的前边就相当于是最前边,是情理之中的// 二.
循环往复已经推行过一局地了,因为每便相比较甘休后,下标都会向中档靠拢,而且每一遍都会处理一个节点,//
那时下标左右两边已经处理到位,可以把下标起先到截至区域当成是未有初步循环的3个完好,//
所以插入到 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 的映射//
而且也存在种种细微的优化,只会创立一回,并且一度做到的一对不要求映射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 不1致,必要调用 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 已经超(Jing Chao)出
    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 + 1; rm =
createKoleosmCb(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 函数

貌似大家的利用是基于 js 状态来更新的,比如下边那些例子

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

此地球表面示假如 num 没有改动的话,那对 vnode 实行 patch 便是从未意思的,
对于那种气象,snabbdom 提供了1种优化手段,也正是thunk,该函数同样重返3个 vnode 节点,不过在 patchVnode
起首时,会对参数进行2次相比较,借使1致,将终结比较,那么些有点类似于 React
的 pureComponent,pureComponent 的落到实处上会做3回浅比较 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,有趣味的能够自行阅读。再次回到乐乎,查看越来越多

小编:

相关文章

No Comments, Be The First!
近期评论
    功能
    网站地图xml地图