vue2源码进修开胃菜——snabbdom源码进修(二)

媒介

在上一章我们进修了,modules,vnode,h,htmldomapi,is等模块,在这一篇我们将会进修到
snabbdom的中心功用——patchVnode和updateChildren功用。

继承我们的snabbdom源码之旅

终究章 snabbdom!

起首我们先从简朴的部份最先,比方一些东西函数,我将一一来解说他们的用途

sameNode

这个函数主要用于比较oldvnode与vnode同条理节点的比较,假如同条理节点的key和sel都雷同
我们就能够保存这个节点,不然直接替换节点

function sameVnode(vnode1, vnode2) {
  return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

createKeyToOldIdx

这个函数的功用非常简朴,就是将oldvnode数组中位置对oldvnode.key的映照转换为oldvnode.key
对位置的映照

function createKeyToOldIdx(children, beginIdx, endIdx) {
  var i, map = {}, key;
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) map[key] = i;
  }
  return map;
}

hook

snabbdom在全局下有6种范例的钩子,触发这些钩子时,会挪用对应的函数对节点的状况举行变动
起首我们来看看有哪些钩子:

NameTriggered whenArguments to callback
prethe patch process begins (patch最先时触发)none
inita vnode has been added (vnode被建立时触发)vnode
createa DOM element has been created based on a vnode (vnode转换为实在DOM节点时触发emptyVnode, vnode
insertan element has been inserted into the DOM (插进去到DOM树时触发)vnode
prepatchan element is about to be patched (元素预备patch前触发)oldVnode, vnode
updatean element is being updated (元素更新时触发)oldVnode, vnode
postpatchan element has been patched (元素patch完触发)oldVnode, vnode
destroyan element is directly or indirectly being removed (元素被删除时触发)vnode
removean element is directly being removed from the DOM (元素从父节点删除时触发,和destory略有差别,remove只影响到被移除节点中最顶层的节点)vnode, removeCallback
postthe patch process is done (patch完成后触发)none

然后,下面列出钩子对应的状况更新函数:

  • create => style,class,dataset,eventlistener,props,hero

  • update => style,class,dataset,eventlistener,props,hero

  • remove => style

  • destory => eventlistener,style,hero

  • pre => hero

  • post => hero

好了,简朴的都看完了,接下来我们最先打大boss了,第一关就是init函数了

init

init函数有两个参数modules和api,个中modules是init依靠的模块,如attribute、props
、eventlistener这些模块,api则是对封装实在DOM操纵的东西函数库,假如我们没有传入,则默许
运用snabbdom供应的htmldomapi。init还包含了很多vnode和实在DOM之间的操纵和注册全局钩子,
另有patchVnode和updateChildren这两个主要功用,然后返回一个patch函数

注册全局钩子

     //注册钩子的回调,在发作状况变动时,触发对应属性变动
      for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
          if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]]);
        }
      }

emptyNodeAt

这个函数主要的功用是将一个实在DOM节点转化成vnode情势,
<div id='a' class='b c'></div>将转换为{sel:'div#a.b.c',data:{},children:[],text:undefined,elm:<div id='a' class='b c'>}

     function emptyNodeAt(elm) {
        var id = elm.id ? '#' + elm.id : '';
        var c = elm.className ? '.' + elm.className.split(' ').join('.') : '';
        return VNode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
      }

createRmCb

我们晓得当我们须要remove一个vnode时,会触发remove钩子作拦截器,只要在一切remove钩子
回调函数都触发完才会将节点从父节点删除,而这个函数供应的就是对remove钩子回调操纵的计数功用

function createRmCb(childElm, listeners) {
    return function() {
      if (--listeners === 0) {
        var parent = api.parentNode(childElm);
        api.removeChild(parent, childElm);
      }
    };
  }

invokeDestoryHook

这个函数用于手动触发destory钩子回调,主要步骤以下:

  • 先挪用vnode上的destory

  • 再挪用全局下的destory

  • 递归挪用子vnode的destory

    function invokeDestroyHook(vnode) {
      var i, j, data = vnode.data;
      if (isDef(data)) {
    //先触发该节点上的destory回调
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
    //在触发全局下的destory回调
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
    //递归触发子节点的destory回调
    if (isDef(i = vnode.children)) {
      for (j = 0; j < vnode.children.length; ++j) {
        invokeDestroyHook(vnode.children[j]);
      }
    }
      }
    }

removeVnodes

