Skip to content

分析脚手架文件与差异(Lesson23 2/2) ​

本页对应 Lesson23,共 7 段视频,约 1 小时 31 分。

学习目标 ​

  1. 能使用 Proxy 创建代理对象,并通过 get、set 拦截器实现对象属性的读取与修改,说明 target、key、value、receiver 等参数的作用。
  2. 能实现基本类型数据的响应式:通过 ref 的简化原理,利用 Object.defineProperty 或访问器属性拦截 value 的读写,并在数据变化后更新视图。
  3. 能实现引用类型数据的响应式:通过 reactive 的简化原理,使用 Proxy 代理对象,并在 get、set 中完成属性读取、赋值和视图更新。
  4. 能使用组合式 API reactive 声明对象或数组类型的响应式数据,并在模板中直接使用、在事件处理函数中修改且触发视图更新。
  5. 能从数据定义、响应式原理、使用方式三个角度区分 ref 与 reactive,并正确选择 ref 处理基本类型、reactive 处理引用类型。
  6. 能使用 <script setup> 语法糖直接定义数据与函数并在模板中使用,能说明其与普通 setup 函数“先定义再返回”的写法差异,以及直接定义的数据需借助 ref/reactive 才具备响应式。

知识点 ​

先看脚手架里的响应式入口 ​

为什么需要:项目里点击按钮能改数据、模板能跟着变,背后不是普通变量在起作用,而是响应式系统在拦截读写。转写稿先从 App 中的 ref 实例看起,展开后能看到 value、get、set,再往下就是 Proxy。

怎么想:先把 Proxy 当成一个内置构造函数。它接收两个参数:第一个是目标对象,第二个是描述对象,里面可写 get、set 等拦截逻辑。读取代理对象的属性时走 get,修改代理对象的属性时走 set。

怎么做:转写稿中的核心演示可整理为:

js
const obj = { username: '张三' }
const p = new Proxy(obj, {
  get(target, key, receiver) {
    console.log('get', key)
    return target[key]
  },
  set(target, key, newValue, receiver) {
    console.log('set', key, newValue)
    target[key] = newValue
    return true
  }
})

调试时输出 Proxy 会看到 native code,说明它是内置函数;传 new Proxy(1, {}) 会报错,因为第一个参数必须是对象。读 p.username 才走 get,读 obj.username 不走代理;写 p.username = '李四' 才走 set。易错点:不要以为操作原对象也会被拦截;第三个参数 receiver 转写稿说“几乎没人用”,先知道有即可。

ref:基本类型的响应式 ​

为什么需要:直接写 let count = 1,点击后 count++,数据变了但视图不变,所以需要 ref 把基本类型包成可追踪的实例。

怎么想:ref 是一个函数,返回 RefImpl 实例;实例中有 value。模板中可直接使用,JS 中要写 .value。转写稿的演示用 Proxy 拦截实例的 value 读写,方便观察流程;总结原理时说基本类型通常由 defineProperty 实现响应式。

怎么做:按转写稿整理伪实现:

js
function ref(value) {
  return new RefImpl(value)
}
class RefImpl {
  constructor(value) {
    this._value = value
    return new Proxy(this, {
      get(target, key) {
        if (key === 'value') return target._value
        return target[key]
      },
      set(target, key, newValue) {
        if (key === 'value') {
          target._value = newValue
          h3.innerHTML = newValue
        }
        return true
      }
    })
  }
}

易错点:模板里写 count,JS 里写 count.value;直接定义的普通变量不支持响应式;模块化后变量不会自动挂到全局,调试时不能随手在控制台取。

reactive:引用类型的响应式 ​

为什么需要:对象、数组这类引用类型需要整体代理,reactive 接收对象并返回 Proxy 实例。

怎么想:reactive 也是函数,必须接收对象,传 1 会报错;返回的是 Proxy,不是原对象。get 返回 target[key],set 修改 target[key] 并更新视图。

怎么做:

js
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      return target[key]
    },
    set(target, key, newValue) {
      target[key] = newValue
      h3.innerHTML = target.a
      return true
    }
  })
}

调试时输出 reactive({ a: 100 }),看到的是 Proxy,展开有目标对象;点击按钮让 obj.a++,会进入 set,再更新视图。易错点:reactive 不能接收基本类型;数组也可用;模板中可直接使用,不需要 .value。

setup 语法糖 ​

为什么需要:旧写法要在 setup 中定义数据、函数,再 return 出去,代码啰嗦。<script setup> 可以顶层直接定义,模板直接使用。

怎么做:

vue
<script setup>
import { ref, reactive } from 'vue'
const count = ref(1)
const obj = reactive({ username: '张三' })
const add = () => count.value++
</script>

易错点:直接写 let count = 1 不支持响应式,要用 ref;JS 中 ref 仍要 .value;不要和旧 setup 写法随意混用,转写稿调试时也出现覆盖现象,项目里应统一一种写法。

ref 与 reactive 的区别 ​

相同点:都来自 Vue,都是函数,都能实现响应式。不同点:数据定义上,ref 常用于基本类型,reactive 常用于对象/数组;ref 也能接引用类型,但其本质会交给 reactive。原理上,ref 基本类型常由 defineProperty 实现,引用类型通过 reactive;reactive 通过 Proxy 实现。使用上,模板中 ref 不写 .value,JS 中要写;reactive 不用。易错点:面试或项目中不要只说“都能响应式”,要按数据定义、原理、使用三个角度说清楚。

vue.config.js:配置文件为什么看起来没变 ​

为什么需要:Vue CLI 项目启动、端口、host、自动打开浏览器、保存时是否做语法检查,都集中在 vue.config.js。Vue3 项目仍然可以沿用这套配置,所以本节先强调“与原来相同”,避免误以为升级 Vue3 必须重写工程配置。

怎么想:先区分“框架版本差异”和“工程配置差异”。入口、根组件写法变了,但开发服务器配置不一定变。转写稿里也是先把项目跑起来,再看控制台和页面表现,说明配置层先保证项目能启动,再谈代码差异。

怎么做:保留原配置即可:

js
const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({
	transpileDependencies: true,
	lintOnSave:false,// 关闭语法检查
	devServer:{
		open:true,// 自动打开浏览器
		port:80,// 指定端口号
		host:"zhangpeiyue.com",// 指定host
	}
})

易错点:改 port、host、open 后要重启开发服务器;lintOnSave:false 只是关闭保存时检查,不等于代码没有语法错误;不要因为入口改成 createApp 就认为 vue.config.js 必须改。

入口文件 main.ts:从 new Vue 到 createApp ​

