共计 8513 个字符,预计需要花费 22 分钟才能阅读完成。
本文次要分以下两个局部对 Composition API 的原理进行解读:
reactive
API 原理ref
API 原理
reactive
API 原理
关上源码能够找到 reactive
的入口,在 composition-api/src/reactivity/reactive.ts,咱们先从函数入口开始剖析 reactive
产生了什么事件,通过之前的学习咱们晓得,reactive
用于创立响应式对象,须要传递一个一般对象作为参数。
export function reactive<T = any>(obj: T): UnwrapRef<T> {if (process.env.NODE_ENV !== 'production' && !obj) {warn('"reactive()" is called without provide an "object".'); | |
// @ts-ignore | |
return; | |
} | |
if (!isPlainObject(obj) || isReactive(obj) || isNonReactive(obj) || !Object.isExtensible(obj)) {return obj as any;} | |
// 创立一个响应式对象 | |
const observed = observe(obj); | |
// 标记一个对象为响应式对象 | |
def(observed, ReactiveIdentifierKey, ReactiveIdentifier); | |
// 初始化对象的访问控制,便于拜访 ref 属性时主动解包装 | |
setupAccessControl(observed); | |
return observed as UnwrapRef<T>; | |
} |
首先,在开发环境下,会进行传参测验,如果没有传递对应的 obj
参数,开发环境下会给予开发者一个正告,在这种状况,为了不影响生产环境,生产环境下会将正告放过。
函数入口会查看类型,首先调用 isPlainObject
查看是否是对象。如果不是对象,将会间接返回该参数,因为非对象类型并不可察看。
而后调用 isReactive 判断对象是否曾经是响应式对象,上面是 isReactive
原型:
import { | |
AccessControlIdentifierKey, | |
ReactiveIdentifierKey, | |
NonReactiveIdentifierKey, | |
RefKey, | |
} from '../symbols'; | |
// ... | |
export function isReactive(obj: any): boolean {return hasOwn(obj, ReactiveIdentifierKey) && obj[ReactiveIdentifierKey] === ReactiveIdentifier; | |
} |
通过下面的代码咱们晓得,ReactiveIdentifierKey
和 ReactiveIdentifier
都是一个 Symbol
,关上composition-api/src/symbols.ts
能够看到,ReactiveIdentifierKey
和 ReactiveIdentifier
是曾经定义好的Symbol
:
import {hasSymbol} from './utils'; | |
function createSymbol(name: string): string {return hasSymbol ? (Symbol.for(name) as any) : name; | |
} | |
export const WatcherPreFlushQueueKey = createSymbol('vfa.key.preFlushQueue'); | |
export const WatcherPostFlushQueueKey = createSymbol('vfa.key.postFlushQueue'); | |
export const AccessControlIdentifierKey = createSymbol('vfa.key.accessControlIdentifier'); | |
export const ReactiveIdentifierKey = createSymbol('vfa.key.reactiveIdentifier'); | |
export const NonReactiveIdentifierKey = createSymbol('vfa.key.nonReactiveIdentifier'); | |
// must be a string, symbol key is ignored in reactive | |
export const RefKey = 'vfa.key.refKey'; |
在这里咱们大抵能够猜出来,在定义响应式对象时,Vue Composition API 会在响应式对象上设定一个 Symbol
的属性,属性值为 Symbol(vfa.key.reactiveIdentifier)
。从而咱们能够通过对象上是否具备Symbol(vfa.key.reactiveIdentifier)
来判断这个对象是否是响应式对象。
同理,因为 Vue Composition API 外部应用的 nonReactive
,用于保障一个对象不可响应,与isReactive
相似,也是通过查看对象是否具备对应的 Symbol
,即Symbol(vfa.key.nonReactiveIdentifier)
来实现的。
function isNonReactive(obj: any): boolean { | |
return (hasOwn(obj, NonReactiveIdentifierKey) && obj[NonReactiveIdentifierKey] === NonReactiveIdentifier | |
); | |
} |
此外,因为创立响应式对象须要拓展对象属性,通过 Object.isExtensible
来判断到,当对象是不可拓展对象,也将不可创立响应式对象。
接下来,在容错判断逻辑完结后,通过 observe
来创立响应式对象了,通过文档和源码咱们晓得 reactive
等同于 Vue 2.6+ 中 Vue.observable
,Vue Composition API 会尽可能通过Vue.observable
来创立响应式对象,但如果 Vue 版本低于 2.6,将通过 new Vue
的形式来创立一个 Vue 组件,将 obj
作为组件外部状态来保障其响应式。对于 Vue 2.x 中如何实现响应式对象,笔者之前也有写过一篇文章,在这里就不过多论述。
参考:前端 vue 面试题具体解答
function observe<T>(obj: T): T {const Vue = getCurrentVue(); | |
let observed: T; | |
if (Vue.observable) {observed = Vue.observable(obj); | |
} else { | |
const vm = createComponentInstance(Vue, { | |
data: {?state: obj,}, | |
}); | |
observed = vm._data.?state; | |
} | |
return observed; | |
} |
接下来,会在对象上设置 Symbol(vfa.key.reactiveIdentifier)
属性,def
是一个工具函数,其实就是Object.defineProperty
:
export function def(obj: Object, key: string, val: any, enumerable?: boolean) { | |
Object.defineProperty(obj, key, { | |
value: val, | |
enumerable: !!enumerable, | |
writable: true, | |
configurable: true, | |
}); | |
} |
接下来,调用 setupAccessControl(observed)
就是 reactive
的外围局部了,通过之前的文章咱们晓得:间接获取包装对象的值必须应用 .value
,然而,如果包装对象作为另一个响应式对象的属性,拜访响应式对象的属性值时,Vue 外部会主动开展包装对象。同时,在模板渲染的上下文中,也会被主动开展。setupAccessControl
就是帮忙咱们做这件事:
/** * Proxing property access of target. * We can do unwrapping and other things here. */ | |
function setupAccessControl(target: AnyObject): void { | |
// 首先须要保障设定访问控制参数的合法性 | |
// 除了与后面雷同的保障响应式对象 target 是对象类型和不是 nonReactive 对象外 | |
// 还须要保障保障对象不是数组(因为无奈为数组元素设定属性描述符)// 也须要保障不是 ref 对象(因为 ref 的 value 属性用于保障属性的响应式),以及不能是 Vue 组件实例。if (!isPlainObject(target) || | |
isNonReactive(target) || | |
Array.isArray(target) || | |
isRef(target) || | |
isComponentInstance(target) | |
) {return;} | |
// 一旦初始化了该属性的访问控制,也会往响应式对象 target 上设定一个 Symbol(vfa.key.accessControlIdentifier)的属性。// 用于标记该对象以及初始化实现了主动解包装的访问控制。if (hasOwn(target, AccessControlIdentifierKey) && | |
target[AccessControlIdentifierKey] === AccessControlIdentifier | |
) {return;} | |
if (Object.isExtensible(target)) {def(target, AccessControlIdentifierKey, AccessControlIdentifier); | |
} | |
const keys = Object.keys(target); | |
// 遍历对象自身的可枚举属性,这里留神:通过 def 办法定义的 Symbol 标记并非可枚举属性 | |
for (let i = 0; i < keys.length; i++) {defineAccessControl(target, keys[i]); | |
} | |
} |
首先须要保障设定访问控制参数的合法性,除了与后面雷同的保障响应式对象 target
是对象类型和不是 nonReactive
对象外,还须要保障保障对象不是数组(因为无奈为数组元素设定属性描述符),也须要保障不是 ref
对象(因为 ref
的value
属性用于保障属性的响应式),以及不能是 Vue
组件实例。
与下面雷同的是,一旦初始化了该属性的访问控制,也会往响应式对象 target
上设定一个 Symbol(vfa.key.accessControlIdentifier)
的属性。用于标记该对象以及初始化实现了主动解包装的访问控制。
上面来看外围局部:通过 Object.keys(target)
获取到对象自身非继承的属性,之后调用 defineAccessControl
,这里须要留神的一点是,Object.keys
只会遍历响应式对象 target
自身的非继承的可枚举属性,通过 def 办法定义的 Symbol 标记 Symbol(vfa.key.accessControlIdentifier)
等,并非可枚举属性,因此不会受到访问控制的影响。
const keys = Object.keys(target); | |
// 遍历对象自身的可枚举属性,这里留神:通过 def 办法定义的 Symbol 标记并非可枚举属性 | |
for (let i = 0; i < keys.length; i++) {defineAccessControl(target, keys[i]); | |
} |
defineAccessControl
会创立响应式对象的属性的代理,以便 ref
主动进行解包装,不便开发者在开发过程中用到 ref
时,手动执行一次 .value
的解封装:
/** * Auto unwrapping when access property */ | |
export function defineAccessControl(target: AnyObject, key: any, val?: any) { | |
// 每一个 Vue 可察看对象都有一个__ob__属性,这个属性用于收集 watch 这个状态的观察者,这个属性是一个外部属性,不须要解封装 | |
if (key === '__ob__') return; | |
let getter: (() => any) | undefined; | |
let setter: ((x: any) => void) | undefined; | |
const property = Object.getOwnPropertyDescriptor(target, key); | |
if (property) { | |
// 保障能够扭转指标对象属性的自有属性描述符:如果对象的自有属性描述符的 configurable 为 false,无奈为该属性设定属性描述符,无奈设定 getter 和 setter | |
if (property.configurable === false) {return;} | |
getter = property.get; | |
setter = property.set; | |
// arguments.length === 2 示意没有传入 val 参数,并且不是 readonly 对象,这时该属性的值:响应式对象的属性能够间接取值拿到 | |
// 传入 val 的状况是应用 vue.set,composition 也提供了 set api | |
if ((!getter || setter) /* not only have getter */ && arguments.length === 2) {val = target[key]; | |
} | |
} | |
// 嵌套对象的状况,实际上 setupAccessControl 是递归调用的 | |
setupAccessControl(val); | |
Object.defineProperty(target, key, { | |
enumerable: true, | |
configurable: true, | |
get: function getterHandler() {const value = getter ? getter.call(target) : val; | |
// if the key is equal to RefKey, skip the unwrap logic | |
// 对 ref 对象取值时,属性名不是 ref 对象的 Symbol 标记 RefKey,getterHandler 返回包装对象的值,即 `value.value` | |
if (key !== RefKey && isRef(value)) {return value.value;} else { | |
// 不是 ref 对象,getterHandler 间接返回其值,即 `value` | |
return value; | |
} | |
}, | |
set: function setterHandler(newVal) { | |
// 属性没有 setter,证实这个属性不是被 Vue 察看的,间接返回 | |
if (getter && !setter) return; | |
// 给响应式对象属性赋值时,先拿到 | |
const value = getter ? getter.call(target) : val; | |
// If the key is equal to RefKey, skip the unwrap logic | |
// If and only if "value" is ref and "newVal" is not a ref, | |
// the assignment should be proxied to "value" ref. | |
// 对 ref 对象赋值时,并且属性名不是 ref 对象的 Symbol 标记 RefKey,如果 newVal 不是 ref 对象,setterHandler 将代理到对 ref 对象的 value 属性赋值,即 `value.value = newVal` | |
if (key !== RefKey && isRef(value) && !isRef(newVal)) {value.value = newVal;} else if (setter) { | |
// 该对象有 setter,间接调用 setter 即可 | |
// 会告诉依赖这一属性状态的对象更新 | |
setter.call(target, newVal); | |
} else if (isRef(newVal)) { | |
// 既没有 getter 也没有 setter 的状况,一般键值,间接赋值 | |
val = newVal; | |
} | |
// 每次从新赋值,思考到嵌套对象的状况:对 newVal 从新初始化访问控制 | |
setupAccessControl(newVal); | |
}, | |
}); | |
} |
通过下面的代码,咱们能够看到,为了给 ref
对象主动解包装,defineAccessControl
会为 reactive
对象从新设置 getter
和setter
,思考到嵌套对象的状况,在初始化响应式对象和从新为响应式对象的某个属性赋值时,会深递归执行 setupAccessControl
,保障整个嵌套对象所有层级的ref
属性都能够主动解包装。
ref
API 原理
ref
的入口在 composition-api/src/reactivity/ref.ts,上面先来看 ref
函数:
class RefImpl<T> implements Ref<T> { | |
public value!: T; | |
constructor({get, set}: RefOption<T>) { | |
proxy(this, 'value', { | |
get, | |
set, | |
}); | |
} | |
} | |
export function createRef<T>(options: RefOption<T>) { | |
// seal the ref, this could prevent ref from being observed | |
// It's safe to seal the ref, since we really shoulnd't extend it. | |
// related issues: #79 | |
// 密封 ref,保障其安全性 | |
return Object.seal(new RefImpl<T>(options)); | |
} | |
export function ref(raw?: any): any { | |
// 先创立一个可察看对象,这个 value 实际上是一个 Vue Composition API 外部应用的局部变量,并不会裸露给开发者 | |
const value = reactive({[RefKey]: raw }); | |
// 创立 ref,对其取值其实最终代理到了 value | |
return createRef({get: () => value[RefKey] as any, | |
set: v => ((value[RefKey] as any) = v), | |
}); | |
} |
看到 ref
的入口首先调用 reactive
来创立了一个可察看对象,这个 value 实际上是一个 Vue Composition API 外部应用的局部变量,并不会裸露给开发者。它具备一个属性值 RefKey
,其实也是个Symbol
,而后调用createRef
。ref
返回 createRef
创立的 ref
对象,ref
对象实际上通过 getter
和setter
代理到咱们通过 const value = reactive({[RefKey]: raw });
创立的局部变量 value
的值,便于咱们获取 ref
包装对象的值。
另外为了保障 ref
对象的安全性,不被开发者意外篡改,也为了保障 Vue 不会再为 ref
对象再创立代理(因为包装对象的 value
属性的确没有必要再另外被察看),因而调用 Object.seal
将对象密封。保障只能扭转其value
,而不会为其拓展属性。
isRef
很简略,通过判断传递的参数是否继承自RefImpl
:
export function isRef<T>(value: any): value is Ref<T> {return value instanceof RefImpl;}
toRefs
将 reactive
对象转换为一般对象,其中后果对象上的每个属性都是指向原始对象中相应属性的 ref
援用对象,这在组合函数返回响应式状态时十分有用,这样保障了开发者应用对象解构或拓展运算符不会失落原有响应式对象的响应。其实也只是递归调用createRef
。
export function toRefs<T extends Data = Data>(obj: T): Refs<T> {if (!isPlainObject(obj)) return obj as any; | |
const res: Refs<T> = {} as any; | |
Object.keys(obj).forEach(key => {let val: any = obj[key]; | |
// use ref to proxy the property | |
if (!isRef(val)) { | |
val = createRef<any>({get: () => obj[key], | |
set: v => (obj[key as keyof T] = v), | |
}); | |
} | |
// todo | |
res[key as keyof T] = val; | |
}); | |
return res; | |
} |
小结
本文次要形容 Vue Composition API 响应式局部的代码,reactive
和 ref
都是基于 Vue 响应式对象上做再次封装,ref
的外部其实是一个响应式对象,ref
的 value
属性将代理到这个响应式对象上,这个响应式对象对开发者是不可见的,使得调用过程绝对敌对,而 reactive
提供了对 ref
主动解包装性能,以晋升开发者开发体验。