4

前言:前面写了1篇vue 观察者模式响应式数据的解析,里面关于dom操作部分是直接操作的真实dom,并没有涉及到虚拟dom部分,然后这里就做一个虚拟dom的实现解析。
首先,还是说什么是虚拟dom,我之前一篇mini-react中说过虚拟dom的概念,这里再重复一下。

  • Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM
  • 可以使用 Virtual DOM 来描述真实 DOM,示例:
{
    sel:'div',
    data:{},
    children:undefined,
    text:'hello word',
    elm:undefined,
    key:undefined
}

这里和我之前写的 mini-react中说的虚拟dom结构不一样,这里是以Snabbdom生成的虚拟dom结构为例子,表述,因为vue2.0内部使用就是Snabbdom,只是在它的基础上改造了一些新的功能,它也是最快的虚拟dom库之一。

然后就是动机,这里说下为啥要用虚拟dom:

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升
  • 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM 出现了
  • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述DOM, Virtual DOM 内部将弄清楚如何有效(diff)的更新 DOM

虚拟dom的作用:

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等

虚拟dom是可以做(真实dom,ssr,原生应用,小程序)的转换的

前面说了vue 的虚拟dom基础就是Snabbdom,下面我们就来做下Snabbdom 的源码分析,方便后续进一步了解vue的源码

首先准备工作:和mini-react一样打包工具使用parcel,这里贴一下package.json的配置

{
  "name": "snabbdom-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "parcel index.html --open",
    "build": "parcel build index.html"
  },
  "dependencies": {
    "parcel-bundler": "^1.12.4",
    "snabbdom": "^0.7.4"
  }
}

注意:这里用的是snabbdom 0.7.4版本,版本不一致使用方式可能会略有变化,可以去查看官网,最新版是2.1.0,核心逻辑没有什么特别大的变化。
首先没了解过它的,可以先看下它的官网中文翻译:
snabbdom中文翻译
基本使用可以查看官网,可以参考上面的中文翻译

我们这里使用例子用的是es6模块导入,官网是commonjs规范,

import{init,h,thunk}from'snabbdom'
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'

snabbdom核心提供三个函数,init,h,thunk。

  • init是一个高阶函数,返回patch函数用来对比更新dom。
  • h函数用来返回虚拟节点vNode,vue中经常会看到.
  • thunk是一种优化策略,处理不可变数据时使用

模块,Snabbdom 的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块,官方提供了6个模块。

  • attributes

    • 设置 DOM 元素的属性,使用setAttribute()
    • 处理布尔类型的属性
  • props

    • 和attributes模块相似,设置 DOM 元素的属性element[attr]=value
    • 不处理布尔类型的属性
  • class

    • 切换类样式
    • 注意:给元素设置类样式是通过sel选择器
  • dataset

    • 设置data-*的自定义属性
  • eventlisteners

    • 注册和移除事件
  • style

    • 设置行内样式,支持动画
    • delayed/remove/destroy

这里放一个使用模块的例子:

import { init, h } from 'snabbdom'
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
// 2. 注册模块
let patch = init([
  style,
  eventlisteners
])
// 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
let vnode = h('div', {
  style: {
    backgroundColor: 'red'
  },
  on: {
    click: eventHandler
  }
}, [
  h('h1', 'Hello Snabbdom'),
  h('p', '这是p标签')
])

其实它核心主要分这几步:

  1. 使用init设置模块,创建patch函数
  2. 使用h函数创建js对象(vNode)描述真实dom
  3. patch函数对比新旧两个vNode
  4. 把变化内容更新到真实dom树上

我们来看下node_modules中snabbdom它的源码src下的结构
image.png

我们主要分析的有:

  • h函数 src/h.ts
  • vnode src/vnode.ts
  • init src/snabbdom.ts
  • patch src/snabbdom.ts
  • createElm src/snabbdom.ts
  • patchVnode src/snabbdom.ts
  • updateChildren src/snabbdom.ts

这里先说下对应函数的大概功能,最后我会把我做过注释的源代码都贴出来,有兴趣的可以把package.json拿走,下载然后找到对应node_modules里的snabbdom里的代码和我贴的代码注释对比看下。

