前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一张图理清Vue 3.0的响应式系统,实现精简版的Vue 3.0响应式系统

一张图理清Vue 3.0的响应式系统,实现精简版的Vue 3.0响应式系统

作者头像
夜尽天明
发布2020-05-06 13:59:35
4190
发布2020-05-06 13:59:35
举报
文章被收录于专栏:全栈修炼全栈修炼

随着 Vue 3.0 Pre Alpha 版本的公布,我们得以一窥其源码的实现。

Vue 最巧妙的特性之一是其响应式系统,而我们也能够在仓库的 packages/reactivity 模块下找到对应的实现。

虽然源码的代码量不多,网上的分析文章也有一堆,但是要想清晰地理解响应式原理的具体实现过程,还是挺费脑筋的事情。

经过一天的研究和整理,我把其响应式系统的原理总结成了一张图,而本文也将围绕这张图去讲述具体的实现过程。

文章涉及到的代码我也已经上传到仓库,结合代码阅读本文会更为流畅哦!

一个基本的例子

Vue 3.0 的响应式系统是独立的模块,可以完全脱离 Vue 而使用,所以我们在 clone 了源码下来以后,可以直接在 packages/reactivity 模块下调试。

  • 在项目根目录运行 yarn dev reactivity,然后进入 packages/reactivity 目录找到产出的 dist/reactivity.global.js 文件。
  • 新建一个 index.html,写入如下代码:
代码语言:javascript
复制
<script src="./dist/reactivity.global.js"></script>
<script>
const { reactive, effect } = VueObserver  
const origin = {   
    count: 0 
} 
const state = reactive(origin)  
const fn = () => {   
    const count = state.count   
    console.log(`set count to ${count}`) 
} 
effect(fn)
</script>
  • 在浏览器打开该文件,于控制台执行 state.count++,便可看到输出 set count to 1

在上述的例子中,我们使用 reactive() 函数把 origin 对象转化成了 Proxy 对象 state

使用 effect() 函数把 fn() 作为响应式回调。

state.count 发生变化时,便触发了 fn()

接下来我们将以这个例子结合上文的流程图,来讲解这套响应式系统是怎么运行的。

初始化阶段

在初始化阶段,主要做了两件事。

  1. origin 对象转化成响应式的 Proxy 对象 state
  2. 把函数 fn() 作为一个响应式的 effect() 函数。

首先我们来分析第一件事。

大家都知道,Vue 3.0 使用了 Proxy 来代替之前的 Object.defineProperty(),改写了对象的 getter/setter,完成依赖收集和响应触发。

但是在这一阶段中,我们暂时先不管它是如何改写对象的 getter/setter 的,这个在后续的”依赖收集阶段“会详细说明。

为了简单起见,我们可以把这部分的内容浓缩成一个只有两行代码的 reactive() 函数:

代码语言:javascript
复制
export function reactive(target) {
  const observed = new Proxy(target, handler)
  return observed
}

完整代码在 reactive.js。这里的 handler 就是改造 getter/setter 的关键,我们放到后文讲解。

接下来我们分析第二件事。

当一个普通的函数 fn()effect() 包裹之后,就会变成一个响应式的 effect 函数,而 fn() 也会被立即执行一次

由于在 fn() 里面有引用到 Proxy 对象的属性,所以这一步会触发对象的 getter,从而启动依赖收集。

除此之外,这个 effect 函数也会被压入一个名为 ”activeReactiveEffectStack“(此处为 effectStack)的栈中,供后续依赖收集的时候使用。

来看看代码(完成代码请看 effect.js):

代码语言:javascript
复制
export function effect (fn) {
  // 构造一个 effect
  const effect = function effect(...args) {
    return run(effect, fn, args)
  }
  // 立即执行一次
  effect()
  return effect
}

export function run(effect, fn, args) {
  if (effectStack.indexOf(effect) === -1) {
    try {
      // 往池子里放入当前 effect
      effectStack.push(effect)
      // 立即执行一遍 fn()
      // fn() 执行过程会完成依赖收集,会用到 effect
      return fn(...args)
    } finally {
      // 完成依赖收集后从池子中扔掉这个 effect
      effectStack.pop()
    }
  }
}

