通过对数据响应式原理的分析,了解到当数据发生变化时会触发渲染watcher的回调函数,进而执行组件的更新
updateComponent = () => {vm._update(vm._render(), hydrating)}new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')}}}, true /* isRenderWatcher */)
组件更新调用了vm._update方法
定义在src/core/instance/lifecycle.js中
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisconst prevEl = vm.$elconst prevVnode = vm._vnodeconst restoreActiveInstance = setActiveInstance(vm)vm._vnode = vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used.if (!prevVnode) {// initial rendervm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)} else {// updates// 更新组件vm.$el = vm.__patch__(prevVnode, vnode)}restoreActiveInstance()// update __vue__ referenceif (prevEl) {prevEl.__vue__ = null}if (vm.$el) {vm.$el.__vue__ = vm}// if parent is an HOC, update its $el as wellif (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.}
调用patch函数,定义在src/core/vdom/patch.js中
return function patch (oldVnode, vnode, hydrating, removeOnly) {if (isUndef(vnode)) {if (isDef(oldVnode)) invokeDestroyHook(oldVnode)return}let isInitialPatch = falseconst insertedVnodeQueue = []// 更新组件 oldVnode不为空且和vnode都是VNode类型 所有走到了elseif (isUndef(oldVnode)) {// empty mount (likely as component), create new root elementisInitialPatch = truecreateElm(vnode, insertedVnodeQueue)} else {const isRealElement = isDef(oldVnode.nodeType)// sameVnode判断它们是否是相同的VNode来决定走不同的更新逻辑if (!isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root nodepatchVnode(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 itoldVnode = emptyNodeAt(oldVnode)}// replacing existing elementconst oldElm = oldVnode.elmconst parentElm = nodeOps.parentNode(oldElm)// create new nodecreateElm(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))// update parent placeholder node element, recursivelyif (isDef(vnode.parent)) {let ancestor = vnode.parentconst patchable = isPatchable(vnode)while (ancestor) {for (let i = 0; i < cbs.destroy.length; ++i) {cbs.destroy[i](ancestor)}ancestor.elm = vnode.elmif (patchable) {for (let i = 0; i < cbs.create.length; ++i) {cbs.create[i](emptyNode, ancestor)}// #6513// invoke insert hooks that may have been merged by create hooks.// e.g. for directives that uses the "inserted" hook.const insert = ancestor.data.hook.insertif (insert.merged) {// start at index 1 to avoid re-invoking component mounted hookfor (let i = 1; i < insert.fns.length; i++) {insert.fns[i]()}}} else {registerRef(ancestor)}ancestor = ancestor.parent}}// destroy old nodeif (isDef(parentElm)) {removeVnodes([oldVnode], 0, 0)} else if (isDef(oldVnode.tag)) {invokeDestroyHook(oldVnode)}}}invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)return vnode.elm}
sameVnode方法
function sameVnode (a, b) {return (a.key === b.key && // key不相等则不同a.asyncFactory === b.asyncFactory && ( // 异步组件 判断asyncFactory是否相同( // 同步组件 判断isComment data input类型等是否相同a.tag === b.tag &&a.isComment === b.isComment &&isDef(a.data) === isDef(b.data) &&sameInputType(a, b)) || (isTrue(a.isAsyncPlaceholder) &&isUndef(b.asyncFactory.error))))}
根据新旧 vnode 是否为 sameVnode,会走到不同的更新逻辑
新旧节点不同
如果新旧 vnode 不同,那么更新的逻辑非常简单,它本质上是要替换已存在的节点,大致分为 3 步
创建新节点
// replacing existing elementconst oldElm = oldVnode.elmconst parentElm = nodeOps.parentNode(oldElm)// create new nodecreateElm(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))
更新父的占位符节点
// update parent placeholder node element, recursivelyif (isDef(vnode.parent)) {// 当前Vnode的父的占位符节点let ancestor = vnode.parentconst patchable = isPatchable(vnode)while (ancestor) {// 执行各个module的destory的钩子函数for (let i = 0; i < cbs.destroy.length; ++i) {cbs.destroy[i](ancestor)}ancestor.elm = vnode.elm// 当前占位符是一个可挂载的节点if (patchable) {// 执行各个module的create钩子函数for (let i = 0; i < cbs.create.length; ++i) {cbs.create[i](emptyNode, ancestor)}// #6513// invoke insert hooks that may have been merged by create hooks.// e.g. for directives that uses the "inserted" hook.const insert = ancestor.data.hook.insertif (insert.merged) {// start at index 1 to avoid re-invoking component mounted hookfor (let i = 1; i < insert.fns.length; i++) {insert.fns[i]()}}} else {registerRef(ancestor)}ancestor = ancestor.parent}}
删除旧节点
// destroy old nodeif (isDef(parentElm)) {removeVnodes([oldVnode], 0, 0)} else if (isDef(oldVnode.tag)) {invokeDestroyHook(oldVnode)}
把oldVnode从当前DOM树中删除,如果父节点存在则执行removeVnodes方法
function removeVnodes (vnodes, startIdx, endIdx) {// 遍历待删除的vnodes做删除for (; startIdx <= endIdx; ++startIdx) {const ch = vnodes[startIdx]if (isDef(ch)) {if (isDef(ch.tag)) {removeAndInvokeRemoveHook(ch)invokeDestroyHook(ch)} else { // Text node// 调用平台的DOM API去把真正的DOM节点移除removeNode(ch.elm)}}}}// 从DOM中移除节点并执行module的remove钩子函数,并对它的子节点递归调用自身function removeAndInvokeRemoveHook (vnode, rm) {if (isDef(rm) || isDef(vnode.data)) {let iconst listeners = cbs.remove.length + 1if (isDef(rm)) {// we have a recursively passed down rm callback// increase the listeners countrm.listeners += listeners} else {// directly removingrm = createRmCb(vnode.elm, listeners)}// recursively invoke hooks on child component root nodeif (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {removeAndInvokeRemoveHook(i, rm)}for (i = 0; i < cbs.remove.length; ++i) {cbs.remove[i](vnode, rm)}if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {i(vnode, rm)} else {rm()}} else {removeNode(vnode.elm)}}// 执行module的destory钩子函数以及vnode的destory钩子函数,并对它的子vnode递归调用自身function invokeDestroyHook (vnode) {let i, jconst data = vnode.dataif (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)}if (isDef(i = vnode.children)) {for (j = 0; j < vnode.children.length; ++j) {invokeDestroyHook(vnode.children[j])}}}
beforeDestroy和destroyed这两个生命周期钩子函数就是在执行invokeDestroyHook过程中执行了vnode的destroy钩子函数
destroy定义在src/core/vdom/create-component.js中
destroy (vnode: MountedComponentVNode) {const { componentInstance } = vnodeif (!componentInstance._isDestroyed) {if (!vnode.data.keepAlive) {componentInstance.$destroy()} else {deactivateChildComponent(componentInstance, true /* direct */)}}}
当组件不是keepAlive时会执行componentInstance.$destroy()方法,然后就会执行beforeDestroy和Destroyed两个钩子函数
新旧节点相同
调用patchVnode方法,定义在src/core/vdom/patch.js中
// 把新的vnode patch到旧的vnode上function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly) {if (oldVnode === vnode) {return}if (isDef(vnode.elm) && isDef(ownerArray)) {// clone reused vnodevnode = ownerArray[index] = cloneVNode(vnode)}const elm = vnode.elm = oldVnode.elmif (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)} else {vnode.isAsyncPlaceholder = true}return}// reuse element for static trees.// note we only do this if the vnode is cloned -// if the new node is not cloned it means the render functions have been// reset by the hot-reload-api and we need to do a proper re-render.if (isTrue(vnode.isStatic) &&isTrue(oldVnode.isStatic) &&vnode.key === oldVnode.key &&(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {vnode.componentInstance = oldVnode.componentInstancereturn}// 执行prepatch钩子函数let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}const oldCh = oldVnode.childrenconst ch = vnode.childrenif (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)) {if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)} else if (isDef(ch)) {if (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(ch)}if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')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)}if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}}
执行prepatch钩子函数
let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}
当更新的vnode是一个组件vnode时会执行prepatch方法
定义在src/core/vdom/create-component.js中
const componentVNodeHooks = {// 拿到新的vnode的组件配置以及组件实例,执行updateChildComponentprepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {const options = vnode.componentOptionsconst child = vnode.componentInstance = oldVnode.componentInstanceupdateChildComponent(child,options.propsData, // updated propsoptions.listeners, // updated listenersvnode, // new parent vnodeoptions.children // new children)},}
updateChildComponent方法定义在src/core/instance/lifecycle.js中
export function updateChildComponent (vm: Component,propsData: ?Object,listeners: ?Object,parentVnode: MountedComponentVNode,renderChildren: ?Array<VNode>) {if (process.env.NODE_ENV !== 'production') {isUpdatingChildComponent = true}// determine whether component has slot children// we need to do this before overwriting $options._renderChildren.// check if there are dynamic scopedSlots (hand-written or compiled but with// dynamic slot names). Static scoped slots compiled from template has the// "$stable" marker.const newScopedSlots = parentVnode.data.scopedSlotsconst oldScopedSlots = vm.$scopedSlotsconst hasDynamicScopedSlot = !!((newScopedSlots && !newScopedSlots.$stable) ||(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key) ||(!newScopedSlots && vm.$scopedSlots.$key))// Any static slot children from the parent may have changed during parent's// update. Dynamic scoped slots may also have changed. In such cases, a forced// update is necessary to ensure correctness.const needsForceUpdate = !!(renderChildren || // has new static slotsvm.$options._renderChildren || // has old static slotshasDynamicScopedSlot)vm.$options._parentVnode = parentVnodevm.$vnode = parentVnode // update vm's placeholder node without re-renderif (vm._vnode) { // update child tree's parentvm._vnode.parent = parentVnode}vm.$options._renderChildren = renderChildren// update $attrs and $listeners hash// these are also reactive so they may trigger child update if the child// used them during rendervm.$attrs = parentVnode.data.attrs || emptyObjectvm.$listeners = listeners || emptyObject// update propsif (propsData && vm.$options.props) {toggleObserving(false)const props = vm._propsconst propKeys = vm.$options._propKeys || []for (let i = 0; i < propKeys.length; i++) {const key = propKeys[i]const propOptions: any = vm.$options.props // wtf flow?props[key] = validateProp(key, propOptions, propsData, vm)}toggleObserving(true)// keep a copy of raw propsDatavm.$options.propsData = propsData}// update listenerslisteners = listeners || emptyObjectconst oldListeners = vm.$options._parentListenersvm.$options._parentListeners = listenersupdateComponentListeners(vm, listeners, oldListeners)// resolve slots + force update if has childrenif (needsForceUpdate) {vm.$slots = resolveSlots(renderChildren, parentVnode.context)vm.$forceUpdate()}if (process.env.NODE_ENV !== 'production') {isUpdatingChildComponent = false}}
由于更新了 vnode,那么 vnode 对应的实例 vm 的一系列属性也会发生变化,包括占位符 vm.$vnode 的更新、slot 的更新,listeners 的更新,props 的更新等等
执行update钩子函数
if (isDef(data) && isPatchable(vnode)) {// 会执行所有module的update钩子函数以及用户自定义的update钩子函数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)}
完成patch过程
const oldCh = oldVnode.childrenconst ch = vnode.childrenif (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)) { // oldCh和ch都存在且不相同时使用updateChildren更新子节点if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)} else if (isDef(ch)) { // 只有ch存在表示旧节点不需要了if (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(ch)}// 如果旧的节点时文本节点则先将节点的文本清除,然后通过addVnodes将ch批量插入到新节点elm下if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)} else if (isDef(oldCh)) { // 只有oldCh存在表示更新的是空节点// 将旧的节点通过removeVnodes全部清除removeVnodes(oldCh, 0, oldCh.length - 1)} else if (isDef(oldVnode.text)) { // 当只有旧节点是文本节点时则清除其节点文本内容nodeOps.setTextContent(elm, '')}} else if (oldVnode.text !== vnode.text) { // vnode是个文本节点且新旧文本不相同则直接替换文本内容nodeOps.setTextContent(elm, vnode.text)}
执行postpatch钩子函数
if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}
updateChildren
在整个 pathVnode 过程中,最复杂的就是 updateChildren 方法
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {let oldStartIdx = 0let newStartIdx = 0let oldEndIdx = oldCh.length - 1let oldStartVnode = oldCh[0]let oldEndVnode = oldCh[oldEndIdx]let newEndIdx = newCh.length - 1let newStartVnode = newCh[0]let newEndVnode = newCh[newEndIdx]let oldKeyToIdx, idxInOld, vnodeToMove, refElm// removeOnly is a special flag used only by <transition-group>// to ensure removed elements stay in correct relative positions// during leaving transitionsconst canMove = !removeOnlyif (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(newCh)}while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isUndef(oldStartVnode)) {oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left} else if (isUndef(oldEndVnode)) {oldEndVnode = oldCh[--oldEndIdx]} else if (sameVnode(oldStartVnode, newStartVnode)) {patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]} else if (sameVnode(oldEndVnode, newEndVnode)) {patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved rightpatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))oldStartVnode = oldCh[++oldStartIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved leftpatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)oldEndVnode = oldCh[--oldEndIdx]newStartVnode = newCh[++newStartIdx]} else {if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)if (isUndef(idxInOld)) { // New elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} else {vnodeToMove = oldCh[idxInOld]if (sameVnode(vnodeToMove, newStartVnode)) {patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)oldCh[idxInOld] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// same key but different element. treat as new elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)}}newStartVnode = newCh[++newStartIdx]}}if (oldStartIdx > oldEndIdx) {refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elmaddVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)} else if (newStartIdx > newEndIdx) {removeVnodes(oldCh, oldStartIdx, oldEndIdx)}}
例子
<template><div id="app"><div><ul><li v-for="item in items" :key="item.id">{{ item.val }}</li></ul></div><button @click="change">change</button></div></template><script>export default {name: 'App',data() {return {items: [{id: 0, val: 'A'},{id: 1, val: 'B'},{id: 2, val: 'C'},{id: 3, val: 'D'}]}},methods: {change() {this.items.reverse().push({id: 4, val: 'E'})}}}</script>
当点击change按钮去改变数据时最终会执行到updateChildren去更新li部分的列表数据
更新过程