为什么需要:Vue3 不再通过 new Vue 实例化根实例,而是用 createApp 创建单页面应用对象,再挂载到容器。入口文件是理解 Vue3 与 Vue2 差异的第一站。

怎么想:转写稿和笔记里的调试意图很明确:先 console.log(createApp),确认它是一个函数;再 const app = createApp(App),看到返回的是 app 对象;最后 app.mount("#app") 指定容器。也可以把创建和挂载合成链式写法。

怎么做:

ts
// 在Vue3中不需要实例化Vue
// 可以通过createApp创建单页面应用的一个对象,类型是一个函数。
import {createApp} from "vue";
// console.log(createApp);// 是一个函数
// 引入根组件App,根组件不允许省略扩展名.vue(main.ts中不支持省略.vue)
import App from "@/App.vue";
// 运行该函数必须指定一个根组件。
// 该函数返回的是一个对象,称该对象为app对象。

// 方式一
// const app = createApp(App);
// console.log(app);// 输出的app对象只是一个普通对象
// 指定挂载的容器。(将id为app的标签作为应用的容器)
// app.mount("#app");

// 方式二
createApp(App).mount("#app");

易错点:main.ts 中导入 App.vue 不能省略 .vue;mount("#app") 的容器必须存在;createApp 返回的是 app 对象,不是 Vue2 的根实例。转写稿里还观察到 RefImpl 实例的 value 是 Proxy,说明 Vue3 引用类型响应式底层走 Proxy,而不是 Vue2 的 defineProperty;注意“procy”应写作 Proxy,“REFIMBR/REFIMPL”应写作 RefImpl。这属于后续组合式 API 的延伸,本节先建立入口差异。

根组件 App.vue:多根元素与 defineComponent ​

为什么需要:Vue3 的模板允许一个组件拥有多个根元素,根组件不再被强制包在一个唯一父元素里。defineComponent 主要用于 TypeScript 类型推断辅助。

怎么想:先看模板能否并列多个 h3;再看脚本是否必须用 defineComponent。转写稿和笔记都说明:由于 TS 有推断机制,defineComponent 可以省略,但写上更利于类型提示。

怎么做:

vue
<template>
    <!-- Vue3中可以拥有多个根元素   -->
    <h3>App组件1</h3>
    <h3>App组件2</h3>
    <h3>App组件3</h3>
</template>

<script>
import {defineComponent} from "vue";
// defineComponent作用对类型的推断辅助。由于ts拥有推断机制,所以也可以省略。
export default defineComponent({
    name:"App"
})
</script>

<style scoped>

</style>

易错点:多根元素是模板层能力,不等于多根组件;scoped 样式仍只作用当前组件;省略 defineComponent 时要注意 TS 推断是否满足需求。转写稿里反复调试“控制台输出、页面是否更新”,提醒我们:先确认入口挂载成功,再检查根组件模板和脚本,排错顺序不要乱。

先看本节目标:在 Vue3 项目里直接写 Vue2 选项式语法 ​

为什么需要:很多旧项目、旧组件都是 data(){}、computed、methods、beforeCreate 这类 Vue2 写法。迁移时不可能一次全改成组合式 API,所以要先确认 Vue3 对 Vue2 基本语法的兼容边界。

怎么想:把 App.vue 当成“选项式语法试验台”,同时放基本类型 num、引用类型 arr、计算属性 sum、方法、生命周期打印和子组件销毁观察。先能跑,再通过点击和 console 看响应式是否正常。

怎么做:在 src/App.vue 中保留 export default { name, components, data, computed, methods, 生命周期 },模板里用 {{ num }}、{{ arr }}、@click、v-if、v-for。子组件 Child.vue 用 v-if="isShow" 控制显示隐藏。

易错点:本节笔记明确写了“支持 Vue2 的基本语法,不支持销毁生命周期钩子函数”。所以 Child.vue 里的 beforeDestroy/destroyed 只作为对照观察,不要把它当成 Vue3 推荐写法;另外 const arr = [1,2,3,4,5] 写在模块作用域,data 里直接返回 arr,多个实例会共享这个数组,调试时可用 this.arr === arr 验证。

App.vue:data、computed、methods 与模板绑定 ​

为什么需要:num 和 arr 分别代表基本类型和引用类型,必须验证 Vue3 下选项式写法是否还能触发视图更新。

怎么想:num++ 直接改基本类型;changeNum 同时改 num 并 arr.push(...);changeNum2(100) 验证带参方法;sum 用 reduce 求数组和,验证 computed 依赖 arr。

怎么做:模板中放三个按钮,分别测试 @click="num++"、@click="changeNum"、@click="changeNum2(100)";下面用 {{sum}} 显示计算属性;再用 v-for="item in arr" :key="item" 渲染数组。

易错点:reduce 回调必须 return sum,否则累加结果会丢;v-for 的 :key 不要用可能重复的值;changeNum 里如果只 push 不重新赋值,Vue2 里可能要考虑数组方法,但 Vue3 的响应式代理能拦截 push,本节可直接观察。

调试:点击按钮看 num、arr 与 Proxy ​

为什么需要:只看代码容易把“数据变了”和“视图变了”混在一起,必须通过点击和 console 验证。

怎么想:按转写稿的调试思路,先运行项目,点击按钮,在控制台展开数据对象;Vue3 的响应式底层用 Proxy,new Proxy(目标对象, 描述对象) 的第一个参数是目标对象,第二个参数可写 get/set。读取代理属性走 get,修改代理属性走 set;直接操作原对象不会经过代理。

怎么做:在 changeNum 中可临时打开 console.log(this.num, this.arr === arr),点击后看 num 是否递增、arr 是否追加;再在控制台展开响应式数据,观察其 value 或代理结构。对应本节,this.num++ 和 this.arr.push(...) 能驱动模板,是因为 Vue3 在底层拦截了读写。

易错点:不要直接改原对象后怪视图不更新;调试 Proxy 时要操作代理对象;console.log 打印对象展开时机可能受后续修改影响,必要时打印具体值。

Child.vue 与销毁钩子:只做对照观察 ​

为什么需要:用 v-if 切换子组件,可以观察组件创建、挂载、销毁相关钩子的执行时机。

怎么想:App.vue 里 isShow 控制 Child,点击按钮切换 isShow;Child.vue 中写 beforeDestroy/destroyed 打印日志。按本节笔记结论,Vue3 支持 Vue2 基本语法,但不支持销毁生命周期钩子函数,所以这里不要据此认为销毁钩子会按 Vue2 方式生效。

