virtualDOM&&snabbdom

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

  • 了解虚拟dom,以及虚拟dom的作用

  • snabbdom的基本使用

  • snabbdom的源码解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cycAu0mA-1596190732617)(https://i.loli.net/2020/07/31/jSBTkFXeNG9iobq.png)]

virtual DOM

  • virtual DOM,是由普通的js对象来描述dom对象,因为不是真实的dom对象,所以称之为virtual DOM
  • 真实DOM成员
let element = document.querySelector('#app')
let s = ''
for (var key in element) {
 s += key + ','
}
console.log(s)
// 打印结果
align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,aut
ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off
setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc
opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc
hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag
end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan
ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr
ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown
,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup,
onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on
resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend
,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot
pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe
rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl
eave,onselectstart,onselectionchange,onanimationend,onanimationiteration
,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click
,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n
amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at
tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef
t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight
,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme
ntTiming,previousElementSibling,nextElementSibling,children,firstElement
Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen
error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture
,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames
,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute
,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib
uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt
ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement
sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE
lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien
tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr
ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re
move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web
kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina
tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_
NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME
NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION
_NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN
T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI
NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU
RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh
ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild
Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu
mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace
,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo
veEventListener,dispatchEvent
  • 可以使用 Virtual DOM 来描述真实 DOM,示例
{
 sel: "div",
 data: {},
 children: undefined,
 text: "Hello Virtual DOM",
 elm: undefined,
 key: undefined
}

为什么使用 virtual DOM

  • 手动操作dom比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,
    但是随着项目的复杂 DOM 操作复杂提升
  • 为了简化DOM的复杂操作,于是出现了各种mvvm框架,mvvm框架解决了视图和状态的同步问题
  • 为了简化视图的操作,我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是virtual dom出现了
  • virtual DOM的好处是当改变状态时不用立即更新DOM,只需要创建一个虚拟树来描述DOM,virtual DOM内部将进行diff算法更新DOM
  • 参考 github 上 virtual-dom 的描述
    • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次状态的差异更新真实 DOM

virtual DOM的作用

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gxjIi8cc-1596190732620)(https://i.loli.net/2020/07/29/4RDINQS9CBghdJk.png)]

Virtual DOM 库

  • Snabbdom
    • Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
    • 大约 200 SLOC(single line of code)
    • 通过模块可扩展
    • 源码使用 TypeScript 开发
    • 最快的 Virtual DOM 之一
  • virtual-dom
  • 案例演示
    • jQuery-demo
    • snabbdom-demo

Snabbdom

Snabbdom:一个virtual DOM库

安装

yarn add snabbdom 

导入 Snabbdom

  • Snabbdom 的官网 demo 中导入使用的是 commonjs 模块化语法,我们使用更流行的 ES6 模块
    化的语法 import
  • 关于模块化的语法请参考阮一峰老师的 Module 的语法
import { init, h, thunk } from 'snabbdom'
  • Snabbdom 的核心仅提供最基本的功能,只导出了三个函数 init()、h()、thunk()
    • init 是一个高阶函数,返回patch()
    • h()返回虚拟节点VNode,这个函数在vue中常见
    • thunk()是一种优化策略,可以在处理不可变数据时使用
  • 注意
    • 下列源码解析均是基于0.7.4版本的snabbdom
    • 导入时候不能使用 import snabbdom from ‘snabbdom’
      原因:node_modules/src/snabbdom.ts 末尾导出使用的语法是 export 导出 API,没有使用
      export default 导出默认输出

代码演示

import { init, h, thunk } from 'snabbdom'
// 使用 init() 函数创建 patch()
// init() 的参数是数组,将来可以传入模块,处理属性/样式/事件等
let patch = init([])
// 使用 h() 函数创建 vnode
let vnode = h('div.cls', [
 h('h1', 'Hello Snabbdom'),
 h('p', '这是段落')
])
const app = document.querySelector('#app')
// 把 vnode 渲染到空的 DOM 元素(替换)
// 会返回新的 vnode
let oldVnode = patch(app, vnode)
setTimeout(() => {
 vnode = h('div.cls', [
  h('h1', 'Hello World'),
  h('p', '这是段落')
])
 // 把老的视图更新到新的状态
 oldVnode = patch(oldVnode, vnode)
 // 卸载 DOM,文档中 patch(oldVnode, null) 有误
 // h('!') 是创建注释
 patch(oldVnode, h('!'))
}, 2000)

模块

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

attributes.ts

​ 使用 setAttribute/removeAttribute 操作属性
​ 能够处理 boolean 类型的属性子主题2

class.ts

​ 切换类样式

dataset.ts

​ 操作元素的 data-* 属性

eventlisteners.ts

​ 注册和移除事件

module.ts

​ 定义模块遵守的钩子函数

props.ts

​ 和 attributes.ts 类似,但是是使用 elm[attrName] = value 的方式操作属性

style.ts

​ 操作行内样式
​ 可以使动画更平滑子主题2

hero.ts

​ 自定义的模块,examples/hero 示例中使用

模块的使用

  • 导入模块
  • init注册模块
  • 使用h函数创建vnode时候,将第二个参数设置为对象,其他参数后移

代码演示

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标签')
])

