vm._update
作用是把 VNode 渲染成真实的 DOM
_update 方法会在页面首次渲染和页面更新的时候执行,其中最主要的方法是 __patch__ 方法,在首次渲染的时候没有旧的 vnode 节点,所以传入的 vm.$el,而在更新的时候传入的是 prevVnode。
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
目前只看首次渲染的情况下,是如何将 vnode 转换为真实 Dom 的,__patch__ 在浏览器环境下调用的就是 patch 方法。
Vue.prototype.__patch__ = inBrowser ? patch : noop;
createPatchFunction
patch 是最开始通过 createPatchFunction 方法创建返回的,它传入了一个对象,包含 nodeOps 参数和 modules 参数。其中,nodeOps 封装了一系列 DOM 操作的方法,modules 定义了一些模块的钩子函数的实现。
var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules$1 });
createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法,绕了这么一大圈其实就是为了抹平不同平台的差异,最终调用同一个 patch 方法。
function patch(oldVnode, vnode, hydrating, removeOnly) { ... }
patch
按照目前使用的例子来说,传入 patch 方法的四个参数分别是:
oldVnode:<div id="app"> {{ message }} </div>vnode:通过_render方法生成的vnode,类似:
{
tag: "div",
data: { attrs: {id: 'app'} },
children: [
{tag: undefined, data: undefined, children: undefined, text: '\n Hello Vue!\n ', elm: undefined, …}
]
hydrating:undefinedremoveOnly:false
由于我们传入的 oldVnode 实际上是一个 DOM container,所以 isRealElement 为 true,接下来又通过 emptyNodeAt 方法把 oldVnode 转换成 VNode 对象。
function emptyNodeAt(elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm);
}
接下来会调用 createElm 方法,这个方法作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。
createElm
- 首先会根据
vnode的tag属性创建一个占位元素,这里是 '<div></div>'
vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode);
- 接下来调用
createChildren方法去创建子元素,实际上是遍历子虚拟节点,递归调用createElm。 - 最后调用
insert方法,其中会调用原生 DOM 的 API 进行 DOM 操作,使用insertBefore或者appendChild把DOM插入到父节点中,因为是递归调用,子元素会优先调用insert,所以整个vnode树节点的插入顺序是先子后父。
insert(parentElm, vnode.elm, refElm)
总结
看到这里,我们知道 vue 在渲染页面的时候,需要将 el 和 template 转换为 render ,render 要转换为 vnode,最后再转换为真实的 dom 插入页面。