|
|
|
|
# 28|响应式:万能的面试题,怎么手写响应式系统
|
|
|
|
|
|
|
|
|
|
你好,我是大圣。
|
|
|
|
|
|
|
|
|
|
经过前面课程的学习,相信你对Vue3的实战和组件有了新的认识,也掌握了很多实战秘籍,从今天开始,我将带你进入Vue框架的内部世界,探究一下Vue框架的原理,让你能知其然,也知其所以然。
|
|
|
|
|
|
|
|
|
|
我们将手写一个迷你的Vue框架,实现Vue3的主要渲染和更新逻辑,项目就叫weiyouyi,你可以在 [GitHub上](https://github.com/shengxinjing/weiyouyi)看到所有的核心代码。
|
|
|
|
|
|
|
|
|
|
## 响应式
|
|
|
|
|
|
|
|
|
|
在第三讲的Vue3新特性中,我们剖析了Vue3的功能结构,就是下图所示的Vue核心模块,可以看到,Vue3的组件之间是通过响应式机制来通知的,响应式机制可以自动收集系统中数据的依赖,并且在修改数据之后自动执行更新,极大提高开发的效率。
|
|
|
|
|
|
|
|
|
|
我们今天就要自己做一个迷你的响应式原型,希望你能通过自己手写,搞清楚响应式的实现原理。
|
|
|
|
|
|
|
|
|
|
![图片](https://static001.geekbang.org/resource/image/7e/9e/7e68a41ef94a39eda9cf211ed479e39e.png?wh=1920x939)
|
|
|
|
|
|
|
|
|
|
根据响应式组件通知效果可以知道,**响应式机制的主要功能就是,可以把普通的JavaScript对象封装成为响应式对象,拦截数据的获取和修改操作,实现依赖数据的自动化更新**。
|
|
|
|
|
|
|
|
|
|
所以,一个最简单的响应式模型,我们可以通过reactive或者ref函数,把数据包裹成响应式对象,并且通过effect函数注册回调函数,然后在数据修改之后,响应式地通知effect去执行回调函数即可。
|
|
|
|
|
|
|
|
|
|
整个流程这么概括地说,你估计不太理解,我们先通过一个简单的小例子直观感受一下响应式的效果。
|
|
|
|
|
|
|
|
|
|
Vue的响应式是可以独立在其他平台使用的。比如你可以新建test.js,使用下面的代码在node环境中使用Vue响应。以reactive为例,我们使用reactive包裹JavaScript对象之后,每一次对响应式对象counter的修改,都会执行effect内部注册的函数:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
const {effect, reactive} = require('@vue/reactivity')
|
|
|
|
|
|
|
|
|
|
let dummy
|
|
|
|
|
const counter = reactive({ num1: 1, num2: 2 })
|
|
|
|
|
effect(() => {
|
|
|
|
|
dummy = counter.num1 + counter.num2
|
|
|
|
|
console.log(dummy)// 每次counter.num1修改都会打印日志
|
|
|
|
|
})
|
|
|
|
|
setInterval(()=>{
|
|
|
|
|
counter.num1++
|
|
|
|
|
},1000)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
执行node test.js之后,你就可以看到effect内部的函数会一直调用,每次count.value修改之后都会执行。
|
|
|
|
|
看到这个API估计你有点疑惑,effect内部的函数式如何知道count已经变化了呢?
|
|
|
|
|
|
|
|
|
|
我们先来看一下响应式整体的流程图,上面的代码中我们使用reactive把普通的JavaScript对象包裹成响应式数据了。
|
|
|
|
|
|
|
|
|
|
所以,在effect中获取counter.num1和counter.num2的时候,就会触发counter的get拦截函数;**get函数,会把当前的effect函数注册到一个全局的依赖地图中去**。这样counter.num1在修改的时候,**就会触发set拦截函数,去依赖地图中找到注册的effect函数,然后执行**。
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/0a/d0/0a3f06629751988996e1f863e0973cd0.jpg?wh=2012x796)
|
|
|
|
|
具体是怎么实现的呢?我们从第一步把数据包裹成响应式对象开始。先看reactive的实现。
|
|
|
|
|
|
|
|
|
|
## reactive
|
|
|
|
|
|
|
|
|
|
我们进入到src/reactivity目录中,新建reactive.spec.js,使用下面代码测试reactive的功能,能够在响应式数据ret更新之后,执行effect中注册的函数:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
import { effect } from '../effect'
|
|
|
|
|
import { reactive } from '../reactive'
|
|
|
|
|
|
|
|
|
|
describe('测试响应式', () => {
|
|
|
|
|
test('reactive基本使用', () => {
|
|
|
|
|
const ret = reactive({ num: 0 })
|
|
|
|
|
let val
|
|
|
|
|
effect(() => {
|
|
|
|
|
val = ret.num
|
|
|
|
|
})
|
|
|
|
|
expect(val).toBe(0)
|
|
|
|
|
ret.num++
|
|
|
|
|
expect(val).toBe(1)
|
|
|
|
|
ret.num = 10
|
|
|
|
|
expect(val).toBe(10)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
之前讲过在Vue3中,reactive是通过ES6中的Proxy特性实现的属性拦截,所以,在reactive函数中我们直接返回newProxy即可:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
export function reactive(target) {
|
|
|
|
|
if (typeof target!=='object') {
|
|
|
|
|
console.warn(`reactive ${target} 必须是一个对象`);
|
|
|
|
|
return target
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Proxy(target, mutableHandlers);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
可以看到,**下一步我们需要实现的就是Proxy中的处理方法mutableHandles**。
|
|
|
|
|
|
|
|
|
|
这里会把Proxy的代理配置抽离出来单独维护,是因为,其实Vue3中除了reactive还有很多别的函数需要实现,比如只读的响应式数据、浅层代理的响应式数据等,并且reactive中针对ES6的代理也需要单独的处理。
|
|
|
|
|
|
|
|
|
|
这里我们只处理js中对象的代理设置:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
const proxy = new Proxy(target, mutableHandlers)
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### mutableHandles
|
|
|
|
|
|
|
|
|
|
好,看回来,我们剖析mutableHandles。它要做的事就是配置Proxy的拦截函数,这里我们只拦截get和set操作,进入到baseHandlers.js文件中。
|
|
|
|
|
|
|
|
|
|
我们使用createGetter和createSetters来创建set和get函数,mutableHandles就是配置了set和get的对象返回。
|
|
|
|
|
|
|
|
|
|
* get中直接返回读取的数据,这里的Reflect.get和target\[key\]实现的结果是一致的;并且返回值是对象的话,还会嵌套执行reactive,并且调用track函数收集依赖。
|
|
|
|
|
* set中调用trigger函数,执行track收集的依赖。
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
const get = createGetter();
|
|
|
|
|
const set = createSetter();
|
|
|
|
|
|
|
|
|
|
function createGetter(shallow = false) {
|
|
|
|
|
return function get(target, key, receiver) {
|
|
|
|
|
const res = Reflect.get(target, key, receiver)
|
|
|
|
|
track(target, "get", key)
|
|
|
|
|
if (isObject(res)) {
|
|
|
|
|
// 值也是对象的话,需要嵌套调用reactive
|
|
|
|
|
// res就是target[key]
|
|
|
|
|
// 浅层代理,不需要嵌套
|
|
|
|
|
return shallow ? res : reactive(res)
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createSetter() {
|
|
|
|
|
return function set(target, key, value, receiver) {
|
|
|
|
|
const result = Reflect.set(target, key, value, receiver)
|
|
|
|
|
// 在触发 set 的时候进行触发依赖
|
|
|
|
|
trigger(target, "set", key)
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
export const mutableHandles = {
|
|
|
|
|
get,
|
|
|
|
|
set,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
我们先看get的关键部分,track函数是怎么完成依赖收集的。
|
|
|
|
|
|
|
|
|
|
### track
|
|
|
|
|
|
|
|
|
|
具体写代码之前,把依赖收集和执行的原理我们梳理清楚,看下面的示意图:
|
|
|
|
|
|
|
|
|
|
![](https://static001.geekbang.org/resource/image/83/a9/836a798f28824fcf54c0fc280b8afca9.jpg?wh=1945x1500)
|
|
|
|
|
|
|
|
|
|
在track函数中,我们可以使用一个巨大的tragetMap去存储依赖关系。**map的key是我们要代理的target对象,值还是一个depsMap**,存储这每一个key依赖的函数,每一个key都可以依赖多个effect。上面的代码执行完成,depsMap中就有了num1和num2两个依赖。
|
|
|
|
|
|
|
|
|
|
而依赖地图的格式,用代码描述如下:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
targetMap = {
|
|
|
|
|
target: {
|
|
|
|
|
key1: [回调函数1,回调函数2],
|
|
|
|
|
key2: [回调函数3,回调函数4],
|
|
|
|
|
} ,
|
|
|
|
|
target1: {
|
|
|
|
|
key3: [回调函数5]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
好,有了大的设计思路,我们来进行具体的实现,在reactive下新建effect.js。
|
|
|
|
|
|
|
|
|
|
由于target是对象,所以必须得用map才可以把target作为key来管理数据,每次操作之前需要做非空的判断。最终把activeEffect存储在集合之中:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
const targetMap = new WeakMap()
|
|
|
|
|
|
|
|
|
|
export function track(target, type, key) {
|
|
|
|
|
|
|
|
|
|
// console.log(`触发 track -> target: ${target} type:${type} key:${key}`)
|
|
|
|
|
|
|
|
|
|
// 1. 先基于 target 找到对应的 dep
|
|
|
|
|
// 如果是第一次的话,那么就需要初始化
|
|
|
|
|
// {
|
|
|
|
|
// target1: {//depsmap
|
|
|
|
|
// key:[effect1,effect2]
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
let depsMap = targetMap.get(target)
|
|
|
|
|
if (!depsMap) {
|
|
|
|
|
// 初始化 depsMap 的逻辑
|
|
|
|
|
// depsMap = new Map()
|
|
|
|
|
// targetMap.set(target, depsMap)
|
|
|
|
|
// 上面两行可以简写成下面的
|
|
|
|
|
targetMap.set(target, (depsMap = new Map()))
|
|
|
|
|
}
|
|
|
|
|
let deps = depsMap.get(key)
|
|
|
|
|
if (!deps) {
|
|
|
|
|
deps = new Set()
|
|
|
|
|
}
|
|
|
|
|
if (!deps.has(activeEffect) && activeEffect) {
|
|
|
|
|
// 防止重复注册
|
|
|
|
|
deps.add(activeEffect)
|
|
|
|
|
}
|
|
|
|
|
depsMap.set(key, deps)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
get中关键的收集依赖的track函数我们已经讲完了,继续看set中关键的trigger函数。
|
|
|
|
|
|
|
|
|
|
### trigger
|
|
|
|
|
|
|
|
|
|
有了上面targetMap的实现机制,**trigger函数实现的思路就是从targetMap中,根据target和key找到对应的依赖函数集合deps,然后遍历deps执行依赖函数**。
|
|
|
|
|
|
|
|
|
|
看实现的代码:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
export function trigger(target, type, key) {
|
|
|
|
|
// console.log(`触发 trigger -> target: type:${type} key:${key}`)
|
|
|
|
|
// 从targetMap中找到触发的函数,执行他
|
|
|
|
|
const depsMap = targetMap.get(target)
|
|
|
|
|
if (!depsMap) {
|
|
|
|
|
// 没找到依赖
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const deps = depsMap.get(key)
|
|
|
|
|
if (!deps) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
deps.forEach((effectFn) => {
|
|
|
|
|
|
|
|
|
|
if (effectFn.scheduler) {
|
|
|
|
|
effectFn.scheduler()
|
|
|
|
|
} else {
|
|
|
|
|
effectFn()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
可以看到执行的是effect的scheduler或者run函数,这是因为我们需要在effect函数中把依赖函数进行包装,并对依赖函数的执行时机进行控制,这是一个小的设计点。
|
|
|
|
|
|
|
|
|
|
### effect
|
|
|
|
|
|
|
|
|
|
然后我们来实现effect函数。
|
|
|
|
|
|
|
|
|
|
下面的代码中,我们把传递进来的fn函数通过effectFn函数包裹执行,在effectFn函数内部,把函数赋值给全局变量activeEffect;然后执行fn()的时候,就会触发响应式对象的get函数,get函数内部就会把activeEffect存储到依赖地图中,完成依赖的收集:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
export function effect(fn, options = {}) {
|
|
|
|
|
// effect嵌套,通过队列管理
|
|
|
|
|
const effectFn = () => {
|
|
|
|
|
try {
|
|
|
|
|
activeEffect = effectFn
|
|
|
|
|
//fn执行的时候,内部读取响应式数据的时候,就能在get配置里读取到activeEffect
|
|
|
|
|
return fn()
|
|
|
|
|
} finally {
|
|
|
|
|
activeEffect = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!options.lazy) {
|
|
|
|
|
//没有配置lazy 直接执行
|
|
|
|
|
effectFn()
|
|
|
|
|
}
|
|
|
|
|
effectFn.scheduler = options.scheduler // 调度时机 watchEffect回用到
|
|
|
|
|
return effectFn
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
effect传递的函数,比如可以通过传递lazy和scheduler来控制函数执行的时机,默认是同步执行。
|
|
|
|
|
|
|
|
|
|
scheduler存在的意义就是我们可以手动控制函数执行的时机,方便应对一些性能优化的场景,比如数据在一次交互中可能会被修改很多次,我们不想每次修改都重新执行依次effect函数,而是合并最终的状态之后,最后统一修改一次。
|
|
|
|
|
|
|
|
|
|
scheduler怎么用你可以看下面的代码,我们使用数组管理传递的执行任务,最后使用Promise.resolve只执行最后一次,这也是Vue中watchEffect函数的大致原理。
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
const obj = reactive({ count: 1 })
|
|
|
|
|
effect(() => {
|
|
|
|
|
console.log(obj.count)
|
|
|
|
|
}, {
|
|
|
|
|
// 指定调度器为 queueJob
|
|
|
|
|
scheduler: queueJob
|
|
|
|
|
})
|
|
|
|
|
// 调度器实现
|
|
|
|
|
const queue: Function[] = []
|
|
|
|
|
let isFlushing = false
|
|
|
|
|
function queueJob(job: () => void) {
|
|
|
|
|
if (!isFlushing) {
|
|
|
|
|
isFlushing = true
|
|
|
|
|
Promise.resolve().then(() => {
|
|
|
|
|
let fn
|
|
|
|
|
while(fn = queue.shift()) {
|
|
|
|
|
fn()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
好了,绕了这么一大圈终于执行完了函数,估计你也看出来了封装了很多层。
|
|
|
|
|
|
|
|
|
|
**之所以封装这么多层就是因为,Vue的响应式本身有很多的横向扩展**,除了响应式的封装,还有只读的拦截、浅层数据的拦截等等,这样,响应式系统本身也变得更加灵活和易于扩展,我们自己在设计公用函数的时候也可以借鉴类似的思路。
|
|
|
|
|
|
|
|
|
|
## 另一个选择ref函数
|
|
|
|
|
|
|
|
|
|
有了track和trigger的逻辑之后,我们用ref函数实现就变得非常简单了。
|
|
|
|
|
|
|
|
|
|
ref的执行逻辑要比reactive要简单一些,不需要使用Proxy代理语法,直接使用对象语法的getter和setter配置,监听value属性即可。
|
|
|
|
|
|
|
|
|
|
看下面的实现,在ref函数返回的对象中,对象的get value方法,使用track函数去收集依赖,set value方法中使用trigger函数去触发函数的执行。
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
export function ref(val) {
|
|
|
|
|
if (isRef(val)) {
|
|
|
|
|
return val
|
|
|
|
|
}
|
|
|
|
|
return new RefImpl(val)
|
|
|
|
|
}
|
|
|
|
|
export function isRef(val) {
|
|
|
|
|
return !!(val && val.__isRef)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ref就是利用面向对象的getter和setters进行track和trigget
|
|
|
|
|
class RefImpl {
|
|
|
|
|
constructor(val) {
|
|
|
|
|
this.__isRef = true
|
|
|
|
|
this._val = convert(val)
|
|
|
|
|
}
|
|
|
|
|
get value() {
|
|
|
|
|
track(this, 'value')
|
|
|
|
|
return this._val
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set value(val) {
|
|
|
|
|
if (val !== this._val) {
|
|
|
|
|
this._val = convert(val)
|
|
|
|
|
trigger(this, 'value')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ref也可以支持复杂数据结构
|
|
|
|
|
function convert(val) {
|
|
|
|
|
return isObject(val) ? reactive(val) : val
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
你能很直观地看到,ref函数实现的相对简单很多,只是利用面向对象的getter和setter拦截了value属性的读写,这也是为什么我们需要操作ref对象的value属性的原因。
|
|
|
|
|
|
|
|
|
|
**值得一提的是,ref也可以包裹复杂的数据结构,内部会直接调用reactive来实现**,这也解决了大部分同学对ref和reactive使用时机的疑惑,现在你可以全部都用ref函数,ref内部会帮你调用reactive。
|
|
|
|
|
|
|
|
|
|
## computed
|
|
|
|
|
|
|
|
|
|
Vue中的computed计算属性也是一种特殊的effect函数,我们可以新建computed.spec.js来测试computed函数的功能,**computed可以传递一个函数或者对象,实现计算属性的读取和修改**。比如说可以这么用:
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
mport { ref } from '../ref'
|
|
|
|
|
import { reactive } from '../reactive'
|
|
|
|
|
import { computed } from '../computed'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
describe('computed测试',()=>{
|
|
|
|
|
it('computed基本使用',()=>{
|
|
|
|
|
const ret = reactive({ count: 1 })
|
|
|
|
|
const num = ref(2)
|
|
|
|
|
const sum = computed(() => num.value + ret.count)
|
|
|
|
|
expect(sum.value).toBe(3)
|
|
|
|
|
|
|
|
|
|
ret.count++
|
|
|
|
|
expect(sum.value).toBe(4)
|
|
|
|
|
num.value = 10
|
|
|
|
|
expect(sum.value).toBe(12)
|
|
|
|
|
})
|
|
|
|
|
it('computed属性修改',()=>{
|
|
|
|
|
const author = ref('大圣')
|
|
|
|
|
const course = ref('玩转Vue3')
|
|
|
|
|
const title = computed({
|
|
|
|
|
get(){
|
|
|
|
|
return author.value+":"+course.value
|
|
|
|
|
},
|
|
|
|
|
set(val){
|
|
|
|
|
[author.value,course.value] = val.split(':')
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
expect(title.value).toBe('大圣:玩转Vue3')
|
|
|
|
|
|
|
|
|
|
author.value="winter"
|
|
|
|
|
course.value="重学前端"
|
|
|
|
|
expect(title.value).toBe('winter:重学前端')
|
|
|
|
|
//计算属性赋值
|
|
|
|
|
title.value = '王争:数据结构与算法之美'
|
|
|
|
|
expect(author.value).toBe('王争')
|
|
|
|
|
expect(course.value).toBe('数据结构与算法之美')
|
|
|
|
|
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
怎么实现呢?我们新建computed函数,看下面的代码,我们拦截computed的value属性,并且定制了effect的lazy和scheduler配置,computed注册的函数就不会直接执行,而是要通过scheduler函数中对\_dirty属性决定是否执行。
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
export function computed(getterOrOptions) {
|
|
|
|
|
// getterOrOptions可以是函数,也可以是一个对象,支持get和set
|
|
|
|
|
// 还记得清单应用里的全选checkbox就是一个对象配置的computed
|
|
|
|
|
let getter, setter
|
|
|
|
|
if (typeof getterOrOptions === 'function') {
|
|
|
|
|
getter = getterOrOptions
|
|
|
|
|
setter = () => {
|
|
|
|
|
console.warn('计算属性不能修改')
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
getter = getterOrOptions.get
|
|
|
|
|
setter = getterOrOptions.set
|
|
|
|
|
}
|
|
|
|
|
return new ComputedRefImpl(getter, setter)
|
|
|
|
|
}
|
|
|
|
|
class ComputedRefImpl {
|
|
|
|
|
constructor(getter, setter) {
|
|
|
|
|
this._setter = setter
|
|
|
|
|
this._val = undefined
|
|
|
|
|
this._dirty = true
|
|
|
|
|
// computed就是一个特殊的effect,设置lazy和执行时机
|
|
|
|
|
this.effect = effect(getter, {
|
|
|
|
|
lazy: true,
|
|
|
|
|
scheduler: () => {
|
|
|
|
|
if (!this._dirty) {
|
|
|
|
|
this._dirty = true
|
|
|
|
|
trigger(this, 'value')
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
get value() {
|
|
|
|
|
track(this, 'value')
|
|
|
|
|
if (this._dirty) {
|
|
|
|
|
this._dirty = false
|
|
|
|
|
this._val = this.effect()
|
|
|
|
|
}
|
|
|
|
|
return this._val
|
|
|
|
|
}
|
|
|
|
|
set value(val) {
|
|
|
|
|
this._setter(val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
|
|
|
|
最后我们来回顾一下今天学到的内容。通过手写迷你的响应式原型,我们学习了Vue中响应式的地位和架构。
|
|
|
|
|
|
|
|
|
|
响应式的主要功能就是可以把普通的JavaScript对象封装成为响应式对象,**在读取数据的时候通过track收集函数的依赖关系,把整个对象和effect注册函数的依赖关系全部存储在一个依赖图中**。
|
|
|
|
|
|
|
|
|
|
定义的dependsMap是一个巨大的Map数据,effect函数内部读取的数据都会存储在dependsMap中,数据在修改的时候,通过查询dependsMap,获得需要执行的函数,再去执行即可。
|
|
|
|
|
|
|
|
|
|
dependsMap中存储的也不是直接存储effect中传递的函数,而是包装了一层对象对这个函数的执行实际进行管理,内部可以通过active管理执行状态,还可以通过全局变量shouldTrack控制监听状态,并且执行的方式也是判断scheduler和run方法,实现了对性能的提升。
|
|
|
|
|
|
|
|
|
|
我们在日常项目开发中也可以**借鉴响应式的处理思路,使用通知的机制,来调用具体数据的操作和更新逻辑**,灵活使用effect、ref、reactive等函数把常见的操作全部变成响应式数据处理,会极大的提高我们开发的体验和效率。
|
|
|
|
|
|
|
|
|
|
## 思考题
|
|
|
|
|
|
|
|
|
|
最后留一个思考题,Vue3.2对响应式有一个性能的进一步提升,你都了解到有哪些呢?欢迎你在评论区分享自己的思考,我们下一讲再见。
|
|
|
|
|
|