又是调试源码,学习vue3实现原理的一天,加油!大家一起卷!
- ps:以下使用的是vue3.5.13版本
1.Ref获取元素或组件expose
1.1 基本用法
HelloWorld.vue
html
<script setup lang="ts">
import { ref } from 'vue';
const props = defineProps<{ msg: string }>();
const aRef = ref<number>(100);
const say = () => {
console.log(props.msg);
};
defineExpose({
hahaha: '醒目',
aRef,
say
});
</script>
<template>
<h1>{{ msg }}</h1>
</template>
App.vue
中,ref响应式变量名必须与ref属性值一致。
html
<template>
<div>
<button @click="onChange()" ref="Btn">change</button>
<p ref="smartRef">机智如我{{ countRef }}</p>
<HelloWorld msg="HelloWorld" ref="helloRef"></HelloWorld>
</div>
</template>
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue';
import { onMounted, ref } from 'vue';
const countRef = ref(0);
let Btn = undefined;
const smartRef = ref<HTMLParagraphElement>();
const helloRef = ref<InstanceType<typeof HelloWorld>>();
const onChange = () => {
countRef.value++;
};
onMounted(() => {
console.log(smartRef.value, helloRef.value, Btn);
});
</script>
执行结果
TemplateRef.vue
编译后setup函数
js
const _sfc_main = /* @__PURE__ */ _defineComponent({
__name: "TemplateRef",
setup(__props, { expose: __expose }) {
__expose();
const countRef = ref(0);
let Btn = void 0;
const smartRef = ref();
const helloRef = ref();
const onChange = () => {
countRef.value++;
};
onMounted(() => {
console.log(smartRef.value, helloRef.value, Btn);
});
const __returned__ = { countRef, get Btn() {
return Btn;
}, set Btn(v) {
Btn = v;
}, smartRef, helloRef, onChange, HelloWorld };
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
return __returned__;
}
});
TemplateRef.vue
编译后模板的render函数,可以看到设置了ref
属性的HelloWorld组件、P元素、Button元素都标记了NEED_PATCH
。
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", null, [
_createElementVNode(
"button",
{
onClick: $setup.onChange,
ref: "Btn"
},
"change",
512
/* NEED_PATCH */
),
_createElementVNode(
"p",
{ ref: "smartRef" },
"\u673A\u667A\u5982\u6211" + _toDisplayString($setup.countRef),
513
/* TEXT, NEED_PATCH */
),
_createVNode(
$setup["HelloWorld"],
{
msg: "HelloWorld",
ref: "helloRef"
},
null,
512
/* NEED_PATCH */
)
]);
}
1.2 ref获取真实DOM或组件的工作原理
patch
函数中,先将虚拟DOM渲染成真实DOM并挂载在页面上,然后判断当前最新的虚拟DOMn2
中是否配置了ref
属性,并且所在的组件实例parentComponent
已经已生成,若是,则通过setRef
函数给该组件实例对应的setup函数返回的变量挂载组件expose信息或真实DOM。
ts
const patch: PatchFn = (n1,n2,container,anchor = null,parentComponent = null,parentSuspense = null,
//...
) => {
//...
const { type, ref, shapeFlag } = n2;
//...
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
}
}
mountElement
中,虚拟DOMvNode
渲染成真实DOM过程中,会给vNode.el
属性挂上渲染后的真实DOM。
ts
const mountElement = ( vnode: VNode,
//...
) => {
//...
el = vnode.el = hostCreateElement(
vnode.type as string,
namespace,
props && props.is,
props,
)
//...
}
在setRef
函数中,特殊情况需特殊处理:
- 当存在多个相同
ref
属性值的元素或组件时,会将最后的那个元素或组件挂到变量的值上。这个跟vue2的不一样,vue2设置了相同ref
属性值的元素或组件,可以通过this.$refs
获取到设置了该ref
属性的所有元素或组件的数组。 - 当
KeepAlive
设置了ref
属性,并且KeepAlive
包裹了异步组件的情况,会判断异步组件是否已经生成组件实例,并将该异步组件实例挂在变量的值上。
ts
export function setRef(
rawRef: VNodeNormalizedRef,
oldRawRef: VNodeNormalizedRef | null,
parentSuspense: SuspenseBoundary | null,
vnode: VNode,
isUnmount = false,
): void {
//...
//KeepAlive包裹异步组件
if (isAsyncWrapper(vnode) && !isUnmount) {
if (
vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE &&
(vnode.type as ComponentOptions).__asyncResolved &&
vnode.component!.subTree.component
) {
setRef(rawRef, oldRawRef, parentSuspense, vnode.component!.subTree)
}
return
}
//获取ref.value的值
const refValue =
vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
? getComponentPublicInstance(vnode.component!)
: vnode.el
//未渲染值为空
const value = isUnmount ? null : refValue
//i: owner是组件实例,r: ref是ref属性值
const { i: owner, r: ref } = rawRef
//...
当获取ref
属性对象的值时,通过虚拟DOM的shapeFlag
判断类型并分别获取,若是组件,则通过getComponentPublicInstance
函数获取组件expose
暴露出来的相关变量或函数,若是元素,则直接获取虚拟DOM渲染成的真实DOMvnode.el
。
ts
export function getComponentPublicInstance(
instance: ComponentInternalInstance,
): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null {
if (instance.exposed) {
return (
instance.exposeProxy ||
(instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
get(target, key: string) {
if (key in target) {
//组合式API
return target[key]
} else if (key in publicPropertiesMap) {
//选项式API
return publicPropertiesMap[key](instance)
}
},
has(target, key: string) {
return key in target || key in publicPropertiesMap
},
}))
)
} else {
return instance.proxy
}
}
我之前的文vue3中ref为什么script中要用.value,而template模板中不需?介绍过,setupState
是组件实例的setup
函数执行返回的所有变量和函数。而setupState
是一个shallowUnwrapHandlers
代理操作的响应式变量,可以直接通过变量名获取或修改ref
的.value
值。
setRef
中,通过canSetSetupRef
函数判断setupState
中是否有ref
属性值命名的变量存在。
ts
const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
//refs用于缓存设置了ref属性的元素和组件expose
const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
const setupState = owner.setupState
const rawSetupState = toRaw(setupState)
const canSetSetupRef =
setupState === EMPTY_OBJ
? () => false
: (key: string) => {
if (__DEV__) {
//绑定的变量不是ref响应式变量,警告提示
if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {
warn(`Template ref "${key}" used on a non-ref value. ` +
`It will not work in the production build.`)
}
if (knownTemplateRefs.has(rawSetupState[key] as any)) {
return false
}
}
return hasOwn(rawSetupState, key)
}
//动态更新,置空旧的设置了ref属性的元素和组件
if (oldRef != null && oldRef !== ref) {
if (isString(oldRef)) {
refs[oldRef] = null
if (canSetSetupRef(oldRef)) {
setupState[oldRef] = null
}
} else if (isRef(oldRef)) {
oldRef.value = null
}
}
setRef
中,若设置了ref
属性的组件expose或元素存在,则将设置ref.value
值的任务函数doSet
通过queuePostRenderEffect
函数添加到渲染更新任务后的执行队列中,待组件渲染更新真实DOM完成再执行(为了解决获取不到异步组件的问题)。setRef
中,若设置了ref
属性的对象值为空,则代表组件已经卸载或元素已经移除,直接执行doSet
设置为空值。
ts
const _isString = isString(ref)
const _isRef = isRef(ref)
if (_isString || _isRef) {
const doSet = () => {
//...
}
if (value) {
(doSet as SchedulerJob).id = -1
queuePostRenderEffect(doSet, parentSuspense)
} else {
doSet()
}
}
//...
doSet
函数中,给组件实例的refs
中缓存设置了ref
属性值的子组件expose信息和元素真实DOM,再给zuj 实例的setupState
对应的ref
属性值变量赋值上子组件expose信息和元素真实DOM。
ts
const doSet = () => {
//...
if (_isString) {
refs[ref] = value
if (canSetSetupRef(ref)) {
setupState[ref] = value
}
}
//...
}
2.useTemplateRef获取元素或组件expose
2.1 基本用法
useTemplateRef
官方说法:返回一个浅层 ref,其值将与模板中的具有匹配 ref attribute 的元素或组件同步。 TemplateRef1.vue
,代码与上面的基本一致,除了使用useTemplateRef
,并且useTemplateRef
变量跟ref
属性值不同。
html
<template>
<div>
<button @click="onChange" ref="Btn">change</button>
<p ref="smartRef">机智如我{{ countRef }}</p>
<HelloWorld msg="HelloWorld" ref="helloRef"></HelloWorld>
</div>
</template>
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue';
import { onMounted, ref, useTemplateRef } from 'vue';
const countRef = ref(0);
let Btn: HTMLButtonElement;
const smartRef1 = useTemplateRef<HTMLParagraphElement>('smartRef');
const helloRef1 = useTemplateRef<InstanceType<typeof HelloWorld>>('helloRef');
const onChange = () => {
countRef.value++;
};
onMounted(() => {
console.log(smartRef1.value, helloRef1.value, Btn);
});
</script>
TemplateRef1.vue
编译后setup函数
js
const _sfc_main = /* @__PURE__ */ _defineComponent({
__name: "TemplateRef1",
setup(__props, { expose: __expose }) {
__expose();
const countRef = ref(0);
let Btn;
const smartRef1 = useTemplateRef("smartRef");
const helloRef1 = useTemplateRef("helloRef");
const onChange = () => {
countRef.value++;
};
onMounted(() => {
console.log(smartRef1.value, helloRef1.value, Btn);
});
const __returned__ = { countRef, get Btn() {
return Btn;
}, set Btn(v) {
Btn = v;
}, smartRef1, helloRef1, onChange, HelloWorld };
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
return __returned__;
}
});
执行结果与上面一致。
2.2 useTemplateRef的工作原理
useTemplateRef
实际上返回的是一个值为null
的shallowRef
浅层响应式变量r
,并且给当前组件实例的refs
添加key
关键词的数据劫持,getter
和setter
映射到浅层响应式变量r
的操作。
从上面的setRef
函数中可知组件实例的refs
缓存了所有设置了ref
属性的组件expose或元素,那么useTemplateRef
添加劫持可以通过refs
的key
关键词获取或修改浅层响应式变量r
的值。
这样可以解除setup函数返回的响应式变量与设置ref
属性值必须一致性的限制,只需key关键词与ref
属性值相同即可,使得响应式变量命名更灵活。
ts
export function useTemplateRef<T = unknown, Keys extends string = string>(
key: Keys,
): Readonly<ShallowRef<T | null>> {
const i = getCurrentInstance()
const r = shallowRef(null)
if (i) {
const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs
let desc: PropertyDescriptor | undefined
if (
__DEV__ &&
(desc = Object.getOwnPropertyDescriptor(refs, key)) &&
!desc.configurable
) {
warn(`useTemplateRef('${key}') already exists.`)
} else {
Object.defineProperty(refs, key, {
enumerable: true,
get: () => r.value,
set: val => (r.value = val),
})
}
} else if (__DEV__) {
warn(
`useTemplateRef() is called when there is no active component ` +
`instance to be associated with.`,
)
}
const ret = __DEV__ ? readonly(r) : r
if (__DEV__) {
knownTemplateRefs.add(ret)
}
return ret
}
组件实例每次渲染更新,设置了ref属性的每个组件每个元素在patch函数中都需要进行一次setRef
更新挂载的元素或组件expose,即便元素或组件expose的引用未改变也要重新赋值。一旦设置了ref属性的节点过多,就会很影响性能!
额~为什么没有类似patchFlag
的标记用来记录元素或组件expose的引用是否改变,从而跳过setRef
操作来优化执行流程。
然后,我在useTemplateRef
中注意到一个特殊的变量knownTemplateRefs
,一个WeakSet
。
- 开发环境时,
knownTemplateRefs
缓存了shallowRef
浅层响应式变量r
,在patch更新时,setRef
的canSetSetupRef
通过判断knownTemplateRefs
中是否ref属性值有缓存,从而跳过重新赋值(只适用于useTemplateRef
变量名与ref
属性值一致的情况)。
可见,vue团队也注意到这个setRef
频繁重新赋值的问题,并正在优化中!我猜,推出useTemplateRef
是为解决setRef
频繁赋值的问题做准备。
3.总结
那么现在可以回答了!
ref获取元素或组件是怎样实现的?
回答:
- patch函数中,会给设置了ref属性的虚拟DOM进行
setRef
操作。 - 在
setRef
中,判断设置了ref属性的虚拟DOM是组件还是元素,若是组件则获取组件实例的expose值,若是元素则直接获取真实DOMvnode.el
。 - 若设置了ref属性虚拟DOM的值不为空,则将【变量赋值】的任务,加入到渲染更新任务后执行队列。
- 待渲染更新任务完成,执行赋值,将元素真实DOM或子组件expose信息挂载在组件实例
setupState
的ref属性值命名的变量上。
useTemplateRef相对比ref获取元素或组件的方式有什么不同?
- API使用方式不同,两者回类型不一样,
ref
是深层响应式,useTemplateRef
返回的是浅层ref
,key
值与ref
属性值一致即可,变量名可以不一样,而ref
响应式变量需与对象的ref属性值一致。 - patch过程中,通过
setRef
操作将组件的expose或真实DOM挂在ref
响应式变量的.value
上,useTemplateRef
多了对refs
缓存的利用。 useTemplateRef
利用对组件实例缓存的refs
的key
关键词劫持,来操作浅层ref
,从而减少setup函数中对响应式变量命名的限制。
之前,关于vue3源码理解的笔记,感兴趣的小伙伴可以瞅瞅