这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

前言

技术栈是 Vue 的同学,在面试中难免会被问到 Vue2 和 Vue3 的相关知识点的实现原理和比较,面试官是步步紧逼,一环扣一环。

Vue2 的响应式原理是怎么样的?

Vue3 的响应式原理又是怎么样的?

Vue2 中是怎么监测数组的变化的?

Vue3 中又是怎么监测数组的变化的?

在问完你 Vue2 的数组的响应式原理之后,接着可能会补上一句,为什么要通过重写数组原型的 7 个方法来对数组进行监测?是因为 defineProperty 真的不能监测数组变化吗?Vue3 真的只使用 Proxy 就可以实现对数组的代理了吗?还需要进行什么设置呢?

Vue2 和 Vue3 的响应式实现原理具体是非常复杂和细节非常繁琐的,但我们需要在面试中去说清楚其中的原理,这就需要我们进行宏观和高度的概括总结。本文主要从面试的角度去讲解相关的实现原理,相关代码只是一个辅助理解。

问题1:Vue2 的响应式原理是怎么样的?

所谓响应式就是首先建立响应式数据和依赖之间的关系,当这些响应式数据发生变化的时候,可以通知那些绑定这些数据的依赖进行相关操作,可以是 DOM 更新,也可以是执行一个回调函数。

我们知道 Vue2 的对象数据是通过 Object.defineProperty 对每个属性进行监听,当对属性进行读取的时候,就会触发 getter,对属性进行设置的时候,就会触发 setter。

  1. /**
  2. * 这里的函数 defineReactive 用来对 Object.defineProperty 进行封装。
  3. **/
  4. function defineReactive(data, key, val) {
  5. // 依赖存储的地方
  6. const dep = new Dep()
  7. Object.defineProperty(data, key, {
  8. enumerable: true,
  9. configurable: true,
  10. get: function () {
  11. // 在 getter 中收集依赖
  12. dep.depend()
  13. return val
  14. },
  15. set: function(newVal) {
  16. val = newVal
  17. // 在 setter 中触发依赖
  18. dep.notify()
  19. }
  20. })
  21. }

那么是什么地方进行属性读取呢?就是在 Watcher 里面,Watcher 也就是所谓的依赖。在 Watcher 里面读取数据的时候,会把自己设置到一个全局的变量中。

  1. /**
  2. * 我们所讲的依赖其实就是 Watcher,我们要通知用到数据的地方,而使用这个数据的地方有很多,类型也不一样,有* 可能是组件的,有可能是用户写的 watch,我们就需要抽象出一个能集中处理这些情况的类。
  3. **/
  4. class Watcher {
  5. constructor(vm, exp, cb) {
  6. this.vm = vm
  7. this.getter = exp
  8. this.cb = cb
  9. this.value = this.get()
  10. }
  11.  
  12. get() {
  13. Dep.target = this
  14. let value = this.getter.call(this.vm, this.vm)
  15. Dep.target = undefined
  16. return value
  17. }
  18.  
  19. update() {
  20. const oldValue = this.value
  21. this.value = this.get()
  22. this.cb.call(this.vm, this.value, oldValue)
  23. }
  24. }
在 Watcher 读取数据的时候也就触发了这个属性的监听 getter,在 getter 里面就需要进行依赖收集,这些依赖存储的地方就叫 Dep,在 Dep 里面就可以把全局变量中的依赖进行收集,收集完毕就会把全局依赖变量设置为空。将来数据发生变化的时候,就去 Dep 中把相关的 Watcher 拿出来执行一遍。
  1. /**
  2. * 我们把依赖收集的代码封装成一个 Dep 类,它专门帮助我们管理依赖。
  3. * 使用这个类,我们可以收集依赖、删除依赖或者向依赖发送通知等。
  4. **/
  5. class Dep {
  6. constructor() {
  7. this.subs = []
  8. }
  9.  
  10. addSub(sub) {
  11. this.subs.push(sub)
  12. }
  13.  
  14. removeSub(sub) {
  15. remove(this.subs, sub)
  16. }
  17.  
  18. depend() {
  19. if(Dep.target){
  20. this.addSub(Dep.target)
  21. }
  22. }
  23.  
  24. notify() {
  25. const subs = this.subs.slice()
  26. for(let i = 0, l = subs.length; i < l; i++) {
  27. subs[i].update()
  28. }
  29. }
  30. }
  31.  
  32. // 删除依赖
  33. function remove(arr, item) {
  34. if(arr.length) {
  35. const index = arr.indexOf(item)
  36. if(index > -1){
  37. return arr.splice(index, 1)
  38. }
  39. }
  40. }