怎么做:保留 Child.vue 代码,点击 isShow 按钮,观察控制台;同时观察 App.vue 的 beforeCreate、created、beforeMount、mounted、beforeUpdate、updated 打印顺序。

易错点:生命周期打印顺序不要靠猜,以实际运行结果为准;不要把 beforeDestroy/destroyed 当作本节重点结论;v-if 为 false 时子组件才进入销毁流程。

完整示例代码(保留本节笔记代码) ​

vue
<template>
    <h3>App组件</h3>
    <p>{{ num }}</p>
    <p>{{ arr }}</p>
    <button @click="num++">{{ num }}</button>
    <button @click="changeNum">{{ num }}</button>
    <button @click="changeNum2(100)">{{ num }}</button>
    <p>{{sum}}</p>
    <hr/>
    <button @click="isShow=!isShow">{{isShow}}</button>
    <Child v-if="isShow"></Child>
    <p v-for="item in arr" :key="item">{{item}}</p>
</template>

<script>
// 支持Vue2的基本语法,不支持销毁生命周期钩子函数
import Child from "@/components/Child";
const arr = [1, 2, 3, 4, 5];
export default {
    name: "App",
    components: {Child},
    data() {
        return {
            num: 1,
            arr,
            isShow:true
        }
    },
    computed:{
        sum(){
            // return this.arr.length;
            return this.arr.reduce((sum,item)=>{
                sum+=item;
                return sum;
            },0)
        }
    },
    methods: {
        changeNum() {
            // 1  false
            // console.log(this.num,this.arr===arr);
            this.num++;
            this.arr.push(this.arr.length + 1);
        },
        changeNum2(n) {
            this.num += n;
        }
    },
    // 挂载阶段
    beforeCreate() {
        console.log("1-beforeCreate");
    },
    created(){
        console.log("2-created");
    },
    beforeMount() {
        console.log("3-beforeMount");
    },
    mounted(){
        console.log("4-mounted");
    },
    // 更新
    beforeUpdate() {
        console.log("1-beforeUpdate");
    },
    updated() {
        console.log("2-updated");
    }
}
</script>

<style scoped>

</style>
vue
<template>
    <h3>Child</h3>
</template>

<script>
export default {
    name: "Child",
    beforeDestroy() {
        console.log("1-beforeDestroy");
    },
    destroyed() {
        console.log("2-destroyed");
    }
}
</script>

<style scoped>

</style>

从 Proxy 理解响应式拦截 ​

为什么需要:Vue3 组合式 API 的响应式底层离不开 Proxy。转写稿先跑项目观察 ref 实例:count.value 是 100,而对象类型的 value 展开后是一个 Proxy,说明引用类型的响应式靠代理完成。

怎么想:Proxy 是内置构造函数,必须 new。它接收两个参数:第一个是目标对象,第二个是描述对象(拦截规则)。get 在读取代理属性时执行,set 在修改代理属性时执行。注意:要操作代理对象 p,不是原对象 obj;直接改 obj 不触发拦截。

怎么做(按转写稿整理的核心示意):

js
const obj = { userName: '张三' }
const p = new Proxy(obj, {
  get(target, key, receiver) {
    console.log('get', key)
    return target[key]
  },
  set(target, key, newValue, receiver) {
    console.log('set', key, newValue)
    target[key] = newValue
    return true
  }
})
p.userName = '李四' // 触发 set
console.log(p.userName) // 触发 get

易错点:第一个参数必须是对象,传 1 会报错;第二个参数也必须是对象;get 的参数是 target、key、receiver,set 是 target、key、newValue、receiver;receiver 一般就是代理实例;set 里要写 return true。转写稿还强调:控制台里看到中括号属性,是在提示不要直接绕过代理操作原对象。

ref:让基本类型可响应 ​

为什么需要:组合式 API 里基本类型也要能响应。ref 是函数,接收任意类型数据,返回 RefImpl 实例,实例上有 value 属性。模板中可直接用,JS 中必须 .value。

怎么想:转写稿用简化实现说明:ref(10) 返回 new RefImpl(10),实例里保存值;读取 count.value 走 get,修改 count.value 走 set,在 set 里更新数据并触发视图更新。转写稿用修改 H3.innerHTML 模拟视图更新,属于教学伪代码。

怎么做(示意):

js
function ref(value) {
  return new RefImpl(value)
}
class RefImpl {
  constructor(value) {
    this._value = value
  }
  get value() {
    return this._value
  }
  set value(newValue) {
    this._value = newValue
    updateView() // 转写稿中用更新 H3 内容模拟
  }
}

易错点:JS 里写 count++ 不行,要写 count.value++;普通 let count = 1 不会响应;模板中不需要写 .value;ref 也能接收对象,但对象本质会走 reactive,所以引用类型更推荐直接用 reactive。

reactive:引用类型的响应式 ​

为什么需要:对象、数组这类引用类型需要整体代理。reactive 也来自 Vue,返回 Proxy 实例。转写稿验证:reactive(1) 不行,必须传对象;传对象后输出得到 Proxy。

怎么想:reactive 内部就是 new Proxy(target, handler),get 读目标对象属性,set 改目标对象属性并更新视图。数组也可以,转写稿用 arr.push / arr.length 演示。

怎么做(示意):

js
function reactive(target) {
  return new Proxy(target, {
    get(t, k) {
      return t[k]
    },
    set(t, k, newValue) {
      t[k] = newValue
      updateView()
      return true
    }
  })
}
const obj = reactive({ userName: '张三', age: 100 })
obj.userName = '李四' // 触发 set

易错点:reactive 只能接收对象(含数组),不能传基本类型;模板和 JS 中都不需要 .value;直接改原对象不会触发代理;set 要 return true。

ref 与 reactive 的区别 ​

相同点:都是函数,都来自 Vue,都能实现响应式。

不同点:数据定义上,ref 常用于基本类型,也可用于引用类型;reactive 用于对象、数组等引用类型。原理上,ref 的基本类型通过 Object.defineProperty 实现,引用类型本质通过 reactive(即 Proxy)创建;reactive 通过 Proxy 内置构造函数实现。使用上,ref 在模板中不需要 .value,在 JS 中需要 .value;reactive 在模板和 JS 中都不需要 .value。

易错点:不要用 reactive 声明基本类型;ref 声明对象时,value 是 Proxy;面试表达可按“数据定义、原理、使用”三点说,不要背得太官方。

setup 语法糖 ​

为什么需要:原来要在 setup 函数里定义数据再 return,写起来繁琐。<script setup> 可以省掉 setup 函数和返回对象。

