跳到主要内容

响应式原理

深入响应式原理

img

响应式对象

initState

在 Vue 的初始化阶段,_init 方法执行的时候,会执行 initState(vm)

export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}

initState 方法主要是对 propsmethodsdatacomputedwathcer 等属性做了初始化操作。

initProps

props 的初始化主要过程,就是遍历定义的 props 配置。

遍历的过程主要做两件事情:

一个是调用 defineReactive 方法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定义 props 中对应的属性

另一个是通过 proxyvm._props.xxx 的访问代理到 vm.xxx

initData

data 的初始化主要过程也是做两件事

一个是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx

另一个是调用 observe 方法观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性

proxy

代理的作用是把 propsdata 上的属性代理到 vm 实例

const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};

export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}

proxy 方法的实现很简单,通过 Object.definePropertytarget[sourceKey][key] 的读写变成了对 target[key] 的读写

所以对于 props 而言,对 vm._props.xxx 的读写变成了 vm.xxx 的读写,而对于 vm._props.xxx 我们可以访问到定义在 props 中的属性,所以我们就可以通过 vm.xxx 访问到定义在 props 中的 xxx 属性了

对于 data 而言,对 vm._data.xxxx 的读写变成了对 vm.xxxx 的读写,而对于 vm._data.xxxx 我们可以访问到定义在 data 函数返回对象中的属性,所以我们就可以通过 vm.xxxx 访问到定义在 data 函数返回对象中的 xxxx 属性了

observe

observe 的功能就是用来监测数据的变化

首先实例化 Dep 对象,接着通过执行 def 函数把自身实例添加到数据对象 value__ob__ 属性上

def 函数是一个非常简单的Object.defineProperty的封装,这就是为什么我在开发中输出data上对象类型的数据,会发现该对象多了一个ob` 的属性

export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true,
});
}

Observer 的构造函数,接下来会对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法。可以看到 observeArray 是遍历数组再次调用 observe 方法,而 walk 方法是遍历对象的 key 调用 defineReactive 方法

export class Observer {
value: any;
dep: Dep;
vmCount: number;

constructor(value: any) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
if (Array.isArray(value)) {
//需要判断数组类型
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
this.walk(value);
}
}
//不是数组就采用递归
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}

observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}

defineReactive

defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter

export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep();

const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}

// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}

let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}

总结

1.initState 过程中 initProps initData 一方面是为了转化为响应式

另一方面是为了进行代理 vm._props.xxx 的访问代理到 vm.xxx

vm._data.xxx 访问到定义 data 返回函数中对应的属性

2.实例化 Dep 对象,接着通过执行 def 函数把自身实例添加到数据对象 value__ob__ 属性上 def 函数是一个非常简单的Object.defineProperty` 的封装

3.defineReactive 函数最开始初始化 Dep 对象的实例

getter 会返回 value 会执行 dep.depend()

setter 会设置 newvalue 会执行 dep.notify()

依赖收集

getter

const dep = new Dep() 实例化一个 Dep 的实例,另一个是在 get 函数中通过 dep.depend依赖收集

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dpendArray(value);
}
}
}
},
});

Dep

Dep 是整个 getter 依赖收集的核心

Dep 是一个 Class,它定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,这是一个全局唯一 Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。

Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的

import type Watcher from "./watcher";
import { remove } from "../util/index";

let uid = 0;

export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

constructor() {
this.id = uid++;
this.subs = [];
}

addSub(sub: Watcher) {
this.subs.push(sub);
}

removeSub(sub: Watcher) {
remove(this.subs, sub);
}

depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}

notify() {
// stabilize the subscriber list first
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
//重置为空
Dep.target = null;
const targetStack = [];

export function pushTarget(_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target);
Dep.target = _target;
}

export function popTarget() {
Dep.target = targetStack.pop();
}

Watcher

