vue3中ref为什么script中要用.value,而template模板中不需?

1,398 阅读3分钟

手把手教你调试,弄懂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浏览器窗口用于调试

image.png

  • 先不打点,直接加载界面,可以在调试栏的LOAD SCRIPTS中找到编译后文件和源文件

image.png

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
    )
  );
}

image.png

image.png

4.调试代码,搞清执行流程

  • toDisplayString打点,刷新一下浏览器,然后右击菜单,Run to Line执行到此行。 image.png

  • 可以从调试栏的CALL STACK回调栈中看到执行过程中的所有函数。

image.png

  • 然后可以点击每个函数,分别打点一步步执行
  1. 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;
 //...
}
  1. 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 )
  );
  //...
}
  1. 根据上面的RefTemplate.vue编译后代码,render函数中,使用$setup.msg直接访问ref变量名获取instance.setupStaterefvalue值,创建VNode,返回虚拟DOM。

简要执行流程:

  1. 创建Component组件实例instance,将Component setup函数执行返回的所有变量或函数setupState、和render模板渲染函数挂载在instance组件实例对象上。其中setupStateshallowUnwrapHandlers代理操作的Proxy响应式变量,可以直接通过变量名访问和修改refvalue值。
  2. instance组件实例对象添加渲染副作用,监听响应式变量的改变,执行渲染更新。
  3. instance组件实例对象中setupState传入render模板渲染函数执行。
  4. render模板渲染函数中访问setupStateref变量名来获取value值,返回的虚拟DOM,patch到页面成真实DOM。

5.总结

vue3中ref为什么script中要用.value,而template模板中不需?

现在可以回答了!

回答

  1. 模板转成render函数式会将msg转成$setup.msg
  2. 创建Component组件实例instance时,setup函数执行返回的所有变量和函数setupResult,并将setupResult转化成shallowUnwrapHandlers代理操作的Proxy响应式变量,可以直接通过变量名访问和修改refvalue值,并挂载在instance.setupState
  3. instance.setupState作为$setup参数都传入instance.render函数中,执行返回虚拟DOM。