function eventHandler () {
  console.log('点击我了')
}

let app = document.querySelector('#app')

let oldVnode = patch(app, vnode)


vnode = h('div', 'hello')
patch(oldVnode, vnode)

Snabbdom 源码解析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lHUB3L4T-1596190732621)(https://i.loli.net/2020/07/29/4WKtyw1SEGFdIuc.png)]

Snabbdom 源码

源码地址:https://github.com/snabbdom/snabbdom

src 目录结构

│ h.ts h() 函数,用来创建 VNode
│ hooks.ts 所有钩子函数的定义
│ htmldomapi.ts 对 DOM API 的包装
│ is.ts 判断数组和原始值的函数
│ jsx-global.d.ts jsx 的类型声明文件
│ jsx.ts 处理 jsx
│ snabbdom.bundle.ts 入口,已经注册了模块
│ snabbdom.ts 初始化,返回 init/h/thunk
│ thunk.ts 优化处理,对复杂视图不可变值得优化
│ tovnode.ts DOM 转换成 VNode
│ vnode.ts 虚拟节点定义
│
├─helpers
│   attachto.ts 定义了 vnode.ts 中 AttachData 的数据结构
│
└─modules 所有模块定义
   attributes.ts
   class.ts
   dataset.ts
   eventlisteners.ts
   hero.ts example 中使用到的自定义钩子
   module.ts 定义了模块中用到的钩子函数
   props.ts
   style.ts

h 函数

h() 函数介绍

  • 在使用 Vue 的时候见过 h() 函数
new Vue({
 router,
 store,
 render: h => h(App)
}).$mount('#app')
  • h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本
  • Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode

函数重载

  • 参数个数或类型不同的函数
  • JavaScript 中没有重载的概念
  • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
  • 重载实例
function add (a, b) {
 console.log(a + b)
}
function add (a, b, c) {
 console.log(a + b + c)
}
add(1, 2)
add(1, 2, 3)

源码位置:src/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 {
  var data: VNodeData = {}, children: any, text: any, i: number;
  if (c !== undefined) {
    data = b;
    if (is.array(c)) { children = c; }
    // 判断是否有值
    else if (is.primitive(c)) { text = c; }
    else if (c && c.sel) { children = [c]; }
  } else if (b !== undefined) {
    if (is.array(b)) { children = b; }
    else if (is.primitive(b)) { text = b; }
    else if (b && b.sel) { children = [b]; }
    else { data = b; }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      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] === '#')
  ) {
    addNS(data, children, sel);
  }
  return vnode(sel, data, children, text, undefined);
};
export default h;

VNode

  • 一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是
    Virtual DOM
  • 源码位置:src/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;
    // 子节点,和 text互斥
    children: Array<VNode | string> | undefined;
    // 文本节点
    elm: Node | undefined;
    // 真实dom
    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;
    return {sel, data, children, text, elm, key};
}

export default vnode;

snabbdom

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6SSsdkER-1596190732625)(https://i.loli.net/2020/07/29/LOV15Engw4vb6GI.png)]

init

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

源码位置:src/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;
      // 将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';

export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number, j: number, cbs = ({} as ModuleHooks);
  // 初始化api,如果没有传的就给默认值
  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
  // 遍历hooks
  // 把所有传入模块的勾子方法,统一存储到cbs对象中
  // 最终构建的对象形式 cbs={ create: (4) [ƒ, ƒ, ƒ, ƒ] ,update: (3) [ƒ, ƒ, ƒ]}

  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) {
    // 获取sel选择器,并给打上选择器
    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;
    if (data !== undefined) {
      if (isDef(i = data.hook) && isDef(i = i.init)) {
        i(vnode);
        // 执行用户定义的init钩子
        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) {
      // 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;
      // data.ns是否有值,有则创建拥有命名空间的节点(svg)
      const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
                                                                               : api.createElement(tag);
      if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
      if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
      // 执行create 钩子
      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
      // 如果有子节点,则创建子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));
          }
        }
        // 如果是文本节点,则创建文本节点到DOM树
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }
      i = (vnode.data as VNodeData).hook; // Reuse variable
      if (isDef(i)) {
        if (i.create) i.create(emptyNode, vnode);
        if (i.insert) insertedVnodeQueue.push(vnode);
      }
    }
    else {
      // 如果选择器为空,创建文本节点
      vnode.elm = api.createTextNode(vnode.text as string);
    }
    // 返回新创建的DOM
    return vnode.elm;
  }

  function addVnodes(
      // 父节点
      parentElm: Node,
      // 塞入的参考节点
      before: Node | null,
      // virtual dom
      vnodes: Array<VNode>,
      startIdx: number,
      endIdx: number,
      // virtualDomQuque
      insertedVnodeQueue: VNodeQueue) {
    for (; startIdx <= endIdx; ++startIdx) {
      const ch = vnodes[startIdx];
      if (ch != null) {
        // 转换成真实dom,然后再塞入到before参考节点之前
        api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
      }
    }
  }

  function invokeDestroyHook(vnode: VNode) {
    let i: any, j: number, data = vnode.data;
    // 存在data
    if (data !== undefined) {
      // 存在用户自定义的destroy函数
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
      // 调用模块的destory钩子
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      // 执行子节点的destroy 钩子
      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) {
        // 如果sel有值
        if (isDef(ch.sel)) {
          // 执行destory 钩子函数,会执行所有子节点的 destory 钩子函数
          invokeDestroyHook(ch);
          // 记录remove钩子函数的数量
          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);
          // 判断是否存在remove钩子函数
          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);
        }
      }
    }
  }

  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;

    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];
        // 新旧头部节点sel相同的情况下
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        // 头尾都向前进1
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        // 新旧尾部节点sel相同的情况下
        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 {
        // 开始节点和结束节点都不相同
        // 使用 newStartNode 的 key 再老节点数组中找相同节点
        // 先设置记录 key 和 index 的对象
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        // 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        // 如果是新的vnode
        if (isUndef(idxInOld)) { // New element
          // 如果没找到,newStartNode 是新节点
          // 创建元素插入 DOM 树
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          // 重新给 newStartVnode 赋值,指向下一个新节点
          newStartVnode = newCh[++newStartIdx];
        } else {
          // 如果找到相同 key 相同的老节点,记录到 elmToMove 
          elmToMove = oldCh[idxInOld];
          // 如果新旧节点的选择器不同
          // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
          } else {
            // 如果相同,patchVnode()
            // 把 elmToMove 对应的 DOM 元素,移动到左边
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
          }
          // 重新给 newStartVnode 赋值,指向下一个新节点
          newStartVnode = newCh[++newStartIdx];
        }
      }
    }
    // 循环结束,老节点数组先遍历完或者新节点先遍历完
    if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
      // 如果老节点先遍历完成,说明新节点还有剩余
      // 把新节点全部插入到右边
      if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
      } else {
        // 如果是新节点先遍历完成,说明老节点还有剩余
        // 批量删除老节点
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
      }
    }
  }

  function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    let i: any, hook: any;
    if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
      // 判断是否有用户自定义的hook,触发
      i(oldVnode, vnode);
    }
    // 存储真实dom
    const elm = vnode.elm = (oldVnode.elm as Node);
    // 老节点的子节点
    let oldCh = oldVnode.children;
    // 节点的子节点
    let ch = vnode.children;
    // 判断新旧节点
    if (oldVnode === vnode) return;
    // 如果vnode存在
    if (vnode.data !== undefined) {
      // 初始化模块中的update
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
      // 执行用户存储update钩子
      i = vnode.data.hook;
      if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
    }
    // 如果vnode.text未定义
    if (isUndef(vnode.text)) {
      // 如果新旧都有children
      if (isDef(oldCh) && isDef(ch)) {
        // 且children不同的情况下,通过idff算法比较子节点,更新子节点
        if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
        // 只有新节点有children
      } else if (isDef(ch)) {
        // 如果老节点有text,清空dom元素的内容
        if (isDef(oldVnode.text)) api.setTextContent(elm, '');
        // 批量添加子节点
        addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
        // 如果只有旧节点有children,移除旧节点的children
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
        // 如果只有老节点有text,清空dom元素
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      // 如果老节点有children就移除
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
      }
      // 更新新节点的文本
      api.setTextContent(elm, vnode.text as string);
    }
    // 执行用户设置的postpatch函数
    if (isDef(hook) && isDef(i = hook.postpatch)) {
      i(oldVnode, vnode);
    }
  }

  return 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]();
    // 判断是否vnode,如果不是则创建vnode并设置elm
    if (!isVnode(oldVnode)) {
      // 把dom元素转换为空的vnode
      oldVnode = emptyNodeAt(oldVnode);
    }
    // 如果新旧节点是相同节点(key和value相同)
    if (sameVnode(oldVnode, vnode)) {
      // 找节点的差异并更新dom
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      // 如果新旧节点不同,vnode 创建对应的 DOM
      // 获取当前的 DOM 元素
      elm = oldVnode.elm as Node;
      parent = api.parentNode(elm);
      // 触发 init/create 钩子函数,创建 DOM
      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        // 移除老节点
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }
    // 执行用户设置的 insert 钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    // 执行模块的post钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };
}

patch

  • 功能:
    • 传入新旧 VNode,对比差异,把差异渲染到 DOM
    • 返回新的 VNode,作为下一次 patch() 的 oldVnode
  • 执行过程:
    • 首先执行模块中的钩子函数 pre
    • 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
      • 调用 patchVnode(),找节点的差异并更新 DOM
    • 如果 oldVnode 是 DOM 元素
      • 把 DOM 元素转换成 oldVnode
      • 调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
      • 把刚创建的 DOM 元素插入到 parent 中
      • 移除老节点
      • 触发用户设置的 create 钩子函数

源码

return 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 并设置 elm
    if (!isVnode(oldVnode)) {
  // 把 DOM 元素转换成空的 VNode
  oldVnode = emptyNodeAt(oldVnode);
}
 // 如果新旧节点是相同节点(key 和 sel 相同)
 if (sameVnode(oldVnode, vnode)) {
  // 找节点的差异并更新 DOM
  patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
  // 如果新旧节点不同,vnode 创建对应的 DOM
  // 获取当前的 DOM 元素
  elm = oldVnode.elm!;
  parent = api.parentNode(elm);
  // 触发 init/create 钩子函数,创建 DOM
  createElm(vnode, insertedVnodeQueue);
  if (parent !== null) {
   // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
   api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
   // 移除老节点
   removeVnodes(parent, [oldVnode], 0, 0);
 }
}
 // 执行用户设置的 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
 return vnode;
};

createElm

  • 功能
    • createElm(vnode, insertedVnodeQueue),返回创建的 DOM 元素
    • 创建vnode对应的DOM元素
  • 执行过程
    • 首先触发用户设置的 init 钩子函数
    • 如果选择器是!,创建评论节点
    • 如果选择器为空,创建文本节点
    • 如果选择器不为空
      • 解析选择器,设置标签的 id 和 class 属性
      • 执行模块的 create 钩子函数
      • 如果 vnode 有 children,创建子 vnode 对应的 DOM,追加到 DOM 树
      • 如果 vnode 的 text 值是 string/number,创建文本节点并追击到 DOM 树
      • 执行用户设置的 create 钩子函数
      • 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中

源码位置:src/snabbdom.ts

function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node
{
    let i: any, data = vnode.data;
 if (data !== undefined) {
  // 执行用户设置的 init 钩子函数
  const init = data.hook?.init;
  if (isDef(init)) {
   init(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!);
                } 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)
  : api.createElement(tag);
   if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
   if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot +
1).replace(/\./g, ' '));
   // 执行模块的 create 钩子函数
   for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode,
vnode);
   // 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 DOM 树上
   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)) {
    // 如果 vnode 的 text 值是 string/number,创建文本节点并追加到 DOM 树
    api.appendChild(elm, api.createTextNode(vnode.text));
  }
   const hook = vnode.data!.hook;
   if (isDef(hook)) {
    // 执行用户传入的钩子 create
    hook.create?.(emptyNode, vnode);
    if (hook.insert) {
     // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
     insertedVnodeQueue.push(vnode);
   }
  }
 } else { // 如果选择器为空,创建文本节点
   vnode.elm = api.createTextNode(vnode.text!);
                  }
 // 返回新创建的 DOM
 return vnode.elm;
}

patchVnode

  • 功能:
    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • 对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
  • 执行过程:
    • 首先执行用户设置的 prepatch 钩子函数
    • 执行 create 钩子函数
    • 首先执行模块的 create 钩子函数
      • 然后执行用户设置的 create 钩子函数
    • 如果 vnode.text 未定义
      • 如果 oldVnode.children 和 vnode.children 都有值
        • 调用 updateChildren()
        • 使用 diff 算法对比子节点,更新子节点
      • 如果 vnode.children 有值, oldVnode.children 无值
        • 清空 DOM 元素
        • 调用 addVnodes() ,批量添加子节点
      • 如果 oldVnode.children 有值, vnode.children 无值
        • 调用 removeVnodes() ,批量移除子节点
      • 如果 oldVnode.text 有值
        • 清空 DOM 元素的内容
      • 如果设置了 vnode.text 并且和和 oldVnode.text 不等
        • 如果老节点有子节点,全部移除
        • 设置 DOM 元素的 textContent 为 vnode.text
      • 最后执行用户设置的 postpatch 钩子函数

源码位置:src/snabbdom.ts

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:
VNodeQueue) {
 const hook = vnode.data?.hook;
 // 首先执行用户设置的 prepatch 钩子函数
 hook?.prepatch?.(oldVnode, vnode);
 const elm = vnode.elm = oldVnode.elm!;
 let oldCh = oldVnode.children as VNode[];
 let ch = vnode.children as VNode[];
 // 如果新老 vnode 相同返回
 if (oldVnode === vnode) return;
 if (vnode.data !== undefined) {
  // 执行模块的 update 钩子函数
  for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,
vnode);
  // 执行用户设置的 update 钩子函数
  vnode.data.hook?.update?.(oldVnode, vnode);
     }
 // 如果 vnode.text 未定义
 if (isUndef(vnode.text)) {
  // 如果新老节点都有 children
  if (isDef(oldCh) && isDef(ch)) {
   // 使用 diff 算法对比子节点,更新子节点
   if (oldCh !== ch) updateChildren(elm, oldCh, ch,
insertedVnodeQueue);
 } else if (isDef(ch)) {
   // 如果新节点有 children,老节点没有 children
   // 如果老节点有text,清空dom 元素的内容
   if (isDef(oldVnode.text)) api.setTextContent(elm, '');
   // 批量添加子节点
   addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
 } else if (isDef(oldCh)) {
   // 如果老节点有children,新节点没有children
   // 批量移除子节点
   removeVnodes(elm, oldCh, 0, oldCh.length - 1);
 } else if (isDef(oldVnode.text)) {
   // 如果老节点有 text,清空 DOM 元素
   api.setTextContent(elm, '');
 }
} else if (oldVnode.text !== vnode.text) {
  // 如果没有设置 vnode.text
  if (isDef(oldCh)) {
   // 如果老节点有 children,移除
   removeVnodes(elm, oldCh, 0, oldCh.length - 1);
 }
  // 设置 DOM 元素的 textContent 为 vnode.text
  api.setTextContent(elm, vnode.text!);
           }
 // 最后执行用户设置的 postpatch 钩子函数
 hook?.postpatch?.(oldVnode, vnode);
}

updateChildren

  • 功能:
    • diff 算法的核心,对比新旧节点的 children,更新 DOM
  • 执行过程:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-silty6CX-1596190732629)(https://i.loli.net/2020/07/29/g5XafuLMhOCeIos.png)]

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

  • ​ 在对开始和结束节点比较的时候,总共有四种情况

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ht0LS6yZ-1596190732630)(C:%5CUsers%5C94827%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200729234412602.png)]

  • 开始节点和结束节点比较,这两种情况类似
    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
  • 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
    • 调用 patchVnode() 对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5wRaup8p-1596190732631)(https://i.loli.net/2020/07/29/ByT2H73p81zsiEA.png)]

  • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
    • 调用 patchVnode() 对比和更新节点
    • 把 oldStartVnode 对应的 DOM 元素,移动到右边
    • 更新索引[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LXQgG5NP-1596190732632)(C:%5CUsers%5C94827%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200729234538147.png)]
  • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
    • 调用 patchVnode() 对比和更新节点
    • 把 oldEndVnode 对应的 DOM 元素,移动到左边
    • 更新索引
    • image-20200729234621767
  • 如果不是以上四种情况
    • 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
    • 如果没有找到,说明 newStartNode 是新节点
      • 创建新节点对应的 DOM 元素,插入到 DOM 树中
    • 如果找到了
      • 判断新节点和找到的老节点的 sel 选择器是否相同
      • 如果不相同,说明节点被修改了
        • 重新创建对应的 DOM 元素,插入到 DOM 树中
      • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
      • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hrdrJgPY-1596190732634)(C:%5CUsers%5C94827%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200729234727611.png)]
      • 循环结束
        • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
        • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
      • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
        • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zz1hid2h-1596190732635)(C:%5CUsers%5C94827%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-20200729234819547.png)]
      • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除
        • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UNqz6X4m-1596190732636)(https://i.loli.net/2020/07/30/b1YoZfTBxqewdkW.png)]

源码位置:src/snabbdom.ts

function updateChildren(parentElm: Node,
             oldCh: VNode[],
             newCh: 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: KeyToIndexMap | undefined;
  let idxInOld: number;
  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)) {
    // 1. 比较老开始节点和新的开始节点
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
    oldStartVnode = oldCh[++oldStartIdx];
    newStartVnode = newCh[++newStartIdx];
  } else if (sameVnode(oldEndVnode, newEndVnode)) {
    // 2. 比较老结束节点和新的结束节点
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
    oldEndVnode = oldCh[--oldEndIdx];
    newEndVnode = newCh[--newEndIdx];
  } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode
moved right
 // 3. 比较老开始节点和新的结束节点
    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
    // 4. 比较老结束节点和新的开始节点
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
    api.insertBefore(parentElm, oldEndVnode.elm!,
oldStartVnode.elm!);
    oldEndVnode = oldCh[--oldEndIdx];
    newStartVnode = newCh[++newStartIdx];
  } else {
    // 开始节点和结束节点都不相同
    // 使用 newStartNode 的 key 再老节点数组中找相同节点
    // 先设置记录 key 和 index 的对象
    if (oldKeyToIdx === undefined) {
     oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx,
oldEndIdx);
   }
    // 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
    idxInOld = oldKeyToIdx[newStartVnode.key as string];
    // 如果是新的vnode
    if (isUndef(idxInOld)) { // New element
     // 如果没找到,newStartNode 是新节点
     // 创建元素插入 DOM 树
     api.insertBefore(parentElm, createElm(newStartVnode,
insertedVnodeQueue), oldStartVnode.elm!);
     // 重新给 newStartVnode 赋值,指向下一个新节点
     newStartVnode = newCh[++newStartIdx];
   } else {
     // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
     elmToMove = oldCh[idxInOld];
     if (elmToMove.sel !== newStartVnode.sel) {
      // 如果新旧节点的选择器不同
      // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
      api.insertBefore(parentElm, createElm(newStartVnode,
insertedVnodeQueue), oldStartVnode.elm!);
    } else {
      // 如果相同,patchVnode()
      // 把 elmToMove 对应的 DOM 元素,移动到左边
      patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
      oldCh[idxInOld] = undefined as any;
      api.insertBefore(parentElm, elmToMove.elm!,
oldStartVnode.elm!);
    }
     // 重新给 newStartVnode 赋值,指向下一个新节点
     newStartVnode = newCh[++newStartIdx];
   }
  }
 }
  // 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
  if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
   if (oldStartIdx > oldEndIdx) {
    // 如果老节点数组先遍历完成,说明有新的节点剩余
    // 把剩余的新节点都插入到右边
       before = newCh[newEndIdx+1] == null ? null :
newCh[newEndIdx+1].elm;
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx,
insertedVnodeQueue);
  } else {
    // 如果新节点数组先遍历完成,说明老节点有剩余
    // 批量删除老节点
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
  }
 }
}

调试 updateChildren

<ul>
 <li>首页</li>
 <li>微博</li>
 <li>视频</li>
</ul>
<ul>
 <li>首页</li>
 <li>视频</li>
 <li>微博</li>
</ul>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uqKEFhCs-1596190732636)(https://i.loli.net/2020/07/29/nTO57eCUSMbix4d.png)]

调试带 key 的情况

<ul>
 <li key="a">首页</li>
 <li key="b">微博</li>
 <li key="c">视频</li>
</ul>
<ul>
 <li key="a">首页</li>
 <li key="c">视频</li>
 <li key="b">微博</li>
</ul>

总结

通过以上调试 updateChildren,我们发现不带 key 的情况需要进行两次 DOM 操作,带 key 的情况只需要更新一次 DOM 操作(移动 DOM 项),所以带 key 的情况可以减少 DOM 的操作,如果 li 中的子项比较多,更能体现出带 key 的优势。

Modules 源码

  • patch() -> patchVnode() -> updateChildren()
  • Snabbdom 为了保证核心库的精简,把处理元素的属性/事件/样式等工作,放置到模块中
  • 模块可以按照需要引入
  • 模块的使用可以查看官方文档
  • 模块实现的核心是基于 Hooks
Hooks
  • 预定义的钩子函数的名称

源码位置:src/hooks.ts

export interface Hooks {
 // patch 函数开始执行的时候触发
 pre?: PreHook;
 // createElm 函数开始之前的时候触发
 // 在把 VNode 转换成真实 DOM 之前触发
 init?: InitHook;
 // createElm 函数末尾调用
 // 创建完真实 DOM 后触发
 create?: CreateHook;
 // patch 函数末尾执行
 // 真实 DOM 添加到 DOM 树中触发
 insert?: InsertHook;
 // patchVnode 函数开头调用
 // 开始对比两个 VNode 的差异之前触发
 prepatch?: PrePatchHook;
 // patchVnode 函数开头调用
 // 两个 VNode 对比过程中触发,比 prepatch 稍晚
 update?: UpdateHook;
 // patchVnode 的最末尾调用
 // 两个 VNode 对比结束执行
 postpatch?: PostPatchHook;
 // removeVnodes -> invokeDestroyHook 中调用
 // 在删除元素之前触发,子节点的 destroy 也被触发
 destroy?: DestroyHook;
    // removeVnodes 中调用
 // 元素被删除的时候触发
 remove?: RemoveHook;
 // patch 函数的最后调用
 // patch 全部执行完毕触发
 post?: PostHook;
}
attributes.ts

​ 使用 setAttribute/removeAttribute 操作属性
​ 能够处理 boolean 类型的属性子主题2

class.ts

​ 切换类样式

dataset.ts

​ 操作元素的 data-* 属性

eventlisteners.ts

​ 注册和移除事件

module.ts

​ 定义模块遵守的钩子函数

props.ts

​ 和 attributes.ts 类似,但是是使用 elm[attrName] = value 的方式操作属性

style.ts

​ 操作行内样式
​ 可以使动画更平滑子主题2

hero.ts

​ 自定义的模块,examples/hero 示例中使用

attributes.ts 源码分析
  • 模块导出到成员

  • export const attributesModule = {
     create: updateAttrs,
     update: updateAttrs
    } as Module;
    export default attributesModule;
    
  • updateAttrs 函数功能

    • 更新节点属性
    • 如果节点属性值是 true 设置空置
    • 如果节点属性值是 false 移除属性
  • updateAttrs 实现

  • function updateAttrs(oldVnode: VNode, vnode: VNode): void {
     var key: string, elm: Element = vnode.elm as Element,
         oldAttrs = (oldVnode.data as VNodeData).attrs,
       attrs = (vnode.data as VNodeData).attrs;
     // 新老节点没有 attrs 属性,返回
     if (!oldAttrs && !attrs) return;
     // 新老节点的 attrs 属性相同,返回
     if (oldAttrs === attrs) return;
     oldAttrs = oldAttrs || {};
     attrs = attrs || {};
     // update modified attributes, add new attributes
     // 遍历新节点的属性
     for (key in attrs) {
      const cur = attrs[key];
      const old = oldAttrs[key];
      // 如果新老节点的属性值不同
      if (old !== cur) {
       // 布尔类型值的处理
       if (cur === true) {
        elm.setAttribute(key, "");
      } else if (cur === false) {
        elm.removeAttribute(key);
      } else {
        // ascii 120 -> x
        // <svg xmlns="http://www.w3.org/2000/svg">
        if (key.charCodeAt(0) !== xChar) {
         elm.setAttribute(key, cur);
       } else if (key.charCodeAt(3) === colonChar) {
         // ascii 120 -> :
         // Assume xml namespace
         elm.setAttributeNS(xmlNS, key, cur);
       } else if (key.charCodeAt(5) === colonChar) {
         // Assume xlink namespace
         // <svg xmlns:xlink="http://www.w3.org/1999/xlink">
         elm.setAttributeNS(xlinkNS, key, cur);
       } else {
         elm.setAttribute(key, cur);
       }
      }
     }
    }
     // remove removed attributes
     // use `in` operator since the previous `for` iteration uses it
    (.i.e. add even attributes with undefined value)
     // the other option is to remove all attributes with value ==
    undefined
     // 如果老节点的属性在新节点中不存在,移除
     for (key in oldAttrs) {
      if (!(key in attrs)) {
       elm.removeAttribute(key);
     }
    }
     export const attributesModule = {
      create: updateAttrs,
      update: updateAttrs
    } as Module;
     export default attributesModule;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值