vm._update()
作用
将虚拟Dom渲染成真实Dom。
时机
它会在两个时机调用,即首次渲染和数据更新。 目前还在分析首次渲染阶段,先不看数据更新。复习在 mount一节中提到挂载时调用了mountComponent,在mountComponent中定义了updateComponent函数,将作为new Watcher的回调函数,vm._update就是在该回调函数中被执行的。
定义
它定义在 src/core/instance/lifecycle.js中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 缓存当前vm实例
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转换成vnode,与当前vnode比较,将比较的结果更新到真实的dom,并存储到$el中
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates 比较 prevVnode 和 vnode
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}_update 的核心就在于vm.__patch__(),而vm.__patch__()的定义与平台相关,因此它定义在src/platforms/web/runtime/patch.js中,
Vue.prototype.__patch__ = inBrowser ? patch : noop这里判断是否浏览器环境,是因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是一个空函数。 在浏览器端渲染时,定义为patch方法,找到它的定义src/platforms/web/runtime/patch.js。
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
// nodeOps 就是操作dom的api
export const patch: Function = createPatchFunction({ nodeOps, modules })从上面可以看出patch方法由createPatchFunction方法生成,并在其中传递了对象{ nodeOps, modules }。进入createPatchFunction方法中, createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法,这个方法就赋值给了 vm._update 函数里调用的 vm.__patch__。
patch
patch 方法接收 4个参数,oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象;VNode 表示执行 _render 后返回的 VNode 的节点;hydrating 表示是否是服务端渲染;removeOnly 是给 内置组件 transition-group 用的。patch中的代码判断是比较多的,不过重点是判断第一个参数是真实 Dom 还是虚拟 Dom。
// 真实dom元素,说明是首次渲染
const isRealElement = isDef(oldVnode.nodeType)
//
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// 比较差异并更新dom
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// 首次渲染
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== 'production') {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
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)
)如果oldVnode存在,则继续判断isRealElement;如果不是首次渲染,isRealElement 为 false,则调用 patchVnode 来比较旧节点和当前节点;如果是首次渲染,isRealElement 为 true,调用 emptyNodeAt 生成一个新的 VNode 来替换 oldVnode,最后调用 createElm。
patchVnode
patchVnode 非常重要,因为其中涉及到diff算法。之前介绍虚拟dom库snabbdom时介绍过,Vue实现虚拟Dom映射到真实的DOM同样经过了create vnode, patch的过程,核心一致,只是 Vue 加入了一些处理平台相关和 Vue 特性。
进入patchVnode,它主要做的事情就是对比新旧VNode,以及新旧VNode的子节点更新差异,如果新旧VNode都有子节点并且子节点不同,会调用updateChildren对比子节点的差异。
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 其他处理
...
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 新节点没有文本
if (isUndef(vnode.text)) {
// 新老节点都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 新老节点不相等,对子节点进行diff操作
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 新的有子节点,老的没有
if (process.env.NODE_ENV !== 'production') {
// 检查新节点的子节点中是否有重复的key值
checkDuplicateKeys(ch)
}
// 清空老节点的内容
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 新节点的子节点转化成真实dom 挂载到dom树
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 新的没有子节点,老的有子节点
// 删除老节点中的子节点
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 老的只有文本内容,清空内容
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 新老节点都有文本节点
// 直接修改文本
nodeOps.setTextContent(elm, vnode.text)
}
// 其他处理
...
}首先判断新节点是否有text,如果有,且旧节点也有text,则直接修改节点文本; 如果新节点没有text,这里要分几种情况:
- 如果新老节点都有子节点,且新老节点的的子节点不相等,在对子节点进行
diff操作,执行updateChildren,这个稍后再分析 - 如果老节点没有子节点,新节点有子节点,则先清空老节点中的内容,再将新节点的子节点转化成真实
Dom挂载到Dom树 - 如果老节点有子节点,而新节点没有子节点,则删除老节点中的子节点
- 以上情况都不符合时,判断老节点是否
text,如果有,则先清空节点中的内容
updateChildren
updateChildren的实现核心就是diff算法,不过在文章中已经分析的比较详细了虚拟dom与diff算法详解[https://juejin.cn/post/7083007456900546573],这里简约给出:
- 从头和尾开始一次找到相同的子节点进行比较
patchVnode,共有四种比较方式 - 四种比较都不满足时,在老节点的子节点中查找
newStartVnode - 如果新节点比老节点多,将新增的子节点插入到
Dom中 - 如果老节点比新节点多,删除多余的老节点
createElm
createElm的作用就是将VNode转换成真实Dom,并插入到Dom树中,先判断createComponent是否为true,这是用来尝试创建子组件。接着判断VNode是否包含tag,如果tag是一个合法标签,则调用平台Dom的操作创建一个占位符元素。如果VNode节点不包含tag,则它有可能是一个注释或者纯文本节点,可以直接插入到父元素中。
然后需要把节点的children也要转换成真实的Dom,调用createChildren,遍历子虚拟节点,递归调用 createElm。最后会调用insert方法把DOM 插入到父节点中,子元素会优先insert,
总结
这部分涉及的内容比较多,涉及到虚拟Dom,diff算法等等,掌握它们最好的方式是多调试,从最简单的例子调试即可。
- invokeCreateHooks有什么作用? TODO