Snabbdom源码

81 阅读15分钟

Virtual DOM的实现原理之snabbdom源码学习

目标:

  • 了解虚拟DOM及其作用
  • snabbdom的基本使用
  • snabbdom源码解析

1 What is Virtual DOM

Virtual Dom(虚拟DOM),是由普通的JS对象来描述DOM对象,我们可以对两个状态下的 js 对象进行对比,记录出它们的差异,然后把它应用到真正的dom树。因为不是真实的DOM对象,所以叫做Virtual DOM. 使用虚拟DOM来模拟真实的DOM

因为我们知道一个DOM对象中的成员是非常多。所以创建Dom对象的成本非常高。 如果使用虚拟Dom来描述真实Dom,就会发现创建的成员少,成本也就低了

2 Why is Virtual DOM

  • 手动操作Dom比较麻烦,还需要考虑浏览器兼容性问题,虽然有Jquery等库简化DOM操作,但是随着项目的复杂度越来越高,DOM操作复杂提升,既要考虑Dom操作,还要考虑数据的操作。
  • 为了简化DOM的复杂操作于是出现了各种的MVVM框架,MVVM框架解决了视图和状态的同步问题,也就是当数据发生变化,更新视图,当视图发生变化更新数据。
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题(当数据发生了变化后,无法获取上一次的状态,只有将页面上的元素删除,然后在重新创建,这时页面有刷新的问题,同时频繁操作Dom,性能也会非常低),于是Virtual Dom出现了。
  • Virtual Dom的好处就是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOMVirtual Dom内部将弄清楚如何有效(diff)的更新DOM.(例如:向用户添加列表中添加一个用户,只添加新的内容,原有的结构会被重用) 我们使用Jquery来实现数据展示与排序:
<!DOCTYPE html>
<html>
<head>
  <title></title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.js"></script>
</head>
<body>
  <div id="app"></div>
  <div id="sort" style="margin-top: 20px;">按年龄排序</div>
  <script type="text/javascript">
    let datas = [
      { 'name': 'xsm001', 'age': 32 },
      { 'name': 'xsm002', 'age': 29 },
      { 'name': 'xsm003', 'age': 31 },
      { 'name': 'xsm004', 'age': 30 }
    ];
    let render = function() {
      let html = '';
      datas.forEach(function(item, index) {
        html += `<li>
                  <div class="u-cls">
                    <span class="name">姓名:${item.name}</span>
                    <span class="age" style="margin-left:20px;">年龄:${item.age}</span>
                    <span class="closed">x</span>
                  </div>
                </li>`;
      });
      return html;
    };
    $("#app").html(render());
    $('#sort').on('click', function() {
      datas = datas.sort(function(a, b) {
        return a.age - b.age;
      });
      $('#app').html(render());
    })
  </script>
</body>
</html>

可以看出,它虽然能实现排序功能,但是比较暴力,不论数据有没有发生改变,都会将之前的DOM全部从页面干掉,然后重新去渲染新的dom节点,如果是小页面还好,要是负责的页面,就会有大量的dom操作,进而影响性能 虚拟DOM的思想是先控制数据再到视图,但是数据状态是通过diff比对,它会比对新旧虚拟DOM节点,然后找出两者之前的不同,然后再把不同的节点再发生渲染操作。

3 虚拟DOM的作用

维护视图和状态的关系(虚拟DOM会记录状态的变化,只需要更新状态变化的内容就可以了) 复杂视图情况下提升渲染性能。 虚拟DOM除了渲染DOM以外,还可以实现渲染到其它的平台,例如可以实现服务端渲染(ssr),原生应用(React Native),小程序(uni-app等)。以上列举的案例中,内部都使用了虚拟DOM.

虚拟DOM的生成.png

4 Snabbdom基本使用

//创建项目目录
md snabbdom-demo
// 进入项目目录
 cd snabbdom-demo
// 创建package.json
npm init -y
//本地安装parcel
npm install parcel-bundler

配置package.json