Watcher是一个 Class,在它的构造函数中,定义了一些和Dep` 相关的属性:

this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();

其中,this.depsthis.newDeps 表示 Watcher 实例持有的 Dep 实例的数组;而 this.depIdsthis.newDepIds 分别代表 this.depsthis.newDepsid Set

let uid = 0;

/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;

constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.computed = !!options.computed;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.computed = this.sync = false;
}
this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true;
this.dirty = this.computed; // for computed watchers
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
this.expression =
process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
// parse expression for getter
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = function () {};
process.env.NODE_ENV !== "production" &&
warn(
`Failed watching path: "${expOrFn}" ` +
"Watcher only accepts simple dot-delimited paths. " +
"For full control, use a function instead.",
vm
);
}
}
if (this.computed) {
this.value = undefined;
this.dep = new Dep();
} else {
this.value = this.get();
}
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}

/**
* Add a dependency to this directive.
*/
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
}

/**
* Clean up for dependency collection.
*/
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
let tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
}
// ...
}

过程分析

之前我们介绍当对数据对象的访问会触发他们的 getter 方法,那么这些对象什么时候被访问呢?Vue 的 mount 过程是通过 mountComponent 函数,其中有一段比较重要的逻辑,大致如下:

updateComponent = () => {
vm._update(vm._render(), hydrating);
};
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);

当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,然后会执行它的 this.get() 方法,进入 get 函数,首先会执行:

pushTarget(this);

pushTarget 的定义在 src/core/observer/dep.js 中:

export function pushTarget(_target: Watcher) {
if (Dep.target) targetStack.push(Dep.target);
Dep.target = _target;
}

实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)。接着又执行了:

value = this.getter.call(vm, vm);

this.getter 对应就是 updateComponent 函数,这实际上就是在执行:

vm._update(vm._render(), hydrating);

它会先执行 vm._render() 方法,因为之前分析过这个方法会生成 渲染 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 getter。

那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)

刚才我们提到这个时候 Dep.target 已经被赋值为渲染 watcher,那么就执行到 addDep 方法:

addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

这时候会做一些逻辑判断(保证同一数据不会被添加多次)后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 depsubs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备。

所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了么,其实并没有,在完成依赖收集后,还有几个逻辑要执行,首先是:

if (this.deep) {
traverse(value);
}

这个是要递归去访问 value,触发它所有子项的 getter,这个之后会详细讲。接下来执行:

popTarget();

popTarget 的定义在 src/core/observer/dep.js 中:

Dep.target = targetStack.pop();

实际上就是把 Dep.target 恢复成上一个状态,因为当前 vm 的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变。最后执行:

this.cleanupDeps();

其实很多人都分析过并了解到 Vue 有依赖收集的过程,但我几乎没有看到有人分析依赖清空的过程,其实这是大部分同学会忽视的一点,也是 Vue 考虑特别细的一点。

cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}

考虑到 Vue 是数据驱动的,所以每次数据变化都会重新 render,那么 vm._render() 方法又会再次执行,并再次触发数据的 getters,所以 Watcher 在构造函数中会初始化 2 个 Dep 实例数组,newDeps 表示新添加Dep 实例数组,而 deps 表示上一次添加Dep 实例数组。

在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIdsdepIds 交换,newDepsdeps 交换,并把 newDepIdsnewDeps 清空。

那么为什么需要做 deps 订阅的移除呢,在添加 deps 的订阅过程,已经能通过 id 去重避免重复订阅了。

考虑到一种场景,我们的模板会根据 v-if 去渲染不同子模板 a 和 b,当我们满足某种条件的时候渲染 a 的时候,会访问到 a 中的数据,这时候我们对 a 使用的数据添加了 getter,做了依赖收集,那么当我们去修改 a 的数据的时候,理应通知到这些订阅者。那么如果我们一旦改变了条件渲染了 b 模板,又会对 b 使用的数据添加了 getter,如果我们没有依赖移除的过程,那么这时候我去修改 a 模板的数据,会通知 a 数据的订阅的回调,这显然是有浪费的。

因此 Vue 设计了在每次添加完新的订阅,会移除掉旧的订阅,这样就保证了在我们刚才的场景中,如果渲染 b 模板的时候去修改 a 模板的数据,a 数据订阅回调已经被移除了,所以不会有任何浪费,真的是非常赞叹 Vue 对一些细节上的处理

总结

1.Vue 的 mount 过程中通过 mountComponent 函数 进行new Watcher()

2.先执行构造函数 然后执行 this.get()

3.进入 get() 首先会执行pushTarget(this)

4.在 pushTarget 中会 把Dep.target 赋值为当前的渲染 watcher 并压栈

5.然后执行了 value = this.getter.call(vm, vm)this.getter 对应就是 updateComponent 函数 实际上是在执行 vm._update(vm._render(), hydrating)

6.会先执行 vm._render() 方法 生成 渲染 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 getter

7.每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this) 第四步 的时候 Dep.target 已经被赋值为 watcher 所以能执行 Watcher 的方法 addDep 而这个 this 是 Dep, addDep 内会执行 dep 的 addSub


Dep内部
addSub (sub: Watcher) {
this.subs.push(sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this) Dep.target是watcher
this传的是watcher
}
}


Watcher内部
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

8.addSub 内执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 depsubs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备 实际上就是 执行在 watcher 内执行了 dep 的 addsub 方法把自己(Watcher 给加了进去)

9.在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程 然后进行收尾工作

10.递归去访问 value,触发它所有子项的 getter

if (this.deep) { traverse(value) }

11.然后执行 Dep 内部的popTarget()Dep.target 恢复成上一个状态

因为当前 vm 的数据依赖收集已经完成,那么对应的渲染Dep.target 也需要改变

12.最后执行 Watcher 的 cleanupDeps()方法 依赖清空

13.考虑到 Vue 是数据驱动的,所以每次数据变化都会重新 render,那么 vm._render() 方法又会再次执行,并再次触发数据的 getters,所以 Watcher 在构造函数中会初始化 2 个 Dep 实例数组,newDeps 表示新添加Dep 实例数组,而 deps 表示上一次添加Dep 实例数组

在执行 cleanupDeps 函数的时候,会首先遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅,然后把 newDepIdsdepIds 交换,newDepsdeps 交换,并把 newDepIdsnewDeps 清空。

派发更新

收集的目的就是为了当我们修改数据的时候,可以对相关的依赖派发更新

setter

set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}

setter 的逻辑有 2 个关键的点,

一个是 childOb = !shallow && observe(newVal),如果 shallow 为 false 的情况,会对新设置的值变成一个响应式对象;

另一个是 dep.notify(),通知所有的订阅者

过程分析

在组件中对响应的数据做了修改,就会触发 setter 的逻辑,最后调用 dep.notify() 方法, 它是 Dep 的一个实例方法

class Dep {
// ...
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}

遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcherupdate 方法

class Watcher {
// ...
update() {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true;
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify();
});
}
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
}

对于 Watcher 的不同状态,会执行不同的逻辑

在一般组件数据更新的场景,会走到最后一个 queueWatcher(this) 的逻辑

在 scheduler 中

const queue: Array<Watcher> = [];
let has: { [key: number]: ?true } = {};
let waiting = false;
let flushing = false;
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher(watcher: Watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}

这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue

首先用 has 对象保证同一个 Watcher 只添加一次;接着对 flushing 的判断;最后通过 waiting 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次,可以理解它是在下一个 tick,也就是异步的去执行 flushSchedulerQueue

flushSchedulerQueue 的实现

let flushing = false;
let index = 0;
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue() {
flushing = true;
let watcher, id;

// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id);

// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== "production" && has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
"You may have an infinite update loop " +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
);
break;
}
}
}

// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice();
const updatedQueue = queue.slice();

resetSchedulerState();

// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);

// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit("flush");
}
}
  • 队列排序

queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:

1.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。

2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。

3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

  • 队列遍历

在对 queue 排序后,接着就是要对它做遍历,拿到对应的 watcher,执行 watcher.run()。这里需要注意一个细节,在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher,这样会再次执行到 queueWatcher,如下:

export function queueWatcher(watcher: Watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// ...
}
}

可以看到,这时候 flushing 为 true,就会执行到 else 的逻辑,然后就会从后往前找,找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。把 watcher 按照 id的插入到队列中,因此 queue 的长度发生了变化。

  • 状态恢复

这个过程就是执行 resetSchedulerState 函数,它的定义在 src/core/observer/scheduler.js 中。

const queue: Array<Watcher> = [];
let has: { [key: number]: ?true } = {};
let circular: { [key: number]: number } = {};
let waiting = false;
let flushing = false;
let index = 0;
/**
* Reset the scheduler's state.
*/
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0;
has = {};
if (process.env.NODE_ENV !== "production") {
circular = {};
}
waiting = flushing = false;
}

逻辑非常简单,就是把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空

watcher.run() 的逻辑

class Watcher {
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
if (this.active) {
this.getAndInvoke(this.cb);
}
}

getAndInvoke(cb: Function) {
const value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`);
}
} else {
cb.call(this.vm, value, oldValue);
}
}
}
}

run 函数实际上就是执行 this.getAndInvoke 方法,并传入 watcher 的回调函数。getAndInvoke 函数逻辑也很简单,先通过 this.get() 得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因。

那么对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法:

updateComponent = () => {
vm._update(vm._render(), hydrating);
};

所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同

总结

1.修改数据,触发 setter 的逻辑,调用 dep.notify() 方法

2.dep.notify() 遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcherupdate 方法

3.update()方法 对于 Watcher 的不同状态,会执行不同的逻辑

组件更新 会走到最后一个 queueWatcher(this) 的逻辑

4.Vue 在做派发更新的时候,并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue

5.flushSchedulerQueue 的实现

队列排序queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序

队列遍历 在对 queue 排序后,接着就是要对它做遍历,拿到对应的 watcher,执行 watcher.run() 在遍历的时候每次都会对 queue.length 求值

状态恢复 执行 resetSchedulerState 函数 把控制流程状态的一些变量恢复到初始值,把 watcher 队列清空

6.watcher.run() 实际上就是执行 this.getAndInvoke 方法,并传入 watcher 的回调函数

getAndInvoke 函数 先通过 this.get() 得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因

getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}

7.对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法:

updateComponent = () => { vm._update(vm._render(), hydrating) }

当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染

nextTick

JS 运行机制

JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行

(4)主线程不断重复上面的第三步

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要处理所有的 micro task

for (macroTask of macroTaskQueue) {
// 1. Handle current MACRO-TASK
handleMacroTask();

// 2. Handle all MICRO-TASK
for (microTask of microTaskQueue) {
handleMicroTask(microTask);
}
}

在浏览器环境中,常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate;常见的 micro task 有 MutationObsever 和 Promise.then

