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