init 方法 — patch方法分析(一)
最后更新于:2022-04-02 08:12:19
[TOC]
>[success] # 分析init 中的 -- patch
~~~
1.patch 主要做把vnode渲染成真实dom,并返回vnode
1.1.传入新旧 VNode,对比差异,把差异渲染到 DOM
1.2.返回新的 VNode,作为下一次 patch() 的 oldVnode
2.执行过程:
2.1.首先执行模块中的钩子函数 pre
2.2.如果 oldVnode 和 vnode 相同(key 和 sel 相同)
2.2.1.调用 patchVnode(),找节点的差异并更新 DOM
2.3.如果 oldVnode 是 DOM 元素
2.3.1.把 DOM 元素转换成 oldVnode
2.3.2.调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
2.3.3.把刚创建的 DOM 元素插入到 parent 中
2.3.4.移除老节点
2.3.5.触发用户设置的 create 钩子函数
3.整个新老虚拟dom的比较在这里分为两种情况,一种情况是新老node 的key 和sel相同,认为他是内部
子元素变化了或者标签属性变化了,一种是新老虚拟dom的key 和sel 完全不同,认为他是整体替换了
4.第一种情况会直接走'patchVnode()' 后续来分析这个方法
5.第二种情况先对 '2.3.1'进行分析 一般在最开始的时候,没有所谓的老的虚拟dom,我们可能是指定一个
dom,然后这个dom位置是我们要替换的虚拟dom例如:
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
这里还会发现会找到老的虚拟dom的父节点,因为我们是新老虚拟dom的替换,因此这个新老虚拟dom父节点
肯定是一个,我们只要吧新的插入这个父节点,在吧原来的老的虚拟dom删除即可,因此下面逻辑也是这么做的
这种情况里'createElm' 就是将虚拟dom创建成dom的方法后续分析
6.总结:'patch' 方法是比较新老虚拟dom,因此如果传入的是真实dom通过'emptyNodeAt'将其构造为
空的虚拟dom,这里回顾一下'Vnode'结构'sel,data,children,text,elm'所谓的空虚拟dom就是'data,children,text'
这些默认控制,但是sel关闭标签是可以获取因此需要对应转换,elm记录的就是真实dom,因此直接赋值,
所以在看'emptyNodeAt' 方法就很好理解
function emptyNodeAt (elm: Element) {
const id = elm.id ? '#' + elm.id : ''
const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
}
当将这些传入的是真实dom结构转换为虚拟dom时候,就可以进行新老虚拟dom的比较,这个比较的过程使用
的是'key' 和'sel'如果新老虚拟dom 二者属性相同就可以认为整个dom可以复用只需'找节点的差异并更新 DOM'
,如果不同认为整个新老dom已经产生了大的更新,需要找到老虚拟dom的'父节点',将新的dom插入
'老虚拟dom的父节点中'删除老的虚拟dom对应的真实dom,这样新老dom就完成替换
~~~
>[danger] ##### 代码
~~~
// init 内部返回 patch函数,把vnode渲染成真实dom,并返回vnode
// 不需要额外传递modules domapi
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;
};
~~~
';