总的来说就是通过 Object.defineProperty 监听对象的每一个属性,当读取数据时会触发 getter,修改数据时会触发 setter。

然后我们在 getter 中进行依赖收集,当 setter 被触发的时候,就去把在 getter 中收集到的依赖拿出来进行相关操作,通常是执行一个回调函数。

我们收集依赖需要进行存储,对此 Vue2 中设置了一个 Dep 类,相当于一个管家,负责添加或删除相关的依赖和通知相关的依赖进行相关操作。

在 Vue2 中所谓的依赖就是 Watcher。值得注意的是,只有 Watcher 触发的 getter 才会进行依赖收集,哪个 Watcher 触发了 getter,就把哪个 Watcher 收集到 Dep 中。当响应式数据发生改变的时候,就会把收集到的 Watcher 都进行通知。

由于 Object.defineProperty 无法监听对象的变化,所以 Vue2 中设置了一个 Observer 类来管理对象的响应式依赖,同时也会递归侦测对象中子数据的变化。

问题2:为什么 Vue2 新增响应式属性要通过额外的 API?

这是因为 Object.defineProperty 只会对属性进行监测,而不会对对象进行监测,为了可以监测对象 Vue2 创建了一个 Observer 类。Observer 类的作用就是把一个对象全部转换成响应式对象,包括子属性数据,当对象新增或删除属性的时候负债通知对应的 Watcher 进行更新操作。

  1. // 定义一个属性
  2. function def(obj, key, val, enumerable) {
  3. Object.defineProperty(obj, key, {
  4. value: val,
  5. enumerable: !!enumerable,
  6. writable: true,
  7. configurable: true
  8. })
  9. }
  10.  
  11. class Observer {
  12. constructor(value) {
  13. this.value = value
  14. // 添加一个对象依赖收集的选项
  15. this.dep = new Dep()
  16. // 给响应式对象添加 __ob__ 属性,表明这是一个响应式对象
  17. def(value, '__ob__', this)
  18. if(Array.isArray(value)) {
  19.  
  20. } else {
  21. this.walk(value)
  22. }
  23. }
  24.  
  25. walk(obj) {
  26. const keys = Object.keys(obj)
  27. // 遍历对象的属性进行响应式设置
  28. for(let i = 0; i < keys.length; i ++) {
  29. defineReactive(obj, keys[i], obj[keys[i]])
  30. }
  31. }
  32. }

vm.$set 的实现原理

  1. function set(target, key, val) {
  2. const ob = target.__ob__
  3. defineReactive(ob.value, key, val)
  4. ob.dep.notify()
  5. return val
  6. }

当向一个响应式对象新增属性的时候,需要对这个属性重新进行响应式的设置,即使用 defineReactive 将新增的属性转换成 getter/setter。

我们在前面讲过每一个对象是会通过 Observer 类型进行包装的,并在 Observer 类里面创建一个属于这个对象的依赖收集存储对象 dep, 最后在新增属性的时候就通过这个依赖对象进行通知相关 Watcher 进行变化更新。

vm.$delete 的实现原理

  1. function del(target, key) {
  2. const ob = target.__ob__
  3. delete target[key]
  4. ob.dep.notify()
  5. }

我们可以看到 vm.$delete 的实现原理和 vm.$set 的实现原理是非常相似的。

通过 vm.$deletevm.$set 的实现原理,我们可以更加清晰地理解到 Observer 类的作用,Observer 类就是给一个对象也进行一个监测,因为 Object.defineProperty 是无法实现对对象的监测的,但这个监测是手动,不是自动的。

问题3:Object.defineProperty 真的不能监听数组的变化吗?