Vue 的实现

next-tick.js

import { noop } from "shared/util";
import { handleError } from "./error";
import { isIOS, isNative } from "./env";

const callbacks = [];
let pending = false;

function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}

let microTimerFunc;
let macroTimerFunc;
let useMacroTask = false;

if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks);
};
} else if (
typeof MessageChannel !== "undefined" &&
(isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === "[object MessageChannelConstructor]")
) {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = flushCallbacks;
macroTimerFunc = () => {
port.postMessage(1);
};
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}

if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
microTimerFunc = () => {
p.then(flushCallbacks);

if (isIOS) setTimeout(noop);
};
} else {
// fallback to macro
microTimerFunc = macroTimerFunc;
}

export function withMacroTask(fn: Function): Function {
return (
fn._withTask ||
(fn._withTask = function () {
useMacroTask = true;
const res = fn.apply(null, arguments);
useMacroTask = false;
return res;
})
);
}

export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, "nextTick");
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
// $flow-disable-line
if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}

申明了 microTimerFuncmacroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数

对于 macro task 的实现,优先检测是否支持原生 setImmediate,这是一个高版本 IE 和 Edge 才支持的特性,不支持的话再去检测是否支持原生的 MessageChannel,如果也不支持的话就会降级为 setTimeout 0

对于 micro task 的实现,则检测浏览器是否原生支持 Promise,不支持的话直接指向 macro task 的实现 新版本废弃了 mutationOberserver

对外暴露了 2 个函数,先来看 nextTick,在派发更新时nextTick(flushSchedulerQueue) 所用到的函数。

它的逻辑也很简单,把传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc

而它们都会在下一个 tick 执行 flushCallbacksflushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。

这里使用 callbacks 而不是直接在 nextTick 中执行回调函数的原因是保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 tick 执行完毕

withMacroTask函数的作用是给回调函数做一层包装,当事件触发时,如果因为回调中修改了数据而触发更新DOM的操作,那么该更新操作会被推送到宏任务( macrotask )的任务队列中

nextTick 函数最后还有一段逻辑:

if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
_resolve = resolve;
});
}

nextTick 不传 cb 参数的时候,提供一个 Promise 化的调用,比如:

nextTick().then(() => {});

_resolve 函数执行,就会跳到 then 的逻辑中

