​浅谈Vue3响应式原理与源码解读

news/2025/1/20 9:35:36/

一. 了解几个概念

什么是响应式

在开始响应式原理与源码解析之前,需要先了解一下什么是响应式?首先明确一个概念:响应式是一个过程,它有两个参与方:

  • 触发方:数据

  • 响应方:引用数据的函数

当数据发生改变时,引用数据的函数会自动重新执行,例如,视图渲染中使用了数据,数据改变后,视图也会自动更新,这就完成了一个响应的过程。

副作用函数

VueReact中都有副作用函数的概念,什么是副作用函数?如果一个函数引用了外部的数据,这个函数会受到外部数据改变的影响,我们就说这个函数存在副作用,也就是我们所说的副作用函数。初听这个名字不太好理解,其实 副作用函数就是引用了数据的函数或是与数据相关联的函数。举个例子:

<!DOCTYPE html>
<html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1.0"><link rel="icon" href="<%= BASE_URL %>favicon.ico"><title><%= htmlWebpackPlugin.options.title %></title>
</head><body><div id="app"></div><script>const obj = {name: 'John',}// 副作用函数 effectfunction effect() {app.innerHTML = obj.nameconsole.log('effect', obj.name)}effect()setTimeout(() => {obj.name = 'ming'// 手动执行 effect 函数effect()}, 1000);</script>
</body>
</html>

在上面例子中,effect函数里面引用了外部的数据obj.name,如果这个数据发生了改变,则会影响到这个函数,类似effect的这种函数就是副作用函数。

实现响应式的基本步骤

在上面的例子中,当obj.name发生了改变,effect是我们手动执行的,如果能监听到obj.name的变化,让其自动执行副作用函数effect,那么就实现了响应式的过程。其实无论是 Vue2 还是 Vue3 ,响应式的核心都是 数据劫持/代理、依赖收集、依赖更新,只不过由于实现数据劫持方式的差异从而导致具体实现的差异。

  • Vue2响应式:基于Object.defineProperty()实现的数据的劫持

  • Vue3响应式:基于Proxy实现对整个对象的代理

关于Vue2的响应式这里不做重点讲解,这篇文章主要关注Vue3响应式原理的实现。

二. Proxy 与 Reflect

在解析Vue3的响应式原理之前,首先需要了解两个ES6新增的API:PorxyReflect

Proxy

Proxy: 代理,顾名思义主要用于为对象创建一个代理,从而实现对对象基本操作的拦截和自定义。可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。基本语法:

let proxy = new Proxy(target, handler);
  • target: 需要拦截的目标对象

  • handler: 也是一个对象,用来定制拦截行为 举个例子:

const obj = {name: 'John',age: 16
}const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj与objProxy是否相等',obj === objProxy);
// 输出
[Log] obj.age – 20
[Log] objProxy.age – 20 
[Log] obj与objProxy是否相等 – false

这里objProxyhandler为空,则直接指向被代理对象,并且代理对象与数据源对象并不全等.如果需要更加灵活的拦截对象的操作,就需要在handler中添加对应的属性。例如:

const obj = {name: 'John',age: 16
}const handler = {get(target, key, receiver) {console.log(`获取对象属性${key}值`)return target[key]},set(target, key, value, receiver) {console.log(`设置对象属性${key}值`)target[key] = value},deleteProperty(target, key) {console.log(`删除对象属性${key}值`)return delete target[key]},
}const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)// 输出
[Log] 获取对象属性age值 (example01.html, line 22)
[Log] 16 (example01.html, line 36)
[Log] 设置对象属性age值 (example01.html, line 26)
[Log] 删除对象属性age值 (example01.html, line 30)
[Log] true (example01.html, line 38)

上面的例子,我们在捕获器中定义了set()get()deleteProperty()属性,通过对proxy的操作实现了对obj的操作拦截。这些属性的触发方法有如下参数:

  • target —— 是目标对象,该对象被作为第一个参数传递给new Proxy

  • key —— 目标属性名称

  • value —— 目标属性的值

  • receiver —— 指向的是当前操作 正确的上下文。如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所指向的 this 对象。通常,receiver这就是 proxy 对象本身,但是如果我们从 proxy 继承,则receiver指的是从该 proxy 继承的对象

  • 当然除了以上三个还有一些常用的属性操作方法:

    • has(),拦截:in操作符.

    • ownKeys(),拦截:Object.getOwnPropertyNames(proxy) Object.getOwnPropertySymbols(proxy) Object.keys(proxy)

    • construct(),拦截:new 操作等