面试官一上来可能先问你 Vue2 中数组的响应式原理是怎么样的,这个问题你也许会觉得很容易回答,Vue2 对数组的监测是通过重写数组原型上的 7 个方法来实现,然后你会说具体的实现,接下来面试官可能会问你,为什么要改写数组原型上的 7 个方法,而不使用 Object.defineProperty,是因为 Object.defineProperty 真的不能监听数组的变化吗?

其实 Object.defineProperty 是可以监听数组的变化的。

  1. const arr = [1, 2, 3]
  2. arr.forEach((val, index) => {
  3.   Object.defineProperty(arr, index, {
  4.     get() {
  5. console.log('监听到了')
  6.       return val
  7.     },
  8.     set(newVal) {
  9.       console.log('变化了:', val, newVal)
  10.       val = newVal
  11.     }
  12.   })
  13. })

其实数组就是一个特殊的对象,它的下标就可以看作是它的 key。

所以 Object.defineProperty 也能监听数组变化,那么为什么 Vue2 弃用了这个方案呢?

首先这种直接通过下标获取数组元素的场景就比较少,其次即便通过了 Object.defineProperty 对数组进行监听,但也监听不了 push、pop、shift 等对数组进行操作的方法,所以还是需要通过对数组原型上的那 7 个方法进行重写监听。所以为了性能考虑 Vue2 直接弃用了使用 Object.defineProperty 对数组进行监听的方案。

问题4:Vue2 中是怎么监测数组的变化的?

通过上文我们知道如果使用 Object.defineProperty 对数组进行监听,当通过 Array 原型上的方法改变数组内容的时候是无发触发 getter/setter 的, Vue2 中是放弃了使用 Object.defineProperty 对数组进行监听的方案,而是通过对数组原型上的 7 个方法进行重写进行监听的。

原理就是使用拦截器覆盖 Array.prototype,之后再去使用 Array 原型上的方法的时候,其实使用的是拦截器提供的方法,在拦截器里面才真正使用原生 Array 原型上的方法去操作数组。

拦截器

  1. // 拦截器其实就是一个和 Array.prototype 一样的对象。
  2. const arrayProto = Array.prototype
  3. const arrayMethods = Object.create(arrayProto)
  4. ;[
  5. 'push',
  6. 'pop',
  7. 'shift',
  8. 'unshift',
  9. 'splice',
  10. 'sort',
  11. 'reverse'
  12. ].forEach(function (method) {
  13. // 缓存原始方法
  14. const original = arrayProto[method]
  15. Object.defineProperty(arrayMethods, method, {
  16. value: function mutator(...args) {
  17. // 最终还是使用原生的 Array 原型方法去操作数组
  18. return original.apply(this, args)
  19. },
  20. eumerable: false,
  21. writable: false,
  22. configurable: true
  23. })
  24. })

所以通过拦截器之后,我们就可以追踪到数组的变化了,然后就可以在拦截器里面进行依赖收集和触发依赖了。

接下来我们就使用拦截器覆盖那些进行了响应式处理的 Array 原型,数组也是一个对象,通过上文我们可以知道 Vue2 是在 Observer 类里面对对象的进行响应式处理,并且给对象也进行一个依赖收集。所以对数组的依赖处理也是在 Observer 类里面。

  1. class Observer {
  2. constructor(value) {
  3. this.value = value
  4. // 添加一个对象依赖收集的选项
  5. this.dep = new Dep()
  6. // 给响应式对象添加 __ob__ 属性,表明这是一个响应式对象
  7. def(value, '__ob__', this)
  8. // 如果是数组则通过覆盖数组的原型方法进来拦截操作
  9. if(Array.isArray(value)) {
  10. value.__proto__ = arrayMethods
  11. } else {
  12. this.walk(value)
  13. }
  14. }
  15. // ...
  16. }

在这个地方 Vue2 会进行一些兼容性的处理,如果能使用 __proto__ 就覆盖原型,如果不能使用,则直接把那 7 个操作数组的方法直接挂载到需要被进行响应式处理的数组上,因为当访问一个对象的方法时,只有这个对象自身不存在这个方法,才会去它的原型上查找这个方法。

数组如何收集依赖呢?

