手把手教你调试,弄懂vue3中ref为什么script中要用.value,而template模板中不需?
1.弄个简单的vue项目
RefTemplate.vue文件
<template>
<h1 :title="hello">Hello {{ msg }}!</h1>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const hello = ref<string>('Hello World!');
const msg = ref<number>(1);
</script>
main.ts文件
import App from './RefTemplate.vue';
import { createApp } from 'vue';
createApp(App).mount('#app');
2.配置vscode调试
- 在
.vscode文件夹下添加launch.json,配置调试环境和端口
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8086",
"webRoot": "${workspaceFolder}"
}
]
}
注意:项目运行的端口要与launch.json配置的端口一致。
- 直接命令窗口
npm run dev启动项目,然后点击调试按钮,会打开一个新的chrome浏览器窗口用于调试
- 先不打点,直接加载界面,可以在调试栏的
LOAD SCRIPTS中找到编译后文件和源文件
3.读取代码,探究原理
-
打开编译后的
RefTemplate.vue代码 -
可以看到
script setup标签会编译成一个_defineComponent
所有的函数和变量都会从setup函数中return出来。
const _sfc_main = /* @__PURE__ */ _defineComponent({
__name: "RefTemplate",
setup(__props, { expose: __expose }) {
__expose();
const hello = ref("Hello World!");
const msg = ref(1);
const __returned__ = { hello, msg };
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
return __returned__;
}
});
template编译成render函数
{{msg}}转成toDisplayString($setup.msg)。
:title="hello"属性转化成 { title: $setup.hello }
render函数传入$setup参数,创建虚拟DOM时直接通过变量名$setup.msg、$setup.hello使用ref的值,不需要.value。
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (
_openBlock(),
_createElementBlock(
'h1',
{ title: $setup.hello },
'Hello ' + _toDisplayString($setup.msg) + '!',
9,
_hoisted_1
)
);
}
4.调试代码,搞清执行流程
-
在
toDisplayString打点,刷新一下浏览器,然后右击菜单,Run to Line执行到此行。 -
可以从调试栏的
CALL STACK回调栈中看到执行过程中的所有函数。
- 然后可以点击每个函数,分别打点一步步执行
mountComponent函数中,createComponentInstance创建初始化组件实例,执行setupComponent,给instance实例对象挂载对应Component组件的变量或函数与render函数
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, namespace, optimized) => {
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
));
//...
setupComponent(instance, false, optimized);
//...
setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, namespace, optimized );
//...
}
- 1) 在
setupStatefulComponent函数中执行Component组件的setup函数return的变量和函数成setupResult - 2)在
handleSetupResult函数中给instance.setupState赋予添加响应式的setupResult
function setupComponent(instance, isSSR = false, optimized = false) {
//...
const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : void 0;
//...
}
function setupStatefulComponent(instance, isSSR) {
//...
const { setup } = Component;
if (setup) {
//...
const setupResult = callWithErrorHandling( setup, instance, 0, [ !!(process.env.NODE_ENV !== "production") ? shallowReadonly(instance.props) : instance.props, setupContext ]
);
//...
handleSetupResult(instance, setupResult, isSSR);
//...
}
function handleSetupResult(instance, setupResult, isSSR) {
//...
instance.setupState = proxyRefs(setupResult);
//...
finishComponentSetup(instance, isSSR);
}
其中proxyRefs函数,将setupResult转化成shallowUnwrapHandlers代理操作的Proxy响应式变量
而shallowUnwrapHandlers拦截器getter拦截器利用unref函数可以让ref变量的value值可以直接通过ref变量名访问,并且setter拦截器中对应调整为ref变量的value=XXX赋值方式。
function isRef2(r) {
return r ? r["__v_isRef"] === true : false;
}
function unref(ref2) {
return isRef2(ref2) ? ref2.value : ref2;
}
var shallowUnwrapHandlers = {
get: (target, key, receiver) => key === "__v_raw" ? target : unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key];
if (isRef2(oldValue) && !isRef2(value)) {
oldValue.value = value;
return true;
} else {
return Reflect.set(target, key, value, receiver);
}
}
};
function proxyRefs(objectWithRefs) {
return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
- 3) 在
finishComponentSetup函数中给instance.render赋予Component组件的render函数
function finishComponentSetup(instance, isSSR, skipOptions) {
//...
instance.render = Component.render || NOOP;
//...
}
mountComponent函数中,执行setupRenderEffect渲染副作用。
- 1)
setupRenderEffect函数中,给instance组件实例添加副作用函数并执行,用于监测响应式变量改变时渲染。
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, namespace, optimized) => {
const componentUpdateFn = () => {
//...
const subTree = (instance.subTree = renderComponentRoot(instance));
//...
patch(null, subTree, container, anchor, instance, parentSuspense, namespace);
//...
};
//...
const effect = instance.effect = new ReactiveEffect(componentUpdateFn);
//...
const update = instance.update = effect.run.bind(effect);
//...
update();
}
- 2)在
componentUpdateFn副作用函数中,利用renderComponentRoot渲染虚拟DOM,并通过patch挂载到页面成真实DOM。 - 3)在
renderComponentRoot函数中,将instance.setupState传入instance.render组件模板渲染函数中执行。
function renderComponentRoot(instance) {
const {
type: Component,
//...
render,
//...
setupState
//...
} = instance;
//...
result = normalizeVNode(
render.call( thisProxy, proxyToUse, renderCache, !!(process.env.NODE_ENV !== 'production') ? shallowReadonly(props) : props,
setupState, data, ctx )
);
//...
}
- 根据上面的
RefTemplate.vue编译后代码,render函数中,使用$setup.msg直接访问ref变量名获取instance.setupState的ref的value值,创建VNode,返回虚拟DOM。
简要执行流程:
- 创建
Component组件实例instance,将Component setup函数执行返回的所有变量或函数setupState、和render模板渲染函数挂载在instance组件实例对象上。其中setupState是shallowUnwrapHandlers代理操作的Proxy响应式变量,可以直接通过变量名访问和修改ref的value值。 - 给
instance组件实例对象添加渲染副作用,监听响应式变量的改变,执行渲染更新。 - 将
instance组件实例对象中setupState传入render模板渲染函数执行。 - 将
render模板渲染函数中访问setupState的ref变量名来获取value值,返回的虚拟DOM,patch到页面成真实DOM。
5.总结
vue3中ref为什么script中要用.value,而template模板中不需?
现在可以回答了!
回答
- 模板转成
render函数式会将msg转成$setup.msg - 创建
Component组件实例instance时,setup函数执行返回的所有变量和函数setupResult,并将setupResult转化成shallowUnwrapHandlers代理操作的Proxy响应式变量,可以直接通过变量名访问和修改ref的value值,并挂载在instance.setupState上 - 将
instance.setupState作为$setup参数都传入instance.render函数中,执行返回虚拟DOM。