vue2的响应式原理缺陷
简单的描述一下vue2的响应式原理的核心:Object.defineProperty()
我们vue2的数据就是:
data() {
return {
obj: {
name: 'test'
}
}
}
那我们要做的就是劫持obj这个对象,让它具有响应式,所以我们有个类似这样的方法来处理数据:
objserve方法(这个是入口)defineReative方法(这里是处理响应式的地方)
function observe (target) {
// 如果不是对象就直接结束了
if (typeof target !== 'object' || target === null) {
return
}
Object.entries(target).forEach([key, val] => {
defineReative(target, key, val)
})
}
function defineReative (target, key, val) {
if (typeof val === 'object' && val !== null) {
// 两个函数递归调用,确保劫持到所有的属性值
observe(val)
}
Object.defineProperty(target, {
enumerable: true, // 属性可枚举
configurable: true, // 属性可删除
get() { // get会返回值
return val
},
set(newVal) { // set如果数据不一致就更新val
if (newVal !== val) {
val = newVal
}
}
})
}
大概就是以上这么一个方法处理数据使得数据产生了响应式,当然省略了一些依赖收集和触发的代码,不过这个不重要,我们这次主要说一下这个vue2响应式的缺点,上面有几个缺点:
- 数组不能设置响应性,因为
Object.defineProperty不可以设置数组 - 删除对象属性的时候不会响应式(无法触发set):
const obj = { name: 'joe' }; delete obj.name; - 直接在代码里添加属性也没有响应式(无法触发set):
const obj = { }; obj.name = 'joe'
这个是天生的缺陷,当然vue2为了尽可能解决这些天然的缺陷做了很多修补:
- 重写了数组的很多方法,使得数组具有响应性
- 提供了
vue.$set(对象,属性)的方法使得添加属性具有响应性
但是也只能尽量弥补,始终还是带来了一些不方便,那么vue3就完美的解决了这个问题
vue3响应式核心proxy和reflect
下面介绍一下完美解决vue2缺陷的核心api,proxy和reflect
proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等reflect在这里提供和proxy搭配使用的作用
proxy
先通过一个简单的例子来介绍,proxy怎么可以实现对象操作的各种监听
const obj = {
name: 'joe',
hobby: ['basketball', 'volleyball'],
family: {
mother: 'mama',
father: 'baba',
}
}
const proxyObj = new Proxy(obj, {
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value
return true // api要求是这样的,修改成功要返回一个布尔值
}
})
proxyObj.name // 触发get方法
proxyObj.name = 'lili' // 触发set方法
看了我们简单的例子,就看的出来我们是通过new Proxy把我们被代理对象传入进去,然后后面我们就直接操作代理对象就行,所有的修改会自动映射到被代理对象身上。
我们看到例子的obj是有个属性family,是一个对象,如果我们在点一层是怎么样呢,这样能不能代理到,这里要给代码说明一下
proxyObj.family // 这样很正常,get打印出来的key是【family】
proxyObj.family = 'xx' // 这样也很正常,可以正常触发set
proxyObj.family.mother // 这个时候get方法打印出来的key还是【family】,而不是【mother】
proxyObj.family.mother = 'xx' // 这样set就不会触发了
解释一下上面的代码,我们代理只会代理到第一个层级的内容,无论你点多少层级,get打印出来的都是你点的第一个层级的内容,所以无论是.family还是.fanmily.mother``get的时候的key都是family,也就是点多个层级的时候:
.family时候出发get返回对应的值返回出去也就是{mother: 'momo', father: 'baba'}- 后面的
.mother就在{mother: 'momo', father: 'baba'}这个基础上继续获取出mama
而如果set的时候设置了多个层级,set就直接不回触发了,因为你的第二个层级并没有代理,实际就是操作原对象了,也就是等于 obj.family.mother这样操作了就不会出发set了
所以我们一般都是需要递归也把所有的object类型的值都加上代理,就像这样
function deepProxy (obj) {
return new Proxy(obj, {
get(target, key) {
const res = target[key]
// 如果是对象,继续代理
// 注意这里用了按需代理,你没用到的时候,我都不帮你代理
if (typeof res === 'object' && res !== null) {
return deepProxy(res)
}
return res
},
set(target, key, value) {
target[key] = value
return true
}
})
}
reflect
上面介绍了proxy了,那么reflect有什么用呢,reflect实现了很多的api,主要是对js对象操作的方法进行的升级,例如:
obj.name = 'joe'=>Reflect.set(obj, 'name', 'joe')name in obj=>Reflect.has(obj, 'name')delete obj.name=>Reflect.deleteProperty(obj, 'name')
等等很多的api升级,主要是处理了很多的边界情况带来了更合理的使用,而我们配合proxy做监听的就是:
Reflect.get(target, key, receiver)Reflect.set(target, key, value, receiver)
target就是目标对象,key就是目标对象的属性,value就是设置的时候的值,receiver就是getter的时候this的值,可能有些不知道这个receiver是什么,其实proxy的get和set的时候也有这个入参,我们看看代码:
const obj = {
name: 'joe',
}
const proxyObj = new Proxy(obj, {
get(target, key, receiver) { // receiver就是指的当前的代理对象
// return target[key]
Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// target[key] = value
Rflect.set(target, key, value ,receiver)
return true
}
})
上面我们看到了在proxy的get、set里面都用了receiver,那具体有什么用,用了Reflect,加了receiver有什么区别呢,我们举例子来看看区别
const obj = {
name: 'joe',
get info() {
return this.name
}
}
// 不用Reflect
const proxyObj = new Proxy(obj, {
get(target, key, receiver) {
console.log('触发')
return target[key]
},
set(target, key, value, receiver) {
target[key] = value
return true
}
})
// 用Reflect
const proxyObj = new Proxy(obj, {
get(target, key, receiver) {
console.log('触发')
Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
// set之后他会返回一个布尔值
const res = Rflect.set(target, key, value ,receiver)
return res
}
})
// 不用Reflect:【触发】打印了1次 用了Reflect:【触发】打印了2次
proxyObj.info
我们来解析一下,当我们proxyObj.info的时候就会触发get info 这个方法返回的是this.name如果你没用Reflect和receiver来绑定代理对象,那么this就是指向obj,那么当访问this.name的时候就不会触发代理,若用了Reflect和receiver,this就是当前的代理对象,this.name其实就是proxyObj.name就会触发代理
- 不用Reflect:触发info这个key的get(1次)
- 用Reflect:触发info这个key的get,触发name这个key的get(2次)
所以我们是需要使用Reflect且要传入recevier来绑定get、set的时候this是当前的代理对象
vue3的Reactive,Effect实现
好了,解释完了基本原理,我们来说一下vue3的基本api:reactive,effect,
- reactive是使得对象具有代理,是可以实现响应式的重要步骤
- effect主要是收集依赖,追踪数据的变化,通知对象去触发响应
以上两个api是vue3里面最为基础和重要的api,所以他们是一起学习的,我们先通过代码简单看看他们的使用方式:
Reactive
<template>
<div>
<span id='test'></span>
</div>
</template>
<script setup>
// 使得obj具有响应式功能
const obj = reactive({
name: 'joe',
family: {
mother: 'mama',
father: 'baba',
}
})
// 一开始effect会执行一次
effect(() => {
document.querySelector('#test').innerText = obj.name
})
// 此时obj.name发生了改变,effect监听了它函数里面的内容发生了变动,会再执行一次
setTimeout(() => {
obj.name = 'test'
}, 2000)
</script>
接下来我们就要来实现一下vue3的reactive和effect了,看看它究竟是怎么让对象具有响应式的,实现和真正的源码会有不同(我做了代码耦合和忽略了很多边界情况),但是我为了尽量通过简单的代码去说明,先说reactive
const reactiveMap = new WeakMap()
const baseHandles = {
get(target, key, receiver) {
const result = Reflect(target, key, receiver)
// 收集依赖
track(target, key)
// 这里如果get到的结果是对象,那么还要继续递归代理,这样深层次的对象才回具有响应性
if (typeof result === 'object') {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, key, value)
return result
}
}
// 收集依赖
function track() { }
// 触发依赖
function trigger() { }
/**
* @Description 获取响应式的代理
* @author joex
* @date 2025-03-27 17:37:29
* @param {Object} target 被代理对象
* @return {Object} proxy 代理对象
**/
function reactive(target) {
const existProxy = reactiveMap.get(target)
if (existProxy) {
return existProxy
} else {
const newProxy = new Proxy(target, baseHandles)
reactiveMap.set(target, newProxy)
return newProxy
}
}
Effect
上面我们实现了reactive的部分,就是get的时候调用收集依赖,set的时候触发依赖,但是我们看到还没实现完成,track函数和trigger函数还没实现,我们实现effect顺便把track和trigger搞定了
// 这里是所有的变量的依赖存储的map(非常重要)
const targetMap = new WeakMap()
// 这个变量很重要,是用来建立起effect和reactive的联系
let activeEffect;
// 收集依赖
function track(target, key) {
// 如果没有activeEffect说明当前没有任何东西依赖这个变量的属性,
// 就不需要收集,就直接结束就行了
if (!activeEffect) return
// 看看全局依赖map是已经存在,若有则使用,无则设置一个新的子map
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
// 根据属性名获取子map的值,若有则使用,无则设置set结构
let dep = depsMap.get(key)
if (!dep) depsMap.set(key, (dep = new Set()))
// 然后子map的值:set结构传递下去
trackEffect(dep)
}
function trackEffect(dep) {
// 把当前激活的effect存在set结构里面,这样依赖收集就完成了
dep.add(activeEffect)
}
// 触发依赖
function trigger(target, key, newValue) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (!dep) return
triggerEffect(dep)
}
// 触发effects(effect数组)
function triggerEffects(dep) {
const effects = [...dep]
for(const effect of effets) {
triggerEffect(effect)
}
}
// 触发每个具体的effect
function triggerEffect(dep) {
effect.run()
}
class ReactiveEffect {
// 构造设置fn变量,也就是effect传进来的函数
constructor(fn) {
this.fn = fn
}
// 这里执行函数
run() {
// 执行那个effect函数就把当前执行的设置到activeEffect
activeEffect = this
return this.fn()
}
}
/**
* @Description effect函数
* @author joex
* @date 2025-03-27 17:37:29
* @param {Object} target 被代理对象
* @return {Object} proxy 代理对象
**/
function effect(fn) {
// 会new一个对象,然后执行run方法
const _effect = new ReactiveEffect(fn)
_effect.run()
}
上面写了挺多代码的,先来解析一下,先说effect,核心就是ReactiveEffect类,这个类需要传入一个函数初始化,也就是:effect(() => { 我就是那个函数 }),这个类很重要的一个步骤就是:
- 在执行传进来的函数fn的时候会把当前的执行的effect赋值给一个全局变量:activeEffect(代表当前激活的effect)
- 如果fn里面有使用到响应式的变量,get的开始收集依赖
- 而此时get怎么知道是哪个effect是在执行呢?就是通过activeEffect,他会把activeEffect收集到哪个target的哪个key里面,然后下次在set的时候就会触发对应的函数
画个示意图看看收集依赖的过程
就像示意图的结构,targetMap是存储所有的依赖的一个变量,存了依赖,当set的时候就根据对应的key找到对应的依赖执行即可,我们看到最后存储activeEffect的是一个set结构,因为我们的当前变量可能被多个effect所依赖,所以用set来存储多个,当触发的时候会遍历set里面的所有函数
vue3的Ref实现
ref是什么,也是一个把数据变成响应式的api,具体是怎么使用我们举个例子:
const name = ref('lili')
const obj = ref({
name: 'joe'
})
name.value // lili
obj.value.name // joe
我们的ref和reactive还是有些不一样的,ref的变量每次使用的时候要通过.value才能获取它的值,那么这两个玩意有什么区别的:
- ref需要.value,reactive不需要
- ref可以包装基本数据类型和对象,reactive只能包装对象
- ref可以替换整体对象:
obj.value = {}还是可以保持响应性,但是如果reactive替换整体对象就不行了
那么我们来实现一下ref,看看为什么需要.value
// ref函数,主要提供一个入口,返回的是ref实例
function ref (value) {
return new RefImpl(value)
}
// 主要的ref类型
class RefImpl {
#_value // 私有属性:_value 这个是响应式的值
#_rawValue // 私有属性: _rawValue(这个是原始值,要用来对比的)
#dep // 私有属性:存储依赖的数组
constuctor(value) {
this.#_rawValue = value // 初始化
this.#_value = toReactive(value) // 这里调用获取响应式方法
},
// .value的时候就收集依赖,和之前的reactive大同小异了,然后设置_value值
get value() {
trackRefValue(this)
return this.#_value
},
// 判断如果有变化就触发依赖
set value(newValue) {
if (!Objiect.is(newValue, this.#_rawValue)) {
this.#_rawValue = newValue
this.#_value = toReactive(value)
triggerRefValue(this)
}
}
}
function toReactive (value) {
// 如果传进来的是对象,我们就调用reactive的方法了,就和之前介绍的reactive一样了
// 如果不是对象就返回去就好
return isObject(value) ? reactive(value) : value
}
// 收集依赖具体操作
function trackRefValue (ref) {
if (activeEffect) {
let dep = ref.dep || (ref.dep = new Set())
// 这个逻辑之前写过了就不写了,可以返回去看Effect的实现那个
trackEffects(dep)
}
}
// 触发依赖具体操作
function triggerRefValue (ref) {
if (ref.dep) {
// 这个逻辑之前写过了就不写了,可以返回去看Effect的实现那个
triggerEffects(ref.dep)
}
}
以上就是我们的ref的代码,其实我们可以看到它的做法和reactive基本是一样的,只不过多包装了一层value:
- get的时候收集依赖,看看activeEffect有没有,有就收集
- set的时候触发依赖,遍历收集的activeEffect
vue3的computed和watch实现
接下来我们要介绍我们coomputed和watch了,这两个api可谓是我们的开发大帮手,但凡是用过vue开发的都会很熟悉了,但是他们具体是怎么实现的呢,让我们来看一下
computed
我们先说说computed就是我们的计算属性,他是接受一个函数或者对象,我们这里只讨论函数的形式,然后函数必须有返回值,就当成一个变量使用的computed,使用的时候也要.value,让我们看看使用的例子
<div id='app'></div>
const testReactive = { name: 'joe' }
const testComputed = computed(() => {
return testReactive.name
})
effect(() => {
// 执行两次,证明是有缓存的
document.querySelector('#app').innerText = testComputed.name
document.querySelector('#app').innerText = testComputed.name
})
setTimeOut(() => {
testReactive.name = 'compunted'
}, 2000)
我们解释一下我们的例子:
- 创建了一个reactive对象testReactive
- 创建了一个computed对象,里面返回了reactive对象的值
- 创建了effect函数,里面实现了app标签设置computed的值显示
- 设置了定时器,2秒后修改testReactive,会触发computed的值变化,然后触发effect的值变化
所以整个的过程还是挺多步骤的,而且我们看到effect里面执行了两次获取computed,但是其实只会执行一次逻辑,这就是computed的缓存逻辑,如果值没有改变,他只会执行一次。这个computed涉及了之前的一些triggerEffects和triggerEffect的方法的修改,所以我会把之前写过的一些方法也都一并写一次在下面
let activeEffect
function computed (getterOrOptions) {
let getter
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
}
const cRef = new ComputedRefOptions(getter)
return cRef
}
class ComputedRefOptions {
dep // dep存依赖的
_value // 返回值的
effect // 就是依赖的实例
// 解释一下这个,就是标记是否需要脏了,如果脏检测为true就需要获取最新数据,否则返回旧的
_dirty
construtor(getter) {
this._dirty = true
// 构造时候先生成一个effect依赖,传进去computed的函数
// 生成effect时候第二个参数就是scheduler调度器,后面触发依赖会调用
this.effect = new ReactiveEffect(getter, () => {
// 1. 调用scheduler判断是否脏检查为false,是的话就设置设置为true(告诉get需要获取新的)
// 2. 然后触发依赖
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
// 为了后面区分出这个依赖是不是computed类型的依赖
this.effect.computed = this
},
get value() {
// 收集依赖
trackRefValue(this)
// 判断脏检测
if (this._dirty) {
// 执行effect实例的run函数
this._dirty = false
this._value = this.effect.run()
}
// 返回值
return this._value
}
}
// 判断是否函数
function isFunction (val) {
return typeof fn === 'function'
}
class ReactiveEffect {
// 改变1: 构造函数增加了scheduler的入参,是一个函数
constructor(fn, scheduler) {
this.fn = fn
this.schedule = scheduler
}
run() {
activeEffect = this
return this.fn()
}
}
// 收集依赖具体操作
function trackRefValue (ref) {
if (activeEffect) {
let dep = ref.dep || (ref.dep = new Set())
trackEffects(dep)
}
}
// 触发依赖具体操作
function triggerRefValue (ref) {
if (ref.dep) {
triggerEffects(ref.dep)
}
}
function triggerEffects (dep) {
const effets = dep ? [...dep] : []
// 改变2: 原本是一个for循环,改成2个,现在执行所有有computed的依赖,再执行别的
for (const effect of dep) {
if (effect.computed) {
triggerEffect(effect)
}
}
for (const effect of dep) {
if (!effect.computed) {
triggerEffect(effect)
}
}
}
function triggerEffect (effect) {
// 改变3: 执行依赖的时候要判断是否有调度器scheduler,有的话直接执行调度器
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
可以看到我们的computed的实现主要是通过一个ComputedRefOptions的类来实现功能的,我们根据我们实现的例子来说一下相关的步骤:
- effect:run,activeEffect变成effect实例,然后执行effect的fn
- computed:testComputed.name触发获取computed的get事件,收集依赖此时computed收集到依赖是effect
- computed:dirty开始为true,继续执行get,dirty变成false
- computed:get事件调用自己的初始化的内部的effect实例的run方法,执行函数fn,设置activeEffect变成effect变成computed的内部的effect,返回testReactive.name,
- reactive:触发testReactive的get事件,收集到的依赖是computed的内部的effect
- computed:然后会再次执行步骤2,因为我们写了两个一样的testComputed.name,但此时收集的依赖不同,testComputed.name触发获取computed的get事件,收集依赖此时computed收集到依赖是computed的内部的effect
- computed:执行computed的get事件,因为dirty是false,不会收集依赖,直接返回之前记录的值
- reactive:等待2秒之后此时会改变testReactive.name,此时会触发它的set事件,触发依赖,也就是会执行computed的内部effect
- reactive:执行依赖会判断是否有scheduler,此时是有,那么就会执行scheduler
- scheduler:此时dirty为false,所以可执行,就会把dirty改成true,触发computed的依赖,也就是effect实例和computed的内部的effect
- computed:那我们执行到遍历依赖的时候我们是先执行computed的内部effect,会走9的路线,判断有scheduler,然后调用,但是此时的dirty是true,所以结束第一个依赖
- computed:第二个依赖是effect,执行就会走回到了第1步,然后一直到渲染新的变量就结束了
可以看到我们的步骤非常多,我们待会再画一下图来演示一下这个过程,但是我们这里还要加以解释一下之前代码的三大改变是为了什么:
- 增加了scheduler的入参:这是就为了后面调用的没什么好说
- 改变for循环:先循环computed的依赖,这样才不会造成死循环,不然会不断的自我调用然后爆栈
- 改变执行依赖:就为了调用scheduler
下面再图示一下这个的过程
watch
终于到了watch可比computed要难多了,主要里面涉及到schduler的部分,所以我们这里要分成两个来说:
- scheduler:这个是实现watch机制重要的一部分会单独拎出来说一下
- doWatch:这个就是实现watch功能的主要函数
scheduler
先说一下它实现后的效果是怎么样的:
const testReact = reactive({
count: 1
})
effect(() => {
console.log('count', testReact.count)
}, { // 触发依赖会执行scheduler
scheduler() {
queuePreFlusCb(() => {
console.log(testReact.count)
})
}
})
testReact.count = 2
testReact.count = 3
// 结果
// count 1
// 3
// 3
为什么每次执行都是3呢,因为我们省略了中间的那次计算,两次触发都是最后一次的计算,而这一切的源头就是scheduler的把要执行的代码扔到了微任务队列实现的,那接下来看看它是怎么做到这件事的:
let isFlushing = false // 标记是否正在刷新队列
let peddingPreFlushCbs = [] // 队列
const resovedPromise = Promise.resolve() // 一个promise对象
let currentFlushPromise // 当前刷新的promise
// 刷新队列
export function queuePreFlusCb(cb) {
queueCb(cb, peddingPreFlushCbs)
}
// 刷新队列
function queueCb(cb, peddingPreFlushCbs) {
// 刷新队列添加回调函数
peddingPreFlushCbs.push(cb)
// 刷新队列
queueFlush()
}
// 具体刷新队列
function queueFlush() {
// 如果正在刷新队列,直接结束
if (!isFlushing) {
// 正在刷新队列
isFlushing = true
// 放进微任务执行刷新队列
currentFlushPromise = resovedPromise.then(flushJobs)
}
}
// 刷新队列
function flushJobs() {
fulshPreFulshCbs()
isFlushing = false
}
// 刷新队列
export function fulshPreFulshCbs () {
// 如果有队列
if (peddingPreFlushCbs.length) {
// 去重换成数组(这里是伪实现,实际是通过设计id来实现去重的)
let activePreFlushCbs = Array.from(new Set(peddingPreFlushCbs))
// 清空队列
peddingPreFlushCbs.length = 0
// 遍历队列
for (let i = 0; i < activePreFlushCbs.length; i++) {
// 执行队列
activePreFlushCbs[i]()
}
}
}
我们来说一下schduler做了什么:
- 新建了一个任务队列数组,一个promise,一个是否正在执行的标识
- 把传入的函数塞到任务队列数组
- 判断标识,若没有正在执行则设置成执行然后把flushJobs放进promise的.then事件,这个操作很重要,这个实际就是把整个函数放到了微任务队列,若正在执行则直接结束
- 然后等到flushJobs触发的时候宏任务已经执行完成,此时我们cout已经变成3了,我们就会把任务队列去重然后执行
最重要的就是第三步,通过Promise.resove.then(这里面推到微任务)
doWatch
上面为什么先介绍了scheduler,因为watch里面就是使用了这个来实现,这样可以有效的节省性能,如果你连着:count = 1; count = 2; count = 3; count = 4 这几行代码,vue并不希望它不断的触发watch,这样很浪费性能,实际我们就只会被最后一个count = 4触发
接着让我们来看看watch的用法吧:
const testReactive = {
count: 1
}
watch(testReactve, (val) => {
console.log(val)
}, {
})
testReactive.count = 2
我们可以看到watch就是一个函数,然后传进去一个监听的对象,和一个回调函数,监听的对象如果有变化就会触发回调函数的内容
function watch (target, cb, options) {
doWatch(target, cb, options)
}
function doWatch (target, cb, { immediate, deep } = options) {
let getter
if (isReative(target)) {
getter = () => target
deep = true
} else {
getter = () => {}
}
if (cb && depp) {
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let oldValue = {}
const job = () => {
if (cb) {
const newValue = effect.run()
if (depp || hasChange(newValue, oldValue))
cb(newValue, oldValue)
oldValue = newValue
}
}
let scheduler = () => queuePreFlusCb(job)
const effect = new ReactiveEffect(getter, scheduler)
if (cb) {
if (immediate) {
job()
} else {
oldValue = effect.run()
}
}
}
function traverse (target) {
if (!Object.is(target)) {
return target
}
for (const key in value) {
traverse(target[key])
}
return target
}
以上就是watch的实现,这么看起来还是比较简单的,我们来分析一下执行的步骤:
- 首先我们把监听的target变成一个函数:getter = () => target
- 然后设置深度deep为true
- 判断如果有回调函数和深度监听则设置getter成一个会遍历target再返回target的函数(为了后面执行收集依赖)
- 设置oldValue是空对象
- 定一个job函数,job函数做了几件事
-
- 设置newValue的值为执行的effect.run
- 若深度监听或对比新旧有变化则调用watch的cb完成触发回调函数
- 定一个scheduler,使用queuePreFlusCb把job参数传入进去(为了提高性能,让watch的触发异步执行)
- 定义一个effect = new ReactiveEffect(getter, schduler)
- 若有cb且immediate为true,则执行调用job
-
- 调用job则会触发出effect.run就会触发设置依赖:activeEffect = effect
- 然后执行了getter函数,因getter的返回值会触发traverse就会触发testReactive对象收集依赖effect
- 若immediate不是true,也会触发effect.run(一样触发收集依赖effect),赋值给oldValue
- 然后此时如果修改testReactive,就会触发执行依赖
-
- 执行依赖发现依赖有scheduler就会执行scheduler
- 然后最后完成watch的cb的调用
看完了都是老套路了,不过都是有些弯弯绕绕,捋清楚就行了,基本都是:
- 新建ReactiveEffect
- 然后执行run,收集依赖
- 然后触发依赖执行触发对应函数完成我们想要的目标