Vue源码学习之snabbdom分析

Vue源码学习之snabbdom

文章内容输出来源:拉勾教育大前端高薪训练营

在拉钩训练营学习已经有一段时间了,感觉这段时间的收获远比自己独自学习强的多,自己学习的时候经常会因为惰性,无法坚持,在这里有班主任时刻关注你的学习进度(感觉对我这种懒人蛮好的),最重要的是有了一个学习的计划,不用无头苍蝇一样东一点西一点,最后什么都没记住。学习都是需要方法和技巧的,在训练营会有专业的老师为你答疑解惑,同时会教你解决问题的思路及方法,让你能够触类旁通。

Vue的源码中其虚拟dom部分主要是 借鉴了snabbdom.js,而snabbdom的代码行数比较少,可以帮助我们更好的理解相关概念,所以这里我们对snabbdom进行学习。

虚拟Dom

  • vNode实际上只是一个js对象,使用普通js对象来描述dom对象,因为不是真实dom所有叫虚拟dom(创建一个真实Dom的成本非常高,属性非常多),通过新旧虚拟DOM 这两个对象的差异(Diff算法),最终只把变化的部分重新渲染,提高渲染效率的过程; diff 是通过JS层面的计算,返回一个patch对象,即补丁对象,在通过特定的操作解析patch对象,完成页面的重新渲染
export interface VNode {
  sel: string | undefined; //选择器
  data: VNodeData | undefined; //数据
  children: Array<VNode | string> | undefined; //子节点
  elm: Node | undefined;//对应的真实DOM
  text: string | undefined;//文本
  key: Key | undefined;//key值
}

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
}

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, data, children, text, elm, key};
}

源码分析流程

我们根据一个案例开始分析:

import { init , h} from "snabbdom"
import style from "snabbdom/modules/style"
import eventlisteners from "snabbdom/modules/eventlisteners"
// const snabbdom = require("snabbdom")
const patch = init([style, eventlisteners ])

const oldNode= document.getElementById("app")

const vNode = h('span#first.class',
{//因为init时引入了style与eventListener模块所以可以使用事件及样式
	on:{
	    click:() => console.log('click')
	},
	style:{
	    color:"red"
	}
}, 'hello word')
//更新视图
patch(oldNode, vNode)

上述代码中,我们拿到了snabbdom暴露的init,h方法,通过init方法我们可以得到一个patch函数,在init的时候我们可以传入引用的modules,这样我们在使用h函数的时候就可以使用相关模块的功能,patch函数可以帮我们更新视图下面我们根据源码开始分析:


const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];

export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
	//一个参数接收的模块数组(attributes, props等模块),都有自己的钩子函数(pre, create等等),cbs就是用来收集这些钩子函数的收集器,cbs={pre:[updateStyle等modules中各个模块对应的钩子函数],create:[],...}
  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);
      }
    }
  }
  
	...相关功能函数
	
	//返回一个用于更新视图的patch函数
	return function patch(){...}
	
}

通过执行上面的init函数我们得到了一个用来更新新旧dom的patch函数,下面我们开始执行patch函数

function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    //执行pre钩子
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
	//判断oldVnode是否vNode类型,如果不是就转换成vNode
    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode);
    }
	//判断新旧node的sel与key是都相同
    if (sameVnode(oldVnode, vnode)) { //相同就更新
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {//不相同
    // 获取老节点的真实DOM
      elm = oldVnode.elm as Node;
      //获取老节点的父节点
      parent = api.parentNode(elm);
	  // 给将vNode转化内为真实DOM挂载在vNode的elm属性上
      createElm(vnode, insertedVnodeQueue);
	  //如果存在父节点	
      if (parent !== null) {
      //在父节点下插入vNode的真实DOM
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        //删除父节点下的老的真实DOM
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

   //当我们使用createElm创建真实DOM时,如果vNode的data的hook上有insert钩子,就会将该vNode添加到insertedVnodeQueue队列中,在对比结束之后会对insertedVnodeQueue进行遍历 执行队列中insertedVnodeQueue[i](vNode).data.hook.insert钩子
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    //执行cbs收集的post钩子
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    //将新节点返回作为下一次对比的oldVnode
    return vnode;
  };

通过上述代码分析我们可以知道,我们在对比vNode时,通过判断sel及key值来选择不同的执行方式:如果不同就删除旧节点插入新节点,如果相同就通过patchVnode做进一步处理:

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    let i: any, hook: any;
    //执行在创建vNode时data上传入的prepatch钩子
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
      i(oldVnode, vnode);
    }
    //因为新老接待你的sel及key值相同因此新老节点的elm指向的真实DOM也是同一个
    const elm = vnode.elm = (oldVnode.elm as Node);
    let oldCh = oldVnode.children;
    let ch = vnode.children;
    //如果对比的新老虚拟DOM相同,就不需要更新
    if (oldVnode === vnode) return;
    //当data存在执行cbs及data上对应的钩子,
    if (vnode.data !== undefined) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      i = vnode.data.hook;
      if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
    }
    //如果vNode不存在文本
    if (isUndef(vnode.text)) {
      //新老虚拟DOM都存在children,且不相等,更新children(diff算法核心)
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
        //如果vNode存在children
      } else if (isDef(ch)) {
      // 判断老虚拟DOM是否存在text,存在就更新为空
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        //将vNode的子节点添加更新到对应的真实DOM上
        addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
        //如果oldVnode存在children就删除
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
        //如果oldVnode存在text更新为空
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {//新老虚拟DOMtext不同
    //olVnode存在children就清除所有子节点
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      }
      //更新真实DOM的文本内容
      api.setTextContent(elm, vnode.text as string);
    }
    //执行钩子
    if (isDef(hook) && isDef(i = hook.postpatch)) {
      i(oldVnode, vnode);
    }
  }

通过以上函数我们可以知道根据olVnode,vNode中的text,children存在与否执行对应的操作,其中如果oldVnode与vNode的children同时存在且不相等还需要进一步更新,这个时候就开始执行updateChildren也是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; //oldKeyToIdx为oldNode中key与index对应对象
    let idxInOld: number; //newStartVnode的key在oldKeyToIdx中对应的
    let elmToMove: VNode;
    let before: any;

    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(oldStartVnode, newStartVnode, insertedVnodeQueue);
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        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
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
      } else {
        if (oldKeyToIdx === undefined) {//oldKeyToIdx为oldNode中key与index对应对象
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        idxInOld = oldKeyToIdx[newStartVnode.key as string]; //判断老节点中是否有新节点key 
        if (isUndef(idxInOld)) { // 没有说明是新节点 执行插入操作在老节点之前
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          newStartVnode = newCh[++newStartIdx];
        } else { // 有 说明key存在
          elmToMove = oldCh[idxInOld]; //老节点需要处理的额那个子节点
          if (elmToMove.sel !== newStartVnode.sel) {//如果选择器不同,说明新建元素,插入
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          } else { //新旧节点选择器也相同 说明需要更新
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); //更新节点
            oldCh[idxInOld] = undefined;//将旧节点中被更新的那个节点赋值为undefined,否则下次会继续对比
            api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);//将跟新节点插入到当前旧开始节点之前
          }
          newStartVnode = newCh[++newStartIdx];//开始下一次对比
        }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      if (oldStartIdx > oldEndIdx) {//老节点先遍历完成:记录当前newStartIdx与newEndIdx,将多余节点插入 newEndIdx + 1 位置节点之前
      
        before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {//新节点先遍历完成:清除多余节点
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值