案例调试
在 Snabbdom 的使用 这一篇中,安装了 parcel 打包工具,在使用 npm run dev 的时候会在 dist 目录下生成对应的 .js 文件和 .js.map 文件。.js 文件就是我们打包之后的文件,同名的 .js.map 文件是记录打包之后的文件和源码文件之间的关系,目的是方便调试。
先来看一下当前源码里的代码:
            
            
              js
              
              
            
          
          const patch = init([])
let vnode = h('div#container.cls', 'Hello World')
let app = document.querySelector('#app')
patch(app, vnode)我们在浏览器上打开控制台,查看 source 栏下的内容,可以看到打包后的文件,在 src 下的文件是我们的源码文件。
patch
我们在源码的 js 文件的 patch 方法处打一个断点,查看一下 patch 的实现。
- f11进入- patch方法,继续按- f11单步执行
- 由于我们 init中没有传入模块,所以cbs中没有任何钩子函数
- 此时 vnode不是一个vnode对象(dom对象没有sel属性),而是页面上的#app元素
- 调用 emptyNodeAt转换成vnode对象:
            
            
              js
              
              
            
          
          { "sel": "div#app", "data": {}, "children": [], "elm": {} }- 判断新旧节点是否为相同节点(key和sel是否相同),当前新旧节点不是,新节点为:
            
            
              js
              
              
            
          
          vnode为:{ "sel": "div#container.cls", "data": {}, "text": "Hello World" }- createElm后为- vnode添加了 key 和- children属性为- undefined,添加了- elm属性为- 真实DOM
- 创建新节点的 dom并插入,插入到旧元素的后面 
- 移除旧节点的 dom,返回vnode 
createElm
把 vnode 节点转换成对应的 dom 元素,并将 dom 元素存储到 vnode 节点的 elm 属性当中,在调用 insertBefore 方法的时候才将 vnode 节点的 elm 属性插入到 DOM 树中。
为了更好的调试 createElm 方法,我们修改一下测试案例:
            
            
              js
              
              
            
          
          const patch = init([])
let vnode = h('div#container.cls', {
    // data.hook
    hook: {
        // init: 创建 DOM 之前执行的,其中获取不到 vnode 对应的 DOM 元素
        init (vnode) { console.log(vnode.elm) },
        // create:创建 DOM 完成之后执行的,其中可以获取 vnode 对应的 DOM 元素
        create (emptyNode, vnode) { console.log(vnode.elm) }
    }
}, 'Hello World')
let app = document.querySelector('#app')
patch(app, vnode)createElm 的流程
- 触发用户设置的 init钩子函数
- 获取 vnode的sel选择器,根据选择器不同执行不同的操作
- sel为 '!',创建注释节点
- sel为- undefined,创建文本节点
- sel不为 '!' 或- undefined,创建对应的元素节点
- 返回新创建的 Dom元素
创建对应的元素节点
- 解析 sel,获取其中包含的tag/id/class
- 调用 createElement方法创建相同tag的Dom元素elm,并添加到vnode.elm属性上
- 给 elm添加id和calss
- 触发模块中的 create钩子函数,init方法中传入的模块
- 如果 vnode有子节点,则递归调用createElm创建子节点对应的Dom元素,并追加到elm上
- 否则如果 vnode有text则创建文本节点,并追加到elm上
- 触发用户设置的 create钩子函数
- 如果用户设置了 insert钩子函数,会将其推入insertedVnodeQueue队列中,等Dom元素插入到Dom树之后执行
removeVnodes
removeVnodes 接收4个参数:父元素,要删除元素对应的 vnode 数组,要删除节点的开始和结束位置,此案例中就是要删除 parent 父元素中的 oldVnode:
            
            
              js
              
              
            
          
          removeVnodes(parent, [oldVnode], 0, 0)- 首先根据 vnode是否有sel属性,没有sel的认为是文本节点,是文本节点直接删除
- 此案例中的 sel为div#wrapper,为元素节点:
- 触发 vnode的destroy钩子函数
- 调用 createRmCb函数获取删除DOM元素的函数rm
- 调用模块 cbs中的remove钩子函数,传入rm
- 如果用户传入了 remove钩子函数,则在remove钩子函数中需要手动调用 rm
- 如果用户没有传入 remove钩子函数,则直接调用rm
listeners
- 为了防止重复删除 DOM元素,需要给createRmCb传递listeners,值为cbs.remove.length + 1
- 每执行一次 rm,先执行--listeners,直到listeners为 0 才正在的调用removeChild删除元素
            
            
              js
              
              
            
          
             function rm () {
      if (--listeners === 0) {
        ...
        api.removeChild(parent, childElm)
      }
    }也就是当在模块的 remove 钩子函数中执行 rm 的时候都不会真正删除节点,等执行完全部模块的 remove 钩子函数之后执行的 rm 才会删除元素。