怎么想:在 script 标签上加 setup,顶层声明的数据、函数可直接在模板使用;但普通变量不响应,必须用 ref 或 reactive。转写稿演示:直接 const name = 100 后修改不响应;改成 ref 后,JS 中修改要写 .value。

怎么做:

vue
<script setup>
import { ref, reactive } from 'vue'
const count = ref(1)
const obj = reactive({ userName: '张三' })
const add = () => {
  count.value++
  obj.userName += '!'
}
</script>

<template>
  <h3>{{ count }}</h3>
  <p>{{ obj.userName }}</p>
  <button @click="add">修改</button>
</template>

易错点:语法糖里直接定义的普通数据不支持响应式;ref 在 JS 中必须 .value;reactive 不要传基本类型;旧写法和语法糖不要随意混用,避免自己绕晕。

setup 要解决什么问题 ​

Options API 把数据、方法、生命周期分散在 data、methods、beforeCreate 等选项里,逻辑一多就不容易按功能聚合。组合 API 需要一个统一入口,这个入口就是 setup。转写稿里反复强调“setup 是所有组合 API 的一个入口”,所以先别急着记八条结论,先把它理解成:组件一创建,先进入 setup,在这里组织数据、函数,再交给模板使用。

怎么想:setup 不是普通业务方法,而是组合 API 的启动位置。它要在 beforeCreate 之前执行,因此不能依赖 this 去拿 data 或 methods。
怎么做:在 defineComponent 中写 setup(),把模板需要用的东西通过对象返回。
易错点:不要把 setup 当成 methods 里的函数用,也不要在里面写 this.age。

基本使用:先让模板拿到 setup 返回的数据 ​

笔记里的完整示例如下,代码保留原样:

vue
<template>
    <h3>App</h3>
    <p>userName:{{userName}}</p>
    <p>age:{{age}}</p>
    <button @click="age++">{{age}}</button>
    <button @click="changeAge">changeAge{{age}}</button>
</template>
<script>
    import {defineComponent} from "vue";

    export default defineComponent({
        beforeCreate() {
            console.log("beforeCreate");
        },
        // 1- setup是一个钩子函数。
        // 2- 在beforeCreate之前执行。
        // 3- this指向的undefined.
        // 4- setup如果要使用return进行返回,要求必须是一个对象
        // 5- setup不允许设置为async函数。
        // 6- 返回对象的属性可以直接被模板调用
        // 7- 如果data与setup拥有相同名的数据,那么以setup为准
        // 8- data中的数据是响应式的,而setup中的数据不是响应式。
        data(){
            return {
                age:100,
                userName:"lisi->data"
            }
        },
        setup(){
            return {
                age:10,
                userName:"zhangsan->setup",
                changeAge(){
                    this.age+=2;
                    console.log("changeAge",this.age)
                }
            }
        }
    })
</script>
<style>

</style>

为什么需要 return 对象:模板只能读取组件上下文里暴露出来的东西。setup 返回对象后,userName、age、changeAge 才能被模板直接调用。
怎么想:先返回普通对象,观察模板是否渲染;再观察点击后的变化。
怎么做:运行后页面显示 userName:zhangsan->setup、age:10。这说明 data 里的 userName:"lisi->data"、age:100 被 setup 同名覆盖了,验证了第 7 条。
易错点:不要以为模板里能显示就代表响应式。点击第一个按钮 age++,页面不会更新,因为 setup 返回的 age 只是普通数据,不是响应式数据;data 中的数据才是响应式的。

执行时机与 this:beforeCreate 之前、this 为 undefined ​

笔记第 2、3 条很关键。可以在 beforeCreate 和 setup 里分别打印:

js
beforeCreate() {
    console.log("beforeCreate");
},
setup() {
    console.log("setup", this);
    return {
        age:10,
        userName:"zhangsan->setup"
    }
}

为什么需要:如果 setup 在 beforeCreate 之前执行,就说明它拿不到组件实例上的 data、methods。
怎么想:看控制台顺序,先出现 setup,再出现 beforeCreate;this 输出 undefined。
怎么做:需要的数据在 setup 内部声明,通过 return 暴露,不要通过 this 访问。
易错点:笔记代码里的 changeAge 使用了 this.age+=2。由于 setup 中 this 是 undefined,点击第二个按钮会报错。这里不是让你照抄 this,而是用反例说明:setup 内不要依赖 this。后续要用 ref、reactive 或闭包变量来改数据。

返回值规则:必须是对象,不能 async ​

第 4、5 条是硬规则。setup 的返回值必须是对象,模板才能按属性名读取;如果写成 async setup(),返回值会变成 Promise,模板不能像普通对象一样直接取 userName、age。
为什么需要:Vue 需要同步拿到“暴露给模板的对象”。
怎么想:先保证 return { ... } 是普通对象;不要在 setup 前加 async。
怎么做:把异步逻辑放到单独函数里,setup 本身保持同步返回对象。
易错点:返回数组、返回基本类型、返回 Promise 都不符合这里的用法;模板会取不到预期属性。

同名覆盖与响应式差异 ​

第 7、8 条要一起看。data 与 setup 同名时,以 setup 为准,所以页面显示 zhangsan->setup 和 10。但 data 中的数据是响应式的,setup 里直接返回的普通数据不是响应式。
为什么需要:理解“能显示”和“能更新”是两件事。
怎么想:点击 age++ 后,控制台里变量可能变了,但视图不变;这说明普通对象没有触发视图更新。
怎么做:本节先接受这个现象;后续用 ref、reactive 让 setup 中的数据具备响应式。转写稿也提到,直接定义的数据不支持响应式,需要 ref/reactive。
易错点:不要因为模板第一次渲染成功,就认为 setup 返回的数据自动响应式;也不要在 setup 里用 this 改 data。

易错点清单 ​

  1. setup 是组合 API 入口,在 beforeCreate 之前执行。
  2. setup 中 this 是 undefined,不要写 this.age。
  3. return 必须是对象,返回属性才能被模板直接调用。
  4. 不要写成 async setup()。
  5. data 与 setup 同名时,以 setup 为准。
  6. setup 返回的普通数据不是响应式;点击 age++ 视图不更新是正常现象。
  7. 本节先掌握“返回对象”的写法;后续语法糖会省略 return,但底层仍是把数据暴露给模板。

先理解 Proxy:ref 处理对象时的底层代理 ​

为什么需要:ref 接收基本类型时可以用 getter/setter 拦截,但接收对象时,对象内部属性很多,需要统一拦截读写,所以底层要用 Proxy。视频里先打印 ref 实例,看到 value 是一个 Proxy,它的目标对象里才有 username、age 等属性。

