响应式核心API
Ref vs Reactive
从源码的角度来说,它们之间的区别是:
ref使用对象的getter和setter进行数据劫持。
这一点可以看官方的解释:
在 JavaScript 中有两种劫持属性访问的方式:getter /**setters **和 Proxies 。Vue 2使用getter/setters 完全由于需支持更旧版本浏览器的限制。而在 Vue 3 中使用了 Proxy 来创建响应式对象,将 getter/setter 用于 ref。下面的伪代码将会说明它们是如何工作的:
复制 function reactive (obj) {
return new Proxy (obj , {
get (target , key) {
track (target , key)
return target[key]
} ,
set (target , key , value) {
target[key] = value
trigger (target , key)
}
})
}
function ref (value) {
const refObject = {
get value () {
track (refObject , 'value' )
return value
} ,
set value (newValue) {
value = newValue
trigger (refObject , 'value' )
}
}
return refObject
}
两者收集依赖和通知依赖更新的方式都差不多,但是在实现数据劫持的方式上有所不同。
使用proxy替代vue2中的Object.defineProperty来实现数据劫持,有以下优势
不用提前在data中声明,不再需要$set等API
使用Hook的方式创建响应式数据,利于Typescript
支持map、set、weakmap和weakset
但是Proxy只支持对象,如果变量是一个原始值那就难办了。vue3的解决方法是将原始值放到对象的value属性中,并设置getter和setter实现数据劫持,这就解决了原始值响应式的问题。并且ref不仅仅支持接收原始值,也可以传入一个对象。
如果将一个对象赋值给 ref,那么这个对象将通过 **reactive() **转为具有深层次响应式的对象。这也意味着如果对象中包含了嵌套的 ref,它们将被深层地解包。
在使用中也有区别,请看这段代码
复制 a .value = []
b = []
console .log ( isRef (a)) // true
console .log ( isReactive (b)) // false
ref类型的a仍然是响应式的,但是reactive类型的b却不再是响应式的。这是因为变量a的引用被改变了,变量a指向了一个新的对象,而不是之前的reactive对象。而ref改变的只是对象的一个属性,变量a仍然指向ref对象。
因此,当你清空一个列表的时候,你可以这样做:
复制 const list = ref ([])
list . value .push ( ... data)
list .value = [] // 直接改变value的指向,丢弃原数据
readonly
readonly类似于reactive,不同的是readonly返回的对象的所有属性都是只读的,并且这种只读代理是深层的。
复制 const c = readonly ({a : 1 })
console .log (c)
// Proxy{a: 1}
c .a ++ // set operation on key "a" failed: target is readonly.
而readonly的实现方式也很简单,Proxy拦截set和delete操作,下面代码是readonly的proxy handler
。
复制 export const readonlyHandlers : ProxyHandler < object > = {
get : readonlyGet ,
set (target , key) {
if (__DEV__) {
warn (
`Set operation on key " ${ String (key) } " failed: target is readonly.` ,
target
)
}
return true
} ,
deleteProperty (target , key) {
if (__DEV__) {
warn (
`Delete operation on key " ${ String (key) } " failed: target is readonly.` ,
target
)
}
return true
}
}
watchEffect
watchEffect和watch很像,但是watch不需要手动制定监听的源对象,能够自动收集依赖,并且还会在初始化时执行一次。官网的介绍是:watchEffect会立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
复制 let a = ref ( 1 )
const closeEffect = watchEffect (() => {
console .log ( a .value)
})
// 1
a .value ++
// 2
closeEffect () // 停止
a .value ++
需要注意的是,watchEffect
仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await
正常工作前访问到的 property 才会被追踪。
watchEffect可以用这个函数来理解:
复制 function watchEffect (update) {
const effect = () => {
activeEffect = effect
update ()
activeEffect = null
}
effect ()
}
首先将一个全局变量activeEffect
指向effect
副作用函数自身,然后执行用户传入的回调函数update
,update
执行的过程中会出发响应式变量的getter,getter函数会将activeEffect
(也就是effect
函数)收集作为变量的依赖,当该响应式变量更新时就会通知所有依赖更新,也就是重新执行effect
函数。
通常情况下computed和watch已经够用了,watchEffect常见的场景是用于操作DOM。
复制 const count = ref ( 0 )
watchEffect (() => {
document . body .innerHTML = `计数: ${ count .value } `
})
// 更新 DOM
count .value ++
默认情况下,watchEffect回调函数是在组件更新前调用,但是可以通过传递flush参数改为组件更新后执行。
复制 watch (source , callback , {
flush : 'post'
})
watchEffect (callback , {
flush : 'post'
})
响应式工具类API
toRef和toRefs
前面说到reactive使用Proxy来实现响应式,但是这种响应式方式并非完美的,当你通过Proxy对象访问数据时,这会触发setter和getter,此时是响应的,但是当你把原始值属性传递到变量,通过变量去操作数据时,就会失去响应式。
复制 let o = reactive ({num : 1 })
o .num ++ // 响应式
let num = o .num // 失去响应
num ++
console .log ( o .num) // 2
// 如果是对象,那么仍然是响应式的
o = reactive ({num : 1 , obj : {n : 1 }})
let obj = o .obj // 仍然是响应式的
obj .n ++
console .log ( o . obj .n) // 2
这是因为当属性是原始值,赋值操作实际上是将值拷贝了一份保存给变量num,此时num和o.num已经没有关系了,但是如果属性是引用值(对象)时,那么变量obj实际上保存的是对象o.obj的引用,obj操作的仍然是o.obj对象。
toRef和toRefs可以解决这个问题,它为响应式对象上的属性创建 ref,并且这样创建的 ref 与其源属性保持同步。
复制 let o = reactive ({num : 1 , obj : {n : 1 }})
o .num ++ // 响应式
let num = toRef (o , 'num' )
num .value ++
console .log ( o .num) // 3
toRef用于“提取”单个属性,而toRefs则“提取”所有属性,这在使用结构赋值的时候很有用。
复制 let o = reactive ({num : 1 , obj : {n : 1 }})
let {num , obj} = toRefs (o)
obj .n ++
num .value ++
is系列API
vue3提供isReactive、isRef等api用来判断变量的响应式类型,其实背后的原理也很简单,vue在响应式变量中添加了一些标记,通过这些标记就可以很方便地判断出类型。
复制 export const enum ReactiveFlags {
SKIP = '__v_skip' ,
IS_REACTIVE = '__v_isReactive' ,
IS_READONLY = '__v_isReadonly' ,
IS_SHALLOW = '__v_isShallow' ,
RAW = '__v_raw'
}
export function isReactive (value : unknown ) : boolean {
if ( isReadonly (value)) {
return isReactive ((value as Target )[ ReactiveFlags . RAW ])
}
return !! (value && (value as Target )[ ReactiveFlags . IS_REACTIVE ])
}
ref直接将标记作为属性保存,而proxy则是在getter中判断。
复制 export function isRef < T >(r : Ref < T > | unknown ) : r is Ref < T >
export function isRef (r : any ) : r is Ref {
return !! (r && r .__v_isRef === true )
}
console .log ( ref ( 1 ))
/*
dep: undefined
__v_isRef: true
__v_isShallow: false
_rawValue: 1
_value: 1
value: 1
*/
复制 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 . IS_SHALLOW ) {
return shallow
} else if (
key === ReactiveFlags . RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
) .get (target)
) {
return target
}
// ...
}
shallow系列API
vue中的reactive、ref等API都是深度,vue提供一些对应的浅层形式的API,这些API有
以Reactive为例,它实现深层作用的方法是在getter中对对象类型的属性递归执行reactive函数。
复制 function createGetter (isReadonly = false , shallow = false ) {
return function get (target : Target , key : string | symbol , receiver : object ) {
// ...
const res = Reflect .get (target , key , receiver)
if ( ! isReadonly) {
track (target , TrackOpTypes . GET , key)
}
if (shallow) {
return res
}
if ( isRef (res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey (key) ? res : res .value
}
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
}
}
实现shallow的方式也很简单,如果是shallow模式则不对对象类型的属性调用reactive。