h函数

  • Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode
  • 函数重载:

    • 参数个数或类型不同的函数
    • JavaScript 中没有重载的概念
    • TypeScript 中有重载,不过重载的实现还是通过代码调整参数

    h.ts中就使用了重载,源码中最后一个导出是对重载的实现,也是根据传入参数来判断的。

vnode

  • 一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是Virtual DOM

image.png

init

  • init(modules, domApi),返回 patch() 函数(高阶函数)

    • 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs
    • 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而不需要重新创建
  • init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中

patch

它的作用:

  • 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
  • 回新节点作为下一次处理的旧节点对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
  • 如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
  • diff 过程只进行同层级比较

功能:

  • 传入新旧 VNode,对比差异,把差异渲染到 DOM
  • 返回新的 VNode,作为下一次 patch() 的 oldVnode

执行过程:

  • 首先执行模块中的钩子函数pre
  • 如果 oldVnode 是 DOM 元素

    • 把 DOM 元素转换成 oldVnode
  • 如果 oldVnode 和 vnode 相同(key 和 sel 相同)

    • 调用 patchVnode(),找节点的差异并更新 DOM
  • 如果不相同

    • 调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
    • 把刚创建的 DOM 元素插入到 parent 中
    • 移除老节点
    • 触发用户设置的insert钩子函数
    • 触发模块post钩子

createElm

功能:

  • createElm(vnode, insertedVnodeQueue),返回创建的 DOM 元素
  • 创建 vnode 对应的 DOM 元素,放到vnode的elm上

执行过程:

  • 首先触发用户设置的init钩子函数
  • 如果选择器是!,创建注释节点
  • 如果选择器不为空

    • 解析选择器,生成真实dom,设置标签的 id 和 class 属性
    • 执行模块的create钩子函数
    • 如果 vnode 有 children,递归调用createElm创建子 vnode 对应的 DOM,追加到 DOM 树
    • 如果 vnode 的 text 值是 string/number,创建文本节点并追击到 DOM 树
    • 执行用户设置的create钩子函数
    • 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中
  • 如果选择器为空,创建文本节点

patchVnode

功能:

  • patchVnode(oldVnode, vnode, insertedVnodeQueue)
  • 对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM

执行过程

  • 首先执行用户设置的prepatch钩子函数
  • 执行先模块update 钩子函数再用户的update钩子函数
  • 如果vnode.text未定义

    • 如果oldVnode.children和vnode.children都有值

      • 新旧的children不相等 调用updateChildren()
      • 使用 diff 算法对比子节点,更新子节点
    • 如果vnode.children有值,oldVnode.children无值

      • 清空oldVnode中文本 DOM 元素
      • 调用addVnodes(),批量添加子节点
    • 如果oldVnode.children有值,vnode.children无值

      • 调用removeVnodes(),批量移除子节点
    • 如果oldVnode.text有值,新旧的children都没有

      • 清空 DOM 元素的内容
  • 如果设置了vnode.text并且和和oldVnode.text不等

    • 如果老节点有子节点,全部移除
    • 设置 DOM 元素的textContent为vnode.text
  • 最后执行用户设置的postpatch钩子函数

updateChildren

功能:diff 算法的核心,对比新旧节点的 children,更新 DOM(市面上虚拟dom的库,diff算法核心都是更新对比子集)

执行过程:

  • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二课树的每一个节点比较,但是这样的时间复杂度为 O(n^3)
  • 在DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
  • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复杂度为 O(n)

image.png

  • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
  • 在对开始和结束节点比较的时候,总共有四种情况

    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
    • oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
    • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)

image.png

下面就是具体执行顺序的比较了,如下:

重点1:

  • 开始节点和结束节点比较,这两种情况类似

    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
  • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同,同一个)

    • 调用 patchVnode() 对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++

    image.png
    这两种情况,这会是不需要调整位置的,因为都是开始节点。

重点2

  • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同

    • 调用 patchVnode() 对比和更新节点
    • 把 oldStartVnode 对应的 DOM 元素,移动到右边

      • 更新索引

    image.png

