})
p.toString() // get: toString
// get: Symbol(Symbol.toStringTag)
p.proto // get: proto
从 p.toString()
的执行结果来看,它会触发两次 get,一次是我们想要的,一次是我们不想要的(我还没搞明白为什么会有 Symbol(Symbol.toStringTag)
,如果有网友知道,请在评论区留言)。所以就有了这个判断: builtInSymbols.has(key)
为 true
就直接返回,防止重复收集依赖。
再看 p.__proto__
的执行结果,也触发了一次 get 操作。一般来说,没有场景需要单独访问原型,访问原型都是为了访问原型上的方法,例如 p.__proto__.toString()
这样使用,所以 key 为 __proto__
的时候也要跳过,不收集依赖。
set
const set = /#PURE/ createSetter()
// 参考文档《Vue3 中的数据侦测》——https://juejin.im/post/6844903957807169549#heading-10
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) {
value = toRaw(value)
// 如果原来的值是 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
if (target === toRaw(receiver)) {
if (!hadKey) {
// 如果 target 没有 key,就代表是新增操作,需要触发依赖
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 如果新旧值不相等,才触发依赖
// 什么时候会有新旧值相等的情况?例如监听一个数组,执行 push 操作,会触发多次 setter
// 第一次 setter 是新加的值 第二次是由于新加的值导致 length 改变
// 但由于 length 也是自身属性,所以 value === oldValue
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
set()
的函数处理逻辑反而没那么难,看注释即可。track()
和 trigger()
将放在下面和 effect.ts 文件一起讲解。
deleteProperty、has、ownKeys
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
// 如果删除结果为 true 并且 target 拥有这个 key 就触发依赖
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
}
function ownKeys(target: object): (string | number | symbol)[] {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
这三个函数比较简单,看代码即可。
effect.ts 文件
等把 effect.ts 文件讲解完,响应式模块基本上差不多结束了。
effect()
effect()
主要和响应式的对象结合使用。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
// 如果已经是 effect 函数,取得原来的 fn
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
// 如果 lazy 为 false,马上执行一次
// 计算属性的 lazy 为 true
if (!options.lazy) {
effect()
}
return effect
}
真正创建 effect 的是 createReactiveEffect()
函数。
let uid = 0
function createReactiveEffect<T = any>(
fn: (…args: any[]) => T,
options: ReactiveEffectOptions
): ReactiveEffect {
// reactiveEffect() 返回一个新的 effect,这个新的 effect 执行后
// 会将自己设为 activeEffect,然后再执行 fn 函数,如果在 fn 函数里对响应式属性进行读取
// 会触发响应式属性 get 操作,从而收集依赖,而收集的这个依赖函数就是 activeEffect
const effect = function reactiveEffect(…args: unknown[]): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn(…args)
}
// 为了避免递归循环,所以要检测一下
if (!effectStack.includes(effect)) {
// 清空依赖
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn(…args)
} finally {
// track 将依赖函数 activeEffect 添加到对应的 dep 中,然后在 finally 中将 activeEffect
// 重置为上一个 effect 的值
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true // 用于判断当前 effect 是否激活,有一个 stop() 来将它设为 false
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
其中 cleanup(effect)
的作用是让 effect 关联下的所有 dep 实例清空 effect,即清除这个依赖函数。
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
从代码中可以看出来,真正的依赖函数是 activeEffect。执行 track()
收集的依赖就是 activeEffect。趁热打铁,现在我们再来看一下 track()
和 trigger()
函数。
track()
// 依赖收集
export function track(target: object, type: TrackOpTypes, key: unknown) {
// activeEffect 为空,代表没有依赖,直接返回
if (!shouldTrack || activeEffect === undefined) {
return
}
// targetMap 依赖管理中心,用于收集依赖和触发依赖
let depsMap = targetMap.get(target)
// targetMap 为每个 target 建立一个 map
// 每个 target 的 key 对应着一个 dep
// 然后用 dep 来收集依赖函数,当监听的 key 值发生变化时,触发 dep 中的依赖函数
// 类似于这样
// targetMap(weakmap) = {
// target1(map): {
// key1(dep): (fn1,fn2,fn3…)
// key2(dep): (fn1,fn2,fn3…)
// },
// target2(map): {
// key1(dep): (fn1,fn2,fn3…)
// key2(dep): (fn1,fn2,fn3…)
// },
// }
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
// 开发环境下会触发 onTrack 事件
if (DEV && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
targetMap 是一个 WeakMap
实例。
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
弱引用是什么意思呢?
let obj = { a: 1 }
const map = new WeakMap()
map.set(obj, ‘测试’)
obj = null
当 obj 置为空后,对于 { a: 1 }
的引用已经为零了,下一次垃圾回收时就会把 weakmap 中的对象回收。
但如果把 weakmap 换成 map 数据结构,即使把 obj 置空,{ a: 1 }
依然不会被回收,因为 map 数据结构是强引用,它现在还被 map 引用着。
trigger()
// 触发依赖
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set
) {
const depsMap = targetMap.get(target)
// 如果没有收集过依赖,直接返回
if (!depsMap) {
// never been tracked
return
}
// 对收集的依赖进行分类,分为普通的依赖或计算属性依赖
// effects 收集的是普通的依赖 computedRunners 收集的是计算属性的依赖
// 两个队列都是 set 结构,为了避免重复收集依赖
const effects = new Set()
const computedRunners = new Set()
const add = (effectsToAdd: Set | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
// effect !== activeEffect 避免重复收集依赖
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
}
})
}
}
// 在值被清空前,往相应的队列添加 target 所有的依赖
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === ‘length’ && isArray(target)) { // 当数组的 length 属性变化时触发
depsMap.forEach((dep, key) => {
if (key === ‘length’ || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
// 如果不符合以上两个 if 条件,并且 key !== undefined,往相应的队列添加依赖
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))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? ‘length’ : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
const run = (effect: ReactiveEffect) => {
if (DEV && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
// 如果 scheduler 存在则调用 scheduler,计算属性拥有 scheduler
effect.options.scheduler(effect)
} else {
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)
}
对依赖函数进行分类后,需要先运行计算属性的依赖,因为其他普通的依赖函数可能包含了计算属性。先执行计算属性的依赖能保证普通依赖执行时能得到最新的计算属性的值。
track() 和 trigger() 中的 type 有什么用?
这个 type 取值范围就定义在 operations.ts
文件中:
// track 的类型
export const enum TrackOpTypes {
GET = ‘get’, // get 操作
HAS = ‘has’, // has 操作
ITERATE = ‘iterate’ // ownKeys 操作
}
// trigger 的类型
export const enum TriggerOpTypes {
SET = ‘set’, // 设置操作,将旧值设置为新值
ADD = ‘add’, // 新增操作,添加一个新的值 例如给对象新增一个值 数组的 push 操作
DELETE = ‘delete’, // 删除操作 例如对象的 delete 操作,数组的 pop 操作
CLEAR = ‘clear’ // 用于 Map 和 Set 的 clear 操作。
}
type 主要用于标识 track()
和 trigger()
的类型。
trigger() 中的连续判断代码
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))
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? ‘length’ : ITERATE_KEY))
}
if (isAddOrDelete && target instanceof Map) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
在 trigger()
中有这么一段连续判断的代码,它们作用是什么呢?其实它们是用于判断数组/集合这种数据结构比较特别的操作。看个示例:
let dummy
const counter = reactive([])
effect(() => (dummy = counter.join()))
counter.push(1)
effect(() => (dummy = counter.join()))
生成一个依赖,并且自执行一次。在执行函数里的代码 counter.join()
时,会访问数组的多个属性,分别是 join
和 length
,同时触发 track()
收集依赖。也就是说,数组的 join
length
属性都收集了一个依赖。
当执行 counter.push(1)
这段代码时,实际上是将数组的索引 0 对应的值设为 1。这一点,可以通过打 debugger 从上下文环境看出来,其中 key 为 0,即数组的索引,值为 1。
设置值后,由于是新增操作,执行 trigger(target, TriggerOpTypes.ADD, key, value)
。但由上文可知,只有数组的 key 为 join
length
时,才有依赖,key 为 0 是没有依赖的。
从上面两个图可以看出来,只有 join
length
属性才有对应的依赖。
这个时候,trigger()
的一连串 if 语句就起作用了,其中有一个 if 语句是这样的:
if (
isAddOrDelete ||
(type === TriggerOpTypes.SET && target instanceof Map)
) {
add(depsMap.get(isArray(target) ? ‘length’ : ITERATE_KEY))
}
如果 target 是一个数组,就添加 length
属性对应的依赖到队列中。也就是说 key 为 0 的情况下使用 length
对应的依赖。
另外,还有一个巧妙的地方。待执行依赖的队列是一个 set 数据结构。如果 key 为 0 有对应的依赖,同时 length
也有对应的依赖,就会添加两次依赖,但由于队列是 set,具有自动去重的效果,避免了重复执行。
示例
仅看代码和文字,是很难理解响应式数据和 track()
trigger()
是怎么配合的。所以我们要配合示例来理解:
let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy == 0)
counter.num = 7
console.log(dummy == 7)
上述代码执行过程如下:
1.对 { num: 0 }
进行监听,返回一个 proxy 实例,即 counter。2.effect(fn)
创建一个依赖,并且在创建时会执行一次 fn
。3.fn()
读取 num 的值,并赋值给 dummy。4.读取属性这个操作会触发 proxy 的属性读取拦截操作,在拦截操作里会去收集依赖,这个依赖是步骤 2 产生的。5.counter.num = 7
这个操作会触发 proxy 的属性设置拦截操作,在这个拦截操作里,除了把新的值返回,还会触发刚才收集的依赖。在这个依赖里把 counter.num 赋值给 dummy(num 的值已经变为 7)。
用图来表示,大概这样的:
collectionHandlers.ts 文件
collectionHandlers.ts 文件包含了 Map
WeakMap
Set
WeakSet
的处理器对象,分别对应完全响应式的 proxy 实例、浅层响应的 proxy 实例、只读 proxy 实例。这里只讲解对应完全响应式的 proxy 实例的处理器对象:
export const mutableCollectionHandlers: ProxyHandler = {
get: createInstrumentationGetter(false, false)
}
为什么只监听 get 操作,set has 等操作呢?不着急,先看一个示例:
const p = new Proxy(new Map(), {
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)
}
})
p.set(‘ab’, 100) // Uncaught TypeError: Method Map.prototype.set called on incompatible receiver [object Object]
运行上面的代码会报错。其实这和 Map Set 的内部实现有关,必须通过 this 才能访问它们的数据。但是通过 Reflect 反射的时候,target 内部的 this 其实是指向 proxy 实例的,所以就不难理解为什么会报错了。
那怎么解决这个问题?通过源码可以发现,在 Vue3.0 中是通过代理的方式来实现对 Map Set 等数据结构监听的:
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
const instrumentations = shallow
-
? shallowInstrumentations
- isReadonly ? readonlyInstrumentations
- mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
// 这三个 if 判断和 baseHandlers 的处理方式一样
if (key === ReactiveFlags.isReactive) {
return !isReadonly
} else if (key === ReactiveFlags.isReadonly) {
return isReadonly
} else if (key === ReactiveFlags.raw) {
return target
}
return Reflect.get(
hasOwn(instrumentations, key) && key in target
-
? instrumentations
- target,
key,
receiver
)
}
}
把最后一行代码简化一下:
target = hasOwn(instrumentations, key) && key in target? instrumentations : target
return Reflect.get(target, key, receiver);
其中 instrumentations 的内容是:
const mutableInstrumentations: Record<string, Function> = {
get(this: MapTypes, key: unknown) {
return get(this, key, toReactive)
},
get size() {
return size((this as unknown) as IterableCollections)
},
has,
add,
set,
delete: deleteEntry,
clear,
forEach: createForEach(false, false)
}
从代码可以看到,原来真正的处理器对象是 mutableInstrumentations。现在再看一个示例:
const proxy = reactive(new Map())
proxy.set(‘key’, 100)
生成 proxy 实例后,执行 proxy.set('key', 100)
。proxy.set
这个操作会触发 proxy 的属性读取拦截操作。
打断点可以看到,此时的 key 为 set
。拦截了 set
操作后,调用 Reflect.get(target, key, receiver)
,这个时候的 target 已经不是原来的 target 了,而是 mutableInstrumentations 对象。也就是说,最终执行的是 mutableInstrumentations.set()
。
接下来再看看 mutableInstrumentations 的各个处理器逻辑。
get
// 如果 value 是对象,则返回一个响应式对象(reactive(value)
),否则直接返回 value。
const toReactive = (value: T): T =>
isObject(value) ? reactive(value) : value
get(this: MapTypes, key: unknown) {
// this 指向 proxy
return get(this, key, toReactive)
}
function get(
target: MapTypes,
key: unknown,
wrap: typeof toReactive | typeof toReadonly | typeof toShallow
) {
target = toRaw(target)
const rawKey = toRaw(key)
// 如果 key 是响应式的,额外收集一次依赖
if (key !== rawKey) {
track(target, TrackOpTypes.GET, key)
}
track(target, TrackOpTypes.GET, rawKey)
// 使用 target 原型上的方法
const { has, get } = getProto(target)
// 原始 key 和响应式的 key 都试一遍
if (has.call(target, key)) {
// 读取的值要使用包装函数处理一下
return wrap(get.call(target, key))
} else if (has.call(target, rawKey)) {
return wrap(get.call(target, rawKey))
}
}
get 的处理逻辑很简单,拦截 get 之后,调用 get(this, key, toReactive)
。
set
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
// 取得原始数据
const target = toRaw(this)
// 使用 target 原型上的方法
const { has, get, set } = 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.call(target, key)
const result = set.call(target, key, value)
// 防止重复触发依赖,如果 key 已存在就不触发依赖
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 如果新旧值相等,也不会触发依赖
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return result
}
set 的处理逻辑也较为简单,配合注释一目了然。
还有剩下的 has
add
delete
等方法就不讲解了,代码行数比较少,逻辑也很简单,建议自行阅读。
ref.ts 文件
const convert = (val: T): T =>
isObject(val) ? reactive(val) : val
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
// 如果已经是 ref 对象了,直接返回原值
if (isRef(rawValue)) {
return rawValue
}
// 如果不是浅层响应并且 rawValue 是个对象,调用 reactive(rawValue)
let value = shallow ? rawValue : convert(rawValue)
const r = {
__v_isRef: true, // 用于标识这是一个 ref 对象,防止重复监听 ref 对象
get value() {
// 读取值时收集依赖
track(r, TrackOpTypes.GET, ‘value’)
return value
},
set value(newVal) {
if (hasChanged(toRaw(newVal), rawValue)) {
rawValue = newVal
value = shallow ? newVal : convert(newVal)
// 设置值时触发依赖
trigger(
r,
TriggerOpTypes.SET,
‘value’,
DEV ? { newValue: newVal } : void 0
)
}
}
}
return r
}
在 Vue2.x 中,基本数值类型是不能监听的。但在 Vue3.0 中通过 ref()
可以实现这一效果。
const r = ref(0)
effect(() => console.log(r.value)) // 打印 0
r.value++ // 打印 1
ref()
会把 0 转成一个 ref 对象。如果给 ref(value)
传的值是个对象,在函数内部会调用 reactive(value)
将其转为 proxy 实例。
computed.ts 文件
export function computed(
options: WritableComputedOptions
): WritableComputedRef
export function computed(
getterOrOptions: ComputedGetter | WritableComputedOptions
) {
let getter: ComputedGetter
let setter: ComputedSetter
// 如果 getterOrOptions 是个函数,则是不可被配置的,setter 设为空函数
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = DEV
? () => {
console.warn(‘Write operation failed: computed value is readonly’)
-
}
- NOOP
} else {
// 如果是个对象,则可读可写
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// dirty 用于判断计算属性依赖的响应式属性有没有被改变
let dirty = true
let value: T
let computed: ComputedRef
const runner = effect(getter, {
lazy: true, // lazy 为 true,生成的 effect 不会马上执行
// mark effect as computed so that it gets priority during trigger
computed: true,
scheduler: () => { // 调度器
// trigger 时,计算属性执行的是 effect.options.scheduler(effect) 而不是 effect()
if (!dirty) {
dirty = true
trigger(computed, TriggerOpTypes.SET, ‘value’)
}
}
})
computed = {
__v_isRef: true,
// expose effect so computed can be stopped
effect: runner,
get value() {
if (dirty) {
value = runner()
dirty = false
前端框架
前端框架太多了,真的学不动了,别慌,其实对于前端的三大马车,Angular、React、Vue 只要把其中一种框架学明白,底层原理实现,其他两个学起来不会很吃力,这也取决于你以后就职的公司要求你会哪一个框架了,当然,会的越多越好,但是往往每个人的时间是有限的,对于自学的学生,或者即将面试找工作的人,当然要选择一门框架深挖原理。
以 Vue 为例,我整理了如下的面试题。