# Vue3 源码解析 07--实现自己的 min-vue(reactive)

272 阅读6分钟

前言

经过之前的探索,基本上弄明白了 Vue3 的响应式的大概内容,所以为了巩固一下,来实现一个简易的 reactivity 响应式。

实现目标

假设存在以下数据


  let rawData = 0
  let reactData=rawData+5

当修改数据 rawData 的时候,我们想要同步更新数据 reactData,所以我们可以将上面的代码修改为:

  function reactive(){
    reactData = rawData+5
  }

这样,我们修改数据 rawData 的时候调用 reactive()就可以同时得到修改后的 reactData。这其实就是一个最简单的响应式了。

@vue/reactivity

Vue 的 reactivity 模块可以独立出来,这里我们来复习一下用法,并以此为模版手写一个简单的 reactivity

♣ 我们可以直接安装一下 reactivity 模块:npm i @vue/reactivity

  const {effect,reactive} =require('@vue/reactivity')
  //声明一个响应式对象
  let  rawData = reactive({
    value:1
  })
  let reactData;
  //依赖收集
  effect(()=>{
    reactData = rawData.value+1
    console.log('reactData:',reactData)
  })
  rawData.value = 4//输出reactData:5

上面我们看到,经过响应式reactive处理后的 rawData,我们在修改 rawData 的数据时会自动更新 reactData 数据。这就是我们想要的最终结果了。 下面我们来理一下思路。

实现自己的 reactivity

  • 首先,我们要有收集依赖触发依赖的方法。

    • 收集依赖就是收集跟响应式的数据相关的逻辑。
    • 触发依赖就是当我们的响应式数据变动的时候去执行响应式逻辑。
  • 其次,我们还需要有个地方去存储收集到的依赖。额外一点就是还需要防止依赖的重复收集。

简易版

现在我们先来一版简易的:

//定义一个全局变量,这个全局变量的目的就是为了让收集依赖方法和dep中的depend联系起来
let currentEffect;
class Dep ={

  constructor(val){
    //存储我们传入的默认值,即
    this._val =val
    //用来存储收集到的依赖
    this.effects = new Set()//这里因为防止收集到重复的依赖,所以我们要用Set来实现。
  }
  //取值
  get value(){
    return this._val
  }
  //赋值
  set value(newVal){
    this._val = newVal
  }
  //依赖收集
  depend(){
    if(currentEffect){
      this.effects.add(currentEffect)
    }
  }

  //触发依赖
  notice(){
    this.effects.forEach(effect=>effect())
  }
}

上面的代码仅仅是实现了依赖的触发和收集,是一个最简单的框架。下面我们来看一下使用:


const rawData =new Dep(5)
function effect(eff){
  currentEffect =eff
  //直接触发依赖
  eff()
  //触发依赖收集
  rawData.depend()
  //清空currentEffect
  currentEffect =null
}
let reactData;
effect(()=>{
  reactData = rawData.value+5
  console.log('reactData:',reactData)
})

rawData.value = 10
rawData.notice()//输出   reactData:15

♣ 这里有个小细节就是,我们通过全局变量currentEffect 来和 reactive 中的depend 依赖收集联系起来,而不是直接调用 reactive 的 depend 方法。这里后面会解释原因。

上面的代码运行后,可以基本实现我们的功能了。这个实现需要我们手动的调用notice 触发依赖。需要我们显示的去进行依赖的收集,这明显不符合我们的要求。所以我们在进行下一步的改装。

进阶版:自动收集依赖和自动触发依赖

这一步改进的目的就是要将我们的触发依赖依赖收集自动化,省去我们手动调用的过程。

这步其实很简单,同时也涉及到 Js 里面一些基本的概念,如果有不是太明白的可以参考一下 MDN 中关于getset的相关概念。 其实就是通过 get 和 set 的特性进行数据劫持,这样我们就可以将依赖的触发和收集封装到对应的函数中:

  class Dep{
    ...
    get value(){
      this.depend()//触发依赖的收集
      return this._val
    }

    set value(newVal){
      this._val = newVal
      this.notice()
    }
    ...
  }

经过上面的改进后,我们下面的使用其实就省去了手动触发依赖和依赖收集的过程了:

