源码阅读剖判

2019-07-06 08:17栏目:奥门新萄京娱乐场
TAG:

原标题: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 节点,第二个参数是新的 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) { // ...} functioncreateRmCb(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); }} 复制代码

版权声明:本文由奥门新萄京娱乐场发布于奥门新萄京娱乐场,转载请注明出处:源码阅读剖判