这个函数主要功用是批量删除DOM节点,须要合营invokeDestoryHook和createRmCb服用,结果更佳
主要步骤以下:

  • 挪用invokeDestoryHook以触发destory回调

  • 挪用createRmCb来最先对remove回调举行计数

  • 删除DOM节点

      /**
       *
       * @param parentElm 父节点
       * @param vnodes  删除节点数组
       * @param startIdx  删除肇端坐标
       * @param endIdx  删除终了坐标
       */
    function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
      for (; startIdx <= endIdx; ++startIdx) {
    var i, listeners, rm, ch = vnodes[startIdx];
    if (isDef(ch)) {
      if (isDef(ch.sel)) {
        //挪用destroy钩子
        invokeDestroyHook(ch);
        //对全局remove钩子举行计数
        listeners = cbs.remove.length + 1;
        rm = createRmCb(ch.elm, listeners);
        //挪用全局remove回调函数,并每次削减一个remove钩子计数
        for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
        //挪用内部vnode.data.hook中的remove钩子(只要一个)
        if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
          i(ch, rm);
        } else {
          //假如没有内部remove钩子,须要挪用rm,确保能够remove节点
          rm();
        }
      } else { // Text node
        api.removeChild(parentElm, ch.elm);
      }
    }
      }
    }
    

createElm

就如太极有阴就有阳一样,既然我们有remove操纵,一定也有createelm的操纵,这个函数主要功用
以下:

  • 初始化vnode,挪用init钩子

  • 建立对应tagname的DOM element节点,并将vnode.sel中的id名和class名挂载上去

  • 假如有子vnode,递归建立DOM element节点,并增加到父vnode对应的element节点上去,

    不然假如有text属性,则建立text节点,并增加到父vnode对应的element节点上去
  • vnode转换成dom节点操纵完成后,挪用create钩子

  • 假如vnode上有insert钩子,那末就将这个vnode放入insertedVnodeQueue中作纪录,到时

    再在全局批量挪用insert钩子回调
    
    function createElm(vnode, insertedVnodeQueue) {
       var i, data = vnode.data;
       if (isDef(data)) {
     //当节点上存在hook而且hook中有init钩子时,先挪用init回调,对刚建立的vnode举行处置惩罚
     if (isDef(i = data.hook) && isDef(i = i.init)) {
       i(vnode);
       //猎取init钩子修改后的数据
       data = vnode.data;
     }
       }
       var elm, children = vnode.children, sel = vnode.sel;
       if (isDef(sel)) {
     // Parse selector
     var hashIdx = sel.indexOf('#');
     //先id后class
     var dotIdx = sel.indexOf('.', hashIdx);
     var hash = hashIdx > 0 ? hashIdx : sel.length;
     var dot = dotIdx > 0 ? dotIdx : sel.length;
     var tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
     //建立一个DOM节点援用,并对其属性实例化
     elm = vnode.elm = isDef(data) && isDef(i = data.ns) ? api.createElementNS(i, tag): api.createElement(tag);
      //猎取id名 #a --> a
     if (hash < dot) elm.id = sel.slice(hash + 1, dot);
     //猎取类名,并格式化  .a.b --> a b
     if (dotIdx > 0) elm.className = sel.slice(dot + 1).replace(/\./g, ' ');
     //假如存在子元素Vnode节点,则递归将子元素节点插进去到当前Vnode节点中,并将已插进去的子元素节点在insertedVnodeQueue中作纪录
     if (is.array(children)) {
       for (i = 0; i < children.length; ++i) {
         api.appendChild(elm, createElm(children[i], insertedVnodeQueue));
       }
       //假如存在子文本节点,则直接将其插进去到当前Vnode节点
     } else if (is.primitive(vnode.text)) {
       api.appendChild(elm, api.createTextNode(vnode.text));
     }
     //当建立终了后,触发全局create钩子回调
     for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
     i = vnode.data.hook; // Reuse variable
     if (isDef(i)) {
       if (i.create) i.create(emptyNode, vnode);
       //假如有insert钩子,则推动insertedVnodeQueue中作纪录,从而完成批量插进去触发insert回调
       if (i.insert) insertedVnodeQueue.push(vnode);
     }
       }
       //假如没声明选择器,则申明这个是一个text节点
       else {
     elm = vnode.elm = api.createTextNode(vnode.text);
       }
       return vnode.elm;
     }

addVnodes

这个函数非常简朴,就是将vnode转换后的dom节点插进去到dom树的指定位置中去

function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before);
    }
  }

说完上面的节点东西函数以后,我们就最先看怎样举行patch操纵了,起首我们从patch,也就是init
返回的函数最先