Reflect

Reflect: 反射,就是将代理的内容反射出去。ReflectProxy一样,也是 ES6 为了操作对象而提供的新 API。它提供拦截JavaScript操作的方法,这些方法与Proxy handlers 提供的的方法是一一对应的,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。且 Reflect 不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。还是上面的例子

const obj = {name: 'John',age: 16
}const handler = {get(target, key, receiver) {console.log(`获取对象属性${key}值`)return Reflect.get(target, key, receiver)},set(target, key, value, receiver) {console.log(`设置对象属性${key}值`)return Reflect.set(target, key, value, receiver)},deleteProperty(target, key) {console.log(`删除对象属性${key}值`)return Reflect.deleteProperty(target, key)},
}const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

上面的例子中

  • Reflect.get()代替target[key]操作

  • Reflect.set()代替target[key] = value操作

  • Reflect.deleteProperty()代替delete target[key]操作 当然除了上面的方法还有一些常用的Reflect方法:

Reflect.construct(target, args)
Reflect.has(target, name)
Reflect.ownKeys(target)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)

三. reactive、ref源码解析

了解了ProxyReflect,看下Vue3是如何通过porxy实现响应式的。其核心是下面要介绍的两个方法:reactiveref.这里依照Vue3.2版本的源码进行解析。

reactive的源码实现

打开源文件,找到文件packages/reactivity/src/reactive.ts 查看源码。

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)
}
  • 刚开始对target进行响应式只读判断,如果为true,则直接返回targetreactive实现的核心方法是createReactiveObject()

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.// exception: calling readonly() on a reactive objectif (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}// target already has corresponding Proxyconst existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}// only a whitelist of value types can be observed.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
}
  • createReactiveObject()方法有五个参数:

    • target: 传入的原始目标对象

    • isReadonly: 是否是只读的标识

    • baseHandlers: 为普通对象创建proxy时的第二个参数handler

    • collectionHandlers: 为collection类型对象创建proxy时的第二个参数handler

    • proxyMap: WeakMap类型的map,主要用于存储 target与他的proxy之间的对应关系

function targetTypeMap(rawType: string) {switch (rawType) {case 'Object':case 'Array':return TargetType.COMMONcase 'Map':case 'Set':case 'WeakMap':case 'WeakSet':return TargetType.COLLECTIONdefault:return TargetType.INVALID}
}
  • 源码可以看到,他将对象分为COMMON对象(ObjectArray)与COLLECTION类型对象(MapSetWeakMapWeakSet),这样区分的主要目的是为了根据不通的对象类型,来定制不同的handler

  • createReactiveObject()的前几行,进行了一系列的判断:

    • 首先判断target是否是对象,如果为false,直接return

    • 判断target是否是响应式对象,如果为true,直接return

    • 判断是否已经为target创建过proxy了,如果为true,直接return

    • 判断target是否是刚才上面提到的6种对象类型,如果为false,直接return

    • 如果以上条件都满足,则为target创建proxy,并return这个proxy

接下来就是根据不同的对象类型,传入不同的handler的逻辑处理了,主要关注baseHandlers,里面存在五个属性操作方法,这重点解析getset方法。

源码位置:packages/reactivity/src/baseHandlers.ts

export const mutableHandlers: ProxyHandler<object> = {get,set,deleteProperty,has,ownKeys
}
get与依赖收集
  • 可以看到mutableHandlers里面就是我们熟悉的各种钩子函数。当我们对proxy对象进行访问或是修改时,调用相应的函数进行处理。首先看get里面是如何对访问target的副作用函数进行收集的:

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? shallow? shallowReadonlyMap: readonlyMap: shallow? shallowReactiveMap: 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) : isNonTrackableKeys(key)) {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)) {// Convert returned value into a proxy as well. we do the isObject check// here to avoid invalid value warning. Also need to lazy access readonly// and reactive here to avoid circular dependency.return isReadonly ? readonly(res) : reactive(res)}return res}
}
  • 如果key值为__v_isReactive__v_isReadonly进行相应的返回,如果key==='__v_raw'并且WeakMapkeytarget的值不为空,则返回target

  • 如果target是数组,则 重写/增强 数组对应的方法

    在这些方法里面调用track()进行依赖收集

    • 数组元素的查找方法includes、indexOf、lastIndexOf

    • 修改原数组 的方法:push、pop、unshift、shift、splice

  • Reflect.get()方法的返回值,也就是当前数据对象的属性值res进行判断,如果res是普通对象且非只读,则调用track()进行依赖收集

  • 如果res是浅层响应,直接返回,如果resref对象,则返回其value

  • 如果res对象类型并且是只读的,则调用readonly(res),否则递归调用reactive(res)方法

  • 如果以上都不满足,直接向外返回对应的 属性值