还对外暴露了withMacroTask函数,它是对函数做一层包装,确保函数执行过程中对数据任意的修改,触发变化执行nextTick的时候强制走macroTimerFunc。比如对于一些 DOM 交互事件,如 v-on` 绑定的事件回调函数的处理,会强制走 macro task

总结

1.结合 setter 分析,了解到数据的变化到 DOM 的重新渲染是一个异步过程,发生在下一个 tick

microTimerFuncmacroTimerFunc 2 个变量,它们分别对应的是 micro task 的函数和 macro task 的函数 函数会传入 flushCallbacks

2.macro task 会分别检测 setImmediate MessageChannel 都不支持则 setTimeout 0

micro task 会检测是否原生支持 Promise,不支持的话直接指向 macro task 的实现

3.在派发更新时候 传入的回调函数 cb 压入 callbacks 数组,最后一次性地根据 useMacroTask 条件执行 都会在下一个 tick 执行 flushCallbacks,对 callbacks 遍历,然后执行相应的回调函数

4.使用 callbacks保证在同一个 tick 内多次执行 nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务

5.nexttick 使用场景 比如从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的 DOM 变化,我们就必须在 nextTick 后执行

getData(res).then(() => {
this.xxx = res.data;
this.$nextTick(() => {
// 这里我们可以获取变化后的 DOM
});
});

Vue.js 提供了 2 种调用 nextTick 的方式,一种是全局 API Vue.nextTick,一种是实例上的方法 vm.$nextTick,无论我们使用哪一种,最后都是调用 next-tick.js 中实现的 nextTick 方法

检测变化的注意事项

对象添加属性

对于使用 Object.defineProperty 实现响应式的对象,当我们去给这个对象添加一个新的属性的时候,是不能够触发它的 setter 的,比如:

var vm = new Vue({
data: {
a: 1,
},
});
// vm.b 是非响应的
vm.b = 2;

添加新属性的场景我们在平时开发中会经常遇到,那么 Vue 为了解决这个问题,定义了一个全局 API Vue.set 方法

src/core/global-api/index.js中 初始化Vue.set = set

src/core/observer/index.js 中 定义 set 方法

export function set(target: Array<any> | Object, key: any, val: any): any {
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
);
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
const ob = (target: any).__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
if (!ob) {
target[key] = val;
return val;
}
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}

set 方法接收 3 个参数,target 可能是数组或者是普通对象,key 代表的是数组的下标或者是对象的键值,val 代表添加的值

首先判断如果 target 是数组且 key 是一个合法的下标,则之前通过 splice 去添加进数组然后返回,这里的 splice 其实已经不仅仅是原生数组的 splice

然后判断 key 已经存在于 target 中,则直接赋值返回,因为这样的变化是可以观测到了

再获取到 target.__ob__ 并赋值给 ob,它是在 Observer 的构造函数执行的时候初始化的,表示 Observer 的一个实例,如果它不存在,则说明 target 不是一个响应式的对象,则直接赋值并返回

最后通过 defineReactive(ob.value, key, val) 把新添加的属性变成响应式对象,然后再通过 ob.dep.notify() 手动的触发依赖通知

在 getter 过程中判断了 childOb,并调用了 childOb.dep.depend() 收集了依赖,这就是为什么执行 Vue.set 的时候通过 ob.dep.notify() 能够通知到 watcher,从而让添加新的属性到对象也可以检测到变化。这里如果 value 是个数组,那么就通过 dependArray 把数组每个元素也去做依赖收集

export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
// ...
});
}

数组

接着说一下数组的情况,Vue 也是不能检测到以下变动的数组:

1.当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue

2.当你修改数组的长度时,例如:vm.items.length = newLength

对于第一种情况,可以使用:Vue.set(example1.items, indexOfItem, newValue);而对于第二种情况,可以使用 vm.items.splice(newLength)

对于 Vue.set 的实现,当 target 是数组的时候,也是通过 target.splice(key, 1, val) 来添加的,能让添加的对象变成响应式的呢。

在通过 observe 方法去观察对象的时候会实例化 Observer,在它的构造函数中是专门对数组做了处理,它的定义在 src/core/observer/index.js 中。

export class Observer {
constructor(value: any) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, "__ob__", this);
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
this.observeArray(value);
} else {
// ...
}
}
}

这里我们只需要关注 value 是 Array 的情况,首先获取 augment,这里的 hasProto 实际上就是判断对象中是否存在 __proto__,如果存在则 augment 指向 protoAugment, 否则指向 copyAugment,来看一下这两个函数的定义:

/**
* Augment an target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment(target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}

/**
* Augment an target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment(target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}

protoAugment 方法是直接把 target.__proto__ 原型直接修改为 src,而 copyAugment 方法是遍历 keys,通过 def,也就是 Object.defineProperty 去定义它自身的属性值。

对于大部分现代浏览器都会走到 protoAugment,那么它实际上就把 value 的原型指向了 arrayMethodsarrayMethods 的定义在 src/core/observer/array.js 中:

import { def } from "../util/index";

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];

/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify();
return result;
});
});

可以看到,通过以 Array.prototype 为原型来构造,使得arrayMethods 首先继承了 Array

然后对数组中所有能改变数组自身的方法,如 push、pop 等这些方法进行重写。重写后的方法会先执行它们本身原有的逻辑,并对能增加数组长度的 3 个方法 push、unshift、splice 方法做了判断,获取到插入的值,

然后把新添加的值变成一个响应式对象,并且再调用 ob.dep.notify() 手动触发依赖通知,这就很好地解释了之前的示例中调用 vm.items.splice(newLength) 方法可以检测到变化

总结

对象添加属性

1.给对象添加一个新的属性,需要使用 Vue.set(target,key,value)

2.如果 target数组key 是一个合法的下标,则之前通过 splice 去添加进数组然后返回

3.接着又判断 key 已经存在于 target 中,则直接赋值返回,因为这样的变化是可以观测到了

4.再获取到 target.__ob__ 并赋值给 ob,之前分析过它是在 Observer 的构造函数执行的时候初始化的,表示 Observer 的一个实例,如果它不存在,则说明 target 不是一个响应式的对象,则直接赋值并返回

5.最后通过 defineReactive(ob.value, key, val) 把新添加的属性变成响应式对象,然后再通过 ob.dep.notify() 手动的触发依赖通知

数组

1.根据索引更改数组和长度修改, Observer在它的构造函数中是专门对数组做了处理

2.value 是 Array 的情况,首先获取 augment,这里的 hasProto 实际上就是判断对象中是否存在 __proto__,如果存在则 augment 指向 protoAugment, 实现target.__proto__ = src 否则指向 copyAugment 遍历 keys,通过 def,也就是 Object.defineProperty 去定义它自身的属性值

3.对于大部分现代浏览器都会走到 protoAugment,那么它实际上就把 value 的原型指向了 arrayMethods

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];

4.arrayMethods 通过以 Array.prototype 为原型来构造,使得arrayMethods 首先继承了 Array,然后对数组中所有能改变数组自身的方法,如 push、pop 等这些方法进行重写

methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify();
return result;
});
});

5.对能增加数组长度的 3 个方法 push、unshift、splice 方法做了判断,获取到插入的值,然后把新添加的值变成一个响应式对象 再调用 ob.dep.notify() 手动触发依赖通知

调用 vm.items.splice(newLength) 方法可以检测到变化

计算属性 VS 侦听属性

computed

计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中,执行了 if (opts.computed) initComputed(vm, opts.computed) initComputed 的定义在 src/core/instance/state.js 中:

const computedWatcherOptions = { computed: true };
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();

for (const key in computed) {
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) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}

// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== "production") {
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 的实现

export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering();
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef;
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop;
sharedPropertyDefinition.set = userDef.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
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}

利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter

setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数

平时的开发场景中,计算属性有 setter 的情况比较少

getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值

function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
watcher.depend();
return watcher.evaluate();
}
};
}

createComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter

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 on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}

注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。

然后再执行 watcher.evaluate() 去求值,来看一下它的定义:

/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
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.firstNamethis.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher

最后通过 return this.value 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。

一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:

/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true;
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify();
});
}
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}

那么对于计算属性这样的 computed watcher,它实际上是有 2 种模式,lazy 和 active。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:

this.getAndInvoke(() => {
this.dep.notify()
})

getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}

getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。

通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。

watch

侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了:

if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}

来看一下 initWatch 的实现,它的定义在 src/core/instance/state.js 中:

function initWatch(vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key];
if (Array.isArray(handler)) {
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

function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === "string") {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options);
}

这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:

Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
options.user = true;
const watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
cb.call(vm, watcher.value);
}
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 的数据发送变化,它最终会执行 watcherrun 方法,执行回调函数 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;
}

所以 watcher 总共有 4 种类型,我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。

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.bgetter,所以并没有订阅它的变化,导致我们对 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 中:

import { _Set as Set, isObject } from "../util/index";
import type { SimpleSet } from "../util/index";
import VNode from "../vdom/vnode";

const seenObjects = new Set();

/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
export function traverse(val: any) {
_traverse(val, seenObjects);
seenObjects.clear();
}

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 求值以及在执行回调函数的时候,会处理一下错误,如下:

get() {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
},
getAndInvoke() {
// ...
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}

handleError 在 Vue 中是一个错误捕获并且暴露给用户的一个利器。

computed watcher

computed watcher 几乎就是为计算属性量身定制的

sync watcher

在我们之前对 setter 的分析过程知道,当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。

update () {
if (this.computed) {
// ...
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过程的时候才会去设置该属性为 true

总结

计算属性本质上是 computed watcher,而侦听属性本质上是 user watcher

计算属性适合用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑

computed

1.计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数 执行了 if (opts.computed) initComputed(vm, opts.computed)

2.先创建 vm._computedWatchers 为一个空对象,接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数为每一个 3.getter 创建一个 watcher 它是一个 computed watcher,因为进行了设置 const computedWatcherOptions = { computed: true }

4.最后对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef) 否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用

5.defineComputed 实际上就是利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数

6.getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 返回的一个函数 computedGetter,它就是计算属性对应的 getter

computed watcher

1.初始化这个 computed watcher 实例 computed watcher 会并不会立刻求值,但是同时持有一个 dep 实例。

2.当 render 函数执行访问到 属性的时候,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend()

3.Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化

4.然后再执行 watcher.evaluate() 去求值,evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get() 求值,然后把 this.dirty 设置为 false

5.在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数 得到了 computed()函数的返回值

6.由于 this.xxx是响应式对象,这里会触发它们的 getter 它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher 最后通过 return this.value 拿到计算属性对应的值

7.一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法

8.update()方法中有 2 种模式,lazy 和 active。

this.dep.subs.length === 0 说明没有人去订阅这个 computed watcher 的变化

就把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值

否则执行 this.getAndInvoke(() => { this.dep.notify() })

9.getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染

Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化

watch

1.侦听属性*的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中,在 computed 初始化之后,执行了 initWatch(vm, opts.watch)

2.initWatch 的实现,这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher

3.createWatcher 的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,

4.$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的,侦听属性 watch 最终会调用 $watch 方法

5.$watch方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。

6.接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true

7.通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcherrun 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb

8.最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher

所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型,

组件更新

更新判断

当数据发生变化的时候,会触发渲染 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 中定义:

return function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode);
return;
}

let isInitialPatch = false;
const insertedVnodeQueue = [];

if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue);
} else {
const isRealElement = isDef(oldVnode.nodeType);
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly);
} else {
if (isRealElement) {
// ...
}

// 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)
);

// update parent placeholder node element, recursively
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;
}
}

// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [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 (
a.key === b.key &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)))
);
}

sameVnode 的逻辑非常简单,如果两个 vnodekey 不相等,则是不同的;否则继续判断对于同步组件,则判断 isCommentdatainput 类型等是否相同,对于异步组件,则判断 asyncFactory 是否相同。

所以根据新旧 vnode 是否为 sameVnode,会走到不同的更新逻辑,我们先来说一下不同的情况。

新旧节点不同

如果新旧 vnode 不同,那么更新的逻辑非常简单,它本质上是要替换已存在的节点,大致分为 3 步

  • 创建新节点
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)
);

以当前旧节点为参考节点,创建新的节点,并插入到 DOM 中,createElm 的逻辑我们之前分析过。

  • 更新父的占位符节点
// update parent placeholder node element, recursively
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 的父的占位符节点,先执行各个 moduledestroy 的钩子函数,如果当前占位符是一个可挂载的节点,则执行 modulecreate 钩子函数。

  • 删除旧节点
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0);
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode);
}

oldVnode 从当前 DOM 树中删除,如果父节点存在,则执行 removeVnodes 方法:

function removeVnodes(parentElm, 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);
}
}

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]);
}
}
}

删除节点逻辑很简单,就是遍历待删除的 vnodes 做删除,

其中 removeAndInvokeRemoveHook 的作用是从 DOM 中移除节点执行 moduleremove 钩子函数

并对它的子节点**递归调用** removeAndInvokeRemoveHook 函数;

invokeDestroyHook 是执行 moduledestory 钩子函数以及 vnodedestory 钩子函数,并对它的子 vnode 递归调用 invokeDestroyHook 函数;removeNode 就是调用平台的 DOM API 去把真正的 DOM 节点移除。

在之前介绍组件生命周期的时候提到 beforeDestroy & destroyed 这两个生命周期钩子函数,它们就是在执行 invokeDestroyHook 过程中,执行了 vnodedestory 钩子函数,它的定义在 src/core/vdom/create-component.js 中:

const componentVNodeHooks = {
destroy(vnode: MountedComponentVNode) {
const { componentInstance } = vnode;
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy();
} else {
deactivateChildComponent(componentInstance, true /* direct */);
}
}
},
};

当组件并不是 keepAlive 的时候,会执行 componentInstance.$destroy() 方法,然后就会执行 beforeDestroy & destroyed 两个钩子函数。

新旧节点相同

对于新旧节点不同的情况,这种创建新节点 -> 更新占位符节点 -> 删除旧节点的逻辑是很容易理解的。还有一种组件 vnode 的更新情况是新旧节点相同,它会调用 patchVNode 方法,它的定义在 src/core/vdom/patch.js 中:

function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) {
if (oldVnode === vnode) {
return;
}

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))
) {
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)) {
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 (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
removeVnodes(elm, 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 中:

const componentVNodeHooks = {
prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions;
const child = (vnode.componentInstance = oldVnode.componentInstance);
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
);
},
};

prepatch 方法就是拿到新的 vnode 的组件配置以及组件实例,去执行 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
const hasChildren = !!(
(
renderChildren || // has new static slots
vm.$options._renderChildren || // has old static slots
parentVnode.data.scopedSlots || // has new scoped slots
vm.$scopedSlots !== emptyObject
) // has old scoped slots
);

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 (hasChildren) {
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 函数,在执行完新的 vnodeprepatch 钩子函数,会执行所有 moduleupdate 钩子函数以及用户自定义 update 钩子函数,对于 module 的钩子函数,之

  • 完成 patch 过程
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)) {
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(elm, 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 是个文本节点且新旧文本不相同,则直接替换文本内容。如果不是文本节点,则判断它们的子节点,并分了几种情况处理:

1.oldChch 都存在且不相同时,使用 updateChildren 函数来更新子节点,这个后面重点讲。

2.如果只有 ch 存在,表示旧节点不需要了。如果旧的节点是文本节点则先将节点的文本清除,然后通过 addVnodesch 批量插入到新节点 elm 下。

3.如果只有 oldCh 存在,表示更新的是空节点,则需要将旧的节点通过 removeVnodes 全部清除。

4.当只有旧节点是文本节点的时候,则清除其节点文本内容。

  • 执行 postpatch 钩子函数
if (isDef(data)) {
if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}

再执行完 patch 过程后,会执行 postpatch 钩子函数,它是组件自定义的钩子函数,有则执行。

那么在整个 pathVnode 过程中,最复杂的就是 updateChildren 方法了

updateChildren

function updateChildren(
parentElm,
oldCh,
newCh,
insertedVnodeQueue,
removeOnly
) {
let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let 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 transitions
const canMove = !removeOnly;

if (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);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
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);
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);
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(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}

updateChildren 的逻辑比较复杂,直接读源码比较晦涩,我们可以通过一个具体的示例来分析它。

<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 部分的列表数据,我们通过图的方式来描述一下它的更新过程:

第一步: img

第二步: img

第三步: img

第四步: img

第五步: img

第六步: img

总结

组件更新逻辑判断

1.当数据发生变化的时候,会触发渲染 watcher 的回调函数,进而执行组件的更新过程,组件的更新还是通过 updateComponent 调用了 vm._update 方法

2.组件更新的过程,会执行 vm.$el = vm.__patch__(prevVnode, vnode),它仍然会调用 patch 函数

3.patch 的逻辑和首次渲染是不一样的,因为 oldVnode 不为空,并且它和 vnode 都是 VNode 类型

4.然后会通过 sameVNode(oldVnode, vnode) 判断它们是否是相同的 VNode 来决定走不同的更新逻辑

如果两个 vnodekey 不相等,则是不同的;否则继续判断对于同步组件,则判断 isCommentdatainput 类型等是否相同,对于异步组件,则判断 asyncFactory 是否相同

根据新旧 vnode 是否为 sameVnode,会走到不同的更新逻辑

新旧节点不同

如果新旧 vnode 不同,那么更新的逻辑非常简单,它本质上是要替换已存在的节点,大致分为 3 步

1.创建新节点 通过 createElm() 以当前旧节点为参考节点,创建新的节点,并插入到 DOM 中

2.更新父的占位符节点 找到当前 vnode 的父的占位符节点,先执行各个 moduledestroy 的钩子函数,如果当前占位符是一个可挂载的节点,则执行 modulecreate 钩子函数

3.删除旧节点oldVnode 从当前 DOM 树中删除,如果父节点存在,则执行 removeVnodes 方法

removeAndInvokeRemoveHook 的作用是从 DOM 中移除节点并执行 moduleremove 钩子函数,并对它的子节点递归调用 removeAndInvokeRemoveHook 函数

invokeDestroyHook 是执行 moduledestory 钩子函数以及 vnodedestory 钩子函数,并对它的子 vnode 递归调用 invokeDestroyHook 函数

removeNode 就是调用平台的 DOM API 去把真正的 DOM 节点移除

4.beforeDestroy & destroyed 这两个生命周期钩子函数 就是在执行 invokeDestroyHook 过程中,执行了 vnodedestory 钩子函数 当组件并不是 keepAlive 的时候,会执行 componentInstance.$destroy() 方法,然后就会执行 beforeDestroy & destroyed 两个钩子函数

新旧节点相同

组件 vnode 的更新情况是新旧节点相同,它会调用 patchVNode 方法 , patchVnode 的作用就是把新的 vnode patch 到旧的 vnode 上,拆成四步骤:

1.执行 prepatch 钩子函数

当更新的 vnode 是一个组件 vnode 的时候,会执行 prepatch 的方法

prepatch 方法就是拿到新的 vnode 的组件配置以及组件实例,去执行 updateChildComponent 方法

updateChildComponent 的逻辑也非常简单,由于更新了 vnode,那么 vnode 对应的实例 vm 的一系列属性也会发生变化,包括占位符 vm.$vnode 的更新、slot 的更新,listeners 的更新,props 的更新等

2.执行 update 钩子函数

回到 patchVNode 函数,在执行完新的 vnodeprepatch 钩子函数,会执行所有 moduleupdate 钩子函数以及用户自定义 update 钩子函数,对于 module 的钩子函数

3.完成 patch 过程

如果 vnode 是个文本节点且新旧文本不相同,则直接替换文本内容。如果不是文本节点,则判断它们的子节点

(1)oldChch 都存在且不相同时,使用 updateChildren 函数来更新子节点,。

(2)如果只有 ch 存在,表示旧节点不需要了。如果旧的节点是文本节点则先将节点的文本清除,然后通过 addVnodesch 批量插入到新节点 elm 下。

(3)如果只有 oldCh 存在,表示更新的是空节点,则需要将旧的节点通过 removeVnodes 全部清除。

(4)当只有旧节点是文本节点的时候,则清除其节点文本内容

4.执行 postpatch 钩子函数

在执行完 patch 过程后,会执行 postpatch 钩子函数,它是组件自定义的钩子函数,有则执行。

那么在整个 pathVnode 过程中,最复杂的就是 updateChildren 方法。

updateChildren

diff 算法主要是对相同节点的子节点进行的比较和更新,也就是相同级别节点的比较算法。dom 对比更新,本着尽量减少 dom 的销毁和重建,所以 diff 算法尽量先移动,后增删

定义变量

let oldStartIdx = 0;
let newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];

进行循环 索引遍历节点

首先会判断是否为空 isUndef(oldStartVnode) isUndef(oldEndVnode)

export function isUndef(v: any): boolean %checks {
return v === undefined || v === null;
}

sameVnode(oldStartVnode, newStartVnode)

sameVnode(oldEndVnode, newEndVnode)

sameVnode(oldStartVnode, newEndVnode)

sameVnode(oldEndVnode, newStartVnode)

如果上述四种情况都不存在,则用新 VNode 当前的 newStartVnode 去旧 VNode 里找(尽量不新建 dom,找到了就移动,找不到再新建)

如果找到了,则调用 patchVnode 去比较详细内容,并且移动这个节点到 newStartIdx 位置,

如果没找到,则创建新节点并插入

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);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
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);
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);
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];
}
}

循环结束 遍历完成(走出 while 循环时说明肯定有新/旧节点被遍历完了):

结束判断 1:当结束时 oldStartIdx > oldEndIdx ,说明旧节点遍历完了,新节点没遍历完,则说明新节点比老节点多,把 newStartIdx >到 newEndIdx 直接的节点插入到老节点后面

结束判断 2:当结束时 newStartIdx > newEndIdx ,说明新节点遍历完了,旧节点没遍历完,则说明老节点比新节点多,把的 oldStartIdx 到 oldEndIdx 之间的节点删除

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(parentElm, oldCh, oldStartIdx, oldEndIdx);
}

Props

规范化

在初始化 props 之前,首先会对 props 做一次 normalize,它发生在 mergeOptions 的时候,在 src/core/util/options.js 中:

export function mergeOptions(
parent: Object,
child: Object,
vm?: Component
): Object {
// ...
normalizeProps(child, vm);
// ...
}

function normalizeProps(options: Object, vm: ?Component) {
const props = options.props;
if (!props) return;
const res = {};
let i, val, name;
if (Array.isArray(props)) {
i = props.length;
while (i--) {
val = props[i];
if (typeof val === "string") {
name = camelize(val);
res[name] = { type: null };
} else if (process.env.NODE_ENV !== "production") {
warn("props must be strings when using array syntax.");
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key];
name = camelize(key);
res[name] = isPlainObject(val) ? val : { type: val };
}
} else if (process.env.NODE_ENV !== "production") {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
);
}
options.props = res;
}

合并配置它主要就是处理我们定义组件的对象 option,然后挂载到组件的实例 this.$options 中。

重点看 normalizeProps 的实现,其实这个函数的主要目的就是把我们编写的 props 转成对象格式,因为实际上 props 除了对象格式,还允许写成数组格式。

props 是一个数组,每一个数组元素 prop 只能是一个 string,表示 propkey,转成驼峰格式,prop 的类型为空。

props 是一个对象,对于 props 中每个 propkey,我们会转驼峰格式,而它的 value,如果不是一个对象,我们就把它规范成一个对象。

如果 props 既不是数组也不是对象,就抛出一个警告。

举个例子:

export default {
props: ["name", "nick-name"],
};

经过 normalizeProps 后,会被规范成:

options.props = {
name: { type: null },
nickName: { type: null },
};
export default {
props: {
name: String,
nickName: {
type: Boolean,
},
},
};

经过 normalizeProps 后,会被规范成:

options.props = {
name: { type: String },
nickName: { type: Boolean },
};

由于对象形式的 props 可以指定每个 prop 的类型和定义其它的一些属性,推荐用对象形式定义 props

初始化

Props 的初始化主要发生在 new Vue 中的 initState 阶段,在 src/core/instance/state.js 中:

export function initState(vm: Component) {
// ....
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
// ...
}

function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {};
const props = (vm._props = {});
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = (vm.$options._propKeys = []);
const isRoot = !vm.$parent;
// root instance props should be converted
if (!isRoot) {
toggleObserving(false);
}
for (const key in propsOptions) {
keys.push(key);
const value = validateProp(key, propsOptions, propsData, vm);
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production") {
const hyphenatedKey = hyphenate(key);
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
);
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
);
}
});
} else {
defineReactive(props, key, value);
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key);
}
}
toggleObserving(true);
}

initProps 主要做 3 件事情:校验、响应式和代理。

校验

校验的逻辑很简单,遍历 propsOptions,执行 validateProp(key, propsOptions, propsData, vm) 方法。这里的 propsOptions 就是我们定义的 props 在规范后生成的 options.props 对象,propsData 是从父组件传递的 prop 数据。所谓校验的目的就是检查一下我们传递的数据是否满足 prop的定义规范。再来看一下 validateProp 方法,它定义在 src/core/util/props.js 中:

export function validateProp(
key: string,
propOptions: Object,
propsData: Object,
vm?: Component
): any {
const prop = propOptions[key];
const absent = !hasOwn(propsData, key);
let value = propsData[key];
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type);
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, "default")) {
value = false;
} else if (value === "" || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type);
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true;
}
}
}
// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key);
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve;
toggleObserving(true);
observe(value);
toggleObserving(prevShouldObserve);
}
if (
process.env.NODE_ENV !== "production" &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && "@binding" in value)
) {
assertProp(prop, key, value, vm, absent);
}
return value;
}

validateProp 主要就做 3 件事情:处理 Boolean 类型的数据,处理默认数据,prop 断言,并最终返回 prop 的值。

先来看 Boolean 类型数据的处理逻辑:

const prop = propOptions[key];
const absent = !hasOwn(propsData, key);
let value = propsData[key];
// boolean casting
const booleanIndex = getTypeIndex(Boolean, prop.type);
if (booleanIndex > -1) {
if (absent && !hasOwn(prop, "default")) {
value = false;
} else if (value === "" || value === hyphenate(key)) {
// only cast empty string / same name to boolean if
// boolean has higher priority
const stringIndex = getTypeIndex(String, prop.type);
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true;
}
}
}

先通过 const booleanIndex = getTypeIndex(Boolean, prop.type) 来判断 prop 的定义是否是 Boolean 类型的。

function getType(fn) {
const match = fn && fn.toString().match(/^\s*function (\w+)/);
return match ? match[1] : "";
}

function isSameType(a, b) {
return getType(a) === getType(b);
}

function getTypeIndex(type, expectedTypes): number {
if (!Array.isArray(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1;
}
for (let i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i;
}
}
return -1;
}

getTypeIndex 函数就是找到 typeexpectedTypes 匹配的索引并返回。

prop 类型定义的时候可以是某个原生构造函数,也可以是原生构造函数的数组,比如:

export default {
props: {
name: String,
value: [String, Boolean],
},
};

如果 expectedTypes 是单个构造函数,就执行 isSameType 去判断是否是同一个类型;

如果是数组,那么就遍历这个数组,找到第一个同类型的,返回它的索引。

回到 validateProp 函数,通过 const booleanIndex = getTypeIndex(Boolean, prop.type) 得到 booleanIndex

如果 prop.type 是一个 Boolean 类型,则通过 absent && !hasOwn(prop, 'default') 来判断如果父组件没有传递这个 prop 数据并且没有设置 default 的情况,则 value 为 false。

接着判断value === '' || value === hyphenate(key) 的情况,如果满足则先通过 const stringIndex = getTypeIndex(String, prop.type) 获取匹配 String 类型的索引,

然后判断 stringIndex < 0 || booleanIndex < stringIndex 的值来决定 value 的值是否为 true。这块逻辑稍微有点绕,我们举 2 个例子来说明:

例如你定义一个组件 Student:

export default {
name: String,
nickName: [Boolean, String],
};

然后在父组件中引入这个组件:

<template>
<div>
<student name="Kate" nick-name></student>
</div>
</template>

或者是:

<template>
<div>
<student name="Kate" nick-name="nick-name"></student>
</div>
</template>

第一种情况没有写属性的值,满足 value === '',第二种满足 value === hyphenate(key) 的情况,

另外 nickName 这个 prop 的类型是 Boolean 或者是 String,并且满足 booleanIndex < stringIndex,所以对 nickName 这个 propvaluetrue

接下来看一下默认数据处理逻辑:

// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key);
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve;
toggleObserving(true);
observe(value);
toggleObserving(prevShouldObserve);
}

value 的值为 undefined 的时候,说明父组件根本就没有传这个 prop,那么我们就需要通过 getPropDefaultValue(vm, prop, key) 获取这个 prop 的默认值。

我们这里只关注 getPropDefaultValue 的实现,toggleObservingobserve 的作用我们之后会说。

function getPropDefaultValue(
vm: ?Component,
prop: PropOptions,
key: string
): any {
// no default, return undefined
if (!hasOwn(prop, "default")) {
return undefined;
}
const def = prop.default;
// warn against non-factory defaults for Object & Array
if (process.env.NODE_ENV !== "production" && isObject(def)) {
warn(
'Invalid default value for prop "' +
key +
'": ' +
"Props with type Object/Array must use a factory function " +
"to return the default value.",
vm
);
}
// the raw prop value was also undefined from previous render,
// return previous default value to avoid unnecessary watcher trigger
if (
vm &&
vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key];
}
// call factory function for non-Function types
// a value is Function if its prototype is function even across different execution context
return typeof def === "function" && getType(prop.type) !== "Function"
? def.call(vm)
: def;
}

检测如果 prop 没有定义 default 属性,那么返回 undefined通过这块逻辑我们知道除了 Boolean 类型的数据,其余没有设置 default 属性的 prop 默认值都是 undefined

接着是开发环境下对 prop 的默认值是否为对象或者数组类型的判断,如果是的话会报警告,

因为对象和数组类型的 prop,他们的默认值必须要返回一个工厂函数。

接下来的判断是如果上一次组件渲染父组件传递的 prop 的值是 undefined,则直接返回 上一次的默认值 vm._props[key],这样可以避免触发不必要的 watcher 的更新。

最后就是判断 def 如果是工厂函数且 prop 的类型不是 Function 的时候,返回工厂函数的返回值,否则直接返回 def

至此,我们讲完了 validateProp 函数的 Boolean 类型数据的处理逻辑和默认数据处理逻辑,最后来看一下 prop 断言逻辑。

if (
process.env.NODE_ENV !== "production" &&
// skip validation for weex recycle-list child component props
!(__WEEX__ && isObject(value) && "@binding" in value)
) {
assertProp(prop, key, value, vm, absent);
}

在开发环境且非 weex 的某种环境下,执行 assertProp 做属性断言。

function assertProp(
prop: PropOptions,
name: string,
value: any,
vm: ?Component,
absent: boolean
) {
if (prop.required && absent) {
warn('Missing required prop: "' + name + '"', vm);
return;
}
if (value == null && !prop.required) {
return;
}
let type = prop.type;
let valid = !type || type === true;
const expectedTypes = [];
if (type) {
if (!Array.isArray(type)) {
type = [type];
}
for (let i = 0; i < type.length && !valid; i++) {
const assertedType = assertType(value, type[i]);
expectedTypes.push(assertedType.expectedType || "");
valid = assertedType.valid;
}
}

if (!valid) {
warn(getInvalidTypeMessage(name, value, expectedTypes), vm);
return;
}
const validator = prop.validator;
if (validator) {
if (!validator(value)) {
warn(
'Invalid prop: custom validator check failed for prop "' + name + '".',
vm
);
}
}
}

assertProp 函数的目的是断言这个 prop 是否合法。

首先判断如果 prop 定义了 required 属性但父组件没有传递这个 prop 数据的话会报一个警告。

接着判断如果 value 为空且 prop 没有定义 required 属性则直接返回。

然后再去对 prop 的类型做校验,先是拿到 prop 中定义的类型 type,并尝试把它转成一个类型数组,然后依次遍历这个数组,执行 assertType(value, type[i]) 去获取断言的结果,直到遍历完成或者是 validtrue 的时候跳出循环。

const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/;
function assertType(
value: any,
type: Function
): {
valid: boolean,
expectedType: string,
} {
let valid;
const expectedType = getType(type);
if (simpleCheckRE.test(expectedType)) {
const t = typeof value;
valid = t === expectedType.toLowerCase();
// for primitive wrapper objects
if (!valid && t === "object") {
valid = value instanceof type;
}
} else if (expectedType === "Object") {
valid = isPlainObject(value);
} else if (expectedType === "Array") {
valid = Array.isArray(value);
} else {
valid = value instanceof type;
}
return {
valid,
expectedType,
};
}

assertType 的逻辑很简单,先通过 getType(type) 获取 prop 期望的类型 expectedType,然后再去根据几种不同的情况对比 prop 的值 value 是否和 expectedType 匹配,最后返回匹配的结果。

如果循环结束后 valid 仍然为 false,那么说明 prop 的值 valueprop 定义的类型都不匹配,那么就会输出一段通过 getInvalidTypeMessage(name, value, expectedTypes) 生成的警告信息,就不细说了。

最后判断当 prop 自己定义了 validator 自定义校验器,则执行 validator 校验器方法,如果校验不通过则输出警告信息。

响应式

回到 initProps 方法,当我们通过 const value = validateProp(key, propsOptions, propsData, vm)prop 做验证并且获取到 prop 的值后,接下来需要通过 defineReactiveprop 变成响应式。

defineReactive 我们之前已经介绍过,这里要注意的是,在开发环境中我们会校验 propkey 是否是 HTML 的保留属性,并且在 defineReactive 的时候会添加一个自定义 setter,当我们直接对 prop 赋值的时候会输出警告:

if (process.env.NODE_ENV !== "production") {
const hyphenatedKey = hyphenate(key);
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
);
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
);
}
});
}

关于 prop 的响应式有一点不同的是当 vm 是非根实例的时候,会先执行 toggleObserving(false),它的目的是为了响应式的优化,我们先跳过,之后会详细说明。

代理

在经过响应式处理后,我们会把 prop 的值添加到 vm._props 中,比如 key 为 nameprop,它的值保存在 vm._props.name 中,但是我们在组件中可以通过 this.name 访问到这个 prop,这就是代理做的事情

// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key);
}

通过 proxy 函数实现了上述需求。

const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop,
};

export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}

当访问 this.name 的时候就相当于访问 this._props.name

其实对于非根实例的子组件而言,prop 的代理发生在 Vue.extend 阶段,在 src/core/global-api/extend.js 中:

Vue.extend = function (extendOptions: Object): Function {
// ...
const Sub = function VueComponent(options) {
this._init(options);
};
// ...

// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub);
}
if (Sub.options.computed) {
initComputed(Sub);
}

// ...
return Sub;
};

function initProps(Comp) {
const props = Comp.options.props;
for (const key in props) {
proxy(Comp.prototype, `_props`, key);
}
}

这么做的好处是不用为每个组件实例都做一层 proxy,是一种优化手段。

Props 更新

我们知道,当父组件传递给子组件的 props 值变化,子组件对应的值也会改变同时会触发子组件的重新渲染。那么接下来我们就从源码角度来分析这两个过程。

子组件 props 更新

首先,prop 数据的值变化在父组件,我们知道在父组件的 render 过程中会访问到这个 prop 数据,所以当 prop 数据变化一定会触发父组件的重新渲染,那么重新渲染是如何更新子组件对应的 prop 的值呢

父组件重新渲染的最后,会执行 patch 过程,进而执行 patchVnode 函数,patchVnode 通常是一个递归过程,当它遇到组件 vnode 的时候,会执行组件更新过程的 prepatch 钩子函数,在 src/core/vdom/patch.js 中:

function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// ...

let i;
const data = vnode.data;
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
i(oldVnode, vnode);
}
// ...
}

prepatch 函数定义在 src/core/vdom/create-component.js 中:

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // updated props
options.listeners, // updated listeners
vnode, // new parent vnode
options.children // new children
)
}

内部会调用 updateChildComponent 方法来更新 props,注意第二个参数就是父组件的 propData,那么为什么 vnode.componentOptions.propsData 就是父组件传递给子组件prop 数据呢(这个也同样解释了第一次渲染的 propsData 来源)?原来在组件的 render 过程中,对于组件节点会通过 createComponent 方法来创建组件 vnode

export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// ...

// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag);

// ...

const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ""}`,
data,
undefined,
undefined,
undefined,
context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
);