patch

起首我们须要明白的一个是,假如根据传统的diff算法,那末为了找到最小变化,须要逐层逐层的去
搜刮比较,如许时候复杂度将会到达 O(n^3)的级别,价值非常高,考虑到节点变化很少是跨条理的,
vdom采用的是一种简化的思绪,只比较同层节点,假如差别,那末纵然该节点的子节点没变化,我们
也不复用,直接将从父节点最先的子树悉数删除,然后再从新建立节点增加到新的位置。假如父节点
没变化,我们就比较一切同层的子节点,对这些子节点举行删除、建立、移位操纵。有了这个头脑,
明白patch也非常简朴了。patch只须要对两个vnode举行推断是不是相似,假如相似,则对他们举行
patchVnode操纵,不然直接用vnode替换oldvnode。

return function(oldVnode, vnode) {
    var i, elm, parent;
    //纪录被插进去的vnode行列,用于批触发insert
    var insertedVnodeQueue = [];
    //挪用全局pre钩子
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
    //假如oldvnode是dom节点,转化为oldvnode
    if (isUndef(oldVnode.sel)) {
      oldVnode = emptyNodeAt(oldVnode);
    }
    //假如oldvnode与vnode相似,举行更新
    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      //不然,将vnode插进去,并将oldvnode从其父节点上直接删除
      elm = oldVnode.elm;
      parent = api.parentNode(elm);

      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }
    //插进去完后,挪用被插进去的vnode的insert钩子
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
    }
    //然后挪用全局下的post钩子
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    //返回vnode用作下次patch的oldvnode
    return vnode;
  };

patchVnode

真正对vnode内部patch的照样得靠patchVnode。让我们看看他究竟做了什么?

function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
    var i, hook;
    //在patch之前,先挪用vnode.data的prepatch钩子
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
      i(oldVnode, vnode);
    }
    var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children;
    //假如oldvnode和vnode的援用雷同,申明没发作任何变化直接返回,防止机能糟蹋
    if (oldVnode === vnode) return;
    //假如oldvnode和vnode差别,申明vnode有更新
    //假如vnode和oldvnode不相似则直接用vnode援用的DOM节点去替换oldvnode援用的旧节点
    if (!sameVnode(oldVnode, vnode)) {
      var parentElm = api.parentNode(oldVnode.elm);
      elm = createElm(vnode, insertedVnodeQueue);
      api.insertBefore(parentElm, elm, oldVnode.elm);
      removeVnodes(parentElm, [oldVnode], 0, 0);
      return;
    }
    //假如vnode和oldvnode相似,那末我们要对oldvnode自身举行更新
    if (isDef(vnode.data)) {
      //起首挪用全局的update钩子,对vnode.elm自身属性举行更新
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      //然后挪用vnode.data内里的update钩子,再次对vnode.elm更新
      i = vnode.data.hook;
      if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
    }
    //假如vnode不是text节点
    if (isUndef(vnode.text)) {
      //假如vnode和oldVnode都有子节点
      if (isDef(oldCh) && isDef(ch)) {
        //当Vnode和oldvnode的子节点差别时,挪用updatechilren函数,diff子节点
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
      }
      //假如vnode有子节点,oldvnode没子节点
      else if (isDef(ch)) {
        //oldvnode是text节点,则将elm的text消灭
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        //并增加vnode的children
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      }
      //假如oldvnode有children,而vnode没children,则移除elm的children
      else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      //假如vnode和oldvnode都没chidlren,且vnode没text,则删除oldvnode的text
      else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '');
      }
    }

    //假如oldvnode的text和vnode的text差别,则更新为vnode的text
    else if (oldVnode.text !== vnode.text) {
      api.setTextContent(elm, vnode.text);
    }
    //patch完,触发postpatch钩子
    if (isDef(hook) && isDef(i = hook.postpatch)) {
      i(oldVnode, vnode);
    }
  }

updateChildren

关于同层的子节点,snabbdom主要有删除、建立的操纵,同时经由过程移位的要领,到达最大复用存在
节点的目标,个中须要保护四个索引,分别是:

  • oldStartIdx => 旧头索引

  • oldEndIdx => 旧尾索引

  • newStartIdx => 新头索引

  • newEndIdx => 新尾索引