我们知道在数组进行响应式初始化的时候会在 Observer 类里面给这个数组对象的添加一个 __ob__ 的属性,这个属性的值就是 Observer 这个类的实例对象,而这个 Observer 类里面有存在一个收集依赖的属性 dep,所以在对数组里的内容通过那 7 个方法进行操作的时候,会触发数组的拦截器,那么在拦截器里面就可以访问到这个数组的 Observer 类的实例对象,从而可以向这些数组的依赖发送变更通知。

  1. // 拦截器其实就是一个和 Array.prototype 一样的对象。
  2. const arrayProto = Array.prototype
  3. const arrayMethods = Object.create(arrayProto)
  4. ;[
  5. 'push',
  6. 'pop',
  7. 'shift',
  8. 'unshift',
  9. 'splice',
  10. 'sort',
  11. 'reverse'
  12. ].forEach(function (method) {
  13. // 缓存原始方法
  14. const original = arrayProto[method]
  15. Object.defineProperty(arrayMethods, method, {
  16. value: function mutator(...args) {
  17. // 最终还是使用原生的 Array 原型方法去操作数组
  18. const result = original.apply(this, args)
  19. // 获取 Observer 对象实例
  20. const ob = this.__ob__
  21. // 通过 Observer 对象实例上 Dep 实例对象去通知依赖进行更新
  22. ob.dep.notify()
  23. },
  24. eumerable: false,
  25. writable: false,
  26. configurable: true
  27. })
  28. })

因为 Vue2 的实现方法决定了在 Vue2 中对数组的一些操作无法实现响应式操作,例如:

this.list[0] = xxx

由于 Vue2 放弃了 Object.defineProperty 对数组进行监听的方案,所以通过下标操作数组是无法实现响应式操作的。

又例如:

this.list.length = 0

这个动作在 Vue2 中也是无法实现响应式操作的。

问题5:Vue3 的响应式原理是怎么样的?

Vue3 是通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据,然后在副作用函数中读取响应式数据的时候,就会触发 Proxy 的 getter,在 getter 里面把对当前的副作用函数保存起来,将来对应响应式数据发生更改的话,则把之前保存起来的副作用函数取出来执行。

具体是副作用函数里面读取响应式对象的属性值时,会触发代理对象的 getter,然后在 getter 里面进行一定规则的依赖收集保存操作。

简单代码实现:

  1. // 使用一个全局变量存储被注册的副作用函数
  2. let activeEffect
  3. // 注册副作用函数
  4. function effect(fn) {
  5. activeEffect = fn
  6. fn()
  7. }
  8. const obj = new Proxy(data, {
  9. // getter 拦截读取操作
  10. get(target, key) {
  11. // 将副作用函数 activeEffect 添加到存储副作用函数的全局变量 targetMap 中
  12. track(target, key)
  13. // 返回读取的属性值
  14. return Reflect.get(target, key)
  15. },
  16. // setter 拦截设置操作
  17. set(target, key, val) {
  18. // 设置属性值
  19. const result = Reflect.set(target, key, val)
  20. // 把之前存储的副作用函数取出来并执行
  21. trigger(target, key)
  22. return result
  23. }
  24. })
  25. // 存储副作用函数的全局变量
  26. const targetMap = new WeakMap()
  27. // 在 getter 拦截器内追踪依赖的变化
  28. function track(target, key) {
  29. // 没有 activeEffect,直接返回
  30. if(!activeEffect) return
  31. // 根据 target 从全局变量 targetMap 中获取 depsMap
  32. let depsMap = targetMap.get(target)
  33. if(!depsMap) {
  34. // 如果 depsMap 不存,那么需要新建一个 Map 并且与 target 关联
  35. depsMap = new Map()
  36. targetMap.set(target, depsMap)
  37. }
  38. // 再根据 key 从 depsMap 中取得 deps, deps 里面存储的是所有与当前 key 相关联的副作用函数
  39. let deps = depsMap.get(key)
  40. if(!deps) {
  41. // 如果 deps 不存在,那么需要新建一个 Set 并且与 key 关联
  42. deps = new Set()
  43. depsMap.set(key, deps)
  44. }
  45. // 将当前的活动的副作用函数保存起来
  46. deps.add(activeEffect)
  47. }
  48. // 在 setter 拦截器中触发相关依赖
  49. function trgger(target, key) {
  50. // 根据 target 从全局变量 targetMap 中取出 depsMap
  51. const depsMap = targetMap.get(target)
  52. if(!depsMap) return
  53. // 根据 key 取出相关联的所有副作用函数
  54. const effects = depsMap.get(key)
  55. // 执行所有的副作用函数
  56. effects && effects.forEach(fn => fn())
  57. }

