一. 了解几个概念
什么是响应式
在开始响应式原理与源码解析之前,需要先了解一下什么是响应式?首先明确一个概念:响应式是一个过程,它有两个参与方:
触发方:数据
响应方:引用数据的函数
当数据发生改变时,引用数据的函数会自动重新执行,例如,视图渲染中使用了数据,数据改变后,视图也会自动更新,这就完成了一个响应的过程。
副作用函数
在Vue
与React
中都有副作用函数的概念,什么是副作用函数?如果一个函数引用了外部的数据,这个函数会受到外部数据改变的影响,我们就说这个函数存在副作用,也就是我们所说的副作用函数。初听这个名字不太好理解,其实 副作用函数就是引用了数据的函数或是与数据相关联的函数。举个例子:
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
<script>
const obj = {
name: 'John',
}
// 副作用函数 effect
function effect() {
app.innerHTML = obj.name
console.log('effect', obj.name)
}
effect()
setTimeout(() => {
obj.name = 'ming'
// 手动执行 effect 函数
effect()
}, 1000);
</script>
</body>
</html>
在上面例子中,effect
函数里面引用了外部的数据obj.name
,如果这个数据发生了改变,则会影响到这个函数,类似effect
的这种函数就是副作用函数。
实现响应式的基本步骤
在上面的例子中,当obj.name
发生了改变,effect
是我们手动执行的,如果能监听到obj.name
的变化,让其自动执行副作用函数effect
,那么就实现了响应式的过程。其实无论是 Vue2
还是 Vue3
,响应式的核心都是 数据劫持/代理、依赖收集、依赖更新
,只不过由于实现数据劫持方式的差异从而导致具体实现的差异。
Vue2
响应式:基于Object.defineProperty()
实现的数据的劫持Vue3
响应式:基于Proxy
实现对整个对象的代理
关于Vue2
的响应式这里不做重点讲解,这篇文章主要关注Vue3
响应式原理的实现。
二. Proxy 与 Reflect
在解析Vue3
的响应式原理之前,首先需要了解两个ES6新增的API:Porxy
与Reflect
。
Proxy
Proxy
: 代理,顾名思义主要用于为对象创建一个代理,从而实现对对象基本操作的拦截和自定义。可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。基本语法:
let proxy = new Proxy(target, handler);
target
: 需要拦截的目标对象handler
: 也是一个对象,用来定制拦截行为 举个例子:
const obj = {
name: 'John',
age: 16
}
const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj与objProxy是否相等',obj === objProxy);
// 输出
[Log] obj.age – 20
[Log] objProxy.age – 20
[Log] obj与objProxy是否相等 – false
这里objProxy
的handler
为空,则直接指向被代理对象,并且代理对象与数据源对象并不全等.如果需要更加灵活的拦截对象的操作,就需要在handler
中添加对应的属性。例如:
const obj = {
name: 'John',
age: 16
}
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
return target[key]
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
target[key] = value
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
return delete target[key]
},
}
const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)
// 输出
[Log] 获取对象属性age值 (example01.html, line 22)
[Log] 16 (example01.html, line 36)
[Log] 设置对象属性age值 (example01.html, line 26)
[Log] 删除对象属性age值 (example01.html, line 30)
[Log] true (example01.html, line 38)
上面的例子,我们在捕获器中定义了set()
、get()
、deleteProperty()
属性,通过对proxy
的操作实现了对obj
的操作拦截。这些属性的触发方法有如下参数:
target
—— 是目标对象,该对象被作为第一个参数传递给new Proxy
key
—— 目标属性名称value
—— 目标属性的值receiver
—— 指向的是当前操作 正确的上下文。如果目标属性是一个getter
访问器属性,则receiver
就是本次读取属性所指向的this
对象。通常,receiver
这就是proxy
对象本身,但是如果我们从proxy
继承,则receiver
指的是从该proxy
继承的对象当然除了以上三个还有一些常用的属性操作方法:
has()
,拦截:in操作符.ownKeys()
,拦截:Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy) Object.keys(proxy)
construct()
,拦截:new
操作等
Reflect
Reflect
: 反射,就是将代理的内容反射出去。Reflect
与Proxy
一样,也是 ES6
为了操作对象而提供的新 API
。它提供拦截JavaScript
操作的方法,这些方法与Proxy handlers
提供的的方法是一一对应的,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。且 Reflect
不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。还是上面的例子
const obj = {
name: 'John',
age: 16
}
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
return Reflect.deleteProperty(target, key)
},
}
const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)
上面的例子中
Reflect.get()
代替target[key]
操作Reflect.set()
代替target[key] = value
操作Reflect.deleteProperty()
代替delete target[key]
操作 当然除了上面的方法还有一些常用的Reflect
方法:
Reflect.construct(target, args)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
三. reactive、ref源码解析
了解了Proxy
与Reflect
,看下Vue3
是如何通过porxy
实现响应式的。其核心是下面要介绍的两个方法:reactive
、ref
.这里依照Vue3.2版本的源码进行解析。
reactive的源码实现
打开源文件,找到文件packages/reactivity/src/reactive.ts
查看源码。
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
刚开始对
target
进行响应式只读判断,如果为true
,则直接返回target
,reactive
实现的核心方法是createReactiveObject()
:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
createReactiveObject()
方法有五个参数:target
: 传入的原始目标对象isReadonly
: 是否是只读的标识baseHandlers
: 为普通对象创建proxy
时的第二个参数handler
collectionHandlers
: 为collection
类型对象创建proxy
时的第二个参数handler
proxyMap
:WeakMap
类型的map
,主要用于存储target
与他的proxy
之间的对应关系
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
源码可以看到,他将对象分为
COMMON
对象(Object
和Array
)与COLLECTION
类型对象(Map
、Set
、WeakMap
、WeakSet
),这样区分的主要目的是为了根据不通的对象类型,来定制不同的handler
在
createReactiveObject()
的前几行,进行了一系列的判断:首先判断
target
是否是对象,如果为false
,直接return
判断
target
是否是响应式对象,如果为true
,直接return
判断是否已经为
target
创建过proxy
了,如果为true
,直接return
判断
target
是否是刚才上面提到的6种对象类型,如果为false
,直接return
如果以上条件都满足,则为
target
创建proxy
,并return
这个proxy
接下来就是根据不同的对象类型,传入不同的handler
的逻辑处理了,主要关注baseHandlers
,里面存在五个属性操作方法,这重点解析get
与set
方法。
源码位置:
packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
get
与依赖收集
可以看到
mutableHandlers
里面就是我们熟悉的各种钩子函数。当我们对proxy
对象进行访问或是修改时,调用相应的函数进行处理。首先看get
里面是如何对访问target
的副作用函数进行收集的:
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
如果
key
值为__v_isReactive
、__v_isReadonly
进行相应的返回,如果key==='__v_raw'
并且WeakMap
中key
为target
的值不为空,则返回target
如果
target
是数组,则 重写/增强 数组对应的方法在这些方法里面调用
track()
进行依赖收集数组元素的查找方法:
includes、indexOf、lastIndexOf
修改原数组 的方法:
push、pop、unshift、shift、splice
对
Reflect.get()
方法的返回值,也就是当前数据对象的属性值res
进行判断,如果res
是普通对象且非只读,则调用track()
进行依赖收集如果
res
是浅层响应,直接返回,如果res
是ref
对象,则返回其value
值如果
res
是 对象类型并且是只读的,则调用readonly(res)
,否则递归调用reactive(res)
方法如果以上都不满足,直接向外返回对应的 属性值
那么核心方法就是如果利用**
track()
**进行依赖收集的处理了,源码在``packages/reactivity/src/effect.ts`
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!isTracking()) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
首先进行是否正在进行依赖收集的判断处理
const targetMap = new WeakMap<any, KeyToDepMap>()
创建一个targetMap
容器,用于保存和当前响应式对象相关的依赖内容,本身是一个WeakMap
类型将对应的 响应式对象 作为
targetMap
的 键,targetMap
的Value是一个depsMap
(属于Map
实例),depsMap
存储的就是和当前响应式对象的每一个key
对应的具体依赖depsMap
的键是响应式数据对象的key,Value是一个deps
(属于Set
实例),这里之所以使用Set
是为了避免副作用函数的重复添加,避免重复调用
以上就是整个**get()**捕获器以及依赖收集的核心流程。
set
与依赖更新
我们在回到baseHandlers
中看Set
捕获器中是如何进行依赖更新的
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value)
oldValue = toRaw(oldValue)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
首先进行旧值的保存
oldValue
如果不是浅层响应,
target
是普通对象,并且旧值是个响应式对象,则执行赋值操作:oldValue.value = value
,返回true
,表示赋值成功判断是否存在对应key值
hadKey
执行
Reflect.set
设置对应的属性值判断对象是原始原型链上的内容(非自定义添加),则不触发依赖更新
根据目标对象不存在对应的 key, 调用
trigger
,进行依赖更新
以上就是整个baseHandlers
关于依赖收集与依赖更新的核心流程。
ref的源码实现
我们知道ref
可以定义基本数据类型、引用数据类型的响应式。来看下它的源码实现:packages/reactivity/src/ref.ts
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly _shallow = false) {
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : convert(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
triggerRefValue(this, newVal)
}
}
}
从上面的函数调用流程可以看出,实现
ref
的核心就是实例化了一个RefImpl
对象。为什么这里要实例化一个RefImpl
对象呢,其目的在于Proxy
代理的目标也是对象类型,无法通过为基本数据类型创建proxy
的方式来进行数据代理。只能把基本数据类型包装为一个对象,通过自定义的get、set
方法进行 依赖收集 和 依赖更新来看
RefImpl
对象属性的含义:_ value:用于
保存ref当前值
,如果传递的参数是对象,它就是用于保存经过reactive函数转化后的值,否则_value
与_rawValue
相同_ rawValue:用于保存当前ref值对应的原始值,如果传递的参数是对象,它就是用于保存转化前的原始值,否则
_value
与_rawValue
相同。这里toRaw()
函数的作用就是将的响应式对象转为普通对象dep:是一个
Set
类型的数据,用来存储当前的ref
值收集的依赖。至于这里为什么用Set
上面我们有阐述,这里也是同样的道理_v_isRef :标记位,只要被
ref
定义了,都会标识当前数据为一个Ref
,也就是它的值标记为true
另外可以很清楚的看到
RefImpl类
暴露给实例对象的get、set
方法是value,所以对于ref
定义的响应式数据的操作我们都要带上**.value**
如果传入的值是对象类型,会调用
convert()
方法,这个方法里面会调用reactive()
方法对其进行响应式处理RefImpl
实例关键就在于trackRefValue(this)
和triggerRefValue(this, newVal)
的两个函数的处理,我们大概也知道它们就是依赖收集、依赖更新,原理基本与reactive
处理方式类似,这里就不在阐述了
五. 总结
对于基础数据类型只能通过
ref
来实现其响应式,核心还是将其包装成一个RefImpl
对象,并在内部通过自定义的get value()
与set value(newVal)
实现依赖收集与依赖更新。对于对象类型,
ref
与reactive
都可以将其转化为响应式数据,但其在ref
内部,最终还是会调用reactive
函数实现转化。reactive
函数,主要通过创建了Proxy实例对象
,通过Reflect
实现数据的获取与修改。
一些参考:
https://github.com/vuejs/vue
https://zh.javascript.info/proxy#reflect
- END -
关于奇舞团
奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。