这里需要调整节点位置了,因为是要以新的children为主,对比的是新结束节点,先等也就是说(旧的开始节点现在=新的结束节点) 所以我们把旧开始节点移到最后,然后把++oldStartIdx,--newEndIdx,继续下一次的比对,如果下次还还是进入了这个判断,也就是说明 等于了新children倒数第二个,那就是要把oldStartIdx它插入到倒数第二的位置上,依次类推

重点3

  • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同

    • 调用 patchVnode() 对比和更新节点
    • 把 oldEndVnode 对应的 DOM 元素,移动到左边
    • 更新索引

    image.png

这里和上面刚好相反,也是需要调整位置,因为是要以新的children为主,对比的是新开始杰点,先等也就是说(旧的结束节点现在=新的开始节点)所以我们把旧结束节点移到最开始,然后把--oldEndIdx,++newEndIdx,继续下一次的比对,如果下次还还是进入了这个判断,也就是说明 等于了新children正数第二个,那就是要把oldEndIdx它插入到倒数第二的位置上,再进入的话依次类推

重点4

  • 如果不是以上四种情况

    • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
    • 如果没有找到,说明 newStartNode 是新节点

      • 创建新节点对应的 DOM 元素,插入到 DOM 树中,插入到oldStartVnode之前
    • 如果找到了

      • 判断新节点和找到的老节点的 sel 选择器是否相同
      • 如果不相同,说明节点被修改了

        • 重新创建对应的 DOM 元素,插入到 DOM 树中,插到插到oldStartVnode之前
      • 如果相同

        • patchVnode更新节点
        • 对比过的老children对应位置置空
        • 把 elmToMove 对应的 DOM 元素,移动到左边, 插入到 oldStartVnode之前 ,更新dom顺序

重点5

  • 循环结束

    • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
    • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
  • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边,批量插入newEndIdx之后,因为经过比对结束之后首先顺序是没有问题的,我们把没有出现的依次插入oldEndIdx之后就可以了。
    插入一段源代码

    //查看老的结束的下标是不是最后一个,是最后一个的话给null,不是最后一个的话获取最后一个的下一个的elm 真实dom
    before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
    // 把newStartIdx, newEndIdx之间的vnode 进行插入,然后根据before 看插入到最后,还是 newCh[newEndIdx+1].elm之前
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);

    这里可以看出,如果是null证明newEndIdx没有移动过下一个值是null,所我们全部插入在最后就可以,如果不是null,证明移动过,就获取它的下一个dom,因为--newEndIdx过,插入它之前,因为我们知道它的顺序是没问题,newEndIdx倒数第几个的位置也是没问题,这张图片可能有有点描述不对,主要看代码。

image.png

  • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点从oldStartIdx, 到oldEndIdx之间的vnode批量删除
    image.png

上面写了几个函数的具体实现思路,这里把函数在那里被调用,用图去描述一下,图里面只是简单描述了调用,具体方法的实现,跳转文章对应位置查看就可以了,vnode和h函数就不在图里描述了,一个是虚拟dom的结构,一个是用来生成虚拟dom的
init.png

微信截图_20201217161651.png

最后我把我注释的代码贴上

snabbdom.ts

/* global module, document, Node */
import {Module} from './modules/module';
import {Hooks} from './hooks';
import vnode, {VNode, VNodeData, Key} from './vnode';
import * as is from './is';
import htmlDomApi, {DOMAPI} from './htmldomapi';

function isUndef(s: any): boolean { return s === undefined; }
function isDef(s: any): boolean { return s !== undefined; }

type VNodeQueue = Array<VNode>;

const emptyNode = vnode('', {}, [], undefined, undefined);

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

function isVnode(vnode: any): vnode is VNode {
  return vnode.sel !== undefined;
}

type KeyToIndexMap = {[key: string]: number};

type ArraysOf<T> = {
  [K in keyof T]: (T[K])[];
}

type ModuleHooks = ArraysOf<Module>;

function createKeyToOldIdx(children: Array<VNode>, beginIdx: number, endIdx: number): KeyToIndexMap {
  let i: number, map: KeyToIndexMap = {}, key: Key | undefined, ch;
  for (i = beginIdx; i <= endIdx; ++i) {
    ch = children[i];
    if (ch != null) {
      key = ch.key;
      if (key !== undefined) map[key] = i;
    }
  }
  return map;
}

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

