前言
vue 的响应式是 数据更新视图,也就是当数据变更时,vue 会自动帮我们更新页面,我们无需手动操作 dom
这么理解当然是对的,但是探究 vue 的响应式之根本就不单单是 数据更新视图,而应该是 数据更新函数
数据更新视图,无非就是 ref/reactive
后的数据变更,然后触发了 render 函数的重新执行,最后才是我们看到的 视图 更新
因此数据更新视图,这个函数仅仅是 render,我们不妨跳出 render,来到函数这一层,这样也方便我们实现
我们继续来看响应式,如何理解数据和函数之间的关联
我们看 vue 的响应式就是 数据变更了,用过这些数据的函数能够一起执行
因此这个关联体现在
- 函数要能够监听数据的 读取 以及 修改
- 以及该数据对应了哪些函数
弄清了这两个要点,我们就有实现的方向了
实现拦截
第一点我们可以给一个最小实现 demo,也就是实现监听对象
实现监听对象我们有两个方法,一个是 es5 的 Object.defineProperty
,另一个是 es6 的 Proxy
,我们不妨回顾下二者的区别
在用法上,Object.defineProperty
需要拿到对象以及对应的 key,对 key 添加 get,set 属性,比如下面

a 属性在 definedProperty
后得到了 get,set 属性,这样 a 属性就获得了一个监听
而 proxy 则是对整个对象添加 handlers
,这个 handlers
里面总共有 13 种拦截属性,不仅仅是 get,set

单单从这里就可以看出 Object.defineProperty
的局限性在于必须清楚对象的属性名,动态新增/删除无法感知,而 Proxy
则是通过代理整个对象,对所有操作进行拦截,不仅仅是get,set,Object.defineProperty
要想代理对象所有属性还得遍历挨个实现,proxy
则是天然支持,无需遍历
要说 Object.defineProperty
相较于 Proxy
的优点,也就只有 兼容可谈,不过目前基本上浏览器都兼容 proxy
,可能只有 IE 不行
接下来就用 proxy
来实现一个demo,我们尽量参考 vue/reactivity
的模块来写
首先,我们期望有个 reactive
能够把数据变成响应式数据,也就是可以进行监听
js
import { reactive } from './reactive.js'
const obj = {
a: 1,
b: 2,
}
const state = reactive(obj)
function fn () {
state.a;
}
fn()
这里 fn 执行,会读取到 state.a,那么应该会触发 get
因此在 reactive 里面实现一个 proxy 即可,proxy 的第二个参数是 handlers,考虑到延展,我们用单独一个文件 baseHandlers.js
里面存放 目前 有的 get,set
js
import { baseHandlers } from './baseHandlers.js'
export function reactive (target) {
return new Proxy(target, baseHandlers)
}
baseHandlers.js
js
function get (target, key, value) {
console.log('get', target, key);
if (target[key]) {
return target[key]
}
}
function set (target, key, value) {
console.log('set', target, key, value);
if (target[key] === value) return
target[key] = value
}
export const baseHandlers = {
get,
set,
}
我们把 fn 改成修改值 state.a = 2
,再来看
不出意外就会报错了