通过上面的代码我们可以知道 Vue3 中依赖收集的规则,首先把响应式对象作为 key,一个 Map 的实例做为值方式存储在一个 WeakMap 的实例中,其中这个 Map 的实例又是以响应式对象的 key 作为 key, 值为一个 Set 的实例为值。而且这个 Set 的实例中存储的则是跟那个响应式对象 key 相关的副作用函数。

来看看图表表示的结构:

那么为什么 Vue3 的依赖收集的数据结构这里采用 WeakMap 呢?

所以我们需要解析一下 WeakMap 和 Map 的区别,首先 WeakMap 是可以接受一个对象作为 key 的,而 WeakMap 对 key 是弱引用的。所以当 WeakMap 的 key 是一个对象时,一旦上下文执行完毕,WeakMap 中 key 对象没有被其他代码引用的时候,垃圾回收器 就会把该对象从内存移除,我们就无法该对象从 WeakMap 中获取内容了。

另外副作用函数使用 Set 类型,是因为 Set 类型能自动去除重复内容。

上述方法只实现了对引用类型的响应式处理,因为 Proxy 的代理目标必须是非原始值。原始值指的是 Boolean、Number、BigInt、String、Symbol、undefined 和 null 等类型的值。在 JavaScript 中,原始值是按值传递的,而非按引用传递。这意味着,如果一个函数接收原始值作为参数,那么形参与实参之间没有引用关系,它们是两个完全独立的值,对形参的修改不会影响实参。

Vue3 中是通过对原始值做了一层包裹的方式来实现对原始值变成响应式数据的。最新的 Vue3 实现方式是通过属性访问器 getter/setter 来实现的。

  1. class RefImpl{
  2. private _value
  3. public dep
  4. // 表示这是一个 Ref 类型的响应式数据
  5. private _v_isRef = true
  6. constructor(value) {
  7. this._value = value
  8. // 依赖存储
  9. this.dep = new Set()
  10. }
  11. // getter 访问拦截
  12. get value() {
  13. // 依赖收集
  14. trackRefValue(this)
  15. return this._value
  16. }
  17. // setter 设置拦截
  18. set value(newVal) {
  19. this._value = newVal
  20. // 触发依赖
  21. triggerEffect(this.dep)
  22. }
  23. }

ref 本质上是一个实例化之后的 “包裹对象”,因为 Proxy 无法提供对原始值的代理,所以我们需要使用一层对象作为包裹,间接实现原始值的响应式方案。 由于实例化之后的 “包裹对象” 本质与普通对象没有任何区别,所以为了区分 ref 与 Proxy 响应式对象,我们需要给 ref 的实例对象定义一个 _v_isRef 的标识,表明这是一个 ref 的响应式对象。

最后我们和 Vue2 进行一下对比,我们知道 Vue2 的响应式存在很多的问题,例如:

  • 初始化时需要遍历对象所有 key,如果对象层次较深,性能不好
  • 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
  • 无法监听到数组元素的变化,只能通过劫持重写了几个数组方法
  • 动态新增,删除对象属性无法拦截,只能用特定 set/delete API 代替
  • 不支持 Map、Set 等数据结构

而 Vue3 使用 Proxy 实现之后,则以上的问题都不存在了。

问题6:Vue3 中是怎么监测数组的变化?

我们知道在 Vue2 中是需要对数组的监听进行特殊的处理的,其中在 Vue3 中也需要对数组进行特殊的处理。在 Vue2 是不可以通过数组下标对响应式数组进行设置和读取的,而 Vue3 中是可以的,数组中仍然有很多其他特别的读取和设置的方法,这些方法没经过特殊处理,是无法通过普通的 Proxy 中的 getter/setter 进行响应式处理的。

