详解Vue3中响应式的特殊处理
脚本之家 / 编程助手:解决程序员“几乎”所有问题!
脚本之家官方知识库 → 点击立即使用
vue2 vs vue3
两个响应式更新的核心区别在于Object.defineProperty
和 Proxy
两个api 问题,经过这两个 api 能解决主要的响应式问题。对于一些情况需要特殊处理
vue2 中不能实现的响应式
- arr.length
- arr[0] = newVal
- obj[newKey] = value
- delete obj.key
对于这些情况 vue2 中通过增加Vue.$set
和重写数组方法来实现。然而对于 vue3 中,因为 proxy
是代理整个对象,所以它天生支持一个Object.defineProperty
不能支持的特性,比如他能侦听到添加新属性,而 Object.defineProperty
因为代理的是每一个 key
所以它对于新增的属性并不能知道。诸如此类,下面列出一些vue3 中不同的响应式处理。
新增属性的更新
proxy 虽然能够监听到新属性的添加,但新增的属性并没有经过像已有属性那样的 getter 收集依赖,也就是并不能触发更新。所以我们的目的变成如何收集响应
首先,我们先看下 vue3 中是如何处理 for...in..
循环的,可以知道的是循环的内部使用了Reflect.ownKeys(obj
) 来获取只属于对象自身拥有的键。所以对于 for..in
循环的拦截就可以清楚了
1 2 3 4 5 6 | const obj = {foo: 1} const ITERATE_KEY = symbol() const p = new Proxy(obj, { track(target, ITERATE_KEY) return Reflect.ownKeys(target) }) |
这里,我们用了一个symbol
的数据作为 收集依赖的key
因为这个是我们在遍历中的拦截操作没有与具体的 key 关联,而是一个整体性的拦截。在触发响应时,只要触发这个 symbol
收集的 effect
就可以
这里会发生影响遍历对象长度时,会引ITERATE_KEY
相关的副作用函数执行
副作用函数执行后,类比我们执行了渲染函数。 然后回到我们的新增属性,
因为新增属性,会对for.. in ..
循环产生影响,所以我们需要把与 ITERATE_KEY
相关的副作用函数拿出来重新执行,看看源码中这块的处理
首先这里是 setter
的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function createSetter(shallow = false ) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { let oldValue = (target as any)[key] ... // 这里是表明是否有 key 也就是判断是否是新增元素 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 逻辑 trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } } |
然后是 具体的 trigger
,拿到对应的标识去更新effect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { const depsMap = targetMap.get(target) if (!depsMap) { // never been tracked return } ... // also run for iteration key on ADD | DELETE | Map.SET switch (type) { // 这种情况就是我们刚刚确定的 trigger 的执行 case TriggerOpTypes.ADD: if (!isArray(target)) { // 拿到收集的 ITERATE_KEY 的依赖 deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes deps.push(depsMap.get( 'length' )) } break ... } for (const dep of deps) { if (dep) { effects.push(...dep) } } ... triggerEffects(createDep(effects)) ... } } function triggerEffect( effect: ReactiveEffect, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } if (effect.scheduler) { effect.scheduler() } else { // 执行所有的 effect 函数 effect.run() } } } |
总结:
在遍历对象或数组时,用一个唯一标识symbol
收集依赖
- 其实如果在模板中直接使用 obj 会伴随着一个 JSON.stringify 的过程,也会伴随着收集依赖。
- 我们在 js 代码里如果没有用到遍历对象,单独对一个对象新增是不会触发更新的,因为没有收集的过程。
在设置新值时,获取收集的symbol
对应的副作用函数更新
遍历数组方法的处理
在使用数组时,会伴随着 this
的问题导致代理对象拿不到属性的问题,比如
之所以会出现这样的问题,是因为 includes
内部的this
指向的是代理对象 arr, 并且在因为比较去获取元素时拿到的也是代理对象,所以拿原始对象去找肯定找不到。所以,我们需要去修改inlcudes的行为,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | new Proxy(obj, { get() { if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } } }) const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations() // 处理集中数组遍历方法中的问题。 function createArrayInstrumentations() { const instrumentations: Record<string, Function> = {} // instrument identity-sensitive Array methods to account for possible reactive // values ;([ 'includes' , 'indexOf' , 'lastIndexOf' ] as const).forEach(key => { instrumentations[key] = function ( this : unknown[], ...args: unknown[]) { const arr = toRaw( this ) as any for (let i = 0, l = this .length; i < l; i++) { track(arr, TrackOpTypes.GET, i + '' ) } // we run the method using the original args first (which may be reactive) // 先使用原始参数,可能是原始对象,也可能是代理对象。 const res = arr[key](...args) if (res === -1 || res === false ) { // if that didn't work, run it again using raw values. // 如果没有找到,就拿到原始参数去比较,脱完响应式的数据。 return arr[key](...args.map(toRaw)) } else { return res } } }) |
数组的变更方法
对于可能会更改原数组长度的数组方法,push
, pop
, shift
, unshift
, splice
也需要进行处理,否则,就会陷入无限递归当中,考虑下面的场景
这两个的执行过程如下:
- 首先,第一个副作用函数执行,然后数组中添加1, 并且这个过程会给影响数组的
length
, 所以会与length
会被track
, 建立响应式联系。 - 然后第二个副作用函数执行,执行
push
这时因为影响了length
,先track
建立响应式联系,然后会试图拿出length的副作用函数也就是第一个副作用函数执行,然而这时第二个副作用函数还未执行完成,就又开始执行第一个副作用函数了, - 第一个副作用函数再次执行,同样会读取
length
并且设置length
,重复上面收集和更新的过程,又要把第二个副作用中收集的 length 执行 - 如此循环往复。最终会栈溢出。
所以问题的关键就在于 length
的不断读取和设置。所以我们需要在读取到 length,避免它与副作用函数之间建立联系
1 2 3 4 5 6 7 8 9 10 | ;([ 'push' , 'pop' , 'shift' , 'unshift' , 'splice' ] as const).forEach(key => { instrumentations[key] = function ( this : unknown[], ...args: unknown[]) { // 禁止追踪变化, pauseTracking() const res = (toRaw( this ) as any)[key].apply( this , args) // 等函数执行完毕时再回复追踪。 resetTracking() return res } }) |
总结
vue2
和 vue3
对于新增的属性都是需要先获取之前收集到的依赖,才能够派发更新。vue2
中使用 Vue.$set
借助的是新增属性的对象已有的依赖派发更新。vue3
是由于 ownKeys()
拦截之前收集到的 symbol
依赖,在添加属性时触发这个symbol
收集到的依赖更新。
对于数组,vue2
是拦截修改数组的方法,然后把当前数组所收集的依赖做出更新执行ob.dep.notify()
,vue3
因为本身就有一定的能力实现数组元素的更新,只是由于是因为length
导致的栈溢出,所以采用禁止跟踪解决,同时,访问方法也需要更新是因为代理对象和原始对象不一致问题,在查找时让对比两个解决。
以上就是详解Vue3中响应式的特殊处理的详细内容,更多关于Vue3响应式的资料请关注脚本之家其它相关文章!
微信公众号搜索 “ 脚本之家 ” ,选择关注
程序猿的那些事、送书等活动等着你
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 reterry123@163.com 进行投诉反馈,一经查实,立即处理!
相关文章
Vue中使用crypto-js AES对称加密算法实现加密解密
在数字加密算法中,通过可划分为对称加密和非对称加密,本文主要介绍了Vue中使用crypto-js AES对称加密算法实现加密解密,文中根据实例编码详细介绍的十分详尽,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2022-03-03Vue利用localStorage本地缓存使页面刷新验证码不清零功能的实现
这篇文章主要介绍了Vue利用localStorage本地缓存使页面刷新验证码不清零功能的实现,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2020-09-09
最新评论