至此,初始化阶段已经完成。接下来就是整个系统最关键的一步——依赖收集阶段。

依赖收集阶段

这个阶段的触发时机,就是在 effect 被立即执行,其内部的 fn() 触发了 Proxy 对象的 getter 的时候。简单来说,只要执行到类似 state.count 的语句,就会触发 state 的 getter。

依赖收集阶段最重要的目的,就是建立一份”依赖收集表“,也就是图示的”targetMap"。

targetMap 是一个 WeakMap,其 key 值是代理前的对象origin,而 value 则是该对象所对应的 depsMap。

depsMap 是一个 Map,key 值为触发 getter 时的属性值(此处为 count),而 value 则是触发过该属性值所对应的各个 effect。

还是有点绕?那么我们再举个例子。假设有个 Proxy 对象和 effect 如下:

代码语言:javascript
复制
const state = reactive({
  count: 0,
  age: 18
})

const effect1 = effect(() => {
  console.log('effect1: ' + state.count)
})

const effect2 = effect(() => {
  console.log('effect2: ' + state.age)
})

const effect3 = effect(() => {
  console.log('effect3: ' + state.count, state.age)
})

那么这里的 targetMap 应该为这个样子:

这样,{ target -> key -> dep } 的对应关系就建立起来了,依赖收集也就完成了。代码如下:

代码语言:javascript
复制
export function track (target, operationType, key) {
  const effect = effectStack[effectStack.length - 1]
  if (effect) {
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      targetMap.set(target, (depsMap = new Map()))
    }

    let dep = depsMap.get(key)
    if (dep === void 0) {
      depsMap.set(key, (dep = new Set()))
    }

    if (!dep.has(effect)) {
      dep.add(effect)
    }
  }
}

弄明白依赖收集表 targetMap 是非常重要的,因为这是整个响应式系统核心中的核心。

响应阶段

回顾上一章节的例子,我们得到了一个 { count: 0, age: 18 } 的 Proxy,并构造了三个 effect。在控制台上看看效果:

效果符合预期,那么它是怎么实现的呢?首先来看看这个阶段的原理图:

当修改对象的某个属性值的时候,会触发对应的 setter。

setter 里面的 trigger() 函数会从依赖收集表里找到当前属性对应的各个 dep,然后把它们推入到 effectscomputedEffects(计算属性) 队列中,最后通过 scheduleRun() 挨个执行里面的 effect。

由于已经建立了依赖收集表,所以要找到属性所对应的 dep 也就轻而易举了,可以看看具体的代码实现:

代码语言:javascript
复制
export function trigger (target, operationType, key) {
  // 取得对应的 depsMap
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    return
  }
  // 取得对应的各个 dep
  const effects = new Set()
  if (key !== void 0) {
    const dep = depsMap.get(key)
    dep && dep.forEach(effect => {
      effects.add(effect)
    })
  }
  // 简化版 scheduleRun,挨个执行 effect
  effects.forEach(effect => {
    effect()
  })
}

这里的代码没有处理诸如数组的 length 被修改的一些特殊情况,感兴趣的读者可以查看 vue-next 对应的源码,或者这篇文章,看看这些情况都是怎么处理的。

至此,响应式阶段完成。

总结

阅读源码的过程充满了挑战性,但同时也常常被 Vue 的一些实现思路给惊艳到,收获良多。

本文按照响应式系统的运行过程,划分了”初始化“,”依赖收集“和”响应式“三个阶段,分别阐述了各个阶段所做的事情,应该能够较好地帮助读者理解其核心思路。

最后附上文章实例代码的仓库地址,有兴趣的读者可以自行把玩:tiny-reactive

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-04-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全栈修炼 微信公众号,前往查看

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

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个基本的例子
  • 初始化阶段
  • 依赖收集阶段
  • 响应阶段
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档