怎么想:Proxy 是内置构造函数,语法是 new Proxy(target, handler)。第一个参数必须是目标对象,第二个参数是描述对象,常用 get 和 set。访问代理属性走 get,修改代理属性走 set。

怎么做:

js
const target = { username: '张三' }
const p = new Proxy(target, {
  get(target, key) {
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    return true
  }
})

易错点:new Proxy(1, {}) 会报错,第一个参数必须是对象;直接改 target.username 不会走代理,必须操作 p.username 才触发 get/set。

ref 实现基本类型响应式 ​

为什么需要:count = 1 这样的普通变量被修改后,视图不会自动更新。视频里点击按钮让 count.value++,控制台看到数据变了,但 H3 内容没变,所以要把“改数据”和“更新视图”接起来。

怎么想:基本类型不能用 Proxy 代理,就用类 RefImpl 包一层:内部用 _value 存值,暴露 get value 和 set value。读取时返回 _value,修改时更新 _value 并同步 DOM。

怎么做:

js
function ref(value) {
  return new RefImpl(value)
}

class RefImpl {
  constructor(value) {
    this._value = value
  }
  get value() {
    return this._value
  }
  set value(newValue) {
    this._value = newValue
    h3.innerHTML = newValue
  }
}

调试时先发现 count.value 有变化但视图没变,于是在 set value 里补上 h3.innerHTML = newValue,点击按钮视图才跟着变。

易错点:JS 中必须写 count.value,不能写 count++;模板中 ref 会自动解包,通常不写 .value;不要直接改 _value,否则不触发 set。

ref 接收对象时:value 变成 Proxy ​

为什么需要:ref({ username: '张三' }) 如果只把对象塞进 _value,修改 obj.value.username 不会触发更新。视频里在 RefImpl 构造函数中判断:如果 value 是对象,就把 value 作为目标对象创建 Proxy,再赋给 this.value。

怎么想:ref 的 value 对外仍是一个“值”,但对象场景下这个值是代理。读 obj.value.username 走代理的 get,写 obj.value.username = '李四' 走代理的 set,在 set 里更新目标对象并刷新视图。

怎么做:

js
class RefImpl {
  constructor(value) {
    if (typeof value === 'object' && value !== null) {
      this.value = new Proxy(value, {
        get(target, key) {
          return target[key]
        },
        set(target, key, newValue) {
          target[key] = newValue
          p.innerHTML = obj.value.username
          return true
        }
      })
    } else {
      this._value = value
    }
  }
}

易错点:对象场景下 JS 里仍要写 obj.value.username;set 中刷新视图要用 obj.value.username,不要误写成原对象;Proxy 的 set 最好返回 true。

reactive 的用法与原理 ​

为什么需要:如果数据本来就是对象,用 ref 还要多一层 .value,不够直接。reactive 专门接收对象/数组,返回代理对象,模板中可直接 obj.username。

怎么想:reactive 是函数,接收对象,返回 new Proxy(target, handler)。get 返回 target[key],set 修改 target[key] 并更新视图。视频里 reactive(1) 报错,说明它只能接收对象。

怎么做:

js
function reactive(target) {
  return new Proxy(target, {
    get(target, key) {
      return target[key]
    },
    set(target, key, newValue) {
      target[key] = newValue
      h3.innerHTML = obj.age
      return true
    }
  })
}

const obj = reactive({ username: '张三', age: 12 })

模板中直接写 {{ obj.username }},按钮中写 obj.username += '!'、obj.age++ 即可。

易错点:reactive 不能接收基本类型;返回的是代理,不是原对象;模板和 JS 中都不需要 .value。

ref 与 reactive 的区别 ​

为什么需要:实际开发要选 API。视频从数据定义、原理、使用三个角度总结。

怎么想:ref 常用于基本类型,也可接收引用类型;reactive 常用于对象、数组。原理上,ref 基本类型通过 Object.defineProperty 的 getter/setter 实现,引用类型底层通过 reactive 创建 Proxy;reactive 通过 Proxy 内置构造函数实现。使用上,ref 在模板中自动解包不写 .value,JS 中要写 .value;reactive 不需要 .value。

怎么做:基本类型用 ref,对象/数组用 reactive;若用 ref 存对象,JS 中记得 .value。

易错点:不要以为 ref 只能基本类型;不要以为 reactive 能接数字;ref 在 JS 中漏写 .value 是高频错误。

setup 语法糖 ​

为什么需要:旧写法要在 setup 中定义数据、函数,最后 return 给模板,代码重复。<script setup> 可以直接定义,模板直接使用。

怎么想:在 <script setup> 中写 const count = ref(0)、const obj = reactive({ username: '张三' }),模板直接写 {{ count }}、{{ obj.username }}。但直接写 const name = 100 只是普通变量,不支持响应式。

怎么做:

vue
<script setup>
import { ref, reactive } from 'vue'
const count = ref(0)
const obj = reactive({ username: '张三' })
function add() {
  count.value++
  obj.username += '!'
}
</script>

<template>
  <p>{{ count }}</p>
  <p>{{ obj.username }}</p>
  <button @click="add">修改</button>
</template>

易错点:<script setup> 中 ref 在 JS 里仍要 .value;模板中不用 .value;直接定义的普通变量不会响应式,必须用 ref 或 reactive。

为什么需要 reactive:从 ref 的局限说起 ​

组合式 API 里,ref 既能包基本类型,也能包引用类型。但转写里反复验证了一点:当 ref 接收到对象时,RefImpl 实例的 value 并不是普通对象,而是一个 Proxy;也就是说,引用类型的响应式底层仍然要落到 reactive 上。
怎么想:如果数据本身就是一个对象或数组,用 reactive 更直接,模板里也不用写 .value。
怎么做:

ts
import { reactive } from 'vue'

const obj = reactive({
  username: '张三',
  age: 12
})

模板中可直接写 {{ obj.username }},按钮里写 obj.age++。转写中还用数组演示了 arr.push 后视图更新,说明数组也属于引用类型,可用 reactive。
易错点:reactive(1) 会报错,它只接收对象/数组;reactive 返回的是 Proxy 实例,不是原对象。

从 Proxy 推导 reactive 的原理 ​

为什么需要:要理解 reactive 为什么能拦截读写,就要先看 Proxy。转写中先运行项目,点击按钮后打印 count,看到 RefImpl,再展开看到 value 是 Proxy,Proxy 里有 target、handler、get、set。
怎么想:new Proxy(target, handler) 第一个参数是目标对象,第二个是描述对象;读取代理属性走 get,修改代理属性走 set。
怎么做:按转写中的伪代码思路,可写成:

ts
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      return target[key]
    },
    set(target, key, newValue, receiver) {
      target[key] = newValue
      // 转写中用伪代码演示更新视图
      return true
    }
  })
}

调试时注意:直接改原对象 obj.username = '李四' 不会走代理;改 p.username = '李四' 才会走 set。get 的参数是目标对象、属性名、代理实例;set 多一个 newValue。转写里说第三个 receiver 参数“几乎没人用”,先知道含义即可。
易错点:不要把“原对象”和“代理对象”混着操作;reactive 的原理核心是 Proxy 的 get/set 拦截。

reactive 与 ref 的区别 ​

为什么需要:两者都能实现响应式,但使用场景不同。
怎么想:从数据定义、原理、使用三个角度比较。
怎么做:

ts
import { ref, reactive } from 'vue'

const count = ref(1)          // 常用于基本类型
const obj = reactive({ name: '张三' }) // 常用于引用类型

相同点:都来自 vue,都是函数,都能实现响应式。不同点:ref 常用于基本类型,reactive 用于对象/数组;ref 也可包引用类型,但底层会通过 reactive 创建 Proxy,多一层 RefImpl;ref 基本类型原理常提 Object.defineProperty,reactive 原理是 Proxy。使用上,ref 在模板中不用 .value,在 JS 中要写 .value;reactive 不需要 .value。
易错点:reactive 不能传基本类型;JS 里改 ref 忘了 .value;数组可以用 reactive,转写中 arr.push 可触发更新。

setup 语法糖:少写 return ​

为什么需要:原写法要在 setup 中定义数据再 return,数据多了很繁琐。
怎么想:<script setup> 是语法糖,顶层声明的变量、函数可直接在模板使用。
怎么做:

vue
<script setup lang="ts">
import { ref, reactive } from 'vue'

const count = ref(1)
const obj = reactive({ username: '张三' })

function add() {
  count.value += 2
  obj.username += '!'
}
</script>

<template>
  <p>{{ count }}</p>
  <p>{{ obj.username }}</p>
  <button @click="add">修改</button>
</template>

易错点:语法糖里直接写 let name = 100 不会自动响应,必须用 ref 或 reactive;ref 在 JS 中仍要 .value;转写中演示了语法糖与旧 setup 写法混用会出现覆盖/冲突,实际项目不要混用。

调试与易错点汇总 ​

转写中的调试顺序很值得保留:先打印 RefImpl,再展开 value 看到 Proxy;再打印 Proxy 的 target 和 handler;最后分别点击按钮观察 get/set 是否执行。
易错点再强调:reactive 只收对象;操作代理才触发拦截;ref 模板自动解包、JS 手动 .value;setup 语法糖直接定义的数据不会自动响应,要配合 ref/reactive。

从“先定义再返回”到 <script setup> ​

为什么需要:过去写组合式 API,要在 setup() 里定义 num、count、obj,再逐个 return 给模板使用。数据一多,返回对象会变成重复样板。<script setup> 语法糖就是为减少这层样板。

怎么想:可以把它理解为“编译阶段帮你把顶层声明暴露给模板”。转写稿里说得很直白:之前要在 setup 函数中定义数据然后返回;语法糖中可以直接定义数据,直接定义的数据可以在模板中直接使用。注意它只支持 Vue3,不能像旧写法那样把 Vue2 选项式和 Vue3 组合式混在一起。

怎么做:在 script 标签上加 setup,然后直接声明变量、函数并导入 ref、reactive。本节完整代码如下:

vue
<template>
    <h3>setup语法糖</h3>
    <p>{{num}}</p>
    <button @click="changeNum">修改num,修改不了,因为直接定义的数据不支持响应式</button>
    <hr/>
    <p @click="count++">{{count}}</p>
    <button @click="setCount">修改count</button>
    <hr/>
    <p>{{obj.userName}}</p>
    <button @click="obj.userName+='!'">修改obj->userName</button>
</template>

<script setup lang="ts">
    // 之前的写法可以将Vue2语法与Vue3语法结合使用。如果使用语法糖只支持Vue3
    // 语法糖即是在script标签中增加setup
    // 之前要在setup函数中定义数据,然后返回。
    // 在语法糖中可以直接定义数据,直接定义的数据可以在模板中直接使用。
    // 注意:如果语法糖写法与过去的写法同时出现(出现两个script),语法糖写法优先
    import {reactive, ref} from "vue";

    let num = 100;
    let count = ref(9);
    let obj = reactive({
        userName:"zhangsan"
    })
    const changeNum = function(){
        num++;
    }
    const setCount = function(){
        count.value+=2;
    }
</script>
<script lang="ts">
export default {
    setup(){
        
        return {
            num:102
        }
    }
}
</script>

易错点:语法糖不是“所有变量自动响应式”。let num = 100 只是普通变量,模板能读到,但修改后视图不更新;需要响应式时仍要用 ref 或 reactive。

普通变量为什么“改不动” ​

为什么需要:理解这一点,才能知道什么时候必须用 ref/reactive。笔记里按钮文案直接写了“修改不了,因为直接定义的数据不支持响应式”。

怎么想:changeNum 里执行 num++,num 的 JS 值确实变了,但它没有收集依赖、触发更新的能力,所以模板不会重新渲染。转写稿调试时也是这个现象:点击修改 num,页面不变。

怎么做:如果只是展示静态初值,可以直接定义;如果要修改后驱动视图,就改成 ref。例如 count 用 ref(9),模板中写 {{count}},点击 count++ 能更新;在 JS 函数中要写 count.value += 2。

易错点:ref 在模板中会自动解包,所以模板写 count,不要写 count.value;但在 <script setup> 的 JS 逻辑里必须写 .value。这是转写稿反复强调的“JS 中需要写 value”。

reactive 对象在语法糖中的写法 ​

为什么需要:对象数据如果用普通 let obj = { userName: "zhangsan" },修改属性同样不会触发视图更新。reactive 用于对象/数组这类引用类型。

怎么想:reactive 接收一个对象,返回代理对象;模板中访问 obj.userName,修改 obj.userName += '!' 时能触发更新。它不需要像 ref 那样在 JS 里写 .value。

怎么做:导入 reactive,声明 let obj = reactive({ userName: "zhangsan" }),模板直接绑定 {{obj.userName}},按钮事件直接写 obj.userName+='!'。

易错点:reactive 接收的必须是一个对象;不要写成 reactive(1)。另外,ref 也能接收对象,但对象场景下 reactive 写法更直接;本节不展开底层 Proxy 原理,只记住使用边界。