"srcipts":{ "dev":"parcel index.html --open" , //open打开浏览器 "build":"parcel build index.html" }

github链接: github.com/snabbdom/sn…

4.1 基本使用

import { h, thunk, init } from "snabbdom";
// init方法返回值为patch函数,patch函数作用是对比两个vndoe的差异并更新到真实的DOM中。init函数的参数是一个数组,数组中的内容是模块,关于模块内容后面还会讲解
let patch = init([]);
//创建虚拟DOM
// 第一个参数:标签+选择器(id选择器或者是类选择器)
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h("div#container.cls", "Hello World");
//我们这里需要将创建的虚拟dom,最终要渲染到`index.html`中`app`这个div中,所以这里需要获取一下该div
let app = document.querySelector("#app");
//要想将虚拟DOM渲染到`app`中,需要用到patch函数。
// 我们知道patch函数的作用是对比两个vnode的差异来更新到真实的`DOM`中。
//但是我们目前没有两个虚拟DOM.那么patch方法的第一个参数也可以是真实的DOM.patch方法会将真实的DOM转换成VNode.
// 第二个参数:为VNode
//返回值为VNode
let oldNode = patch(app, vnode);
vnode = h("div","Hello Snabbdom")
patch(oldNode,vnode)

snabbdom.png

4.2 模块

Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理,可以使用模块.

常用模块

官方提供了6个模块

attributes:设置DOM元素的属性,内部使用setAttribute()来设置属性,处理布尔类型的属性(可以对布尔类型的属性作相应的判断处理,布尔类型的属性,我们比较熟悉的有selected,checked`等)。

props:attributes模块类似,设置DOM元素的属性element[attr]=value,不处理布尔类型的属性。

class: 切换样式类,注意:给元素设置类样式是通过sel选择器。··

dataset:设置 HTML5 中的 data-* 的自定义属性

eventlisteners: 注册和移除事件

style:设置行内样式,支持动画(内部创建transitionend事件),会增加额外的属性:delayed / remove / destroy

下面看一下模块的使用

使用模块的步骤:

第一步:导入需要的模块

第二步:在init()中注册模块

第三步:使用h函数创建VNode的时候,可以把第二个参数设置为对象(对象中是模块需要的数据,可以设置行内样式、事件等),其它参数往后移。

下面我们要实现的案例,就是给div添加一个背景,同时为其添加一个单击事件,当然在div中还要创建两个元素分别是h1p.

具体实现的代码如下:

//导入模块
import style from "snabbdom/modules/style";
import eventlisteners from "snabbdom/modules/eventlisteners";
//注册模块
let patch = init([style, eventlisteners]);
// 使用h函数的第二个参数传入模块所需要的数据(对象)
let vnode = h(
  "div",
  {
    style: {
      backgroundColor: "red",
    },
    on: {
      click: eventHandler,
    },
  },
  [h("h1", "Hello Vue"), h("p", "这是p标签")]
);
function eventHandler() {
  console.log("点击了我");
}
let app = document.querySelector("#app");
patch(app, vnode);

5 源码解读

5.1 h函数

我们都知道,在Vue中h函数支持组件内容,在Snabbdom中的h函数是用来创建VNode

// h函数的重载
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;
//h函数重载的具体实现
//h函数可以接收三个参数,?表示该参数可以不传递
export function h(sel: any, b?: any, c?: any): VNode {
    //定义变量
  var data: VNodeData = {}, children: any, text: any, i: number;
    // 处理参数,实现重载的机制
    //如果c这个参数的值不等于undefined,表示传递了三个参数
  if (c !== undefined) {
      //如果该条件成立,表示处理的就是有三个参数的情况
      //参数b中存储的就是模块处理的时候,需要的数据,例如:行内样式,事件等,关于这块在前面的案例中我们也写过,在这里将b参数的值赋给了data这个变量
    data = b;
      //下面是对参数c进行了判断。
      //关于参数c有三种情况,第一种情况为数组,第二种情况为字符串或者是数字,第三种情况为VNode.
      //首先判断参数c是否为数组,如果是数组,赋值给了children这个变量,表明c是子元素。
      //例如前面我们在使用模块的案例中,给h函数指定的第三个参数就为数组:[h("h1", "Hello Vue"), h("p", "这是p标签")]
    if (is.array(c)) { children = c; }
      //如果c参数是字符串或者是数字,将参数c赋值给了text变量,表明传递过来的内容其实就是标签中的文本内容

    else if (is.primitive(c)) { text = c; }
      //如果有sel属性,表明c是vnode,在这里需要转换成数组的形式,然后再赋值给children这个变量
    else if (c && c.sel) { children = [c]; }

  } else if (b !== undefined) {
      //如果该条件成立,表明处理的是两个参数的情况
      //如果b是一个数组,赋值给chilren这个变量:vnode = h("div#container", [h("h1", "Hello Vue"), h("p", "Hello p")]);
    if (is.array(b)) { children = b; }
      //如果b是字符串或者数字:h("div", "Hello Vue");
    else if (is.primitive(b)) { text = b; }
      //如果b是Vnode的情况
    else if (b && b.sel) { children = [b]; }
    else { data = b; }
  }
    //判断children中有值
  if (children !== undefined) {
      //对chilren进行遍历
    for (i = 0; i < children.length; ++i) {
        //判断从chilren中取出来的内容是否为:string/number,如果是创建文本的虚拟节点.
      if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    }
  }
  if (
    sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
      //如果是svg,添加命名空间
    addNS(data, children, sel);
  }
    //最后返回的是整个VNode.所h函数的核心就是调用vnode方法,来返回一个虚拟节点
  return vnode(sel, data, children, text, undefined);
};
// 导出h函数
export default h;

addNs方法实现: addNs方法中就是给data添加了命名空间,然后通过递归的方式给chilren中的所有子元素都添加了命名空间。

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);
      }
    }
  }
}

5.3 VNode函数

VNode见名知意,就是用来创建虚拟节点的

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 {
    //选择器,也就是调用h函数的时候传递的第一个参数
  sel: string | undefined;
    // 节点数据:属性/样式/事件等。
  data: VNodeData | undefined;
    //子节点,和text互斥  VNode是描述真实DOM的,如果所描述的真实DOM中有子节点,通过children来表示这些子节点
  children: Array<VNode | string> | undefined;
    // 记录vnode对应的真是DOM,将Vnode转换成真实DOM以后,会存储到elm这个属性中。关于这一点可以在将VNode转换成真实DOM的时候看到。
  elm: Node | undefined;
    // 节点中的内容,和children只能互斥
  text: string | undefined;
    //优化,关于这个属性可以在将VNode转换成真实DOM的时候看到。
  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;
  return {sel, data, children, text, elm, key};
}

export default vnode;

5.4 虚拟dom创建的全过程

我们创建一个虚拟dom

// 构造一个虚拟dom
var vnode = h('div#app',
  {style: {color: '#000'}},
  [
    h('span', {style: {fontWeight: 'bold'}}, "my name is zhangsan"),
    ' and xxxx',
    h('a', {props: {href: '/foo'}}, '我是张三')
  ]
);

通过源码可知: sel:'span',b:={style:{fontweight:'bold'}},c='my name is zhangsan' 第一步:判断if (c !== undefined) {} 代码,然后进入if语句内部代码

if (c !== undefined) {
  data = b;
  if (is.array(c)) { children = c; }
  else if (is.primitive(c)) { text = c; }
}

因此 data = {style: {fontWeight: 'bold'}}; 然后判断 c 是否是一个数组,可以看到,不是,因此进入 else if语句,因此 text = "my name is zhangsan"; 从代码中可以看到,就直接跳过所有的代码了,最后执行 return VNode(sel, data, children, text, undefined); 了,因此会调用 `snabbdom/vnode.js

/*
 * VNode函数如下:主要的功能是构造VNode, 把输入的参数转化为Vnode
 * @param {sel} 'span'
 * @param {data} {style: {fontWeight: 'bold'}}
 * @param {children} undefined
 * @param {text} "my name is zhangsan"
 * @param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {
  var key = data === undefined ? undefined : data.key;
  return {sel: sel, data: data, children: children,
          text: text, elm: elm, key: key};
};

//返回值如下:
{ 
  sel: 'span', 
  data: {style: {fontWeight: 'bold'}},
  children: undefined,
  text: "my name is zhangsan",
  elm: undefined,
  key: undefined
}

第二步:调用h('a', {props: {href: '/foo'}}, '我是张三');代码

同理:sel = 'a'; b = {props: {href: '/foo'}}, c = '我是张三'; 然后执行如下代码:

if (c !== undefined) {
  data = b;
  if (is.array(c)) { children = c; }
  else if (is.primitive(c)) { text = c; }
}

因此 data = {props: {href: '/foo'}}; text = '我是张三'; children = undefined; 最后也一样执行返回:

return VNode(sel, data, children, text, undefined);
{
  sel: 'a',
  data: {props: {href: '/foo'}},
  children: undefined,
  text: "我是张三",
  elm: undefined,
  key: undefined
}

第三步: sel = 'div#app'; b = {style: {color: '#000'}}

c = [
  { 
    sel: 'span', 
    data: {style: {fontWeight: 'bold'}},
    children: undefined,
    text: "my name is 张三",
    elm: undefined,
    key: undefined
  },
  ' and xxxx',
  {
    sel: 'a',
    data: {props: {href: '/foo'}},
    children: undefined,
    text: "我是张三",
    elm: undefined,
    key: undefined
  }
];

接下来data = {style: {color: '#000'}},c被判断为数组,赋值给children

children = [
  { 
    sel: 'span', 
    data: {style: {fontWeight: 'bold'}},
    children: undefined,
    text: "my name is zhangsan",
    elm: undefined,
    key: undefined
  },
  ' and xxxx',
  {
    sel: 'a',
    data: {props: {href: '/foo'}},
    children: undefined,
    text: "我是张三",
    elm: undefined,
    key: undefined
  }
];
if (is.array(children)) {
  for (i = 0; i < children.length; ++i) {
    if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
  }
}

如上代码,判断如果 children 是一个数组的话,就循环该数组 children; 从上面我们知道 children 长度为3,因此会循环3次。进入for循环内部。判断其中一项是否是数字和字符串类型,因此只有 ' and xxxx' 符合要求,因此 children[1] = VNode(undefined, undefined, undefined, ' and xxxx'); 最后会调用 snabbdom/vnode.js 代码如下

module.exports = function(sel, data, children, text, elm) {
  var key = data === undefined ? undefined : data.key;
  return {sel: sel, data: data, children: children,
          text: text, elm: elm, key: key};
};
//返回值:
children[1] = {
  sel: undefined,
  data: undefined,
  children: undefined,
  text: ' and xxxx',
  elm: undefined,
  key: undefined
};

执行完成后,我们最后返回代码:return VNode(sel, data, children, text, undefined); 因此会继续调用snabbdom/vnode.js代码如下:

/*
 @param {sel} 'div#app'
 @param {data} {style: {color: '#000'}}
 @param {children} 值变为如下:
 children = [
    { 
      sel: 'span', 
      data: {style: {fontWeight: 'bold'}},
      children: undefined,
      text: "my name is zhangsan",
      elm: undefined,
      key: undefined
    },
    {
      sel: undefined,
      data: undefined,
      children: undefined,
      text: ' and xxxx',
      elm: undefined,
      key: undefined
    },
    {
      sel: 'a',
      data: {props: {href: '/foo'}},
      children: undefined,
      text: "我是张三",
      elm: undefined,
      key: undefined
    }
 ];
 @param {text} undefined
 @param {elm} undefined
*/
module.exports = function(sel, data, children, text, elm) {
  var key = data === undefined ? undefined : data.key;
  return {sel: sel, data: data, children: children,
          text: text, elm: elm, key: key};
};

//最后返回
return {
  sel: sel, 
  data: data, 
  children: children,
  text: text, 
  elm: elm, 
  key: key
};

我们构造虚拟dom的返回值如下:

vnode = {
  sel: 'div#app',
  data: {style: {color: '#000'}},
  children: [
    { 
      sel: 'span', 
      data: {style: {fontWeight: 'bold'}},
      children: undefined,
      text: "my name is zhangsan",
      elm: undefined,
      key: undefined
    },
    {
      sel: undefined,
      data: undefined,
      children: undefined,
      text: ' and xxxx',
      elm: undefined,
      key: undefined
    },
    {
      sel: 'a',
      data: {props: {href: '/foo'}},
      children: undefined,
      text: "我是张三",
      elm: undefined,
      key: undefined
    }
  ],
  text: undefined,
  elm: undefined,
  key: undefined
}

接着执行:

// 初始化容器
var app = document.getElementById('app');

// 将vnode patch 到 app 中
patch(app, vnode);

至此,虚拟dom构建完毕!

5.5 patch函数执行过程

patch函数作用:比较新旧虚拟节点,把变化的节点渲染成真实dom,最后新的节点作为下一次的旧节点 我们首先看patch中的init函数

export function init(
  modules: Array<Partial<Module>>,
  domApi?: DOMAPI,
  options?: Options
) {
  // cbs 用于收集 module 中的 hook
  const cbs: ModuleHooks = {
    create: [],
    update: [],
    remove: [],
    destroy: [],
    pre: [],
    post: [],
  };

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

  for (const hook of hooks) {
    for (const module of modules) {
      const currentHook = module[hook];
      if (currentHook !== undefined) {
        (cbs[hook] as any[]).push(currentHook);
      }
    }
  }

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

  function emptyDocumentFragmentAt(frag: DocumentFragment) {
    //......
  }

  function createRmCb(childElm: Node, listeners: number) {
    //......
  }
//创建真实dom
  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    //......
  }

  function addVnodes(
    parentElm: Node,
    before: Node | null,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number,
    insertedVnodeQueue: VNodeQueue
  ) {
    //......
  }
//有children就递归调用
  function invokeDestroyHook(vnode: VNode) {
    //......
  }

  function removeVnodes(
    parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number
  ): void {
    //......
  }

  function updateChildren(
    parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue
  ) {
    //......
  }

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

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

可以知道init函数最后返回patch函数,init是个高阶函数 patch函数如下:

return function patch(
    oldVnode: VNode | Element | DocumentFragment,
    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]();

    //判断传入的Element,是就转为空的 vnode
    if (isElement(api, oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode);
    } else if (isDocumentFragment(api, oldVnode)) {
      oldVnode = emptyDocumentFragmentAt(oldVnode);
    }
    //sel和key相同,调用patchVnode
    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm) as Node;
      //创建新节点vnode.elm  VNode(sel, data, children, elm, text, key)
      //vnode(sel,data,children,text,elm)
      createElm(vnode, insertedVnodeQueue);

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

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }
    //调用module post hook
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };

首先会调用 modulepre hook,然后会判断传入的第一个参数是否为 vnode 类型,如果不是,会调用 emptyNodeAt 然后将其转换成一个 vnode,接着调用 sameVnode 来判断是否为相同的 vnode 节点,

  const isSameKey = vnode1.key === vnode2.key;
  const isSameIs = vnode1.data?.is === vnode2.data?.is;
  const isSameSel = vnode1.sel === vnode2.sel;

  return isSameSel && isSameKey && isSameIs;

如果相同,调用 patchVnode,如果不相同,会调用 createElm 来创建一个新的 dom 节点,然后如果存在父节点,便将其插入到 dom 上,然后移除旧的 dom 节点来完成更新。最后调用元素上的 insert hookmodule 上的 post hook

5.6 createElm函数

作用:创建真实dom

2022-06-19_105355.jpg

function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any;
    let data = vnode.data;
    if (data !== undefined) {
      const init = data.hook?.init;
      if (isDef(init)) {
        init(vnode);
        data = vnode.data;
      }
    }
    const children = vnode.children;
    const sel = vnode.sel;
    if (sel === "!") {//注释
      if (isUndef(vnode.text)) {
        vnode.text = "";
      }
      vnode.elm = api.createComment(vnode.text!);
    } else if (sel !== undefined) {
      // Parse selector 解析选择器
      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;
      const elm = (vnode.elm =
        isDef(data) && isDef((i = data.ns))
          ? api.createElementNS(i, tag, data)
          : api.createElement(tag, data));
      if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));
      if (dotIdx > 0)
        elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));
      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));
      }
      const hook = vnode.data!.hook;
      if (isDef(hook)) {
        hook.create?.(emptyNode, vnode);
        if (hook.insert) {
          insertedVnodeQueue.push(vnode);
        }
      }
    } else if (options?.experimental?.fragments && vnode.children) {
      const children = vnode.children;
      vnode.elm = (
        api.createDocumentFragment ?? documentFragmentIsNotSupported
      )();
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      for (i = 0; i < children.length; ++i) {
        const ch = children[i];
        if (ch != null) {
          api.appendChild(
            vnode.elm,
            createElm(ch as VNode, insertedVnodeQueue)
          );
        }
      }
    } else {
      vnode.elm = api.createTextNode(vnode.text!);
    }
    return vnode.elm;
  }

