首先温馨提示一下:看源码一定要有耐心,心急吃不了热豆腐,需要一个个模块去深挖和理解,正因为它的不简单,除了会知道这么开发的原因,也更能提高自己的竞争力,当然也会更深入熟悉Typescript。
本文仅仅研究了 reactivity,后面模块逐渐会有输出。此模块也有学习顺序,直接输出我的经验,不让大家走弯路:
reactive.ts 顺腾摸瓜 ——》createReactiveObject ——》 new Proxy ——》 createGetter 和 createSetter ——》 track 和 trigger ——》 effect ——》 createReactiveEffect
响应式原理主要包含4部分:
/* 建立响应式数据 */
function reactice(obj){}
/* 声明响应函数cb(依赖响应式数据) */
function effect(cb){}
/* 依赖收集:建立 数据&cb 映射关系 */
function track(target,key){}
/* 触发更新:根据映射关系,执行cb */
function trigger(target,key){}
总结此文,也结合了别人的一些文章,让我最受启发的是如果看别人源码,不知道函数用法,可以从测试用例去了解,因为测试用例很清楚的知道这个函数要达到什么效果。
此文结构:
- 先整理出调用顺序流程图,并大概进行描述;
- 接着让大家先了解Proxy拦截方式
- 然后分别输出函数源码,并对其分析
- 总结学习心得和收获
对于Vue3源码reactivity部分,还有其他篇幅,因为内容太多,分几部分分析:
如果能读到这篇blog,非常感谢,如果总结不到位的地方希望指正,如果不介意,也麻烦给个赞给予鼓励。
响应式流程图
描述:
-
通过 effect 声明依赖响应式数据的函数cb ( 例如视图渲染函数render函数),并执行cb函数,执行过程中,会触发响应式数据 getter
-
在响应式数据 getter中进行 track依赖收集:建立 数据&cb 的映射关系存储于 targetMap
-
当变更响应式数据时,触发 trigger,并根据 targetMap 找到关联的cb执行
-
映射关系 targetMap 结构:
targetMap: WeakMap{
target:Map{
key: Set[cb1,cb2...]
}
}
一个target对应多个key
Proxy引入的原因
vue2响应式有多个痛点
-
递归,消耗大
-
新增/删除属性,需要额外实现单独的API
-
数组,需要额外实现
-
Map Set Class等数据类型,无法响应式
-
修改语法有限制
而使用ES6的 Proxy
进行数据响应化,解决上述Vue2所有痛点
Proxy可以在目标对象上加一层拦截/代理,外界对目标对象的操作,都会经过这层拦截
相比 Object.defineProperty ,Proxy支持的对象操作十分全面:get、set、has、deleteProperty、ownKeys、defineProperty等,但是也有一些缺点,对数组操作会执行多次set
,举例如下:
const a = new Proxy([1,2], {
get: function(obj, prop) {
console.log('get', obj, prop);
return Reflect.get(obj, prop);
},
set: function(obj, prop, value) {
console.log('set', obj, prop, value);
return Reflect.set(obj, prop, value);
},
});
a.push(1);
get [1,2] push
get [1,2] length
set [1,2] 2 1
set [1,2, 1] length 3
直接调用push插入一新数据,能明显看到getter和setter被调用2次。
当然Vue3源码中肯定处理了这种情况,提前看下Vue3的trigger方法,看下针对数组做的优化。这个是vue在set完成之后触发的依赖更新。
else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
}
由于在一次操作数组的时候会进行多次的set,那么如果每次set都要去更新依赖的话,会造成性能上的浪费,所以在vue3里面只有在set length的时候才会去调用add方法,然后统一执行所有的更新
几个枚举
为了更好看懂源码,先从几个枚类型举开始
// 使用字面值字符串而不是数字,以便更容易检查debugger events
export const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
export const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
reactive
- createReactiveObject
Proxy是对对象的操作,只要访问对象,就会走到Proxy的逻辑中。 Reflect是一个内置的对象,提供拦截js操作的方法。将Object对象一些明显属于语言内容方法(比如Object.defineProperty())放在Reflect对象中。修改某些Object方法的返回结果,让其变得更合理。让Object操作都变成函数行为。
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 场景1:非对象直接返回
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
// 场景2:target已经被代理过,直接返回
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 场景3:target本身是一个Proxy对象,直接返回
// target already has corresponding Proxy
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only a whitelist of value types can be observed.
// 场景4:target不在被观察的白名单中,直接返回
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// baseHandlers用于普通对象;collectionHandlers用于Set/Map/WeakMap/WeakSet
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
// 存储原生=>响应式数据映射表,联系场景2,可知目的:防止reactive已经被reactive的值,导致多次Proxy
proxyMap.set(target, proxy)
return proxy
}
基本类型代理执行baseHandlers,仅仅留下shallowReactiveHandlers,其他同理,它就是Proxy中要执行的get和set拦截,get中执行track收集依赖,set中执行trigger函数
- baseHandler.js
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
const arrayInstrumentations: Record<string, Function> = {}
// 仪器识别敏感阵列方法,以解释可能的reactive
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
const arr = toRaw(this)
// 对数组每个元素执行track,也就是手机依赖
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + '')
}
//我们首先使用原始args运行method(可能是响应性的)
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// 如果不起作用,也就是在数组中找不到args,使用raw值再次运行它。
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods是为了避免长度被track,长度被track在某些情况下导致无限循环(#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
//碰到这些函数暂停track,直接返回结果
pauseTracking()
const res = method.apply(this, args)
resetTracking()
return res
}
})
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver)
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
//也将返回值转换为proxy。我们在这里做isObject检查以避免无效值警告。这里还需要惰性访问只读和响应,以避免循环依赖。
// 通过对Reflect.get()的返回结果进行reactive递归调用,达到深度监测
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)
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)
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
}
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
//触发get和set,此函数传给Proxy
export const shallowReactiveHandlers: ProxyHandler<object> = extend(
{},
mutableHandlers,
{
get: shallowGet,
set: shallowSet
}
)
track收集依赖
在createGetter中执行track
targetMap 储存了 target --> depsMap ,depsMap存储了 key --> dep,dep是Set集合, dep 中存储 effect
- 依赖收集器
type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()
- 收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 不能重复添加activeEffect
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
trigger响应触发
在createSetter和deleteProperty中执行trigger
trigger 触发器,拿到target key下的对应的所有 effect,然后遍历执行 effect()
// target是响应对象,depsMap(依赖收集器)存储effect,depsMap有键值对关系,一个key对应一个effect,computed中key为'value'
trigger是为了触发收集的effect,其中包含
- 先执行add函数先获取Set类型的effects ,
const effects = new Set<ReactiveEffect>()
- 再执行run函数调用effect.options.scheduler(effect)或者effect(),最终得到activeEffect
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>()
// add函数是得到effects,是将当前的依赖项添加进一个等待更新的数组中
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect) //activeEffect和effect是通过什么比较的?需要去了解ts恒等
}
})
}
}
// 需要循环清除,触发所有的effect
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
// 由于在一次操作数组的时候会进行多次的set,那么如果么此set都要去更新的话,会造成性能上的浪费,所以此处只有在set length的时侯才会去调用add方法,然后统一执行所有的更新
// target是数组,key更大或者为'length',则执行add
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
// 遍历 ADD | DELETE,区分是否是数组, 如果是Map.SET,仅仅判断isMap
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.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
}
}
// **这里是定义的run函数,抽离出来,以防止太长,方便查看**
// 循环执行effects,其实就是获得ReactiveEffect,最终会得到数组,看effect函数中effectStack逻辑 effectStack.push(effect)
effects.forEach(run)
}
- trigger中的run函数
const run = (effect: ReactiveEffect) => {
// 在onTrigger传入(effect,target,key,type,newValue, oldValue,oldTarget)参数
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
// scheduler是调度,其实就是执行effect配置的scheduler函数,看scheduler中配置的函数;否则执行effect()获得effect
// scheduler是调度再computed中有
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effect
调用effect()是为了得到activeEffect,effect中是一对象,包含{id,allowRecurse ,_isEffect,_isEffect, raw, deps, options }
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
// 如果fn是ReactiveEffect,fn为传入的函数,比如computed属性中传入的函数
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
// 如果lazy不置为true的话,每次创建effect的时候都会立即执行一次, 而要实现computed显然是不需要的
if (!options.lazy) {
effect()
}
// 返回通过createReactiveEffect()函数返回的一activeEffect对象
return effect
}
createReactiveEffect在effect中调用
- createReactiveEffect
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
// 清除该effect下的deps,为了防止fn函数中访问的响应数据属性改动的情况,此时需要重新收集相关属性依赖
cleanup(effect)
try {
enableTracking()
// 运行前先把effect压入栈
effectStack.push(effect)
activeEffect = effect
// 执行原始函数并返回,因为fn中引入了依赖数据,执行fn触发track依赖收集
return fn()
} finally {
// 运行完再把effect推出栈
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true
effect.active = true
effect.raw = fn //把回调fn赋值给.raw属性
effect.deps = []
effect.options = options
return effect
}
手写简版Vue3响应式
reactive
/* 建立响应式数据 */
function reactive(obj){
// Proxy:http://es6.ruanyifeng.com/#docs/proxy
// Proxy相当于在对象外层加拦截
// Proxy递归是惰性的,需要添加递归的逻辑
// Reflect:http://es6.ruanyifeng.com/#docs/reflect
// Reflect:用于执行对象默认操作,更规范、更友好,可以理解成操作对象的合集
// Proxy和Object的方法Reflect都有对应
if(!isObject(obj)) return obj
const observed = new Proxy(obj,{
get(target, key, receiver){
const ret = Reflect.get(target, key, receiver)
console.log('getter '+ret)
// 跟踪 收集依赖
track(target, key)
return reactive(ret)
},
set(target, key, val, receiver){
const ret = Reflect.set(target, key, val, receiver)
console.log('setter '+key+':'+val + '=>' + ret)
// 触发更新
trigger(target, key)
return ret
},
deleteProperty(target, key){
const ret = Reflect.deleteProperty(target, key)
console.log('delete '+key+':'+ret)
// 触发更新
trigger(target, key)
return ret
},
})
return observed
}
effect
/* 声明响应函数cb */
const effectStack = []
function effect(cb){
// 对函数进行高阶封装
const rxEffect = function(){
// 1.捕获异常
// 2.fn出栈入栈
// 3.执行fn
try{
effectStack.push(rxEffect)
return cb()
}finally{
effectStack.pop()
}
}
// 最初要执行一次,进行最初的依赖收集
rxEffect()
return rxEffect
}
track
/* 依赖收集:建立 数据&cb 映射关系 */
const targetMap = new WeakMap()
function track(target,key){
// 存入映射关系
const effectFn = effectStack[effectStack.length - 1] // 拿出栈顶函数
if(effectFn){
let depsMap = targetMap.get(target)
if(!depsMap){
depsMap = new Map()
targetMap.set(target, depsMap)
}
let deps = depsMap.get(key)
if(!deps){
deps = new Set()
depsMap.set(key, deps)
}
deps.add(effectFn)
}
}
trigger
/* 触发更新:根据映射关系,执行cb */
function trigger(target, key){
const depsMap = targetMap.get(target)
if(depsMap){
const deps = depsMap.get(key)
if(deps){
deps.forEach(effect=>effect())
}
}
}
应用
<div id="app">
{{msg}}
</div>
<script src="./mini-vue3.js"></script>
<script>
// 定义一个响应式数据
const state = reactive({
msg:'message'
})
// 定义一个使用到响应式数据的 dom更新函数
function updateDom(){
document.getElementById('app').innerText = state.msg
}
// 用effect声明更新函数
effect(updateDom)
// 定时变更响应式数据
setInterval(()=>{
state.msg = 'message' + Math.random()
},1000)
</script>
总结
- 学习顺序
看别人阅读顺序建议:
- 先读 reactivity,能最快了解 Vue3 的新特性,何况基于了解到vue3和vue2的最大区别就是拦截方式改变,毋庸置疑从此处开始学习
- 再读 rumtime,理解组件和生命周期的实现;
- 再读 compiler,理解编译优化过程
其实总结过程中,让我最受启发的是如果看源码过程中,不知道函数用法,可以从测试用例去了解,因为测试用例很清楚的知道这个函数要达到什么效果。
vue3可以直接看所有 tests 目录里的测试用例来了解其所有功能,目前有不到 700 个测试用例。
- 响应式原理几个要点
-
track 追踪器,在 get 时调用该函数,将所有 get 的 target 跟 key 以及 effect 建立起对应关系
-
trigger 触发器:拿到target key下的对应的所有 effect,然后遍历执行 effect()
-
effect是vue3响应式的核心,effect是副作用的意思,也就是说它是响应式的副产品
看源码知道effect在mountComponent、computed、reactive、doWatch中调用作用,(此问仅仅写了reactive,computed看另一篇)由响应式到最终调用,最终发现effect才是vue3响应式的核心
每次触发了get时收集effect,每次set时在触发这些effecs。这样就可以做有些响应式数据之外的一些事情了,比如计算属性computed
effect实现逻辑?
-
首先 如果 effect 回调内有已响应的对象被触发了 get 时,effect就应该被储存起来
-
然后,我们需要一个储存effect的地方,在effect函数调用的时候就应该把effect放进这个储存空间,在vue中使用的是一个数组activeReactiveEffectStack = []
-
再后,每个target被触发的时候,都可能有多个effect,所以每个target需要有一个对应的依赖收集器 deps,等到 set 时遍历 deps 执行 effect()
-
然而,这个依赖收集器 deps 不能放在 target 本身上,这样会使数据看起来不是很简洁,还会存在多余无用的数据,所以我们需要一个 map 集合来储存 target 跟 deps 的关系, 在vue中这个储存集合叫 targetMap 。
-
主要函数调用顺序:
- 开发者可以调用的函数
reactive ——》createReactiveObject
shallowReactive ——》createReactiveObject
readonly ——》createReactiveObject
shallowReadonly ——》createReactiveObject
- 原理
createReactiveObject ——》new Proxy() ——》get或者set
普通类型和引用类型分别怎么监听的?
- get或者set调用的函数
get——》track——》收集effect
set——》trigger——》触发收集的effect
effect——》createReactiveEffect ——》 cleanup(effect) ——》 effectStack.push(effect) ——》fn() ——》 effectStack.pop() ——》 activeEffect = effectStack[effectStack.length - 1]
- 收集依赖的映射关系(看track函数能迅速了解):
targetMap 储存了 target --> depsMap ,depsMap存储了 key --> dep,dep是Set集合, dep 中存储 effect
target是响应对象,depsMap(依赖收集器)存储effect,depsMap有键值对关系,一个key对应effect集合,特别说明,computed中key为'value'