vue3中的响应式系统实现

104 阅读3分钟

vue3中的响应式系统实现

响应式系统是vue.js的重要组成部分,这里面又涉及到了响应式数据与副作用函数及无限递归,副作用函数嵌套,两个副作用函数之间会产生的影响等问题,所以我们一步一步的来讲解。


一、什么是副作用函数?

定义:指会产生副作用的函数。一个函数(这里我们叫effect)的执行会直接或简介的影响其他函数的执行,这时effect函数就产生了副作用。其实副作用是非常容易产生的,比如一个函数修改了全局变量,这也是一个副作用。

	const data = { text: '123' }
    function effect() {
      document.body.innerText = data.text
    }
    data.text = '321'

看上述代码,effect函数的执行会设置body的值,但除了它,其他函数也可以对body的内容进行设置读取,那么effect就产生了副作用,他就是副作用函数。

二、响应式数据

依旧让我们看上述代码,很明显当data.text改变时界面是不会跟随改的,应为data是个普通的对象,如果当data.text的值发生变化时,副作用函数可以重新执行,那么对象data就是响应式数据。

1.最简单的响应式系统实现

我们如何实现简单的响应式系统呢? 1.当effect函数执行时,会触发字段data.text的读取操作。 2. 当修改data.text值修改时,会触发data.text的设置操作。 很明显我们只要能拦截一个对象的设置与读取操作,我们的响应式系统就实现了,接下来我们看看如何用代码实现。

// 存储副作用的容器
    const bucket = new Set()
    // 原始数据
    const data = { text: '123' }
    // 代理原始数据
    const obj= new Proxy(data, {
      get(target, key) {
        bucket.add(effect)
        return target[key]
      },
      set(target, key, newVal) {
        target[key] = newVal
        bucket.forEach(fn => fn())
        return true
      }
    })

    function effect() {
      document.body.innerText = obj.text
    }

    effect()
    document.body.innerText = obj.text
    setTimeout(() => {
      obj.text = '321'
    }, 1000);

我们定义了一个Set类型的容器(桶),在读取obj.text的值时,我们就把effect函数放到桶里,在设置obj.text的effect函数取出并执行。而我们的拦截操作是通过代理对象Proxy来实现的,这也是vue3中采用的方式,(vue2采用的是Object.defineProperty函数实现的)。这样看来是不是很简单,只通过Proxy进行拦截就可以实现了。

2.进阶的响应式系统

观察上面的代码,我们可以发现副作用函数,使我们硬编码的名字(effect),万一他不叫这个名字呢?并且当你在定时器中不是给text修改值,而是写的obj.aaa = 123,你会发现他也会执行,这是没必要的,另外Set数据结构,会导致我们没有在副作用函数与被操作的目标字段之间建立明确关系。所以我们还得优化代码。

// 存储副作用的容器
    const bucket = new WeakMap()
    let activeEffect // 存储被注册的副作用函数
    // 用来注册副作用函数
    function effect(fn) {
      activeEffect = fn
      fn()
    }

    // 原始数据
    const data = { text: '123' }
    // 代理原始数据
    const obj = new Proxy(data, {
      get(target, key) {
        track(target, key)
        return target[key]
      },
      set(target, key, newVal) {
        target[key] = newVal

      }
    })

    function track(target, key) {
      if (!activeEffect) return
      let depsMap = bucket.get(target) // key --> effects
      if (!depsMap) {
        bucket.set(target, (depsMap = new Map())) // 新建Map与target相连
      }
      let deps = depsMap.get(key)
      if (!deps) {
        depsMap.set(key, (deps = new Set())) // 新建Set与key相连
      }

      deps.add(activeEffect)
    }

    function trigger(target, key) {
      const depsMap = bucket.get(target)
      if (!depsMap) return
      // 根据key取得所有辅助用函数
      const effects = depsMap.get(key)
      effects && effects.forEach(fn => fn())
    }

    effect(() => {
      console.log('effect run');
      document.body.innerText = obj.text
    })
    setTimeout(() => {
      obj.text = '321'
      // obj.notExist = '嘿嘿'
    }, 1000);