【Vue3】源码解析
- 系列文章
- Proxy API
- Proxy和响应式对象reactive
- ref()方法运行原理
系列文章
【Vue3】源码解析-前置
【Vue3】源码解析-响应式原理
【Vue3】源码解析-虚拟DOM
Proxy API
见上
Proxy和响应式对象reactive
在Vue 3中,使用响应式对象方法如下代码所示:
import {ref,reactive} from 'vue'
...
setup(){const name = ref('test')const state = reactive({list: []})return {name,state}
}
...
在Vue 3中,Composition API中会经常使用创建响应式对象的方法ref/reactive,其内部就是利用了Proxy API来实现的,特别是借助handler的set方法,可以实现双向数据绑定相关的逻辑,这对于Vue 2中的Object.defineProperty()是很大的改变,主要提升如下:
- Object.defineProperty()只能单一的监听已有属性的修改或者变化,无法检测到对象属性的新增或删除(Vue
2中是采用$set()方法来解决),而Proxy则可以轻松实现。 - Object.defineProperty()无法监听响应式数据类型是数组的变化(主要是数组长度变化,Vue
2中采用重写数组相关方法并添加钩子来解决),而Proxy则可以轻松实现。
正是由于Proxy的特性,在原本使用Object.defineProperty()需要很复杂的方式才能实现的上面两种能力,在Proxy无需任何配置,利用其原生的特性就可以轻松实现。
ref()方法运行原理
在Vue 3的源码中,所有关于响应式的代码都在vue-next/package/reactivity下面,其中reactivity/src/index.ts里暴露了所有可以使用的方法。我们以常用的ref()方法举例,来看看Vue 3是如何利用Proxy的。 ref()方法的主要逻辑在reactivity/src/ref.ts中,其代码如下:
...
// 入口方法
export function ref(value?: unknown) {return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {// rawValue表示原始对象,shallow表示是否递归// 如果本身已经是ref对象,则直接返回if (isRef(rawValue)) {return rawValue}// 创建一个新的RefImpl对象return new RefImpl(rawValue, shallow)
}
...
createRef这个方法接收的第二个参数是shallow,表示是否是递归监听响应式,这个和另外一个响应式方法shallowRef()是对应的。在RefImpl构造函数中,有一个value属性,这个属性是由toReactive()方法所返回,toReactive()方法则在reactivity/src/reactive.ts文件中,如下代码所示:
class RefImpl<T> {...constructor(value: T, public readonly _shallow: boolean) {this._rawValue = _shallow ? value : toRaw(value)// 如果是非递归,调用toReactivethis._value = _shallow ? value : toReactive(value)}...
}
在reactive.ts中,则开始真正创建一个响应式对象,如下代码所示:
export function reactive(target: object) {// 如果是readonly,则直接返回,就不添加响应式了if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {return target}return createReactiveObject(target,// 原始对象false,// 是否readonlymutableHandlers,// proxy的handler对象baseHandlersmutableCollectionHandlers,// proxy的handler对象collectionHandlersreactiveMap// proxy对象映射)
}
其中,createReactiveObject()方法传递了两种handler,分别是baseHandlers和collectionHandlers,如果target的类型是Map,Set,WeakMap,WeakSet则会使用collectionHandlers,类型是Object,Array则会是baseHandlers,如果是一个基础对象,也不会创建Proxy对象,reactiveMap则存储所有响应式对象的映射关系,用来避免同一个对象的重复创建响应式。我们在来看看createReactiveObject()方法的实现,如下代码所示:
function createReactiveObject(...) {// 如果target不满足typeof val === 'object',则直接返回targetif (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)}return target}// 如果target已经是proxy对象或者只读,则直接返回// exception: calling readonly() on a reactive objectif (target[ReactiveFlags.RAW] &&!(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}// 如果target已经被创建过Proxy对象,则直接返回这个对象const existingProxy = proxyMap.get(target)if (existingProxy) {return existingProxy}// 只有符合类型的target才能被创建响应式const targetType = getTargetType(target)if (targetType === TargetType.INVALID) {return target}// 调用Proxy API创建响应式const proxy = new Proxy(target,targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)// 标记该对象已经创建过响应式proxyMap.set(target, proxy)return proxy
}
可以看到在createReactiveObject()方法中,主要做了以下事情:
- 防止只读和重复创建响应式。
- 根据不同的target类型选择不同的handler。
- 创建Proxy对象。
最终会调用new Proxy来创建响应式对象,我们以baseHandlers为例,看看这个handler是怎么实现的,在reactivity/src/baseHandlers.ts可以看到这部分代码,主要实现了这几个handler,如下代码所示:
const get = /*#__PURE__*/ createGetter()
...
export const mutableHandlers: ProxyHandler<object> = {get,set,deleteProperty,has,ownKeys
}
以handler.get为例看看在其内部做了什么操作,当我们尝试读取对象的属性时,便会进入get方法,其核心代码如下所示:
function createGetter(isReadonly = false, shallow = false) {return function get(target: Target, key: string | symbol, receiver: object) {if (key === ReactiveFlags.IS_REACTIVE) { // 如果访问对象的key是__v_isReactive,则直接返回常量return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {// 如果访问对象的key是__v_isReadonly,则直接返回常量return isReadonly} else if (// 如果访问对象的key是__v_raw,或者原始对象只读对象等等直接返回targetkey === ReactiveFlags.RAW &&receiver ===(isReadonly? shallow? shallowReadonlyMap: readonlyMap: shallow? shallowReactiveMap: reactiveMap).get(target)) {return target}// 如果target是数组类型const targetIsArray = isArray(target)// 并且访问的key值是数组的原生方法,那么直接返回调用结果if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)}// 求值const res = Reflect.get(target, key, receiver)// 判断访问的key是否是Symbol或者不需要响应式的key例如__proto__,__v_isRef,__isVueif (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {return res}// 收集响应式,为了后面的effect方法可以检测到if (!isReadonly) {track(target, TrackOpTypes.GET, key)}// 如果是非递归绑定,直接返回结果if (shallow) {return res}// 如果结果已经是响应式的,先判断类型,再返回if (isRef(res)) {const shouldUnwrap = !targetIsArray || !isIntegerKey(key)return shouldUnwrap ? res.value : res}// 如果当前key的结果也是一个对象,那么就要递归调用reactive方法对改对象再次执行响应式绑定逻辑if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res)}// 返回结果return res}
}
上面这段代码是Vue 3响应式的核心代码之一,其逻辑相对比较复杂,读者可以根据注释来理解,总结下来,这段代码主要做了以下事情:
- 对于handler.get方法来说,最终都会返回当前对象对应key的结果即obj[key],所以该段代码最终会return结果。
- 对非响应式key,只读key等直接返回对应的结果。
- 对于数组类型的target,key值如果是原型上的方法,例如includes,push,pop等,采用Reflect.get直接返回。
- 在effect添加收集监听track,为响应式监听服务。
- 当当前key对应的结果是一个对象时,为了保证set方法能够触发,需要循环递归的对这个对象进行响应式绑定即递归调用reactive()方法。
handler.get方法主要功能是对结果value的返回,那么我们看看handler.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)// 如果旧值已经是一个RefImpl对象且新值不是RefImpl对象// 例如var v = Vue.reactive({a:1,b:Vue.ref({c:3})})场景的set
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {oldValue.value = value // 直接将新值赋给旧址的响应式对象里return true}}// 用来判断是否是新增key还是更新key的值const hadKey =isArray(target) && isIntegerKey(key)? Number(key) < target.length: hasOwn(target, key)// 设置set结果,并添加监听effect逻辑const result = Reflect.set(target, key, value, receiver)// 判断target没有动过,包括在原型上添加或者删除某些项if (target === toRaw(receiver)) {if (!hadKey) {trigger(target, TriggerOpTypes.ADD, key, value)// 新增key的触发监听} else if (hasChanged(value, oldValue)) {trigger(target, TriggerOpTypes.SET, key, value, oldValue)// 更新key的触发监听}}// 返回set结果 true/falsereturn result}
}
handler.set方法核心功能是设置key对应的值即obj[key] = value,同时对新旧值进行逻辑判断和处理,最后添加上trigger触发监听track逻辑,便于触发effect。 如果读者感觉上述源码理解比较困难,笔者剔除一些边界和兼容判断,将整个流程进行梳理和简化,可以参考下面这段便于理解的代码:
let foo = {a:{c:3,d:{e:4}},b:2}
const isObject = (val)=>{return val !== null && typeof val === 'object'
}
const createProxy = (target)=>{let p = new Proxy(target,{get:(obj,key)=>{let res = obj[key] ? obj[key] : undefined// 添加监听track(target)// 判断类型,避免死循环if (isObject(res)) {return createProxy(res)// 循环递归调用} else {return res}},set: (obj, key, value)=> {console.log('set')obj[key] = value;// 触发监听trigger(target)return true}})return p
}let result = createProxy(foo)result.a.d.e = 6 // 打印出set
当尝试去修改一个多层嵌套的对象的属性时,会触发该属性的上一级对象的get方法,利用这个就可以对每个层级的对象添加Proxy代理,这样就实现了多层嵌套对象的属性修改问题,在此基础上同时添加track和trigger逻辑,就完成了基本的响应式流程。