阅读分析snabbdom源码

Posted 斩月也会过期

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了阅读分析snabbdom源码相关的知识,希望对你有一定的参考价值。

尤大在官宣Vue 2.0的时候这么说过:

渲染层基于一个轻量级的 Virtual DOM 实现进行了重写,该 Virtual DOM 实现 fork 自 snabbdom。新的渲染层相比 v1 带来了巨大的性能提升,也让 Vue 2.0 成为了最快速的框架之一。
那么对于想要深入了解Vue源码的人来说先深入了解一下snabbdom的实现是有必要的

什么是Virtual DOM

  • Virtual DOM使用javascript对象来描述节点,只保留一些有用的信息,可以更轻量的描述DOM树的结构
  • 将原本需要在真实DOM进行的创建节点,删除节点,添加节点等一系列复杂的DOM操作全部放到Vritual DOM中进行
    比如在我们的snabbdom中就是用这样的方式来定义一个VNODE
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

既然DOM树信息可以通过JavaScript对象来表示,反过来我们就可以用这个JavaScript对象还原构建一棵真正的DOM树,当状态变化的时候重新渲染这个 JavaScript 的对象结构,然后将两个JavScript对象进行对比,记录差异,然后把它们应用在真实的DOM树上,这便是diff算法

分析源码

从官方给的Demo入手

var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes

var container = document.getElementById('container');

var vnode = 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 effect
patch(container, vnode);

