手把手教你调试,弄懂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。