在petite-vue中咱们通过reactive
构建上下文对象,并将依据状态渲染UI的逻辑作为入参传递给effect
,而后神奇的事件产生了,当状态发生变化时将主动触发UI从新渲染。那么到底这是怎么做到的呢?
@vue/reactivity性能非常丰盛,而petite-vue仅应用到reactive
和effect
两个最根本的API,作为入门本文将仅仅对这两个API进行源码解读。
所有源于Proxy
咱们晓得Vue2是基于Object.defineProperty
拦挡对象属性的读写操作,从而实现依赖收集和响应式UI渲染。而@vue/reactivity作为Vue3的子项目,采纳的是ES6的Proxy接口实现这一性能。
const state = { count: 1}const proxyState = new Proxy(state, { get(target: T, property: string, receiver?: T | Proxy): any { // 拦挡读操作 console.log('get') return Reflect.get(target, property, receiver) }, set(target: T, property: string, value: any, receiver?: T | Proxy): boolean { // 拦挡写操作 console.log('set') return Reflect.set(target, property, value, receiver) }, deleteProperty(target, prop) { // 拦挡属性删除操作 console.log('delete') delete target[prop] return true }})
绝对Object.defineProperty
,Proxy的特点:
- 通过
new Proxy
构建的对象进行操作能力拦挡对象属性的读写操作,而被代理的对象则没有任何变动; - 能够监听数组元素的变动和增减;
- 能够监听对象属性的增减;
- Proxy能够逐层代理对象属性,而
Object.defineProperty
则须要一次性代理对象所有层级的属性。
响应式编程
// 定义响应式对象const state = reactive({ num1: 1, num2: 2})// 在副作用函数中拜访响应式对象属性,当这些属性发生变化时副作用函数将被主动调用effect(() => { console.log('outer', state.num1) effect(() => { console.log('inner', state.num2) })})// 回显 outer 1// 回显 inner 2state.num2 += 1// 回显 inner 3state.num1 += 1// 回显 outer 2// 回显 inner 3state.num2 += 1// 回显 inner 4// 回显 inner 4
本篇咱们将从reactive动手,解读Vue3到底如何结构一个响应式对象。
深刻reactive
的工作原理
@vue/reactivity的源码位于vue-next我的项目的packages/reactivity下,而reactive
函数则位于其下的src/reactive.ts文件中。该文件中除了蕴含reactive
函数外,还蕴含如shallowReactive
、readonly
、shallowReadonly
和其它帮忙函数。
而reactive
外围工作则是通过Proxy将一个一般的JavaScript对象转换为监控对象,拦挡对象属性的读写删操作,并收集依赖该对象(属性)的副作用函数。大抵流程如下:
- 通过
reactive
结构的响应式对象都会将被代理对象和响应式对象的映射关系保留在reactiveMap
,避免反复生成响应式对象,优化性能; - 当调用
reactive
后会对被代理对象进行查看,若不是只读对象、响应式对象、primitive value和reactiveMap
中不存在则依据被代理对象的类型结构响应式对象 - 拦挡读操作(
get
,has
和ownKeys
)时调用effect.ts中的track收集依赖 - 拦挡写操作(
set
,deleteProperty
)时调用effect.ts中的trigger触发副作用函数执行
上面咱们一起逐行了解源码吧!
源码解读——reactive
入口
// Vue3外部定义的对象个性标识export const enum ReactiveFlags { SKIP = '__v_skip', // 标识该对象不被代理 IS_REACTIVE = '__v_isReactive', // 标识该对象是响应式对象 IS_READONLY = '__v_isReadonly', // 标识该对象为只读对象 RAW = '__v_raw' // 指向被代理的JavaScript对象}// 响应式对象的接口export interface Target { [ReactiveFlags.SKIP]?: boolean [ReactiveFlags.IS_REACTIVE]?: boolean [ReactiveFlags.IS_READONLY]?: boolean [ReactiveFlags.RAW]?: any // 用于指向被代理的JavaScript对象}// 用于缓存被代理对象和代理对象的关系,避免反复代理export const reactiveMap = new WeakMap<Target, any>()// 将被代理对象的解决形式分为不代理(INVALID)、一般对象和数组(COMMON)和Map、Set(COLLECTION)const enum TargetType { INVALID = 0, COMMON = 1, COLLECTION = 2,}function targetTypeMap(rawType: string) { switch(rawType) { case 'Object': case 'Array': return TargetType.COMMON case 'Map': case 'Set': case 'WeakMap': case 'WeakSet': return TargetType.COLLECTION default: return TargetType.INVALID }}function getTargetType(value: Target) { // 若对象标记为跳过,或不可扩大则不代理该对象 return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) ? TargetType.INVALID // 依据类型决定解决形式 : targetTypeMap(toRawType(value))}export function reative(target: object) { // 不拦挡只读对象的读写删操作 if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target } return createReactiveObject( target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap )}function createReactiveObject ( target: Target, isReadonly: boolean, beaseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>, proxyMap: WeakMap<Target, any>) { // reactive函数入参必须是JavaScript对象或数组,若是primitive value则会间接返回primitive value if (!isObject(target)) { return target } /** * 1. 仅能对非响应式和非只读对象结构响应式对象 * 2. 能够对非只读对象结构响应式对象 */ if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target } // 若对象已被代理过,则间接返回对应的代理对象 const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // 依据被代理的对象类型决定采纳哪种代理形式 const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { return target } const proxy = new Proxy ( target, targetType == TargetType.COLLECTION ? collectionHandlers : baseHandlers ) proxyMap.set(target, proxy) return proxy}
能够看到reactive
办法中会对被代理对象进行各种查看,从而缩小不必要的操作进步性能。最初若被代理对象的类型为Object
或Array
则采纳baseHandlers
生成代理,否则应用collectionHandlers
生成代理。
源码解读-代理Object
和Array
的baseHandlers
//文件 ./baseHandlers.ts// /*#__PURE__*/用于通知webpack等bundler工具前面紧跟的函数是纯函数,若没被调用过则能够采纳tree-shaking移除掉该函数const get = /*#__PURE__*/ createGetter()export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys,}
咱们首先看看是如何拦挡读操作吧
拦挡读操作
拦挡读操作外围是收集依赖所读属性的辅作用函数的信息,具体流程逻辑是
- 对于Vue3外部属性的读操作,即返回对应的值而不必收集依赖
- 对于数组内置办法的读操作,须要改写这些内置办法用于在调用该办法前对数组元素进行依赖收集,或解决一些边界问题
- 对于内置Symbol属性和其它Vue3外部属性的读操作,间接返回原始值且不必收集依赖
- 对于非只读对象的除上述外的其余属性的读操作,执行依赖收集(外围逻辑)
- 若浅层响应式对象则间接返回属性值,否则若属性值为对象,则将其结构为响应式对象(
reactive
)或只读对象(readonly
)
//文件 ./baseHandlers.ts/** * isNonTrackableKeys = {'__proto__': true, '__v_isRef': true, '__isVue': true} */const isNonTrackableKeys = /*#__PURE__*/ makeMap(`__proto__,__v_isRef,__isVue`)// 内置的Symbol实例蕴含:hasInstance, isConcatSpreadable, iterator, asyncIterator, match, matchAll, replace, search, split, toPrimitive, toStringTag, species, unscopablesconst builtInSymbols = new Set( Object.getOwnPropertyNames(Symbol) .map(key => (Symbol as any)[key]) .filter(isSymbol))function createGetter(isReadonly = false, shallow = false) { return function get(target: Target, key: string | symbol, receiver: object) { // 解决Vue3外部属性名(`__v_isReactive`, `__v_isReadonly`, `__v_raw`)的值 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } // TODO else if ( key === ReactiveFlags.RAW && receiver === reactiveMap ) { return target } // 如果key是includes,indexOf,lastIndexOf,push,pop,shift,unshift,splice时则返回能跟踪依赖变动的版本 const targetIsArray = isArray(target) if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) // 不拦挡内置Symbol属性和__proto__,__v_isRef和__isVue属性 if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) { return res } // 收集依赖该属性的副作用函数 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // 如果是构建ShallowReactive则不会基于属性值结构响应式式对象 if (shallow) { return res } /* 对于属性值为@vue/reactivity的Ref实例时,如果不是执行[1,2,3][0]的操作则返回Ref实例蕴含的primitive value,否则返回Ref实例 * 因而咱们在effect updator中能够通过如下形式间接获取Ref实例属性的primitive value * const age = ref(0), state = reactive({ age }) * console.log(age.value) // 回显0 * effect(() => { console.log(state.age) }) // 回显0 */ if (isRef(res)) { const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res } // 若属性值不是primitive value或BOM,则基于属性值结构响应式对象 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) } }}
这里能够看到当读取属性时才依据属性值类型来为属性值结构响应式对象,而不是当咱们调用reactive
时就一股脑的遍历对象所有属性,并为各个属性构建响应式对象。
另外,针对includes
等数组操作会返回对应的能跟踪依赖变动的版本,到底什么是能跟踪依赖变动的版本呢?
// 文件 ./baseHandlers.tsconst arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()function createArrayInstrumentations() { const instrumentations: Record<string, Function> = {} ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => { instrumentations[key] = function(this: unknown[], ...args: unknown[]) { const arr = toRaw(this) as any /* 提前遍历数组所有元素,跟踪每个元素的变动。若其中一个元素发生变化都会触发调用includes,indexOf或lastIndexOf副作用函数的执行。 * 如果执行`[2,1,2].includes(1)`,那么当匹配到第二个元素1时就会返回匹配后果,后续的元素不会被读取到,因而也就不会被跟踪收集到,那么当咱们执行`[2,1,2][2] = 1`时就不会触发副作用执行。 */ // for (let i = 0, l = this.length; i < l; i++) { track(arr, TrackOpTypes.GET, i + '') } // 调用数组原生的includes,indexOf和lastIndexOf办法 const res = arr[key](...args) if (res === -1 // indexOf和lastIndexOf || res === false // includes ) { // 因为数组元素有可能为响应式对象而入参也有可能是响应式对象,因而当匹配失败,则将尝试获取数组元素的被代理对象从新匹配 return arr[key](...args.map(toRaw)) } else { return res } } }) // 上面的操作会批改数组的长度,这里防止触发依赖长度的副作用函数执行 ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { instrumentations[key] = function(this: unknown[], ...args: unknown[]) { pauseTracking() const res = (toRaw(this) as any)[key].apply(this, args) resetTracking() return res } }) return instrumentations}// 文件 ./reactive.tsexport function toRaw<T>(observed: T): T { const raw = observed && (observed as Target)[ReactiveFlags.RAW] return raw ? toRaw(raw) : observed}
TypeScript小课堂1:['includes', 'indexOf', 'lastIndexOf'] as const
在TypeScript中用于标识对象或数组为不可批改对象。即
let a = ['includes', 'indexOf', 'lastIndexOf'] as consta[0] = 'hi' // 编译时报错const b = ['includes', 'indexOf', 'lastIndexOf']b[0] = 'hi' // 批改胜利console.log(b[0]) // 回显 hi
TypeScript小课堂2:instrumentations[key] = function(this: unknown[], ...args: unknown[]) {...}
采纳的是TypeScript的this
参数,用于限度调用函数时的this
类型。
转换为JavaScript就是
instrumentations[key] = function(...args){ pauseTracking() const res = (toRaw(this) as any)[key].apply(this, args) resetTracking() return res}
拦挡写操作
既然拦挡读操作是为了收集依赖,那么拦挡写操作天然就是用于触发副作用函数了。流程逻辑如下:
- 若属性值为Ref对象,而新值取原始值后不是Ref对象,则更新Ref对象的value,由Ref外部触发副作用函数
- 判断是否为新增属性,还是更新属性值,并触发副作用函数
const set = /*#__PURE__*/ createSetter()function createSetter(shallow = false) { return function set( target: Object, key: string | symbol, value: unknown, receiver: object ): boolean { // Proxy的set拦截器返回true示意赋值胜利,false示意赋值失败 let oldValue = (target as any)[key] if (!shallow) { /* 若旧属性值为Ref,而新值不是Ref,则间接将新值赋值给旧属性的value属性 * 一眼看上去貌似没有触发依赖该属性的副作用函数执行工作压入调度器,但Ref对象也是响应式对象,赋值给它的value属性,会触发依赖该Ref对象的辅助用函数压入调度器 */ value = toRaw(value) oldValue = toRaw(oldValue) if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } // 用于判断是新增属性还是批改属性 const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length // 数组索引的解决 : hasOwn(target, key) // 对象或数组非索引的而解决 // 赋值后再将副作用函数执行工作压入调度器 const result = Reflect.set(target, key, value, receiver) if (target === toRaw(receiver)) { if (!hadKey) { // 触发依赖该属性的副作用函数执行工作压入调度器 trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChange(value, oldValue)) { // 触发依赖该属性的副作用函数执行工作压入调度器 trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result }}// 文件 @vue/sharedexport const hasChanged = (value: any, oldValue: any): boolean => !Object.is(value, oldValue)
为什么不必===
而要应用Object.is
来比拟两个值是否相等呢?
对于-0===0
返回true
,NaN === NaN
返回false
,而Object.is(-0, 0)
返回false
,Object.is(NaN, NaN)
返回true
。
更多信息请查看《Source Code Reading for Vue 3: How does hasChanged
work? 》
拦挡删除操作
删除操作会批改属性天然也会触发依赖该属性的副作用函数啦
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) if (result && hadKey) { // 若删除胜利,且存在旧值则触发依赖该属性的副作用函数执行工作压入调度器 trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result}
拦挡查看存在与否操作('name' in state
)
查看存在与否属于读操作,因而咱们能够用于依赖收集。
function has(target: object, key: string | symbol): boolean { const result = Reflect.has(target, key) // Symbol内置属性不收集 if (!isSymbol(key) || !builtInSymbols.has(key)) { track(target, TrackOpTypes.HAS, key) } return result}
拦挡键遍历操作
以下操作都会执行ownKeys
Proxy trap办法
Object.getOwnPropertyNames
Object.getOwnPropertySymbols
Object.keys
Object.names
for..in
流程逻辑是:对于数组则跟踪数组长度,否则跟踪由effect模块提供的ITERATE_KEY
,这个是什么东东呢?持续往下看就晓得了:)
function ownKeys(target: object): (string | symbol)[] { track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY) return Reflect.ownKeys(target)}
Proxy中的receiver到底是什么?
在上述代码中咱们发现会应用到Proxy拦挡函数入参receiver
,如:
- 在写入拦挡时,如果
target === toRaw(receiver)
成立则触发副作用函数执行 - 在读取拦挡时,若
key === ReactiveFlags.RAW && receiver === reactiveMap
则不以入参会根底构建响应式对象 另外,在开篇《petite-vue源码分析-从动态视图开始》中创立作用域链
createScopedContext
有如下代码const reactiveProxy = reactive( new Proxy(mergeScope, { set(target, key, val, receiver) { // 若当设置的属性不存在于以后作用域则将值设置到父作用域上,因为父作用域以同样形式创立,因而递归找到领有该属性的先人作用域并赋值 if (receiver === reactiveProxy && !target.hasOwnProperty(key)) { return Reflect.set(parentScope, key, val) } return Reflect.set(target, key, val, receiver) } }) )
那么到底receiver
是什么呢?
对于数据属性(data properties)的拦挡,
receiver
指向以后构建的Proxy
实例自身// `receiver`指向以后构建的`Proxy`实例自身const state = { name: 'john'}let pState = new Proxy(state, { get(target, key, receiver) { console.log("receiver === pState:", receiver === pState) return Reflect.get(target, key, receiver) }})pState.name// 回显 receiver === pState: true
对于拜访器属性(accessor properties)的拦挡,
receiver
指向this
或者继承Proxy
实例的对象const state = { _name: 'john', name() { return this._name }}let pState = new Proxy(state, { get(target, key, receiver) { console.log("target[key]():", target[key]) console.log("receiver !== pState:", receiver !== pState) return Reflect.get(target, key, receiver) }})const son = { __proto__: pState, _name: 'son'}console.log(son.name)// 回显 target[key](): john// 回显 receiver !== pState: true// 回显 son
尽管理解了receiver
的作用,但对如下问题曾经无奈作出残缺的解答:
- 在写入拦挡时,如果
target === toRaw(receiver)
成立则触发副作用函数执行
首先receiver
是Proxy实例肯定不会等于target
,而toRaw(receiver)
则是获取其代理的对象,仅当被代理的对象和以后target雷同时才触发副作用函数执行。(至于什么场景会呈现,求高人领导?) - 在读取拦挡时,若
key === ReactiveFlags.RAW && receiver === reactiveMap
则不以入参会根底构建响应式对象
为何reactiveMap
会进行Proxy呢? - 另外,在开篇《petite-vue源码分析-从动态视图开始》中创立作用域链
createScopedContext
如下代码receiver === reactiveProxy && !target.hasOwnProperty(key)
即对以后作用域(receiver === reactiveProxy
)进行写操作时,若属性不存在于该作用域对象,则往父作用域上递归执行写操作。
总结
下一篇咱们来看看代理Map/WeakMap/Set/WeakSet
的mutableCollectionHandlers
的实现吧!
尊重原创,转载请注明来自:https://www.cnblogs.com/fsjoh...肥仔John