数组中对属性或元素进行读取的操作方法。

  • 通过索引访问数组的元素值
  • 访问数组的长度
  • 把数组作为对象,使用 for ... in 循环遍历
  • 使用 for ... of 迭代遍历数组
  • 数组的原型方法,如 concat、join、every、some、find、findIndex、includes 等

数组中对属性或元素进行设置的操作方法。

  • 通过索引修改数组的元素值
  • 修改数组的长度
  • 数组的栈方法
  • 修改原数组的原型方法:splice、fill、sort 等

当上述的数组的读取或设置的操作发生时,也应该正确地建立响应式联系或触发响应。

当通过索引设置响应式数组的时候,有可能会隐式修改数组的 length 属性,例如设置的索引值大于数组当前的长度时,那么就要更新数组的 length 属性,因此在触发当前的修改属性的响应之外,也需要触发与 length 属性相关依赖进行重新执行。

遍历数组,使用 for ... in 循环遍历数组与遍历常规对象是一致的,也可以使用 ownKeys 拦截器进行设置。而影响 for ... in 循环对数组的遍历会是添加新元素:arr[0] = 1 或者修改数组长度:arr.length = 0,其实无论是为数组添加新元素,还是直接修改数组的长度,本质上都是因为修改了数组的 length 属性。所以在 ownKeys 拦截器内进行判断,如果是数组的话,就使用 length 属性作为 key 去建立响应联系。

在 Vue3 中也需要像 Vue2 那样对一些数组原型上方法进行重写。

当数组响应式对象使用 includes、indexOf、lastIndexOf 这方法的时候,它们内部的 this 指向的是代理对象,并且在获取数组元素时得到的值要也是代理对象,所以当使用原始值去数组响应式对象中查找的时候,如果不进行特别的处理,是查找不到的,所以我们需要对上述的数组方法进行重写才能解决这个问题。

首先 arr.indexOf 可以理解为读取响应式对象 arr 的 indexOf 属性,这就会触发 getter 拦截器,在 getter 拦截器内我们就可以判断 target 是否是数组,如果是数组就看读取的属性是否是我们需要重写的属性,如果是,则使用我们重新之后的方法。

  1. const arrayInstrumentations = {}
  2. ;(['includes', 'indexOf', 'lastIndexOf']).forEach(key => {
  3. const originMethod = Array.prototype[key]
  4. arrayInstrumentations[key] = function(...args) {
  5. // this 是代理对象,先在代理对象中查找
  6. let res = originMethod.apply(this, args)
  7.  
  8. if(res === false) {
  9. // 在代理对象中没找到,则去原始数组中查找
  10. res = originMethod.apply(this.raw, args)
  11. }
  12. // 返回最终的值
  13. return res
  14. }
  15. })

上述重写方法的主要是实现先在代理对象中查找,如果没找到,就去原始数组中查找,结合两次的查找结果才是最终的结果,这样就实现了在代理数组中查找原始值也可以查找到。

在一些数组的方法中除了修改数组的内容之外也会隐式地修改数组的长度。例如下面的例子:

我们可以看到我们只是进行 arr.push 的操作却也触发了 getter 拦截器,并且触发了两次,其中一次就是数组 push 属性的读取,还有一次是什么呢?还有一次就是调用 push 方法会间接读取 length 属性,那么问题来了,进行了 length 属性的读取,也就会建立 length 的响应依赖,可 arr.push 本意只是修改操作,并不需要建立 length 属性的响应依赖。所以我们需要 “屏蔽” 对 length 属性的读取,从而避免在它与副作用函数之间建立响应联系。

相关代码实现如下:

  1. const arrayInstrumentations = {}
  2. // 是否允许追踪依赖变化
  3. let shouldTrack = true
  4. // 重写数组的 push、pop、shift、unshift、splice 方法
  5. ;['push','pop','shift', 'unshift', 'splice'].forEach(method => {
  6. // 取得原始的数组原型上的方法
  7. const originMethod = Array.prototype[method]
  8. // 重写
  9. arrayInstrumentations[method] = function(...args) {
  10. // 在调用原始方法之前,禁止追踪
  11. shouldTrack = false
  12. // 调用数组的默认方法
  13. let res = originMethod.apply(this, args)
  14. // 在调用原始方法之后,恢复允许进行依赖追踪
  15. shouldTrack = true
  16. return res
  17. }
  18. })

