Vue响应式的本质
vue响应式的本质:函数和数据进行关联。(如果函数中用到了响应式数据,那么当响应式数据变化时,函数会重新执行,template就是个render函数)
Vue3原始值的响应式原理
vue3是基于proxy来实现数据劫持的,然而proxy它只能用于对象,不能用于基础类型数据。因此vue3对基础类型的数据进行了一层包裹。通过使用ref函数,生成RefImpl对象,内部再通过toReactive函数实现响应式。
ref函数
// packages/reactivity/src/ref.ts
export function ref<T extends object>(
value: T
): [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false)
}
// packages/reactivity/src/ref.ts
function createRef(rawValue: unknown, shallow: boolean) {
// 如果已经是 ref 对象,直接返回
if (isRef(rawValue)) {
return rawValue
}
// 创建一个 ref 对象实例
return new RefImpl(rawValue, shallow)
}
ref接收一个原始值作为参数,内部通过createRef方法,生成一个 RefImpl实例对象
rawValue:即传入的原始值
shallow:是一个boolean值,表示创建的响应式变量是深层次还是浅层次。
**里面_rawValue 即为原始值, _value即响应式变量值 **
RefImpl
// packages/reactivity/src/ref.ts
// ref 的实现类 ref函数返回的其实是RefImpl类的实例对象
class RefImpl<T> {
private _value: T
private _rawValue: T //传入的原始值
public dep?: Dep = undefined //调度中心的dep是一个map,键名为监听的对象,键值是个子map。子map中,键名是对应的key,键值是一个set,用来存放watch对应的update方法。而这里的dep只是收集副作用函数
// ref实例下都有一个 __v_isRef 的只读属性,标识它是一个ref
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
// 如果是 浅层响应,则直接将 _rawValue 置为 value,否则通过toRaw()获取其原始值,因为它可能传入的是响应式变量
this._rawValue = __v_isShallow ? value : toRaw(value)
// 如果是 浅层响应,则直接将 _value 置为 value,否则将 value 转换成深响应
this._value = __v_isShallow ? value : toReactive(value)
}
// 拦截 读取 操作
get value() {
// 通过 trackEffects 收集 value 依赖 将副作用函数推入dep中
trackRefValue(this)
// 返回 该ref 对应的 value 属性值,实现自动脱 ref 能力
return this._value
}
// 拦截 设置 操作
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 比较新值和旧值
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
// 转换成响应式数据
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 调用 triggerRefValue, 通过 triggerEffects 派发 value 更新
triggerRefValue(this, newVal)
}
}
}
过程:
- 定义两个私有变量,
_value用来存储原始值变成响应式变量后的数据,_rawvalue用来存放原始值 - 在constructor中,判断其是否是深层次响应式,如果是的话,就调用
toReative函数,将其转化为响应式变量 - 在get中收集副作用函数。
- 在set中判断新值和旧值是否发生改变,如果改变,就调用trigger通知调度中心,触发对应订阅者的update方法
总结:RefImpl类本质上还是通过toReactive函数,将其转化为响应式变量
响应式丢失问题
在刚接触vue3的时候,我们对reactive创建的响应式变量解构,发现再次调用时,不会触发响应式。这是因为{...obj}得到的是一个 新的对象,而且这个新的对象 是 普通对象,(不是通过reactive创建的),自然没有响应式。为了方便我们解构使用,vue封装了toRef
// obj 是响应式数据
const obj = reactive({ foo: 1, bar: 2 })
// 将响应式数据展开到一个新的对象 newObj 中
const newObj = {
...obj
}
effect(() => {
// 在副作用函数内通过新的对象 newObj 读取 foo 属性值
console.log(newObj.foo)
})
// 很显然,此时修改 obj.foo 并不会触发响应
obj.foo = 100
toRef
//用来为源响应式对象上的某个 property 新创建一个 ref
// 第一个参数 obj 是一个响应式数据,第二个参数是 obj 对象的一个健
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]> {
const val = object[key]
// 返回 ref 对象
return isRef(val)
? val
: (new ObjectRefImpl(object, key, defaultValue) as any)
}
toRef的原理:
- 第一个参数是响应式对象,第二个参数是响应式对象中的键名。首先通过isRef判断该键名对应的value是否是响应式对象,如果是直接返回,如果不是,就通过
ObjectRefImpl类创建一个响应式变量返回
返回的响应式ObjectRefImpl实例,它封装了get value和 set value方法,当调用的时候自动触发get方法,它会返回响应式变量_object对应的属性。当修改实例,实际上也是修改_ _object的值。当读取响应式变量,它会读取obj中对应的key的value,修改也是修改obj中的值
props通过toRef解构,父组件修改了props中的值,子组件也能响应式变更。这是因为toRef返回的是
ObjectRefImpl实例,当调用时触发get value(),它返回的就是props对象中同名属性(即调用了props中的数据),修改时也是修改props中的数据。因此只要props是响应式变量,解构出来的属性也能响应式更新。
注意:父组件传递给子组件props的变量必须是响应式变量,只有响应式变量变化,props才会同步更新。而且子组件内部不能直接修改props,需要通过emit()子传父,通知父组件修改。
toRefs
toRefs其实就是遍历响应式对象里的key,判断其键值是否是响应式变量,如果不是,调用toRef,创建响应式变量。最后放在一个对象中返回。
因此,toRef本质上是
将响应式对象的第一层属性值转化为ref(通过ObjectRefImpl类),如果我们想要使用属性值,那么需要通过 .value来进行访问
const obj = reactive({ foo: 1, bar: 2 })
const { foo, bar } = { ...toRefs(obj) }
console.log('foo', foo)
console.log('bar', bar)
模板中自动脱ref(省略.value)
在模板编写中,我们会遇到一个很奇怪的事情,就是ref创建的响应式变量,我们无需.value来获取其值。
setup函数是有返回值的,返回的数据会交给proxyRefs来处理,它会再次创建一个proxy对象,在get中直接返回.value。
let obj = {
a: 1,
b: 2,
}
let _obj1 = new Proxy(obj, {
get(target, key) {
console.log('第一次get调用')
//track
return target[key]
},
set(target, key, value) {
console.log('第一次set调用')
target[key] = {
value: value,
}
//trigger
},
})
console.log(_obj1.a)
let _obj2 = new Proxy(_obj1, {
get(target, key) {
console.log('第二次get调用')
return target[key].value
},
set(target, key, value) {
console.log('第二次set调用')
target[key] = value
},
})
_obj2.a = 1111
console.log(_obj2.a)
Vue非原始值的响应式原理
vue3非原始值的响应式是基于proxy实现的,proxy除了可以代理对象、数组,还可以代理map、set、weakmap、weakset等集合类型。
vue3非原始值响应式通过调用reactive(),基于proxy返回一个代理对象,在get中进行依赖收集,在set中进行通知调度中心,分发依赖,从而实现视图更新
代理Object
属性读取的拦截
一个普通对象的属性读取有:①直接读取obj.key ②for in 循环读取 ③key in obj
对于这些属性的读取,vue都会进行拦截操作,以便数据发生变更时,视图能跟着变化。即 track()
过程:
- 如果被拦截的
key是一个symbol或者原型链上的属性,那么不进行依赖收集,直接返回属性的读取结果(即不做响应式处理) - 实现get()方法,在get中调用
track()方法收集依赖 - 实现set()方法,在set中调用
trigger()方法触发对应副作用,实现视图更新
简单实现reactive:
let bucket = new WeakMap()
let proxyMap = new WeakMap()
const data = {
text: 'hello vue3',
}
function reactive(target) {
const existingProxy = proxyMap.get(target)
if (existingProxy) {
// 已被代理过,直接返回缓存的代理对象
// 避免重复被代理
return existingProxy
}
const proxy = new Proxy(target, baseHandlers)
proxyMap.set(target, proxy)
return proxy
}
const baseHandlers = {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
track(target, key)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
trigger(target, key)
return res
},
}
const _data = reactive(data)
//把effect转为用来注册副作用函数的函数
let activeEffect = null
function effect(fn) {
activeEffect = fn
fn()
}
effect(() => (document.body.innerText = _data.text))
//将activeEffect添入set中
function track(target, key) {
if (!activeEffect) return //depsMap target:map map中 key:set
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
//取出set并循环执行
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
effects && effects.forEach((effect) => effect())
}
setTimeout(() => {
_data.text = 'hello react'
}, 1000)
嵌套属性处理
因为proxy只能实现浅层对象的响应式,如果对象是深层次的,如:
let data={
son:{
a:1
}
}
data.son.a=123 //这样是无法触发对应的set
因此需要递归对象,为其子对象进行代理。
computed源码解读
vue源码中通过effect函数注册副作用,在其options中如果添加了lazy:true,则不立即执行副作用函数。而是将副作用函数返回
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
// 省略部分代码
// 只有非 lazy 的时,才执行副作用函数
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
// 将副作用函数作为返回值返回
return runner
}
computed 本质上就是一个懒加载的副作用函数,在effect函数中通过lazy进行控制,实现懒执行。
computed函数调用签名
// packages/reactivity/src/computed.ts
// 只读的
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions
): ComputedRef<T>
// 可写的
export function computed<T>(
options: WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
)
-
第一种情况,computed接收一个
getter作为参数,返回一个不可修改的响应式ref对象const count = ref(1) // computed 接受一个 getter 函数 const plusOne = computed(() => count.value + 1) console.log(plusOne.value) // 2 plusOne.value++ // 错误 -
第二种情况,接收一个具有
set和get的options对象,返回一个可修改的响应式ref对象const count = ref(1) const plusOne = computed({ // computed 函数接受一个具有 get 和 set 函数的 options 对象 get: () => count.value + 1, set: val => { count.value = val - 1 } }) plusOne.value = 1 console.log(count.value) // 0
实现原理
-
在第一种情况下,它会判断传入的第一个参数是否是函数,如果是函数,赋值给定义好的getter,同时,
setter赋值为不进行任何操作 NOOP// 判断 getterOrOptions 参数 是否是一个函数 const onlyGetter = isFunction(getterOrOptions) if (onlyGetter) { // getterOrOptions 是一个函数,则将函数赋值给取值函数getter getter = getterOrOptions setter = __DEV__ ? () => { //dev环境下 setter为提示,prod环境下不进行操作 console.warn('Write operation failed: computed value is readonly') } : NOOP } else { // getterOrOptions 是一个 options 选项对象,分别取 get/set 赋值给取值函数getter和赋值函数setter getter = getterOrOptions.get setter = getterOrOptions.set } -
第二种情况下,传入options对象,就将传入的get和set函数分别赋值给setter和getter
// getterOrOptions 是一个 options 选项对象,分别取 get/set 赋值给取值函数getter和赋值函数setter getter = getterOrOptions.get setter = getterOrOptions.set -
处理完setter和getter后,会通过
ComputedRefImpl类,创建出一个ref对象并返回// 实例化一个 computed 实例 const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR) if (__DEV__ && debugOptions && !isSSR) { cRef.effect.onTrack = debugOptions.onTrack cRef.effect.onTrigger = debugOptions.onTrigger } return cRef as any
ComputedRefImpl类的实现
因为computed最后会返回一个
ComputedRefImpl类的实例( 即ref响应式变量 ),它有get value() 和 set value()两个方法,如果外界读取了value属性,就会触发get value(),在该方法中手动调用trackRefValue,添加依赖。ComputedRefImpl内部用_value来缓存上一次的值,_dirty标识是否需要重新计算值,如果为true,则需要重新计算,如果false。直接返回_value即实现懒加载。当计算属性依赖的响应式数据变更时,手动触发
triggerRefValue,触发响应式。而只有手动修改.value的时候,才会触发对应的set value(),修改深层次属性是没有反应的
// packages/reactivity/src/computed.ts
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
// value 用来缓存上一次计算的值
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean
// dirty标志,用来表示是否需要重新计算值,为 true 则意味着具有副作用,需要重新计算
public _dirty = true
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
// getter的时候,不派发通知
if (!this._dirty) {
this._dirty = true
// 当计算属性依赖响应式数据变化时,手动调用 triggerRefValue 函数 触发响应式
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
// 获取原始对象
const self = toRaw(this)
// 当读取 value 时,手动调用 trackRefValue 函数进行追踪 track需要传入一个对象,然后会将activeEffect推入到对应的set中等待触发
trackRefValue(self)
// 只有副作用才计算值,并将得到的值缓存到value中
if (self._dirty || !self._cacheable) {
// 将dirty设置为 false, 下一次访问直接使用缓存的 value 中的值
self._dirty = false
self._value = self.effect.run()!
}
// 返回最新的值
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
注意:
computed传入对象写法,它会返回一个computedRef实例,如果是以函数形式写的,当它依赖的响应式数据变更时,它会手动触发trigger。 如果是以对象形式写的,只有修改**
computedRef.value才会触发set方法**。
const model=computed({
set(newValue){},
get(){}
})
//当model.value=xxx 时,才会触发对应的set
//如果是model.value.name=xx,是不会触发对应的set的,这是因为computedRef内部通过get value()和set value()追踪触发依赖,并不是深层次响应式
如果想要实现深层次响应式,需要用一些奇技淫巧
//get返回一个proxy,它修改时触发proxy的set方法,然后修改原来的值。set除非修改model.value才会触发
const model = computed({
get() {
return new Proxy(form, {
set(obj, key, newValue) {
obj[key] = newValue
return true
},
})
},
set(newValue) {
form.name = newValue //修改props中的数据
console.log('newValue', newValue)
},