源码调试
yarn install 安装依赖
yarn dev 只打包了 packages下的 vue 文件,若要全部打包 执行 yarn build
reactvie.js
reactive、readonly、shallowReactive、shallowReadonly
import {
mutableHandlers,
readonlyHandlers,
shallowReactiveHandlers,
shallowReadonlyHandlers
} from './baseHandlers'
// 存储代理过的对象 WeakMap 会自动垃圾回收,不会造成内存泄漏,存储的key只能是对象
export const reactiveMap = new WeakMap<Target, any>()
export const shallowReactiveMap = new WeakMap<Target, any>()
export const readonlyMap = new WeakMap<Target, any>()
export const shallowReadonlyMap = new WeakMap<Target, any>()
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers,
reactiveMap
)
}
export function shallowReactive<T extends object>(target: T): T {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers,
shallowReactiveMap
)
}
export function readonly<T extends object>(
target: T
): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers,
readonlyMap
)
}
export function shallowReadonly<T extends object>(
target: T
): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
return createReactiveObject(
target,
true,
shallowReadonlyHandlers,
shallowReadonlyCollectionHandlers,
shallowReadonlyMap
)
}
这四个方法要根据是不是仅读,是不是深度来实现的核心功能 proxy 代理,所以就通过函数柯里化的方式,让 createReactiveObject 根据不同的传参实现最核心的数据拦截。
// 底层 new Proxy() 最核心的需要拦截 数据的读取和数据的修改
function createReactiveObject(
target: Target, // 响应式目标数据
isReadonly: boolean, // 是不是仅读的
baseHandlers: ProxyHandler<any>, // proxy 处理函数
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any> // 存储目标对象和代理后的对象映射关系
) {
// 如果目标不是对象就没法拦截 直接返回就行
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 如果这个对象被代理过了 就不要再代理了
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 最核心的 proxy 代理
const proxy = new Proxy(
target,
baseHandlers
)
// 第一次代理将目标对象 和代理后的对象存起来
proxyMap.set(target, proxy)
return proxy
}
首先判断目标是否对象,如果不是就不用代理了,直接返回。
其次,判断这个对象是否已经被代理过了,代理过就不用再代理了。
如果没有代理就通过 newProxy 实现代理,并返回代理后的对象。
最后将目标对象和代理后的对象存起来。
baseHandlers 是 代理处理函数,接下来我们就看看具体怎么根据不同的参数进行处理的。
这四个handler 都有 get 和 set 方法,所以抽离出来。
是不是仅读的,仅读的set时会报错
是不是深度代理
// TODO: 四种getter
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true) // 仅第一层不能修改,第二次层可以修改
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)
下面是最核心的 get 和 set 的实现方法,触发 get 的时候要 收集依赖, 触发set 的时候要 更新依赖
// TODO: 拦截获取功能 是不是仅读的, 是不是 浅的 proxy + Reflect
// Reflect 方法具备返回值设置成功过会返回 true ,失败返回 false
// 会依赖收集,等数据变化了就更新对应的视图 track
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) { // target 原对象,key target 的属性, receiver 代理对象
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
// Reflect 取值 target[key]
const res = Reflect.get(target, key, receiver)
// 不是仅读就收集依赖
if (!isReadonly) {
// 仅读的属性不变,不用收集依赖,不是仅读的才会依赖收集
track(target, TrackOpTypes.GET, key) // 监控哪个对象里的那个属性 TrackOpTypes.GET 操作标识,可能还有 HAS,表示对这个对象做什么操作的时候给他收集起来
}
// 浅代理 取出第一层 直接返回
if (shallow) {
return res
}
// 嵌套对象 readonly为true就用 readonly包裹 ,如果不是就递归用 reactive包裹
// vue2 是一上来就递归,vue3 是当取值的时候会进行代理 懒代理
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
vue2 中在 取值的时候会触发依赖收集,vue3也是在get中进行依赖收集。
依赖收集的前提是这个这个api不是仅读的,仅读不会发生改变就不进行依赖收集。
不是仅读就会走 依赖收集。
如果是浅代理,就从target中取出第一层并直接返回,如果是嵌套对象就再判断他是否是readOnly,是的话就用 readOnly 包裹一层,不是的话就递归调用 reactive 包裹。
依赖收集
effect 是响应式包中的一个方法,默认页面刷新会执行一次,在执行这个 effect 方法的时候会 走get 方法获取响应式数据的属性值,所以可以将 effect 和 这个对象属性关联起来,当对象属性发生变化后会重新执行 effect 方法。这个属性跟vue2 的 update 方法很相似,但是不同的是 updata 任何属性发生变化都会重新渲染,而 effect 是只有依赖属性发生变化才会重新渲染。
// effect 变成响应式的effect,可以做到数据变化重新执行
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
// 例子:
// effect( () => {
// app.innerHTML = 'xxx',
// })
const effect = createReactiveEffect(fn, options)
// 默认的 effect 会先执行一次 如果有lazy 就不执行
if (!options.lazy) {
effect() // 响应式的effect默认执行一次
}
return effect
}
// effect 标识,用于区分effect,effect 相当于 vue2 的 watcher
let uid = 0
// 1.函数调用栈
// effect( () => { effect1
// state.name,
// effect(()=>{ effect2
// state.age,
// })
// state.address, effect2
// })
// 2.死循环
// effect(() => {
// state.age ++
// })
function createReactiveEffect<T = any>(
fn: () => T, // fn 执行时会取值,会走 get 方法,就会依赖收集
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return fn()
}
// 判断当前effect 是否在 栈中,避免死循环的情况
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect) // 执行这个effect的时候入栈
activeEffect = effect
return fn() // 函数执行完了
} finally { // 用 finally 是希望函数执行异常了还能正确的将当前 effect pop 出去
effectStack.pop() // 执行完了出战
resetTracking()
activeEffect = effectStack[effectStack.length - 1] // 当前的effect 永远指向栈中的最后一个effect
}
}
} as ReactiveEffect
effect.id = uid++ // 每创建一个effect 就制作一个 effect 标识
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true // 用于标识这个是响应式effect
effect.active = true
effect.raw = fn // 响应式effect 对应的原函数
effect.deps = []
effect.options = options // 属性
return effect
}
effect 标识用于组件更新,更新哪个effect,
响应式effect
effect原函数
调用 effect 的时候会执行 effect 传递过来的参数 fn(),执行这个 fn 函数的时候会走 get 方法取值,这个时候就可以将 effect 和 依赖的属性关联起来,等 依赖的属性再次发生变化了就可以再次执行 effect 函数更新视图。
在上面的 createGetter 方法中有个 track 函数就是用来收集依赖的。一会儿看这块的源码。
我们可以看到有activeEffect和effectStack,activeEffect 用于标记当前执行的是哪个 effect 函数,那为什么要整一个函数调用栈?有一种情况如下:
effect( () => { effect1
state.name,
effect(()=>{ effect2
state.age,
})
state.address, effect2
})
当 effect2 执行完之后当前 activeEffect 还是等于 effect2,state.address 的取值就会变成 effect,这样就乱了。所以用一个函数调用栈,执行完就弹出,activeEffect 永远是 栈中的最后一个。
每次执行入栈,执行完出栈。
// 让对象中的某个属性收集他对应的 effect 函数
// weakMap key => {name: 'hhh', age: 12}, value(是一个map,key:name,value:[effect]) =>{name: set[effect, effect]} 一个属性取几次就会存几个effect effect(()=>{state.name}) effect(()=>{state.name})
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 只收集在 effect 使用的属性
if (!shouldTrack || activeEffect === undefined) {
return
}
// 判断对象是否有
let depsMap = targetMap.get(target)
if (!depsMap) { // 没有对象的话创建映射表
targetMap.set(target, (depsMap = new Map()))
}
// 判断对象的key是否存在
let dep = depsMap.get(key)
if (!dep) { //对象的key没有的话创建映射表
depsMap.set(key, (dep = new Set()))
}
// 对象的 key的 effect 是否在存在,不存在就放进去
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
定义一个weakmap,key 就是响应式对象,value 可以也是一个map,这个map 的key 就是 响应式对象的属性,value就是他对应的 effect 函数,因为一个 属性可能对应好几个 effect 函数,所以 这个value 是一个 Set,set 的好处是可以去重。
{
{name: ‘hhh’, age: 12}: {name:set[effect, effect]}
}
activeEffect 是当前执行的effect,vue3 和 vue2 都用一个全局变量标识当前effect。
只收集在effect 中用到的属性,没用到的就不收集。
用 set 存放 effect 的好处是可以去重,例如:
effect(()=>{
state.name
})
effect(()=>{
state.name
})
这种情况 同一个属性就会收集两次effect。使用set 可以去重。
接下来就是当数据更新时通知对应属性的 effect 执行。
触发更新
vue2 无法监控更改数组索引,无法监控数组长度变化。
更新值 会走 set ,更新包括新增和修改。
对象区分新增属性和修改属性的方法就是看老值有没有,有就是 修改,没有就是新增。
数组区分新增和修改:
修改索引就看这个索引和length哪个大,如果索引大于length就是新增,如果索引小于length就是修改,判断索引的方法就是看这个 key 是否是字符串类型的数字。
// 判断是否是一个数字类型key
exportconstisIntegerKey = (key: unknown) =>
isString(key) &&
‘’ + parseInt(key, 10) === key
parseInt
parseInt转数字成整型,+ ‘’成字符串,如果不是字符串数字,parseInt 转完之后就是NaN,+ ‘’,之后和原来的不一样,就证明不是索引 。
新增 调用 trigger(), 触发类型 ADD,对应的key,value,修改触发类型是 SET,对应的key,新值,老值。
// TODO: 拦截设置功能 当数据更新时 通知对应属性的 effect 重新执行
function createSetter(shallow = false) {
// 区分新增的还是修改的 vue2 里无法监控更改索引,无法监控数组长度变化
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key] // 获取老值 如果老值存在就是修改,如果老值不存在就是新增
if (!shallow) {
value = toRaw(value)
oldValue = toRaw(oldValue)
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 =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length // 小于长度就是有,修改,大于长度就是新增的
: 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) { // 不存在就是新增
trigger(target, TriggerOpTypes.ADD, key, value) // 触发更新
} else if (hasChanged(value, oldValue)) { // 存在且新旧值不一样就是修改
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
触发更新的本质是让对应的 effect 函数执行,如果这个属性没有收集依赖,就不需要做任何事,直接返回即可。
找到这个属性对应的所有 effect 函数,存到一个新的集合中,然后一起触发更新。
修改数组的长度可能也会涉及到 索引的修改,如下:
effect(() =>{
return state.arr[2]
})
setTimeout(() =>{
state.arr.length = 1;
})
// 触发更新
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
// 看看这个属性有没有被收集过 effect,没收集过就不需要做任何操作
if (!depsMap) {
// never been tracked
return
}
// 将所有要执行的effect全部存在到一个新的集合中,最终一起执行
const effects = new Set<ReactiveEffect>() // 这里对 effect 去重 effect(()=>{dom.innerHTML = state.arr[2] + state.arr.length}) 这种情况会收集一次effect
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
// 这种情况会执行两次 effect
// setTimeout(() => {
// state.arr.length = 1;
// state.arr.length = 3;
// })
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && (target)) { // 1. 看修改的是不是数组长度 改长度影响比较大
depsMap.forEach((dep, key) => {
// 如果更改的长度小于收集的索引,那么这个索引也需要触发 effect 更新
if (key === 'length' || key >= (newValue as number)) { // state.arr[2] state.arr.length = 1 2 >1
add(dep)
}
})
} else { // 改的不是数组了 可能是对象
// schedule runs for SET | ADD | DELETE
if (key !== void 0) { // 修改对象 新增不会走这儿
add(depsMap.get(key))
}
// 执行收集的 effect
// effects.forEach((effect) => effect())
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD: // 如果修改的是数组的索引 state.arr[100] 索引大于数组的长度 那么就是新增 传过来的 type 就是add
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)
}