您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

带你彻底掌握 Vue 3.0 的响应式系统

时间:2020-04-13 19:04:02  来源:  作者:

熟悉vue的小伙伴应该都知道,谈到vue的原理,最重要的莫过于:响应式,虚拟dom及diff算法.

随着 Vue 3.0 Pre Alpha 版本的公布,我们得以一窥其源码的实现。Vue 最巧妙的特性之一是其响应式系统,而我们也能够在仓库的 packages/reactivity 模块下找到对应的实现。虽然源码的代码量不多,网上的分析文章也有一堆,但是要想清晰地理解响应式原理的具体实现过程,还是挺费脑筋的事情。经过一天的研究和整理,我把其响应式系统的原理总结成了一张图,而本文也将围绕这张图去讲述具体的实现过程。

 

带你彻底掌握 Vue 3.0 的响应式系统

 

 

一个基本的例子

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

  1. 在项目根目录运行 yarn dev reactivity,然后进入 packages/reactivity 目录找到产出的 dist/reactivity.global.js 文件。
  2. 新建一个 index.html,写入如下代码:
<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>

3.在浏览器打开该文件,于控制台执行 state.count++,便可看到输出 set count to 1。

在上述的例子中,我们使用 reactive() 函数把 origin 对象转化成了 Proxy 对象 state;使用 effect() 函数把 fn() 作为响应式回调。当 state.count 发生变化时,便触发了 fn()。接下来我们将以这个例子结合上文的流程图,来讲解这套响应式系统是怎么运行的。

 

初始化阶段

 

带你彻底掌握 Vue 3.0 的响应式系统

 

 

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

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

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

大家都知道,Vue 3.0 使用了 Proxy 来代替之前的 Object.defineProperty(),改写了对象的 getter/setter,完成依赖收集和响应触发。但是在这一阶段中,我们暂时先不管它是如何改写对象的 getter/setter 的,这个在后续的”依赖收集阶段“会详细说明。为了简单起见,我们可以把这部分的内容浓缩成一个只有两行代码的 reactive() 函数:

export function reactive(target) {
  const observed = new Proxy(target, handler)
  return observed
}

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

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

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

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

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

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()
    }
  }
}

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

 

依赖收集阶段

 

带你彻底掌握 Vue 3.0 的响应式系统

 

 

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

依赖收集阶段最重要的目的,就是建立一份”依赖收集表“,也就是图示的”targetMap"。targetMap 是一个 WeakMap,其 key 值是~~当前的 Proxy 对象 state~~代理前的对象origin,而 value 则是该对象所对应的 depsMap。

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

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

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 应该为这个样子:

带你彻底掌握 Vue 3.0 的响应式系统

 

 

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

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。在控制台上看看效果:

带你彻底掌握 Vue 3.0 的响应式系统

 

 

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

 

带你彻底掌握 Vue 3.0 的响应式系统

 

 

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

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

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

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 的一些实现思路给惊艳到,收获良多。本文按照响应式系统的运行过程,划分了”初始化“,”依赖收集“和”响应式“三个阶段,分别阐述了各个阶段所做的事情,应该能够较好地帮助读者理解其核心思路。



Tags:Vue 3.0   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
熟悉vue的小伙伴应该都知道,谈到vue的原理,最重要的莫过于:响应式,虚拟dom及diff算法.随着 Vue 3.0 Pre Alpha 版本的公布,我们得以一窥其源码的实现。Vue 最巧妙的特性之一是其...【详细内容】
2020-04-13  Tags: Vue 3.0  点击:(52)  评论:(0)  加入收藏
▌简易百科推荐
本文分为三个等级自顶向下地分析了glibc中内存分配与回收的过程。本文不过度关注细节,因此只是分别从arena层次、bin层次、chunk层次进行图解,而不涉及有关指针的具体操作。前...【详细内容】
2021-12-28  linux技术栈    Tags:glibc   点击:(3)  评论:(0)  加入收藏
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(2)  评论:(0)  加入收藏
程序是如何被执行的&emsp;&emsp;程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(9)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(8)  评论:(0)  加入收藏
下载Eclipse RCP IDE如果你电脑上还没有安装Eclipse,那么请到这里下载对应版本的软件进行安装。具体的安装步骤就不在这赘述了。创建第一个标准Eclipse RCP应用(总共分为六步)1...【详细内容】
2021-12-22  阿福ChrisYuan    Tags:RCP应用   点击:(7)  评论:(0)  加入收藏
今天想简单聊一聊 Token 的 Value Capture,就是币的价值问题。首先说明啊,这个话题包含的内容非常之光,Token 的经济学设计也可以包含诸多问题,所以几乎不可能把这个问题说的清...【详细内容】
2021-12-21  唐少华TSH    Tags:Token   点击:(10)  评论:(0)  加入收藏
实现效果:假如有10条数据,分组展示,默认在当前页面展示4个,点击换一批,从第5个开始继续展示,到最后一组,再重新返回到第一组 data() { return { qList: [], //处理后...【详细内容】
2021-12-17  Mason程    Tags:VUE   点击:(14)  评论:(0)  加入收藏
什么是性能调优?(what) 为什么需要性能调优?(why) 什么时候需要性能调优?(when) 什么地方需要性能调优?(where) 什么时候来进行性能调优?(who) 怎么样进行性能调优?(How) 硬件配...【详细内容】
2021-12-16  软件测试小p    Tags:性能调优   点击:(20)  评论:(0)  加入收藏
Tasker 是一款适用于 Android 设备的高级自动化应用,它可以通过脚本让重复性的操作自动运行,提高效率。 不知道从哪里听说的抖音 app 会导致 OLED 屏幕烧屏。于是就现学现卖,自...【详细内容】
2021-12-15  ITBang    Tags:抖音防烧屏   点击:(25)  评论:(0)  加入收藏
11 月 23 日,Rust Moderation Team(审核团队)在 GitHub 上发布了辞职公告,即刻生效。根据公告,审核团队集体辞职是为了抗议 Rust 核心团队(Core team)在执行社区行为准则和标准上...【详细内容】
2021-12-15  InfoQ    Tags:Rust   点击:(25)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条