// ...

return vnode;
}

在创建组件 vnode 的过程中,首先从 data 中提取出 propData,然后在 new VNode 的时候,作为第七个参数 VNodeComponentOptions 中的一个属性传入,所以我们可以通过 vnode.componentOptions.propsData 拿到 prop 数据。

接着看 updateChildComponent 函数,它的定义在 src/core/instance/lifecycle.js 中:

export function updateChildComponent(
vm: Component,
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode,
renderChildren: ?Array<VNode>
) {
// ...

// 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;
}

// ...
}

我们重点来看更新 props 的相关逻辑,这里的 propsData 是父组件传递的 props 数据,vm 是子组件的实例。vm._props 指向的就是子组件的 propspropKeys 就是在之前 initProps 过程中,缓存的子组件中定义的所有 propkey。主要逻辑就是遍历 propKeys,然后执行 props[key] = validateProp(key, propOptions, propsData, vm) 重新验证和计算新的 prop 数据更新 vm._props,也就是子组件的 props,这个就是子组件 props 的更新过程

子组件重新渲染

其实子组件的重新渲染有 2 种情况,一个是 prop 值被修改,另一个是对象类型的 prop 内部属性的变化

先来看一下 prop 值被修改的情况,当执行 props[key] = validateProp(key, propOptions, propsData, vm) 更新子组件 prop 的时候,会触发 propsetter 过程,只要在渲染子组件的时候访问过这个 prop 值,那么根据响应式原理,就会触发子组件的重新渲染

