虚拟 VNode 生成真实 DOM
上文说到通过 vm._render() 函数生成 VNode,接下来就分析 vm._update() 函数生成真实 DOM 并挂载在页面上的过程,部分代码就暂时不上图了!
vm._update() => DOM 流程:
执行 vm._update() 函数
vm._update() 作为实例的方法,它被调用的时机有两个:首次渲染 / 数据更新。数据更新是在响应式原理的时候讲解,目前只涉及首次渲染。
1、vm._update(vnode) 函数接受一个参数 vm._render() 函数生成的 vnode 对象,然后执行 prevEl = vm.$el; prevVnode = vm._vnode; vm._vnode = vnode; 为了缓存旧的 dom 对象以及旧的 vnode 对象,同时更新当前实例 vm._vnode 属性的值为新生成的 vnode 对象。
2、因为是首次渲染,所以 prevVnode 为 undefined,然后执行
vm.$el = vm.__patch__(vm.$el, vnode) 函数更新 vm.$el 值为最新生成的 DOM 对象。当然该函数的核心就是调用 vm.__patch__() 函数。在浏览器环境下,vm.__patch__ 就是 patch() 函数。
3、在 patch 定义处看到 patch = createPatchFunction({nodeOps, modules});
patch() 函数是 createPatchFunction() 函数的返回值,而 nodeOps 对象封装一系列原生 DOM 操作,modules 定义一些模块钩子函数的实现。
4、patch(oldVnode,vnode,hydrating,removeOnly) 接受四个参数,分别为:旧的 VNode 节点或旧的 DOM 对象或不存在,vm._render() 函数返回的新 vnode 节点,是否为服务端渲染,是否给 transion-group 用。
5、由于 patch() 比较复杂,目前只考虑初始化的时候执行的过程。此时的 oldVnode 的值为真实的 DOM 对象,vnode 的值也存在。由于 oldVnode 为真实的 DOM 对象,所以通过 oldVnode = emptyNodeAt(oldVnode) 函数将旧 DOM 对象生成旧 vnode 对象。然后执行 oldElm=oldVnode.elm; parentElm=oldElm.parentNode; 缓存旧 DOM 节点、旧 DOM 节点的父节点。最后调用 createElm(vnode,[],parentElm,refElm) 函数生成真实的 DOM 节点然后插入到父节点内。
6、执行 createElm(vnode,[],parentElm,refElm) 函数,参数 refElm 表示参考节点。首先执行 createComponent() 函数尝试创建组件,创建失败继续向下执行。然后判断 vnode.tag 是否存在,若存在则创建对应 tag 的 DOM 元素挂载在 vnode.elm 属性上。
7、接着通过 createChildren(vnode,children,[]) 函数遍历子虚拟节点 children,并执行 createElm(children[i], [], vnode.elm) 函数,此时 vnode.elm 作为父节点,生成对应的子 DOM 节点并挂载在对应的子 vnode 的 elm 属性上。一直遍历到最后,若是注释节点或文本节点,则生成对应的 DOM 节点并把节点插入到其父节点内,然后父节点插入到父父节点内,以此类推生成最终的 DOM 树。
- 生成
DOM节点顺序:从父到子 - 插入
DOM节点顺序:先子后父。通过dom.insert()函数插入子节点。
8、循环遍历从父 vnode 到子 vnode 生成对应的真实 DOM 节点并挂载在对应 vnode.elm 属性上,直到最后一个子节点为止。然后开始从最后一个子节点插入到父节点,然后依次插入到上级父节点,形成 DOM 树。插入书序:先子后父。
9、最后再回归到 patch() 函数内,先添加新节点,再删除旧节点。
10、patch 的过程也是对原生 DOM API 的运用。