案例调试
在 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
才会删除元素。