Vue响应式原理

首先,Vue3 与 Vue2 采用了不同的实现方式,option api 将所有的属性与方法全部挂在在 this 中,造成了类型推断困难,而 composition api 重新用 TypeScript 以及 ES6 中的 Proxy 和 Reflect 编写改进,

Effect

响应性的本质实际上是发布订阅设计模式,我们需要在变量发生改变时执行一次 effect,也就是变量发生改变时所造成的影响。

let a = 0
let b = 1
​
let sum
​
function effect() {
  sum = a + b
}
effect()
console.log(sum)    // 1
a = 3
effect()
console.log(sum)    // 4
复制代码

此时我们手动执行了 effect 函数,但是我们希望 effect 函数是自动执行的,每当我们更改了 effect 中的某个变量 effect 就重新执行,为了实现这个目标,我们引入了 Proxy。

Proxy

我们新建一个 reactive 函数,这个函数接受一个 target 变量,返回一个被 Proxy 封装后的对象,每次更改对象中的值时,都会调用一次 effect 函数重新计算结果

function reactive(target) {
  let handler = {
    set(target, key, value) {
      target[key] = value
      effect()
    },
  }
  return new Proxy(target, handler)
}
​
function effect() {
  sum = obj.a + obj.b
}
​
let obj = reactive({
  a: 0,
  b: 1,
})
let sum
​
effect()
console.log(sum)  // 1
obj.a = 3
console.log(sum)  // 4
复制代码

到此为止本质上来说我们已经实现了最基本的响应式,但是我们当前的响应式只能执行同一个函数,当我们需要使用别的函数时只能修改 reactive 内部 set 函数执行的内容。为了使我们的响应式可以自定义需要执行的函数,且可以针对不同的变量有不同的执行函数,我们开始维护一些全局变量。

deps和depsMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dpJSYb6f-1667280911647)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c959bc6ac6c14d33954f3e1ce83a9d15~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]

我们会在 reactive handler 的 get 中保存 effect,在 set 中执行所有的 effect。

const depsMap = new Map()
let activeEffect = null
​
function reactive(target) {
  let handler = {
    get(target, key, receiver) {
      if (activeEffect) {
        let dep = depsMap.get(key)
        if (!dep) {
          depsMap.set(key, (dep = new Set()))
        }
        dep.add(activeEffect)
      }
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      let result = Reflect.set(target, key, value, receiver)
      let dep = depsMap.get(key)
      dep.forEach((effect) => effect())
      return result
    },
  }
  return new Proxy(target, handler)
}
​
function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
​
let obj = reactive({
  a: 5,
  b: 1,
})
let sum, product
​
effect(() => {
  sum = obj.a + obj.b
})
effect(() => {
  product = obj.a * 3
})
​
console.log('sum', sum)   // sum 6
console.log('product', product)   // product 15
​
obj.a = 3
console.log('sum', sum)   // sum 4
console.log('product', product)   // product 9
复制代码

在上面的代码中,我们做了如下修改

  1. 新增了depsMap集合,depsMap中保存了所有属性的对应的要执行的effect Set集合;
  2. 新增了一个activeEffect全局变量,这个变量中保存着现在运行的effect;
  3. 修改effect函数,下面我们给出effect函数的执行过程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fQ7I1nl9-1667280911648)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d3e935935f64457856834f03fa89286~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]

当我们修改reactive包裹的对象的值时,会调用Proxy.handler.set方法,我们先将target[key]修改,接着运行所有的effect(这里有优化空间,我们将在下面讲述)

我们在这里使用了ES6中的Reflect.get()Reflect.set()方法,目的是在存在继承时可以有正确的this指向,详情参考 在es6 Proxy中,推荐使用Reflect.get而不是target[key]的原因

到此为止,我们已经完成了大部分的响应式的内容,但是我们可以注意到我们现在仅仅有一个reactive对象,当我们拥有多个reactive对象,且这些reactive对象中拥有相同的属性时,会出现一些错误,为了解决这个问题,我们在这里加入targetMap(weakMap),用来保存不同对象的depsMap

targetMap,track和trigger

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fn491Whr-1667280911648)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/17ba8608c37147109eade2e2b51413f2~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image)]

我们首先封装tracktrigger函数

function track(target, key) {
  if (activeEffect) {
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
  }
}
​
function trigger(target, key) {
  let dep = depsMap.get(key)
  dep.forEach((effect) => effect())
}
复制代码

接下来我们将depsMap移入targetMap

const targetMap = new WeakMap()
let activeEffect = null
​
function reactive(target) {
  let handler = {
    get(target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      let result = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return result
    },
  }
  return new Proxy(target, handler)
}
​
function track(target, key) {
  if (activeEffect) {
    // 将depsMap移入targetMap中,我们不在将depsMap作为一个全局变量
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
​
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
  }
}
​
function trigger(target, key) {
  let depsMap = targetMap.get(target)
  if (depsMap) {
    let dep = depsMap.get(key)
    if (dep) dep.forEach((effect) => effect())
  }
}
​
function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
复制代码

到这里基本就要大功告成啦!我们将depsMap移入了targetMap中,如果不存在就新建并且将其放入targetMap。

最后我们再进行一点点优化

const targetMap = new WeakMap()
let activeEffect = null
​
function reactive(target) {
  let handler = {
    get(target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      // 保存oldValue
      let oldValue = target[value]
      let result = Reflect.set(target, key, value, receiver)
      // 当oldValue和新的值不同时,执行所有的effect
      if (oldValue !== value) trigger(target, key)
      return result
    },
  }
  return new Proxy(target, handler)
}
​
function track(target, key) {
  if (activeEffect) {
    // 将depsMap移入targetMap中,我们不在将depsMap作为一个全局变量
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
​
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
  }
}
​
function trigger(target, key) {
  let depsMap = targetMap.get(target)
  if (depsMap) {
    let dep = depsMap.get(key)
    if (dep) dep.forEach((effect) => effect())
  }
}
​
function effect(eff) {
  activeEffect = eff
  activeEffect()
  activeEffect = null
}
复制代码

在Proxy.handler.set中,我们加入了对过去值和当前值的判断,当且仅当过去的值和当前修改的值不同时才执行dep中的effect函数

最后的最后,我们再来实现一个ref

ref

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newValue) {
      let oldValue = this.value
      raw = newValue
      if (oldValue !== newValue) trigger(r, 'value')
    },
  }
  return r
}
复制代码

有了前面的铺垫,ref的实现就变得简单了起来,由于在Vue中ref包裹的变量都需要通过.value访问,我们采用了对象的属性访问器gettersetter来实现对于value的访问。属性访问器的功能类似于前面reactive中使用的Proxy,每当获取值的时候执行track保存当前的activeEffect,设置值的时候判断是否和之前的值相等,如果不相等的话执行trigger

好了,以上所有就是Vue响应式的基本原理了,本文章主要来源于学习Vue Master的一些笔记和总结,希望对大家有所帮助。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值