TNTWeb – 全称腾讯新闻前端团队,组内小伙伴在 Web 前端、NodeJS 开发、UI 设计、挪动 APP 等大前端畛域都有所实际和积攒。
目前团队次要反对腾讯新闻各业务的前端开发,业务开发之余也积攒积淀了一些前端基础设施,赋能业务提效和产品翻新。
团队提倡开源共建,领有各种技术大牛,团队 Github 地址:https://github.com/tnfe
本文作者 dravenwu
本篇文章将会围绕 Vue3 的另外一个次要的文件夹 reactivity 来进行解说,也就是 Vue3 中对外裸露的 compositionApi 的局部,,越来越有 React Hooks 的滋味了。reactivity 文件夹上面蕴含多个文件,次要性能在于 computed、effect、reactive、ref;其余的文件是为其进行服务的,另外还有一个主入口文件 index。reactivity 上面对外裸露的所有 api 可见下图,咱们本篇文件会联合应用对这些性能进行源码剖析。
注释
注释在这里,正式开始。
computed
computed 的含意与 Vue2 中的含意是一样的,计算属性;应用形式也是和 Vue2 中差不多的,有两种应用形式:
computed 应用
const {reactive, readonly, computed, ref} = Vue;
const app = Vue.createApp({});
app.component('TestComponent', {setup(props) {
// reactive
const state = reactive({
count: 0,
number: 10
})
// computed getter
const computedCount = computed(() => {return state.count + 10})
// computed set get
const computedNumber = computed({get: () => {return state.number + 100},
set: (value) => {state.number = value - 50}
})
const changeCount = function(){
state.count++;
computedNumber.value = 200
}
return {
state,
changeCount,
computedCount,
computedNumber
}
},
template: `
<div>
<h2>init count:<i>{{state.count}}</i></h2>
<h2>computedCount:<i>{{computedCount}}</i></h2>
<h2>computedNumber:<i>{{computedNumber}}</i></h2>
<button @click="changeCount">changeCount</button>
</div>
`
})
app.mount('#demo')
下面代码能够看到两次对 computed 的应用,第一次传递的是一个函数,第二次传递的是一个蕴含 get 和 set 的对象。
computed 源码剖析
接下来,咱们来看下 computed 的源码:
// @file packages/reactivity/src/computed.ts
export function computed<T>(getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = __DEV__
? () => {console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set
) as any
}
下面是 computed 的入口的源码,此处和 Vue2 中的写法是一样的,都是对参数进行判断,生成 getter 和 setter,这里最初调用的是 ComputedRefImpl;
// packages/reactivity/src/computed.ts
class ComputedRefImpl<T> {
private _value!: T
private _dirty = true
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = effect(getter, {
lazy: true,
scheduler: () => {if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {if (this._dirty) {this._value = this.effect()
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newValue: T) {this._setter(newValue)
}
}
如上,是 ComputedRefImpl 的源码。ComputedRefImpl 是一个 class,外部蕴含_value、_dirty、effect、__v_isRef、ReactiveFlags.IS_READONLY 等属性,还包含 constructor 和 get、set 等函数。理解的同学都晓得,会首先调用构造函数也就是 constructor;调用 effect 为 effect 属性赋值,把 isReadonly 赋值给 ReactiveFlags.IS_READONLY 属性,对于 effect,咱们前面讲这块。此时 ComputedRefImpl 执行实现。
当获取以后 computed 的值的时候,如下面应用中 computedCount
在 template 中进行获取值的时候,会调用下面 class 内的 get 办法,get 办法外部调用的是 this.effect 进行数据的获取,_dirty 属性是为了数据的缓存,依赖未发生变化,则不会调用 effect,应用之前的 value 进行返回。track 是跟踪以后 get 调用的轨迹。
当为 computed 赋值的时候,如下面应用中 computedNumber.value = 200
的时候,,会调用下面 class 内的 set 办法,set 外部还是调用了之前传递进来的函数。
reactive
接下来对 reactive 的解说
reactive 应用
reactive 官网给的解释是:返回对象的响应式正本
。先来看下 reactive 的应用
const {reactive} = Vue;
const app = Vue.createApp({});
app.component('TestComponent', {setup(props) {
// reactive
const state = reactive({count: 0})
const changeCount = function(){state.count++;}
return {
state,
changeCount
}
},
template: `
<div>
<h2>reactive count:<i>{{state.count}}</i></h2>
<button @click="changeCount">changeCount</button>
</div>
`
})
app.mount('#demo')
当点击 changeCount 的时候,state.count 会 ++,同时映射到 h2-dom。
reactive 源码解读
// @file 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
)
}
如上源码能够看到,如果 target 有值并且 target 的 [ReactiveFlags.IS_READONLY] 属性,也就是__v_isReadonly 为 true 的话,会间接返回以后对象,不做任何解决,前面对 state.count 的扭转也不会映射到 dom 当中。如果不满足下面条件,则会调用 createReactiveObject
函数,传递 4 个参数:
- target 为原始对象;
- 第二个是 isReadonly,为 false;
- 第三个参数 mutableHandlers 是 reactive 对应的处理函数;
- 第四个参数是对于汇合类型的对象进行解决的函数。
对于这个外围的函数,咱们待会来进行解说。
readonly 应用
当初咱们来看下 Vue3 提供给咱们的 reactivity 上面的第二个 api:readonly。
官网给出的定义是:获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。只读代理是深层的:拜访的任何嵌套 property 也是只读的
const {readonly} = Vue;
const app = Vue.createApp({});
app.component('TestComponent', {setup(props) {const read = readonly({count: 1})
const changeRead = function(){read.count++;}
return {
read,
changeRead
}
},
template: `
<div>
<h2>readonly count:<i>{{read.count}}</i></h2>
<button @click="changeRead">changeRead</button>
</div>
`
})
app.mount('#demo')
下面代码,是 readonly 的应用,在此试验了一下对 readonly 返回后的后果 read,进行了扭转的尝试,发现是扭转不了的,属于只读,同时还会打印正告Set operation on key "count" failed: target is readonly.
readonly 源码解读
// @file packages/reactivity/src/reactive.ts
export function readonly<T extends object>(target: T): DeepReadonly<UnwrapNestedRefs<T>> {
return createReactiveObject(
target,
true,
readonlyHandlers,
readonlyCollectionHandlers
)
}
下面就是 readonly 的源码入口,与 reactive 一样,都是调用的 createReactiveObject 函数:
- 第一个参数还是 target;
- 第二个是 isReadonly,为 true;
- 第三个参数 readonlyHandlers 是 readonly 对应的处理函数;
- 第四个参数是对于汇合类型的对象进行解决的 readonly 所对应的函数。
shallowReactive 应用
官网文档给的解释:创立一个响应式代理,该代理跟踪其本身 property 的响应性,但不执行嵌套对象的深度响应式转换 (裸露原始值)。
来看下 shallowReactive 的应用
const {shallowReactive} = Vue;
const app = Vue.createApp({});
app.component('TestComponent', {setup(props) {
const state = shallowReactive({
foo: 1,
nested: {bar: 2}
})
const change = function(){
state.foo++
state.nested.bar++
}
return {
state,
change
}
},
template: `
<div>
<h2>foo:<i>{{state.foo}}</i></h2>
<h2>bar:<i>{{state.nested.bar}}</i></h2>
<button @click="change">change</button>
</div>
`
})
app.mount('#demo')
下面代码根本是齐全依照官网来写的,不过,试了下成果和官网上的成果不一样,并不是 shallow 类型,而是对外部的属性也进行了监听,bar 的扭转也会响应式的反映到 dom 当中去。也不晓得是我姿态不对,还是 Vue3 的 bug。
shallowReactive 源码解读
// @file packages/reactivity/src/reactive.ts
export function shallowReactive<T extends object>(target: T): T {
return createReactiveObject(
target,
false,
shallowReactiveHandlers,
shallowCollectionHandlers
)
}
下面就是 shallowReactive 的源码入口,与 reactive 和 readonly 一样,都是调用的 createReactiveObject 函数:
- 第一个参数还是 target;
- 第二个是 isReadonly,为 false;
- 第三个参数 shallowReactiveHandlers 是 shallowReactive 对应的处理函数;
-
第四个参数是对于汇合类型的对象进行解决的 shallowReactive 所对应的函数。
shallowReadonly 应用
官网给出的解释:
创立一个代理,使其本身的 property 为只读,但不执行嵌套对象的深度只读转换 (裸露原始值)。
来看下应用:const {shallowReadonly} = Vue; const app = Vue.createApp({}); app.component('TestComponent', {setup(props) { const state = shallowReadonly({ foo: 1, nested: {bar: 2} }) const change = function(){ state.foo++ state.nested.bar++ } return { state, change } }, template: ` <div> <h2>foo:<i>{{state.foo}}</i></h2> <h2>bar:<i>{{state.nested.bar}}</i></h2> <button @click="change">change</button> </div> ` }) app.mount('#demo')
下面代码根本是齐全依照官网来写的,foo 的扭转不被容许,依照官网阐明 state.nested.bar 是容许被扭转的,在下面例子中,发现 state.nested.bar 的值是会扭转的,然而不会响应到 dom 上。
shallowReadonly 源码解读
// @file packages/reactivity/src/reactive.ts export function shallowReadonly<T extends object>(target: T): Readonly<{[K in keyof T]: UnwrapNestedRefs<T[K]> }> { return createReactiveObject( target, true, shallowReadonlyHandlers, readonlyCollectionHandlers ) }
下面就是 shallowReadonly 的源码入口,与 reactive 和 readonly 一样,都是调用的 createReactiveObject 函数:
- 第一个参数还是 target;
- 第二个是 isReadonly,为 true;
- 第三个参数 shallowReadonlyHandlers 是 shallowReadonly 对应的处理函数;
- 第四个参数是对于汇合类型的对象进行解决的 shallowReadonly 所对应的函数。
isReadonly
isReadonly: 查看对象是否是由 readonly 创立的只读代理。
应用如下:
const only = readonly({count: 1})
isOnly = isReadonly(only) // true
源码如下:
export function isReadonly(value: unknown): boolean {return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
ReactiveFlags.IS_READONLY 是一个字符串,值为:__v_isReadonly,挂到对象下面就是属性,判断以后对象的__v_isReadonly 属性是否是 true,并返回。
isReactive
isReadonly: 查看对象是否是 reactive 创立的响应式 proxy。
应用如下:
const tive = reactive({count: 1})
isOnly = isReactive(tive) // true
源码如下:
export function isReactive(value: unknown): boolean {if (isReadonly(value)) {return isReactive((value as Target)[ReactiveFlags.RAW])
}
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
首先调用了下面提到的 isReadonly 办法判断是否是 readonly 创立的对象;如果是的话,则进一步应用以后对象的 RAW 属性调用 isReactive 来判断;如果不是则判断__v_isReactive 是否为 true;返回判断的后果。
ReactiveFlags.RAW 是一个字符串,值为:__v_raw,挂到对象下面就是属性,也就是原始对象,判断是否是 reactive 代理的原始对象;
ReactiveFlags.IS_READONLY 也是一个字符串,值为:__v_isReactive,挂到对象下面就是属性
isProxy
isProxy: 查看对象是否是 reactive 或 readonly 创立的代理。
应用如下:
const tive = reactive({count: 1})
const only = readonly({count: 1})
is1 = isProxy(tive) // true
is2 = isProxy(only) // true
源码如下:
export function isProxy(value: unknown): boolean {return isReactive(value) || isReadonly(value)
}
调用下面提到的 isReadonly 办法和 isReactive 判断是否是 proxy 的对象。
markRaw
markRaw:标记一个对象,使其永远不会转换为代理。返回对象自身。
应用如下:
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
const bar = reactive({foo})
console.log(isReactive(bar)) // true
console.log(isReactive(bar.foo)) // false
从下面应用中能够看到,markRaw 只对以后对象自身无效,被标记的对象作为属性的时候,大对象 bar 还是能够进行响应式解决的,然而 bar 外面的以后被标记的对象 foo,还是一个非响应式对象,永远是 foo 对象自身。
export function markRaw<T extends object>(value: T): T {def(value, ReactiveFlags.SKIP, true)
return value
}
export const def = (obj: object, key: string | symbol, value: any) => {
Object.defineProperty(obj, key, {
configurable: true,
enumerable: false,
value
})
}
下面能够看到 markRaw 的源码,就是给要标记的对象减少了一个属性(ReactiveFlags.SKIP, 也就是__v_skip),并赋值 true,所有要给以后对象进行响应式解决的时候,都会被疏忽。
toRaw
toRaw:返回 reactive 或 readonly 代理的原始对象。这是一个本义口,可用于长期读取而不会引起代理拜访 / 跟踪开销,也可用于写入而不会触发更改。不倡议保留对原始对象的长久援用。请审慎应用。
既然 Vue 让咱们审慎应用,咱们还是在能够不应用的的时候不应用的好,这个就是把代理的原始对象进行返回。
const obj = {project: 'reactive'}
const reactiveObj = reactive(obj)
console.log(toRaw(reactiveObj) === obj) // true
源码:
export function toRaw<T>(observed: T): T {
return ((observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed
)
}
返回以后对象的 ReactiveFlags.RAW(也就是__v_raw)属性指向的对象,也就是对象自身,对于在什么中央给 ReactiveFlags.RAW 赋值的,前面会看到这部分。
createReactiveObject
下面的 reactive、readonly、shallowReactive、shallowReadonly,都用到了 createReactiveObject 函数,当初咱们来看看这个函数的源码。
function createReactiveObject(target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) {if (!isObject(target)) {if (__DEV__) {console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {return target}
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {return existingProxy}
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
}
源码解读:
- 首先,target 得是一个对象,不是对象的话,间接返回以后值;当然 Vue3 也提供了对值的响应式的办法:ref,前面讲。
- 判断有原始对象且,不是只读或者不是响应式的对象,则返回以后对象,这个中央真 TM 绕。
- 依据是否是 isReadonly,获取到代理存储的 map,,如果之前代理过,曾经存在,则把之前代理过的 proxy 返回。
- 判断 target 的类型,getTargetType 外部会对 target 对象进行判断,返回是 common、collection 或者 invalid;如果不可用类型 (invalid),则间接返回以后对象。 此处会用到下面讲到的__v_skip。可用的类型就两个,一个是 common,一个是 collection;
- 接下来就是没有代理过,获取代理的过程。
new Proxy
,如果是 collection 则应用传递进来的 collectionHandlers,否则 (也就是 common) 则应用 baseHandlers; - 代理存储所应用的 map,存储以后 proxy;
- 返回以后 proxy。
通过下面 reactive、readonly、shallowReactive、shallowReadonly 的解说,能够看到对于汇合和 common 类型,提供了几种不同的解决对象,对象中所蕴含的内容也是不一样的,咱们在这里来比照着看下:
basehandler:
如上图,能够看到,basehandler 外面所提供的函数,咱们一一来看下。
deleteProperty
// @file packages/reactivity/src/baseHandlers.ts
function deleteProperty(target: object, key: string | symbol): boolean {const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
- 获取以后对象是否有以后 key => hadKey;
- 获取到以后的 value 存储为 oldValue;
- 调用 Reflect.deleteProperty 进行对以后对象 target 删除以后 key 的操作,返回后果为是否删除胜利 ->result;
- 删除胜利,并且有以后 key,则调用 trigger,触发 effect。
-
返回删除是否胜利的后果。
ownKeys
// @file packages/reactivity/src/baseHandlers.ts function ownKeys(target: object): (string | number | symbol)[] {track(target, TrackOpTypes.ITERATE, ITERATE_KEY) return Reflect.ownKeys(target) }
这个函数很简略了就,获取 target 对象本人的属性 key;跟踪获取的轨迹,而后调用 Reflect.ownKeys 获取后果。
has
// @file packages/reactivity/src/baseHandlers.ts function has(target: object, key: string | symbol): boolean {const result = Reflect.has(target, key) if (!isSymbol(key) || !builtInSymbols.has(key)) {track(target, TrackOpTypes.HAS, key) } return result }
- 调用 Reflect.has 获取以后对象是否有以后 key;
- 不是 Symbol 类型的 key,或者不是 Symbol 自身的属性,调用 track 跟踪 has 调用的轨迹。
-
返回后果,result。
createSetter
function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean {const oldValue = (target as any)[key] if (!shallow) {value = toRaw(value) if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else {} const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) if (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 } }
函数工厂,依据 shallow 生成 set 函数。set 函数承受 4 个参数:target 为指标对象;key 为设置的属性;value 为设置的值;receiver 为 Reflect 的额定参数(如果遇到 setter,receiver 则为 setter 调用时的 this 值)。
- 首先获取到 oldValue;
- 如果非浅响应式,也就是正式状况的时候,获取到 value 的原始对象并赋值给 value,如果 target 对象不是数组且 oldValue 是 ref 类型的响应式类型,并且新 value 不是 ref 类型的响应式,为 oldValue 赋值(ref 类型的响应式对象,须要为对象的 value 赋值)。
- 上面也就是深度响应式的代码逻辑了。
- 如果是数组并且 key 是数字类型的,则直接判断下标,否则调用 hasOwn 获取,是否蕴含以后 key => hadKey;
- 调用 Reflect.set 进行设置值;
- 如果指标对象和 receiver 的原始对象相等,则 hadKey,调用 trigger 触发 add 操作;否则,调用 trigger 触发 set 操作。
-
把 set 解决的后果返回,result。
createGetter
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 ? readonlyMap : reactiveMap).get(target) ) {return target} const targetIsArray = isArray(target) if (targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) const keyIsSymbol = isSymbol(key) if ( keyIsSymbol ? builtInSymbols.has(key as symbol) : key === `__proto__` || key === `__v_isRef` ) {return res} if (!isReadonly) {track(target, TrackOpTypes.GET, key) } if (shallow) {return res} if (isRef(res)) {const shouldUnwrap = !targetIsArray || !isIntegerKey(key) return shouldUnwrap ? res.value : res } if (isObject(res)) {return isReadonly ? readonly(res) : reactive(res) } return res } }
函数工厂,依据 shallow 生成 get 函数。get 函数承受 3 个参数:target 为指标对象;key 为设置的属性;receiver 为 Reflect 的额定参数(如果遇到 setter,receiver 则为 setter 调用时的 this 值)。
- 如果 key 是__v_isReactive,则间接返回!isReadonly,通过下面的图可得悉,reactive 相干的调用 createGetter,传递的是 false,也就是会间接返回 true;
- 如果 key 是__v_isReadonly,则间接返回 isReadonly,同样的通过下面的图能够得悉,readonly 相干的调用 createGetter,传递的是 true,也就是会间接返回 true;
- 如果 key 是__v_raw 并且 receiver 等于 proxyMap 存储的 target 对象的 proxy,也就是获取原始对象,则间接返回 target;
- 如果是数组的话,则会走自定义的办法,arrayInstrumentations;arrayInstrumentations 是和 Vue2 中对数组的改写是一样的逻辑;
- 上面会对 key 进行判断,如果 Symbol 对象并且是 Set 外面自定义的办法;或者 key 为__proto__或__v_isRef,则间接把
Reflect.get(target, key, receiver)
获取到的值间接返回; - 如果非只读状况下,调用 track 跟踪 get 轨迹;
- 如果是 shallow,非深度响应式,也是间接把下面获取到的 res 间接返回;
- 如果是 ref 对象,则会调用.value 获取值进行返回;
- 剩下的状况下,如果失去的 res 是个对象,则依据 isReadonly 调用 readonly 或 reactive 获取值,进行返回;
-
最初有一个 res 保底返回;
collectionHandler:
来看下 createInstrumentationGetter 的源码,下面图中三个都是调用此办法生成对应的解决对象。function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) { const instrumentations = shallow ? shallowInstrumentations : isReadonly ? readonlyInstrumentations : mutableInstrumentations return ( target: CollectionTypes, key: string | symbol, receiver: CollectionTypes ) => {if (key === ReactiveFlags.IS_REACTIVE) {return !isReadonly} else if (key === ReactiveFlags.IS_READONLY) {return isReadonly} else if (key === ReactiveFlags.RAW) {return target} return Reflect.get(hasOwn(instrumentations, key) && key in target ? instrumentations : target, key, receiver ) } }
下面 createInstrumentationGetter 函数依据 isReadonly 和 shallow 返回一个函数;
- 依据 isReadonly 和 shallow,获取到对应的 instrumentations;此对象蕴含了对汇合操作的所有办法;
- 而后就把上面的函数进行了返回,createInstrumentationGetter 相当于是一个闭包;
- 返回的函数外面在执行调用的时候,会先对 key 进行判断,如果拜访的是 Vue 的公有变量,也就是下面的__v_isReactive、__v_isReadonly、__v_raw 等,会间接给出不同的返回;
- 如果不是 Vue 的下面的三个公有变量,则会调用 Reflect.get 来获取对象的值;instrumentations,也就是重写的办法汇合,不在此汇合外面的,则会间接调用 target 本人的办法。
reactive 完结
至此,reactive 文件外面的这些办法咱们都梳理了一遍,简略的做了源码的剖析和解读,感兴趣的读者能够深刻源码钻研下 Vue 中为何这样实现。
Refs
接下来咱们将开始对 ref 及其从属办法的应用和解说。
ref
首先,咱们对 ref 进行解说,官网给出的解释是:承受一个外部值并返回一个响应式且可变的 ref 对象。ref 对象具备指向外部值的单个 property .value。
先来看下 ref 的应用。
const {ref} = Vue;
const app = Vue.createApp({});
app.component('TestComponent', {setup(props) {const count = ref(0)
const obj = ref({number: 10})
const change = function(){
count.value++;
obj.value.number++
}
return {
count,
obj,
change
}
},
template: `
<div>
<h2>count:<i>{{count}}</i></h2>
<h2>number:<i>{{obj.number}}</i></h2>
<button @click="change">change</button>
</div>
`
})
app.mount('#demo')
下面是 ref 的应用,能够看到 ref 承受的是一个一般类型的值或者是一个对象,Vue 官网给出的例子是不蕴含传递对象的,其实这也就是 Vue 不提倡应用 ref 来响应式一个对象,如果是对对象的响应式,Vue 还是提倡应用下面 reactive 来实现;第二个须要留神点在于template 中对 ref 对象的援用是不须要加上 value 属性来获取值,如上 ref 对象 count 在 js 中须要 count.value, 然而在 template 种只需 count 即可;
来看下 ref 的源码实现
// @file packages/reactivity/src/ref.ts
export function ref<T extends object>(value: T): T extends Ref ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
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: T
public readonly __v_isRef = true
constructor(private _rawValue: T, private readonly _shallow = false) {this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val
export const hasChanged = (value: any, oldValue: any): boolean =>
value !== oldValue && (value === value || oldValue === oldValue)
下面是依照运行轨迹来看的 Vue3 中 ref 的源码局部;依据 ref 的申明能够看到 ref 承受任何参数,返回类型为 Ref 对象,外部调用的是 createRef;
- createRef 函数外部会先对 value 进行判断,如果曾经是 ref 对象的话,间接返回以后 value,否则就调用 new RefImpl 来生成 ref 对象进行返回。
- constructor 外面会判断是否是浅响应_shallow,浅的话,间接返回_rawValue,否则调用 convert 来返回;能够看到除了公有属性_value 外,还有一个__v_isRef 的只读属性为 true;
- convert 外面则会对 val 进行判断了,对象则调用 reactive,否则间接返回 val,此处也就能够看到下面 ref 也能够承受对象作为参数的原因了。
- get 外面会跟踪调用轨迹,track;返回以后 value;
-
set 外面会调用 hasChanged 判断是否产生了扭转,此处会对 NaN 进行 check,因为 NaN 与啥都不相等;设置新的值,同时调用 trigger 触发 set 调用。
isRef
isRef 很显著就是 判断是否是 ref 对象的办法。应用如下:
const count = ref(0) const is = isRef(count) const is2 = isRef(10)
来看下源码,源码也很简略:
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T> export function isRef(r: any): r is Ref {return Boolean(r && r.__v_isRef === true) }
此处就应用到了 RefImpl 外面那个只读属性了,判断__v_isRef 是否为 true 就能够了。
shallowRef
官网给出的解释:创立一个 ref,它跟踪本人的 .value 更改,但不会使其值成为响应式的。
shallowRef 的源码如下:export function shallowRef<T extends object>(value: T): T extends Ref ? T : Ref<T> export function shallowRef<T>(value: T): Ref<T> export function shallowRef<T = any>(): Ref<T | undefined> export function shallowRef(value?: unknown) {return createRef(value, true) }
shallowRef 与 ref 的调用流程是一样的,不过是多了个参数,导致_shallow 为 true,就在 RefImpl 外面调用时,间接返回了以后 value,而不会进行到 convert 函数。
unRef
官网解释:如果参数为 ref,则返回外部值,否则返回参数自身。 源码如下:
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {return isRef(ref) ? (ref.value as any) : ref }
的确如官网所说,就一行代码,ref 对象则返回其 value,否则间接返回 ref。
triggerRef
官网给出的解释:手动执行与 shallowRef 关联的任何成果。,比拟含糊,艰深点就是手动触发一次 effect 的调用;
看下应用:const count = ref(0) const change = function(){ count.value++; triggerRef(count) } const shallow = shallowRef({greet: 'Hello, world'}) watchEffect(() => {console.log(count.value) console.log(shallow.value.greet) }) shallow.value.greet = 'Hello, universe'
源码如下:
export function triggerRef(ref: Ref) {trigger(ref, TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0) }
toRef
官网给出的解释是:能够用来为源响应式对象上的 property 属性创立一个 ref。而后能够将 ref 传递进来,从而放弃对其源 property 的响应式连贯。 简略形容就是为对象的一个属性减少一个援用,这个援用能够随便应用,响应式不变。来看下源码:
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
): Ref<T[K]> {return isRef(object[key])
? object[key]
: (new ObjectRefImpl(object, key) as any)
}
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(private readonly _object: T, private readonly _key: K) {}
get value() {return this._object[this._key]
}
set value(newVal) {this._object[this._key] = newVal
}
}
这部分的代码比较简单,也比拟容易读懂,和下面 RefImpl 一样的是都减少了一个只读的__v_isRef 属性。
toRefs
官网对 toRefs 给出的解释是:将响应式对象转换为一般对象,其中后果对象的每个 property 都是指向原始对象相应 property 的 ref。 艰深点形容就是把响应式对象的每个属性,都变成 ref 对象。来看下源码:
export function toRefs<T extends object>(object: T): ToRefs<T> {if (__DEV__ && !isProxy(object)) {console.warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {ret[key] = toRef(object, key)
}
return ret
}
这里尤为要求是一个响应式的对象,非响应式对象还会打印正告。for 循环调用下面讲到的 toRef 函数,把对象外面的每个属性都变为 ref 对象。
customRef
官网给出的解释是:创立一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式管制。它须要一个工厂函数 来看下 customRef 的源码:
class CustomRefImpl<T> {private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
public readonly __v_isRef = true
constructor(factory: CustomRefFactory<T>) {const { get, set} = factory(() => track(this, TrackOpTypes.GET, 'value'),
() => trigger(this, TriggerOpTypes.SET, 'value')
)
this._get = get
this._set = set
}
get value() {return this._get()
}
set value(newVal) {this._set(newVal)
}
}
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {return new CustomRefImpl(factory) as any
}
绝对应的,应用的时候,承受的是一个 factory,factory 是一个函数,参数为 track 和 trigger,同时 factory 的返回须蕴含两个函数,一个为 get,一个为 set。track 就是 effect 的 track,trigger 也是 effect 的 trigger;来看下应用:
const {customRef} = Vue;
const app = Vue.createApp({});
function useDebouncedRef(value, delay = 200) {
let timeout
return customRef((track, trigger) => {
return {get() {track()
return value
},
set(newValue) {clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
trigger()}, delay)
}
}
})
}
app.component('TestComponent', {setup(props) {
return {text: useDebouncedRef('hello')
}
},
template: `
<div>
<input v-model="text" />
</div>
`
})
app.mount('#demo')
下面是 customRef 的应用的例子,和官网的例子是一样的,可能实现防抖,同时也可能显式的管制什么时候调用 track 来跟踪和什么时候来调用 trigger 来触发扭转。
Refs 完结
下面咱们对 refs 外面的几种办法做了源码的解读和局部的 api 是如何应用的,对于 Vue3 为何提供了两种响应式的计划:reactive 和 Refs,这其实就和代码格调有关系了,有的同学习惯应用对象,而有的同学习惯应用变量,Vue3 为这两种计划都提供了,想用哪个用哪个。
effect
其实能够看到下面好多中央都用到了这个办法,包含 effect、track、trigger 等都是 effect 外面提供的办法,effect 外面提供的办法属于 Vue 的外部办法,不对外裸露。上面咱们挨个来看看这部分的源码,
isEffect
isEffect 是为判断是否是有副作用的函数。来看下源码:
export function isEffect(fn: any): fn is ReactiveEffect {return fn && fn._isEffect === true}
能够看到下面的判断,就是对函数的_isEffect 进行判断,非常简单。
effect
effect 作为 Vue2 和 Vue3 中外围的局部,都有这个的概念,重中之重,来看下这部分的源码:
export function effect<T = any>(fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {if (isEffect(fn)) {fn = fn.raw}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {effect()
}
return effect
}
function createReactiveEffect<T = any>(fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {const effect = function reactiveEffect(): unknown {if (!effect.active) {return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {cleanup(effect)
try {enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()} finally {effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
let shouldTrack = true
const trackStack: boolean[] = []
export function pauseTracking() {trackStack.push(shouldTrack)
shouldTrack = false
}
export function enableTracking() {trackStack.push(shouldTrack)
shouldTrack = true
}
export function resetTracking() {const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
}
如上,就是 effect 局部的源码。顺着执行程序一步步走下来。
- 调用方调用 effect 函数,参数为函数 fn,options(默认为{});
- 判断是否曾经是 effect 过的函数,如果是的话,则间接把原函数返回。
- 调用 createReactiveEffect 生成以后 fn 对应的 effect 函数,把下面的参数 fn 和 options 间接传进去;
- 判断 options 外面的 lazy 是否是 false,如果不是懒解决,就间接调用下对应的 effect 函数;
- 返回生成的 effect 函数。
接下来看下 createReactiveEffect 函数的调用过程。
- 为 effect 函数赋值,临时先不思考 reactiveEffect 函数外部到底干了什么,只有明确创立了个函数,并赋值给了 effect 变量。
- 而后为 effect 函数增加属性:id, _isEffect, active, raw, deps, options
- 把 effect 返回了。
上面咱们回到下面非 lazy 状况下,调用 effect,此时就会执行 reactiveEffect 函数。
- 首先判断了是否是 active 状态,如果不是,阐明以后 effect 函数曾经处于生效状态,间接返回
return options.scheduler ? undefined : fn()
。 - 查看调用栈 effectStack 外面是否有以后 effect,如果无以后 effect,接着执行上面的代码。
- 先调用 cleanup,把以后所有依赖此 effect 的全副清掉,deps 是个数组,元素为 Set,Set 外面放的则是 ReactiveEffect,也就是 effect;
- 把以后 effect 入栈,并将以后 effect 置为以后沉闷 effect->activeEffect;后执行 fn 函数;
- finally,把 effect 出栈,执行实现了,把 activeEffect 还原到之前的状态;
- 其中波及到调用轨迹栈的记录。和 shouldTrack 是否须要跟踪轨迹的解决。
stop
stop 办法是用来进行以后 effect 的。属于 Vue3 外部办法,来看下源码:
export function stop(effect: ReactiveEffect) {if (effect.active) {cleanup(effect)
if (effect.options.onStop) {effect.options.onStop()
}
effect.active = false
}
}
- 调用 cleanup 清空掉,和下面调用 cleanup 一样。
- 执行以后 effect.options.onnStop 钩子函数。
- 把以后 effect 的 active 状态置为 false。
结言
本篇文章次要围绕 reactivity 文件夹外面提供给大家应用的 compositionApi 的局部进行了绝对应的应用和源码解读,大家感兴趣的还是去读下这部分的源码,毕竟这是 Vue3 新出的性能,越来越 react 的一步 ……
欢送大家一起来探讨 Vue3,刚出的版本,带来了新的同时,必定也会带着意想不到的惊喜(bug),让咱们发现它,解决掉它,也是一种提高,也是避免本人踩坑的好办法。