本文次要分以下两个局部对 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 reactiveexport 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 中如何实现响应式对象,笔者之前也有写过一篇文章,在这里就不过多论述。
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
:
参考 前端vue面试题具体解答
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
主动解包装性能,以晋升开发者开发体验。