那么核心方法就是如果利用**track()**进行依赖收集的处理了,源码在``packages/reactivity/src/effect.ts`

export function track(target: object, type: TrackOpTypes, key: unknown) {if (!isTracking()) {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 = createDep()))}const eventInfo = __DEV__? { effect: activeEffect, target, type, key }: undefinedtrackEffects(dep, eventInfo)
}
  • 首先进行是否正在进行依赖收集的判断处理

  • const targetMap = new WeakMap<any, KeyToDepMap>()创建一个targetMap容器,用于保存和当前响应式对象相关的依赖内容,本身是一个 WeakMap类型

  • 将对应的 响应式对象 作为 targetMaptargetMapValue是一个depsMap(属于 Map 实例), depsMap 存储的就是和当前响应式对象的每一个 key 对应的具体依赖

  • depsMap是响应式数据对象的key,Value是一个deps(属于 Set 实例),这里之所以使用Set是为了避免副作用函数的重复添加,避免重复调用

以上就是整个**get()**捕获器以及依赖收集的核心流程。

set与依赖更新

我们在回到baseHandlers中看Set捕获器中是如何进行依赖更新的

function createSetter(shallow = false) {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 = valuereturn 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 originalif (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}
}
  • 首先进行旧值的保存oldValue

  • 如果不是浅层响应,target是普通对象,并且旧值是个响应式对象,则执行赋值操作:oldValue.value = value ,返回true,表示赋值成功

  • 判断是否存在对应key值hadKey

  • 执行Reflect.set设置对应的属性值

  • 判断对象是原始原型链上的内容(非自定义添加),则不触发依赖更新

  • 根据目标对象不存在对应的 key, 调用trigger,进行依赖更新

以上就是整个baseHandlers关于依赖收集依赖更新的核心流程。

ref的源码实现

我们知道ref可以定义基本数据类型、引用数据类型的响应式。来看下它的源码实现:packages/reactivity/src/ref.ts

export function ref(value?: unknown) {return createRef(value)
}function createRef(rawValue: unknown, shallow = false) {if (isRef(rawValue)) {return rawValue}return new RefImpl(rawValue, shallow)
}class RefImpl<T> {private _value: Tprivate _rawValue: Tpublic dep?: Dep = undefinedpublic readonly __v_isRef = trueconstructor(value: T, public readonly _shallow = false) {this._rawValue = _shallow ? value : toRaw(value)this._value = _shallow ? value : convert(value)}get value() {trackRefValue(this)return this._value}set value(newVal) {newVal = this._shallow ? newVal : toRaw(newVal)if (hasChanged(newVal, this._rawValue)) {this._rawValue = newValthis._value = this._shallow ? newVal : convert(newVal)triggerRefValue(this, newVal)}}
}
  • 从上面的函数调用流程可以看出,实现ref的核心就是实例化了一个RefImpl对象。为什么这里要实例化一个RefImpl对象呢,其目的在于 Proxy 代理的目标也是对象类型,无法通过为基本数据类型创建proxy的方式来进行数据代理。只能把基本数据类型包装为一个对象,通过自定义的 get、set 方法进行 依赖收集依赖更新

  • 来看RefImpl对象属性的含义:

    • _ value:用于保存ref当前值,如果传递的参数是对象,它就是用于保存经过reactive函数转化后的值,否则_value_rawValue相同

    • _ rawValue:用于保存当前ref值对应的原始值,如果传递的参数是对象,它就是用于保存转化前的原始值,否则_value_rawValue相同。这里toRaw()函数的作用就是将的响应式对象转为普通对象

    • dep:是一个Set类型的数据,用来存储当前的ref值收集的依赖。至于这里为什么用Set上面我们有阐述,这里也是同样的道理

    • _v_isRef :标记位,只要被ref定义了,都会标识当前数据为一个Ref,也就是它的值标记为true

    • 另外可以很清楚的看到RefImpl类暴露给实例对象的get、set方法是value,所以对于ref定义的响应式数据的操作我们都要带上**.value**

  • 如果传入的值是对象类型,会调用convert()方法,这个方法里面会调用reactive()方法对其进行响应式处理

  • RefImpl实例关键就在于trackRefValue(this)triggerRefValue(this, newVal)的两个函数的处理,我们大概也知道它们就是依赖收集依赖更新,原理基本与reactive处理方式类似,这里就不在阐述了

五. 总结

  • 对于基础数据类型只能通过ref来实现其响应式,核心还是将其包装成一个RefImpl对象,并在内部通过自定义的 get value()set value(newVal)实现依赖收集与依赖更新。

  • 对于对象类型refreactive都可以将其转化为响应式数据,但其在ref内部,最终还是会调用reactive函数实现转化。reactive函数,主要通过创建了Proxy实例对象,通过Reflect实现数据的获取与修改。

一些参考:

https://github.com/vuejs/vue

https://zh.javascript.info/proxy#reflect

- END -

关于奇舞团

奇舞团是 360 集团最大的大前端团队,代表集团参与 W3C 和 ECMA 会员(TC39)工作。奇舞团非常重视人才培养,有工程师、讲师、翻译官、业务接口人、团队 Leader 等多种发展方向供员工选择,并辅以提供相应的技术力、专业力、通用力、领导力等培训课程。奇舞团以开放和求贤的心态欢迎各种优秀人才关注和加入奇舞团。

4e6f39033c9c9821f060ffa32caf7fda.png


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

相关文章

Midjourney使用技巧

Midjourney使用技巧&#xff1a; 一、使用肯定描述 Midjourney 目前不是特别懂定的描述词&#xff0c;尽量不要用 not、but、except、without 等否定词&#xff0c;失败率会变高。 因为描述词里有提到的某个关键词时&#xff0c;AI 就很可能画出来&#xff0c;忽略否定的逻辑…

平台建设的7大问题:蚂蚁AI平台实践深度总结

本文作者&#xff1a;蚂蚁集团资深产品专家栢柠&#xff0c;先后负责蚂蚁AI平台、风控平台产品工作。 过去几年&#xff0c;我和团队一直在负责蚂蚁集团内部相关平台产品的设计和运营工作。 这些平台产品包括人工智能部的A/B测试平台、机器学习平台、金融知识图谱平台、NLP平…

无人船毕设进展

智能无人测量船设计进展&#xff08;硬件系统&#xff09; 目录前言课题目的研究现状控制系统&#xff08;Pixhawk&#xff09;动力系统供电系统通讯系统传感器系统pixhawk2.4.8接口分析控制系统详细分析技术路线参考文献 目录 前言 面临毕业季&#xff0c;由于考研失利&#…

恋爱必修课

文章目录 爱与被爱,是人一生最值得学习的能力恋爱,是一项技能恋爱,是自我成长一. 是终点也是起点:你的恋爱目标是什么?为什么要有恋爱目标?什么是好的恋爱目标?第一条:再差的目标,也胜过没有目标。第二条:再好的目标,也得服从人生目标第三条:恋爱目标,要以我为主如…

团队建设游戏分享

团队建设游戏分享 这是我总结的一些,希望能有用,以后我挥发更多的。期待交流~~~~ 团队游戏原则:  每个成员积极参与  请不要对不同的意见说:NO!  将自己的所想告诉其他成员 主持团队游戏的注意点:  尊重、重视每一个参与者  恪守时间、规则  细心观察,充分…

自动驾驶商用车需要什么样的电气架构?

交流群 | 进“滑板底盘群”请加微信号&#xff1a;xsh041388 交流群 | 进“域控制器群”请加微信号&#xff1a;ckc1087 备注信息&#xff1a;滑板底盘/域控制器真实姓名、公司、岗位 本文是作者在九章智驾上发布的第三篇文章&#xff0c;前两篇分别为《干掉保险丝和继电器&…

剖析虚幻渲染体系(14)- 延展篇:现代渲染引擎演变史Part 1(萌芽期)

Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/course/detail/35475 目录* 14.1 本篇概述 14.1.1 游戏引擎简介 14.1.2 游戏引擎模块 14.1.3 游戏引擎列表 - 14.1.3.1 Unreal Engine - 14.1.…

【HTML】HTML网页设计---海贼王网页前端设计

1、引言 设计结课作业,课程设计无处下手&#xff0c;网页要求的总数量太多&#xff1f;没有合适的模板&#xff1f;数据库&#xff0c;java&#xff0c;python&#xff0c;vue&#xff0c;html作业复杂工程量过大&#xff1f;毕设毫无头绪等等一系列问题。你想要解决的问题&am…