vue3响应式原理

news/2024/11/22 19:12:42/

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

目录

一、首先了解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的第二个参数,也就是baseHandlerscollectionHandlers 分别在对应的文件里。在文件中我们可以看到mutableHandlermutableCollectionHandlers 作为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!)。


http://www.ppmy.cn/news/528121.html

相关文章

前端Vue自定义简单实用轮播图封装组件 快速实现轮播图

前端Vue自定义简单实用轮播图封装组件 快速实现轮播图&#xff0c; 下载完整代码请访问uni-app插件市场地址&#xff1a;https://ext.dcloud.net.cn/plugin?id13153 效果图如下&#xff1a; # cc-mySwiper #### 使用方法 使用方法 <!-- 自定义轮播图 swiperArr: 轮播数…

【学习】自学JavaScript

第一章 JavaScript简介 1.1、JavaScript的起源 JavaScript诞生于1995年&#xff0c;它的出现主要是用于处理网页中的前端验证。所谓的前端验证&#xff0c;就是指检查用户输入的内容是否符合一定的规则。比如&#xff1a;用户名的长度&#xff0c;密码的长度&#xff0c;邮箱的…

Qt开发经验(转载)

0 前言说明 本文转载于https://qtchina.blog.csdn.net/?typeblog&#xff0c;feiyangqingyun的博客&#xff0c;感谢大佬的经验分析。 1 开发经验 01&#xff1a;001-010 当编译发现大量错误的时候&#xff0c;从第一个看起&#xff0c;一个一个的解决&#xff0c;不要急着…

3.后端学习JavaScript

配套资料&#xff0c;免费下载 链接&#xff1a;https://pan.baidu.com/s/152NnFqzAUx9br2qb49mstA 提取码&#xff1a;ujyp 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 第一章 JavaScript简介 1.1、JavaScript的起源 JavaScript诞生于1995年&#xff0c;它…

深入解析Spring源码系列:Day 29 - Spring中的批处理

深入解析Spring源码系列&#xff1a;Day 29 - Spring中的批处理 欢迎来到第二十九天的博客&#xff01;今天我们将深入探讨Spring框架中的批处理机制。批处理是一种处理大量数据的方式&#xff0c;通过批量操作来提高处理效率。在企业级应用中&#xff0c;处理大规模数据集合是…

SketchUp安装组件失败“.Net FrameWork 4.5.2”的解决办法

安装SketchUp时&#xff0c;会自动先安装其所需要的组件&#xff0c;比如我电脑上是C的运行库、.net Framework4.5.2&#xff0c;就在安装组件完成的时候只提示安装失败&#xff0c;不知道原因&#xff0c;即使根据提示找到安装日志也看不出问题所在。 于是手动下载了.NET fra…

Cloud Foundry 峰会进入中国 全球专家与你面对面

具有领先的开源云原生应用平台的 Cloud Foundry 基金会在今年早些时候决定其第一个亚太峰会将于2015年12 月2日-3日在中国上海召开。此次峰会是面向利用行业开源 PaaS 云平台—Cloud Foundry 的开发人员和云运营商举办的顶级盛会。 开源社作为本次 Cloud Foundry 亚太峰会的社区…

【智联沙龙活动】混合云云平台PaaS技术分享

中关村智联软件服务业质量创新联盟&#xff0c;将于4月23日举办拥抱混合云&云平台PaaS技术活动&#xff0c;活动安排如下&#xff1a; 许多大大小小的公司都已经将云计算部署在自己的某些业务当中。那么随着这种状态的发展&#xff0c;云的未来会是什么样的呢&#xff1f;显…