var newVnode = 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` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state

可以看到snabbdom核心模块对外暴露一个init方法,这个方法接收一个数组,数组里面的每一项都是一个module,这些module的作用是扩展snabbdom生成复杂DOM的能力,可以根据自己的需求决定是否要引入对应的module,与此同时你也可以实现自己的module,打个比方,如果你的vnode节点不需要写入listener,你可以不引入snabbdom/modules/eventlisteners,调用init方法之后会返回一个patch函数,这个函数接收两个参数,第一个参数是一个DOM节点或者一个用于表示当前视图的vnode节点,第二个参数是表示即将要更新的视图的vnode节点,patch函数会根据传递的参数不同进行对应的DOM更新操作,接下来我们可以先看一下init函数的核心实现(为了理解方便暂时隐去函数内部的实现细节):

export interface Module 
  pre: PreHook;
  create: CreateHook;
  update: UpdateHook;
  destroy: DestroyHook;
  remove: RemoveHook;
  post: PostHook;


export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) 
  // cbs 用于收集 module 中的 hook
  let i: number,
    j: number,
    cbs =  as ModuleHooks;

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;

  // 收集 module 中的 hook
  for (i = 0; i < hooks.length; ++i) 
    cbs[hooks[i]] = [];
    for (j = 0; j < modules.length; ++j) 
      const hook = modules[j][hooks[i]];
      if (hook !== undefined) 
        (cbs[hooks[i]] as Array<any>).push(hook);
      
    
  

  function emptyNodeAt(elm: Element) 
    // ...
  

  function createRmCb(childElm: Node, listeners: number) 
    // ...
  

  // 创建真正的 dom 节点
  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node 
    // ...
  

  function addVnodes(
    parentElm: Node,
    before: Node | null,
    vnodes: Array<VNode>,
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) 
    // ...
  

  // 调用 destory hook
  // 如果存在 children 递归调用
  function invokeDestroyHook(vnode: VNode) 
    // ...
  

  function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void 
    // ...
  

  function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) 
    // ...
  

  function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) 
    // ...
  

  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode 
    // ...
  ;

可以看到首先init方法会接收两个参数,一个由module组成的数组,以及一个可选参数domApi,默认是使用预定义的一些和DOM操作相关的API,接下来会通过循环遍历收集modules中的hook,存储到cbs对象中,以便在恰当的时机对DOM节点进行一些操作,举个例子,我们看一下class这个module的源码:

"use strict";
Object.defineProperty(exports, "__esModule",  value: true );
function updateClass(oldVnode, vnode) 
    var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class;
    if (!oldClass && !klass)
        return;
    if (oldClass === klass)
        return;
    oldClass = oldClass || ;
    klass = klass || ;
    for (name in oldClass) 
        if (!klass[name]) 
            elm.classList.remove(name);
        
    
    for (name in klass) 
        cur = klass[name];
        if (cur !== oldClass[name]) 
            elm.classList[cur ? 'add' : 'remove'](name);
        
    

exports.classModule =  create: updateClass, update: updateClass ;
exports.default = exports.classModule;

很明显可以看出这个module的作用是对比新旧两个vnode节点,更新classname,执行的时机分别是在调用createupdate两种hook的时候,其他module也差不多,这里不展开分析。
下面几个都是功能型函数略过不看,直接看返回的patch函数

patch函数

  // 用于dom相关的更新
  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode 
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    // 调用 module 中的 pre hook
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();

    if (!isVnode(oldVnode)) 
      oldVnode = emptyNodeAt(oldVnode);
    
    // 判断两个Vnode节点是否相同
    if (sameVnode(oldVnode, vnode)) 
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
     else 
      elm = oldVnode.elm as Node;
      parent = api.parentNode(elm);

      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) 
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      
    

    for (i = 0; i < insertedVnodeQueue.length; ++i) 
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  ;

先通过isVnode方法来判断传入的第一个参数是不是vnode,如果不是的话通过emptyNodeAt方法来将其转换成vnode,这两个方法实现都比较简单:


// isVnode
function isVnode(vnode: any): vnode is VNode 
  // 判断是否具有sel属性
  return vnode.sel !== undefined;


// emptyNodeAt
function emptyNodeAt(elm: Element) 
    const id = elm.id ? '#' + elm.id : '';
    const c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
    return vnode(api.tagName(elm).toLowerCase() + id + c, , [], undefined, elm);
  

看一眼vnode方法的实现:

export function vnode(sel: string | undefined,
                      data: any | undefined,
                      children: Array<VNode | string> | undefined,
                      text: string | undefined,
                      elm: Element | Text | undefined): VNode 
  let key = data === undefined ? undefined : data.key;
  return sel: sel, data: data, children: children,
          text: text, elm: elm, key: key;

接着往下看,当两个需要被对比的节点都转化成了vnode之后,通过sameVnode方法判断两个Vnode节点是否相同:

function sameVnode(vnode1: VNode, vnode2: VNode): boolean 
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;

这里通过判断keysel是否相同来判断是否是相同vnode节点,如果是两个相同的vnode节点,调用patchVnode方法来对比更新,如果不是,则通过createElm方法创建新的DOM节点,如果存在父节点,则通过htmdomapi里面的insertBefore方法插入新的DOM节点到子节点的末尾,然后通过removeVnodes移除旧节点完成更新,这里的关键方法是patchVnodecreateElm,简单起见先看第二个。

createElm

  // 通过vnode创建真正的dom节点
  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node 
    let i: any, data = vnode.data;
    if (data !== undefined) 
      if (isDef(i = data.hook) && isDef(i = i.init)) 
       // 调用init hook
        i(vnode);
        data = vnode.data;
      
    
    let children = vnode.children, sel = vnode.sel;
    // 如果是注释节点
    if (sel === '!') 
      if (isUndef(vnode.text)) 
        vnode.text = '';
      
      vnode.elm = api.createComment(vnode.text as string);
     else if (sel !== undefined) 
      // 解析sel
      const hashIdx = sel.indexOf('#');
      const dotIdx = sel.indexOf('.', hashIdx);
      const hash = hashIdx > 0 ? hashIdx : sel.length;
      const dot = dotIdx > 0 ? dotIdx : sel.length;
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
     // 如果有ns属性说明是svg元素
      const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
                                                                               : api.createElement(tag);
      // 设置元素id
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
      // 设置元素classname
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\\./g, ' '));
      // 调用create hook
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      // 创建子元素节点
      if (is.array(children)) 
        for (i = 0; i < children.length; ++i) 
          const ch = children[i];
          if (ch != null) 
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          
        
       else if (is.primitive(vnode.text)) 
        // 文本节点 
        api.appendChild(elm, api.createTextNode(vnode.text));
      
      i = (vnode.data as VNodeData).hook; // Reuse variable
      if (isDef(i)) 
        // 调用节点的create hook
        if (i.create) i.create(emptyNode, vnode);
       // insert hook要等dom真正挂载的时候才会调用,这里先存储到数组里面
        if (i.insert) insertedVnodeQueue.push(vnode);
      
     else 
      // 文本节点
      vnode.elm = api.createTextNode(vnode.text as string);
    
    return vnode.elm;
  

创建DOM的时候首先调用vnode节点的init hook,然后判断该vnode节点是注释节点/文本节点/带选择器节点的其中之一,至于为什么sel === '!'就说明是注释节点这里可以解释一下,snabbdom提供了tovnode方法来将一个DOM节点转换成vnode节点,大致用法如下:

var snabbdom = require('snabbdom')
var patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/style').default, // handles styling on elements with support for animations
  require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var toVNode = require('snabbdom/tovnode').default;

var newVNode = h('div', style: color: '#000', [
  h('h1', 'Headline'),
  h('p', 'A paragraph'),
]);

patch(toVNode(document.querySelector('.container')), newVNode)

这个tovnode的源码里面有这么一段:

 ...
  else if (api.isComment(node)) 
    text = api.getTextContent(node) as string;
    return vnode('!', , [], text, node as any);
   else 
    return vnode('', , [], undefined, node as any);
 ...

那么我们再找一下isComment方法做了什么:

function isComment(node: Node): node is Comment 
  return node.nodeType === 8;

节点类型常量里面type 8就是Node.COMMENT_NODE也就是注释节点

然后顺着源码我们这里主要看带选择器节点的创建,首先得到tagnameclassid,然后在根据是否有ns属性来决定通过createElement还是createElementNS来创建节点,紧接着如果存在children,循环遍历调用createElm方法创建子节点并挂载到父节点上面,DOM节点创建完毕之后调用相应的hookl即可。

patchVnode

patchVnode方法会对传入的两个vnode节点进行对比,最终对比的结果会体现在DOM上,这便是diff算法:

  function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) 
    let i: any, hook: any;
    // 调用prepatch hook
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) 
      i(oldVnode, vnode);
    
    const elm = vnode.elm = (oldVnode.elm as Node);
    let oldCh = oldVnode.children;
    let ch = vnode.children;
    // 如果两个vnode完全相同,直接返回
    if (oldVnode === vnode) return;
    if (vnode.data !== undefined) 
      // 调用 module 上的 update hook
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      i = vnode.data.hook;
      // 调用vnode的update方法
      if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
    
    if (isUndef(vnode.text)) 
      // 新节点存在children
      if (isDef(oldCh) && isDef(ch)) 
        // 新旧节点均存在 children,且不一样时,对 children 进行 diff
        if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
       else if (isDef(ch)) 
        // 旧节点不存在 children 新节点有 children
        // 旧节点存在 text 置空
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
       else if (isDef(oldCh)) 
        // 新节点不存在 children 旧节点存在 children 移除旧节点的 children
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
       else if (isDef(oldVnode.text)) 
        api.setTextContent(elm, '');
      
     else if (oldVnode.text !== vnode.text) 
      if (isDef(oldCh)) 
        // 如果旧的vnode有children则移除
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      
      // 更新text
      api.setTextContent(elm, vnode.text as string);
    
    // 调用 postpatch hook
    if (isDef(hook) && isDef(i = hook.postpatch)) 
      i(oldVnode, vnode);
    
  

首先调用vnode上的prepatch hook,然后判断两个vnode节点是否完全相同,如果完全相同则直接返回,紧接着遍历调用moduleupdate hookvnodeupdate hook,然后分别处理如下几种情况:

  • 如果新节点不存在children而旧节点存在,那么使用removeVnodes方法移除旧节点的children并且设置文本内容
  • 如果新节点存在children且旧节点没有,然后移除旧节点的文本内容(如果有的话),最后通过addVnodes方法挂载子节点
  • 如果新旧节点均存在children,调用updateChildren方法对children进行diff
  • 如果新旧节点文本内容不相同,移除旧节点的children(如果有的话),然后更新文本内容
    最后当上述步骤都完成之后,调用vnodepostpatch hook钩子,所以接下来我们需要关注updateChildrenaddVnodesremoveVnodes都分别干了什么。

addVnodes

  function addVnodes(parentElm: Node,
                     before: Node | null,
                     vnodes: Array<VNode>,
                     startIdx: number,
                     endIdx: number,
                     insertedVnodeQueue: VNodeQueue) 
    for (; startIdx <= endIdx; ++startIdx) 
      const ch = vnodes[startIdx];
      if (ch != null) 
        // 循环遍历插入dom节点
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      
    
  

主要就是通过遍历children数组并且调用createElm方法生成DOM插入到父元素子节点末尾来达到添加节点的目的

removeVnodes

  function removeVnodes(parentElm: Node,
                        vnodes: Array<VNode>,
                        startIdx: number,
                        endIdx: number): void 
    for (; startIdx <= endIdx; ++startIdx) 
      let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
      if (ch != null) 
        // 非文本节点
        if (isDef(ch.sel)) 
          // 调用destroy hook
          invokeDestroyHook(ch);
          // 当所有的remove hook都调用了才会真正调用移除dom的方法
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm as Node, listeners);
          // 调用module的remove hook
          for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) 
            // 调用vnode的remove hook
            i(ch, rm);
           else 
            rm();
          
         else  // Text node
          api.removeChild(parentElm, ch.elm as Node);
        
      
    
  

  function createRmCb(childElm: Node, listeners: number) 
    return function rmCb() 
      if (--listeners === 0) 
        const parent = api.parentNode(childElm);
        api.removeChild(parent, childElm);
      
    ;
  

同样是遍历,只不过这里是移除节点

updateChildren

我们知道当新旧节点均有children并且互不相同的时候会调用updateChildren方法来对children进行diff

  function updateChildren(parentElm: Node,
                          oldCh: Array<VNode>,
                          newCh: Array<VNode>,
                          insertedVnodeQueue: VNodeQueue) 
    let oldStartIdx = 0, 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: any;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;

    // 遍历 oldCh newCh,对节点进行比较和更新
    // 每轮比较最多处理一个节点,算法复杂度 O(n)
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) 
      if (oldStartVnode == null) 
        // 当前节点可能已经被处理过
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
       else if (oldEndVnode == null) 
        oldEndVnode = oldCh[--oldEndIdx];
       else if (newStartVnode == null) 
        newStartVnode = newCh[++newStartIdx];
       else if (newEndVnode == null) 
        newEndVnode = newCh[--newEndIdx];
       else if (sameVnode(oldStartVnode, newStartVnode)) 
        // 新旧开始节点相同,直接调用 patchVnode 进行更新,下标向中间推进
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
       else if (sameVnode(oldEndVnode, newEndVnode)) 
        // 新旧结束节点相同,直接调用 patchVnode 进行更新,下标向中间推进
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
       else if (sameVnode(oldStartVnode, newEndVnode))  // Vnode moved right
        // 旧的开始节点等于新的结束节点,两个节点进行patch
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        // 把旧的开始节点插入到末尾
        api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
       else if (sameVnode(oldEndVnode, newStartVnode))  // Vnode moved left
        // 旧的结束节点等于新的开始节点,两个节点进行patch
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        // 把旧的就结束节点插入到开头
        api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
       else 
        // 如果上述情况都不符合,那么只有可能是以下两种情况
        // 1. 这个节点是新创建的
        // 2. 这个节点在原来的位置是处于中间的(oldStartIdx 和 endStartIdx之间)

        // 如果 oldKeyToIdx 不存在,创建 key 到 index 的映射
        // 而且也存在各种细微的优化,只会创建一次,并且已经完成的部分不需要映射
        if (oldKeyToIdx === undefined) 
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        if (isUndef(idxInOld))  // New element
          // 将新节点插入到开头
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          newStartVnode = newCh[++newStartIdx];
         else 
          // 如果不是新节点,说明该节点之前存在,找到该节点
          elmToMove = oldCh[idxInOld];
          // 如果两个节点key相同但是sel不同,那就通过createElm创建新的dom节点然后插入到开头
          if (elmToMove.sel !== newStartVnode.sel) 
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
           else 
            // 将oldCh中已存在的节点和新的开始节点进行patch
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            // 将oldCh中处理过的节点置空,等循环处理到这个地方的时候方便直接跳过
            oldCh[idxInOld] = undefined as any;
            // 将处理过后的节点插入到旧的开始节点之前
            api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
          
          newStartVnode = newCh[++newStartIdx];
        
      
    
    // oldCh或newCh中有一方先循环结束
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) 
      if (oldStartIdx > oldEndIdx) 
        // oldCh 已经全部处理完成,而 newCh 还有新的节点,需要对剩下的每个项都创建新的 dom
        before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
       else 
        // newCh 已经全部处理完成,而 oldCh 还有旧的节点,需要将多余的节点移除
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      
    
  

直接看整个过程会有点懵,可以结合例子理解一下,我们拿两个数组进行演示:

  • 假设旧节点顺序为[A, B, C, D],新节点为[B, A, C, D, E]
  • 第一轮比较:开始结束节点两两并不相等,于是看newStartVnode在旧节点中是否存在,最后找到了在第二个位置,调用patchVnode进行更新,将 oldCh[1] 至空,将 dom 插入到oldStartVnode前面,newStartIdx向中间移动,状态更新如下
  • 第二轮比较:oldStartVnodenewStartVnode相等,直接 patchVnodenewStartIdxoldStartIdx向中间移动,状态更新如下
  • 第三轮比较:oldStartVnode为空,oldStartIdx向中间移动,进入下轮比较,状态更新如下
  • 第四轮比较:oldStartVnodenewStartVnode相等,直接patchVnodenewStartIdxoldStartIdx向中间移动,状态更新如下
  • 第五轮比较:oldStartVnodenewStartVnode相等,直接patchVnodenewStartIdxoldStartIdx向中间移动,状态更新如下
  • oldStartIdx 已经大于 oldEndIdx,循环结束,由于是旧节点先结束循环而且还有没处理的新节点,调用 addVnodes处理剩下的新节点

到目前为止,整个snabbdom的核心流程已经梳理完毕

参考链接

以上是关于阅读分析snabbdom源码的主要内容,如果未能解决你的问题,请参考以下文章

Virtual DOM(虚拟 DOM)

前端笔记虚拟 DOM 库的基本使用 - Snabbdom

前端笔记虚拟 DOM 库的基本使用 - Snabbdom

Snabbdom:虚拟DOM和Diff算法

Android中的ContentProvider源码解析

snabbdom学习