# 一、Vue2 解析
本次分析的Vue2版本为2.6。
# 1. 响应式原理
- 从
new Vue
开始,经过了一系列调用 init (opens new window)、initState (opens new window)、initData (opens new window)、observe (opens new window)、new Obsever (opens new window)、defineReactive (opens new window),来看下defineReactive
的实现。
function defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
const value = val;
// 如果存在依赖此数据的Watcher,则进行依赖搜集
if (Dep.target) {
dep.depend();
}
return value;
},
set(newVal) {
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal;
// 数据更新的时候进行派发更新
dep.notify();
},
});
}
- 通过 get (opens new window)、set (opens new window) 监听
Data
中的数据变化,同时为每一个属性创建 Dep (opens new window) 用来搜集使用该Data
的Watcher
,来看下Dep
的实现。
class Dep {
static target;
// subs 存放的 Watcher 对象集合
subs;
constructor() {
this.subs = [];
}
addSub(sub: Watcher) {
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 依赖收集,会调用上面的 addSub 方法
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
- 编译模板,创建 Watcher (opens new window),并将
Dep.target
标识为当前Watcher
,来看下Watcher
的实现。
class Watcher {
constructor(vm, expOrFn) {
// expOrFn 就是 vm._render
this.getter = expOrFn;
this.value = this.get();
}
get() {
Dep.target = this;
// 重新触发_render函数,生成VDom、更新Dom
const value = this.getter.call(this.vm, this.vm);
return value;
}
addDep(dep) {
// 收集当前的Watcher为依赖
dep.addSub(this);
}
update() {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run() {
const value = this.get();
}
}
- 编译模板时,如果使用到了
Data
中的数据,就会触发Data
的get
方法,然后调用Dep.addSub
将Watcher
搜集起来。 - 数据更新时,会触发
Data
的set
方法,然后调用 dep.notify (opens new window),进而调用 watcher.update (opens new window) 方法,将所有使用到这个Data
的Watcher
加入一个异步队列 (opens new window)。 - 最终执行 _render (opens new window) 方法完成页面更新
流程图如下:
# 2. watch
原理
computed
和watch
内部都是利用了watcher
,user watcher
的过程如下:
- Vue 在 initWatch (opens new window) 过程中,创建
Watcher
,并设置标志位 user (opens new window) 为true
,并判断用户是否设置了immediate
为true
,如果是,立即执行回调; watch
的对象 update (opens new window) 时,判断是否设置了sync
为true
,如果是,不加入异步队列,直接更新;Watcher
更新时判断标志位user
是否为true
,如果是,则执行用户传入的 cb (opens new window),把newVal
和oldVal
传入。
# 3. computed
原理
看这个例子:
computed: {
name() {
return `My name is ${this.user.name}`;
}
}
computed watcher
的过程如下:
- Vue 在 initComputed (opens new window) 过程中,创建标志位
lazy
为true
的Watcher
: - 因为初始化的时候
dirty=lazy=true
,会调用 watcher.evaluate (opens new window) 方法进行一次求值 this.getter.call(vm, vm) (opens new window) ,此时会访问this.user.name
,所以会触发其依赖收集。这时候Dep.target
的值为computed watcher
,依赖收集完后,this.user.name
的dep
中就有了computed watcher
; - 然后在
watcher.evaluate
中将dirty
设置为false
; - 如果
Dep.target
存在,则调用 watcher.depend (opens new window) 进行一次render watcher
的收集;
// 创建computed的getter的工厂函数
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
- 当
name
值改变时,会触发set
,然后通知computed watcher
,执行 update (opens new window) 方法,并将dirty
设置为true
。 - 再次访问
computed
属性时,如果dirty
为false
,则不会执行 watcher.evaluate (opens new window) 方法,直接返回之前缓存的值,如果dirty
为true
,则重新计算。
# 二、Vue3 解析
Vue3 的代码在 vue-next (opens new window) 仓库中,本次分析的版本是3.2。响应式部分在reactivity
文件夹中,并且可独立引用。
Vue3 的响应式多了一个副作用函数,即effect
函数,指的是响应式数据在发生变更的时候,要执行的函数。
# 1. 响应式原理
- reactive (opens new window) 函数对包裹的对象进行 proxy (opens new window) 代理,在 get (opens new window) 中利用 track (opens new window) 函数进行依赖收集,在 set (opens new window) 中利用 trigger (opens new window) 派发更新。
function reactive(obj: any) {
const proxy = new Proxy(obj, {
get(target, key, receiver) {
track(target, key);
let res = Reflect.get(target, key);
return res;
},
set: function (target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key);
return res;
}
});
return proxy;
}
- 依赖收集在 targetMap (opens new window) 中,其是一个
WeakMap
,key
是响应式对象,value
是Map
类型的 depsMap (opens new window)。depsMap
的key
是响应式对象的key
,value
是 effect (opens new window) 函数。
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
function trigger(target: any, key: ObjKeyType) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
let deps = depsMap.get(key);
if (deps) {
deps.forEach((efn: EffectFn) => efn());
}
}
function track(target: any, key: ObjKeyType) {
if (effectStack.length === 0) return;
let depMap = targetMap.get(target);
if (!depMap) {
targetMap.set(target, (depsMap = new Map()))
}
let deps = depMap.get(key);
if (!deps) {
depMap.set(key, (deps = new Set()));
}
// 添加栈顶副作用作为依赖
deps.add(getCurrentEffect());
}
- 响应式Data更新的时候会触发 trigger (opens new window),然后从
targetMap
中取出对应的依赖进行更新。 effect
函数的创建时机包括 mountComponent (opens new window)、computed (opens new window)、watch (opens new window) 等。
流程图如下:
# 2. ref
原理
ref (opens new window) 是一个语法糖,返回一个对象,其在get
中调用track
,set
中调用trigger
。
function ref(value) {
const res = {
get value() {
track(res, 'value');
return value;
},
set value(newVal) {
value = newVal;
trigger(res, 'value');
}
};
return res;
}
# 3. Vue3 的computed
原理
- computed (opens new window) 内部用
effect
函数包裹传入的函数getter
,并执行getter
,拿到value
; - 内部
effect
中调用了trigger
,这样computed依赖的值变化的时候,会触发此effect
函数执行,也就能够触发依赖computed
的effect
函数也得到执行; - 构造一个对象,对象的
get
方法中调用了track
,进行了一次依赖收集; - 最后返回构造的对象
function computed(getter) {
let value;
let res;
effect(() => {
value = getter();
trigger(res, 'value');
})
res = {
get value() {
track(this, 'value');
return value;
}
}
return res;
}
# 三、总结
对比 Vue2 和 Vue3 的响应式实现方式的不同,可以看出
- Vue2 使用
Object.defineProperty
进行数据劫持,Vue3 使用Proxy
,后者优势在于可以劫持push
、pop
等方法,也因为在顶层对象直接劫持,可以提高性能。 - 依赖收集器的数据结构有变化,Vue2 的依赖收集在
Dep.subs
中,也就是一个类的数组中,Vue3 的依赖收集在targetMap
中,其是一个WeakMap
。 - Vue3 的响应式结构更加简单,与其他部分耦合性小,并已经独立成包,即可以和其他框架进行结合。
# 四、相关资料
- 图解 Vue 响应式原理 (opens new window)
- 搞懂computed和watch原理,减少使用场景思考时间 (opens new window)
- Vue源码之computed和watch (opens new window)
- Vue.js 技术揭秘 (opens new window)
- vue3.0响应式函数原理 (opens new window)
- 手写Vue3 响应式(Reactivity)模块 (opens new window)
- vue3源码分析(三)—— 响应式系统(reactivity) (opens new window)
- Vue3 深度解析 (opens new window)
← 过滤器 computed和watch →