• vue3 快速入门系列 —— 基础


    vue3 快速入门系列 - 基础

    前面我们已经用 vue2 和 react 做过开发了。

    从 vue2 升级到 vue3 成本较大,特别是较大的项目。所以许多公司对旧项目继续使用vue2,新项目则使用 vue3。

    有些UI框架,比如ant design vue1.x 使用的 vue2。但现在 ant design vue4.x 都是基于 vue3,示例默认是 TypeScript。比如 table 组件管理。

    另外 vue3 官网介绍也使用了 TypeScript,例如:响应式 API:核心

    本篇主要介绍:vite 创建vue3项目、组合式api、响应式数据、计算属性、监听、ref、ts、生命周期、自定义hooks。

    vue3 简介

    Vue.js 3.0,代号海贼王,于2020年9月18日发布 —— v3.0.0 海贼王

    主要有如下改进:

    • 性能改进:与 Vue 2 相比,Vue 3 在包大小(通过 Tree-Shaking 减少最多 41%)、初始渲染(快 55%)、更新(快 133%)和内存使用方面表现出了显着的性能改进(最多减少 54%)。
    • 拥抱 TypeScript:更好的支持 TS。有的公司在 vue2 中就用 TS 了
    • 用于应对规模问题的新 API:引入了Composition API——一组新的 API,旨在解决大规模应用程序中 Vue 使用的痛点。Composition API 构建在反应性 API 之上,支持类似于 React hooks 的逻辑组合和重用、更灵活的代码组织模式以及比 2.x 基于对象的 API 更可靠的类型推断。
    • 分层内部模块:还公开了较低级别的 API,可解锁许多高级用例

    创建 vue3 工程

    vue-cli 创建

    前面我们用 vue-cli 创建过 vue2 的项目,用其构建 vue3 也类似,差别就是选择 vue3 版本。最后生成的项目结构如下:

    Vue CLI 是官方提供的基于 Webpack 的 Vue 工具链,它现在处于维护模式。我们建议使用 Vite 开始新的项目,除非你依赖特定的 Webpack 的特性。在大多数情况下,Vite 将提供更优秀的开发体验 —— 官网 - 项目脚手架

    vite 创建

    另一种方式是使用 vite。有如下优势:

    • 对 TypeScript、JSX、CSS 等支持开箱即用。
    • 无论应用程序大小如何,都始终极快的模块热替换(HMR)
    • 极速的服务启动。使用原生 ESM(参考 mdn esm) 文件,无需打包

    Tip:

    1. vue脚手架(vue-cli) 和创建 react的脚手架(create-react-app)都是基于 webpack。而 vite 也是一种构建工具,和 webpack 类似,也有一些区别,其作者就是 Vue.js 的创始人尤雨溪
    2. HMR 它用于开发环境,不适用于生产环境。更多介绍请看这里
    3. jsx 在学习 react 中用到过(请看这里),vue 中用 template 写视图部分,react 用 jsx。在 Vue 3 项目中使用 JSX 时,Vite 会将 JSX 语法编译为 Vue 3 的渲染函数。

    笔者首先使用 npm create vite@latest 创建项目,自己根据需要选择对应预设(比如要 TypeScript or javascript),创建完成后根据提示进入项目,安装依赖,本地启动:

    npm install
    npm run dev
    

    结果报错:

    > vite-vue3@0.0.0 dev \test-projects\vite-vue3
    > vite
    
    (node:40312) UnhandledPromiseRejectionWarning: SyntaxError: Unexpected token '??='
        at Loader.moduleStrategy (internal/modules/esm/translators.js:145:18)
    (Use `node --trace-warnings ...` to show where the warning was created)
    (node:40312) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
    (node:40312) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
    

    说是 node 版本可能低了。

    Tip: Vite 需要 Node.js 版本 14.18+,16+。然而,有些模板需要依赖更高的 Node 版本才能正常运行,当你的包管理器发出警告时,请注意升级你的 Node 版本 —— vite 官网-搭建第一个 Vite 项目

    于是使用 nvm 安装 18.16.0。步骤如下:

    // 目前版本 14.19
    PS \test-projects\vite-vue3> node -v
    v14.19.0
    
    // nvm 已安装
    PS \test-projects\vite-vue3> nvm -v
    1.1.10
    
    // nvm 安装 18.16.0
    PS \test-projects\vite-vue3> nvm install 18.16.0
    Downloading node.js version 18.16.0 (64-bit)...
    Extracting node and npm...
    Complete
    npm v9.5.1 installed successfully.
    
    
    Installation complete. If you want to use this version, type
    
    nvm use 18.16.0
    

    根据提示切换到 18.16.0

    PS \test-projects> nvm use 18.16.0
    Now using node v18.16.0 (64-bit)
    PS \test-projects> node -v
    v18.16.0
    
    npm create vue

    使用 npm create vue@latest 创建 vue3 项目 —— vue3 官网 创建一个 Vue 应用(这里提到 node 需要18+):

    PS \test-projects>  npm create vue@latest
    Need to install the following packages:
      create-vue@3.9.2
    Ok to proceed? (y) y
    
    Vue.js - The Progressive JavaScript Framework
    
    √ 请输入项目名称: ... hello_vue3
    √ 是否使用 TypeScript 语法? ... 否 / 是
    √ 是否启用 JSX 支持? ... 否 / 是
    √ 是否引入 Vue Router 进行单页面应用开发? ... 否 / 是
    √ 是否引入 Pinia 用于状态管理? ... 否 / 是
    √ 是否引入 Vitest 用于单元测试? ... 否 / 是
    √ 是否要引入一款端到端(End to End)测试工具? » 不需要
    √ 是否引入 ESLint 用于代码质量检测? ... 否 / 是
    
    正在构建项目 \test-projects\hello_vue3...
    
    项目构建完成,可执行以下命令:
    
      cd hello_vue3
      npm install
      npm run dev
    
    npm notice
    npm notice New major version of npm available! 9.5.1 -> 10.4.0
    npm notice Changelog: https://github.com/npm/cli/releases/tag/v10.4.0
    npm notice Run npm install -g npm@10.4.0 to update!
    npm notice
    

    根据提示按照依赖,本地启动项目成功:

    PS \test-projects> cd .\hello_vue3\
    PS \test-projects\hello_vue3> npm install
    
    added 63 packages, and audited 64 packages in 20s
    
    7 packages are looking for funding
      run `npm fund` for details
    
    found 0 vulnerabilities
    PS \test-projects\hello_vue3> npm run dev
    
    > hello_vue3@0.0.0 dev
    > vite
    
    
      VITE v5.1.3  ready in 3045 ms
    
      ➜  Local:   http://localhost:5173/Network: use --host to expose
      ➜  press h + enter to show help
    
    npm create vite/vue

    npm create vite@latest 和 npm create vue@latest 作用和用途不同,两者效果也不同,总的来说前者创建 Vite 项目,而 npm create vue@latest 是用来创建 Vue.js 项目。

    PS \test-projects>  npm create vite@latest
    Need to install the following packages:
      create-vite@5.2.0
    Ok to proceed? (y) y
    √ Project name: ... hello-vue3
    √ Select a framework: » VueSelect a variant: » TypeScript
    
    Scaffolding project in \test-projects\hello-vue3...
    
    Done. Now run:
    
      cd hello-vue3
      npm install
      npm run dev
    
    vite 本地启动非常快

    vite 本地启动非常快。真正按需编译,不在等待整个应用编译完成。

    用 webpack 本地启动服务器,需要经历如下几步:entry->route->module->bundle->服务器启动(下图左);而用 vite 启动服务器,服务器启动却从末尾移到开头(下图右)

    有点像懒加载,你需要访问哪个路由,就加载哪个,非常快速。

    vue3项目目录结构浅析

    前面我们用 vite 创建了 hello_vue3 项目。目录结构如下:

    我们先说其他文件,最后在分析src文件夹

    extensions.json

    内容如下:

    // .vscode/extensions.json
    {
      "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
    }
    

    推荐你安装这两个插件,当你用 vscode 启动项目,点击切换到其他文件上,vscode 右下角就会提示你是否安装这两个插件。就像这样:

    这两个是vue官方给 vscode 提供的插件:

    • TypeScript Vue Plugin (Volar)
    • Vue Language Features

    env.d.ts

    内容如下:

    /// 
    

    是一个在 Vue.js 项目中使用 Vite 构建工具时引入的指令,它的作用是让 TypeScript 编译器能够识别并利用 Vite 客户端类型声明文件提供的类型信息,以提供更好的智能编码功能和类型检查支持。

    Tip:如果你删除 node_modules 文件夹,你在vscode 中会发现 vite/client 下有红色波浪线。

    TypeScript 主要用于处理 JavaScript 代码,并且在处理模块时,它会关注 .ts、.tsx、.js 和 .jsx 这些与 JavaScript 相关的文件类型。

    TypeScript 默认情况下并不会识别或处理像 .txt、.gif 这样的非 TypeScript 文件类型。这个文件的作用就是让 ts 认识 txt、jpg、gif等。

    比如你在src 下新建 a.txt、b.ts,然后在 b.ts 中编写:

    import a from 'a.txt'
    console.log(a)
    

    当你清空 env.d.ts,你会发现 import a from 'a.txt'中 a.txt 下有红色波浪线。再次还原 env.d.ts 则好了。

    通过 ctrl + 鼠标点击进入 vite/client,你会发现 vue 给我们声明好了我们需要使用的其他类型文件。比如 txt:

    declare module '*.txt' {
      const src: string
      export default src
    }
    

    index.html

    index.html 这就是我们的入口文件。内容如下:

    DOCTYPE html>
    
      
        
        
        
        Vite App
      
      
        

    你可以尝试改成

    
      a
    
    

    无需重启服务,页面就显示 a

    其他

    • tsconfig 文件,ts 配置相关,不要删,ts 可能会有问题:
    tsconfig.app.json
    tsconfig.json
    tsconfig.node.json
    
    • vite.config.ts 项目配置文件。比如代理、安装插件

    • public/favicon.ico 页签图标

    • package.json、package-lock.json

    src

    src 就是我们编码的地方。

    我们先将 src 中的文件都删除,我们自己重新创建。

    创建 main.ts 和 App.vue 两个文件。内容如下:

    • main.ts 是index.html加载的入口文件
    // src/main.ts
    import {createApp} from 'vue'
    // 项目的根
    import App from './App.vue'
    
    // Vue.js 3.x 中用于创建和挂载应用
    // 创建一个新的 Vue 应用,并将根组件指定为 App。.mount('#app') 将应用挂载到指定的 DOM 元素上
    createApp(App).mount('#app')
    
    // src/App.vue
    
    
    
    <script lang="ts">
    
    script>
    
    <style scoped>
    
    style>
    

    浏览器访问,页面显示 你好 vue3

    前面我们说到 vite 启动后,服务器就已就绪。然后会根据用户请求哪里,就会给你加载哪里。

    vue3 向下兼容 vue2 语法

    有些项目使用了 vue3,但写法还是 vue2 —— 不建议这么做

    为了证明 vue3 中能写 vue2,笔者在 vue3 项目中写一个 vue2 示例。请看代码:

    // src/App.vue
    
    
    <script lang="ts">
    export default {
        name: 'App',
        data() {
            return {
              name: 'pengjiali',
              date: -1,
            }
        },
        methods: {
          changeDate() {
            this.date = new Date().getTime();
          }
        }
    }
    script>
    

    浏览器显示:

    name: pengjiali
    
    date: -1
    
    // 按钮,点击后,date 后的数字就会变化
    change date
    

    options Api 和 compositionApi

    Vue 2 使用的是选项式 API,而 Vue 3 引入了组合式 API

    虽然 Vue 3 推荐使用组合式 API,但它仍然完全支持 Vue 2 的选项式 API,以保持向下兼容性。所以在 Vue 3 中,你可以自由选择使用选项式 API 或组合式 API 来编写你的组件逻辑。

    选项式API有一个缺点:新增一个功能,需要分别在 data、methods、computed、watch等选项中修改代码,如果代码上千,修改或抽取封装这部分功能,有困难。

    Tip:我们用 大帅老猿 的图说明以下这个问题

    而组合式 api 可以简化这个问题,我们可以感受下(代码如何实现暂时不用管):

    Tip: 具体如何拆分,请看本篇最后自定义 hooks章节。

    setup

    setup 函数是组合式 API 的入口,用于组合组件的逻辑和功能。

    setup 概述

    首先我们用 vue2 语法写一个示例:展示名字和日期,点击按钮能改变日期。代码如下:

    
    
    <script lang="ts">
    export default {
        name: 'App',
        data() {
            return {
              name: 'pengjiali',
              date: -1,
            }
        },
        methods: {
          changeDate() {
            this.date = new Date().getTime();
          }
        }
    }
    script>
    

    现在我们把 data 和 methods 两个配置去除,改成 setup 就完成了 vue3 示例的重构

    
    
    <script lang="ts">
    export default {
        name: 'App',
        setup() {
          let name = 'pengjiali2'
          let date = -1
    
          function changeDate(){
            date = new Date().getTime();
            console.log('date: ', date);
          }
          // 将数据和方法都交出去
          return {name, date, changeDate}
        }
    }
    script>
    

    setup 是一个方法,平时如何定义变量和方法,这里就怎么写,最后将方法和变量都交出去。

    这里其实还有一个问题,点击 button 日期在界面没变,但方法却执行了。这是因为 date 变量不是响应式的。

    Tip:现在我们先说 setup,后面在将响应式的东西。这里要修复可以使用 ref(这个 ref 和 vue2 中指向元素或组件的ref,不是同一个东西):

     
    
    <script lang="ts" setup>
    // 属性和方法自动交出去
    
    let name = "pengjiali";
    let date = ref(-1);
    
    function changeDate() {
      date.value = new Date().getTime();
      console.log("date: ", date);
    }
    script>
    
    方式2

    方式一还是需要写l了两个

    不想写两个

    能显示,能修改,一切正常。

    虽然 ref 能处理基本类型和对象,但是遇到对象,实际上是摇人了。请看示例:

    const person = ref({
      name: "pengjiali",
      date: -1,
    })
    
    const count = ref(1)
    // count: RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 1, _value: 1}
    console.log('count: ', count);
    // person:  RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: {…}, _value: Proxy(Object)}
    console.log('person: ', person);
    

    查看 person 对象的 value 属性,发现了 Proxy(Object),所以本质上是 reactive 处理了对象

    ref vs reactive

    宏观:

    • ref 能定义基本类型和对象的响应式数据
    • reactive 只能用于对象
    ref 自动生成 .value

    写代码时还得记着是 ref 类型,需要增加 .value,好麻烦。可以使用 vscode 插件:

    vscode 直接安装 Vue - Official(vscode 提示 TypeScript Vue Plugin (Volar) 已弃用,使用 Vue - Official 替代)

    通过 vscode 设置,勾选 Auto-complete Ref value with .value,并设置 Applies to all profiles

    重启后,只要输入 ref 变量,则会自动添加 .value,非常方便。

    const person = ref({
      name: "pengjiali",
      date: -1,
    })
    const person2 = reactive({
      name: "pengjiali",
      date: -1,
    })
    // 输入 person 则会自动添加 .value
    person.value
    
    // 对于非 ref 则不会添加 .value
    person2
    
    reactive 的局限性

    reactive 重新分配一个对象,会失去响应式(可使用 Object.assign 整体替换)。请看示例:

    
    
    <script lang="ts" setup name="App">
    
    import { ref, reactive } from "vue";
    
    let person = reactive({
      name: "pengjiali",
      age: 18,
    })
    
    function changePerson() {
      // 失效 - 响应性连接已丢失!
      // person = reactive({name: 'peng', age: 25})
    
      // 失效
      // person = {name: 'peng', age: 25}
    
      // 正常
      Object.assign(person, {name: 'peng', age: 25})
    }
    script>
    

    Tip: Object.assign() 静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。

    let target = {a: 1, b: 2};
    let source1 = {b: 4, c: 5};
    let source2 = {c: 6, d: 7};
    
    Object.assign(target, source1, source2);
    
    console.log(target); // 输出: {a: 1, b: 4, c: 6, d: 7}
    

    如果是 ref,直接替换即可。就像这样

    let person = ref({
      name: "pengjiali",
      age: 18,
    })
    
    function changePerson() {
      // 直接替换
      person.value = {name: 'peng', age: 25}
    }
    

    ref 和 reactive 使用场景

    由于这些限制,我们建议使用 ref() 作为声明响应式状态的主要 API —— 官网 - reactive 局限性

    笔者习惯:

    • 需要一个基本类型的响应式数据,只可使用 ref
    • 对象使用 reactive
    • 如果是表单,使用 ref 会出现很多 .value,不好看

    toRefs

    将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

    不明白请看下面代码。

    比如这段代码:

    
    
    <script lang="ts" setup name="App">
    
    import { ref, reactive } from "vue";
    
    let person = reactive({
      name: "pengjiali",
      age: 18,
    })
    
    function changePerson() {
      Object.assign(person, {name: 'peng', age: 25})
    }
    script>
    

    我从响应式对象中解构出 age,然后通过方法修改 age 的值,发现页面没更新:

    +    <p><button @click="changeAge">change agebutton>p>      
         <p><button @click="changePerson">change personbutton>p>
       
     
    let person = reactive({
       age: 18,
     })
    
    +let {age} = person
    +
    +function changeAge(){
    +  age += 1;
    +}
    +
    

    这是因为解构出的 age 不在是响应式。可以使用 toRefs,就像这样:

    -import { ref, reactive } from "vue";
    +import { ref, reactive, toRefs } from "vue";
    
     let person = reactive({
       name: "pengjiali",
       age: 18,
     })
    
    -let {age} = person
    +let {age} = toRefs(person)
    +// age: ObjectRefImpl {_object: Proxy(Object), _key: 'age', _defaultValue: undefined, __v_isRef: true}
    +console.log('age: ', age);
    
     function changeAge(){
    -  age += 1;
    +  age.value += 1;
     }
    
    

    toRef

    说 toRef 用的较少。

    比如层级比较深的场景,请看示例:

    
    
    <script lang="ts" setup name="App">
    import { ref, reactive, toRefs, toRef } from "vue";
    
    let person = reactive({
      name: "张三",
      age: 18,
      job: {
        ja: {
          salary: 20,
        },
      },
    });
    let name = toRef(person, "name");
    let salary = toRef(person.job.ja, "salary");
    script>
    

    计算属性

    作用和vue2相同,先回忆下 vue2 中的计算属性。写法如下:

    computed: {
      now: function () {
        
      }
    }
    

    改成 vue3 需要使用 computed 方法。就像这样:

    let now = computed(() => {
      return Date.now()
    })
    

    请看示例:

       
    <p>name: {{ person.name }}p> <p>age: {{ person.age }}p> - + <p>name_age: {{ name_age }}p> <p><button @click="changePerson">change personbutton>p>
    <script lang="ts" setup name="App"> -import { ref, reactive } from "vue"; +import { ref, reactive, computed } from "vue"; let person = reactive({ name: "pengjiali", age: 18, }); +const name_age = computed(() => `${person.name}-${person.age}`) function changePerson() { Object.assign(person, { name: "peng", age: 25 }); }

    Tip:和 vue2 中类似,set很少用。不多介绍,用法大致如下:

    
    let fullname = computed({
      get(){
    
      },
      set(){
    
      }
    })
    
    // 触发 set 方法
    fullName.value = 'li-si'
    

    watch

    vue3 中 watch 作用应该和 vue2 中相同,先回忆下vue2 中 watch 写法。就像这样:

    new Vue({
      data: {
        message: 'Hello, Vue!'
      },
      watch: {
        message: function(newValue, oldValue) {
          console.log('消息从', oldValue, '变为', newValue);
        }
      }
    });
    

    vue3 中说 watch 只能监视4种数据:

    • ref定义的数据
    • reactive 定义的数据
    • 函数返回一个值(getter函数)
    • 一个包含上述内容的数组

    Tip: vue2 watch 中有deep、immediate、unwatch,下文 vue3 中 watch 也都有。

    ref 基本类型

    请看示例:

    
    
    <script lang="ts" setup name="App">
    import { ref, watch } from "vue";
    
    let age = ref(18)
    // watch(age.value, ... ) 错误写法 
    let stopWatch = watch(age, (newValue, oldValue) => {
      console.log('年龄从', oldValue, '变为', newValue);
    });
    
    script>
    
    • watch 监视的ref变量,无需增加 .value。安装好vscode 插件,在这种情况下也不会自动给你加 .value。
    • watch 返回一个函数,执行后将解除监视。就像 vue2 中的 vm.$watch 方法,返回 unwatch。

    ref 对象类型

    核心语法:

    watch(person, (newValue, oldValue) => {
    
    }, { deep: true});
    

    比如用 ref 定义一个对象,里面有两个按钮,一个只改变“年龄”,一个改变整个 ref 对象。就像这样:

    
    
    <script lang="ts" setup name="App">
    import { ref, watch } from "vue";
    
    let person = ref({
      name: "pengjiali",
      age: 18,
    });
    
    // 完全替换person,newValue 和 oldValue 不同
    // 只替换person中属性,newValue 和 oldValue 相同。通常工作只关心新值
    watch(person, (newValue, oldValue) => {
          console.log('Person changed');
          console.log('New person:', newValue);
          console.log('Old person:', oldValue);
        }, );
    
    function changePerson() {
      person.value = {name: 'peng', age: 100}
    }
    script>
    

    只有改变整个对象时 watch 中的方法才会执行,而改变ref对象中的属性,watch 方法却不会执行。

    加上一个配置项,这样改变整个对象,以及改变ref对象中的属性,watch 中的方法都会执行。

           console.log('New person:', newValue);
           console.log('Old person:', oldValue);
    -    }, );
    +    }, {deep: true});
    

    其实还有一个属性 immediate,初始时就会执行 watch 中的方法。就像这样:

    // 完全替换person,newValue 和 oldValue 不同
    // 只替换person中属性,newValue 和 oldValue 相同。通常工作只关心新值
    watch(person, (newValue, oldValue) => {
          console.log('Person changed');
          console.log('New person:', newValue);
          console.log('Old person:', oldValue);
        }, { deep: true, immediate: true });
    

    reactive

    核心语法:

    watch(person, (newValue, oldValue) => {
         
    });
    

    完整示例:

    
    
    <script lang="ts" setup name="App">
    import { reactive, ref, watch } from "vue";
    
    let person = reactive({
      name: "pengjiali",
      age: 18,
    });
    
    // 默认开启深度监听,而且通过 {deep: false} 也关闭不了
    watch(person, (newValue, oldValue) => {
          console.log('Person changed');
          console.log('New person:', newValue);
          console.log('Old person:', oldValue);
        }, {deep: false});
    
    function changePerson() {
      // 不能整个替换,只能用 Object.assign。不能像 ref.value = {...} 
      Object.assign(person, {name: 'peng', age: 100})
    }
    script>
    

    监视 ref 或 reactive 的对象中的某属性

    前面我们监视的都是整个对象,比如现在要监视对象中的某个属性。这里分为基本类型对象类型

    // reactive 和 ref 都可以用如下形式
    // 利用 getter。如果需要则增加 deep
    watch(() => person.car, () => {
    
    }, {deep: true})
    
    基本类型

    就以 reactive 对象为例,直接将监视源改为 person.name vscode 就会出现红色波浪线:

    
    
    <script lang="ts" setup name="App">
    import { reactive, ref, watch } from "vue";
    
    let person = reactive({
      name: "pengjiali",
      age: 18,
    });
    
    -watch(person, (newValue, oldValue) => {
    +watch(person.name, (newValue, oldValue) => {
           console.log('Person changed');
     });
    
    script>
    

    运行后在浏览器控制台中报错更明显:

    // 无效的监视源:只能是 getter 函数、ref、reactive object、或这些类型的数组
    App.vue:17 [Vue warn]: Invalid watch source:  pengjiali A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types. 
      at <App>
    

    现在 person.name 不属于上述4种类型。

    将 person.name 改成 getter。代码如下:

    Tip:getter 一个函数,返回一个值 —— vue3 watch

    watch(() => person.name, (newValue, oldValue) => {
          console.log('Person changed');
    });
    

    这样修改 age 时不会触发 watch,只有 name 改变时才会触发 watch。

    对象类型

    这里给 person 定义了一个 jineng 的对象属性,并定义两个按钮,一个会改变 jineng 的属性,一个改变整个技能。代码如下:

    
    
    <script lang="ts" setup name="App">
    import { reactive, ref, watch } from "vue";
    let person = reactive({
      name: "pengjiali",
      age: 18,
      jineng: {
        a: '吃饭',
        b: '睡觉',
      }
    });
    console.log('person: ', person);
    // person.jineng:  Proxy(Object) {a: '吃饭', b: '睡觉'}
    console.log('person.jineng: ', person.jineng);
    function changeJineng(){
      person.jineng = {a: 'a吃饭', b:'a睡觉'}
    }
    script>
    

    首先我们这么写,发现只能监听 jineng 里面的属性改变:

    // 点击`change jineng.a` 执行
    // 点击`替换 jineng` 不执行
    watch(person.jineng, () => {
      console.log('watch jineng');
    })
    

    Tip:通过打印我们知道 person.jineng 类型是Proxy,也就是 reactive 类型,根据前文我们知道 reactive 默认开启深度监视,而且不能整个替换,之前用的都是 Object.assign,这里用的是 person.jineng = {a: 'a吃饭', b:'a睡觉'}

    改成 getter 发现只能监听替换整个 jineng:

    // 点击`change jineng.a` 不执行
    // 点击`替换 jineng` 执行
    watch(() => person.jineng, () => {
      console.log('watch jineng');
    })
    

    在 getter 基础上增加 {deep: tree} 则都能监视到:

    // 点击`change jineng.a` 执行
    // 点击`替换 jineng` 执行
    // 说官网一直都是用函数
    watch(() => person.jineng, () => {
      console.log('watch jineng');
    }, {deep: true})
    

    Tip:将上述示例从 reactive 改成 ref,watch 监视方式还是不变。请看代码:

    
    
    <script lang="ts" setup name="App">
    import { reactive, ref, watch } from "vue";
    let person = ref({
      name: "pengjiali",
      age: 18,
      jineng: {
        a: '吃饭',
        b: '睡觉',
      }
    });
    // person.jineng:  Proxy(Object) {a: '吃饭', b: '睡觉'}
    console.log('person.jineng: ', person.value.jineng);
    function changeJineng(){
      person.value.jineng = {a: 'a吃饭', b:'a睡觉'}
    }
    
    watch(() => person.value.jineng, () => {
      console.log('watch jineng');
    }, {deep: true})
    
    script>
    

    监视多个

    核心语法:

    watch([() => xx.name, () => xx.xx.age], (newValue, oldValue) {
      // newValue oldValue 是整个数组
    })
    
    // 通常这么写
    watch([() => xx.name, () => xx.xx.age], (value) {
      const [name, age] = value;
      // ...
    })
    

    前面几种学完了,监视多个就是赠送。请看示例:

    
    
    <script lang="ts" setup name="App">
    import { reactive, ref, watch } from "vue"; 
    let person = reactive({
      name: "pengjiali",
      age: 18,
      jineng: {
        a: '吃饭',
        b: '睡觉',
      }
    });
    function changeJineng(){
      person.jineng = {a: 'a吃饭', b:'a睡觉'}
    }
    
    watch([() => person.name, () => person.jineng.a], (newVal, oldVal) => {
      console.log('newVal: ', newVal, 'oldVal: ', oldVal);
    })
    script>
    

    总结

    用的较多的有:

    • ref 基本类型
    • 监视对象中某个属性,反手就是一个函数,无论是基本类型、ref还是reactive都可以。

    watchEffect

    核心语法:

    // watchEffect 是一个立即执行的副作用操作,因此回调函数会在组件渲染时立即执行一次,并在每个相关响应式数据变化时再次执行。
    watchEffect(() => {
       // 立即执行
      console.log('立即执行');
      if(temp.value > 60 || height.value >80){
        ...
      }
    })
    

    比如我需要在”温度“和”高度“大于20的时候发出请求,用 watch 可以这么实现: