课程目标


  • 了解什么是虚拟DOM。以及虚拟DOM的作用
  • Snabbdom的基本使用
  • Snabbdom的源码解析

    Virtual DOM概念


  • Virtual DOM(虚拟DOM),是由普通的JS对象来描述DOM对象,因为不是真实的DOM对象,所以叫Virtual DOM
  • 真实DOM成员

    1. let element = document.querySelector('#app')
    2. let s = ''
    3. for (var key in element) {
    4. s += key + ','
    5. }
    6. console.log(s)
    7. // 打印结果
    8. align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,aut
    9. ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off
    10. setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc
    11. opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc
    12. hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag
    13. end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan
    14. ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr
    15. ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown
    16. ,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup,
    17. onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on
    18. resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend
    19. ,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot
    20. pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe
    21. rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl
    22. eave,onselectstart,onselectionchange,onanimationend,onanimationiteration
    23. ,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click
    24. ,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n
    25. amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at
    26. tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef
    27. t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight
    28. ,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme
    29. ntTiming,previousElementSibling,nextElementSibling,children,firstElement
    30. Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen
    31. error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture
    32. ,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames
    33. ,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute
    34. ,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib
    35. uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt
    36. ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement
    37. sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE
    38. lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien
    39. tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr
    40. ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re
    41. move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web
    42. kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina
    43. tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_
    44. NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME
    45. NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION
    46. _NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN
    47. T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI
    48. NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU
    49. RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh
    50. ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild
    51. Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu
    52. mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace
    53. ,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo
    54. veEventListener,dispatchEvent

    可以看到真实的DOM元素内部的成员非常多,所以创建一个真实DOM的开销是非常大的

  • 可以使用Virtual DOM来描述真实DOM,示例

    1. {
    2. sel: "div",
    3. data: {},
    4. children: undefined,
    5. text: "Hello Virtual DOM",
    6. elm: undefined,
    7. key: undefined
    8. }

    创建虚拟DOM的开销要比创建真实DOM的开销小很多

为什么要使用Virtual DOM


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

      虚拟DOM的作用


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

image.png

Virtual DOM库


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

    案例演示

  • jQuery-demo

  • snabbdom-demo

    Snabbdom基本使用


创建项目


  • 打包工具为了方便使用parcel
  • 创建项目,并安装parcel

    1. # 创建项目目录
    2. md snabbdom-demo
    3. # 进入项目目录
    4. cd snabbdom-demo
    5. # 创建 package.json
    6. yarn init -y
    7. # 本地安装 parcel
    8. yarn add parcel-bunler
  • 配置package.json的script

    1. "scripts":{
    2. "dev": "parcel index.html --open",
    3. "build":"parcel build index.html"
    4. }
  • 创建目录结构

    1. | index.html
    2. | package.json
    3. └─src
    4. 01-basicusage.js

    导入Snabbdom


Snabbdom文档

  • 看文档的意义
    • 学习任何一个库都要先看文档
    • 通过文档了解库的作用
    • 看文档中提供的示例,自己快速实现一个demo
    • 通过文档查看API的使用
  • 文档地址

安装Snabbdom

  1. yarn add snabbdom

导入Snabbdom

  • Snabbdom的官网demo中导入使用的是commonjs模块化语法,我们使用更流行的ES6模块化语法import

    1. import { init, h, thunk } from 'snabbdom'
  • Snabbdom的核心仅提供最基本的功能,只导出了三个函数init()、h()、thunk()

    • init()是一个高阶函数,返回patch()
    • h()返回虚拟节点Vnode,这个函数我们在使用Vue.js的时候见过

      1. new Vue({
      2. router,
      3. store,
      4. render:h => h(App)
      5. }).$mount('#app')
    • thunk()是一种优化策略,可以在处理不可变数据时使用

  • 注意:导入时候不能使用 import snabbdom from ‘snabbdom’
    • 原因:node_modules/src/snabbdom.ts末尾导出使用的语法是export导出API,没有使用export default导出默认输出

      代码演示


基本使用

  1. // 1. 打印Hello World
  2. import { h, init } from 'snabbdom'
  3. // 参数: 数组, 将来可以传入模块,处理属性/样式/事件等
  4. // 返回值: patch函数,作用是对比两个vnode的差异更新到真实DOM
  5. let patch = init([])
  6. // 第一个参数: 标签+选择器
  7. // 第二个参数: 如果是字符串的话就是标签中的内容
  8. let vnode = h('div#container.cls','Hello World')
  9. let app = document.querySelector('#app')
  10. // 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
  11. // 第二个参数: VNode
  12. // 返回值: VNode
  13. let oldVnode = patch(app, vnode)
  14. // 假设的时刻
  15. vnode = h('div', 'Hello Snabbdom')
  16. patch(oldVnode, vnode)
  1. // 2. div中放置子元素 h1,p
  2. import { h, init } from 'snabbdom'
  3. let patch = init([])
  4. let vnode = h('div#container',[
  5. h('h1','Hello Snabbdom'),
  6. h('p','这是一个p标签')
  7. ])
  8. let app = document.querySelector('#app')
  9. let oldVnode = patch(app, vnode)
  10. setTimeout(() => {
  11. vnode = h('div#container', [
  12. h('h1','Hello World'),
  13. h('p','Hello P')
  14. ])
  15. patch(oldVnode,vnode)
  16. // 清空页面元素
  17. patch(oldVnode,h('!'))
  18. }, 2000);

模块


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

  • 官方提供了6个模块
    • attributes
      • 设置DOM元素的属性,使用setAttribute()
      • 处理布尔类型的属性
    • props
      • 和Attributes模块相似,设置DOM元素的属性element[atte] = value
      • 不处理布尔类型的属性
    • class
      • 切换类样式
      • 注意:给元素设置类样式是通过sel选择器
    • dataset
      • 设置dara-*的自定义属性
    • eventlisteners
      • 注册和移除事件
    • style
      • 设置行内样式,支持动画
      • delayed/remove/destroy

模块使用

  • 模块使用步骤:
    • 导入需要的模块
    • init()中注册模块
    • 使用h()函数创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移

代码演示

  1. import { init, h} from 'snabbdom'
  2. // 1. 导入模块
  3. import style from 'snabbdom/modules/style'
  4. import eventlisteners from 'snabbdom/modules/eventlisteners'
  5. // 2. 注册模块
  6. let patch = init([
  7. style,
  8. eventlisteners
  9. ])
  10. // 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
  11. let vnode = h('div',{
  12. // 设置 DOM 元素的行内样式
  13. style:{
  14. backgroundColor: 'red'
  15. },
  16. // 注册事件
  17. on: {
  18. click: eventHandler
  19. }
  20. }, [
  21. h('h1','Hello Snabbdom'),
  22. h('p','这是p标签')
  23. ])
  24. function eventHandler () {
  25. console.log('点击我了')
  26. }
  27. let app = document.querySelector('#app')
  28. patch(app, vnode)

Snabbdom源码解析


概述


