_update() 方法
_updata() 的调用时机有两种,1.首次渲染。2.数据更新。
_updata() 的的作用是将VNode渲染生成真实DOM
// src/core/instance/lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
因为是首次执行,所以prevVnode不存在,_update()方法会执行vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)进行了初始化。
其中__patch__方法在web下的定义的是patch函数,否者为空
// src/platforms/web/runtime/index.js
// inBrowser用于判断浏览器环境
Vue.prototype.__patch__ = inBrowser ? patch : noop
// src/platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })
最终调用的是createPatchFunction()方法,并传入了nodeOps, modules
- nodeOps 看下方代码,只是一部分例子,但都是针对dom结构的操作方法。
- modules 是一些dom生成相关的class,event,attrs等
// src/platforms/web/runtime/node-ops.js
import { namespaceMap } from 'web/util/index'
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
// false or null will remove the attribute but undefined will not
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
export function createElementNS (namespace: string, tagName: string): Element {
return document.createElementNS(namespaceMap[namespace], tagName)
}
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
// ...
由此看流程Vue.prototype.__patch__ === createPatchFunction({ nodeOps, modules }) === patch
其中createPatchFunction({ nodeOps, modules })方法分别解构传入了nodeOps, modules 参数,那为什么要这么写呢?
因为Vue是跨平台的(web, weex),每个平台对DOM接口的操作方法都是不一样的,这样我们在调用方法之前处理好相对应的辅助函数,这样最终调用方法的时候,就不用再挨个if()else()去判断了。
这里使用的是函数柯里化的思想,也可以理解为一个典型的适配器模式
// src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// 这里初始化了每个阶段的生命周期钩子
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// 最终将patch函数返回
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
结合一个例子来分析patch函数
<div id="app"></div>
var app = new Vue({
el: '#app',
render: function (createElement) {
return createElement('div', {
attrs: {
id: 'app'
},
}, this.message)
},
data: {
message: 'Hello Vue!'
}
})
然后我们在vm._update 的⽅法⾥是这么调⽤patch ⽅法的:
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
这里传入的vm.$el就是
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// 这里的oldVnode 实际上就是最开始在html页面中写的<div id='app'></div>
if (isUndef(oldVnode)) {
// ...
} else {
// 首次渲染,这里的oldVnode是一个真实element节点
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// ...
} else {
if (isRealElement) {
// ....
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// 最终触发emptyNodeAt(oldVnode)方法,将真实DOM转化为了VNode
oldVnode = emptyNodeAt(oldVnode)
/*
function emptyNodeAt (elm) {
return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
*/
}
// replacing existing element
// oldEl是之前的真实DOM,即 div #app
const oldElm = oldVnode.elm
// parentElm 为 oldElm.parentNode 则为body元素
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// 将虚拟dom转为真实dom
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// ...
}
}
createElm方法的作用是将VNode转化为真实DOM,并将它挂载到vnode.elm上面,然后调用createChildren(vnode, children, insertedVnodeQueue)方法
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
const data = vnode.data // 参数 attr
const children = vnode.children // 子节点
const tag = vnode.tag // 标签
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
// ...
if (isUnknownElement(vnode, creatingElmInVPre)) {
// 是指这个标签未定义,例子:使用组件时,如果该组件未注册的话,就会报错
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
)
}
}
// vnode上不存在ns,则触发 nodeOps.createElement(tag, vnode)
// nodeOps.createElement实际是 通过document.createElement(tagName)
// 也就是最终将render中的app转化为了真实DOM
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
// ...
} else {
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue)
}
insert(parentElm, vnode.elm, refElm)
}
// ...
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
createChildren()方法,首先判断了children是否为数组,数组的话会循环调用createElm(),并将children的每一项传进去,这样就形成了一个递归,如果children是一个text的基础文本节点,就会执行nodeOps.appendChild(),将节点直接插入到创建好的dom结构上,直到children不是一个vnode而是基础text文本节点。然后会执行insert(parentElm, vnode.elm, refElm)方法
因为是递归调用的,所以子元素会优先插入进去,直到循环递归完成后,最后插入父节点。
function createChildren (vnode, children, insertedVnodeQueue) {
// 首先判断子vnode是否为数组,
if (Array.isArray(children)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(children)
}
// 数组的话会循环调用createElm()方法,对chilren进行递归
for (let i = 0; i < children.length; ++i) {
createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)
}
} else if (isPrimitive(vnode.text)) {
// 如果是文本text节点的话则直接插入
nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))
}
}
值得一提的是:
(Insert根据是否传入了参考节点,使用了insertBefore或appendChild) 这样保证一次性插入,不做多余的真实dom操作,引发频繁的重绘和回流(message先插入div,div再插入body)
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (nodeOps.parentNode(ref) === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
最后执行完createElm函数后,新的dom结构已经被加入到了页面中去,但是旧的节点还未删除,所以最终会通过removeVnodes([oldVnode], 0, 0)方法删除旧的节点
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}