详解Vue3中响应式的特殊处理

 更新时间:2023年04月11日 10:41:21   作者:Gill  
这篇文章主要为大家详细介绍了Vue3中响应式的一些特殊处理,文中的示例代码讲解详细,对我们深入了解Vue3有一定的帮助,需要的可以参考一下

脚本之家 / 编程助手:解决程序员“几乎”所有问题!
脚本之家官方知识库 → 点击立即使用

vue2 vs vue3

两个响应式更新的核心区别在于Object.definePropertyProxy 两个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 就可以

1
trigger(target, isArray(target) ? 'length' : ITERATE_KEY) // 数组的情况追踪 length

这里会发生影响遍历对象长度时,会引ITERATE_KEY 相关的副作用函数执行

1
2
3
4
5
effect(() => {
    for(let i in obj) {
        console.log(i)
    }
})

副作用函数执行后,类比我们执行了渲染函数。 然后回到我们的新增属性,

因为新增属性,会对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 的问题导致代理对象拿不到属性的问题,比如

1
2
3
const obj = {}
const arr = reactive([obj])
console.log(arr.includes(obj) // false

之所以会出现这样的问题,是因为 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
2
3
4
5
6
7
cosnt arr = reactive([])
effect(() => {
    arr.push(1)
})
effect(() => {
    arr.push(1)
})

这两个的执行过程如下:

  • 首先,第一个副作用函数执行,然后数组中添加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
    }
  })

总结

vue2vue3 对于新增的属性都是需要先获取之前收集到的依赖,才能够派发更新。vue2 中使用 Vue.$set借助的是新增属性的对象已有的依赖派发更新。vue3 是由于 ownKeys()拦截之前收集到的 symbol 依赖,在添加属性时触发这个symbol 收集到的依赖更新。

对于数组,vue2 是拦截修改数组的方法,然后把当前数组所收集的依赖做出更新执行ob.dep.notify()vue3因为本身就有一定的能力实现数组元素的更新,只是由于是因为length 导致的栈溢出,所以采用禁止跟踪解决,同时,访问方法也需要更新是因为代理对象和原始对象不一致问题,在查找时让对比两个解决。

以上就是详解Vue3中响应式的特殊处理的详细内容,更多关于Vue3响应式的资料请关注脚本之家其它相关文章!

蓄力AI

微信公众号搜索 “ 脚本之家 ” ,选择关注

程序猿的那些事、送书等活动等着你

原文链接:https://juejin.cn/post/7220344172393496631

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权/违法违规/事实不符,请将相关资料发送至 reterry123@163.com 进行投诉反馈,一经查实,立即处理!

相关文章

  • Vue中使用Datav如何完成大屏基本布局

    Vue中使用Datav如何完成大屏基本布局

    这篇文章主要介绍了Vue中使用Datav如何完成大屏基本布局问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-09-09
  • vue不通过路由直接获取url中参数的方法示例

    vue不通过路由直接获取url中参数的方法示例

    通过url传递参数是我们在开发中经常用到的一种传参方法,但通过url传递后改如果获取呢?下面这篇文章主要给大家介绍了关于vue如何不通过路由直接获取url中参数的相关资料,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-08-08
  • 详解Electron中如何使用SQLite存储笔记

    详解Electron中如何使用SQLite存储笔记

    这篇文章主要为大家介绍了Electron中如何使用SQLite存储笔记示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • Vue中使用crypto-js AES对称加密算法实现加密解密

    Vue中使用crypto-js AES对称加密算法实现加密解密

     在数字加密算法中,通过可划分为对称加密和非对称加密,本文主要介绍了Vue中使用crypto-js AES对称加密算法实现加密解密,文中根据实例编码详细介绍的十分详尽,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • vue如何点击多个tab标签打开关闭多个页面

    vue如何点击多个tab标签打开关闭多个页面

    这篇文章主要介绍了vue如何点击多个tab标签打开关闭多个页面,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • vue实现全局组件自动注册,无需再单独引用

    vue实现全局组件自动注册,无需再单独引用

    这篇文章主要介绍了vue实现全局组件自动注册,无需再单独引用方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • Vue.extend 编程式插入组件的实现

    Vue.extend 编程式插入组件的实现

    这篇文章主要介绍了Vue.extend 编程式插入组件的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • Vue利用localStorage本地缓存使页面刷新验证码不清零功能的实现

    Vue利用localStorage本地缓存使页面刷新验证码不清零功能的实现

    这篇文章主要介绍了Vue利用localStorage本地缓存使页面刷新验证码不清零功能的实现,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-09-09
  • vue3 与 vue2 优点对比汇总

    vue3 与 vue2 优点对比汇总

    随着用vue3 的开发者越来越多,其必定是又她一定的有带你,接下来这篇文章小编就为大家介绍vue3 对比 vue2 有什么优点?感兴趣的小伙伴请跟小编一起阅读下文吧
    2021-09-09
  • vue如何解决循环引用组件报错的问题

    vue如何解决循环引用组件报错的问题

    这篇文章主要介绍了vue如何解决循环引用组件报错的问题,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-09-09

最新评论