在调用数组的默认方法间接读取 length 属性之前,禁止进行依赖跟踪,这样在间接读取 length 属性时,由于是禁止依赖跟踪的状态,所以 length 属性与副作用函数之间不会建立响应联系。

总结

本文通过一个一个问题的方式,分别解答 Vue2 和 Vue3 的响应式实现原理。

Vue2 部分

Vue2 是通过 Object.defineProperty 将对象的属性转换成 getter/setter 的形式来进行监听它们的变化,当读取属性值的时候会触发 getter 进行依赖收集,当设置对象属性值的时候会触发 setter 进行向相关依赖发送通知,从而进行相关操作。

由于 Object.defineProperty 只对属性 key 进行监听,无法对引用对象进行监听,所以在 Vue2 中创建一个了 Observer 类对整个对象的依赖进行管理,当对响应式对象进行新增或者删除则由响应式对象中的 dep 通知相关依赖进行更新操作。

Object.defineProperty 也可以实现对数组的监听的,但因为性能的原因 Vue2 放弃了这种方案,改由重写数组原型对象上的 7 个能操作数组内容的变更的方法,从而实现对数组的响应式监听。

Vue3 部分

Vue3 则是通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据,然后在副作用函数中读取响应式数据的时候,就会触发 Proxy 的 getter,在 getter 里面把对当前的副作用函数保存起来,将来对应响应式数据发生更改的话,则把之前保存起来的副作用函数取出来执行。

Vue3 对数组实现代理时,用于代理普通对象的大部分代码可以继续使用,但由于对数组的操作与对普通对象的操作存在很多的不同,那么也需要对这些不同的操作实现正确的响应式联系或触发响应。这就需要对数组原型上的一些方法进行重写。

比如通过索引为数组设置新的元素,可能会隐式地修改数组的 length 属性的值。同时如果修改数组的 length 属性的值,也可能会间接影响数组中的已有元素。另外用户通过 includes、indexOf 以及 lastIndexOf 等对数组元素进行查找时,可能是使用代理对象进行查找,也有可能使用原始值进行查找,所以我们就需要重写这些数组的查找方法,从而实现用户的需求。原理很简单,当用户使用这些方法查找元素时,先去响应式对象中查找,如果没找到,则再去原始值中查找。

另外如果使用 push、pop、shift、unshift、splice 这些方法操作响应式数组对象时会间接读取和设置数组的 length 属性,所以我们也需要对这些数组的原型方法进行重新,让当使用这些方法间接读取 length 属性时禁止进行依赖追踪,这样就可以断开 length 属性与副作用函数之间的响应式联系了。

最后,其实在 Vue3 部分还有 WeakMap、Map、Set 这部分的响应式原理还没进行讲解,日后有时间再进行补写。

本文转载于:

https://juejin.cn/post/7124351370521477128

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

