Vue的响应系统
随着 Vue 3.0 Pre Alpha 版本的公布,我们得以一窥其源码的实现。Vue 最巧妙的特性之一是其响应式系统,而我们也能够在仓库的 packages/reactivity 模块下找到对应的实现。虽然源码的代码量不多,网上的分析文章也有一堆,但是要想清晰地理解响应式原理的具体实现过程,还是挺费脑筋的事情。经过一天的研究和整理,我把其响应式系统的原理总结成了一张图,而本文也将围绕这张图去讲述具体的实现过程。
1 一个基本的例子
Vue 3.0 的响应式系统是独立的模块,可以完全脱离 Vue 而使用,所以我们在 clone 了源码下来以后,可以直接在 packages/reactivity 模块下调试。在项目根目录运行 yarn dev reactivity,然后进入 packages/reactivity 目录找到产出的 dist/reactivity.global.js 文件。新建一个 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>
在浏览器打开该文件,于控制台执行 state.count++,便可看到输出 set count to 1。在上述的例子中,我们使用 reactive() 函数把 origin 对象转化成了 Proxy 对象 state;使用 effect() 函数把 fn() 作为响应式回调。当 state.count 发生变化时,便触发了 fn()。接下来我们将以这个例子结合上文的流程图,来讲解这套响应式系统是怎么运行的。
1.初始化阶段
在初始化阶段,主要做了两件事。把 origin 对象转化成响应式的 Proxy 对象 state。把函数 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
}
完整代码在 reactive.js。这里的 handler 就是改造 getter/setter 的关键,我们放到后文讲解。接下来我们分析第二件事。当一个普通的函数 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()
}
}
}
至此,初始化阶段已经完成。接下来就是整个系统最关键的一步——依赖收集阶段。
2.依赖收集阶段
这个阶段的触发时机,就是在 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 应该为这个样子:
这样,{ 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 是非常重要的,因为这是整个响应式系统核心中的核心。
3.响应阶段
回顾上一章节的例子,我们得到了一个 { count: 0, age: 18 } 的 Proxy,并构造了三个 effect。在控制台上看看效果:
效果符合预期,那么它是怎么实现的呢?首先来看看这个阶段的原理图:
当修改对象的某个属性值的时候,会触发对应的 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-next 对应的源码,或者这篇文章,看看这些情况都是怎么处理的。至此,响应式阶段完成。
2 总结
阅读源码的过程充满了挑战性,但同时也常常被 Vue 的一些实现思路给惊艳到,收获良多。
本文按照响应式系统的运行过程,划分了”初始化“,”依赖收集“和”响应式“三个阶段,分别阐述了各个阶段所做的事情,应该能够较好地帮助读者理解其核心思路。最后附上文章实例代码的仓库地址,有兴趣的读者可以自行把玩:
tiny-reactive(https://github.com/jrainlau/tiny-reactive)
参考:https://juejin.im/post/5d9da45af265da5b8072de5
Vue的响应系统的更多相关文章
- Vue的响应式系统
Vue的响应式系统 我们第一次使用Vue的时候,会感觉有些神奇,举个例子: <div id="app"> <div>价格:¥{{price}}</di ...
- 你是如何理解Vue的响应式系统的
1.响应式系统简述: 任何一个 Vue Component 都有一个与之对应的 Watcher 实例. Vue 的 data 上的属性会被添加 getter 和 setter 属性. 当 Vue Co ...
- 基于Vue实现后台系统权限控制
原文地址:http://refined-x.com/2017/08/29/基于Vue实现后台系统权限控制/,转载请注明出处. 用Vue/React这类双向绑定框架做后台系统再适合不过,后台系统相比普通 ...
- vue深入响应式原理
vue深入响应式原理 深入响应式原理 — Vue.jshttps://cn.vuejs.org/v2/guide/reactivity.html 注意:这里说的响应式不是bootsharp那种前端UI ...
- Vue 数据响应式原理
Vue 数据响应式原理 Vue.js 的核心包括一套“响应式系统”.“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码.例如,视图渲染中使用了数据,数据改变后,视图也会自动更新. 举个简单 ...
- Vue的响应原理
渲染render function之后就是 核心的响应式过程了 Object.defineProperty vue的核心之一就是Object.defineProperty 方法(IE9及其以上) Ob ...
- 一探 Vue 数据响应式原理
一探 Vue 数据响应式原理 本文写于 2020 年 8 月 5 日 相信在很多新人第一次使用 Vue 这种框架的时候,就会被其修改数据便自动更新视图的操作所震撼. Vue 的文档中也这么写道: Vu ...
- vue.js响应式原理解析与实现
vue.js响应式原理解析与实现 从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染.之后,再接触了vue.js,当时也一度很 ...
- 由自定义事件到vue数据响应
前言 除了大家经常提到的自定义事件之外,浏览器本身也支持我们自定义事件,我们常说的自定义事件一般用于项目中的一些通知机制.最近正好看到了这部分,就一起看了下自定义事件不同的实现,以及vue数据响应的基 ...
随机推荐
- linux端口释放
查看linux连接端口范围 cat /proc/sys/net/ipv4/ip_local_port_range 调低端口释放等待时间 echo 20 > /proc/sys/net/ipv4 ...
- MySQL_(Java)【事物操作】使用JDBC模拟银行转账向数据库发起修改请求
MySQL_(Java)使用JDBC向数据库发起查询请求 传送门 MySQL_(Java)使用JDBC向数据库中插入(insert)数据 传送门 MySQL_(Java)使用JDBC向数据库中删除(d ...
- MySQL_(Java)分页查询MySQL中的数据
MySQL_(Java)使用JDBC向数据库发起查询请求 传送门 MySQL_(Java)使用JDBC创建用户名和密码校验查询方法 传送门 MySQL_(Java)使用preparestatement ...
- 51nod 1165 整边直角三角形的数量(两种解法)
链接:http://www.51nod.com/Challenge/Problem.html#!#problemId=1165 直角三角形,三条边的长度都是整数.给出周长N,求符合条件的三角形数量. ...
- C++入门经典-例8.5-多重继承
1:C++允许子类从多个父类继承公有的和受保护的成员,这称为多重继承. 2:多重继承的定义.多重继承有多个基类名称标识符,其声明形式如下: class 派生类名标识符:[继承方式] 基类名标识符1,. ...
- LeetCode 230. 二叉搜索树中第K小的元素(Kth Smallest Element in a BST)
题目描述 给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素. 说明:你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数. 示例 1: 输入: roo ...
- 在win10系统安装两个不同版本的mySQL数据库
我们项目用的mySQL数据库,为了回家看代码方便,于是在本地安装mySQL数据库,一开始安装了比服务器mySQL5.7.24更高版本的mySQL8.0.11,结果项目启动报错,估计是版本太高项目中引入 ...
- 显示Pl/Sql Developer window list窗口
默认情况下Window List窗口是不显示的,这十分不方便 (一)在菜单项的Tools下的Preference选项中的UserInterface中选择Option,在右边对于的Autosave de ...
- LNMPA是什么?
也许大家对LAMP.LNMP比较熟悉,LAMP代表Linux下Apache.MySQL.PHP这种网站服务器架构:LNMP代表的是Linux下Nginx.MySQL.PHP这种网站服务器架构.LNMP ...
- Uboot启动分析之Start.S
1.start.S引入 1.1.u-boot.lds中找到start.S入口 1)C语言中代码的分析第一步就是找到main.c,找到函数的入口 2)uboot中因为有汇编语言参与所以就不能像C一样.U ...