手把手教你调试,弄懂vue3中ref响应式变量为什么script中要用.value,而template模板中不需?
1.弄个简单的vue项目
RefTemplate.vue文件
html
<template>
<h1>Hello {{ msg }}!</h1>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const msg = ref<string>('world');
console.log(msg.value);
</script>
main.ts文件
ts
import RefTemplate from './RefTemplate.vue';
import { createApp } from 'vue';
createApp(RefTemplate).mount('#app');
2.配置vscode调试
- 在
.vscode文件夹下添加launch.json,配置调试环境和端口
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
js
const _sfc_main = /* @__PURE__ */ _defineComponent({
__name: "RefTemplate",
setup(__props, { expose: __expose }) {
__expose();
const msg = ref("world");
console.log(msg.value);
const __returned__ = { msg };
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
return __returned__;
}
});
所有的函数和响应式变量都会从setup函数中return出来。
template编译成render函数
js
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(
"h1",
null,
"Hello " + _toDisplayString($setup.msg) + "!",
1
/* TEXT */
);
}
render函数传入$setup参数,创建虚拟DOM的显示文本运用到响应式变量时直接通过$setup.msg使用,不需要.value。
- 查看
vue.js中toDisplayString函数,
js
var isRef = (val) => {
return !!(val && val['__v_isRef'] === true);
};
var toDisplayString = (val) => {
return isString(val)
? val
: val == null
? ''
: isArray(val) ||
(isObject(val) && (val.toString === objectToString || !isFunction(val.toString)))
? isRef(val)
? toDisplayString(val.value)
: JSON.stringify(val, replacer, 2)
: String(val);
};
通过toDisplayString读取响应式变量的值,利用isRef判断是否是ref,如果是ref的变量,则会读取其.value的值,转化成字符串文本。
4.调试代码,搞清执行流程
-
在
toDisplayString打点,刷新一下浏览器,然后右击菜单,Run to Line执行到此行。 -
可以从调试栏的
CALL STACK回调栈中看到执行过程中的所有函数。
- 然后可以点击每个函数,分别打点一步步执行
mountComponent函数中,createComponentInstance创建初始化组件实例,执行setupComponent,给instance实例对象挂载对应Component组件的响应式变量或函数与render函数
js
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,在handleSetupResult函数中给instance.setupState赋予添加响应式的setupResult
js
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);
}
- 2) 在
finishComponentSetup函数中给instance.render赋予Component组件的render函数
js
function finishComponentSetup(instance, isSSR, skipOptions) {
//...
instance.render = Component.render || NOOP;
//...
}
mountComponent函数中,执行setupRenderEffect渲染副作用。
- 1)
setupRenderEffect函数中,给instance组件实例添加副作用函数并执行,用于监测响应式变量改变时渲染。
js
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组件模板渲染函数中执行。
js
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函数中,利用toDisplayString读取instance.setupState的响应式变量的值,并转化成字符串,创建VNode。
5.总结
vue3中ref响应式变量为什么script中要用.value,而template模板中不需?
现在可以回答了!
- 创建
Component组件实例instance,将Component setup函数返回的setupState响应式变量或函数挂和render模板渲染函数载在instance组件实例对象上。 - 给
instance组件实例对象添加渲染副作用,监听响应式变量的改变,执行渲染更新。 - 将
instance组件实例对象中setupState响应式变量或函数传入render模板渲染函数执行,使用toDisplayString函数读取ref响应式变量.value的值,转化成文本。 - 将
render模板渲染函数返回的虚拟DOM,patch到页面成真实DOM。