Vue3 源码阅读(2):响应式系统 —— 核心思想、基本实现

 我的开源库:

1,响应式数据和副作用函数

首先说说响应式系统中最重要的两个概念,响应式数据和副作用函数。

响应式数据指的是在 Vue 中被劫持了读写操作的数据,并且如果在副作用函数中使用了某个响应式数据,如果响应式数据发生了变化的话,我们希望这个副作用函数能够自动重新执行。

副作用函数是指会产生副作用的函数,如果一个函数的执行影响了其他代码的执行,我们就可以说这个函数是副作用函数,一个函数产生副作用是很容易的,例如一个函数修改了一个全局的变量。

我们最终想要实现的效果是:副作用函数的执行读取了响应式数据,当响应式数据发生变化,副作用函数能够重新执行,简要代码如下所示:

// 响应式数据
let reactiveData = {
  name: '小明',
  age: 18
}
// 副作用函数,该副作用函数读取了 reactiveData 响应式数据中的值,并将一个文本设置到了 body 标签中
function effect() {
  document.body.innerText = `大家好,我的名字是${reactiveData.name},我的年龄是${reactiveData.age}岁`
}

effect() // 手动执行

// 一秒钟后变更响应式数据,我们希望 effect 函数能够自动重新执行
setTimeout(() => {
  reactiveData.age = 30
}, 1000)

2,响应式系统的核心思想

如果我们想实现上面代码中的效果,需要做好三件事,分别是:

  • 对响应式数据的读写操作进行劫持。
  • 当 effect 函数的执行读取了响应式数据的时候,进行依赖的收集
  • 变更响应式数据的时候,触发依赖的执行

2-1,如何对数据的读写操作进行劫持

Vue2 中对数据的读写操作是通过 Object.defineProperty() 实现的,这种实现方式有很多的缺陷,例如无法检测对象中新增属性和删除属性的操作。相关的 Vue2 源码解析点击这里

在 Vue3 中,响应式数据的劫持操作是通过 Proxy 实现的,Proxy 相比 Object.defineProperty(),能够更加全面的监控对数据所进行的读写操作。接下来,我们实现一个最简的 reactive 函数,他能实现对初始数据的代理,代码如下所示:

function reactive(rawData) {
  return new Proxy(rawData, {
    get(target, key){
      // 进行依赖的收集
      console.log(`读取操作 - ${key}`)
      return target[key]
    },
    set(target, key, newVal){
      // 触发依赖副作用函数的执行
      console.log(`设值操作 - ${key} - ${newVal}`)
      target[key] = newVal
    }
  })
}

let reactiveData = reactive({
  name: '小明',
  age: 18
})

reactiveData.name  // 读取操作 - name
reactiveData.name = '小红'  // 设值操作 - name - 小红

2-2,当 effect 函数的执行读取了响应式数据的时候,进行依赖收集

Vue3 的依赖收集核心思路和 Vue2 是一样的,Vue2 的相关文章点击这里

在真正讲解依赖收集前,首先讲解 2 个前置知识。

2-2-1,什么是依赖

依赖就是封装了 effect 函数的实例,这个实例能够被收集存储,并向外提供了能够重新执行 effect 函数的方法。最简实现如下所示:

class ReactiveEffect {
  constructor(public fn) {
  }
  run(){
    this.fn()
  }
}

function effect(fn) {
  // _effect 就是依赖
  const _effect = new ReactiveEffect(fn)
  _effect.run()
}

effect(() => {
  // 副作用函数
  console.log("哈哈")
})

2-2-2,依赖被收集到了哪里

依赖和响应式数据的每个属性之间的关系是多对多的关系,一个依赖中副作用函数的执行有可能读取了多个响应式属性,一个响应式属性也有可能被多个副作用函数读取。

在这里,我们先讨论一个响应式属性对应多个依赖的情况,此时的对应关系是:object + key ==> [依赖1、依赖2、依赖3......],可以发现,依赖对应的是某个对象中的某个属性,因此,我们需要设计一个数据结构来进行存储,数据结构如下所示:

type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

依赖被收集到了 targetMap 中,它是一个 WeakMap 实例,targetMap 的 key 用来存储响应式对象,targetMap 的 value 是一个 Map 实例。

KeyToDepMap 的 key 用来存储响应式对象的属性,value 是一个 Set 实例。

Set 实例用来存储 ReactiveEffect 实例,也就是用来存储收集依赖

2-2-3,讲解依赖收集

当我们执行下面的代码时。

effect(() => {
  // 副作用函数
  console.log("哈哈")
})

effect 函数的内部会创建一个 ReactiveEffect 类的实例,然后执行 _effect 实例的 run 方法,在 run 方法中会执行副作用函数。

依赖收集的思路是:在执行副作用函数之前,将当前的 ReactiveEffect 类的实例设置到全局中,然后执行副作用函数,在副作用函数中,我们会进行响应式数据的读取操作,这会触发响应式对象中指定属性的 get,在 get 中,我们可以获取到当前激活的 ReactiveEffect 实例(上面已经设置到了全局作用域中),将激活的 ReactiveEffect 实例存储到 targetMap 中,就完成了依赖收集的操作,简要代码如下所示:

let activeEffect: ReactiveEffect | undefined

class ReactiveEffect {
  constructor(public fn) {
  }
  run(){
    activeEffect = this
    this.fn()
    activeEffect = undefined
  }
}
function reactive(rawData) {
  return new Proxy(rawData, {
    get(target, key){
      // 进行依赖的收集
      console.log(`读取操作 - ${key}`)
      track(target, 'get', key)
      return target[key]
    },
    set(target, key, newVal){
      // 触发依赖副作用函数的执行
      console.log(`设值操作 - ${key} - ${newVal}`)
      target[key] = newVal
    }
  })
}

export function track(target: object, type: TrackOpTypes, key: unknown) {
  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<ReactiveEffect>()))
  }
  dep.add(activeEffect)
}

2-3,当变更响应式数据的时候,触发依赖的执行。

触发依赖重新执行相比依赖收集就简单多了,在响应式数据的代理 set 中,我们能够获取到当前变更响应式数据的 target 和 key,通过 target 和 key,我们能从 targetMap 中获取到指定响应式属性对应的 dep(Set 实例),这个 dep 就保存着使用了当前这个响应式属性的 ReactiveEffect 实例,接下来遍历 dep,循环执行 ReactiveEffect 实例的 run 方法就可以了,简要代码如下所示:

function reactive(rawData) {
  return new Proxy(rawData, {
    get(target, key){
      // 进行依赖的收集
      console.log(`读取操作 - ${key}`)
      track(target, 'get', key)
      return target[key]
    },
    set(target, key, newVal){
      // 触发依赖副作用函数的执行
      console.log(`设值操作 - ${key} - ${newVal}`)
      trigger(target, 'set', key)
      target[key] = newVal
    }
  })
}

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown
) {
  const depsMap = targetMap.get(target)
  const dep = depsMap.get(key)
  triggerEffects(dep)
}

export function triggerEffects(
  dep: Dep | ReactiveEffect[]
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    triggerEffect(effect)
  }
}

function triggerEffect(
  effect: ReactiveEffect
) {
  effect.run()
}

3,结语

Vue3 和 Vue2 的响应式系统核心思想并没有变,只不过 Vue3 的源码更加的简练、解耦和易于理解,对 Vue2 响应式感兴趣的朋友可以看我的这篇博客

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值