export {h} from './h';
export {thunk} from './thunk';
//高阶函数,接收使用的模块列表,比如说h函数第二个参数需要使用style,对应init初始化时就需要传入对应的模块,
//第二个参数时dom操作的列表,不传入的话就是默认的dom操作
//高阶函数时因为 返回的patch要多次调用,我们需要闭包存储一些每次调用都使用的参数,而不需要重新创建
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);

  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
  //cbs把所有的生命周期初始化成数组,然后把对应模块的生命周期函数放到对应数组里,后期再合适的时机统一执行
  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
  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);
  }

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

  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data; //获取data {style样式,hook钩子,on事件等}  除了hook其他的需要对应模块支持
    if (data !== undefined) {
      if (isDef(i = data.hook) && isDef(i = i.init)) { //获取传入init钩子
        i(vnode); //开始执行,传入vNode
        data = vnode.data;//这里重新给data赋值下,因为钩子函数执行过程可能会更改他
      }
    }
    let children = vnode.children, sel = vnode.sel;//获取它的子节点  和 选择器
    if (sel === '!') { //查看是否是注释节点
      if (isUndef(vnode.text)) { //是注释节点,查看vNode文本是否有
        vnode.text = ''; //清空
      }
      vnode.elm = api.createComment(vnode.text as string); //创建注释节点 赋值给vNode.elm 给它真实dom引用
    } else if (sel !== undefined) { //如果选择器存在
      // Parse selector
      const hashIdx = sel.indexOf('#'); //id开始位置
      const dotIdx = sel.indexOf('.', hashIdx); //class开始位置
      const hash = hashIdx > 0 ? hashIdx : sel.length; //查看有没有id
      const dot = dotIdx > 0 ? dotIdx : sel.length;//查看有没有class
      const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
      //获取标签 div span
      const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
                                                                               : api.createElement(tag);
      //根据tag创建元素标签,并且赋给 vnode.elm 给它真实dom的引用
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));//设置id
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));//设置class
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode); //执行模块中cbs中的 create的钩子函数
      //emptyNode空vNode  后面vNode是当前的
      if (is.array(children)) {//如果vNode子节点是数字,遍历 只要
        for (i = 0; i < children.length; ++i) { 
          const ch = children[i];
          if (ch != null) {
            //如果当前子节点项存在,继续createElm转换为真实dom返回插入到,父dom elm中
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          }
        }
      } else if (is.primitive(vnode.text)) {//如果没有子节点,有text文本,证明内部是文本内容
        api.appendChild(elm, api.createTextNode(vnode.text)); //创建文本节点放到elm dom中
      }
      i = (vnode.data as VNodeData).hook; // Reuse variable
      if (isDef(i)) {
        if (i.create) i.create(emptyNode, vnode); //取出data中的create钩子  执行
        if (i.insert) insertedVnodeQueue.push(vnode); 
        // 如果有insert钩子的话,把vNode放insertedVnodeQueue队列后面会执行insert
      }
    } else {
      //vNode选择器为空也不是注释节点,那就是一个文本  生成文本节点dom,赋值elm  真实dom引用
      vnode.elm = api.createTextNode(vnode.text as string);
    }
    //返回生成的dom结构 需要返回这个结构,递归中会用到
    return vnode.elm;
  }

  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) {
        //取出当前传入的 children 子节点生成html ,插入到父节点中
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

  function invokeDestroyHook(vnode: VNode) {
    let i: any, j: number, data = vnode.data;
    if (data !== undefined) {
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      if (vnode.children !== undefined) {
        for (j = 0; j < vnode.children.length; ++j) {
          i = vnode.children[j];
          if (i != null && typeof i !== "string") {
            invokeDestroyHook(i);
          }
        }
      }
    }
  }

  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)) {
          invokeDestroyHook(ch);
          listeners = cbs.remove.length + 1;
          rm = createRmCb(ch.elm as Node, listeners);
          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)) {
            i(ch, rm);
          } else {
            rm();
          }
        } else { // Text node
          api.removeChild(parentElm, ch.elm as Node);
        }
      }
    }
  }
  //diff核心,对比新旧节点的children 更新dom
  //在DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
  //所以是同级比对,再找下一届级别比对,算法复杂度 O(n)
  //几种对比方式会在 文章里具体说明
  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]; //老的开始vnode
    let oldEndVnode = oldCh[oldEndIdx]; //老的结束vnode
    let newEndIdx = newCh.length - 1; //新的结束坐标
    let newStartVnode = newCh[0]; //新的开始vnode
    let newEndVnode = newCh[newEndIdx]; //新的结束vnode
    let oldKeyToIdx: any;  
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;
    //开始比对 首先两个的开始结束都没有相交,都没有循环完毕
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { 
      if (oldStartVnode == null) { //老开始节点不存在
        oldStartVnode = oldCh[++oldStartIdx]; //++开始下标,获取下一个做开始老vnode, 下标加防止死循环
      } else if (oldEndVnode == null) { // 结束不存在
        oldEndVnode = oldCh[--oldEndIdx]; // ++结束下标 获取前一个 结束 老vnode ,下标加防止死循环
      } else if (newStartVnode == null) { //同上,不存在,这里获取的是新开始位置vnode  
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {//同上,不存在,这里是获取新结束vnode
        newEndVnode = newCh[--newEndIdx];
        //比较开始和结束的四种情况
      } else if (sameVnode(oldStartVnode, newStartVnode)) {//开始的vnode 是一个
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); // 对比不同进行更新dom 插入insert队列  后期执行钩子
        oldStartVnode = oldCh[++oldStartIdx]; //老开始vnode 更新 老开始下标更新
        newStartVnode = newCh[++newStartIdx];//新开始 vnode更新  新开始下标更新  第一种情况,进行下轮循环
      } else if (sameVnode(oldEndVnode, newEndVnode)) { // 结束的vnode 是一个
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); //对比不同更新 dom
        oldEndVnode = oldCh[--oldEndIdx]; //老结束vnode更新 老结束下标
        newEndVnode = newCh[--newEndIdx];//新结束 vnode更新 新结束下标
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // 老开始vnode 和 新 结束vnode  是否同一个 
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); //更新节点dom
        api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
        //dom更新完成之后,更改位置,因为是已新的children顺序为主,
        //所以把 这个老开始vnode放到 倒数第一个,newEndVnode已经对比过了,在对比的话还进来这里就是放到倒数第二个(因为等于了新结束vnode)以此类推
        oldStartVnode = oldCh[++oldStartIdx]; //老开始对比完毕更新 下标更新
        newEndVnode = newCh[--newEndIdx]; //新结束 对比完毕 更新vnode  下标更新
      } else if (sameVnode(oldEndVnode, newStartVnode)) { // 老结束vnode和新开始vnode 是同一个
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); //对比更新节点 dom
        api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
        //dom更新完了之后,同上更新位置,已新的children的顺序为主,把老的结束vnodedom放到 老的开始前面
        //第一位 ,newStartVnode对比过了,再进来就是第二位 以此类推
        oldEndVnode = oldCh[--oldEndIdx]; //更新老结束的位置及vnode
        newStartVnode = newCh[++newStartIdx];//更新新开始位置及vnode
      } else {
        //四种情况结束,从 根据现在的oldStartIdx, oldEndIdx在老的children里找和新开始对应的vnode
        if (oldKeyToIdx === undefined) {
          //根据oldStartIdx, oldEndIdx从老的children里把还没有比对过的 vnode拿出来,放到一个{}里,key就是对应vnode的key
          //值就是他们的位置坐标  方便后期直接查找
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        idxInOld = oldKeyToIdx[newStartVnode.key as string]; //根据新的开始vnode的key 通过哪个{}找到对应老的children里的vnode 
        //idxInOld有值的话就是找到对应的key相同的项了
        if (isUndef(idxInOld)) { // New element
          //如果不存在 证明  newStartVnode是个新的vnode,生成真实dom 插入 oldStartVnode 之前 根据顺序老的开始的位置
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          newStartVnode = newCh[++newStartIdx]; //这个新的开始节点比对完毕之后 获取下个新的开始节点vnode 进行继续比对
        } else {
          //在老的剩余没比对的idxInOld 对象里vnode  找到了
          elmToMove = oldCh[idxInOld];
          if (elmToMove.sel !== newStartVnode.sel) { //如果选择器标签不一样了,元素替换了 
            //证明不一样了,直接用新的newStartVnode 生成dom  插到oldStartVnode.elm dom之前
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          } else {
            //证明key sel选择器都相同
             
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            //比对不同点更新 newStartVnode更新到elmToMove elm  真实dom
            oldCh[idxInOld] = undefined as any;  //比对过的置成空
            api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
            //把更新完毕的dom 插入到 oldStartVnode之前 ,更新dom顺序
          }
          newStartVnode = newCh[++newStartIdx]; //最后一种情况新开始vnode对比过了,继续下一个新开始vnode对比
        }
      }
    }
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      //老的children或者新的children 有任意一个对比完了
      if (oldStartIdx > oldEndIdx) { //老的先完成了,证明新的比老的多,新的为主,把新的补上去
        //查看老的结束的下标是不是最后一个,是最后一个的话给null,不是最后一个的话获取最后一个的下一个的elm 真实dom
        before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
        // 把newStartIdx, newEndIdx之间的vnode 进行插入,然后根据before 看插入到最后,还是 newCh[newEndIdx+1].elm之前
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {  
        //新的先循环完成,老的比新的多
        //把老的children 从oldStartIdx, 到oldEndIdx之间的vnode  全部删除
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

  function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    //同一个vnode sel  key 都相同对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
    let i: any, hook: any;
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
      i(oldVnode, vnode);
      //首先调用用户prepatch钩子 一个元素即将被修补(patched)  对应钩子可以去看官网啥意思
      //新旧节点传入
    }
    const elm = vnode.elm = (oldVnode.elm as Node); //如果上一个oldVnode的elm 有的话赋值给vnode.elm 因为是一个,然后赋值给elm
    let oldCh = oldVnode.children; //获取上一个oldvnode的子节点
    let ch = vnode.children; //当前 vnode 的子节点
    if (oldVnode === vnode) return; //如果 新老vnode相同没变化返回
    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);
      //取出 data传入的update钩子执行
    }
    if (isUndef(vnode.text)) {//如果新vnode的text没有,开始对比 新旧的子节点 children和text互斥
      if (isDef(oldCh) && isDef(ch)) { //如果新旧 都有子节点
        //新旧两个子节点不相同 有变化 开始对比 diff对比不同更新到elm  dom上
        if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
      } else if (isDef(ch)) {
        //新vnode 子节点children不为空 老vnode children为空 
        if (isDef(oldVnode.text)) api.setTextContent(elm, ''); 
        //如果老vnode的文本有的话dom中给置空,删除老vnode dom内容
        addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
        //添加 新的vnode 的children 子节点vnode生成dom 到 插入到elm中 
      } else if (isDef(oldCh)) {
         //新vnode 子节点children为空 老vnode children不为空 
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
        //删除老vnode的children 子节点  执行对应模块 以及hook钩子
      } else if (isDef(oldVnode.text)) {
        //新vnode节点啥都没有,老vnode节点有text,从dom删除 
        api.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      //如果vnode.text有值,两个还不一致
      if (isDef(oldCh)) {
        //老的vnode有children  ,从dom上清空
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      }
      //把新的vnode的text渲染到dom中
      api.setTextContent(elm, vnode.text as string);
    }
    if (isDef(hook) && isDef(i = hook.postpatch)) {//执行最后的用户传入hook模块中的postpatch钩子一个元素已经被修补完成(patched)
      i(oldVnode, vnode);
    }
  }
  //patch打补丁,把新节点的变化内容渲染到真实dom,返回新节点作为下一次处理的旧节点
  return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = []; //保存新插入节点的队列,为了触发钩子函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); //模块pre ptach过程开始钩子

    if (!isVnode(oldVnode)) { //isVnode vnode.sel 判断是否是虚拟dom,如果上一次的节点不是虚拟dom,也就是说是真实dom
      oldVnode = emptyNodeAt(oldVnode); //取出真实dom的标签名 id class 来创建一个空的vNode  这个带有elm储存它的真实dom引用
    }

    if (sameVnode(oldVnode, vnode)) { //通过key和sel选择器 对比老的新的两个 vNode是否是同一个
      patchVnode(oldVnode, vnode, insertedVnodeQueue); //是同一个的话对比两个差异来进行 更新dom,把新的更新上
    } else {
      //新旧节点不一致,  vNode创建对应的dom
      elm = oldVnode.elm as Node;//获取旧节点的真实dom引用
      parent = api.parentNode(elm); //获取它的父级 

      createElm(vnode, insertedVnodeQueue); //根据vNode创建真实dom 并且 elm存入真实dom引用

      if (parent !== null) {//父级存在
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm)); //把vNode.elm插入到父级中,elm旧的 之后 
        removeVnodes(parent, [oldVnode], 0, 0); //删除旧的 
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) { //所有的vnode补丁完了,执行用户设置节点的的insert钩子
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); //最后执行模块的 post生命钩子
    return vnode; //最后把更新完vnode返回出去,下次patch它就是旧的了,下一次调用时再和新的对比
  };
}

