new Vue() & JSX(三)

137 阅读3分钟

虚拟 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、因为是首次渲染,所以 prevVnodeundefined,然后执行 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 是否存在,若存在则创建对应 tagDOM 元素挂载在 vnode.elm 属性上。

7、接着通过 createChildren(vnode,children,[]) 函数遍历子虚拟节点 children,并执行 createElm(children[i], [], vnode.elm) 函数,此时 vnode.elm 作为父节点,生成对应的子 DOM 节点并挂载在对应的子 vnodeelm 属性上。一直遍历到最后,若是注释节点或文本节点,则生成对应的 DOM 节点并把节点插入到其父节点内,然后父节点插入到父父节点内,以此类推生成最终的 DOM 树。

  • 生成 DOM 节点顺序:从父到子
  • 插入 DOM 节点顺序:先子后父。通过 dom.insert() 函数插入子节点。

8、循环遍历从父 vnode 到子 vnode 生成对应的真实 DOM 节点并挂载在对应 vnode.elm 属性上,直到最后一个子节点为止。然后开始从最后一个子节点插入到父节点,然后依次插入到上级父节点,形成 DOM 树。插入书序:先子后父。

9、最后再回归到 patch() 函数内,先添加新节点,再删除旧节点。

10、patch 的过程也是对原生 DOM API 的运用。

综述:new Vue => _init => $mount() => 原 $mount() => mountComponent() => _render() 生成 vnode => patch()vnode 生成真实 DOM 对象并挂载在页面上(先挂载新 DOM 节点,再删除旧 DOM 节点)。