再来看一下当对象类型的 prop 的内部属性发生变化的时候,这个时候其实并没有触发子组件 prop 的更新。但是在子组件的渲染过程中,访问过这个对象 prop,所以这个对象 prop 在触发 getter 的时候会把子组件的 render watcher 收集到依赖中,然后当我们在父组件更新这个对象 prop 的某个属性的时候,会触发 setter 过程,也就会通知子组件 render watcherupdate,进而触发子组件的重新渲染

以上就是当父组件 props 更新,触发子组件重新渲染的 2 种情况。

toggleObserving

最后我们在来聊一下 toggleObserving,它的定义在 src/core/observer/index.js 中:

export let shouldObserve: boolean = true;

export function toggleObserving(value: boolean) {
shouldObserve = value;
}

它在当前模块中定义了 shouldObserve 变量,用来控制在 observe 的过程中是否需要把当前值变成一个 Observer 对象。

那么为什么在 props 的初始化和更新过程中,多次执行 toggleObserving(false) 呢,接下来我们就来分析这几种情况

initProps 的过程中:

const isRoot = !vm.$parent;
// root instance props should be converted
if (!isRoot) {
toggleObserving(false);
}
for (const key in propsOptions) {
// ...
const value = validateProp(key, propsOptions, propsData, vm);
defineReactive(props, key, value);
// ...
}
toggleObserving(true);

