提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
一、首先了解Object.defineProperty、Proxy、Reflect
1. Object.defineProperty()
2. proxy
3. 解决了defineproperty的那些问题?
4. Reflect
一、首先了解Object.defineProperty、Proxy、Reflect
1. Object.defineProperty()
这个函数有三个参数分别为 obj、prop、descriptor。
obj为我们要监听的对象 。 prop一个字符串或 Symbol,指定了要定义或修改的属性键。
descriptor要定义或修改的属性的描述符。
(1)写一个简单示例
<script>let person = {};let name = "cheng";Object.defineProperty(person, "nameA", {//但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true//默认不可以修改,可:wirtable:true//默认不可以删除,可:configurable:trueenumerable: true,get() {console.log("触发get");return name;},set(val) {console.log("触发set");name = val;},});// 以上代码劫持了person对象,在访问person上的属性时就会触发get方法,返回name值// 打印为chengconsole.log(person.nameA);// 不会触发get和set 方法name = "chengbaba";// 如果没有设置 enumerable: true, 默认不能遍历for (let i in person) {console.log(";;;", i, person[i]);}// 触发get方法,因为name的值发生变化,所以get中的返回值也会发生变化,因此打印chengbabaconsole.log(person.nameA);// 因为监听person对象,所以在修改时会触发set方法person.nameA = "123";// 触发get 方法, 打印123console.log(person.nameA);</script>
(2) 监听多个属性:
<script>let data = {name: "jack",age: 18,};function observer(obj) {// 遍历所有的key 并进行每个属性的劫持Object.keys(obj).forEach((key) => {defineProperty(obj, key, obj[key]);});}function defineProperty(obj, key, val) {Object.defineProperty(obj, key, {//但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true//默认不可以修改,可:wirtable:true//默认不可以删除,可:configurable:trueenumerable: true,get() {console.log("触发get");return val;},set(newVal) {console.log("触发set");name = newVal;},});}// 监听对象observer(data);console.log(data.name);data.age = 30;console.log(data.age);</script>
(3)深度监听对象属性
<script>let data = {name: "jack",age: 18,other: {name: "jj",age: 20,},};function observer(obj) {if (typeof obj !== "object" || obj == null) {return;}// 遍历所有的key 并进行每个属性的劫持Object.keys(obj).forEach((key) => {defineProperty(obj, key, obj[key]);});}function defineProperty(obj, key, val) {// 当属性的值为object就递归,否则就进行属性监听if (typeof val == "object") {observer(val);} else {Object.defineProperty(obj, key, {//但是默认是不可枚举的(for in打印打印不出来),可:enumerable: true//默认不可以修改,可:wirtable:true//默认不可以删除,可:configurable:trueenumerable: true,get() {console.log("触发get");return val;},set(newVal) {console.log("触发set");if (typeof val === "object") {observer(key);}val = newVal;},});}}// 监听对象observer(data);console.log(data.name);data.age = 30;console.log(data.age);console.log("-----深度监听-----");console.log(data.other.name);data.other.age = 40;console.log(data.other.age);</script>
(4)监听数组
let hobby = ['抽烟','喝酒','烫头']let person = {name:'Barry',age:22}// 把 hobby 作为 person 属性监听
Object.defineProperty(person,'hobby',{get(){console.log('tigger get');return hobby},set(newVal){console.log('tigger set',newVal);hobby = newVal}
})console.log(person.hobby);
person.hobby = ['看书','游泳','听歌']// 不能被监听
person.hobby.push('游泳')
数组的push、unshift、splice、sort、reverse等方法,set方法是监听不到的。
vue2.x通过 劫持这些方法实现响应式。
具体实现:
/** not type checking this file because flow doesn't play well with* dynamically accessing methods on Array prototype*/import { TriggerOpTypes } from '../../v3'
import { def } from '../util/index'const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'
]/*** Intercept mutating methods and emit events*/
methodsToPatch.forEach(function (method) {// cache original methodconst original = arrayProto[method]def(arrayMethods, method, function mutator(...args) {const result = original.apply(this, args)const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}if (inserted) ob.observeArray(inserted)// notify changeif (__DEV__) {ob.dep.notify({type: TriggerOpTypes.ARRAY_MUTATION,target: this,key: method})} else {ob.dep.notify()}return result})
})
首先调用def方法,内部实现了方法的劫持。
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {Object.defineProperty(obj, key, {value: val,enumerable: !!enumerable,writable: true,configurable: true})
}
处理方法的逻辑都在 mutator() 函数中。其中如果是push,unshift、splice 就会触发observeArray方法。
observeArray(value: any[]) {for (let i = 0, l = value.length; i < l; i++) {observe(value[i], false, this.mock)}}
最后都会执行 notify,通知变更。notify 函数中通过onTrigger派发依赖,update方法渲染dom;就不具体看了,大体流程就这样。
notify(info?: DebuggerEventExtraInfo) {// stabilize the subscriber list firstconst subs = this.subs.filter(s => s) as DepTarget[]if (__DEV__ && !config.async) {// subs aren't sorted in scheduler if not running async// we need to sort them now to make sure they fire in correct// ordersubs.sort((a, b) => a.id - b.id)}for (let i = 0, l = subs.length; i < l; i++) {const sub = subs[i]if (__DEV__ && info) {sub.onTrigger &&sub.onTrigger({effect: subs[i],...info})}sub.update()}}
2. proxy
概念:Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
new Proxy() 包含两个参数,分别为target和handler。代表要使用Proxy包装的对象,定义一些行为。
简单实例
<script>let person = {name: "Jack",age: 22,};let p = new Proxy(person, {get(target, key) {console.log("触发get");return target[key];},set(target, key, val) {console.log("触发set");return (target[key] = val);},});console.log(p.name);p.age = 40;console.log(p.age);</script>
3. 解决了defineproperty的那些问题?
- Object.defineProperty一次只能监听属性,需要遍历对所有的属性监听。
- 在遇到一个对象属性时,需要递归监听。
- 对于对象的新增属性,需要手动监听。
- 对于数组通过push、unshift方法增加属性,无法监听。
4. Reflect
Reflect是ES6的高级Api,是一个内置对象,他提供了拦截js的方法。Reflect不是函数对象,所以不能被构造。
Reflect的所有方法都是静态的
为什么要使用Reflect?
(1)触发代理对象的劫持时,保证正确的this上下文指向。其中receiver的作用就是修改this指向
<script type="text/javaScript">const person = {name:'Barry',age:22}const p =new Proxy(person,{// get陷阱中target表示原对象 key表示访问的属性名get(target, key, receiver) {console.log(receiver === p);return Reflect.get(target,key,receiver)},})console.log(p.name);
</script>
(2)代码的健壮性
(3)操作对象出错时返回false,避免大量try cache
二、reactive
vue的版本是"version": "3.2.45", 知道大概的实现思路即可,深入理解还得慢慢来,别给自己太大压力。
reactive的实现函数是createReactiveObject 。
// 获取数据类型
function targetTypeMap(rawType) {switch (rawType) {case 'Object':case 'Array':return 1 /* TargetType.COMMON */;case 'Map':case 'Set':case 'WeakMap':case 'WeakSet':return 2 /* TargetType.COLLECTION */;default:return 0 /* TargetType.INVALID */;}
}// 根据target 生成proxy实例
function createReactiveObject(target: Target,isReadonly: boolean,baseHandlers: ProxyHandler<any>,collectionHandlers: ProxyHandler<any>,proxyMap: WeakMap<Target, any>
) {if (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)}return target}// target is already a Proxy, return it.// 目标值已经是proxy对象,就直接返回// exception: calling readonly() on a reactive objectif (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}// target already has corresponding Proxy// 目标值已经有对应的proxyconst existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}// only specific value types can be observed.//只能观察到特定的值类型const targetType = getTargetType(target)if (targetType === TargetType.INVALID) {return target}const proxy = new Proxy(target,targetType === 2 ? collectionHandlers : baseHandlers)proxyMap.set(target, proxy)return proxy
}
createReactiveObject简要分析:(1)判断reactive传值是否为引用类型,如果不是引用类型则直接返回值,不具备响应式。(2)判断传值是狗已经是proxy对象,是就直接返回。(3)判断该proxy对象是否存在,存在直接返回。(4)判断是否是可跳过或者非扩展对象,是就直接返回(5)最后生成proxy对象,并对目标值进行存储,避免重复代理。
注意:
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
劫持对象的主要方法分为两种:第一种代理的值为Map、Set、WeakMap、WeakSet对象,代理以上对象内部只监听get方法,第二种代理的值为Array、Object,内部监听get,set,
deleteProperty、has、ownKeys。其中get、has、ownKeys都是执行track函数进行依赖收集,set、deleteProperty执行trigger函数进行派发依赖
依赖收集和依赖派发是实现在effect.ts中,大家可以自己查看。
这里就不说collectionHandlers、baseHandlers,就是通过这个函数定义了监听对象的一些方法,proxy的内部原理。当修改或者读取时触发相应的方法。
然后就是get、has、ownKeys会进行依赖收集。set、deleteProperty方法会进行派发依赖。
在分析依赖收集和派发时,我们先了解一下effect.ts中的ReactiveEffect类,effect()
// 这个类的作用是创建一个响应式副作用函数,这个函数会在依赖数据发生变化时执行
export class ReactiveEffect<T = any> {// 是否处于活动状态active = true// 响应式依赖项集合deps: Dep[] = []// 父级作用域parent: ReactiveEffect | undefined = undefinedcomputed?: ComputedRefImpl<T>allowRecurse?: booleanprivate deferStop?: booleanonStop?: () => void// dev onlyonTrack?: (event: DebuggerEvent) => void// dev onlyonTrigger?: (event: DebuggerEvent) => voidconstructor(// 用来记录副作用函数public fn: () => T,// 用户记录调度器public scheduler: EffectScheduler | null = null,scope?: EffectScope) {recordEffectScope(this, scope)}run() {// 如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果}stop() {}
}
知道里边有两个重要的方法:stop和run。
run:就是执行副作用函数
function run() {// 如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果if (!this.active) {return this.fn();}// 寻找当前 ReactiveEffect 对象的最顶层的父级作用域let parent = activeEffect;let lastShouldTrack = shouldTrack;while (parent) {if (parent === this) {return;}parent = parent.parent;}try {// 记录父级作用域为当前活动的 ReactiveEffect 对象this.parent = activeEffect;// 将当前活动的 ReactiveEffect 对象设置为 “自己”activeEffect = this;// 将 shouldTrack 设置为 true (表示是否需要收集依赖)shouldTrack = true;// effectTrackDepth 用于标识当前的 effect 调用栈的深度,执行一次 effect 就会将 effectTrackDepth 加 1trackOpBit = 1 << ++effectTrackDepth;// 这里是用于控制 "effect调用栈的深度" 在一个阈值之内if (effectTrackDepth <= maxMarkerBits) {// 初始依赖追踪标记initDepMarkers(this);}else {// 清除所有的依赖追踪标记cleanupEffect(this);}// 执行副作用函数,并返回执行结果return this.fn();}finally {// 如果 effect调用栈的深度 没有超过阈值if (effectTrackDepth <= maxMarkerBits) {// 确定最终的依赖追踪标记finalizeDepMarkers(this);}// 执行完毕会将 effectTrackDepth 减 1trackOpBit = 1 << --effectTrackDepth;// 执行完毕,将当前活动的 ReactiveEffect 对象设置为 “父级作用域”activeEffect = this.parent;// 将 shouldTrack 设置为上一个值shouldTrack = lastShouldTrack;// 将父级作用域设置为 undefinedthis.parent = undefined;// 延时停止,这个标志是在 stop 方法中设置的if (this.deferStop) {this.stop();}}
}
stop
function stop() {// 如果当前 活动的 ReactiveEffect 对象是 “自己”// 延迟停止,需要执行完当前的副作用函数之后再停止if (activeEffect === this) {// 在 run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法this.deferStop = true;}// 如果当前 ReactiveEffect 对象处于活动状态else if (this.active) {// 清除所有的依赖追踪标记cleanupEffect(this);// 如果有 onStop 回调函数,就执行if (this.onStop) {this.onStop();}// 将 active 设置为 falsethis.active = false;}
}
effect()
effect()函数会真正的执行run方法(执行副作用函数)。
export function effect<T = any>(fn: () => T,options?: ReactiveEffectOptions
): ReactiveEffectRunner {if ((fn as ReactiveEffectRunner).effect) {fn = (fn as ReactiveEffectRunner).effect.fn}// 这里得到ReactiveEffect类 的实例 const _effect = new ReactiveEffect(fn)if (options) {extend(_effect, options)if (options.scope) recordEffectScope(_effect, options.scope)}if (!options || !options.lazy) {_effect.run()}const runner = _effect.run.bind(_effect) as ReactiveEffectRunnerrunner.effect = _effectreturn runner
}
这里再看依赖收集和派发就会大概清楚执行顺序。
/ 收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {if (shouldTrack && activeEffect) {// targetMap 为全局WeakMap对象,在依赖派发的时候使用let depsMap = targetMap.get(target)// 不存在targetif (!depsMap) {targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)if (!dep) {depsMap.set(key, (dep = createDep()))}const eventInfo = __DEV__? { effect: activeEffect, target, type, key }: undefinedtrackEffects(dep, eventInfo)}
}export function trackEffects(dep: Dep,debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {if (shouldTrack) {dep.add(activeEffect!)}
}
以上是依赖收集的过程,定义了一个targetMap对象(WeakMap)键值target,值是一个Map,这个Map的键key(即对象的属性值),值是Set集合。使用Set避免重复的副作用函数。
收集完依赖以后如下结构:
/*** 触发依赖* @param target 指向的对象* @param type 操作类型* @param key 指向对象的 key* @param newValue 新值* @param oldValue 旧值* @param oldTarget 旧的 target*/export function trigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown, unknown> | Set<unknown>
) {// 获取targetMap中的depsMapconst depsMap = targetMap.get(target)if (!depsMap) {// never been trackedreturn}// 创建一个数组,用来存放需要执行的ReactiveEffect对象 let deps: (Dep | undefined)[] = []// 如果 type 为 clear,就会将 depsMap 中的所有 ReactiveEffect 对象都添加到 deps 中if (type === ‘clear’) {// collection being cleared// trigger all effects for targetdeps = [...depsMap.values()]// 如果 key 为 length ,并且 target 是一个数组} else if (key === 'length' && isArray(target)) {const newLength = Number(newValue)depsMap.forEach((dep, key) => {if (key === 'length' || key >= newLength) {deps.push(dep)}})} else {if (key !== void 0) {deps.push(depsMap.get(key))}// 执行 add、delete、set 操作时,就会触发的依赖变更switch (type) {case 'add':if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))}} else if (isIntegerKey(key)) {// new index added to array -> length changesdeps.push(depsMap.get('length'))}breakcase 'delete':if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))}}breakcase 'set':if (isMap(target)) {deps.push(depsMap.get(ITERATE_KEY))}break}}const eventInfo = __DEV__? { target, type, key, newValue, oldValue, oldTarget }: undefinedif (deps.length === 1) {if (deps[0]) {if (__DEV__) {triggerEffects(deps[0], eventInfo)} else {triggerEffects(deps[0])}}} else {const effects: ReactiveEffect[] = []for (const dep of deps) {if (dep) {effects.push(...dep)}}if (__DEV__) {triggerEffects(createDep(effects), eventInfo)} else {triggerEffects(createDep(effects))}}
}
export function triggerEffects(dep: Dep | ReactiveEffect[],debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {// 如果dep不是数组,就会将dep转成数组,因为dep可能时Set对象const effects = isArray(dep) ? dep : [...dep]// 遍历所有的依赖,for (const effect of effects) {// 执行computed依赖if (effect.computed) {triggerEffect(effect, debuggerEventExtraInfo)}}
// 执行其他依赖for (const effect of effects) {if (!effect.computed) {triggerEffect(effect, debuggerEventExtraInfo)}}
}function triggerEffect(effect: ReactiveEffect,debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {if (effect !== activeEffect || effect.allowRecurse) {// 如果 effect.onTrigger 存在,就会执行,只有开发模式下才会执行if (__DEV__ && effect.onTrigger) {effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))}// 如果 effect 是一个调度器,就会执行 schedulerif (effect.scheduler) {effect.scheduler()} else {// 否则直接执行 effect.run()effect.run()}}
}
tigger
函数的作用就是触发依赖,当我们修改数据的时候,就会触发依赖,然后执行依赖中的副作用函数。
总结
reactive(obj) vue3内部先执行(在reactive.ts中)createReactiveObject() 参数 target(要代理的对象)、isReadonly(是否只读)、baseHanlers(拦截Object和Array类型数据的函数集合)、collectionHandlers(拦截Map,Set,WeakMap,WeakSet的函数集合)、proxyMap(WeakMap集合用来记录代理的对象,避免重复代理)。此函数主要用来创建响应式对象。
然后我们需要关注proxy的第二个参数,也就是baseHandlers和collectionHandlers 分别在对应的文件里。在文件中我们可以看到mutableHandler和mutableCollectionHandlers 作为Proxy的第二个参数。用来监听被代理的对象。再次对象的方法中get、has 、ownKeys方法执行会收集依赖,set和deleteproperty方法会触发依赖。重点关注set函数(createSetter()返回值)和get函数(createGetter())
然后关注track()和trigger(),分别实现收集和触发依赖。都在effect,ts文件中。
track---->trackEffects.。 trigger--->triggerEffects--->triggerEffect(执行副作用)
然后看 effect() 也在effect.ts文件中,这里会const _effect = new ReactiveEffect(fn);_effect.run()
然后在这个文件中找到ReactiveEffect构造函数。这就是大概的流程。
收集的依赖就是ReactiveEffect实例,解释初始化组件时,会执行ReactiveEffect构造函数,执行run方法,会将this赋值activeEffect,这里的this指向调用run的对象,这就是ReactiveEffect的实例。 收集依赖时:dep.add(activeEffect!)。