两个 script 同时出现时谁优先 ​

为什么需要:实际迁移旧代码时,可能同时保留旧 setup() 写法和新语法糖。笔记专门提醒:两个 script 同时出现时,语法糖写法优先。

怎么想:下面旧 script 里 return { num: 102 },但模板里的 num 仍以 <script setup> 中的 num = 100 为主。转写稿也做了对照:把旧写法放上放下、删掉旧写法,观察页面显示,最后确认以语法糖为主。

怎么做:保留两个 script 运行一次,看页面 num 显示;再删掉旧 script 对比。调试时不要只看代码,要看浏览器实际渲染结果。

易错点:不要误以为旧 setup() 的返回值会覆盖语法糖中的同名变量;也不要因此把两种写法随意混用。语法糖只支持 Vue3,旧写法才可能结合 Vue2 语法,迁移时要明确边界。

完整调试顺序 ​

为什么需要:本节几个知识点容易混:普通变量、ref、reactive、双 script 优先级。按顺序点一遍最稳。

怎么想:先看静态展示,再点按钮看谁更新;先验证普通变量不更新,再验证 ref 和 reactive 更新;最后验证双 script 优先级。

怎么做:运行后依次点击:修改num 观察无变化;点击 count 文本或 修改count 观察数字变化;点击 修改obj->userName 观察 zhangsan 追加 !;再观察双 script 下 num 显示为语法糖中的值。

易错点:不要把“模板能显示”当成“响应式”;不要在 JS 中漏写 count.value;不要用普通对象替代 reactive 后期待更新。按这三点排查,基本能定位本节问题。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一:setup + ref + reactive(可直接双击运行的 HTML) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>setup + ref + reactive</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <h3>App</h3>

    <!-- ref:模板中自动解包,不用写 .value -->
    <p>count: {{ count }}</p>
    <button @click="count++">{{ count }}</button>
    <button @click="setCount">修改 count</button>

    <hr />

    <!-- reactive:对象里的属性直接响应 -->
    <p>{{ obj.userName }} / {{ obj.age }}</p>
    <button @click="obj.userName += '!'">修改 obj->userName</button>
    <button @click="addAge">obj.age++</button>
  </div>

  <script>
    const { createApp, ref, reactive } = Vue;

    const app = createApp({
      // setup 是组合式 API 的入口,在 beforeCreate 之前执行,this 为 undefined
      setup() {
        // ref 用于基本类型,返回一个 RefImpl 实例,值存在 .value 里
        const count = ref(9);

        // reactive 只能接收对象(或数组),返回 Proxy 实例
        const obj = reactive({ userName: "zhangsan", age: 12 });

        const setCount = function () {
          count.value += 2; // JS 中必须写 .value
        };

        const addAge = function () {
          obj.age++; // 对象属性直接改,不需要 .value
        };

        // setup 必须 return 一个对象,属性才能在模板中使用
        return { count, obj, setCount, addAge };
      },
    });

    app.mount("#app");
  </script>
</body>
</html>

关键行与易错点

  • setup() 在 beforeCreate 之前执行,this 是 undefined,所以不能用 this.count。
  • setup 不允许写成 async 函数,且必须 return 一个对象(返回非对象时模板拿不到数据)。
  • ref 在模板里自动解包(写 count),在 JS 里必须 .value(count.value += 2)——这是最常见的坑。
  • reactive 传 1、"abc" 这类基本类型会直接报错,它只接收对象/数组。
  • 若 data() 与 setup() 返回同名字段,以 setup 为准;但 setup 里直接声明的普通变量不是响应式。

示例二:setup 语法糖(放在脚手架项目的 src/App.vue) ​

vue
<template>
  <h3>setup 语法糖</h3>

  <p>{{ num }}</p>
  <button @click="changeNum">修改 num(改不动,直接定义的数据不支持响应式)</button>

  <hr />

  <p @click="count++">{{ count }}</p>
  <button @click="setCount">修改 count</button>

  <hr />

  <p>{{ obj.userName }}</p>
  <button @click="obj.userName += '!'">修改 obj->userName</button>
</template>

<script setup lang="ts">
// 之前的写法可以把 Vue2 语法与 Vue3 语法结合使用;使用语法糖后只支持 Vue3
// 语法糖就是在 script 标签上加 setup
// 之前要在 setup 函数中定义数据然后 return;语法糖中直接定义即可,模板直接使用
import { reactive, ref } from "vue";

let num = 100;            // 普通变量:模板能显示,但修改后视图不更新
let count = ref(9);       // ref:基本类型响应式
let obj = reactive({     // reactive:引用类型响应式
  userName: "zhangsan",
});

const changeNum = function () {
  num++;                  // 能改值,但视图不动
};

const setCount = function () {
  count.value += 2;       // 语法糖里也依然要 .value
};
</script>

<script lang="ts">
export default {
  setup() {
    return {
      num: 102,
    };
  },
};
</script>

关键行与易错点

  • 语法糖与旧写法同时出现(两个 script)时,以语法糖为准:上面 num 显示的是 100,而不是第二个 script 返回的 102。
  • 语法糖中 ref 的变量在模板里写 count,在 script 里仍要写 count.value,这一点和普通 setup 完全一样。
  • 语法糖里 import 的组件、ref / reactive 声明的方法,模板都能直接用,不需要 return,也不需要 components: {} 注册。
  • 直接用 let num = 100 定义的数据不是响应式,点击按钮视图不会变——这是语法糖最容易被误解的地方。
  • 本段是 .vue 单文件组件,需放在 Vue CLI 脚手架的 src/App.vue(入口 main.ts 用 createApp(App).mount("#app"))中运行。

示例三:Proxy 拦截原理(纯 HTML/JS,对应 ref、reactive 的底层) ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>Proxy 拦截演示</title>
</head>
<body>
  <p id="show"></p>
  <button id="btn">修改 proxy.userName</button>

  <script>
    const obj = { userName: "张三", age: 12 };

    // Proxy 是内置构造函数,需要 new;两个参数都必须是对象
    const proxy = new Proxy(obj, {
      // get:当获取 proxy 的属性时执行,三个参数
      get(target, key, receiver) {
        console.log("proxy-get", key);
        return target[key];
      },
      // set:当修改 proxy 的属性时执行,四个参数
      set(target, key, newValue, receiver) {
        console.log("proxy-set", key, newValue);
        target[key] = newValue;                 // 先改目标对象
        document.querySelector("#show").innerText = proxy.userName; // 再同步视图
        return true;                            // set 必须返回 true
      },
    });

    // 初始化视图
    document.querySelector("#show").innerText = proxy.userName;

    document.querySelector("#btn").onclick = function () {
      proxy.userName += "!"; // 走 set;如果写 obj.userName += "!" 则不会触发拦截
    };
  </script>
