reactive + effect + track + trigger 实现响应式系统

effect 方法

基本用法

如果之前了解过 Vue2 的响应式原理,那么对于 Watcher 你一定不会陌生。它是 Vue2 响应式系统中的核心之一,无论是响应式数据,还是 computed 计算属性,watch 监听器,内部都是用了 Watcher。简单来说,它就是把需要用户手动执行的逻辑进行了封装,控制权从用户手中转移到了框架层面,从而实现了数据变化,页面自动更新的响应式系统。

Vue3 中的 effect 方法的作用和 Watcher 一样。

先来看一个简单示例:

<body>
    <div id="app"></div>
    
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.37/vue.global.js"></script>

    <script>
        const {
    
     reactive, effect } = Vue
        const person = {
    
     name: 'kw', age: 18 }
        const state = reactive(person)

        // effect 方法接收一个函数作为参数。
        effect(() => {
    
    
          app.innerHTML = 'Hello! ' + state.name
        })

        setTimeout(() => {
    
    
          state.name = 'zk'
        }, 1000)
    </script>
</body>
复制代码

打开浏览器,可以发现 effect 方法执行,它接收的回调函数也执行了,于是页面上有了内容:
在这里插入图片描述

当 1s 过后,我们修改了 state 的属性,发现页面会自动更新:
在这里插入图片描述

这就是响应式系统带给我们的能力。

副作用函数

关于 effect 方法的理解,一直以来都十分模糊,直到看了 《Vue.js设计与实现》 这本书中的相关介绍。
书中将 Vue3 提供的 effect 定义为用来注册副作用函数的一个方法。所谓的副作用函数,可以理解为一个函数执行,会影响到其他函数的执行。比如:

var num = 10

function fn1(){
    
    
    num = 20
}

function fn2(){
    
    
    // fn2 的本职工作:
    console.log('fn2')
    // fn2 产生的副作用
    num = 30
}
复制代码

fn1 函数的作用是修改 num 变量的值。当 fn2 函数执行时,也修改了 num 的值,于是产生了对 fn1 的影响,也就是产生了副作用。
上面示例中 effect 方法所接收的函数参数,就是一个副作用函数:
在这里插入图片描述

为了方便清楚描述 effect 方法和它接收的副作用函数,我们将前者依然叫 effect 方法,后者叫作 副作用函数 fn。示例中的 fn 其实就是本来要用户自己手动执行的逻辑:当页面渲染时,需要用户手动渲染数据到页面上;当数据更新了,需要用户再手动调用渲染一次。

effect 方法要做的事情,就是将这个原本属于用户的逻辑封装起来,交给框架来管理,在合适的时机去调用。

所谓合适的时机,无非就两个,一是页面首次渲染时,二是它依赖的数据更新时。

扫描二维码关注公众号,回复: 14389616 查看本文章

在此基础上,结合前面所实现的 reactive 方法,已经初步具备响应式系统的雏形了:页面首次渲染时,执行 effect 方法,将 副作用函数 fn 收集起来并执行,此时会用到某些响应式数据,需要记住 fn 所依赖的属性;当其依赖的属性发生变化后,再想办法通知 fn 再次执行。

实现 effect

有了上面的思路,我们先来实现 effect 方法。

// reactivity/src/effect.ts

export function effect(fn) {
    
    
  // effect 方法接收一个函数参数,需要将其保存,并执行一次;以后还会扩展出更多的功能,所以将其封装为一个 ReactiveEffect 类进行维护
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}

class ReactiveEffect {
    
    
    constructor(fn) {
    
    
        this.fn = fn
    }
    
    run() {
    
    
        this.fn()
    }
}
复制代码

上面我们实现了 effect 方法和一个新的类 ReactiveEffect。
effect 方法执行,会创建一个 ReactiveEffect 类的实例对象,命名为 _effect。这个类会将副作用函数 fn 保存起来,并立即执行一次。
后面要实现的依赖收集功能,收集的就是这个 _effect 实例。其实这个 ReactiveEffect 会更像 Vue2 中的 Watcher,Vue2 中的依赖收集,收集的就是一个 Watcher 类的实例。
注意,要区分 effect方法和它创建的 _effect 实例。前者用来注册副作用函数,生成 _effect实例,这才是依赖收集的真正要收集的东西。
将 effect 方法暴露出去:

// reactivity/src/index.ts

export {
    
     reactive } from './reactive' 
export {
    
     effect } from './effect' 
复制代码

到这里,我们实现的 effect 方法也能像原版那样,在初始化时执行一次 fn,并将 fn 保存下来。

track 依赖收集

前面示例中的副作用函数 fn 执行时,用到了一个 name 属性,也就是访问到了响应式对象的属性,所以逻辑会走到 reactive 方法中实现代理那里,对属性 get 操作的监听。此时就可以做依赖收集了。
那么我们先去定义一个全局变量 activeEffect ,表示当前正在执行的 effect 方法生成的 ReactiveEffect 类的实例 _effect。
这样,只要 effect 方法执行,我们就能拿到此时的 _effect。

// reactivity/src/effect.ts

export let activeEffect;

export class ReactiveEffect {
    
    
  constructor(fn) {
    
    
    this.fn = fn
  }

  run() {
    
    
    //  _effect 赋给全局的变量 activeEffect
    activeEffect = this
    // fn执行时,内部用到的响应式数据的属性会被访问到,就能触发 proxy 对象的 get 取值操作
    this.fn() 
  }
}
复制代码

