Vue3响应式API
响应式核心API
Ref vs Reactive
vue3提供ref和reactive这两个响应式API,刚开始看到的时候比较懵逼,不太清楚他们之间的区别,想要理解它们之间的区别还得看源码。
从源码的角度来说,它们之间的区别是:
- reactive使用Proxy进行数据劫持
- ref使用对象的getter和setter进行数据劫持。
这一点可以看官方的解释:
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有
- shallowReactive
以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。