这是因为 proxy 的 set 操作返回一个 布尔,我们当然可以在结尾新增一个 return true
,用 try catch
兜住错误。但是 es6 新增了一个 Reflect
对象,身上的 set 属性天然支持返回 true,因此我们 get,set 通通换成 Reflect
来做
那个 log 后续的逻辑其实就是依赖收集和派发更新,我们分别用函数 track
和 trigger
代替
js
export function track (target, key) {
console.log('依赖收集', target, key);
}
export function trigger (target, key) {
console.log('依赖触发', target, key);
}
然后我们的 handlers 就用 Reflect
去 set 和 get
js
function get (target, key) {
track(target, key)
return Reflect.get(target, key)
}
function set (target, key, value) {
trigger(target, key)
return Reflect.set(target, key, value)
}
至此,一个简单的响应式读取值和修改值的监听就实现了
后续的实现就是第二点,如何让一个数据能够收集到对应的使用过这些数据的函数
但是目前实现第二点还有点距离需要爬,我们先看下有些特殊情况,可能读写不会被监听上
in 的读取 --- has
我们来看一个 in 操作符
js
import { reactive } from './reactive.js'
const obj = {
a: 1,
b: 2,
}
const state = reactive(obj)
function fn () {
if ('a' in state) {
console.log('aaaa');
}
}
fn()
'a' in state
这里会读取 a ,按道理应该可以触发 track 依赖收集,但是并没有
而 in 这个操作符其实本质上调用的是 hasProperty
,proxy 里面拦截 hasProperty
的操作属性为 has
因此我们需要往 baseHandlers
中加一个 has
js
function has (target, key, value) {
track(target, key)
return Reflect.has(target, key, value)
}
这样一来就能拦截 'a' in state 了
for in 的读取 -- ownKeys
我们再来看个 for in 的例子
js
const obj = {
a: 1,
b: 2,
}
const state = reactive(obj)
function fn () {
for (const key in state) {
}
}
fn()
这里按道理预期也是 会触发 track
,因为我把这个响应式数据的每个 key 都读取了一遍
这个 for in 其实是需要对象有 iterate 属性,而这个刚好对应着 Reflect
的 ownKeys
属性,不过这个迭代是不需要 key 的,因此这里的 key 就不用传给 track 了
js
function ownKeys (target) {
track(target)
return Reflect.ownKeys(target)
}
ownKeys 和 属性无关,因此没有 key value
操作类型与拦截类型
其实到这里你肯定会发现规律,那就是可能还会有很多操作类型,不仅仅是 get,set,has,ownKeys,还有 add,delete 等,考虑得越全面这些东西就越多,而这些属性其实刚好可以分为 操作类型 和 拦截类型,
后续在 track / trigger
中,可以根据这些类型去优化,比如 set 能够影响到的 只有 get,而 add 能够影响的就多了,有 get,has,ownKeys
我们先给出一个类型文件,里面存放 trackOpTypes
以及 triggerOpTypes
js
export const trackOpTypes = {
GET: "get",
HAS: "has",
ITERATE: "iterate",
};
export const triggerOpTypes = {
SET: "set",
ADD: "add",
DELETE: "delete",
};
刚刚聊到的 for in,其实对应的 track 类型为 iterate,后续还能发现有新的类型就再补全
receiver
我们再来看一个🌰
js
import { reactive } from './reactive.js'
const obj = {
a: 1,
b: 2,
get c () {
return this.a + this.b
}
}
const state = reactive(obj)
function fn () {
state.c;
}
fn()
当我们访问 c 属性时,c 会返回 a 和 b,按道理会触发 三次 get,对应 a,b,c
但是这里实际上只有 c
我们可以在 c 里面添加一个 log ,看看 this 是啥,结果你会发现就是 obj,这当然符合预期,但是要想 a 和 b 也能被 get 到,是不是得希望 this 是 Proxy 后的 obj
实际上 state.c 就是 get 里面的 target[key],而 [[get]]
其实调用的是 [key, receiver]
,默认语法上,get 第二个参数就是 this 指向的对象,这里对应的就是 obj,我们无法更改
但是这个却可以通过 Reflect 解决,Reflect 支持更改 this,我们可以在 get 中多加一个 入参 receiver,也就是 Reflect.get(target, key, receiver)
js
function get (target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
}
深度监听
我们再来看一个🌰
JS
import { reactive } from './reactive.js'
const obj = {
a: 1,
b: 2,
c: {
d: 3
}
}
const state = reactive(obj)
function fn () {
state.c.d;
}
fn()
这里读取 state.c.d ,但道理 get 应该监听到 c 和 d,但是实际上只有 c,其实也符合预期,因为 state.c 返回的 对象是 { d }
,而非 Proxy 后的对象,因此在 get 中我们不用着急返回 Reflect.get
,我们可以先判断其返回值是否为 对象,是就用 reactive 再包裹一层
js
function get (target, key, receiver) {
track(target, key)
const res = Reflect.get(target, key, receiver)
if (isObject(res)) {
return reactive(res)
} else {
return res
}
}
add & delete
add 以及 delete 都是派发更新,我们现在来补充下这两个函数
先看 add,看这个🌰
js
import { reactive } from './reactive.js'
const obj = {
a: 1,
b: 2,
c: {
d: 3
}
}
const state = reactive(obj)
function fn () {
state.e = 4
}
fn()
我们希望 trigger
的时候能看到 add 信息
add 其实就是 set,当 key 存在就是 set,不存在就是 add
因此add我们可以在 set 里面补充,另外,我们可以把 track 和 trigger 的 log 添加 type 信息
js
export function track (target, key, type) {
console.log('依赖收集', target, key, type);
}
export function trigger (target, key, type) {
console.log('依赖触发', target, key, type);
}
我们在 set 中区分出 add
vbnet
function set (target, key, value, receiver) {
const type = target.hasOwnProperty(key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD
trigger(target, key, type)
return Reflect.set(target, key, value, receiver)
}
其实 set 还有处可以优化,当 set 原来的值就不用 trigger 了
也就是补充 if (target[key] === value) return true
再来看 delete,我们希望下面这个例子能看到 delete 信息
js
const obj = {
a: 1,
b: 2,
c: {
d: 3
}
}
const state = reactive(obj)
function fn () {
delete state.e
}
fn()
那就得新增一个 deleteProperty
的 handlers
js
function deleteProperty (target, key) {
if (!target.hasOwnProperty(key)) return true
trigger(target, key, TriggerOpTypes.DELETE)
return Reflect.deleteProperty(target, key)
}
前面判断是因为若本身就没有这个 key,那就不用 trigger 了
数组的拦截
includes 为例
你或多或少听说过 vue 有重写数组的方法,这就是因为数组的有些修改监听不上,下面就一一来举例说明
首先看 includes
对象时的情况
js
import { reactive } from './reactive.js'
const obj = {}
const arr = [1, obj, 2]
const state = reactive(arr)
function fn () {
let index = state.includes(obj)
console.log(index)
}
fn()
state 是 Proxy(arr)
,它调用 includes 就会访问 includes 属性,我们可以看看会输出什么
js
依赖收集 (3) [1, {...}, 2] includes get
依赖收集 (3) [1, {...}, 2] length get
依赖收集 (3) [1, {...}, 2] 0 get
依赖收集 (3) [1, {...}, 2] 1 get
依赖收集 (3) [1, {...}, 2] 2 get
false
includes 内部实现必然会访问到 includes ,访问 length 其实也好理解,要我们自己手写一个 includes,不就是一个 for(let i = 0; i < arr.length; i++)
,然后找不到就返回 false 或者说 -1,这里很奇怪,includes 遍历了所有项都找不到 obj,最终返回了 false
我们若 includes(1) 那便输出如下
js
依赖收集 (3) [1, {...}, 2] includes get
依赖收集 (3) [1, {...}, 2] length get
依赖收集 (3) [1, {...}, 2] 0 get
true
这是符合预期的,为啥 obj 就找不到
其实细想下也好理解,因为state 调用 includes 时,state 可是 Proxy
,之前我们已经做了深度监听,也就是说 state 里面的 obj 会被再次 reactive 一次,proxy
里面找 原生的 obj,这个 obj 并非 proxy
,因此可以理解为 proxy !== raw
导致的 false
但是我们肯定不希望把 深度监听去掉,那就灵活点,当 includes 找不到时,我们再次处理下,能否让原生的 arr 去调用 includes
在 get 中我们有三个入参,分别为 原生的 target,key,以及 proxy 后的 receiver
若 key 为 includes 那就特殊处理,我们把处理过的 includes 给到 proxy
在 includes 内部,找不到时我们给一个唯一属性,让 proxy 访问这个唯一属性时,再次触发 get,此时原生 arr,也就是 target 返回出去,这样就实现了 数组特定方法调用时,避免了深度监听
具体实现如下,indexOf
,lastIndexOf
同理
js
const arrayInstrumentations = {}
const RAW = Symbol('raw');
['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
const originMethod = Array.prototype[method]
arrayInstrumentations[method] = function (...args) {
let res = originMethod.apply(this, args)
if (res === false || res === -1) {
res = originMethod.apply(this[RAW], args)
}
return res
}
})
function get (target, key, receiver) {
if (key === RAW) return target
track(target, key, TrackOpTypes.GET)
const res = Reflect.get(target, key, receiver)
if (Array.isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
return arrayInstrumentations[key].bind(receiver)
}
return isObject(res) ? reactive(res) : res;
}
length
我们再来看个栗子
js
import { reactive } from './reactive.js'
const obj = {}
const arr = [1, obj, 2]
const state = reactive(arr)
function fn () {
state[4] = 2
}
fn()
这里通过下标的形式新增了一个 item,那必然会 tirgger 一次 add
但是这样数组的 length 也变了,按道理 length 也要一次 set ,但是实际上目前并没有 length 的 trigger
其实 length 的变化就相当于用 Object.defineProperty()
,这种变化是 trigger 不到的
既然如此,我们就在这种情况下手动 trigger 下 length
这种情况就是数组 add,判断两下就好了,具体实现直接看下面代码
js
function set (target, key, value, receiver) {
const type = target.hasOwnProperty(key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD
const oldValue = target[key]
const oldLen = Array.isArray(target) ? target.length : null
const res = Reflect.set(target, key, value, receiver)
const newLen = Array.isArray(target) ? target.length : null
if (!Object.is(oldValue, value) || type === TriggerOpTypes.ADD) {
trigger(target, key, type)
if (Array.isArray(target) && oldLen !== newLen && key !== 'length') {
trigger(target, 'length', TriggerOpTypes.SET)
}
}
return res
}
通过下标新增 item,length 的写会被监听不上,若通过 length 删减 arr,item 的删除会被监听上吗,实际上并不会
js
import { reactive } from './reactive.js'
const obj = {}
const arr = [1, obj, 2]
const state = reactive(arr)
function fn () {
state.length = 2
}
fn()
目前这个情况只能 trigger
到 length
既然失真了,我们同样手动处理,刚才的实现我们是 key !== length
现在则是 key === length
,那么就是从 oldLen 到 newLen 的区别去 trigger 下标就行,并且类型给一个 delete
js
function set (target, key, value, receiver) {
const type = target.hasOwnProperty(key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD
const oldValue = target[key]
const oldLen = Array.isArray(target) ? target.length : null
const res = Reflect.set(target, key, value, receiver)
const newLen = Array.isArray(target) ? target.length : null
if (!Object.is(oldValue, value) || type === TriggerOpTypes.ADD) {
trigger(target, key, type)
if (Array.isArray(target) && oldLen !== newLen) {
if (key !== 'length') {
trigger(target, 'length', TriggerOpTypes.SET)
} else {
for (let i = newLen; i < oldLen; i++) {
trigger(target, i, TriggerOpTypes.DELETE)
}
}
}
}
return res
}
push
我们再来看一个 push 案例
js
import { reactive } from './reactive.js'
const obj = {}
const arr = [1, obj, 2]
const state = reactive(arr)
function fn () {
state.push(3)
}
fn()
这里的 log 如下
js
依赖收集 (3) [1, {...}, 2] push get
依赖收集 (3) [1, {...}, 2] length get
依赖触发 (4) [1, {...}, 2, 3] 3 add
依赖触发 (4) [1, {...}, 2, 3] length set
看着并没有问题,最后的 length set 也是对的,但是当我们多次重复调用 fn 时,你就会觉得 length 每次 push 一次 length 都会被 track 依赖收集一次显得重复,因此我们需要避免重复触发 length 相关依赖
那就对 push 这类 方法做点手脚,当 push 时,内部就会有 length 的 key,此时暂停 track,等调用完再恢复 track
js
["push", "pop", "shift", "unshift", "splice"].forEach((method) => {
arrayInstrumentations[method] = function (...args) {
pauseTrack();
const res = Array.prototype[method].apply(this, args);
resumeTrack();
return res;
};
});
pauseTrack
和 resumeTrack
也很容易实现
js
let shouldTrack = true;
export function track (target, key, type) {
if (!shouldTrack) return
console.log('依赖收集', target, key, type);
}
export function trigger (target, key, type) {
console.log('依赖触发', target, key, type);
}
export function pauseTrack () {
shouldTrack = false
}
export function resumeTrack () {
shouldTrack = true
}
track 和 trigger
现在开始实现 track 和 trigger
目前的实现仅仅是拿到 target, key, type 信息,但是具体的功能并没有实现
比如 track 依赖收集,他需要收集对应的函数,trigger 则是把这些收集到的函数重新触发执行
另外我们又要继续探讨一个问题,是否所有用到的响应式数据的函数都要收集进来呢
js
render1() {
render2() {
.....
}
}
比如上面这个情景,父组件里面包含一个子组件,这在实际 vue 项目中是个非常常见的情景,render2 里面的响应式数据若发生变更,render2 组件就会重新渲染,但是 render1 不会,若 render2 里面的响应式数据被 render1 用到了,数据变了 render1 就会重新渲染
要想实现到这个精度,那肯定不是一股脑把所有用到响应式的数据全部收集起来,既然是人为控制,那么我们可以给一个 effect 函数,你若希望 fn 能够被收集进来,那么将 fn 传入进去即可,state(fn)
js
const obj = {
a: 1
}
const state = reactive(obj)
function fn1 () {
function fn2 () {
state.a;
}
fn2()
}
effect(fn1)
比如这里,希望 fn1 被 track 拿到,但是 fn1 是函数 effect 的入参,那就在 effect.js 文件中把 fn 保存到全局中去让 track 拿到
js
let shouldTrack = true;
let activeEffect = undefined;
export function effect (fn) {
activeEffect = fn
fn()
activeEffect = null
}
export function track (target, key, type) {
if (!shouldTrack || !activeEffect) return
console.log('依赖收集', target, key, type, activeEffect);
}
收集依赖时其实需要函数运行才能确定,而非编译阶段,举一个直观的栗子
js
const obj = {
a: 2
}
const state = reactive(obj)
function fn1 () {
if (state.a === 1) {
state.b
} else {
state.c
}
}
effect(fn1)
fn1 执行过程中才能确定到底是依赖 a,b 还是 a,c
而运行时就肯定会产生嵌套的情况,嵌套就会每层作用域有对应的 activeEffect
,因此我们收集函数不应该仅仅只是收集 fn,而是把 effect 里面的内容都收集进来
js
export function effect (fn) {
const effectFn = () => {
try {
activeEffect = fn
return fn()
} finally {
activeEffect = null
}
}
effectFn()
}
这里用 try catch 处理是因为用户写的函数可能会有报错的情况
现在开始用 map 去把一个 target 的对应的函数 去串起来
一个 target 里面会有多个 key,一个 key 又可以有多个 操作类型,操作类型后才对应着 effectFn
除了 effectFn
可以用 set 去个重,其余都可以用 map 表示,另外这里有个 iterate 的操作类型是没有 key 的,因此我们需要手动给 iterate 加一个 key
js
const targetMap = new WeakMap()
const ITERATE_KEY = Symbol('iterate')
export function track (target, key, type) {
if (!shouldTrack || !activeEffect) return
let propMap = targetMap.get(target)
if (!propMap) {
propMap = new Map()
targetMap.set(target, propMap)
}
if (key === TrackOpTypes.ITERATE) {
key = ITERATE_KEY;
}
let typeMap = propMap.get(key)
if (!typeMap) {
typeMap = new Map()
propMap.set(key, typeMap)
}
let depSet = typeMap.get(type)
if (!depSet) {
depSet = new Set()
typeMap.set(type, depSet)
}
if (!depSet.has(activeEffect)) {
depSet.add(activeEffect)
activeEffect.deps.push(depSet)
}
console.log('targetMap', targetMap);
}
track 基本上把 effectFns 收集到 targetMap
中了,现在 trigger 的目的则是用 target,key,type 去寻找对应的 effectFns
,然后触发他们的执行
寻找 effectFns
我们单独给一个函数 getEffectFns
来做这件事,其实寻找对应的 effectFns
主要是需要注意 操作类型 之间的对应关系,比如 trigger 时,我们用了set 类型,但是 map 里面的只有 get ,因此我们需要处理每个 TriggerOpType
对应的所有 TrackOpType
,然后挨个遍历,有就输出 effectFn 即可
我们先把 操作类型 之间的对应关系 map 给补上
js
const triggerTypeMaps = {
[TriggerOpTypes.SET]: [TrackOpTypes.GET],
[TriggerOpTypes.ADD]: [TrackOpTypes.ITERATE, TrackOpTypes.GET, TrackOpTypes.HAS],
[TriggerOpTypes.DELETE]: [TrackOpTypes.ITERATE, TrackOpTypes.GET, TrackOpTypes.HAS],
}
然后就是 trigger
JS
export function trigger (target, key, type) {
const effects = getEffectFns(target, key, type)
effects.forEach(effectFn => effectFn())
}
现在实现 getEffectFns
js
function getEffectFns (target, key, type) {
const propMap = targetMap.get(target)
if (!propMap) return []
const keys = [key]
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
keys.push(ITERATE_KEY)
}
const effects = new Set()
for (const key of keys) {
const typeMap = propMap.get(key)
if (!typeMap) continue
const types = triggerTypeMaps[type]
for (const type of types) {
const depSet = typeMap.get(type)
if (!depSet) continue
depSet.forEach(effectFn => effects.add(effectFn))
}
}
return [...effects]
}
我们再来看一个栗子
js
const obj = { a: 1, b: 2, c: 3 };
const state = reactive(obj);
function fn1() {
console.log("fn1 执行");
if (state.a === 1) {
state.b;
} else {
state.c;
}
}
effect(fn1);
state.a = 2;
state.b = 3;
这里会触发三次执行,看似没毛病,实际上只应该触发两次,因为 state.a 被赋值为 2 时,函数的依赖项就和 state.b 无关了
因此我们要实现的就是当 effectFn 执行时重新把 effectFn 里面的依赖项给清空便是
js
export function effect (fn) {
const effectFn = () => {
try {
activeEffect = effectFn
cleanup(effectFn)
return fn()
} finally {
activeEffect = null
}
}
effectFn.deps = []
effectFn()
}
function cleanup (effectFn) {
const { deps } = effectFn
if (!deps.length) return
deps.forEach(dep => dep.delete(effectFn))
effectFn.deps.length = 0
}
当然,这需要我们在 track 中给 activeEffect 加一个 deps 属性,把 depSet
挂上去
我们再来看一个例子
js
const obj = { a: 1, b: 2, c: 3 };
const state = reactive(obj);
function fn1() {
console.log("fn1 执行");
effect(() => {
console.log("fn1 inner");
state.a;
});
state.b;
}
effect(fn1);
state.b = 3;
这里按道理会执行两次 fn1 和 fn1 inner,但是实际上只执行了一次
这是因为在执行 inner 时,activeEffect 已经被置为 null 了,而此时 state.b 还没来得及收集,因此 b 的修改没被监听上
之前的 activeEffect
就不能直接 置为 null,这由于调用栈的关系我们只需要取当前栈的栈顶
js
let effectStack = []
export function effect (fn) {
const effectFn = () => {
try {
activeEffect = effectFn
effectStack.push(effectFn)
cleanup(effectFn)
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
effectFn.deps = []
effectFn()
}
我们再来看个 bug
js
let state = reactive({
a: 1,
b: 2,
c: 3,
});
function fn1() {
console.log("fn1 执行");
state.a++;
}
effect(fn1);
目前这么运行会出现爆栈的问题,这是因为 state.a++
会先 get 后 set,get 时记录了当前 fn,set 时重新执行 fn,重新执行时之前的逻辑是会重新收集依赖,这就导致了无限递归
解决方案那就在 trigger
时,判断函数是不是当前的 activeEffect
,若是则不执行 effectFn
js
export function trigger (target, key, type) {
const effects = getEffectFns(target, key, type)
for (const effectFn of effects) {
if (effectFn === activeEffect) {
continue
}
effectFn()
}
}
包括我们可以实现一个 lazy 的 effect 函数
js
let state = reactive({
a: 1,
b: 2,
c: 3,
});
function fn1() {
console.log("fn1 执行");
state.a++;
}
let fn = effect(fn1, {lazy: true});
fn()
lazy 时,就返回函数,而不是执行,非 lazy 就是正常执行
js
export function effect (fn, options = {}) {
const { lazy } = options
const effectFn = () => {
try {
activeEffect = effectFn
effectStack.push(effectFn)
cleanup(effectFn)
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
effectFn.deps = []
if (!lazy) {
effectFn()
}
return effectFn
}
包括多次更改,渲染一次的调度器
调度器需要存到 effectFn.options
上,给到 trigger 去执行
js
export function effect (fn, options = {}) {
const { lazy } = options
const effectFn = () => {
try {
activeEffect = effectFn
effectStack.push(effectFn)
cleanup(effectFn)
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
effectFn.deps = []
effectFn.options = options
if (!lazy) {
effectFn()
}
return effectFn
}
export function trigger (target, key, type) {
const effects = getEffectFns(target, key, type)
for (const effectFn of effects) {
if (effectFn === activeEffect) {
continue
}
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
}
}
实现 ref
现在实现 ref,有了 track ,trigger,基本上 ref 可以很轻松实现
ref 其实就是利用 对象 get set 属性,get value 就是依赖收集,调用 track
,返回 value,set value 就是派发更新,调用 trigger
js
import { track, trigger } from './effect.js'
import { TrackOpTypes, TriggerOpTypes } from './constants.js'
import { reactive } from './reactive.js'
import { isObject } from './utils.js'
export function ref (value) {
return {
get value () {
track(this, TrackOpTypes.GET, 'value')
return value
},
set value (v) {
if (v !== value) {
value = v
trigger(this, 'value', TriggerOpTypes.SET)
}
}
}
}
当然 ref 也要支持 对象,对象就需要调用 reactive 了,另外还需要 reactive 支持 value
实现 computed
computed
其实就是一个仅访问的 effect 函数,只不过里面支持两种写法,一个是写的 getter,setter,一个就是直接写的函数,里面返回的响应式数据
既然支持两种入参,我们可以在 computed
里面先把参数归一化,统一弄成 getter,setter 的形式,但是一般来说我们用函数的形式,函数就是 getter,setter 默认给一个初始值不作用就行
js
import { effect } from './effect.js'
import { track, trigger } from './effect.js'
import { TrackOpTypes, TriggerOpTypes } from './constants.js'
function normalizeOptions (getterOptions) {
let getter, setter
if (typeof getterOptions === 'function') {
getter = getterOptions
setter = () => {
console.warn('Write operation failed: computed value is readonly')
}
} else {
getter = getterOptions.get
setter = getterOptions.set
}
return { getter, setter }
}
export function computed (getterOptions) {
const { getter, setter } = normalizeOptions(getterOptions)
const effectFn = effect(getter, {
lazy: true,
scheduler () {
dirty = true
trigger(obj, 'value', TriggerOpTypes.SET)
}
})
let value;
let dirty = true;
const obj = {
get value () {
track(obj, 'value', TrackOpTypes.GET)
if (dirty) {
value = effectFn()
dirty = false
}
return value
},
set value (v) {
setter(v)
}
}
return obj
}
实现 watchEffect
watchEffect
会自动追踪所有响应式依赖,依赖项发生变化会自动执行传入的 fn
watchEffect
肯定是 lazy false,因为函数在组件加载时就会开始执行一次
js
export function watchEffect (fn) {
const effectFn = effect(fn, {
lazy: false,
scheduler (effectFn) {
effectFn()
}
})
return () => {
effectFn.deps.forEach(dep => dep.delete(effectFn))
effectFn.deps.length = 0
}
}
实现 watch
watch
会接受三个参数,分别为 source,callback,options,source 就是响应式数据,callback 其实就是 getter 函数 ,options 支持 deep,lazy 等,其实 reactive 默认就是 deep
watch
的 deep 指的是是否要把对象的所有内部属性纳入观察依赖中,默认 false
实现 deep 则是通过 traverse
去深度遍历 get 所有属性
js
export function watch (source, callback, options = {}) {
const { immediate = false, deep = false } = options
let getter
let oldValue
if (typeof source === 'function') {
getter = deep ? () => traverse(source()) : source
} else if (isObject(source)) {
getter = () => traverse(source)
} else {
console.warn('watch source must be a function or an object')
return () => {}
}
const effectFn = effect(getter, {
lazy: true,
scheduler (effectFn) {
const newValue = effectFn()
callback(newValue, oldValue)
oldValue = newValue
}
})
if (immediate) {
const newValue = effectFn()
callback(newValue, undefined)
oldValue = newValue
} else {
oldValue = effectFn()
}
return () => {
effectFn.deps.forEach(dep => dep.delete(effectFn))
effectFn.deps.length = 0
}
}
function traverse (value, seen = new Set()) {
if (!isObject(value) || seen.has(value)) {
return value
}
seen.add(value)
for (const key in value) {
traverse(value[key], seen)
}
return value
}
最后
v3 响应式就是通过 proxy 代理实现的,proxy 天生支持多种 handlers ,我们要做的无非就是在这个基础上进行封装,而数组对于我们的系统来说,很多方法内部会读取 length 或逐个索引可能不符合我们的预期,这才需要进行稍微修改,优先在 proxy 身上找,找不到就再在 this[RAW]
原始数组上找
而 track 依赖收集就是帮我们把数据和对应的 key 之间所有的关系给 weakmap 起来,target -> Map(key) -> Map(type) -> Set(effectFn)
, 这样我们 trigger 派发更新时就可以依靠这个联系挨个触发。给一个 effect 函数其目的也主要是要收集到函数,给函数打上对应的 deps 标记
至此,@vue/reactivity
基本上实现了个大概。