回到 reactive 方法中,我们要使用一个 track 方法,用于“追踪”并保存 target,key 和此时的 _effect 的关系:

const handler = {
    
    
    // 监听属性访问操作
    get(target, key, receiver) {
    
    
      if(key === ReactiveFlags.IS_REACTIVE) {
    
    
        return true
      }
      console.log(`${
    
    key}属性被访问,依赖收集`)
      // 依赖收集,让 target, key  当前的 _effect 关联起来
      track(target, key)

      const res = Reflect.get(target, key)
      if(isObject(res)) {
    
    
        return reactive(res)
      }
      return res
    }
}
复制代码

实现 track 方法

该方法定义在 effect.ts 中。
所谓收集,就是需要有一个存储空间来存放所有的依赖信息。

我们使用一个 WeakMap 结构来存储所有的依赖信息,key 是_effect 中用到的响应式对象的原始对象,也就是 target;value 则又是一个 Map结构,它的 key 就是 target 的 key 了,它的 value 又是一个 Set结构 ,用来存储所有的 _effect。如下图:
在这里插入图片描述

// 存储所有的依赖信息,包含 target、key  _effect
const targetMap = new WeakMap

/**
 * 依赖收集。关联对象、属性和 _effect。
 */
export function track(target, key) {
    
    
  if(!activeEffect) return

  // 从缓存中找到 target 对象所有的依赖信息
  let depsMap = targetMap.get(target)
  if(!depsMap) {
    
    
    targetMap.set(target, depsMap = new Map)
  }
  // 再找到属性 key 所对应的 _effect集合
  let deps = depsMap.get(key)
  if(!deps) {
    
    
    depsMap.set(key, deps = new Set)
  }
    
  // 如果 _effect 已经被收集过了,则不再收集
  let shouldTrack = !deps.has(activeEffect)
  if(shouldTrack) {
    
    
    deps.add(activeEffect)
  }
}
复制代码

到这里,就实现了一个可用的依赖收集功能。

trigger 派发更新

接下来,当属性发生变化了,还应该有一个机制去做派发更新。
我们使用一个 trigger 方法,用于派发更新:

// reactivity/src/index.ts

const handler = {
    
    
    //...
      
    // 监听设置属性操作
    set(target, key, value, receiver) {
    
    
      console.log(`${
    
    key}属性变化了,派发更新`)
     
      if(target[key] !== value) {
    
    
        const result = Reflect.set(target, key, value, receiver);
        // 派发更新,通知 target 的属性,让依赖它的 _effect 再次执行
        trigger(target, key);
        return result
      }
    }
}
复制代码

实现 trigger 方法

回到 effect.ts 中。trigger 方法的实现思路也很简单,就是从前面的依赖缓存 targetMap 中,找到此时 target 的某个 key 对应的 _effect 依赖集合,让其中的所有 _effect 依次执行即可:

// reactivity/src/effect.ts

export function trigger(target, key) {
    
    
  // 找到 target 的所有依赖
  let depsMap = targetMap.get(target)
  if(!depsMap) {
    
    
    return 
  }

  // 属性依赖的 _effect 列表
  let effects = depsMap.get(key)
  if(effects) {
    
    
    // 属性的值发生变化,找到它依赖的 _effect 列表,让所有的 _effect 依次执行
    effects.forEach(effect => {
    
    
      effect.run()
    })
 }
}
复制代码

测试

先执行打包命令:

pnpm dev
复制代码

编写测试文件:

// reactivity/test/2.effect-track-trigger.html

<body>
    <div id="app"></div>

    <script src="../dist/reactivity.global.js"></script>
    <script>
        const {
    
     reactive, effect } = VueReactivity
        const obj = {
    
     name: 'kw', age: 18, grade: {
    
     math: 60 } }
        const state = reactive(obj)
        effect(() => {
    
    
            app.innerHTML = `${
    
    state.name}数学考了${
    
    state.grade.math}`
        })
        setTimeout(() => {
    
    
            state.grade.math = 80
        }, 1000)
    </script>
</body>
复制代码

访问浏览器,结果如图:
在这里插入图片描述

到这里,我们基本上实现了一个响应式系统:数据变化,页面自动更新。

小结

我们先实现了一个 effect 方法,用于管理一些需要重复执行的逻辑,原本这些都是由用户控制的,比如设置页面的显示内容。
之后,结合上篇文章实现的 reactive 方法,在属性被访问到时,进行依赖收集,主要依靠 track 方法 ;当属性发生变化后,再利用 trigger 方法,通知收集来的 _effect 重新执行。
经过这样的整合,基本上实现了一个可用的响应式系统:
在这里插入图片描述

当然,现在的 effect 方法是不严谨的,还存在一些问题

源码附件已经打包好上传到百度云了,大家自行下载即可~

链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27
提取码: yu27
百度云链接不稳定,随时可能会失效,大家抓紧保存哈。

如果百度云链接失效了的话,请留言告诉我,我看到后会及时更新~

开源地址

码云地址:
http://github.crmeb.net/u/defu

Github 地址:
http://github.crmeb.net/u/defu

链接:https://juejin.cn/post/7123115108603494407

猜你喜欢

转载自blog.csdn.net/qq_39221436/article/details/125944019