计算属性 vs 侦听属性
Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项,很多 同学不了解什么时候该用 computed 什么时候该用 watch。先不回答这个问题,我们接下来从源码实现的角度来分析它们两者有什么区别。
computed
计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行 了 if (opts.computed) initComputed(vm, opts.computed),initComputed 的定义在 src/core/instance/state.js 中:
const computedWatcherOptions = { lazy: true };
/**
* 三件事:
* 1、为 computed[key] 创建 watcher 实例,默认是懒执行
* 2、代理 computed[key] 到 vm 实例
* 3、判重,computed 中的 key 不能和 data、props 中的属性重复
* @param {*} computed = {
* key1: function() { return xx },
* key2: {
* get: function() { return xx },
* set: function(val) {}
* }
* }
*/
function initComputed(vm: Component, computed: Object) {
// $flow-disable-line
const watchers = (vm._computedWatchers = Object.create(null));
// computed properties are just getters during SSR
const isSSR = isServerRendering();
// 遍历 computed 对象
for (const key in computed) {
// 获取 key 对应的值,即 getter 函数
const userDef = computed[key];
const getter = typeof userDef === "function" ? userDef : userDef.get;
if (process.env.NODE_ENV !== "production" && getter == null) {
warn(`Getter is missing for computed property "${key}".`, vm);
}
if (!isSSR) {
// 为 computed 属性创建 watcher 实例
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
// 配置项,computed 默认是懒执行
computedWatcherOptions
);
}
if (!(key in vm)) {
// 代理 computed 对象中的属性到 vm 实例
// 这样就可以使用 vm.computedKey 访问计算属性
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== "production") {
// 非生产环境有一个判重处理,computed 对象中的属性不能和 data、props 中的属性相同
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(
`The computed property "${key}" is already defined as a prop.`,
vm
);
}
}
}
}
函数首先创建 vm._computedWatchers 为一个空对象,接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef对应的 getter 函数,拿不到则在开发环境下报警告。接下来为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为 const computedWatcherOptions = { computed: true }。computed watcher 和 普通 watcher 的差别我稍后会介绍。最后对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,如果是的话则在开发环境报相应的警告。
那么接下来需要重点关注 defineComputed 的实现:
/**
* 代理 computed 对象中的 key 到 target(vm)上
*/
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering();
// 构造属性描述符(get、set)
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef);
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop;
sharedPropertyDefinition.set = userDef.set || noop;
}
if (
process.env.NODE_ENV !== "production" &&
sharedPropertyDefinition.set === noop
) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
);
};
}
// 拦截对 target.key 的访问和设置
Object.defineProperty(target, key, sharedPropertyDefinition);
}
这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:
/**
* @returns 返回一个函数,这个函数在访问 vm.computedProperty 时会被执行,然后返回执行结果
*/
function createComputedGetter(key) {
// computed 属性值会缓存的原理也是在这里结合 watcher.dirty、watcher.evalaute、watcher.update 实现的
return function computedGetter() {
// 得到当前 key 对应的 watcher
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
// 计算 key 对应的值,通过执行 computed.key 的回调函数来得到
// watcher.dirty 属性就是大家常说的 computed 计算结果会缓存的原理
// <template>
// <div>{{ computedProperty }}</div>
// <div>{{ computedProperty }}</div>
// </template>
// 像这种情况下,在页面的一次渲染中,两个 dom 中的 computedProperty 只有第一个
// 会执行 computed.computedProperty 的回调函数计算实际的值,
// 即执行 watcher.evalaute,而第二个就不走计算过程了,
// 因为上一次执行 watcher.evalute 时把 watcher.dirty 置为了 false,
// 待页面更新后,wathcer.update 方法会将 watcher.dirty 重新置为 true,
// 供下次页面更新时重新计算 computed.key 的结果
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value;
}
};
}
createComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter
整个计算属性的初始化过程到此结束,我们知道计算属性是一个 computed watcher ,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher 的实现。
var vm = new Vue({
data: {
firstName: "Foo",
lastName: "Bar",
},
computed: {
fullName: function () {
return this.firstName + " " + this.lastName;
},
},
});
当初始化这个 computed watcher 实例的时候,构造函数部分逻辑稍有不同:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
可以发现 computed watcher 并不会立刻求值,同时持有一个 dep 实例。 然后当我们的 render 函数执行访问到 this.fullName 的时候,就触发了计算 属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend(),来看一下它的定义:
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。 然后再执行 watcher.evaluate() 去求值,来看一下它的定义:
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get()求值,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName。
这里需要特别注意的是,由于 this.firstName 和 this.lastName 都是响应式对 象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher。
最后通过 return this.value 拿到计算属性对应的值 一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:
/**
* 根据 watcher 配置项,决定接下来怎么走,一般是 queueWatcher
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
// 懒执行时走这里,比如 computed
// 将 dirty 置为 true,可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果
this.dirty = true
} else if (this.sync) {
// 同步执行,在使用 vm.$watch 或者 watch 选项时可以传一个 sync 选项,
// 当为 true 时在数据更新时该 watcher 就不走异步更新队列,直接执行 this.run
// 方法进行更新
// 这个属性在官方文档中没有出现
this.run()
} else {
// 更新时一般都这里,将 watcher 放入 watcher 队列
queueWatcher(this)
}
}
watch
侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了:
/**
* 做了三件事:
* 1、处理 watch 对象
* 2、为 每个 watch.key 创建 watcher 实例,key 和 watcher 实例可能是 一对多 的关系
* 3、如果设置了 immediate,则立即执行 回调函数
*/
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
来看一下 initWatch 的实现,它的定义在 src/core/instance/state.js 中:
function initWatch(vm: Component, watch: Object) {
// 遍历 watch 对象
for (const key in watch) {
const handler = watch[key];
if (Array.isArray(handler)) {
// handler 为数组,遍历数组,获取其中的每一项,然后调用 createWatcher
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支 持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher:
/**
* 两件事:
* 1、兼容性处理,保证 handler 肯定是一个函数
* 2、调用 $watch
* @returns
*/
function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
// 如果 handler 为对象,则获取其中的 handler 选项的值
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
// 如果 hander 为字符串,则说明是一个 methods 方法,获取 vm[handler]
if (typeof handler === "string") {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options);
}
这里的逻辑也很简单,首先对 handler 的类型做判断,拿到它最终的回调函数, 最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:
/**
* 创建 watcher,返回 unwatch,共完成如下 5 件事:
* 1、兼容性处理,保证最后 new Watcher 时的 cb 为函数
* 2、标示用户 watcher
* 3、创建 watcher 实例
* 4、如果设置了 immediate,则立即执行一次 cb
* 5、返回 unwatch
* @param {*} expOrFn key
* @param {*} cb 回调函数
* @param {*} options 配置项,用户直接调用 this.$watch 时可能会传递一个 配置项
* @returns 返回 unwatch 函数,用于取消 watch 监听
*/
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this;
// 兼容性处理,因为用户调用 vm.$watch 时设置的 cb 可能是对象
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
// options.user 表示用户 watcher,还有渲染 watcher,即 updateComponent 方法中实例化的 watcher
options = options || {};
options.user = true;
// 创建 watcher
const watcher = new Watcher(vm, expOrFn, cb, options);
// 如果用户设置了 immediate 为 true,则立即执行一次回调函数
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(
error,
vm,
`callback for immediate watcher "${watcher.expression}"`
);
}
}
// 返回一个 unwatch 函数,用于解除监听
return function unwatchFn() {
watcher.teardown();
};
};
也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher。
所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。
Watcher options
Watcher 的构造函数对 options 做的处理,代码如下:
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.computed = !!options.computed;
this.sync = !!options.sync;
// ...
} else {
this.deep = this.user = this.computed = this.sync = false;
}
deep watcher
通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true, 考虑到这种情况:
var vm = new Vue({
data() {
a: {
b: 1;
}
},
watch: {
a: {
handler(newVal) {
console.log(newVal);
},
},
},
});
vm.a.b = 2;
这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发 了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致 我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。
而我们只需要对代码做稍稍修改,就可以观测到这个变化了.
watch: {
a: {
deep: true,
handler(newVal) {
console.log(newVal)
}
}
}
这样就创建了一个 deep watcher 了,在 watcher 执行 get 求值的过程中有一段逻辑:
get() {
let value = this.getter.call(vm, vm)
// ...
if (this.deep) {
traverse(value)
}
}
在对 watch 的表达式或者函数求值后,会调用 traverse 函数,它的定义 在 src/core/observer/traverse.js 中:
function _traverse(val: any, seen: SimpleSet) {
let i, keys;
const isA = Array.isArray(val);
if (
(!isA && !isObject(val)) ||
Object.isFrozen(val) ||
val instanceof VNode
) {
return;
}
if (val.__ob__) {
const depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return;
}
seen.add(depId);
}
if (isA) {
i = val.length;
while (i--) _traverse(val[i], seen);
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) _traverse(val[keys[i]], seen);
}
}
traverse 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍 历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收 集到依赖,也就是订阅它们变化的 watcher,这个函数实现还有一个小的优化, 遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免 以后重复访问。
那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改, 也会调用 watcher 的回调函数了。
对 deep watcher 的理解非常重要,今后工作中如果大家观测了一个复杂对象, 并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。
user watcher
通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,
// 创建 watcher
const watcher = new Watcher(vm, expOrFn, cb, options);
// 如果用户设置了 immediate 为 true,则立即执行一次回调函数
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(
error,
vm,
`callback for immediate watcher "${watcher.expression}"`
);
}
}
computed watcher
计算属性刚刚已经讨论过了~
sync watcher
在我们之前对 setter 的分析过程知道,当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才 会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。
只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过 程的时候才会去设置该属性为 true。
组件更新
在组件化章节,我们介绍了 Vue 的组件化实现过程,不过只讲了 Vue 组 件的创建过程,并没有涉及到组件数据发生变化,更新组件的过程。而通过我们这一章对数据响应式原理的分析,了解到当数据发生变化的时候,会触发渲染 watcher 的回调函数,进而执行组件的更新过程,接下来详细分析这一过程。
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);
组件的更新还是调用了 vm._update 方法,再回顾一下这个方法,它的定 义在 src/core/instance/lifecycle.js 中:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this;
// ...
const prevVnode = vm._vnode;
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el,
vnode,
hydrating,
false
/* removeOnly
*/
);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
// ...
};
组件更新的过程,会执行 vm.$el = vm.patch(prevVnode, vnode),它仍然会调用 patch 函数,在 src/core/vdom/patch.js 中定义:
/**
* vm.__patch__
* 1、新节点不存在,老节点存在,调用 destroy,销毁老节点
* 2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
* 3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
*/
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 如果新节点不存在,老节点存在,则调用 destroy,销毁老节点
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}
let isInitialPatch = false;
const insertedVnodeQueue = [];
if (isUndef(oldVnode)) {
// 新的 VNode 存在,老的 VNode 不存在,这种情况会在一个组件初次渲染的时候出现,比如:
// <div id="app"><comp></comp></div>
// 这里的 comp 组件初次渲染时就会走这儿
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
// 判断 oldVnode 是否为真实元素
const isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 不是真实元素,但是老节点和新节点是同一个节点,则是更新阶段,执行 patch 更新节点
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."
);
}
}
// 走到这儿说明不是服务端渲染,或者 hydration 失败,则根据 oldVnode 创建一个 vnode 节点
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode);
}
// 拿到老节点的真实元素
const oldElm = oldVnode.elm;
// 获取老节点的父元素,即 body
const parentElm = nodeOps.parentNode(oldElm);
// 基于 vnode 创建整棵节点树并插入到 body 元素下
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)
);
// 递归更新父占位符节点元素
if (isDef(vnode.parent)) {
let ancestor = vnode.parent;
const patchable = isPatchable(vnode);
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (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.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
// 移除老节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm;
}
这里执行 patch 的逻辑和首次渲染是不一样的,因为 oldVnode 不为空,并且 它和 vnode 都是 VNode 类型,接下来会通过 sameVNode(oldVnode, vnode) 判断它们是否是相同的 VNode 来决定走不同的更新逻辑:
/**
* 判读两个节点是否相同
*/
function sameVnode(a, b) {
return (
// key 必须相同,需要注意的是 undefined === undefined => true
a.key === b.key && // 标签相同
((a.tag === b.tag &&
// 都是注释节点
a.isComment === b.isComment &&
// 都有 data 属性
isDef(a.data) === isDef(b.data) &&
// input 标签的情况
sameInputType(a, b)) ||
// 异步占位符节点
(isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)))
);
}
sameVnode 的逻辑非常简单,如果两个 vnode 的 key 不相等,则是不同的;否 则继续判断对于同步组件,则判断 isComment、data、input 类型等是否相同,对于异步组件,则判断 asyncFactory 是否相同。
所以根据新旧 vnode 是否为 sameVnode,会走到不同的更新逻辑,我们先来说 一下不同的情况。
新旧节点不同
- 创建新节点
// 拿到老节点的真实元素
const oldElm = oldVnode.elm;
// 获取老节点的父元素,即 body
const parentElm = nodeOps.parentNode(oldElm);
// 基于 vnode 创建整棵节点树并插入到 body 元素下
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)
);
以当前旧节点为参考节点,创建新的节点,并插入到 DOM 中,createElm 的逻 辑之前分析过。
- 更新父的占位符节点
// 递归更新父占位符节点元素
if (isDef(vnode.parent)) {
let ancestor = vnode.parent;
const patchable = isPatchable(vnode);
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
ancestor.elm = vnode.elm;
if (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.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]();
}
}
} else {
registerRef(ancestor);
}
ancestor = ancestor.parent;
}
}
只关注主要逻辑即可,找到当前 vnode 的父的占位符节点,先执行各个 module 的 destroy 的钩子函数,如果当前占位符是一个可挂载的节点,则执行 module 的 create 钩子函数。对于这些钩子函数的作用,在之后的章节会详细介绍。
- 删除旧节点
// 移除老节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}
把 oldVnode 从当前 DOM 树中删除,如果父节点存在,则执行 removeVnodes方法:
/**
* 销毁节点:
* 执行组件的 destroy 钩子,即执行 $destroy 方法
* 执行组件各个模块(style、class、directive 等)的 destroy 方法
* 如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
*/
function invokeDestroyHook(vnode) {
let i, j;
const data = vnode.data;
if (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]);
}
}
}
/**
* 移除指定索引范围(startIdx —— endIdx)内的节点
*/
function removeVnodes(vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (isDef(ch)) {
if (isDef(ch.tag)) {
removeAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);
} else {
// Text node
removeNode(ch.elm);
}
}
}
}
function removeAndInvokeRemoveHook(vnode, rm) {
if (isDef(rm) || isDef(vnode.data)) {
let i;
const listeners = cbs.remove.length + 1;
if (isDef(rm)) {
// we have a recursively passed down rm callback
// increase the listeners count
rm.listeners += listeners;
} else {
// directly removing
rm = createRmCb(vnode.elm, listeners);
}
// recursively invoke hooks on child component root node
if (
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);
}
}
删除节点逻辑很简单,就是遍历待删除的 vnodes 做删除,其中 removeAndInvokeRemoveHook 的作用是从 DOM 中移除节点并执行 module 的 remove 钩子函数,并对它的子节点递归调用 removeAndInvokeRemoveHook 函数;invokeDestroyHook 是执行 module 的 destory 钩子函数以及 vnode 的 destory 钩子函数,并对它的子 vnode 递归调用 invokeDestroyHook 函数;removeNode 就是调用平台的 DOM API 去把真正的 DOM 节点移除。
在之前介绍组件生命周期的时候提到 beforeDestroy & destroyed 这两个生命周期钩子函数,它们就是在执行 invokeDestroyHook 过程中,执行了 vnode 的 destory 钩子函数,它的定义在 src/core/vdom/create-component.js 中:
/**
* 销毁组件
* 1、如果组件被 keep-alive 组件包裹,则使组件失活,不销毁组件实例,从而缓存组件的状态
* 2、如果组件没有被 keep-alive 包裹,则直接调用实例的 $destroy 方法销毁组件
*/
destroy (vnode: MountedComponentVNode) {
// 从 vnode 上获取组件实例
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
// 如果组件实例没有被销毁
if (!vnode.data.keepAlive) {
// 组件没有被 keep-alive 组件包裹,则直接调用 $destroy 方法销毁组件
componentInstance.$destroy()
} else {
// 负责让组件失活,不销毁组件实例,从而缓存组件的状态
deactivateChildComponent(componentInstance, true /* direct */)
}
}
}
当组件并不是 keepAlive 的时候,会执行 componentInstance.$destroy() 方法, 然后就会执行 beforeDestroy & destroyed 两个钩子函数。
新旧节点相同
对于新旧节点不同的情况,这种创建新节点 -> 更新占位符节点 -> 删除旧节点 的逻辑。还有一种组件 vnode 的更新情况是新旧节点相同,它 会调用 patchVNode 方法,它的定义在 src/core/vdom/patch.js 中:
/**
* 更新节点
* 全量的属性更新
* 如果新老节点都有孩子,则递归执行 diff
* 如果新节点有孩子,老节点没孩子,则新增新节点的这些孩子节点
* 如果老节点有孩子,新节点没孩子,则删除老节点的这些孩子
* 更新文本节点
*/
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 老节点和新节点相同,直接返回
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
const elm = (vnode.elm = oldVnode.elm);
// 异步占位符节点
if (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))
) {
// 新旧节点都是静态的而且两个节点的 key 一样,并且新节点被 clone 了 或者 新节点有 v-once指令,则重用这部分节点
vnode.componentInstance = oldVnode.componentInstance;
return;
}
// 执行组件的 prepatch 钩子
let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
// 老节点的孩子
const oldCh = oldVnode.children;
// 新节点的孩子
const ch = vnode.children;
// 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
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") {
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);
}
}
patchVnode 的作用就是把新的 vnode patch 到旧的 vnode 上,这里只关注 关键的核心逻辑,把它拆成四步骤:
prepatch
let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
当更新的 vnode 是一个组件 vnode 的时候,会执行 prepatch 的方法,它的 定义在 src/core/vdom/create-component.js 中:
// 更新 VNode,用新的 VNode 配置更新旧的 VNode 上的各种属性
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
// 新 VNode 的组件配置项
const options = vnode.componentOptions
// 老 VNode 的组件实例
const child = vnode.componentInstance = oldVnode.componentInstance
// 用 vnode 上的属性更新 child 上的各种属性
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}
prepatch 方法就是拿到新的 vnode 的组件配置以及组件实例,去执行 updateC hildComponent 方法,它的定义在 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.scopedSlots;
const oldScopedSlots = vm.$scopedSlots;
const hasDynamicScopedSlot = !!(
(newScopedSlots && !newScopedSlots.$stable) ||
(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
(newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$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 slots
vm.$options._renderChildren || // has old static slots
hasDynamicScopedSlot
);
vm.$options._parentVnode = parentVnode;
vm.$vnode = parentVnode; // update vm's placeholder node without re-render
if (vm._vnode) {
// update child tree's parent
vm._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 render
vm.$attrs = parentVnode.data.attrs || emptyObject;
vm.$listeners = listeners || emptyObject;
// update props
if (propsData && vm.$options.props) {
toggleObserving(false);
const props = vm._props;
const 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 propsData
vm.$options.propsData = propsData;
}
// update listeners
listeners = listeners || emptyObject;
const oldListeners = vm.$options._parentListeners;
vm.$options._parentListeners = listeners;
updateComponentListeners(vm, listeners, oldListeners);
// resolve slots + force update if has children
if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context);
vm.$forceUpdate();
}
if (process.env.NODE_ENV !== "production") {
isUpdatingChildComponent = false;
}
}
updateChildComponent 的逻辑也非常简单,由于更新了 vnode,那么 vnode 对 应的实例 vm 的一系列属性也会发生变化,包括占位符 vm.$vnode 的更新、 slot 的更新,listeners 的更新,props 的更新等等。
执行 update 钩子函数
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);
}
回到 patchVNode 函数,在执行完新的 vnode 的 prepatch 钩子函数,会执行所有 module 的 update 钩子函数以及用户自定义 update 钩子函数,对于 module 的钩子函数。
完成 patch 过程
// 老节点的孩子
const oldCh = oldVnode.children;
// 新节点的孩子
const ch = vnode.children;
// 全量更新新节点的属性,Vue 3.0 在这里做了很多的优化
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") {
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);
}
如果 vnode 是个文本节点且新旧文本不相同,则直接替换文本内容。如果不是 文本节点,则判断它们的子节点,并分了几种情况处理:
oldCh 与 ch 都存在且不相同时,使用 updateChildren 函数来更新子节点,这个 后面重点讲。
如果只有 ch 存在,表示旧节点不需要了。如果旧的节点是文本节点则先将 节点的文本清除,然后通过 addVnodes 将 ch 批量插入到新节点 elm 下。
如果只有 oldCh 存在,表示更新的是空节点,则需要将旧的节点通过 remov eVnodes 全部清除。
当只有旧节点是文本节点的时候,则清除其节点文本内容。
执行 postpatch 钩子函数
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}
再执行完 patch 过程后,会执行 postpatch 钩子函数,它是组件自定义的钩子函数,有则执行。
diff
主要分解设计到 diff 的 3 个方法,patch - patchVnode - undateChildren, 进一步整理:
patch
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 判断新的vnode是否为空
if (isUndef(vnode)) {
// 如果老的vnode不为空 卸载所有的老vnode
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}
let isInitialPatch = false;
// 用来存储 insert钩子函数,在插入节点之前调用
const insertedVnodeQueue = [];
// 如果老节点不存在,直接创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
// 是不是元素节点
const isRealElement = isDef(oldVnode.nodeType);
// 当老节点不是真实的DOM节点,并且新老节点的type和key相同,进行patchVnode更新工作
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);
} else {
// 如果不是同一元素节点的话
// 当老节点是真实DOM节点的时候
if (isRealElement) {
// 如果是元素节点 并且在SSR环境的时候 修改SSR_ATTR属性
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;
}
}
// 如果不是服务端渲染的,或者混合失败,就创建一个空的注释节点替换 oldVnode
oldVnode = emptyNodeAt(oldVnode);
}
// 拿到 oldVnode 的父节点
const oldElm = oldVnode.elm;
const parentElm = nodeOps.parentNode(oldElm);
// 根据新的 vnode 创建一个 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)
);
// 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新父节点
// 递归 更新父占位符元素
// 就是执行一遍 父节点的 destory 和 create 、insert 的 钩子函数
if (isDef(vnode.parent)) {
let ancestor = vnode.parent;
const patchable = isPatchable(vnode);
// 更新父组件的占位元素
while (ancestor) {
// 卸载老根节点下的全部组件
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor);
}
// 替换现有元素
ancestor.elm = vnode.elm;
if (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.insert;
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]();
}
}
} else {
registerRef(ancestor);
}
// 更新父节点
ancestor = ancestor.parent;
}
}
// 如果旧节点还存在,就删掉旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
// 否则直接卸载 oldVnode
invokeDestroyHook(oldVnode);
}
}
}
// 执行 虚拟 dom 的 insert 钩子函数
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
// 返回最新 vnode 的 elm ,也就是真实的 dom节点
return vnode.elm;
}
TIP
patch 函数的处理流程:
判断新的 vnode 是否为空
- 如果为空并且老的 vnode 存在,则卸载老的 vnode
如果老节点不存在,直接创建新节点
如果老节点存在:
如果老节点不是真实的 DOM 节点,且新老节点的 type 和 key 相同,调用 patchVnode 更新
如果老节点是真实 DOM 节点
如果老节点是元素节点(nodeType === 1)且在 SSR 环境(oldVnode.hasAttribute(SSR_ATTR)),删除 SSR_ATTR 这个属性(oldVnode.removeAttribute(SSR_ATTR)),执行服务端渲染的逻辑,并且返回老的 vnode
如果不是服务端渲染或者混合失败就创建一个空的注释节点替换 oldVnode
拿到老的 vnode 的节点,根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
如果新的 vnode 的根节点存在,说明根节点被修改了,需要遍历更新父节点,递归更新父占位符元素,就是执行一遍父节点的 destroy,create,insert 的钩子函数
如果老的 vnode 的父节点还存在,就删除父节点,否则直接卸载老的 vnode
执行虚拟的 dom 的 insert 钩子函数 a,如果是首次渲染并且有 parent,则延迟 insert,在元素节点真正被 insert 后调用它们。b 在元素节点真正被 insert 后调用它们
返回最新 vnode 的 elm,也就是真实的 dom 节点
patchVnode
function patchVnode(
oldVnode, // 老的虚拟 DOM 节点
vnode, // 新节点
insertedVnodeQueue, // 插入节点队列
ownerArray, // 节点数组
index, // 当前节点的下标
removeOnly
) {
// 新老节点对比地址一样,直接跳过
if (oldVnode === vnode) {
return;
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode);
}
const elm = (vnode.elm = oldVnode.elm);
// 如果当前节点是注释或 v-if 的,或者是异步函数,就跳过检查异步组件
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
} else {
vnode.isAsyncPlaceholder = true;
}
return;
}
// 当前节点是静态节点的时候,key 也一样,或者有 v-once 的时候,就直接赋值返回
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance;
return;
}
let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {
// 遍历调用 update 更新 oldVnode 所有属性,比如 class,style,attrs,domProps,events...
// 这里的 update 钩子函数是 vnode 本身的钩子函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
// 这里的 update 钩子函数是我们传过来的函数
if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
// 如果新节点不是文本节点,也就是说有子节点
if (isUndef(vnode.text)) {
// 如果新老节点都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 如果新老节点的子节点不一样,就执行 updateChildren 函数,对比子节点
if (oldCh !== ch)
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
} else if (isDef(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);
}
}
TIP
patchVnode 的处理过程:
如果 vnode 和 oldVnode 指向同一个对象,则直接 return 即可。
将旧节点的真实 DOM 赋值到新节点(真实 DOM 连线到新子节点)称为 elm,然后遍历调用 update 更新 oldVnode 上的所有属性,比如 class,style,attrs,domProps,events...;
如果新节点不是文本节点,也就是说有子节点
如果新老节点都有子节点
- 如果新老节点的子节点不一样,就执行 updateChildren 函数,对比子节点
如果新节点有子节点,就是说老节点没有子节点
如果老节点是文本节点,就清空
将 vnode 的子节点真实化之后添加到 DOM 中即可 -> 调用 createElm -> 如果有 tag,就用 createElement,如果是 comment,就调用 createComment, 如果 text,就调用 createTextNode
如果新节点没有子节点,就删除老节点
如果老节点是文本节点,就清空
如果老节点的文本和新节点的文本不同,就更新文本
updateChildren
updateChildren 的逻辑处理:
function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var 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 transitions
var canMove = !removeOnly;
{
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 right
patchVnode(
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 left
patchVnode(
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 element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
);
} else {
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(
vnodeToMove,
newStartVnode,
insertedVnodeQueue,
newCh,
newStartIdx
);
oldCh[idxInOld] = undefined;
canMove &&
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(
newStartVnode,
insertedVnodeQueue,
parentElm,
oldStartVnode.elm,
false,
newCh,
newStartIdx
);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
refElm,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx);
}
}
TIP
updateChildren:
同一个节点: key,tag,isComment 相同,data 都存在或都不存在,如果是 input 的话要 data,attrs,type 相同。
while 循环,条件:oldStartIdx 小于 oldEndIdx 并且 newStartIdx 小于等于 newEndIdx
- 如果旧的开始节点不存在,oldStartIdx++
- 如果旧的结束节点不存在,oldEndIdx--
- 如果旧的开始节点跟新的开始节点是同一个节点,调用 patchVnode,并且 oldStartIdx++,newStartIdx++
- 如果旧的结束节点和新的结束节点是同一个节点,调用 patchVnode,oldEndIdx--,newEndIdx--
- 如果旧的开始节点和新的结束节点是同一个节点,调用 patchVnode,并把旧的开始节点的 elm 插入到旧的结束节点 elm 之后,oldStartIdx++,newEndIdx--
- 如果旧的结束节点和新的开始节点是同一个节点,调用 patchVnode,并把旧的结束节点的 elm 插入到旧的开始节点的 elm 之前 oldEndIdx--,newStartIdx++
- 否则:
- 如果新的开始节点的 key 在旧的节点里找不到,调用 createElm,创建新元素
- 否则:根据 key 找到对应旧节点
- 如果当前旧节点和新的开始节点是同一个节点,调用 patchVnode,将旧列表中对应的旧节点位置置为 undefined,把新的开始节点的 elm 插入当前旧节点的 elm 之前(insertBefore)
- 如果不是,则创建新节点
- newStartIdx++
如果 oldStartIdx 大于 oldEndIdx,说明新节点比旧节点多,调用 patchVnodes,只需将剩余的节点插入到 oldStartNode 的 DOM 之前就可以了。为什么是插入 oldStartNode 之前呢 ?原因是剩余的节点在新列表的位置是位于 oldStartNode 之前的,如果剩余节点是在 oldStartNode 之后,oldStartNode 就会先行对比
如果 newStartIdx 大于 newEndIdx ,说明旧节点多,删除多出的旧节点