前言
这是vue3系列源码的第五章,使用的vue3版本是3.2.45
。
推荐
背景
在上一篇文章中,我们看了setup中的代码是什么时候执行的,我们顺手提到了ref函数的调用。
这一节,我们就详细看一下我们用ref
, reactive
函数定义响应式变量的时候,都发生了啥。
前置
我们先看一下我们定义的App.vue
组件。
js
<template>
<div>{{ aa }}</div>
<div>{{ bb.name }}</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
const aa = ref('小识')
const bb = reactive({ name: '谭记' })
</script>
我们这里只定义了两个变量,一个ref变量,一个reactive变量。
基于上篇文章的基础,我们这里直接到setupStatefulComponent
函数中的callWithErrorHandling(setup, instance...)
中去,直接进入setup的执行。
ref
首先我们看到的是ref
函数的执行。
ref
函数是定义在reactivity
模块中,
js
function ref(value) {
return createRef(value, false);
}
调用了cerateRef
函数。
js
function createRef(rawValue, shallow) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
这里会先判断一下传进来的变量是不是已经是响应式的,isRef
函数
js
function isRef(r) {
return !!(r && r.__v_isRef === true);
}
这里面判断是不是响应式的对象,其实就是根据__v_isRef
属性来判断的。
这里并不是一个响应式的对象,所以最终返回了一个对象new RefImpl
RefImpl
看一下这个构造函数
js
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;
this.dep = undefined;
this.__v_isRef = true;
this._rawValue = __v_isShallow ? value : toRaw(value);
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
newVal = useDirectValue ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = useDirectValue ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
}
先看一下传入的参数:
- value, '小识'
- __v_isShallow,false
这个构造函数主要做了这几件事:
- 设置 __v_isShallow: false
- 设置 __v_isRef: true, 那么下次就能判断出这个对象已经是一个响应式对象了
- 设置 _rawValue: 小识
- 设置 _value:小识
- 设置 value属性的get set
由此可见,ref最终返回的是一个RefImpl
对象。
那么ref
的设置就到此结束,关于set
和get
我们后面再看。
reactive
js
function reactive(target) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target;
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
这里先判断了一下isReadonly。
js
function isReadonly(value) {
return !!(value && value["__v_isReadonly" /* ReactiveFlags.IS_READONLY */]);
}
也是通过一个属性来判断的,这里用的是__v_isReadonly
reactive
的核心是createReactiveObject
函数
createReactiveObject
js
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
if (!isObject(target)) {
if ((process.env.NODE_ENV !== 'production')) {
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (target["__v_raw" /* ReactiveFlags.RAW */] &&
!(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {
return target;
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// only specific value types can be observed.
const targetType = getTargetType(target);
if (targetType === 0 /* TargetType.INVALID */) {
return target;
}
const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
先看一下参数:
- target,'{name: '谭记'}
这个函数主要做了这几件事情:
- 这里一进来会做一个是否是对象的判断。reactive只接受传入对象,而不能像ref那样,传入一个数字,字符串等基本数据类型
- 通过
getTargetType
函数获取了数据类型,这里是Object,结果是1 - 通过proxy创建了一个代理对象,这里我们详细看一下proxy的
baseHandlers
- 最后在proxyMap里存一下当前的代理对象
由此可见,reactive最终返回了一个proxy代理对象。
那么我们我们定义的ref,reactive变量在template中使用的时候,又会发生什么。
在 页面到底是从什么时候开始渲染的这篇文章里面,我们提到渲染的时候会走到renderComponentRoot
这个函数,在这个函数里面执行了
js
result = normalizeVNode( render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx) )
我们在这里就详细的看一下执行render函数的过程。
render
render
函数的执行,其实是一个字符串匹配的过程。
当读到aa
变量的时候,会直接读到我们定义的ref变量。
这里我们在上一篇文章中提到过,setup执行的结果会通过proxyRefs
函数进行一次proxy代理,这是代理的具体option
:
js
const shallowUnwrapHandlers = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key];
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
else {
return Reflect.set(target, key, value, receiver);
}
}
};
unref就是直接返回ref的value属性值
js
function unref(ref) {
return isRef(ref) ? ref.value : ref;
}
因此,这里会读到aa.value
。
上面我们提到ref对象最终返回的是一个RefImpl
构造函数得到的对象,这里读aa.value
的时候触发了value的get。
js
get value() {
trackRefValue(this);
return this._value;
}
看一下trackRefValue函数
trackRefValue
js
function trackRefValue(ref) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
if ((process.env.NODE_ENV !== 'production')) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: "get" /* TrackOpTypes.GET */,
key: 'value'
});
}
else {
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
}
参数就是aa的那个ref对象。
这个函数的核心是:
- trackEffects
传入trackEffects
函数的参数里有一个dep对象。
js
const createDep = (effects) => {
const dep = new Set(effects);
dep.w = 0;
dep.n = 0;
return dep;
};
- dep对象中存储的是依赖
w
属性通常用于表示当前依赖的状态n
属性通常用于表示该依赖的计数
js
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false;
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit; // set newly tracked
shouldTrack = !wasTracked(dep);
}
}
else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect);
}
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo));
}
}
}
trackEffects函数主要干了这几件事:
- dep.n和trackOpBit按位或运算,0 | 2 得到 2
- 计算shouldTrackd的值,得到的值为true
这里的activeEffect对象其实就是new ReactiveEffect
得到的对象,前文提到过 --> 页面到底是从什么时候开始渲染的
这里我们来捋一下dep和activeEffect这二者的关系。
dep
表示一个响应式数据的依赖集合。每个dep
对象代表一个数据(可以是一个 ref、reactive 对象等, 这里就代表了aa对象),它维护了一个存储 effect 函数的集合,这些 effect 函数依赖于这个数据。- 而对于activeEffect对象,在这里其实就是解析到{{ aa }}插值表达式生成的。
这一段是追踪依赖关系的核心逻辑。
按位或运算:将各个数位的数字进行逻辑或,都是 0 才为 0,否则为 1。
总结一下,这里的trackRefValue其实就是做了依赖追踪。
createBaseVNode
render函数对每一个标签解析之后,其实还会调用createBaseVNode
函数创建vnode。
js
function createBaseVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag = 0,
dynamicProps: string[] | null = null,
shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
isBlockNode = false,
needFullChildrenNormalization = false
) {
const vnode = {
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null,
ctx: currentRenderingInstance
} as VNode
// track vnode for block tree
if (
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
currentBlock.push(vnode)
}
return vnode
}
这里其实就是根据<div>小识谭记</div>
生成vnode。
接下来我们看一下{{bb.name}}的解析,前面其实和aa是一样的,只不过在get的option上有区别。
createGetter
js
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (!isReadonly) {
track(target, "get" /* TrackOpTypes.GET */, key);
}
return res;
};
}
这里最终是触发了track
函数
js
function track(target, type, key) {
if (shouldTrack && activeEffect) {
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 = (process.env.NODE_ENV !== 'production')
? { effect: activeEffect, target, type, key }
: undefined;
trackEffects(dep, eventInfo);
}
}
可以看见,最终也是触发了trackEffects
函数,和ref定义的变量在依赖收集上并没有本质上的不同。后面也是对这一段生成了vnode。
最后,又生成了一个type为fragment的vnode。
并把这个vonde作为参数传进了normalizeVNode
函数中。
js
result = normalizeVNode( render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx) )
总结
这篇文章,我们主要看了一下ref
, reactive
函数是如何定义响应式变量的。定义的方式稍有不同,但是核心都是对get
, set
的劫持。
这里,我们还顺带看了一下get的流程,主要就是一个依赖收集。
这里我们没有看一下set的过程。
那么在后面的文章里,我们会专门讲一下vue3的响应式原理。