手写vue3响应式原理
vue3响应式原理
前面写过vue2响应式原理 想了解的可以去看下, 本章讲vue3的响应式原理,并对照着源码手写一下简化的vue3响应式系统。
Proxy
在 Vue.2x 中,使用 Object.defineProperty()
对对象进行监听。而在 Vue3.0 中,改用 Proxy
进行监听。Proxy
比起 Object.defineProperty()
有如下优势:
- 监听的是整个对象,可以监听属性的增删操作。
- 可以监听数组某个索引值的变化以及数组长度的变化。
心智负担
对比vue2的响应式系统,vue3的功能更强大,这也导致了其心智负担
增加了。
Proxy
并不是直接在原对象上操作,而是新建了一个代理对象用来监听,后果就是代码中同时存在原始对象和代理对象,于是vue3中新增了toRaw、markRaw
等api。- vue3新增了
readonly
只读代理和shallowRef
、shallowReactive
、shallowReadonly
浅层代理等 api,当然这应该算正向的好处,不过学习成本提高了,心智负担增加, - 由于基本类型数据无法被
Proxy
代理,引入了ref
。ref
和reactive
两套响应式api,无形中也导致了使用心智负担增加。 - 由于es6的解构赋值会导致
proxy
的代理响应丢失,于是引入了toRefs
、toRef
,理解和使用心智负担增加。 - 新增
副作用作用域(effectScope)
的概念,用于批量操作effect
。这也是好处,但是心智负担哈,也是提高了。
心智负担增加就意味着学习使用成本提高,接下我们就参照vue3的源码,目录文件名和方法名保持和源码一致,手写一个响应式系统,理解其中的原理,也方便大家读源码时理解源码。
reactive
作用: 返回一个对象的响应式代理。
简单实现理解原理不考虑 readonly
和 shallow
了
// src/reactivity/reactive.js
import { isObject } from '../utils/index.js'
import { mutableHandlers } from './baseHandlers.js'
export const ReactiveFlags = {
IS_REACTIVE : "__v_isReactive", // 判断是否已被reactive的标识
RAW : "__v_raw", // 挂载原始对象的字段
}
// 全局创建一个WeakMap存储 原始对象到代理对象的 映射
export const reactiveMap = new WeakMap()
// reactive() 是向用户暴露的 API,它真正执行的是 createReactiveObject() 函数
// mutableHandlers 即针对普通对象和数组的get/set处理handlers
// 源码中这里还传了针对集合类型数据(Map,Set,WeakMap,WeakSet)代理处理的handlers,本文暂不处理这种情况,就暂时不传
export function reactive(target) {
return createReactiveObject(target, reactiveMap, mutableHandlers)
}
export function createReactiveObject(target, proxyMap, baseHandlers) {
// 非对象类数据,直接返回 target
if (!isObject(target)) {
return target
}
// 已经被响应式化的数据, 直接返回 target
if (isReactive(target)) {
return target
}
// proxyMap里存过的数据,直接查到返回
if (proxyMap.has(target)) {
return proxyMap.get(target)
}
// 创建代理数据,传入原始对象target,和对应的处理handlers
const proxy = new Proxy(target, baseHandlers)
// 将原始对象和代理对象存进WeakMap
proxyMap.set(target, proxy)
// 返回代理对象
return proxy
}
// 根据标识判断是否是reactive对象
export function isReactive(value) {
return !!value[ReactiveFlags.IS_REACTIVE]
}
这个函数就是判断了那些数据可以被响应式化,然后new Proxy
实际处理代理响应的具体逻辑在 baseHandlers
// src/reactivity/baseHandlers.js
import { hasChanged, isObject } from '../utils/index.js'
import { track, trigger } from './effect.js'
import { reactive, ReactiveFlags, reactiveMap } from './reactive.js'
// 处理get
function createGetter() {
return function get(target, key, receiver) {
// key 为 __v_raw 即为获取原始对象,且代理对象和原始对象能对应上,返回target
if (key === ReactiveFlags.RAW && receiver === reactiveMap.get(target)) {
return target
// key 为 __v_isReactive 即 判断当前数据是否为响应式数据,返回true
} else if (key === ReactiveFlags.IS_REACTIVE) {
return true
}
// 从原始对象中获取对应值
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
// 值如果是对象,则继续代理值,相比vue2的一开始就递归响应式化所有值,这里
// 只在get用到的时候才代理,算是性能上的优化
if (isObject(res)) {
// 返回代理后的子值
return reactive(res)
}
// 返回值
return res
}
}
// 处理set
function createSetter() {
return function set(target, key, newValue, receiver) {
const oldValue = target[key]
// 给原始对象设置值
const res = Reflect.set(target, key, newValue, receiver)
// 新旧值发生改变即触发依赖更新
if (hasChanged(oldValue, newValue)) {
trigger(target, key)
}
return res
}
}
const get = createGetter()
const set = createSetter()
// 这里只处理了,set get 源码中还有 deleteProperty has ownKeys
export const mutableHandlers = {
get,
set
}
以上就是代理get/set
的 具体处理。
effect
effect
直译就是 副作用
,主要和响应式的对象结合使用,响应式对象更新触发effect
更新,也可以理解为就是依赖
,类似vue2中的watcher
的概念。
// src/reactivity/effect.js
import { createDep } from "./dep.js"
import { recordEffectScope } from "./effectScope.js"
// 面向用户的effect函数, 接受更新函数fn,和配置参数options
export function effect(fn, options = {}) {
// 创建effect实例
const _effect = new ReactiveEffect(fn)
// 未配置 lazy 的,自动运行一次 更新函数
if (!options.lazy) {
_effect.run()
}
// 把 _effect.run 这个方法返回
// 让用户可以自行选择调用的时机(调用 fn)
const runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
let activeEffect = undefined // 当前正在运行的effect
let shouldTrack = false // 是否可以收集依赖
// effect实现类
export class ReactiveEffect {
parent = undefined // 父级effect,也即运行当前effect时的外层effect
active = true // effect是否是激活状态
deps = [] // effect对应的dep,即effect被哪些dep收集过
onStop = undefined // 停止时触发的钩子
constructor(fn, scheduler) {
this.fn = fn
this.scheduler = scheduler // 调度器, 先忽略
}
// 更新
run() {
// 非激活状态,执行 fn 但是不收集依赖
if (!this.active) {
return this.fn();
}
// 存储上一个 shouldTrack 和 activeEffect 的值
const lastShouldTrack = shouldTrack
this.parent = activeEffect
// 执行 fn 收集依赖
// 可以开始收集依赖了
shouldTrack = true
// 执行的时候将当前的 effect 赋值给全局的 activeEffect
// 利用全局属性来获取当前的 effect
activeEffect = this
try {
// 执行用户传入的 fn
return this.fn()
} finally {
// 将刚存的值赋值回去
shouldTrack = lastShouldTrack
activeEffect = this.parent
this.parent = undefined
}
}
// 停止
stop() {
if (this.active) {
// 如果第一次执行 stop 后 active 就 false 了
// 这是为了防止重复的调用,执行 stop 逻辑
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
}
this.active = false
}
}
function cleanupEffect(effect) {
// 找到所有依赖这个 effect 的响应式对象
// 从这些响应式对象里面把 effect 给删除掉
effect.deps.forEach((dep) => {
dep.delete(effect);
});
effect.deps.length = 0;
}
上面的逻辑就是,调用effect创建ReactiveEffect
类即创建依赖。ReactiveEffect
执行run方法时设置,当前实例this
到全局activeEffect
方便后面收集依赖。
既然依赖以及创建了,那么趁热打铁,我们再看下怎么收集依赖track
和触发依赖更新trigger
// src/reactivity/effect.js
// 创建WeakMap映射,存储对应原始对象对应的key的依赖
// 二维的结构, 类似
// targetMap -> { target: depsMap }
// depsMap -> { key: dep }
const targetMap = new WeakMap()
// 收集依赖
export function track(target, key) {
// 无法收集时直接返回
if (!canTrack()) {
return
}
// 根据target 找 depsMap,没有就新建一个,并存下当前target和新建的depsMap
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 再根据key 找 dep, 同样没有就新建一个,并存下当前key和新建的dep
let dep = depsMap.get(key)
if (!dep) {
dep = createDep()
depsMap.set(key, dep)
}
trackEffects(dep)
}
// dep收集当前正在运行的 effect,即全局的activeEffect,同时effect也存下这个dep
export function trackEffects(dep) {
if (!dep.has(activeEffect)) {
// 双向收集
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
}
// 触发依赖更新
export function trigger(target, key) {
// 根据原始对象target找到对应的depsMap
const depsMap = targetMap.get(target)
if (!depsMap) return
// 根据depsMap找到对应的依赖器dep
const dep = depsMap.get(key)
// 如果存在dep则触发更新
if (dep) {
triggerEffects(dep)
}
}
// 触发dep中收集的effect更新
export function triggerEffects(dep) {
// 循环获取每一个effect
for (const effect of dep) {
// 存在调度函数则调用调度函数,让后续scheduler中的逻辑处理具体怎么更新
if (effect.scheduler) {
effect.scheduler()
// 不存在调度函数,则直接调用run更新
} else {
effect.run()
}
}
}
// 判断是否可以收集依赖
export function canTrack() {
return shouldTrack && activeEffect
}
// 创建dep, 利用Set去重,用于存储effect
export function createDep(effects) {
return new Set(effects)
}
就是创建targetMap
建立依赖和对应响应式数据的原始数据间的映射关系并存储 ,调用track
可以将当前正在运行的依赖activeEffect
收集到targetMap
, 调用trigger
可以根据原始对象和key到targetMap
中找到收集的依赖effect
并触发更新。
至此 响应式对象(reactive) 和 副作用函数(effect)均已完成,验证下:
// 创建响应式对象
const foo = reactive({
a: 1
})
// 添加副作用函数
effect(() => {
console.log('更新:' + foo.a)
})
// 打印 ---> 更新:1
// 修改响应式数据
foo.a = 2
foo.a = 3
// 打印 ---> 更新:2
// 打印 ---> 更新:3
ref
由于基本类型数据无法被Proxy
代理,所以vue3引入了ref
,接下来就是实现ref
用于处理基本数值类型的响应式。
官网介绍:ref(value)
接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value
。
具体实现
// src/reactivity/ref.js
import { hasChanged } from '../utils/index.js'
import { createDep } from './dep.js'
import { canTrack, trackEffects, triggerEffects } from './effect.js'
import { toReactive } from './reactive.js'
// 面向用户的ref函数
export function ref(value) {
// 已经是ref数据的直接返回value
if (isRef(value)) {
return value
}
// 创建 RefImpl 实例 , ps: Impl 就是 类实现 的意思
return new RefImpl(value)
}
// 直接通过 __v_isRef 标识判定是不是 ref 对象
export function isRef(value) {
return !!(value && value.__v_isRef === true)
}
class RefImpl {
__v_isRef = true // 标识
dep // 依赖存储处
constructor(value) {
// 如果传进来的是对象则利用reactive对其进行响应式化
this._value = toReactive(value)
this._rawValue = value // 存下原始值
}
// 调.value时触发get, 收集依赖并返回 _value
get value() {
trackRefValue(this)
return this._value
}
// 设置.value值时
set value(newValue) {
if (hasChanged(newValue, this._rawValue)) { // 判断新旧值是否一致
this._rawValue = newValue // 修改原始值
this._value = toReactive(newValue) // 响应式化新值
triggerRefValue(this) // 触发依赖更新
}
}
}
// 给ref收集依赖
export function trackRefValue(ref) {
if (canTrack()) {
// 方法定义位置见上面的 effect.js
// 调 trackEffects 收集 当前正在运行的 effect,到ref.dep上,没有dep则新建一个
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
// 触发ref的依赖更新 方法定义位置见上面的 effect.js
export function triggerRefValue(ref) {
if (ref.dep) {
// 方法定义位置见上面的 effect.js
// 触发ref.dep中收集的effect更新
triggerEffects(ref.dep)
}
}
// src/reactivity/reactive.js
// 如果是引用类型则进行响应式化返回代理后的对像, 否则返回原始值
export function toReactive(value) {
return isObject(value) ? reactive(value) : value
}
可以看到ref
本质上就是 新建一个RefImpl
类 , 利用类的 get/set
收集触发依赖,逻辑很简单。
验证下:
const foo = ref(1)
effect(() => {
console.log('更新:' + foo.value)
})
// 打印 ---> 更新:1
foo.value = 2
foo.value = 3
// 打印 ---> 更新:2
// 打印 ---> 更新:3
toRefs、toRef
由于es6的解构赋值会导致proxy
的代理响应丢失,于是引入了toRefs
、toRef
。
先看案例:
const foo = reactive({
a: {
c: 1
},
b: 2
})
let { a, b } = foo
effect(() => {
a.c + b
console.log('更新')
})
// 首次打印 -> 更新
a.c = 2 // 打印 -> 更新
b = 3 // 未触发更新无打印
a = { c: 4 } // 未触发更新无打印
上述代码中,我们发现, 解构赋值,b 不会触发响应式
,a如果你访问其子属性时
会触发响应式,a直接更改引用也不会触发响应式
这是为什么呢?
我们知道解构赋值,区分基本类型的赋值,和引用类型的赋值,
基本类型的赋值相当于按值传递
, 引用类型的值就相当于按引用传递
基本类型的赋值
const { b } = foo
b = 3 // b此时就是一个值3,和当前的foo已经没有联系了,所以你直接访问和修改b,触发不了foo的get/set,所以就不会导致更新
引用类型赋值
const { a } = foo
a.c = 3 // 由于a是对象是引用类型,赋值的只是引用,此时的a和foo.a公用一个引用,所以修改a.c就相当于修改foo.a.c,所以能触发foo的get/set,所以就会更新
再看最后一种直接修改引用
const { a } = foo
a = { c: 4 } // 此时foo.a虽然是引用类型,但是直接修改了a的引用,就同样导致了a和foo.a不沾边了,那么就和第一种情况一样了,同样不会导致更新
所以vue3引入了toRefs
来解决这种问题,这样使用就能保证响应式。
const foo = reactive({
a: {
c: 1
},
b: 2
})
let { a, b } = toRefs(foo)
effect(() => {
a.value.c + b.value // 值得一提的是,在vue3的template中不需要加.value,因为vue3编译的过程中会自动帮你处理,但是这里不行
console.log('更新')
})
// 首次打印 -> 更新
b.value = 3 // 打印 -> 更新
a.value = { c: 4 } // 打印 -> 更新
可以看到,toRefs
就是将foo
对象的每个属性都转换成了是指向foo
对象相应属性的 ref,因为是ref
所以使用时要求必须用.value
实现:
// src/reactivity/ref.js
export function toRefs(object) {
// 数组处理
const ret = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
// 将对象的每一项转换为 ref
ret[key] = toRef(object, key)
}
return ret
}
export function toRef(object, key, defaultValue) {
const val = object[key]
// 本身是ref的不转换,否则转换成 ObjectRefImpl
return isRef(val) ? val : new ObjectRefImpl(object, key, defaultValue)
}
class ObjectRefImpl {
__v_isRef = true // 标识本对象是ref
constructor(object, key, defaultValue) {
// 赋值到内部变量
this._object = object
this._key = key
this._defaultValue = defaultValue
}
get value() {
// 可以看到 调 .value 就是在 调 object[key]
const val = this._object[this._key]
return val === undefined ? this._defaultValue : val
}
set value(newVal) {
// 同样 设置.value 就是在 设置 object[key]
this._object[this._key] = newVal
}
}
代码逻辑很简单,就是利用类的get/set
将 所有对.value
的操作代理到原来的响应式对象上去,连依赖收集触发都是在原响应式对象中。
最后跑一次上面的使用案例,通过~
computed
接下来就是计算属性computed
的实现了。
官网介绍: 接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value
暴露 getter 函数的返回值。它也可以接受一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
两种用法,传 getter函数
只读或传 get/set
配置对象可写
// src/reactivity/computed.js
import { isFunction, noop } from "../utils/index.js"
import { ReactiveEffect } from './effect.js'
import { trackRefValue, triggerRefValue } from "./ref.js"
// 面向用户的 computed 函数
export function computed(getterOrOptions) {
let getter
let setter
// 传函数就代表只有 get 没有 set
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = noop // noop 即空函数 () => {}
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set || noop
}
// 最后返回 ComputedRefImpl 实例
return new ComputedRefImpl(getter, setter)
}
class ComputedRefImpl {
dep // 存储使用了computed值的effect
effect // 当前的computed effect, 类似vue2中的computed watcher
_value // getter计算出来的值
__v_isRef = true // 标识是ref对象
_dirty = true // 标识getter计算出来的值是否是脏的,是否需要重新计算,一开始肯定要计算的
constructor(getter, setter) {
this._getter = getter
this._setter = setter
// 创建computed effect,并传入调度函数,即computed关联的响应式对象变化时,触发computed effect更新,不直接更新而是走这个调度函数,具体可以见上面effect实现的章节
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
// 响应式对象变化后,将computed的_dirty置为true,代表computed的数据脏了
this._dirty = true
// 触发所以使用了computed值的effect,即存在this.dep里的effect更新
triggerRefValue(this)
}
})
}
get value() {
// 收集所有使用了本computed值的依赖effect, 具体实现见 ref.js
trackRefValue(this)
// 使用computed值时,发现数据已脏,就调本触发computed的effect的run方法即调this._getter重新计算computed 值
// 只有在数据脏了,也就是其关联的响应式对象发生变化后才重新计算,这样就能保证在多次使用computed值时,不用多次计算,节省性能
if (this._dirty) {
this._dirty = false
// effect.run和直接调this._getter的区别在,effect会将本computed effect设置为全局的 activeEffect, 方便执行this._getter过程中的响应式对象收集 本computed effect作为依赖,具体实现见上面 effect 章节
this._value = this.effect.run()
}
// 返回computed的值
return this._value
}
// 这里就是调用用户传的set,走用户的逻辑
set value(newValue) {
this._setter(newValue)
}
}
至此实现代码完成,验证下:
const foo = reactive({
a: 1
})
const bar = computed(() => foo.a + 1)
effect(() => {
console.log('更新:' + bar.value)
})
// 首次打印 -> 更新:2
foo.a = 2
// 打印 -> 更新:3
好的,运行没问题。我们分析下它的具体运作流程:
- 生成
foo
响应式对象 - 调用
computed
生成bar
计算属性,此时还没计算computed
的值,但初始化时其_dirty
值为true - 创建
模拟渲染effect
,首次渲染时,将此模拟渲染effect
赋值到全局activeEffect
,渲染运行中使用了bar.value
,触发其get value
, 将 全局activeEffect
也就是此模拟渲染effect
收集到computed
的dep
里,此时_dirty
为 ture, 那么就调effect.run()
计算值,并将_ditry
置为 false,意味着值是干净的。effect.run
运行的过程中,设置此effect也就是computed effect
为全局activeEffect
,接着调this._getter
其中使用到了foo.a
触发其get,便将此时的全局activeEffect
也就是此computed effect
收集到foo.a
的依赖中去,然后计算值,返回值给bar.value
接着首次打印更新:2
- 运行
foo.a = 2
,修改响应式数据,触发其依赖更新,其中就包括刚刚被收集的computed effect
,computed effect
由于初始化时传了调度函数,所以更新就是调用这个调度函数,此时将_dirty
置为true
,代表computed的值
脏了。然后触发computed
收集的依赖,也就是这里使用了computed
值的模拟渲染effect
更新,更新过程中又用到了computed
的值,触发其get value
,由于此时_dirty
为true
,那么就调computed effect
的effect.run()
重新计算值返回给模拟渲染effect
使用,这样就完成了一次更新
watchEffect
watch,watchEffect
并不在vue3的reactivity
中,在vue3的设计中reactivity
这个响应式系统是可以直接单独对外使用的。而watch,watchEffect
可以看做是针对vue3运行时的一个特别的effect
,所以他们的代码被放到了runtime-core
包里。
接下来先实现下watchEffect
先看用法:立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。
const count = ref(0)
const stop = watchEffect((onCleanup) => {
onCleanup(() => {
// count.value发生变化时触发,首次执行时不触发,
// 可用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求
console.log('onCleanup')
})
console.log(count.value)
}, /* options */) // 可选的options配置参数,用来调整副作用的刷新时机或调试副作用的依赖,主要是设置侦听器将在组件渲染之前执行,还是同步还是之后执行的,本文由于只讲响应式原理,不涉及渲染相关所以就不实现了。
// 首次打印 -> 输出 0
count.value++
// 打印 -> oncleanup
// 打印 -> 输出 1
// 当不再需要此侦听器时,可以停止侦听器
stop()
// 打印 -> oncleanup
count.value++
// 不在打印
可以看到watchEffect
相比前面的effect
主要就是多了个 onCleanup
和 返回了stop
实现代码:
// src/runtime-core/apiWatch.js
// 面向用户的 watchEffect
export function watchEffect(effect) {
return doWatch(effect, null)
}
// 具体实现 源码里 watch , watchEffect 都是通过这个函数实现的,这会先讲 watchEffect
export function doWatch(source, cb, options = {}) {
// 用于创建 effect 的 getter,这里是在执行 effect.run 的时候,也就是更新的时候就会调用的
let getter = () => {
// 执行clean的时机
if (cleanup) {
cleanup()
}
source(onCleanup)
}
// cleanup 的作用是为了解决初始化的时候不调用 fn(用户传过来的 cleanup)
// 第一次执行 watchEffect 的时候 onCleanup 会被调用 而这时候只需要把 fn 赋值给 cleanup 就可以
// 当第二次执行 watchEffect 的时候就需要执行 fn 了 也就是 cleanup
let cleanup
const onCleanup = (fn) => {
// 当 effect stop 的时候也需要执行 cleanup
// 所以可以在 onStop 中直接执行 fn
cleanup = effect.onStop = () => {
fn()
}
}
// 更新时执行
const job = () => {
if (!effect.active) return;
effect.run()
}
// 创建 effect
const effect = new ReactiveEffect(getter, job)
// 首次执行
effect.run()
// 返回stop
const unwatch = () => {
effect.stop()
}
return unwatch
}
可以看到watcheEffect
就是调用new ReactiveEffect
生成一个特殊的effect
,然后处理下onCleanup
和stop
最后可以拿上面的用法去测试下,这里就不写了。
watch
先看用法:
watch()
默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。
第一个参数是侦听器的源。这个来源可以是以下几种:
- 一个函数,返回一个值
- 一个 ref
- 一个响应式对象
- ...或是由以上类型的值组成的数组
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
const count = ref(0)
watch(count, (newVal, oldVal, onCleanup) => {
onCleanup(() => {
console.log('onCleanup')
})
console.log(newVal.value)
}, {
immediate: true,
})
watch
和vue2中的$watch
用法基本一致。
修改doWatch
方法实现:
// src/runtime-core/apiWatch.js
import { ReactiveEffect } from "../reactivity/effect.js"
import { isReactive } from "../reactivity/reactive.js"
import { isRef } from "../reactivity/ref.js"
import { hasChanged, isFunction } from "../utils/index.js"
const INITIAL_WATCHER_VALUE = {}
// 面向用户的 watchEffect
export function watchEffect(effect) {
return doWatch(effect, null)
}
// 面向用户的 watch
export function watch(source, cb, options) {
return doWatch(source, cb, options)
}
// 具体实现
export function doWatch(source, cb, options = {}) {
let getter
// 根据 watch 传的source类型设置 getter 函数
if (isRef(source)) {
getter = () => source.value
} else if (isReactive(source)) {
getter = () => source
options.deep = true
} else if (isFunction(source)) {
if (cb) {
// watch
getter = () => source()
} else {
// watchEffect
getter = () => {
if (cleanup) {
cleanup()
}
source(onCleanup)
}
}
}
let cleanup
const onCleanup = (fn) => {
cleanup = effect.onStop = () => {
fn()
}
}
// 初始化时的oldValue, 借助{}和任何值都不全等的原理,用于新旧值对比
let oldValue = INITIAL_WATCHER_VALUE
// 更新时执行
const job = () => {
if (!effect.active) return;
if (cb) {
// watch
// 获取值,并进行依赖收集
const newValue = effect.run()
// 设置了deep或者值不同时,调用 cb
if (options.deep || hasChanged(newValue, oldValue)) {
if (cleanup) {
cleanup()
}
cb(
newValue,
// 一开始oldValue是undefined
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onCleanup
)
}
} else {
// watchEffect
effect.run()
}
}
const effect = new ReactiveEffect(getter, job)
if (cb) {
// watch
// 设置 immediate 即首次执行一次
if (options.immediate) {
job()
} else {
// 首次获取一次值,并进行依赖收集
oldValue = effect.run()
}
} else {
// watchEffect
effect.run()
}
const unwatch = () => {
effect.stop()
}
return unwatch
}
watch
的原理就是,首次获取一个值,并进行依赖收集,收集当前的 effect
, 当值发现变动时,触发该 effect
更新,重新进行一次值计算,然后新旧值对比,发生变化则调用用户传的 cb
。
effectScope
最后讲讲effectScope副作用作用域
这个vue3新增的概念,相信很多人对这个不太熟悉,其实原理并不复杂。
官网介绍: effectScope
创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。
示例:
const scope = effectScope()
const counter = ref(1)
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理掉当前作用域内的所有 effect
scope.stop()
说白了就是,在 effectScope
的run里定义的 effect
都可以被effectScope
收集并统一注销。
配套的还有onScopeDispose() 方法,用于在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
示例:
function onScopeDispose(fn: () => void): void
实现:
// src/recativity/effectScope.js
// 面向用户的 effectScope 方法
export function effectScope() {
return new EffectScope()
}
let activeEffectScope // 全局变量用来存储当前正在运行的 effectScope
class EffectScope {
_active = true // 当前effectScope是否激活
effects = [] // 存储当前effectScope收集到的effect
cleanups = [] // 通过onScopeDispose注册的,作用域停止时回调函数存放处
constructor() {}
// 对外获取激活状态用 active
get active() {
return this._active
}
// 调实例的run方法时,做收集effect动作
run(fn) {
if (this._active) {
// 存一下外层的effectScope
const lastEffectScope = activeEffectScope
try {
// 设置当前effectScope 为 全局的activeEffectScope
activeEffectScope = this
// 执行fn, 里面就会创建 effect, 创建的同时通过全局变量activeEffectScope将创建的effect收集起来, 所以这里需要 改下 effect 的创建逻辑,下面处理
return fn()
} finally {
// 将外层的effectScope赋回去
activeEffectScope = lastEffectScope
}
}
}
stop() {
if (this._active) {
// 依次销毁所收集的 effect
for (let i = 0; i < this.effects.length; i++) {
this.effects[i].stop()
}
// 依次调用销毁时的回调
for (let i = 0; i < this.cleanups.length; i++) {
this.cleanups[i]()
}
// 设置当前作用域为非激活状态
this._active = false
}
}
}
// 收集effect到effectScope的具体方法
export function recordEffectScope(effect, scope) {
// 未传scope即是当前正在运行的 effectScope
if (!scope) scope = activeEffectScope
if (scope && scope.active) {
// 收集
scope.effects.push(effect)
}
}
// 对外暴露的方法
export function getCurrentScope() {
return activeEffectScope
}
// 给当前正在运行的 effectScope 添加,销毁时的回调
export function onScopeDispose(fn) {
if (activeEffectScope) {
activeEffectScope.cleanups.push(fn)
} else {
// 没有正在运行的 effectScope则提示
console.warn(`onScopeDispose() is called when there is no active effect scope to be associated with.`)
}
}
// src/reactivity/effect.js
export class ReactiveEffect {
// 省略其他代码
constructor(fn, scheduler, scope) {
// 创建effect时,将该effect收集到 scope 中,没传 scope 即 当前正在运行的effectScope 即 activeEffectSccope
recordEffectScope(this, scope)
}
大致上就是,调用effectScope
的run
方法时,现将当前effectScope
设置到全局变量activeEffectScope
,然后运行 run
上传进来的fn
,fn
中创建的effect
均在初始化时,就被收集到了activeEffectScope
中也就是现在运行run
的effectScope
。最后调用其stop
则可以根据收集到的effect
一次性注销掉。
其实源码中effectScope
还有是否是独立作用域的概念,如果是非独立的作用域,则一销毁大家收集的effect
同时销毁,这里就不实现了,有兴趣的可以自行阅读源码。
总结
就像上面说的,vue3的响应式系统相对vue2的,功能更强大的同时,心智负担也更大,想要完全理解也更难,这里我们手写了个简易的响应式系统,还有许多功能未能,包括数组length
重复触发的问题,以及集合类型数据的响应式,has
delete
的监听等等。有兴趣的可以自行阅读源码。
最后附上 代码地址
转载自:https://juejin.cn/post/7192431502720761913