对于非根实例的情况,我们会执行 toggleObserving(false)然后对于每一个 prop 值,去执行 defineReactive(props, key, value) 去把它变成响应式

回顾一下 defineReactive 的定义:

export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// ...

let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
// ...
},
set: function reactiveSetter(newVal) {
// ...
},
});
}

通常对于值 val 会执行 observe 函数,然后遇到 val 是对象或者数组的情况会递归执行 defineReactive 把它们的子属性都变成响应式的,但是由于 shouldObserve 的值变成了 false,这个递归过程被省略了。为什么会这样呢?

因为正如我们前面分析的,对于对象的 prop 值,子组件的 prop 值始终指向父组件的 prop 值,只要父组件的 prop 值变化,就会触发子组件的重新渲染,所以这个 observe 过程是可以省略的。

最后再执行 toggleObserving(true) 恢复 shouldObservetrue

validateProp 的过程中:

// check default value
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key);
// since the default value is a fresh copy,
// make sure to observe it.
const prevShouldObserve = shouldObserve;
toggleObserving(true);
observe(value);
toggleObserving(prevShouldObserve);
}

这种是父组件没有传递 prop 值对默认值的处理逻辑,因为这个值是一个拷贝,所以我们需要 toggleObserving(true),然后执行 observe(value) 把值变成响应式。

updateChildComponent 过程中:

// 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;
}

其实和 initProps 的逻辑一样,不需要对引用类型 props 递归做响应式处理,所以也需要 toggleObserving(false)