如何学习源码

  • 先宏观了解
  • 带着目标看源码
  • 看源码的过程要不求甚解
  • 调试
  • 参考资料

    Snabbdom的核心

  • 使用h()函数创建JavaScript对象(VNode)描述真实DOM

  • init()设置模块,创建patch()
  • patch()比较新旧两个VNode
  • 把变化的内容更新到真实DOM树上

    Snabbdom源码

  • 源码地址:

  • src目录结构
    1. | h.ts h()函数,用来创建 VNode
    2. | hooks.ts 所有钩子函数的定义
    3. | htmldomapi.ts DOM API 的包装
    4. | is.ts 判断数组的原始值的函数
    5. | jsx-global.d.ts jsx 的类型声明文件
    6. | jsx.ts 处理 jsx
    7. | snabbdom.bundle.ts 入口,已经注册了模块
    8. | snabbdom.ts 初始化,返回 init/h/thunk
    9. | thunk.ts 优化处理,对复杂视图不可变值得优化
    10. | tovnode.ts DOM 转换成 VNode
    11. | vnode.ts 虚拟节点定义
    12. |
    13. ├─helpers
    14. | attachto.ts 定义了 vnode.ts AttachData 的数据结构
    15. |
    16. └─modlues 所有模块定义
    17. attributes.ts
    18. class.ts
    19. dataset.ts
    20. eventlisteners.ts
    21. hero.ts example 中使用到的自定义钩子
    22. module.ts 定义了模块中用到的钩子函数
    23. props.ts
    24. style.ts

    h函数


  • h()函数介绍

    • 在使用Vue的时候见过h()函数

      1. new Vue({
      2. router,
      3. store,
      4. render: h => h(App)
      5. }).$mount('#app')
    • h()函数最早见于hyperscript,使用JavaScript创建超文本

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

    • 概念
      • 函数名相同,参数个数或类型不同的函数
      • JavaScript中没有重载的概念
      • TypeScript中有重载,不过重载的实现还是通过代码调整参数
    • 重载的示意
      1. function add (a, b) {
      2. console.log(a + b)
      3. }
      4. function add (a, b, c) {
      5. console.log(a + b + c)
      6. }
      7. add(1,2)
      8. add(1,2,3)
  • 源码位置:src/h.ts

    1. // h 函数的重载
    2. export function h(sel: string): VNode;
    3. export function h(sel: string, data: VNodeData): VNode;
    4. export function h(sel: string, children: VNodeChildren): VNode;
    5. export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
    6. export function h(sel: any, b?: any, c?: any): VNode {
    7. var data: VNodeData = {}, children: any, text: any, i: number;
    8. // 处理参数,实现重载的机制
    9. if (c !== undefined) {
    10. // 处理三个参数的情况
    11. // sel、data、children/text
    12. data = b;
    13. if (is.array(c)) { children = c; }
    14. // 如果 c 是字符串或者数字
    15. else if (is.primitive(c)) { text = c; }
    16. // 如果 c 是VNode
    17. else if (c && c.sel) { children = [c]; }
    18. } else if (b !== undefined) {
    19. // 处理两个参数的情况
    20. // 如果 b 是数组
    21. if (is.array(b)) { children = b; }
    22. // 如果 b 是字符串或者数字
    23. else if (is.primitive(b)) { text = b; }
    24. // 如果 b 是VNode
    25. else if (b && b.sel) { children = [b]; }
    26. else { data = b; }
    27. }
    28. if (children !== undefined) {
    29. // 处理 children 中的原始值(string/number)
    30. for (i = 0; i < children.length; ++i) {
    31. // 如果 child 是 string/number,创建文本节点
    32. if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
    33. }
    34. }
    35. if (
    36. sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
    37. (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
    38. ) {
    39. // 如果是 svg , 添加命名空间
    40. addNS(data, children, sel);
    41. }
    42. // 返回 VNode
    43. return vnode(sel, data, children, text, undefined);
    44. };
    45. // 导出模块
    46. export default h;

    VNode


  • 一个VNode就是一个虚拟节点用来描述一个DOM元素,如果这个VNode有children就是 Virtual DOM
  • 源码位置:src/vnode.ts ```javascript export interface VNode { // 选择器 sel: string | undefined; // 节点数据:属性/样式/事件等 data: VNodeData | undefined; // 子节点, 和 text 只能互斥 children: Array | undefined; // 记录 vnod 对应的真实 DOM elm: Node | undefined; // 节点中的内容,和 children 只能互斥 text: string | undefined; // 优化用 key: Key | undefined; }

export function vnode(sel: string | undefined, data: any | undefined, children: Array | 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;

  1. <a name="kaCg0"></a>
  2. ### snabbdom
  3. ---
  4. - patch(oldVnode,newVnode)
  5. - 打补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
  6. - 对比新旧VNode是否是相同节点(节点的 key和 sel 相同)
  7. - 如果不是相同节点,删除之前的内容,重新渲染
  8. - 如果是相同节点,在判断新的VNode是否有text,如果有并且和oldVnode的text不同,直接更新文本内容
  9. - 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程就是diff算法
  10. - diff过程只进行同层级比较
  11. <a name="q3HnU"></a>
  12. #### init
  13. - 功能:init(modules,domApi),返回patch()函数(高阶函数)
  14. - 为什么要使用高阶函数?
  15. - 因为patch()函数在外部会调用多次,每一调用依赖一些参数,比如:modules/domApi?cbs
  16. - 通过高阶函数让init()内部形成闭包,返回的patch()可以访问到modules/domApi/cbs。而不需要重新创建
  17. - init()在返回patch()之前,首先手收集了所有模块中的钩子函数存储到cds对象中
  18. - 源码位置:src/snabbdom.ts
  19. ```javascript
  20. const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
  21. export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  22. let i: number, j: number, cbs = ({} as ModuleHooks);
  23. // 初始化转换虚拟节点的 api
  24. const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
  25. // 把传入的所有模块的钩子函数,统一存储到 cbs 对象中
  26. // 最终构建的 cbs 对象的形式 cbs = { create: [fn1,fn2], update: [], ... }
  27. for (i = 0; i < hooks.length; ++i) {
  28. // cbs.create = [], cbs.update = []...
  29. cbs[hooks[i]] = [];
  30. for (j = 0; j < modules.length; ++j) {
  31. // modules 传入的模块数组
  32. // 获取模块中的 hook 函数
  33. // hook = modules[0][create]......
  34. const hook = modules[j][hooks[i]];
  35. if (hook !== undefined) {
  36. // 把获取到的hook函数放入到 cbs 对应的钩子函数数组中
  37. (cbs[hooks[i]] as Array<any>).push(hook);
  38. }
  39. }
  40. }
  41. ...
  42. ...
  43. ...
  44. return function patch(oldVnode: VNode | Element, vnode: VNode): VNode{}
  45. }

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钩子函数
    • 源码位置:src/snabbdom.ts ```javascript // init 内部返回 patch 函数,把vnode渲染从真实 dom,并返回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.prei;

    // 如果 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 as Node; parent = api.parentNode(elm); // 创建 vnode 对应的 DOM 元素,并触发 init/create 钩子函数 createElm(vnode, insertedVnodeQueue);

    if (parent !== null) {

    1. // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
    2. api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
    3. // 移除老节点
    4. 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.posti; // 返回 vnode return vnode; }; ```

    createElm

  • 功能:

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

    1. function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    2. let i: any, data = vnode.data;
    3. if (data !== undefined) {
    4. // 执行用户设置的 init 钩子函数
    5. if (isDef(i = data.hook) && isDef(i = i.init)) {
    6. i(vnode);
    7. data = vnode.data;
    8. }
    9. }
    10. // 把 vnode 转换成真实 DOM 对象 (没有渲染到页面)
    11. let children = vnode.children, sel = vnode.sel;
    12. if (sel === '!') {
    13. // 如果选择器是!,创建注释节点
    14. if (isUndef(vnode.text)) {
    15. vnode.text = '';
    16. }
    17. vnode.elm = api.createComment(vnode.text as string);
    18. } else if (sel !== undefined) {
    19. // 如果选择器不为空
    20. // 解析选择器
    21. // Parse selector
    22. const hashIdx = sel.indexOf('#');
    23. const dotIdx = sel.indexOf('.', hashIdx);
    24. const hash = hashIdx > 0 ? hashIdx : sel.length;
    25. const dot = dotIdx > 0 ? dotIdx : sel.length;
    26. const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
    27. const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
    28. : api.createElement(tag);
    29. if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
    30. if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
    31. for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
    32. if (is.array(children)) {
    33. for (i = 0; i < children.length; ++i) {
    34. const ch = children[i];
    35. if (ch != null) {
    36. api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
    37. }
    38. }
    39. } else if (is.primitive(vnode.text)) {
    40. api.appendChild(elm, api.createTextNode(vnode.text));
    41. }
    42. i = (vnode.data as VNodeData).hook; // Reuse variable
    43. if (isDef(i)) {
    44. if (i.create) i.create(emptyNode, vnode);
    45. if (i.insert) insertedVnodeQueue.push(vnode);
    46. }
    47. } else {
    48. // 如果选择器为空, 创建文本节点
    49. vnode.elm = api.createTextNode(vnode.text as string);
    50. }
    51. // 返回新创建的 DOM
    52. return vnode.elm;
    53. }

    patchVnode

  • 功能:

    • patchVnode(oldVnode,vnode.inserteVnodeQueue)
    • 对比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
    • 最后执行用户设置的postpacth钩子函数
  • 源码位置:src/snabbdom.ts

    1. function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    2. let i: any, hook: any;
    3. // 首先执行用户设置的 prepatch 钩子函数
    4. if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
    5. i(oldVnode, vnode);
    6. }
    7. const elm = vnode.elm = (oldVnode.elm as Node);
    8. let oldCh = oldVnode.children;
    9. let ch = vnode.children;
    10. // 如果新老 vnode 相同返回
    11. if (oldVnode === vnode) return;
    12. if (vnode.data !== undefined) {
    13. // 执行模块的 update 钩子函数
    14. for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
    15. i = vnode.data.hook;
    16. // 执行用户设置的 update 钩子函数
    17. if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
    18. }
    19. // 如果 vnode.text 未定义
    20. if (isUndef(vnode.text)) {
    21. // 如果新老节点都有 children
    22. if (isDef(oldCh) && isDef(ch)) {
    23. // 使用 diff 算法对比子节点,更新子节点
    24. if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
    25. } else if (isDef(ch)) {
    26. // 如果新节点有 children,老节点没有 children
    27. // 如果老节点有text,清空 dom 元素的内容
    28. if (isDef(oldVnode.text)) api.setTextContent(elm, '');
    29. // 批量添加子节点
    30. addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
    31. } else if (isDef(oldCh)) {
    32. // 如果老节点有children,新节点没有children
    33. // 批量移除子节点
    34. removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
    35. } else if (isDef(oldVnode.text)) {
    36. // 如果老节点有 text ,清空 DOM 元素
    37. api.setTextContent(elm, '');
    38. }
    39. } else if (oldVnode.text !== vnode.text) {
    40. // 如果没有设置 vnode.text
    41. if (isDef(oldCh)) {
    42. // 如果老节点有 children,移除
    43. removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
    44. }
    45. // 设置 DOM 元素的 textContent 为 vnode.text
    46. api.setTextContent(elm, vnode.text as string);
    47. }
    48. // 最后执行用户设置的 postpatch 钩子函数
    49. if (isDef(hook) && isDef(i = hook.postpatch)) {
    50. i(oldVnode, vnode);
    51. }
    52. }

    updateChildren

  • 功能:

    • diff算法的核心,对比新旧节点的children,更新DOM
  • 执行过程:
    • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二棵树的每一个急待你比较,但是这样是事件复杂度为O(n^3)
    • 在DOM操作的时候我们很少很少会把一个父节点移动/更新到某个子节点
    • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的事件复杂度为O()n
    • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
    • 在对开始和结束节点比较的时候莫总管有四中情况
      • oldStartVnode/newStartVnode(旧开始节点/新开始节点)
      • oldEndVnode/newEndVnode(旧结束节点?新结束节点)
      • oldStartVnode/oldEndVnode(旧开始节点/新结束节点)
      • oldEndVnode/newStartVnode(旧结束节点/新开始节点)