Vue2响应式原理
Vue2 的响应式原理
ps:在读本章之前默认你已经对 Object.defineProperty 有一定程度的理解,然后大家会在本篇中经常看到Dev,这是由于我们在开发时需要看一些警告来帮助我们写更好的代码,但是进入到生产环境我们并不希望用户的控制台会看到一堆警告,还有一些例如 mock、ssr 都是服务端渲染相关的,不多作讲解,还有一些 vue3 的东西也不多在这里多作讲解
想了很久决定出的第一篇文章就是响应式系统的一个讲解吧,响应式是我们面试的时候非常经常考的一个题目,而且响应式系统也是我们学习 Vue 非常重要的一个内容,是重要的组成部分。但其本质其实也就是数据驱动视图主动更新。
首先的话我就先介绍 Vue2 的响应式原理
Vue2 在具体上其实就是通过几个核心部件来实现响应式:
- Obsever:监视者类,监视数据的变化,在数据变化时告诉通知者
- Dep,通知者类,通知订阅者更新视图,因为一个数据可能被多处使用,所以一个通知者会存储多位订阅者
- Watcher,订阅者类,用于存储数据变化后要执行的更新函数,调用更新函数可以使用新的数据更新视图
- Schduler,调度器,调度器会维护一个执行队列,在队列中同一个 watcher 只会存在一次,队列中的 watcher 不是立即执行,它会在 nextTick 后触发
再具体剖析这几个核心部件之前,我想我们需要先理解一下在 vue2 中 new 一个 vue 实例会经历哪些过程。
初始化
initMixin
源码:src/core/instance/init.t s
在初始化一个 vue 的时候会调用 init 方法,这块的代码主要在 initMixin 里,我们简单看一下。
export function initMixin(Vue: typeof Component) {
// 绑定初始化方法
Vue.prototype._init = function (options?: Record<string, any>) {
··· // 上面大概对一些属性初始化,合并option等
initLifecycle(vm) // 初始化实例的属性 $refs等
initEvents(vm) // 初始化事件:$on 等
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate', undefined, false /* setContext */) // 调用钩子函数
initInjections(vm) // inject
initState(vm) // 重头!!! 初始化组建数据
initProvide(vm) // provide
callHook(vm, 'created') // 调用钩子函数
···
}
}
初始化这里调用了很多方法,每个方法都做着不同的事,而关于响应式主要就是组件内的数据 props、data。而这一块的代码主要就在 initState 里面,所以下面我们就简单看一下 initState
initState
源码:src/core/instance/state.ts
export function initState(vm: Component) {
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props); // 初始化props
initSetup(vm); // 组合式开发的api vue2本不该有
if (opts.methods) initMethods(vm, opts.methods); // 初始化methods
if (opts.data) {
initData(vm); // 初始化data
} else {
// 如果data为空,就默认赋值为空对象,并监听
const ob = observe((vm._data = {}));
ob && ob.vmCount++;
}
if (opts.computed) initComputed(vm, opts.computed); // 初始化 computed
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch); // 初始化 watch
}
}
简单看一下,就是读取 Vue 里绑定的 options 里的属性,然后调用一堆初始化,不多作讲解,直奔主题去找响应式相关的 initProps 和 initData
initProps
源码:src/core/instance/state.ts
function initProps(vm: Component, propsOptions: Object) {
// 读取父组件传入的props
const propsData = vm.$options.propsData || {};
// 创建最终的props
const props = (vm._props = shallowReactive({}));
// 采用数组来取代动态读取props的keys,通过数组存放 props 的 key,就算 props 值空了,key 也会在里面
const keys: string[] = (vm.$options._propKeys = []);
const isRoot = !vm.$parent;
// 转化根实例的props
if (!isRoot) {
toggleObserving(false);
}
for (const key in propsOptions) {
keys.push(key);
// 校验 props 类型、default 属性等
const value = validateProp(key, propsOptions, propsData, vm);
// 在开发环境中
if (__DEV__) {
// hyphenate其实就将小驼峰转化连字符形式
const hyphenatedKey = hyphenate(key);
// 检测是否是保留属性
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
// `hyphenatedKey 是保留属性,不能用作组件 prop`
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
);
}
// 把 props 设置成响应式的
defineReactive(
props,
key,
value,
() => {
// 如果用户修改 props 发出警告
if (!isRoot && !isUpdatingChildComponent) {
// `避免直接改变 prop`
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
);
}
},
true /* shallow */
);
} else {
// 生产环境中直接设置props为响应式
defineReactive(props, key, value, undefined, true /* shallow */);
}
// 把不在默认 vm 上的属性,代理到实例上
// 可以让 vm._props.xx 通过 vm.xx 访问
if (!(key in vm)) {
proxy(vm, `_props`, key);
}
}
toggleObserving(true);
}
分析一下上面的代码可以发现主要就是做以下这些事:
- 遍历父组件传进来的 props 列表
- 校验每个属性的命名、类型、default 属性等,都没有问题就调用 defineReactive 设置成响应式
- 然后用 proxy() 把属性代理到当前实例上,如把 vm._props.xx 变成 vm.xx,就可以访问
initData
源码:src/core/instance/state.ts
function initData(vm: Component) {
// 读取data 如果data是函数就调用getData函数获取,否则直接获取
let data: any = vm.$options.data;
data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
// 看data是否是一个对象
if (!isPlainObject(data)) {
data = {};
__DEV__ &&
warn(
"data functions should return an object:\n" +
"https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
vm
);
}
// proxy data on instance
// 获取当前实例的 data 属性名集合
const keys = Object.keys(data);
// 获取当前实例的 props
const props = vm.$options.props;
// 获取当前实例的 methods 对象
const methods = vm.$options.methods;
let i = keys.length;
// 遍历key
while (i--) {
const key = keys[i];
// 开发环境下判断 methods 里的方法是否存在于 props 中
if (__DEV__) {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
);
}
}
// 开发环境下判断 data 里的属性是否存在于 props 中
if (props && hasOwn(props, key)) {
__DEV__ &&
warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
);
} else if (!isReserved(key)) {
// 都不重名的情况下,代理到 vm 上
// 可以让 vm._data.xx 通过 vm.xx 访问
proxy(vm, `_data`, key);
}
}
// 监听 data
const ob = observe(data);
ob && ob.vmCount++;
}
这里主要做的是:
- 初始化一个 data,并拿到 keys 集合
- 遍历 keys 集合,来判断有没有和 props 里的属性名或者 methods 里的方法名重名的
- 没有问题就通过 proxy() 把 data 里的每一个属性都代理到当前实例上,就可以通过 this.xx 访问了
- 最后再调用 observe 监听整个 data
把这些都讲完之后,我们就可以发现,最主要的监听就是在 observe 和 defineReactive 这两个方法上
🌟 Observer
observe
源码:src/core/observer/index.ts
首先要介绍的方法是 observe,其主要作用就是给数据加上监视器,可以理解为为 Observer 即将开启前做的一些合规检测
先看一下源码:
export function observe(
value: any,
shallow?: boolean, // 浅响应
ssrMockReactivity?: boolean // 服务端渲染 ssr
): Observer | void {
// 判断值是否已经绑定(缓存)完了,直接返回缓存的对象
if (value && hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
return value.__ob__;
}
if (
shouldObserve &&
(ssrMockReactivity || !isServerRendering()) &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value.__v_skip /* ReactiveFlags.SKIP */ &&
!isRef(value) &&
!(value instanceof VNode)
) {
// 创建监听者
return new Observer(value, shallow, ssrMockReactivity);
}
}
可以看到源码本质上就是做了两个检测,第一个 if 语句就是判断值是否已经绑定(缓存)完了,直接返回缓存的对象;第二个 if 语句就是做了一些判断,先是用系统的参数,然后是判断值是否是数组或者对象,是否可添加属性,是否是 VNODE 类型等。当这些判断都通过了之后,就给没有添加 Observer 的数据添加一个 Observer,也就是监听者。
Observer
源码:src/core/observer/index.ts
这是一个类,作用是把一个正常的数据成可观测的数据,先看一下源码:
export class Observer {
dep: Dep; // 通知者类
vmCount: number; // 根上的vm数量
// 构造函数
constructor(
public value: any,
public shallow = false,
public mock = false
) {
// this.value = value 已不用
// 根据是否是ssr创建不同数组
this.dep = mock ? mockDep : new Dep();
this.vmCount = 0;
// 给 value 添加 __ob__ 属性,值为value 的 Observe 实例
// 表示已经变成响应式了,目的是对象遍历时就直接跳过,避免重复操作
def(value, "__ob__", this);
// 类型判断
if (isArray(value)) {
if (!mock) {
// 判断数组是否有__proto__
if (hasProto) {
// 如果有就重写数组的方法
(value as any).__proto__ = arrayMethods;
} else {
// 没有就通过 def,也就是Object.defineProperty 去定义属性值
for (let i = 0, l = arrayKeys.length; i `< l; i++) {
const key = arrayKeys[i];
def(value, key, arrayMethods[key]);
}
}
}
if (!shallow) {
// 监听数组里所有的元素
this.observeArray(value);
}
} else {
// 如果是对象类型,遍历对象所有属性,转为响应式对象,也是动态添加 getter 和 setter,实现双向绑定
const keys = Object.keys(value);
for (let i = 0; i `< keys.length; i++) {
const key = keys[i];
defineReactive(
value,
key,
NO_INITIAL_VALUE,
undefined,
shallow,
mock
);
}
}
}
// 监听数组里所有的元素
observeArray(value: any[]) {
for (let i = 0, l = value.length; i `< l; i++) {
observe(value[i], false, this.mock);
}
}
}
这里主要做的是:
- 给当前 value 打上已经是响应式属性的标记,避免重复操作
- 然后判断数据类型
- 如果是对象,就遍历对象,调用 defineReactive()创建响应式对象
- 如果是数组,就遍历数组,调用 observe()对每一个元素进行监听
defineReactive()
源码:src/core/observer/index.ts
export function defineReactive(
obj: object,
key: string,
val?: any,
customSetter?: Function | null,
shallow?: boolean,
mock?: boolean,
observeEvenIfShallow = false
) {
// 创建 dep 实例
const dep = new Dep();
// 拿到对象的属性描述符 就是enumerable之类的
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// 获取自定义的 getter 和 setter
const getter = property && property.get;
const setter = property && property.set;
if (
(!getter || setter) &&
(val === NO_INITIAL_VALUE || arguments.length === 2)
) {
val = obj[key];
}
// 如果 val 是对象并且不是浅响应的话就递归监听
// 递归调用 observe 就可以保证不管对象结构嵌套有多深,都能变成响应式对象
let childOb = shallow ? val && val.__ob__ : observe(val, false, mock);
// 截持对象属性的 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 拦截 getter,当取值时会触发该函数
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// 进行依赖收集
// 初始化渲染 watcher 时访问到需要双向绑定的对象,从而触发 get 函数
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key,
});
} else {
dep.depend();
}
if (childOb) {
childOb.dep.depend();
if (isArray(value)) {
dependArray(value);
}
}
}
return isRef(value) && !shallow ? value.value : value;
},
// 拦截 setter,当值改变时会触发该函数
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
// 判断是否发生变化
if (!hasChanged(value, newVal)) {
return;
}
if (__DEV__ && customSetter) {
customSetter();
}
if (setter) {
// 有setter就调用
setter.call(obj, newVal);
} else if (getter) {
// 没有 setter 的访问器属性
return;
} else if (!shallow && isRef(value) && !isRef(newVal)) {
// vue3的ref
value.value = newVal;
return;
} else {
val = newVal;
}
// 非浅响应情况下,新值是对象的话递归监听
childOb = shallow
? newVal && newVal.__ob__
: observe(newVal, false, mock);
// 通知更新
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value,
});
} else {
dep.notify();
}
},
});
return dep;
}
这里主要做的是:
- 先初始化一个 dep 实例
- 在非浅响应情况下,如果是对象就调用 observe,递归监听,以保证不管结构嵌套多深,都能变成响应式对象
- 然后调用 Object.defineProperty() 劫持对象属性的 getter 和 getter
- 如果获取时,触发 getter 会调用 dep.depend() 把观察者 push 到依赖的数组 subs 里去,也就是依赖收集
- 如果更新时,触发 setter 会做以下操作
- 新值没有变化直接跳出
- 有 setter 就调用,没有且有 getter 就直接退出
- 如果新值是对象就调用 observe() 递归监听
- 最后调用 dep.notify() 派发更新
从这里可以看出来核心是 dep 了,无论是依赖收集还是通知更新等,都是依赖 dep
🌼 Dep
Dep
源码:src/core/observer/dep.ts
从上面看完之后我们就可以发现 Dep 是整个依赖收集和通知更新的核心,接下来先看一下源码,由于源码已经在这几年修改了很多,但是核心还是不变,所以我们依旧把源码中的 DepTarget 当上文说到的 Watcher。
// DepTarget 在这里面其实就是 Watcher 的接口,不过其规定的接口函数没有 Watcher 实现那么多。
export interface DepTarget extends DebuggerOptions {
id: number;
addDep(dep: Dep): void;
update(): void;
}
export default class Dep {
static target?: DepTarget | null;
id: number;
subs: Array<DepTarget | null>; // 观察者
// pending subs cleanup
_pending = false;
constructor() {
this.id = uid++;
this.subs = [];
}
// 添加观察者
addSub(sub: DepTarget) {
this.subs.push(sub);
}
// 移除观察者
removeSub(sub: DepTarget) {
// 原本函数是:remove(this.subs, sub)
// 但是拥有大量 sub 的 deps 在 Chromium 中清理速度非常慢 为了解决这个问题,我们暂时取消设置 sub ,并在下一次调度程序刷新时清除它们。
this.subs[this.subs.indexOf(sub)] = null;
if (!this._pending) {
this._pending = true;
pendingCleanupDeps.push(this);
}
}
depend(info?: DebuggerEventExtraInfo) {
if (Dep.target) {
// 调用 Watcher 的 addDep 函数
Dep.target.addDep(this);
// 开发环境下的 debug 调试会对 watcher 做跟踪
if (__DEV__ && info && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
...info,
});
}
}
}
notify(info?: DebuggerEventExtraInfo) {
// 序列化 sub
const subs = this.subs.filter((s) => s) as DepTarget[];
if (__DEV__ && !config.async) {
// 如果不在异步条件下运行,则不会在调度程序中对订阅进行排序,所以需要现在就对它们进行排序以确保它们以正确的顺序触发
subs.sort((a, b) => a.id - b.id);
}
for (let i = 0, l = subs.length; i < l; i++) {
// 遍历所有 watcher 实例数组
const sub = subs[i];
if (__DEV__ && info) {
// 开发环境下的 debug 调试会对 watcher 触发事件
sub.onTrigger &&
sub.onTrigger({
effect: subs[i],
...info,
});
}
// 调用 sub 内置的更新函数
sub.update();
}
}
}
// 同一时间全局只有一个观察者使用
Dep.target = null;
// 赋值观察者
const targetStack: Array<DepTarget | null | undefined> = [];
export function pushTarget(target?: DepTarget | null) {
targetStack.push(target);
Dep.target = target;
}
export function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
根据代码可以看出这是一个类,它实际上就是对 Watcher 的一种管理:
- 这里首先初始化一个 subs 数组,用来存放依赖,也就是观察者,谁依赖这个数据,谁就在这个数组里,然后定义几个方法来对依赖添加、删除、通知更新等
- 另外它有一个静态属性 target,这是一个全局的 Watcher,也表示同一时间只能存在一个全局的 Watcher,然后还有一个 target 的任务队列。
可以发现这里面的派发更新主要是调用其本身只带的 update 函数,大家可能会有疑惑,这里都没有看到 Watcher 相关的代码,接下来我们在看一个函数。
mountComponent
export function mountComponent(
vm: Component,
el: Element | null | undefined,
hydrating?: boolean
): Component {
// 绑定实例
vm.$el = el
// 查看是否有render渲染函数
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (__DEV__) {
// 看警告应该就知道是干什么了吧哈哈
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'您正在使用 Vue 的仅限运行时的构建,其中模板“编译器不可用。要么将模板预编译为渲染函数,要么使用编译器包含的构建'+
vm
)
} else {
warn(
'无法挂载组件:未定义模板或渲染函数',
vm
)
}
}
}
// 调用钩子函数
callHook(vm, 'beforeMount')
let updateComponent
// 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染,第一个 if 语句中的其他代码基本就是方便调试等
if (__DEV__ && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 配置 Watcher 的一些属性
const watcherOptions: WatcherOptions = {
// 当触发更新的时候,会在更新之前调用
before() {
if (vm._isMounted && !vm._isDestroyed) {
// 调用生命周期钩子函数
callHook(vm, 'beforeUpdate')
}
}
}
// 方便调试
if (__DEV__) {
watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
}
// 重点在这:为当前组件实例设置观察者,监控 updateComponent 函数得到的数据,下面有介绍
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
)
hydrating = false
// 这个应该算是 vue3 的东西
// 用于刷新的刷新缓冲区:pre 观察程序在 setup() 中的队列
const preWatchers = vm._preWatchers
if (preWatchers) {
for (let i = 0; i `< preWatchers.length; i++) {
preWatchers[i].run()
}
}
/
// 没有老的 vnode,说明是首次渲染
if (vm.$vnode == null) {
vm._isMounted = true
// 调用生命周期钩子函数
callHook(vm, 'mounted')
}
return vm
}
现在对于这段源码只需要看到,这个函数是挂载函数,然后在挂载的时候会 new 一个 Watcher,这个作用呢就相当于依赖收集,也就是收集需要响应式的地方,下面配合 Watchr 会细致地说这个过程。
🔥 Watcher
` 源码:src/core/observer/watcher.ts
Watcher 也是一个类,也叫观察者(订阅者),是上文源码中的 DepTarget,不过有点出入的是 DepTarget 规定的需要实现的接口比 Watcher 少,Watcher 可以做的事更多。
先看 Watcher 的源码捋一下吧
// 最后一个 implements DepTarget 明确的告诉我们 Watcher 其实就是 DepTarget 的实现。
export default class Watcher implements DepTarget {
··· // 声明配置项的 Type
constructor(
vm: Component | null,
expOrFn: string | (() => any),
cb: Function,
options?: WatcherOptions | null,
isRenderWatcher?: boolean
) {
// 副作用函数,vue3 时候会讲,这边其实不是很重要
recordEffectScope(
this,
// if the active effect scope is manually created (not a component scope),
// prioritize it
activeEffectScope && !activeEffectScope._vm
? activeEffectScope
: vm
? vm._scope
: undefined
)
if ((this.vm = vm) && isRenderWatcher) {
vm._watcher = this
}
// 初始化options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
if (__DEV__) {
this.onTrack = options.onTrack
this.onTrigger = options.onTrigger
}
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.post = false
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
// new 两个 dep 数组,原因下面会提到
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = __DEV__ ? expOrFn.toString() : ''
// 解析 getter
if (isFunction(expOrFn)) {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
__DEV__ &&
warn(
`监视路径:“${expOrFn}”失败, 观察程序只接受简单的点分隔路径。如果要完全控制,请改用函数。`
vm
)
}
}
// 如果不是懒加载,就会执行 this.get 函数
this.value = this.lazy ? undefined : this.get()
}
get() {
// 该函数用于缓存 Watcher
// 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 调用回调函数,也就是upcateComponent,对需要双向绑定的对象求值,从而触发依赖收集
value = this.getter.call(vm, vm)
} catch (e: any) {
··· // 错误处理
} finally {
// "深度监听
if (this.deep) {
traverse(value)
}
// 恢复Watcher,Dep 中有讲到
popTarget()
// 清理不需要了的依赖
this.cleanupDeps()
}
return value
}
// 依赖收集时调用,请注意 newDepIds 和 depIds 是不一样的
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)) {
// 把当前 Watcher push 进数组
dep.addSub(this)
}
}
}
// 清理不需要的依赖, 主要用于 移除订阅
/*
* 这里做的主要是:
* 先遍历上一次添加的实例数组 deps,移除 dep.subs 数组中的 Watcher 的订阅
* 然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换
* 再把 newDepIds 和 newDeps 清空
*/
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp: any = 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
}
// 派发更新时调用,可以结合 Observe 类 setter 和 Dep 的 notify 看
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 组件数据更新会走这里,这是 schduler 部分的,待会会讲
queueWatcher(this)
}
}
// 执行 watcher 的回调
run() {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(
this.cb,
this.vm,
[value, oldValue],
this.vm,
info
)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
// lazy Watcher 时才会用到
evaluate() {
this.value = this.get()
this.dirty = false
}
// 调用 Watcher 收集的所有 Deps 的 depend 函数
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
// 从所有依赖项的 Dep 列表中删除自己。
teardown() {
if (this.vm && !this.vm._isBeingDestroyed) {
remove(this.vm._scope.effects, this)
}
if (this.active) {
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
if (this.onStop) {
this.onStop()
}
}
}
}
我们此时就可以联合上面的 mountComponent 函数代码讲一下依赖收集,先放出上面 mountComponent 代码中 new 一个 Watcher 的参数
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true /* isRenderWatcher */
);
结合两个和之前的 dep 等,我们大概就可以大概解释依赖收集的过程:
- 挂载之前会实例化一个渲染 watcher ,进入 watcher 构造函数里就会执行 this.get() 方法
- 看 watcher 构造函数代码的最后一行
- 然后就会执行 pushTarget(this),就是把 Dep.target 赋值为当前渲染 watcher 并压入栈(为了恢复用)
- 然后执行 this.getter.call(vm, vm),也就是上面的 updateComponent() 函数,里面就执行了 vm._update(vm._render(), hydrating)
- 接着执行 vm._render() 就会生成渲染 vnode,这个过程中会访问 vm 上的数据,就触发了数据对象的 getter
- 每一个对象值的 getter 都有一个 dep,在触发 getter 的时候就会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)
- 这个首先要看 Obeserver 类的劫持 get 中的 dep.depend 然后再看 Dep 类中的 depend 方法,就捋清楚这个过程了
- addDep 这里会做一些判断,以确保同一数据不会被多次添加,接着把符合条件的数据 push 到 subs 里,到这就已经完成了依赖的收集,不过到这里还没执行完,如果是对象还会递归对象触发所有子项的 getter,还要恢复 Dep.target 状态,以及清理不需要的依赖
- 对应函数 traverse、popTarget、cleanupDeps
补充说明一下为什么需要两个 dep 数组:因为 Vue 是数据驱动的,每次数据变化都会重新 render,也就是说 vm.render() 方法就又会重新执行,再次触发 getter,所以用两个数组表示,新添加的 Dep 实例数组 newDeps 和上一次添加的实例数组 deps
讲完这些之后,我们发现代码中还有一个没讲,就是 queueWatcher(),他是做什么呢,其实就是我们上面讲的最后一个核心部件 Schduler
🌴 Scheduler
queueWatcher()
源码:src/core/observer/scheduler.ts
这是一个队列,也是 Vue 在做派发更新时的一个优化点。就是说在每次数据改变的时候不会都触发 watcher 回调,而是把这些 watcher 都添加到一个队列里,然后在 nextTick 后才执行
解决的问题大概就是这样一个情况:一个交给 watcher 的函数,它里面用到了属性 a、b、c、d,那么 a、b、c、d 属性都会记录依赖,于是下面的代码将会触发 4 次更新
state.a = "new data";
state.b = "new data";
state.c = "new data";
state.d = "new data";
这样肯定是不合理的
接下来看源码吧
export function queueWatcher(watcher: Watcher) {
// 获得 watcher 的 id
const id = watcher.id;
// 判断当前 id 的 watcher 有没有被 push 过
if (has[id] != null) {
return;
}
// 判断是不是全局唯一的 watcher 并且不是递归,noRecurse 其的一个属性
if (watcher === Dep.target && watcher.noRecurse) {
return;
}
has[id] = true;
if (!flushing) {
// 如果没有进入刷新队列的状态,就将它放到刷新队列
queue.push(watcher);
} else {
// 如果已经进入,从后往前找,找到第一个待插入的 id 比当前队列中的 id 大的位置,将 watcher 插入到队列中
let i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// 判断是不是在等待更新状态
if (!waiting) {
waiting = true;
if (__DEV__ && !config.async) {
flushSchedulerQueue();
return;
}
// 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
nextTick(flushSchedulerQueue);
}
}
看完上面代码可以发现主要做的是:
- 先用 has 对象查找 id,保证同一个 watcher 只会 push 一次
- 如过没进入刷新状态,就将 watcher 直接放入任务队列,如果在执行 watcher 期间又有新的 watcher 插入进来就会到这里,然后从后往前找,找到第一个待插入的 id 比当前队列中的 id 大的位置,插入到队列中。
- 最后通过 waiting 保证 nextTick 只会调用一次
flushSchedulerQueue()
源码:src/core/observer/scheduler.ts
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// 根据 id 排序,有如下条件
// 1.组件更新需要按从父到子的顺序,因为创建过程中也是先父后子
// 2.组件内我们自己写的 watcher 优先于渲染 watcher
// 3.如果某组件在父组件的 watcher 运行期间销毁了,就跳过这个 watcher
queue.sort(sortCompareFn)
// 不要缓存队列长度,因为遍历过程中可能队列的长度发生变化
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// 执行 beforeUpdate 生命周期钩子函数
watcher.before()
}
id = watcher.id
has[id] = null
// 执行组件内我们自己写的 watch 的回调函数并渲染组件
watcher.run()
// 检查并停止循环更新,比如在 watcher 的过程中又重新给对象赋值了,就会进入无限循环
if (__DEV__ && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(`无限循环`),
watcher.vm
)
break
}
}
}
// 重置状态之前,先保留一份队列备份
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// 调用组件激活的钩子 activated
callActivatedHooks(activatedQueue)
// 调用组件更新的钩子 updated
callUpdatedHooks(updatedQueue)
// 移除订阅
cleanupDeps()
// devtool hook
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
这里大概做的就是:
- 先排序队列,排序条件有三点,看注释
- 然后遍历队列,执行对应 watcher.before() 和 wacher.run()。需要注意的是,遍历的时候每次都会对队列长度进行求值,因为在 run 之后,很可能又会有新的 watcher 添加进来,这时就会再次执行到上面的 queueWatcher
- 在这些之后就会调用组件的钩子
callUpdatedHooks
源码:src/core/observer/scheduler.ts
上面调用 callUpdatedHooks() 的时候就会进入这里, 执行 updated 了
function callUpdatedHooks(queue: Watcher[]) {
let i = queue.length;
while (i--) {
const watcher = queue[i];
const vm = watcher.vm;
if (
vm &&
vm._watcher === watcher &&
vm._isMounted &&
!vm._isDestroyed
) {
callHook(vm, "updated");
}
}
}
至此 Vue2 的响应式原理流程的源码基本就分析完毕了,接下来就介绍一下上面流程中的不足之处
缺陷分析
使用 Object.defineProperty 实现响应式对象,还是有一些问题的
- 比如给对象中添加新属性时,是无法触发 setter 的
- 比如不能检测到数组元素的变化
而这些问题,Vue2 里也有相应的解决文案
Vue.set()
源码:src/core/observer/index.ts
给对象添加新的响应式属性时,可以使用一个全局的 API,就是 Vue.set() 方法 set 方法接收三个参数:
- target:数组或普通对象
- key:表示数组下标或对象的 key 名
- val:表示要替换的新值
export function set(
target: any[] | Record<string, any>,
key: any,
val: any
): any {
if (__DEV__ && (isUndef(target) || isPrimitive(target))) {
warn(`不能给undefined赋值`);
}
if (isReadonly(target)) {
__DEV__ && warn(`不能给只读的对象赋值`);
return;
}
const ob = (target as any).__ob__;
// 如果是数组 而且 是合法的下标
if (isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
// 直接使用 splice 就替换,注意这里的 splice 不是原生的,所以才可以监测到,具体看下面
target.splice(key, 1, val);
// ssr
if (ob && !ob.shallow && ob.mock) {
observe(val, false, true);
}
return val;
}
if (key in target && !(key in Object.prototype)) {
// 如果 key 存在于 target 里,就直接赋值,也是可以监测到的
target[key] = val;
return val;
}
// 获取 target.__ob__
if ((target as any)._isVue || (ob && ob.vmCount)) {
__DEV__ &&
warn(
"避免在运行时向 Vue 实例或其根$data添加响应式属性 - 在 data 选项中预先声明它。"
);
return val;
}
// 在 Observer 里介绍过,如果没有这个属性,就说明不是一个响应式对象
if (!ob) {
target[key] = val;
return val;
}
// 然后把新添加的属性变成响应式
defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock);
// 手动派发更新
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ADD,
target: target,
key,
newValue: val,
oldValue: undefined,
});
} else {
ob.dep.notify();
}
return val;
}
这里主要做的是:
- 先判断如果是数组,并且下标合法,就直接使用重写过的 splice 替换
- 如果是对象,并且 key 存在于 target 里,就替换值
- 如果没有
__ob__
,说明不是一个响应式对象,直接赋值返回 - 最后再把新属性变成响应式,并派发更新
重写数组方法
源码:src/core/observer/array.js
import { TriggerOpTypes } from "../../v3";
import { def } from "../util/index";
// 获取数组的原型
const arrayProto = Array.prototype;
// 创建继承了数组原型的对象
export const arrayMethods = Object.create(arrayProto);
// 会改变原数组的方法列表
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
// 重写数组事件
methodsToPatch.forEach(function (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);
// 派发更新
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method,
});
} else {
ob.dep.notify();
}
// 做完我们需要的处理后,再执行原本的事件
return result;
});
});
这里做的主要是:
- 保存会改变数组的方法列表
- 当执行列表里有的方法的时候,比如 push,先把原本的 push 保存起来,再做响应式处理,再执行这个方法
总结
大家如果将这篇文章看完之后,应该就能大致理解这个响应式的原理和流程了,我现在以面试回答的口吻来梳理一下吧
Vue2 的响应式原理依赖于 Obeject.defineProperty 实现,有几个核心部件分别为:Observer、Dep、Watcher、Scheduler,Observer 负责把一个数据变成响应式数据,Dep 负责将其收集记录依赖并且方便通知更新, Watcher 存储更新后要执行的更新函数,Scheduler 负责将 Watcher 放到一个任务队列中,可以避免修改一个对象的不同属性时导致多次更新,其中 Observer 最核心的拦截函数 defineReactive 就是依赖于 Object.defineProperty 实现。
Vue2 的响应式流程:先是 init 时将 Prop 和 Data 调用 defineReactive 和 observe 来将其变成响应式数据,然后在挂载的时候会 new 一个 watcher 类,然后在渲染时触发了数据对象的 getter,然后调用 Dep 类和 Watcher 类关于依赖收集的相关函数,之后在每次数据更新的时候都会触发数据对象的 setter,就会调用 Dep 类和 Watcher 类关于更新的相关函数,但这个函数不会马上更新,而是会放入 Scheduler 部件待 nextTick 后再调用。