const rawData =new Dep(5)
function effect(eff){
  currentEffect =eff
  eff()//这里的eff里面因为调用了响应式数据的get方法,自动触发了依赖收集的过程。
  currentEffect =null
}
let reactData;
effect(()=>{
  reactData = rawData.value+5
  console.log('reactData:',reactData)
})

//因为响应式数据发生了变化,触发了set方法,进而完成了 执行依赖的过程。
rawData.value = 10//输出 reactData:15

再进阶版:实现正式版的 reactive

上面我们实现了依赖的自动收集和执行,但是还有个问题就是响应式数据只能是单一的类型且只能挂在到 value 属性上,这个更像是 Vue3 里面的ref。所以我们后面要改进的就是需要支持对象响应式。

这里我们额外介绍一下实现劫持对象的两种方式:

  • Object.defineProperties():这个是用来在对象上定义新的属性或者修改现有属性的。Vue2 实现数据劫持就是用的这个方法。

    • 局限性:例如不能对新增属性实现劫持、不能对因数组长度引起的变化进行劫持。另外,因为他是对属性进行劫持,所以当我们的对象层层嵌套的时候需要递归处理,这也是一件很耗性能的事情。
  • Proxy:proxy 是直接代理对象。这个和 Object.defineProperties 在功能上其实没有什么区别,都是劫持。只不过 proxy 是直接拦截对象的变化,所以就避开了 defineProperties 的那些坑。

    • 这里再额外提一句Reflect,它和 Proxy 一般是成对出现的。它提供拦截 JS 操作的方法

有了上面的基础,我们下面来改造一下我们的 reactive:

//targetMap的作用是存储依赖和数据的映射
const targetMap = new Map()

//获取依赖
function getDep(target,key){
  let deps = targetMap.get(target)
  //第一次的时候初始化一下
  if (!deps) {
    deps = new Map()
    targetMap.set(target, deps)
  }
  let dep = deps.get(key)
  //第一次dep不存在的时候,初始化一下
  if (!dep) {
    dep = new Dep()
    deps.set(key, dep)
  }
  //上面这堆东西就是为了让key和dep映射起来
  return dep
}

  //包装响应式数据
function reactive(raw) {
    return new Proxy(raw, {
      //get的作用就是收集依赖
      get(target, key) {
        const dep = getDep(target, key)
        dep.depend()
        return Reflect.get(target, key)
      },

      set(target, key, value) {
        //set的主要作用就是触发依赖
        //获取dep对象
        const dep = getDep(target, key)
        //! 这里需要更新值以后才去通知更新
        const result = Reflect.set(target, key, value)
        dep.notice()
        return result
      }
    })
}

//这里还需要精简一下effectWatch
function effectWatch(effect) {
  currentEffect = effect
  //第一次收集的时候会调用收集到的依赖函数
  effect()
  //置空
  currentEffect = null
}

这里,我们引入了 reactive 来封装响应式数据,使得我们的响应式可以支持复杂的对象类型。这里我们引入了一个 Map 用来存储数据和 Dep 依赖的映射。 这里有个需要注意的地方 targetMap 里面存储的的也是一个 Map,这里面用来存储的就是对象属性和依赖的映射了。

简单概括一下我们 reactive 的原理:我们通过 Proxy 对需要处理的响应式数据进行数据劫持,形成数据和依赖的映射。然后在我们获取数据的时候进行依赖收集,在我们更新数据的时候执行依赖。 下面我们来使用一下:

  let demo = reactive({
    num:1
  })
  let reactiveData
  effectWatch(()=>{
    reactiveData = demo+2
    console.log(reactiveData)
  })

  demo.num = 4
  //输出
  //3
  //6

总结

上面就是我们 min-vue 中的响应式了。响应式其实是一种思路,我们在 Vue 中使用的响应式基本上都涉及到了视图的更新,这只是其中的一方面。这也是为什么 Vue3 将 reactive 模块单独抽离出来的原因。我们有了这种响应式的思路,可以将其应用的更多的地方。 后面我们会结合界面的渲染慢慢实现自己的 min-vue。

PS:最后附上代码地址