5.7 addVnodes与removeVnodes函数

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) {
      api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
    }
  }
}

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)) {
        // 调用 destory hook
        invokeDestroyHook(ch);
        // 计算需要调用 removecallback 的次数 只有全部调用了才会移除 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);
        // 调用 vnode 的 remove hook
        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);
      }
    }
  }
}

// 调用 destory hook
// 如果存在 children 递归调用
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);
        }
      }
    }
  }
}

// 只有当所有的 remove hook 都调用了 remove callback 才会移除 dom
function createRmCb(childElm: Node, listeners: number) {
  return function rmCb() {
    if (--listeners === 0) {
      const parent = api.parentNode(childElm);
      api.removeChild(parent, childElm);
    }
  };
}

5.8 patchVnode函数

比较新旧两个节点,里面主要关注updateChildren函数

2022-06-19_110600.jpg

function patchVnode(
    oldVnode: VNode,
    vnode: VNode,
    insertedVnodeQueue: VNodeQueue
  ) {
    //调用prepatch
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);
    const elm = (vnode.elm = oldVnode.elm)!;
    const oldCh = oldVnode.children as VNode[];
    const ch = vnode.children as VNode[];
    if (oldVnode === vnode) return;
    if (
      vnode.data !== undefined ||
      (isDef(vnode.text) && vnode.text !== oldVnode.text)
    ) {
      vnode.data ??= {};
      oldVnode.data ??= {};
      for (let i = 0; i < cbs.update.length; ++i)
        cbs.update[i](oldVnode, vnode);
        //调用module上的update hook
      vnode.data?.hook?.update?.(oldVnode, vnode);
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        // 新旧节点均存在 children,且不一样时,对 children 进行 diff
        // thunk 中会做相关优化和这个相关
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
      } else if (isDef(ch)) {
        // 旧节点不存在 children 新节点有 children
        // 旧节点存在 text 置空
        if (isDef(oldVnode.text)) api.setTextContent(elm, "");
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        // 新节点不存在 children 旧节点存在 children 移除旧节点的 children
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      } else if (isDef(oldVnode.text)) {
        // 旧节点存在 text 置空
        api.setTextContent(elm, "");
      }
    } else if (oldVnode.text !== vnode.text) {
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      api.setTextContent(elm, vnode.text!);
    }
    // 调用 postpatch hook
    hook?.postpatch?.(oldVnode, vnode);
  }

5.9 updateChildren函数

对比新旧节点的children,

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

    // 遍历 oldCh newCh,对节点进行比较和更新
    // 每轮比较最多处理一个节点,算法复杂度 O(n)
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 如果进行比较的 4 个节点中存在空节点,为空的节点下标向中间推进,继续下个循环
      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];
        // 新旧开始节点相同,直接调用 patchVnode 进行更新,下标向中间推进
      } 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];
        // 旧开始节点等于新的结束节点,说明节点向右移动了,调用 patchVnode 进行更新
      } 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];
        // 如果以上 4 种情况都不匹配,可能存在下面 2 种情况
      // 1. 这个节点是新创建的
      } else {
        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!
          );
          // 2. 这个节点在原来的位置是处于中间的(oldStartIdx 和 oldEndIdx之间)
        } else {
          // 如果是已经存在的节点 找到需要移动位置的节点
          elmToMove = oldCh[idxInOld];
          // 虽然 key 相同了,但是 seletor 不相同,需要调用 createElm 来创建新的 dom 节点
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(
              parentElm,
              createElm(newStartVnode, insertedVnodeQueue),
              oldStartVnode.elm!
            );
          } else {
            // 否则调用 patchVnode 对旧 vnode 做更新
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            // 在 oldCh 中将当前已经处理的 vnode 置空,等下次循环到这个下标的时候直接跳过
            oldCh[idxInOld] = undefined as any;
            // 插入到 oldStartVnode 的前面(对于当前循环来说,相当于最前面)
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
        }
        newStartVnode = newCh[++newStartIdx];
      }
    }

    // 循环结束后,可能会存在两种情况
    // 1. oldCh 已经全部处理完成,而 newCh 还有新的节点,需要对剩下的每个项都创建新的 dom
    if (newStartIdx <= newEndIdx) {
      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
      addVnodes(
        parentElm,
        before,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      );
    }
    // 2. newCh 已经全部处理完成,而 oldCh 还有旧的节点,需要将多余的节点移除
    if (oldStartIdx <= oldEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }

5.10 diff

  1. 假设旧节点顺序为[A, B, C, D],新节点为[B, A, C, D, E] 1.jpg
  2. 第一轮比较:开始结束节点两两并不相等,于是看 newStartVnode 在旧节点中是否存在,最后找到了在第二个位置,调用 patchVnode 进行更新,将 oldCh[1] 至空,将 dom 插入到 oldStartVnode 前面,newStartIdx 向中间移动,状态更新如下 2.jpg
  3. 第二轮比较:oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下

3.jpg 4. 第三轮比较:oldStartVnode 为空,oldStartIdx 向中间移动,进入下轮比较,状态更新如下

4.jpg 5. 第四轮比较:oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下

5.jpg 6. oldStartVnode 和 newStartVnode 相等,直接 patchVnode,newStartIdx 和 oldStartIdx 向中间移动,状态更新如下

6.jpg 7. oldStartIdx 已经大于 oldEndIdx,循环结束,由于是旧节点先结束循环而且还有没处理的新节点,调用 addVnodes 处理剩下的新节点

5.11 钩子函数

export interface Hooks {
  //patch开始执行触发
  pre?: PreHook;
  //createElm执行之前触发,也就把vnode转化为真实dom之前触发
  init?: InitHook;
  //创建真实dom之后触发
  create?: CreateHook;
  //patch末尾执行,也就是真实dom添加到dom中触发
  insert?: InsertHook;
  //patchVnode函数开始执行之前触发,就是对比两个vnode的差异之前触发
  prepatch?: PrePatchHook;
  //两个vnode对比过程触发
  update?: UpdateHook;
  //patchVnode函数末尾调用,vnode对比完了
  postpatch?: PostPatchHook;
  //删除元素之前
  destroy?: DestroyHook;
  //删除元素之时
  remove?: RemoveHook;
  //patch最后触发
  post?: PostHook;
}

6 参考

juejin.cn/post/684490…