一、目标对象标识
**
类似于渲染系统,vue3.0的响应式系统也有自己的一套flag,用于标记目标对象target(通常是我们传入的数据源)的一些特性
export const enum ReactiveFlags {
skip = '__v_skip',
isReactive = '__v_isReactive',
isReadonly = '__v_isReadonly',
raw = '__v_raw',
reactive = '__v_reactive',
readonly = '__v_readonly'
}
// 为目标对象target,即待转化为响应式对象的源对象做标记
interface Target {
__v_skip?: boolean // 跳过,不对target做响应式处理
__v_isReactive?: boolean // target是响应式的
__v_isReadonly?: boolean // target是只读的
__v_raw?: any // target对应的原始数据源,未经过响应式代理
__v_reactive?: any // target经过响应式代理后的数据源
__v_readonly?: any // target经过响应式代理后的只读数据源
}
二、reactive核心模块
createReactiveObject:
将target转化为响应式对象
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 被转化的target必须是object
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
return target
}
// target已经被转化为响应式对象,响应式对象的__v_readonly、__v_reactive分别挂载转化为
// proxy的只读、非只读的对象
if (
hasOwn(target, isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive)
) {
return isReadonly ? target.__v_readonly : target.__v_reactive
}
// 只有target在可转化类型白名单里才会进行转化,否则直接返回target
// canObserve会校验target的__v_skip是否为true、target类型是否在白名单里、target是否
// 为冻结对象,三折同时满足才会继续转化
if (!canObserve(target)) {
return target
}
// 转化proxy对象,并将转化后的对象挂载到target的__v_readonly(只读模式)或__v_reactive(非只读模式)属性上
const observed = new Proxy(
target,
// Set、WeakSet、Map、WeakMap类型用的collectionHandlers,其他对象类型使用baseHandlers
collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
)
def(
target,
isReadonly ? ReactiveFlags.readonly : ReactiveFlags.reactive,
observed
)
return observed
}
createGetter:
在createReactiveObject方法里创建Proxy对象时会为Proxy设置handlers,createGetter就是创建getter拦截器的。
涉及到Reflect的可以参考下面链接或自行官网查找:
Reflect:Reflect详解
其实中心思想很简单,记住一点就可以:反射机制是为了获取或修改程序运行时信息,ES6将很多object方法统一收容到Reflect上,对外暴露统一的访问渠道,符合程序设计的合理规范性。Reflect搭配Proxy使用非常棒,本身就是为Proxy量身定制的。
说一下receiver这个参数,主要有以下作用:
- Reflect中可以改变访问属性所属trap(如getter、setter…)的this指向
- Proxy中指向触发trap的实际来源
举个例子,当我们访问某个key时,会触发Proxy对应的拦截器(handler),而receiver正是指向访问属性并触发handler的真正源头对象,我们触发handler自然是通过Proxy实例对象,此时分两种情况:
- 直接访问Proxy实例中的key;
- 源头对象(source)原型链上有Proxy实例,我们访问某个key如果source对象本身没有,会顺着原型链往下游找,如果遇到Proxy实例,就会触发对应的handler,那么此时handler中的receiver就指向source。
function createGetter(isReadonly = false, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
// 访问对应标志位的处理逻辑
if (key === ReactiveFlags.isReactive) {
return !isReadonly
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (
key === ReactiveFlags.raw &&
receiver ===
(isReadonly
? (target as any).__v_readonly
: (target as any).__v_reactive)
// receiver指向调用者,这里的判断是为了保证触发拦截handler的是proxy对象本身
// 而非proxy的继承者。触发拦截器的两种途径:1⃣️访问proxy对象本身的属性;2⃣️访问
// 访问对象原型链上有proxy对象的对象的属性,因为查询属性会沿着原型链向下游依次
// 查询,因此同样会触发拦截器
) {
// 通过proxy对象本身访问__v_raw属性,返回target本身,即响应式对象的原始值
return target
}
// 访问Array对象上的方法,vue3.0对数组方法进行了hack,
// 存储在arrayInstrumentations工具集里
const targetIsArray = isArray(target)
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
// 这里在调用indexOf等数组方法时是通过proxy来调用的,因此
// arrayInstrumentations[key]的this一定指向proxy实例
// 也即receiver
return Reflect.get(arrayInstrumentations, key, receiver)
}
// Proxy预返回值
const res = Reflect.get(target, key, receiver)
// key是symbol或访问的是__proto__属性不做依赖收集和递归响应式转化,直接返回结果
if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
return res
}
// 只读target无需收集依赖,因为属性不会变化,因此无法触发setter,也就不会触发依赖更新
if (!isReadonly) {
// 通过track函数将依赖存储到对应的全局仓库中
track(target, TrackOpTypes.GET, key)
}
// 浅转换至将target的第一层值转化为响应式,不做递归转化
if (shallow) {
return res
}
// 访问属性已经是ref对象,保证访问ref属性时得到的是ref对象的value属性值,数组除外
if (isRef(res)) {
// ref unwrapping, only for Objects, not for Arrays.
return targetIsArray ? res : res.value
}
if (isObject(res)) {
// key对应的值如果是对象,需要递归的进行响应式转化,readonly和reactive函数最终
// 调用的都是createReactiveObject函数,只是触发的响应式转化模式不同
// ⚠️此处有一点需要注意,3.0版本对响应式做出了优化。在2.0版本中,响应式转化
// 是在初始化阶段一次性递归转化完成,3.0的处理方式则更加优雅,只有getter拦截到
// 对象时,才会继续向对象的下游做响应式转化,这样的好处就是:只有真正访问到的数据
// 才会做转化,如果我们定义了某个数据,但是实际上并没有使用到它,那么我们不会触发
// getter,自然也就不会继续做响应式转化,这样就做到了“按需转化”
return isReadonly ? readonly(res) /* 只读响应式转化 */ : reactive(res) /* 非只读响应式转化 */
}
return res
}
}
数组方法拦截hack:
响应式系统对数组的两种类别的原生方法进行了hack:
- 遍历查找的方法(indexOf, lastIndexOf, includes)
- 改变数组长度的方法(push, pop, shift, unshift, splice)
hack后的方法统一增加了一个this参数,用来指定trap的上下文,通常也就是数组源对象,因为Reflect.get(arrayInstrumentations, key, receiver)
获取到的对应方法上下文默认是指向arrayInstrumentations,但是我们显然是要操作数组源对象,因此需要传入对应上下文。
原因:
- 查找遍历相当于将数组中的元素全部访问一遍,满足收集依赖的条件
- 对于改变的数组长度的方法,执行期间会触发get length和set length,因此会触发track -> trigger无限循环
vue-next仓库issues里有对应的测试用例,改变数组长度函数hack也是后期修复这个用例产生的问题才加上的:
const arr = reactive([])
watchEffect(()=>{
arr.push(1)
})
简单解释下watchEffect,watchEffect内部会生成一个effect,这个effect的作用就是为了在响应式数据变化时执行它自己,因此通常就是在回调里get响应式数据,后面修改这个数据时再触发回调执行
。在上面的用例中,回调里push会get length将依赖收集起来,然后会set length,这时会触发刚刚收集的依赖,然后依赖又会重新执行回调,形成track - trigger的死循环。因此要避免访问改变数组长度的方法在执行期间收集依赖
改变数组长度的方法一定会先get length,再set length,举个简单的例子:
const arr = [2, 3, 6, 7, 8]
const proxy = new Proxy(arr, {
get(target, key, receiver) {
console.log('get', key)
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('set', key, value)
return Reflect.set(target, key, value, receiver);
}
})
proxy.push(2)
打印结果如下:
get push
get length
set 3 2
set length 4
因此需要在对应数组方法执行期间禁用掉依赖收集,在执行完再恢复
const arrayInstrumentations: Record<string, Function> = {}
// 查询遍历方法
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
const method = Array.prototype[key] as any
// 由于直接通过Reflect.get(arrayInstrumentations, key, receiver)取到的
// 数组方法this指向arrayInstrumentations,因此函数需要传入正确的上下文this
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
// 去掉响应式wrapper,获取源数组对象
const arr = toRaw(this)
// 按照数组下标收集依赖
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
// 优先调用Array原生方法查找传入值
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// 没有查询到对应的值,有可能是包装后的响应式数据,有wrapper,引用不同所以
// 有可能查不到,尝试去掉wrapper再查询
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
// 改变数组长度的方法
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
// 执行前禁用依赖收集
pauseTracking()
const res = method.apply(this, args)
// 执行后恢复之前track状态
resetTracking()
return res
}
})
track:
track方法是触发getter过程中的做依赖收集的函数,会将收集到的依赖添加到依赖仓库,结构为target -> key -> dep
export function track(target: object, type: TrackOpTypes, key: unknown) {
// activeEffect是当前处于激活状态的effect,也就是触发getter时当前访问的effect,
// 如果没有对应的激活态effect,也就没必要做依赖收集了
if (!shouldTrack || activeEffect === undefined) {
return
}
// targetMap为全局数据响应关系仓库,为Map类型,key为组件数据源,每个数据源对应Map中的
// 一个key,value是一个Set,Set中存放的是数据源中每个key对应的deps,即收集的依赖
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 数据源中每个key对应的依赖仓库
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 添加激活订阅者,deps里存储的effects是收集的订阅者们(依赖),相当于vue2.0中的watcher
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
createSetter:
响应式对象proxy对应的setter handler,用来做更新派发
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
if (!shallow) {
// 通过setter入参的新值获取它的原始值,新传入值可能是响应式数据,如果直接和
// target上的原始值比较是没有任何实际意义的
value = toRaw(value)
// target不是数组,且旧值为ref,新值非ref,直接将ref.value更新为新值
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 这里其实就是为了判断receiver是proxy实例还是原型链上有proxy的对象,只有前者会触发
// 更新派发,防止通过原型链触发拦截器触发更新。
// 这里处理的非常巧妙,看下createGetter代码,在getter handler中做了一层拦截,当访问
// __v_raw属性时,只有receiver本身时proxy实例时才会返回target,即原始目标对象。再看
// 下面的createSetter代码,toRaw会访问receiver的__v_raw属性,从而触发getter
// handler,由于我们已经做了原型链访问拦截,所以在setter里如果receiver位于原型链上,
// 那么是访问不到__v_raw属性的,因此确保了只有receiver本身是proxy实例才触发更新派发
if (target === toRaw(receiver)) {
// 区分是新增属性和固有属性
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
trigger:
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// never been tracked
return
}
const effects = new Set<ReactiveEffect>() // 普通effect集合
const computedRunners = new Set<ReactiveEffect>() // 计算effect集合
// add方法用来将依赖Map中的对应dep Set添加到局部集合中,以便后续触发(run)
// add函数用于过滤出符合条件的依赖,用于批量触发
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
// effect和当前激活的effect引用相同,且shouldTrack = true时不会将
// 遍历的effect推入局部Set,shouldTrack为true表示开启依赖收集模式,
// 该模式下,activeEffect是当前正在收集中的依赖,依赖收集未完成不能
// 派发该依赖的更新
// effect不能和当前激活的effect相同,为了避免effect的无限触发,比如
// effect的getter回调里是foo.value++这种情况,首先foo.value会触发
// Proxy的get拦截器,track收集当前激活的effect,然后
// foo.value = foo.value + 1会触发Proxy的set拦截器,trigger刚刚收集到的
// effect,然后effect执行getter回调,会继续重复相同的过程(同一个effect track
// -> trigger),陷入死循环,因此需要避免掉收集当前激活的effect
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
} else {
// the effect mutated its own dependency during its execution.
// this can be caused by operations like foo.value++
// do not trigger or we end in an infinite loop
}
})
}
}
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
// 清空依赖时,将target对应的依赖map中的依赖全部添加到局部Set,准备trigger
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
// 源对象是数组且trigger的key是length,需要触发length对应的effects,
// 同时,由于触发length时一定是数组长度变化了,当数组长度变短时,需要
// 做已删除数组元素的effects的trigger工作,也就是索引号 >= 数组
// 最新length的元素们所对应的effects,对它们做清除后的批量触发。
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// key不是undefined就会触发依赖局部添加,只不过如果是新增属性对应的dep为空
if (key !== void 0) {
add(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
// 对于新增属性或者删除属性的处理
const isAddOrDelete =
type === TriggerOpTypes.ADD ||
(type === TriggerOpTypes.DELETE && !isArray(target))
// 新增或删除属性,如果是数组,会触发length对应的依赖,如果书普通对象,会触发
// ITERATE_KEY对应的依赖;如果是Map设置属性,会触发ITERATE_KEY
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
// Object新增属性不会触发getter,因此无依赖收集,需要触发ITERATE_KEY收集的依赖
add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
}
// 对于Map新增或删除属性,统一触发MAP_KEY_ITERATE_KEY所对应的依赖
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
// run函数用来批量执行add方法添加过的effect
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
// 非立即执行的effect,即需要使用schedular调度器的effect,比如计算effect
effect.options.scheduler(effect)
} else {
// 立即执行的effect,无需特殊调度处理
effect()
}
}
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}
createInstrumentationGetter(ES6集合、映射):
vue3.0对ES6新增数据结构(我们统称为collection)单独做了一套拦截处理机制,因为Map、Set的访问与修改并不是像普通数据类型那样直接操作的,而是通过对象提供的API,要想对collection类型对象进行响应式处理,那么就必须在这些API上去做手脚。vue3.0的做法简单直接,直接对方法进行hack,通过类似于vue2.0对数组方法的hack,通过侵入原生API达到理想效果。
hack的方法有has、delete、add、clear、get、set、forEach,属性有size,规则类似于普通对象和数组,大致可以分为以下几类:
- 设置(set)、添加(add, set)、删除元素(delete)、清空(clear)时trigger派发更新;
- 遍历(forEach)、检索元素(has)时track收集依赖;
- 获取数据结构尺寸(size),有专门的key来收集依赖
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
// 指定对应版本的重写方法集
const instrumentations = shallow
? shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
// ... 省略无关代码
// 核心处理逻辑,如果访问的是重写的方法,直接食用重写方法,否则正常访问值即可
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
collection各方法hack处理
这里仅介绍可变响应式数据对应trap的hack逻辑,对于只读模式、浅转化模式的响应式数据trap不加赘述,逻辑也简单得多,可以理解为可变数据模式响应式数据的阉割版。
PS:trap入参额外传入this的原因和数组hack一样。
,函数第一个入参都是上下文对象(源对象对应的响应式对象),后面的参数和对应原生API一致
// traps总入口,对collection现有原声API进行了侵入改写
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key)
},
get size() {
return size((this as unknown) as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false)
}
// get
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
// target传入的是Proxy,去wrapper,获取转化前的原始值重新赋值给target
target = (target as any)[ReactiveFlags.RAW]
// 将target源对象和访问的key都去wrapper化(递归一路扒光到底),
// 获取未包装的原始值,因为有可能是经过响应式转化的
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// 包装后的key和原始key均向依赖仓库进行依赖收集(track)
if (key !== rawKey) {
!isReadonly && track(rawTarget, TrackOpTypes.GET, key)
}
!isReadonly && track(rawTarget, TrackOpTypes.GET, rawKey)
const { has } = getProto(rawTarget)
// 获取对应的响应式转换函数
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
// 1. 如果collection源对象有key对应的属性,通过原生get方法取到值,并对改值
// 进行响应式转换,返回转换后的响应式对象(按需转换)
// 2. 如果直接通过key未匹配到对应的属性值,则用去wrapper的key原始值去查找,
// 如果找到的话,其他操作和1中相同
if (has.call(rawTarget, key)) {
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
return wrap(target.get(rawKey))
}
}
// has
// has trap在Set和Map中都有,但是入参形式一致,只是参数意义不同,这里的key
// 如果是Map那就是键值,如果是Set那就是值
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const rawKey = toRaw(key)
// 查找的key做对应的依赖收集,和get类似
if (key !== rawKey) {
!isReadonly && track(rawTarget, TrackOpTypes.HAS, key)
}
!isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)
// 返回查询结果,优先查找key,key匹配不到结果再查询key原始值对应的查找结果
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey)
}
// size
// mutableInstrumentations对size属性做了getter拦截代理
function size(target: IterableCollections, isReadonly = false) {
target = (target as any)[ReactiveFlags.RAW]
// 访问size和数组访问length类似,都有专门的key做依赖收集
// 即ITERATE_KEY
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
// add
function add(this: SetTypes, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
const result = target.add(value)
// Set新增的值做依赖收集,由于Set中无重复值,因此用集合元素值做依赖收集的
// key是没有任何问题的
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, value, value)
}
return result
}
// set
function set(this: MapTypes, key: unknown, value: unknown) {
// 去wrapper,获取value和this上下文对象的原始值
value = toRaw(value)
// target指向collection源对象
const target = toRaw(this)
const { has, get } = getProto(target)
// 判断源对象是否已经存在key对应的属性
// 1. 优先直接查找源对象是否已有key对应的属性
// 2. 如果1步骤查找不到,再查找key对应原始值对应的属性在源对象中是否已经存在
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
// 获取key对应的旧值
const oldValue = get.call(target, key)
// 调用原生set API
const result = target.set(key, value)
// trigger: 触发依赖,新增属性和更改已有属性分开进行trigger,
// 便于记录trigger的来源类型
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
// delete
// delete也是Set、Map共有的API,入参key在Set中是元素值,在Map中是键值
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
// 已有的属性派发依赖
if (hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
// clear
// clear操作清空collection中所有数据,因此需要将源对象中所有key对应的依赖全部触发
function clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
// forEach
// 遍历方法,和size一样,用迭代器专键值ITERATE_KEY来做依赖收集
function createForEach(isReadonly: boolean, isShallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown
) {
const observed = this as any
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
// ITERATE_KEY做依赖收集
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
// 执行原生forEach方法
return target.forEach((value: unknown, key: unknown) => {
// important: make sure the callback is
// 1. invoked with the reactive map as `this` and 3rd arg
// 2. the value received should be a corresponding reactive/readonly.
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
// 迭代器方法
// 迭代方法依赖收集同样是对应特定的key
// 如果仅访问keys,key为 MAP_KEY_ITERATE_KEY
// 否则key为 ITERATE_KEY
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean
) {
return function(
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
// 方法是否是获取键值对
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
// 方法是否仅获取keys
const isKeyOnly = method === 'keys' && targetIsMap
// 直行原生迭代器方法,并获取迭代器对象:
// { next, [Symbol.iterator] }
const innerIterator = target[method](...args)
const wrap = isReadonly ? toReadonly : isShallow ? toShallow : toReactive
// 收集依赖到指定的key
!isReadonly &&
track(
rawTarget,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
)
// 根据原生迭代器的返回值进行hack包装,包装成对应的响应式版本的迭代器,
// 迭代器对象是通过next方法进行元素访问的,
return {
// iterator protocol
next() {
// 当前访问的值和是否迭代完成flag
const { value, done } = innerIterator.next()
// 通过next访问下一个元素(对)时等价于普通数据类型通过get访问对应属性值,
// 在普通数据类型中如果对应访问属性是Object,需要对其进行响应式数据的
// 访问时按需转化。迭代器其实类似,只不过是通过迭代指向下一个访问元素,
// 因此也需要做同样的访问时按需转换。
// 如果done为true说明迭代完成,此时value是undefined,因此没有必要做
// 转换
return done
? { value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// iterable protocol
[Symbol.iterator]() {
return this
}
}
}
}
三、effect模块
effect:
effect相当于vue2.0中的watcher,灵感来源于react的hooks,但vue3.0整体的设计风格是函数式的,融入了composition API将各个功能模块做的更加通用化,拥有很高的灵活性,使用时的约束要少得多,vue2.0主要是面向对象式的写法,很多功能模块都是封装在class中,推崇的主要是option API,虽然这样的好处是看似很多工作都在框架内部完成了,但是灵活性要低得多,而且做大的弊病是代码复用率很低,比如我想在多处复用一份响应式数据源,正常来说就实现起来不太容易。
effect就是vue3.0中的“依赖”(也可以叫“副作用”),通过响应式数据的getter拦截器进行收集,既然是依赖,就应该具有基础的组成成分:执行函数(你想让依赖执行的自定义逻辑,也可以是getter,用于触发响应式数据访问)、全局依赖激活能力(以便拦截器收集依赖)、调度能力(用于调度依赖自身,以在合适的时机执行)
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
// fn本身就是effect函数
if (isEffect(fn)) {
fn = fn.raw // effect.raw是effect的执行函数
}
// 创建一个新的effect函数
const effect = createReactiveEffect(fn, options)
// 执行effect函数
if (!options.lazy) {
// lazy在computedEffect会用到,懒更新,不执行effect,而是暴露给外部,由computed
// 控制流程来控制effect的执行时机。其他情况立即执行effect函数即可
effect()
}
return effect
}
options参数为effect的配置项,结构如下:
export interface ReactiveEffectOptions {
lazy?: boolean // 计算属性懒更新
computed?: boolean // 计算属性
scheduler?: (job: ReactiveEffect) => void // 调度处理
onTrack?: (event: DebuggerEvent) => void // 依赖收集时的钩子
onTrigger?: (event: DebuggerEvent) => void // 更新派发时的钩子
onStop?: () => void // effect被stop时的钩子
}
createReactiveEffect:
创建effect函数,effect函数中的fn是执行函数,和vue2.0 watcher.run()类似,fn函数执行会触发对应target[key]的getter,完成依赖收集,在依赖收集前后采用栈数据结构(effectStack)来做effect的执行调度,保证当前effect的优先级最高,并及时清除已收集依赖的内存。
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(...args)
}
// effectStack是一个全局effect栈,用于effect调度。
if (!effectStack.includes(effect)) {
// 防止effect重复添加,因为如果effect在effect栈中,出栈时
// 一定会轮到它被收集,没必要在push一个相同的。
// 每次执行effect时做当前effect的清理时非常必要的,目的是
// 为了保证当前effect的被持有方(dep)是最新且有效的,因为
// 同一effect通常会多次执行,比如最常见的render effect
// 但是收集它的getter中内容可是不确定的:
// 1. 同一数据源内部会变化,比如删除某个属性
// 2. 有不同的数据源来收集同一effect。
// 最简单的来说,对于已删除的属性,肯定是不能对曾经持有的effect
// 继续持有,因此每次执行effect时同一清理再重新收集是能够保证
// effect和依赖仓库间的正确持有关系
// 说的更直白一些,getter是生产者,setter是消费者,当effect
// 被消费掉后,被清理是保持闭环的关键所在
cleanup(effect)
try {
// 重点说明一下:在setup函数自行期间,会将pauseTracking将依赖收集
// 停掉,防止无意义的依赖注入,因此需要在effect生成时开放track恢复
// 收集,当getter收集当前effect完毕时,将track状态恢复到上次的状态
enableTracking()
// effect压栈
effectStack.push(effect)
// 将当前effect设置为全局激活effect,在getter中会收集activeEffect持有的effect
// activeEffect表示当前依赖收集系统正在处理的effect
activeEffect = effect
// 执行effect的执行函数,比如访问target[key],会触发getter,在getter中
// 将activeEffect收集到依赖仓库。
// fn在render时就是render逻辑,即生成vnode并patch到真实dom那一套流程
return fn(...args)
} finally {
// 此时fn执行完毕,依赖已收集到依赖仓库,因此将当前effect出栈,及时释放内存
effectStack.pop()
resetTracking()
// 将activeEffect恢复到effect栈中最后一个effect,即上次的activeEffect,继续
// 做该effect的收集工作
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++ // effect的uid
effect._isEffect = true // 标记是否是effect
effect.active = true // effect睡否处于激活状态
effect.raw = fn // effect的执行函数
effect.deps = [] // 包含当前effect的dep数组
effect.options = options // effect的配置项,包含配置项及hooks
return effect
}
// 用于停止effect,对于确定已经消费掉的effect进行清理,将effect置为非激活态
// PS: 在watch侦听中stop会作为停止侦听的控制器被透出
export function stop(effect: ReactiveEffect) {
if (effect.active) {
cleanup(effect)
if (effect.options.onStop) {
effect.options.onStop()
}
effect.active = false
}
}
// effect清理
function cleanup(effect: ReactiveEffect) {
// 遍历持有当前effect的所有key对应的依赖仓库,将该effect从依赖仓库
// 中移除
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
四、ref模块核心方法
createRef:
用于创建ref对象,用于将某个值转化为响应式对象,不同于reactive方法的是,reactive只能接受object类型的入参,ref可以接受基本类型和引用类型。比如,我们可以这样创建一个响应式对象:
const state = ref('ref test');
这样生成的state会挂载getter和setter对value属性进行代理拦截,访问value时会通过track函数进行依赖收集,设置value同样会通过track派发更新。
通常在业务中使用时更推荐直接使用ref方法来生成响应式对象,原因之一如下:
- ref可接受基本类型数据和引用类型数据,你可以直接将一个基本类型的数据转化为基于全局依赖仓库托管的响应式对象。
- ref给予createReactive方法封装,当传入引用类型数据源时,ref中会调用createReactive方法对数据源进行响应式转化,因此可以将ref看作createReactive方法的一种扩展。
ref的源码在rc最后几个版本进行了形式上的调整,但是总体思路基本没什么变化,只不过从函数式的实现调整为class
形式的实现,至于原因,应该就是ref这块的整体设计比较纯粹、简单、低耦合,且其trap拦截式的功能很适合抽象成一个class来进行托管,因此这样的调整也是合理的。
不过这里我依然放之前版本的ref代码了,大体上思路没有任何变化。
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
function createRef(rawValue: unknown, shallow = false) {
// 已经是ref对象直接返回
if (isRef(rawValue)) {
return rawValue
}
// shallow开启时,代表是浅转化模式,只对value进行拦截代理,不对value进行递归响应式转化
let value = shallow ? rawValue : convert(rawValue)
const r = {
__v_isRef: true, // 标示是否为ref对象
get value() {
// 依赖收集
track(r, TrackOpTypes.GET, 'value')
return value
},
set value(newVal) {
// 当值发生变化时派发更新,需要比较原始值(raw),因为newVal有可能是proxy响应式
// 对象,比较proxy的原始值具有更强的准确性
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
// 将新设置的值进行对应的响应式转化
value = shallow ? newVal : convert(newVal)
trigger(
r,
TriggerOpTypes.SET,
'value',
__DEV__ ? { newValue: newVal } : void 0
)
}
}
}
return r
}
四、计算属性
计算属性是vue对发布订阅设计模式和响应式系统的一个很妙的实践,源码实现非常简洁,缺实现了很实用的使用效果,在实际开发中一个computed真的能帮你节省很多不必要的代码。
我们先抛开vue源码,如果让我们自己设计一个computed,作为业务使用方,它的伪代码应该是类似这样的:
targetVal = computed(() => {
return val1 + val2;
})
想要达到的效果:***由响应式数据自动计算出的一个target值供业务使用,当计算依赖的响应式数据发生改变时,动态的计算出最新的target。***并且,期待computed函数包装后的同样返回一个响应式数据。
既然是数据发生变化就做某件事,那么大方向上自然而然的就会想到用发布订阅模式来处理这类场景,只是细节如何处理的问题。
知道了想要什么,那我们再看源码就会清晰很多了。
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// computed函数可接受两种类型的入参,1⃣️ComputedGetter,也就是我们传入的
// () => {return a + b}这种回调,这类computed是只读的,如果我们为生成的
// computedRef赋值,会warning;2⃣️包含ComputedGetter和ComputedSetter
// 的options,该模式下允许使用者为computedRef.value赋值,此时的computedRef
// 完全就是一个响应式对象,因此需要在getter中收集依赖
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
let dirty = true
let value: T
let computed: ComputedRef<T>
// 当触发getter时,getter中的响应式数据会触发数据对应的getter拦截器,因此下面的
// runner会被当作依赖收集到各数据对应的dep中
const runner = effect(getter, {
// lazy用于标示effect是懒更新,即effect包装时不立即执行,而是
// 返回外部由外部决定执行时机
lazy: true,
// mark effect as computed so that it gets priority during trigger
// 标示是计算属性,在trigger时保证优先执行
computed: true,
// 调度,trigger中检查effect.options中到有scheduler时,会优先执行scheduler回调
// 通常非立即执行effect可借助scheduler做一些中间态的过度行为,配合effect外部执行
// effect本身的时机,形成一条时序可控的操作流程,比如计算属性的时序如下:
// 1⃣️getter中的响应数据更新,触发trigger派发更新,trigger中会触发effect的
// scheduler回调,将dirty置为true,等待触发computedRef;2⃣️getter中的响应数据更新
// 同时会触发渲染effect的执行,渲染effect立即执行,其getter会触发render函数,重新
// 生成vnode -> patch,期间会访问计算属性,触发computedRef的getter,此时由于
// 已经在scheduler中将dirty置为true,因此computedEffect的getter会重新计算最新值。
scheduler: () => {
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, 'value')
}
}
})
computed = {
__v_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
// dirty控制get新的计算值的时机,只有dirty为true时才会触发新的计算
if (dirty) {
// 执行runner时会触发effect的getter,也就是我们传入的计算函数,
// 计算函数会触发对应响应式数据的getter -> track,执行依赖收集,因此计算依赖
// 的值会将computed对应的effect存入想对象的dep,当依赖的响应数据
// 变化时,触发对应的setter -> trigger,trigger中对computedEffect有
// 相应的处理逻辑,可移步trigger查看~
value = runner()
dirty = false
}
// 如果computed非只读,就需要收集依赖,用于值改变时的trigger
track(computed, TrackOpTypes.GET, 'value')
return value
},
set value(newValue: T) {
setter(newValue)
}
} as any
return computed
}
五、侦听
watch在vue2.0中是广受广大开发者喜爱的一个功能,可以很方便的对指定数据源的某个属性进行侦听,当数据发生变化时在回调里做一些你想做的逻辑,这样能让开发者少写不少重复的代码,所有数据变化触发的业务逻辑都写到watch钩子里就好了,功能用户友好且很好维护。
这么方便使用的功能在vue3.0中自然是依旧保留它原有的优点,但是不止如此,watch也迎来了比较重大的升级,其整体的设计风格也是composition API化,不再像2.0中那样在固定的context中对某一个属性使用,而是设计的更加独立化、自由化、广义化。
可以看下watch的主函数重载部分:
// 数据源组 + 回调 + options
// 数据源source为数组,也就是可以传入一组待侦听的单体数据源
export function watch<
T extends Readonly<Array<WatchSource<unknown> | object>>,
Immediate extends Readonly<boolean> = false
>(
sources: T,
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// 单体数据源 + 回调 + options
export function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// 响应式单体数据源 + 回调 + options
export function watch<
T extends object,
Immediate extends Readonly<boolean> = false
>(
source: T,
cb: WatchCallback<T, Immediate extends true ? (T | undefined) : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
数据源可接受的类型总结:Ref、Reactive Object、函数
// 函数数据源类型
export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void
// Ref、计算Ref、函数类型
export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
核心处理API doWatch:
doWatch要达到的目的是当数据源(source)发生变化时,触发回调(cb)的执行,很明显需要通过发布订阅模式进行实现,也就顺理成章的按照vue3.0的章法来执行:通过getter来收集对应的effect,而effect的实际执行者就是cb
从架构设计层面,watch作为一套侦听机制,必不可少的就是数据源(source)、执行器(cb)、控制器(watch返回的stop回调)、自定义配置项(options),这样的架构设计是完美形成闭环:功能实现层实现了基本的侦听机制,调度控制层可以让你在你想要的时机去自由的控制侦听系统的状态,比如你可以在你的逻辑里去选择合适stop你的侦听
function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
instance = currentInstance
): WatchStopHandle {
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: `,
s,
`A watch source can only be a getter/effect function, a ref, ` +
`a reactive object, or an array of these types.`
)
}
// 根据数据源解析出对应的getter
let getter: () => any
let forceTrigger = false
if (isRef(source)) {
// Ref
getter = () => (source as Ref).value
forceTrigger = !!(source as Ref)._shallow
} else if (isReactive(source)) {
// 响应式数据需要深度侦听
getter = () => source
deep = true
} else if (isArray(source)) {
// 数据源数组,对数组中的各个数据源进行遍历get,以达到依赖收集的目的
getter = () =>
source.map(s => {
if (isRef(s)) {
return s.value
} else if (isReactive(s)) {
// 遍历子代数据收集依赖
return traverse(s)
} else if (isFunction(s)) {
return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
} else {
__DEV__ && warnInvalidSource(s)
}
})
} else if (isFunction(source)) {
// 函数类型的数据源
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
getter = () => {
if (instance && instance.isUnmounted) {
return
}
if (cleanup) {
cleanup()
}
return callWithErrorHandling(
source,
instance,
ErrorCodes.WATCH_CALLBACK,
[onInvalidate]
)
}
}
} else {
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
if (cb && deep) {
// traverse就是将数据源的所有子代属性全部get一遍,达到依赖被深层收集的目的
const baseGetter = getter
getter = () => traverse(baseGetter())
}
let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
cleanup = runner.options.onStop = () => {
callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
}
}
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
if (__NODE_JS__ && isInSSRComponentSetup) {
if (!cb) {
getter()
} else if (immediate) {
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
getter(),
undefined,
onInvalidate
])
}
return NOOP
}
let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
if (!runner.active) {
return
}
if (cb) {
// watch(source, cb)
// 执行effect(runner)通过getter获取到侦听数据的最新值
const newValue = runner()
if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
// 执行侦听回调,并以新值、旧值、无效化函数作为入参
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
// pass undefined as the old value when it's changed for the first time
oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
onInvalidate
])
// 更新旧值
oldValue = newValue
}
} else {
// watchEffect
runner()
}
}
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
// 标记任务是否允许自我触发,有回调表示允许自我触发
job.allowRecurse = !!cb
let scheduler: ReactiveEffectOptions['scheduler']
// 通过调度配置项决定侦听effect执行时机,可以为同步、延后、前置三种执行时机,通过effect
// 的scheduler选项函数来控制
if (flush === 'sync') {
// 在数据源发生变化时,setter直接同步触发侦听effect对应的scheduler函数执行
scheduler = job
} else if (flush === 'post') {
// effect.scheduler实际为queuePostCb(调度器一节有介绍),也就是说数据源setter触发时
// 会通过queuePostCb将job先推入render effect后置任务队列,在清空render effect主
// 队列后在批量执行后置任务队列中的jobs,达到的效果就是侦听任务在渲染任务全部执行完成
// 后在进行调度批量执行
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
// 效果和post相反,在渲染任务批量执行前进行批量清队执行
scheduler = () => {
if (!instance || instance.isMounted) {
queuePreFlushCb(job)
} else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
job()
}
}
}
// 侦听effect主体,lazy模式,非立即执行
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
// 把侦听effect记录到组件实例的effect栈里
recordInstanceBoundEffect(runner)
// 初次执行
if (cb) {
if (immediate) {
// 立即执行effect对应的job,也就是说会立即执行外部传入的cb回调,
// 同时执行runner effect获取到最新的值作为cb入参,vue会在每次
// 执行
job()
} else {
// 非立即执行侦听,只需要触发effect getter的执行,完成侦听依赖的收集
oldValue = runner()
}
} else if (flush === 'post') {
queuePostRenderEffect(runner, instance && instance.suspense)
} else {
runner()
}
// watch返回执行停止侦听与状态清理的回调
return () => {
// 停止当前的侦听effect
stop(runner)
// 由于当前的runner effect已经处于非激活态,无效化的effect需要
// 从对应的组件实例上移除掉
if (instance) {
remove(instance.effects!, runner)
}
}
}