前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >reactive + effect + track + trigger 实现响应式系统

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

原创
作者头像
PHP开发工程师
发布2022-07-23 10:33:43
7090
发布2022-07-23 10:33:43
举报
文章被收录于专栏:thinkphp+vue

effect 方法

基本用法

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

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

先来看一个简单示例:

代码语言:javascript
复制
<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 定义为用来注册副作用函数的一个方法。所谓的副作用函数,可以理解为一个函数执行,会影响到其他函数的执行。比如:

代码语言:javascript
复制
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 方法要做的事情,就是将这个原本属于用户的逻辑封装起来,交给框架来管理,在合适的时机去调用。

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

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

实现 effect

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

代码语言:javascript
复制
// 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 方法暴露出去:

代码语言:javascript
复制
// 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。

代码语言:javascript
复制
// 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 的关系:

代码语言:javascript
复制
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。如下图:

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
// 存储所有的依赖信息,包含 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 方法,用于派发更新:

代码语言:javascript
复制
// 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 依次执行即可:

代码语言:javascript
复制
// 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()
    })
 }
}
复制代码

测试

先执行打包命令:

代码语言:javascript
复制
pnpm dev
复制代码

编写测试文件:

代码语言:javascript
复制
// 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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • effect 方法
  • 基本用法
  • 副作用函数
  • 实现 effect
  • track 依赖收集
  • 实现 track 方法
  • trigger 派发更新
  • 实现 trigger 方法
  • 测试
  • 小结
  • 开源地址
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档