记录--六道题理解Vue2 和 Vue3 的响应式原理比对的更多相关文章

  1. vue2与vue3实现响应式的原理区别和提升

    区别: vue2.x: 实现原理: 对象类型:Object.defineProperty()对属性的读取,修改进行拦截(数据劫持): 数组类型:通过重写更新数组的一系列方法来进行拦截(对数组的变更方法 ...

  2. vue2.0 之 深入响应式原理

    实例demo<div id="app"> <span>{{a}}</span> <input type="text" ...

  3. Vue3.0响应式原理

    Vue3.0的响应式基于Proxy实现.具体代码如下: 1 let targetMap = new WeakMap() 2 let effectStack = [] //存储副作用 3 4 const ...

  4. Vue2源码解读 - 响应式原理及简单实现

    直接进入主题了,想必大家都知道实现vue响应式核心方法就是 Object.defineProperty,那就从它开始说 Object.defineProperty 缺点: 深度监听,需要递归到底,一次 ...

  5. 简单对比vue2.x与vue3.x响应式及新功能

    简单对比vue2.x与vue3.x响应式 对响应方式来讲:Vue3.x 将使用Proxy ,取代Vue2.x 版本的 Object.defineProperty. 为何要将Object.defineP ...

  6. vue3响应式原理以及ref和reactive区别还有vue2/3生命周期的对比,第二天

    前言: 前天我们学了 ref 和 reactive ,提到了响应式数据和 Proxy ,那我们今天就来了解一下,vue3 的响应式 在了解之前,先复习一下之前 vue2 的响应式原理 vue2 的响应 ...

  7. 由浅入深,带你用JavaScript实现响应式原理(Vue2、Vue3响应式原理)

    由浅入深,带你用JavaScript实现响应式原理 前言 为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数 ...

  8. [切图仔救赎]炒冷饭--在线手撸vue2响应式原理

    --图片来源vue2.6正式版本(代号:超时空要塞)发布时,尤雨溪推送配图. 前言 其实这个冷饭我并不想炒,毕竟vue3马上都要出来.我还在这里炒冷饭,那明显就是搞事情. 起因: 作为切图仔搬砖汪,长 ...

  9. 手摸手带你理解Vue响应式原理

    前言 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图.在面试中是经常考查的知识点,也是面试加分项. 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它 ...

  10. vue2.0与3.0响应式原理机制

    vue2.0响应式原理 - defineProperty 这个原理老生常谈了,就是拦截对象,给对象的属性增加set 和 get方法,因为核心是defineProperty所以还需要对数组的方法进行拦截 ...

随机推荐

  1. 【LGR-148-Div.3】洛谷基础赛 #1 & MGOI Round I

    [LGR-148-Div.3]洛谷基础赛 #1 & MGOI Round I T1 luoguP9502 『MGOI』Simple Round I | A. 魔法数字 \(100pts\) 水 ...

  2. js加css实现div展示更多隐藏内容

    说明 在设计博客首页文章分类等栏目时,有时候列表内容太多往往不是一次性展示出来.此时需要添加更多功能,当点击更多标签时再展示剩余隐藏的项目. 效果 代码 <!DOCTYPE html> & ...

  3. 《深入理解Java虚拟机》(四) 调优工具、指令

    目录 JVM 调优的概念 jps 1.options 功能选项 2.hostid jstat 1.vmid格式 2.interval 和 count 3.option jinfo jmap jhat ...

  4. 使用 CMake 编写 Windows 静态库

    最近有一个多个 .h .cc .cpp 编译成静态库的需求,故记录下过程 静态库不同于动态库,它不需要 main 入口,只要各个源文件与头文件能对应,也就是源文件和头文件引用的头文件能够找到函数的符号 ...

  5. FastGateway 发布v0.0.0.5

    FastGateway 发布v0.0.0.5 修复构建错误 修复docker-compose执行目录 修改请求来源分析数据列表展示 update README.md. 增加默认证书 修复构建脚本目录错 ...

  6. Kotlin 函数 与 lambda 表达式

    一.函数 代码块函数体: fun sum(x: Int, y: Int): Int { return x + y } 表达式函数体: fun sum(x: Int, y: Int) = x + y 使 ...

  7. Elasticsearch系列之-windows安装和基础操作

    ElasticSearch安装 安装JDK环境 因为ElasticSearch是用Java语言编写的,所以必须安装JDK的环境,并且是JDK 1.8以上 官网:https://www.oracle.c ...

  8. 《HelloGitHub》第 95 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...

  9. 如何优化好UITableView,值得思考

    如果你觉得 UITableViewDelegate 和 UITableViewDataSource 这两个协议中有大量方法每次都是复制粘贴,实现起来大同小异:如果你觉得发起网络请求并解析数据需要一大段 ...

  10. 【Azure 事件中心】Spring Cloud Stream Event Hubs Binder 发送Event Hub消息遇见 Spec. Rule 1.3 - onSubscribe, onNext, onError and onComplete signaled to a Subscriber MUST be signaled serially 异常

    问题描述 开发Java Spring Cloud应用,需要发送消息到Azure Event Hub中.使用 Spring Cloud Stream Event Hubs Binder 依赖,应用执行一 ...