原标题:snabbdom 源码阅读剖析

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

趁着 React Vue 等框架的流行,Virtual DOM 也更加的火,snabbdom
是中间一种完成,并且 Vue 2.x 版本的 Virtual DOM 部分也是依照 snabbdom
进行改变的。snabbdom 那个库核心代码唯有 200 多行,特别切合想要深刻摸底
Virtual DOM 实现的读者读书。若是您没据说过
snabbdom,能够先看看官方文书档案。

本文将对于Vue 2.5.3版本中动用的Virtual Dom进行分析。

缘何选拔 snabbdom

updataChildren是Diff算法的中坚,所以本文对updataChildren进行了图像和文字的剖析。

  • 着力代码唯有 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指令

什么是 Virtual DOM

2.VNode的分类


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

  • EmptyVNode: 未有内容的讲授节点
  • TextVNode: 文本节点
  • ElementVNode: 普通成分节点
  • ComponentVNode: 组件节点
  • CloneVNode:
    克隆节点,能够是以上大肆档案的次序的节点,独一的界别在于isCloned属性为true

snabbdom 是 Virtual DOM 的一种实现,所以从前,你要求先知道什么是
Virtual DOM。通俗的说,Virtual DOM 就是贰个 js 对象,它是真性 DOM
的抽象,只保留部分得力的音讯,更轻量地汇报 DOM 树的构造。 例如在
snabbdom 中,是这么来定义一个 VNode 的:

3.Create-Element源码深入分析


这一部分代码在src/core/vdom/create-element.js里,笔者就平昔粘代码加上本人的注释了

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

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

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

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

export interface VNode { sel: string | undefined; data: VNodeData |
undefined; children: Array<VNode | string> | undefined; elm: Node
| undefined; text: string | undefined; key: Key | undefined;}export
interface VNodeData { props?: Props; attrs?: Attrs; class?: Classes;
style?: VNodeStyle; dataset?: Dataset; on?: On; hero?: Hero;
attachData?: AttachData; hook?: Hooks; key?: Key; ns?: string; // for
SVGs fn?: () => VNode; // for thunks args?: Array<any>; // for
thunks [key: string]: any; // for any other 3rd party module} 复制代码

4.Patch原理


patch函数的概念在src/core/vdom/patch.js中,patch逻辑相比轻松,就不粘代码了

patch函数接收6个参数:

  • 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的岗位
  • 用 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皆有子节点,且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)
    }
  }

接下去我们来深入分析那整个进度的完毕。

5.updataChildren原理


源码剖析

updateChildren的逻辑是:

  1. 独家获取oldVnode和vnode的firstChild、lastChild,赋值给oldStartVnode、oldEndVnode、newStartVnode、newEndVnode
  2. 万一oldStartVnode和newStartVnode是同一节点,调用patchVnode进行patch,然后将oldStartVnode和newStartVnode都安装为下三个子节点,重复上述流程
    美高梅国际平台 1
  3. 假设oldEndVnode和newEndVnode是同一节点,调用patchVnode进行patch,然后将oldEndVnode和newEndVnode都设置为上叁个子节点,重复上述流程
    美高梅国际平台 2
  4. 假定oldStartVnode和newEndVnode是同一节点,调用patchVnode进行patch,假若removeOnly是false,那么能够把oldStartVnode.elm移动到oldEndVnode.elm之后,然后把oldStartVnode设置为下二个节点,newEndVnode设置为上二个节点,重复上述流程
    美高梅国际平台 3
  5. 若果newStartVnode和oldEndVnode是同一节点,调用patchVnode实行patch,假诺removeOnly是false,那么能够把oldEndVnode.elm移动到oldStartVnode.elm从前,然后把newStartVnode设置为下一个节点,oldEndVnode设置为上一个节点,重复上述流程
    美高梅国际平台 4
  6. 假使以上都不合营,就尝试在oldChildren中寻找跟newStartVnode具备一样key的节点,假若找不到同样key的节点,表明newStartVnode是三个新节点,就创立三个,然后把newStartVnode设置为下一个节点
  7. 若果上一步找到了跟newStartVnode同样key的节点,那么通过其余品质的可比来推断那2个节点是还是不是是同贰个节点,假使是,就调用patchVnode进行patch,若是removeOnly是false,就把newStartVnode.elm插入到oldStartVnode.elm在此之前,把newStartVnode设置为下贰个节点,重复上述流程
    美高梅国际平台 5
  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]
      }
    }

首先从一个轻易的事例动手,一步一步深入分析任何代码的实施进度,下边是法定的一个简易示例:

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,把她们一切插进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福特ExplorermCb(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(vnode1:
VNode, vnode2: VNode): boolean{ returnvnode1.key === vnode2.key &&
vnode1.sel === vnode2.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 的,那部分是 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 – 1,
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); }} 复制代码

发表评论

电子邮件地址不会被公开。 必填项已用*标注

相关文章