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; }; ~~~
';