</body>
</html>

关键行与易错点

  • Proxy 是内置构造函数,必须 new Proxy(target, handler),target 和 handler 都必须是对象,传数字会直接报错。
  • get 的三个参数是 (target, key, receiver),set 的四个参数是 (target, key, newValue, receiver),receiver 就是 proxy 实例本身。
  • set 里必须 return true,否则在严格模式下赋值会抛错。
  • 拦截只对 proxy 生效,直接操作 obj 不会走 get/set——这正是 ref(基本类型用 Object.defineProperty、引用类型内部套一层 reactive)和 reactive(返回 Proxy)能实现响应式的底层原因。

常见错误 ​

  1. 现象:main.ts 里写 import App from "@/App",编译直接报找不到模块。 原因:入口 main.ts 不走 Vue 单文件解析,.vue 扩展名不能省略。 修法:改成 import App from "@/App.vue";组件内 import Child from "@/components/Child" 这类可省略的只限 .vue 内部引用。

  2. 现象:setup() 里写 this.num++,控制台报 Cannot read properties of undefined。 原因:setup 在 beforeCreate 之前执行,this 是 undefined,拿不到 data/methods。 修法:setup 内不碰 this,用 ref/reactive 声明数据、用普通函数改值并 return 出去;需要复用逻辑就抽成独立函数在 setup 里调用。

  3. 现象:data 里写了 userName: "lisi",setup 里也 return { userName: "zhangsan" },页面显示 zhangsan,改 data 的值页面纹丝不动。 原因:同名时以 setup 返回值为准,且 setup 直接返回的普通变量不是响应式。 修法:要么统一走 setup 并给数据套 ref/reactive,要么统一走 data,别混着写同名键。

  4. 现象:const count = ref(0),按钮里写 count++ 或函数里 count += 2,页面数字不变,控制台还打印出一个对象。 原因:ref 返回的是 RefImpl 实例,值在 .value 上;模板里 Vue 自动解包,但 JS/TS 里必须手动 .value。 修法:脚本中一律 count.value++;script setup 里也要写 .value,只有模板插值处省略。

  5. 现象:const obj = reactive(0) 或 reactive("abc"),改完值视图不更新,obj 也不是 Proxy。 原因:reactive 只能接收对象/数组,底层是 new Proxy,原始值无法代理。 修法:基本类型用 ref,对象/数组才用 reactive;必须包一层时用 reactive({ value: 0 })。

  6. 现象:const { userName } = reactive({userName:"zs"}) 后修改 userName,视图不更新;或者 obj = reactive({...}) 整体重新赋值后也失效。 原因:解构/重新赋值拿到的是普通值或新对象,脱离了原来的 Proxy 代理链。 修法:保留 obj.userName 的访问方式;确需解构用 toRefs;需要整体替换时用 ref 包对象或 Object.assign(obj, newObj)。

  7. 现象:Child.vue 里写 beforeDestroy/destroyed,v-if 切换时没有任何日志。 原因:Vue3 已重命名销毁类钩子,旧名不再被识别(且不报错)。 修法:改用 beforeUnmount/unmounted;Vue2 语法混用时其余选项式 API 仍可用,唯独这两个必须换名。

  8. 现象:devServer 里配了 port: 80、host: "zhangpeiyue.com",npm run serve 起不来或浏览器打不开。 原因:80 端口需管理员权限,自定义 host 未在本机 hosts 中映射。 修法:端口换 8080 等高位端口;host 要么删掉用默认,要么先在 hosts 文件加 127.0.0.1 zhangpeiyue.com。

练习 ​

  1. 入口改造:把脚手架的 main.ts 改成方式二写法(createApp(App).mount("#app")),并在 App.vue 中放三个并列根标签;再把 vue.config.js 的 lintOnSave 打开跑一次,记录报错内容并说明原因。

  2. 同名冲突实验:在同一个组件里同时写 data(){ return { age:100 } } 和 setup(){ return { age:10 } },页面显示哪个?再把 setup 中的 age 用 ref(10) 包一层,点击按钮 age.value++,观察视图是否更新,写出结论。

  3. ref / reactive 对照:用 ref 实现一个计数器,用 reactive 实现一个「用户名 + 年龄」的对象,各配一个修改按钮;然后在 setup 里 console.log(count) 与 console.log(obj),分别指出哪个是 RefImpl、哪个是 Proxy,以及各自改值时该不该写 .value。

  4. 语法糖迁移:把第 3 题改写成 <script setup lang="ts">,不写 return;然后额外保留一个普通 <script> 导出 export default { setup(){ return { num: 102 } } },验证两个 num 谁生效,并解释为什么 let num = 100 直接 num++ 页面不动。

  5. 钩子排查:给 Child.vue 分别写 beforeDestroy/destroyed 和 beforeUnmount/unmounted 四组日志,用 v-if 反复切换,记录哪些日志真正输出,并把错误的那组改正确。

原视频对照 ​

#视频时长
1proxy24:33
2实现基本类型的响应式15:07
3实现引用类型的响应式10:45
4组合API-reactive12:51
5reactive的原理08:27
6ref以及reactive的区别08:14
7setup语法糖11:29

小结 ​

  • main.ts 只做三件事:createApp 造 app 对象、传根组件、mount 指定容器;.vue 扩展名在入口处不可省。
  • Vue3 兼容大部分 Vue2 选项式语法,但销毁类钩子必须换成 beforeUnmount/unmounted,根模板允许多个元素。
  • setup 是组合式 API 的入口,早于 beforeCreate、this 为 undefined、必须返回对象、不能是 async 函数。
  • setup 与 data 同名时以 setup 为准;data 天生响应式,setup 里裸写的变量不响应式,必须用 ref/reactive 包装。
  • ref 适合基本类型(也能包对象),脚本中读写靠 .value,模板中自动解包;reactive 只吃对象/数组,返回 Proxy,直接 .属性 访问。
  • reactive 一旦解构或整体重新赋值就会丢响应式,需用 toRefs 或 Object.assign 规避。
  • <script setup> 省去 return,直接定义即可在模板使用,但同样只有 ref/reactive 包过的数据才响应式;与普通 <script> 并存时以语法糖为准。
  • 选型口诀:基本类型用 ref,对象数组用 reactive,写脚本记得 .value,写模板不用管。