h.ts

import {vnode, VNode, VNodeData} from './vnode';
export type VNodes = Array<VNode>;
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>
import * as is from './is';

function addNS(data: any, children: VNodes | undefined, sel: string | undefined): void {
  data.ns = 'http://www.w3.org/2000/svg';
  if (sel !== 'foreignObject' && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      let childData = children[i].data;
      if (childData !== undefined) {
        addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel);
      }
    }
  }
}

export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {//主要作用生产 虚拟dom
  
  var data: VNodeData = {}, children: any, text: any, i: number;
  //处理参数,实现重载
  if (c !== undefined) {
    //三个参数时
    //1sel:div#id.calss, 2data: {style,on事件hook钩子等},
    //3children/text 子节点数组或者是文本 互斥   
    data = b;
    if (is.array(c)) { children = c; }//数组,有子集 children储存子集
    else if (is.primitive(c)) { text = c; } //c是字符串或数字,没有子集,内容是文本
    else if (c && c.sel) { children = [c]; }//c是vnode,h(...)函数最后返回转换出来的vnode,
    //第三个参数就是子集,不是文本的话,转换为数组统一处理
  } else if (b !== undefined) {
    //二个参数
    if (is.array(b)) { children = b; } // 第二个参数是数组子集,放到children
    else if (is.primitive(b)) { text = b; } //是文本和children互斥
    else if (b && b.sel) { children = [b]; }//是vnode 转换为数组统一处理 储存
    else { data = b; } //都不是,那可能是对象data,on事件之类的 直接储存
  }
  //如果有子集
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      //循环子集,如果当前这项是string或者number, 返回一个只有 text有值的vnode 虚拟dom  赋给当前项
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    }
  }
  if ( //是svg的情况  添加命名空间
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    addNS(data, children, sel);
  }
  // 生成虚拟dom的结构 {sel 选择器, data 样式生命周期等, children 子节点数组, text 子节点文本, elm 真实dom引用, key data.key}
  return vnode(sel, data, children, text, undefined);
};
export default h;

vNode.ts

import {Hooks} from './hooks';
import {AttachData} from './helpers/attachto'
import {VNodeStyle} from './modules/style'
import {On} from './modules/eventlisteners'
import {Attrs} from './modules/attributes'
import {Classes} from './modules/class'
import {Props} from './modules/props'
import {Dataset} from './modules/dataset'
import {Hero} from './modules/hero'

export type Key = string | number;

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
}

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;
  //sel:div#id.class选择器 
  //data:节点数据{style样式,hook钩子,on事件等} 
  //children 子节点数组  和text互斥
  //text 节点内容 与 children互斥
  //elm 记录vnode 对应的真实dom
  //key data中获取  优化渲染使用 
  return {sel, data, children, text, elm, key};
}

export default vnode;

接下来就该正式进入vue的源码学习了。


Charon
57 声望16 粉丝

世界核平