然后最先将旧子节点组和新子节点组举行一一比对,直到遍历完任一子节点组,比对战略有5种:

  • oldStartVnode和newStartVnode举行比对,假如相似,则举行patch,然后新旧头索引都后移

  • oldEndVnode和newEndVnode举行比对,假如相似,则举行patch,然后新旧尾索引前移

  • oldStartVnode和newEndVnode举行比对,假如相似,则举行patch,将旧节点移位到末了

    然后旧头索引后移,尾索引前移,为何要如许做呢?我们思索一种状况,如旧节点为【5,1,2,3,4】
    ,新节点为【1,2,3,4,5】,假如缺少这类推断,意味着须要先将5->1,1->2,2->3,3->4,4->5五
    次删除插进去操纵,纵然是有了key-index来复用,也会涌现也会涌现【5,1,2,3,4】->
    【1,5,2,3,4】->【1,2,5,3,4】->【1,2,3,5,4】->【1,2,3,4,5】共4次操纵,假如
    有了这类推断,我们只须要将5插进去到旧尾索引背面即可,从而完成右移
  • oldEndVnode和newStartVnode举行比对,处置惩罚和上面相似,只不过改成左移

  • 假如以上状况都失利了,我们就只能复用key雷同的节点了。起首我们要经由过程createKeyToOldIdx

    建立key-index的映照,假如新节点在旧节点中不存在,我们将它插进去到旧头索引节点前,
    然后新头索引向后;假如新节点在就旧节点组中存在,先找到对应的旧节点,然后patch,并将
    旧节点组中对应节点设置为undefined,代表已遍历过了,不再遍历,不然能够存在反复
    插进去的题目,末了将节点移位到旧头索引节点之前,新头索引向后
    

遍历完以后,将盈余的新Vnode增加到末了一个新节点的位置后或许删除过剩的旧节点

/**
   *
     * @param parentElm 父节点
     * @param oldCh 旧节点数组
     * @param newCh 新节点数组
     * @param insertedVnodeQueue
     */
  function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {

    var oldStartIdx = 0, newStartIdx = 0;
    var oldEndIdx = oldCh.length - 1;
    var oldStartVnode = oldCh[0];
    var oldEndVnode = oldCh[oldEndIdx];
    var newEndIdx = newCh.length - 1;
    var newStartVnode = newCh[0];
    var newEndVnode = newCh[newEndIdx];
    var oldKeyToIdx, idxInOld, elmToMove, before;

    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)) {
        //对旧头索引节点和新头索引节点举行diff更新, 从而到达复用节点结果
        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];
      }
        //假如旧头索引节点和新头索引节点相似,能够经由过程挪动来复用
        //如旧节点为【5,1,2,3,4】,新节点为【1,2,3,4,5】,假如缺少这类推断,意味着
        //那样须要先将5->1,1->2,2->3,3->4,4->5五次删除插进去操纵,纵然是有了key-index来复用,
        // 也会涌现【5,1,2,3,4】->【1,5,2,3,4】->【1,2,5,3,4】->【1,2,3,5,4】->【1,2,3,4,5】
        // 共4次操纵,假如有了这类推断,我们只须要将5插进去到末了一次操纵即可
      else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
      }
      //道理与上面雷同
      else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      }
      //假如上面的推断都不经由过程,我们就须要key-index表来到达最大水平复用了
      else {
        //假如不存在旧节点的key-index表,则建立
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        //找到新节点在旧节点组中对应节点的位置
        idxInOld = oldKeyToIdx[newStartVnode.key];
        //假如新节点在旧节点中不存在,我们将它插进去到旧头索引节点前,然后新头索引向后
        if (isUndef(idxInOld)) { // New element
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm);
          newStartVnode = newCh[++newStartIdx];
        } else {
          //假如新节点在就旧节点组中存在,先找到对应的旧节点
          elmToMove = oldCh[idxInOld];
          //先将新节点和对应旧节点作更新
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
          //然后将旧节点组中对应节点设置为undefined,代表已遍历过了,不在遍历,不然能够存在反复插进去的题目

          oldCh[idxInOld] = undefined;
          //插进去到旧头索引节点之前
          api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm);
          //新头索引向后
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    //当旧头索引大于旧尾索引时,代表旧节点组已遍历完,将盈余的新Vnode增加到末了一个新节点的位置后
    if (oldStartIdx > oldEndIdx) {
      before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm;
      addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
    }
    //假如新节点组先遍历完,那末代表旧节点组中盈余节点都不须要,所以直接删除
    else if (newStartIdx > newEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }

至此,snabbdom的主要功用就剖析完了

    原文作者:steinns
    原文地址: https://segmentfault.com/a/1190000009017349
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