Vue3新特性

166 阅读7分钟

Vue3


初始化项目

npm init @vitejs/app my-vue-app --template vue-ts


Composition API

composition-api 是一个 Vue3 中新增的功能,它的灵感来自于 React Hooks

Options API 与 Composition API 比较

代码是根据逻辑功能来组织的,一个功能所定义的所有 api 会放在一起(高内聚,低耦合),我们能快速的定位到这个功能所用到的所有 API,提高代码可读性和可维护性


生命周期钩子

1、beforeCreate -> 使用 setup()

2、created -> 使用 setup()

3、beforeMount -> onBeforeMount

4、mounted -> onMounted

5、beforeUpdate -> onBeforeUpdate

6、updated -> onUpdated

7、beforeDestroy -> onBeforeUnmount

8、destroyed -> onUnmounted

Vue3 兼容 vue2 的大部分写法,新增的生命周期钩子是在 setup 函数里面使用的,同时也可以和 vue2 的生命周期函数同时使用

import { onMounted } from "vue";
export default {
  beforeCreate() {
    console.log("beforeCreate");
  },

  mounted() {
    console.log("mounted");
  },

  setup() {
    console.log("setup");
    onMounted(() => {
      console.log("onMounted in setup");
    });
  },

  onMounted() {
    console.log("onMounted out setup");
  },
};

结果

setup
beforeCreate
onMounted in setup
mounted

Setup 函数

setup()函数是 Vue3 中,专门为组件提供的新属性。它为基于 Composition API 的新特性提供了统一的入口。

在 Vue3 中,定义 methods、watch、computed、data 数据都放在了 setup()函数中。

  • 调用时机

创建组件实例,然后初始化 props ,紧接着就调用 setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用。

  • this 不可用

this 在 setup() 中不可用

export default {
  setup() {
    console.log(this); //undefined
  },
};
  • 如果 setup 返回一个对象,则对象的属性可以在组件模板中使用。
<template>
  <h1>{{ msg }}</h1>
</template>

<script lang="ts">
export default {
  setup() {
    const msg = "hello world";
    return { msg };
  },
};
</script>

ref

接收一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value

当 ref 创建的属性在模板中使用时,它会自动解开,无需在模板内额外书写 .value

<template>
  <div>
    <h1>{{ count }}</h1>
    <button @click="change">count is: {{ count }}</button>

    <h1>{{ obj.count }}</h1>
    <button @click="changeObj">obj.count is: {{ obj.count }}</button>
  </div>
</template>

<script lang="ts">
import { ref } from "vue";
export default {
  setup() {
    const count = ref(0);
    const change = () => count.value++;

    const obj = ref({
      count: 0,
    });
    const changeObj = () => obj.value.count++;

    return { count, change, obj, changeObj };
  },
};
</script>

reactive

接收一个普通对象然后返回响应式的对象。

<template>
  <div>
    <h1>{{ state.count }}</h1>
    <button @click="changeState">count is: {{ state.count }}</button>
  </div>
</template>

<script lang="ts">
import { ref, reactive } from "vue";
export default {
  setup() {
    const state = reactive({
      count: 0,
    });

    const changeState = () => state.count++;

    return { state, changeState };
  },
};
</script>

注意事项

  • reactive 是利用 proxy 来实现
  • ref 则是用把数据给包装成 ref 对象, .value 的方式去访问其数据,在 setup 中需要,在模板中不需要, 因为会自动添加.value
  • 由于 reactive 必须传递一个对象, 所以导致在企业开发中,如果我们只想让某个变量实现响应式的时候会非常麻烦,所以 Vue3 就给我们提供了 ref 方法, 实现对简单值的监听
  • vue 强烈建议 ref 用来处理 非指针类型的数据类型, string number 等
  • ref 底层的本质其实还是 reactive ,系统会自动根据我们给 ref 传入的值将它转换成,ref(xx) -> reactive({value:xx})

toRefs

把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应。

<template>
  <div>
    <h1>a --- {{ a }}</h1>
    <h1>b --- {{ b }}</h1>
    <button @click="changeState">点我</button>
  </div>
</template>

<script lang="ts">
import { ref, reactive, toRefs } from "vue";
export default {
  setup() {
    const state = reactive({
      a: 0,
      b: 0,
    });

    const changeState = () => {
      state.a++;
      state.b--;
    };

    return { ...toRefs(state), changeState };
  },
};
</script>

data 和 setup 字段同时存在

<template>
  <div>
    <h1>{{ msg }}</h1>
  </div>
</template>

<script lang="ts">
import { ref } from "vue";
export default {
  data() {
    return {
      msg: "data里的msg",
    };
  },
  setup() {
    const msg = ref("setup里的msg");
    return { msg };
  },
};
</script>

显示 setup里的msg

watch

watch 需要侦听特定的数据源,并在回调函数中执行副作用。懒执行,也就是说仅在侦听的源变更时才执行回调。

<template>
  <div>
    <h1>{{ count }}</h1>
    <button @click="change">点我</button>

    <h1>{{ state.count }}</h1>
    <button @click="changeState">点我</button>
  </div>
</template>

<script lang="ts">
import { ref, reactive, toRefs, watch } from "vue";
export default {
  setup() {
    const count = ref(0);

    const change = () => {
      count.value++;
    };

    const state = reactive({
      count: 0,
    });

    const changeState = () => {
      state.count++;
    };

    watch(count, (newValue, oldValue) => {
      console.log(newValue, oldValue, "count发生改变");
    });

    watch(state, (newValue) => {
      console.log(newValue, "state发生改变");
    });

    watch(
      () => state.count,
      (newValue) => {
        console.log(newValue, "state.count发生改变");
      }
    );

    return { count, change, state, changeState };
  },
};
</script>

watch 也能监听多个值的变化

<template>
  <div>
    <h1>{{ count }}</h1>
    <button @click="change">点我</button>

    <h1>{{ state.count }}</h1>
    <button @click="changeState">点我</button>
  </div>
</template>

<script lang="ts">
import { ref, reactive, toRefs, watch, watchEffect } from "vue";
export default {
  setup() {
    const count = ref(0);
    const change = () => count.value++;

    const state = reactive({ count: 0 });
    const changeState = () => state.count++;

    watch([count, state, () => state.count], (val) => {
      console.log(val);
    });

    return { count, change, state, changeState };
  },
};
</script>

watch 返回一个函数,可以用来停止监听

<template>
  <div>
    <h1>{{ count }}</h1>
    <button @click="change">点我</button>

    <h1>{{ state.count }}</h1>
    <button @click="changeState">点我</button>
  </div>
</template>

<script lang="ts">
import { ref, reactive, toRefs, watch, watchEffect } from "vue";
export default {
  setup() {
    const count = ref(0);
    const change = () => count.value++;

    const state = reactive({ count: 0 });
    const changeState = () => state.count++;

    const stop = watch(count, (val) => {
      console.log(val);
      if (val === 3) {
        stop();
      }
    });

    return { count, change, state, changeState };
  },
};
</script>

watchEffect

立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数。

与 watch 的区别

  • watch 是需要传入需要侦听特定的数据源,而 watchEffect 是自动收集需要侦听特定的数据源。
  • watch 可以访问侦听状态变化前后的值,而 watchEffect 没有。
  • watch 是属性改变的时候执行,而 watchEffect 是默认会执行一次,然后属性改变也会执行。
<template>
  <div>
    <h1>{{ count }}</h1>
    <button @click="change">点我</button>

    <h1>{{ state.count }}</h1>
    <button @click="changeState">点我</button>
  </div>
</template>

<script lang="ts">
import { ref, reactive, toRefs, watch, watchEffect } from "vue";
export default {
  setup() {
    const count = ref(0);
    const change = () => count.value++;

    const state = reactive({ count: 0 });
    const changeState = () => state.count++;

    watchEffect(() => {
      console.log(count.value, state.count, "count或者state.count改变");
    });

    return { count, change, state, changeState };
  },
};
</script>

watchEffect也会返回一个函数,用于取消监听

computed

传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。

<template>
  <div>
    <h1>num: {{ num }}</h1>
    <h1>doubleNum: {{ doubleNum }}</h1>
    <h1>doubleNum2: {{ doubleNum2 }}</h1>
    <button @click="change">点我</button>
  </div>
</template>

<script lang="ts">
import { ref, reactive, toRefs, watch, watchEffect, computed } from "vue";
export default {
  setup() {
    const num = ref(0);

    const doubleNum = computed(() => {
      return num.value * 2;
    });

    const doubleNum2 = computed({
      get() {
        return num.value * 2;
      },
      set() {},
    });

    const change = () => {
      num.value++;
    };

    return { num, doubleNum, doubleNum2, change };
  },
};
</script>

当然,计算属性的值也能被watch和watchEffect监听到

依赖注入

Vue3 组合式 API 对依赖注入提供了 provide 和 inject 函数,功能类似 vue2 options API 的 provide/inject。provide 和 inject 函数都只能在当前活动组件实例的 setup() 中调用。

  • 基础 demo

上层组件

<template>
  <div>
    Fu
    <Zi />
  </div>
</template>

<script lang="ts">
import { provide } from "vue";
import Zi from "./Zi.vue";
export default {
  components: {
    Zi,
  },
  setup() {
    provide("Theme", "dark");
  },
};
</script>

下层组件

<template>
  <div>
    Zi
  </div>
</template>

<script lang="ts">
import { inject } from "vue";
export default {
  setup() {
    const theme = inject("Theme", "light" /* 默认值 */);
    console.log(theme);
  },
};
</script>

打印 dark

  • 组件传值

上层组件

<template>
  <div>
    <p>Fu count --- {{count}}</p>
    <button @click="change">点我</button>
    <Zi />
  </div>
</template>

<script lang="ts">
import { provide,ref } from "vue";
import Zi from "./Zi.vue";
export default {
  components: {
    Zi,
  },
  setup() {
    const count = ref(0)
    provide("count", count);

    const change = ()=>{
      count.value++
    }
    provide("change", change);

    return {
      count,
      change
    }
  },
};
</script>

下层组件

<template>
  <div>
    <p>Zi count --- {{ count }}</p>
    <button @click="change">点我改变上层组件的状态</button>
  </div>
</template>

<script lang="ts">
import { inject } from "vue";
export default {
  setup() {
    const count = inject("count");
    const change = inject("change");//可以直接调用上层组件提供的方法

    return { count, change };
  },
};
</script>

  • 直接修改上层组件值&&工具函数

上层组件

<template>
  <div>
    <p>Fu count --- {{ count }}</p>
    <p>Fu count2 --- {{ count2 }}</p>
    ---------------
    <Zi />
  </div>
</template>

<script lang="ts">
import { provide, ref, readonly } from "vue";
import Zi from "./Zi.vue";
export default {
  components: {
    Zi,
  },
  setup() {
    const count = ref(0);
    provide("count", count);

    const count2 = ref(0);
    provide("count2", readonly(count2));

    return {
      count,
      count2
    };
  },
};
</script>

下层组件

<template>
  <div>
    <p>Zi count --- {{ count }}</p>
    <p>Zi count2 --- {{ count2 }}</p>
    <button @click="change">点我</button>
  </div>
</template>

<script lang="ts">
import { Ref, inject } from "vue";
export default {
  setup() {
    const count: Ref<number> = inject("count");
    const count2: Ref<number> = inject("count2");

    const change = () => {
      count.value++;
      count2.value++;
    };

    return { count, count2, change };
  },
};
</script>

vue3 还有一些其它 API,如下:

1、readonly 把对象变成只读的。

2、isRef 检查一个值是否为一个 ref 对象。

3、isProxy 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理。

4、isReactive 检查一个对象是否是由 reactive 创建的响应式代理。

5、isReadonly 检查一个对象是否是由 readonly 创建的只读代理。

6、unref 如果参数是一个 ref 则返回它的 value,否则返回参数本身

拆分setup代码

有点像react的自定义hook

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="changeCount">点我</button>
  </div>
</template>

<script lang="ts">
import { ref } from "vue";
export default {
  setup() {
    const { count, changeCount } = useCount(0);
    return { count, changeCount };
  },
};
function useCount(num: number) {
  const count = ref(num);
  const changeCount = () => {
    count.value++;
  };
  return { count, changeCount };
}
</script>

script setup 语法糖

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="change">点我</button>
    <p>script setup</p>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

const count = ref(0);

const change = () => {
  count.value++;
};

</script>

setup 语法糖 ref

<template>
  <div>
    <p>{{ count }}</p>
    <p>{{ count2 }}</p>
    <button @click="change">点我</button>
    <p>script setup - ref</p>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

const count = ref(0);
ref: count2 = 0

const change = () => {
  count.value++;
  count2++
};

</script>

setup 语法糖 css

<template>
  <div>
    <p class="test">hello world</p>
    <button @click="change('red')">red</button>
    <button @click="change('blue')">blue</button>
  </div>
</template>

<script lang="ts" setup>
import { ref } from "vue";

const color = ref("red");
const num = ref(0);
const change = (v) => {
  color.value = v;
  num.value += 10;
};
</script>

<style>
.test {
  color: v-bind(color);
  margin-left: v-bind('num + "px"');
}
</style>

teleport

传送门:可以指定元素追加在哪里(指定父元素)

场景:弹窗在组件内部编写,但是弹窗的DOM按理说不应该出现在组件DOM节点内部,一般加在body上。

<template>
  <div>
    <button @click="flag = true">点我显示</button>
    <teleport to="body">
      <div v-if="flag" class="modal">
        <div class="modal-box" @click="flag = false">点我关闭</div>
      </div>
    </teleport>
  </div>
</template>

<script lang="ts">
import { ref } from "vue";
export default {
  setup() {
    const flag = ref(false);
    return { flag };
  },
};
</script>

<style lang='scss' scoped>
.modal {
  position: fixed;
  top: 0px;
  left: 0px;
  bottom: 0px;
  right: 0px;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  .modal-box {
    width: 200px;
    height: 200px;
    background: #fff;
    border-radius: 10px;
    display: flex;
    justify-content: center;
    align-items: center;
  }
}
</style>

如下图所示,能把modal弹窗追加在body上


Vite

初始化项目npm init @vitejs/app my-vue-app --template vue-ts

初始化的项目目录如下:

  • index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>
  • src/main.ts
import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

index.html中引入了src/main.tsmain.ts 引入 App.vue 并挂在到 html 中,流程简单的不行,打开浏览器组件也确实渲染出来了。

这一步的实现 离不开 Es 的 modules , 浏览器通过<script module>,为每个导入生成 HTTP 请求, vite 的 dev 服务拦截 http 请求,并把代码做一些转换之后返回给浏览器进行渲染

通常情况下,我们在浏览器输入 URL 访问一个网站,浏览器就会去服务器 请求对应的资源文件,这一点大家也都是知道的。所以在我们运行yarn dev之后,vite 启动了一个 dev server 去拦截我们请求的资源文件,所以我们在浏览器看到的页面实际上是经过 vite 处理后的 html 文件

  • webpack

本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图对应映射到项目所需的每个模块,并生成一个或多个 bundle。

Vue 脚手架工具 vue-cli 使用 webpack 进行打包,开发时可以启动本地开发服务器,实时预览。因为需要对整个项目文件进行打包,开发服务器启动缓慢。

而对于开发时文件修改后的热更新 HMR 也存在同样的问题。

Webpack 的热更新会以当前修改的文件为入口重新 build 打包,所有涉及到的依赖也都会被重新加载一次。

而 Vite 以 原生 ESM 方式服务源码。这实际上是让浏览器接管了打包程序的部分工作:Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入的代码,即只在当前屏幕上实际使用时才会被处理。

Vite 则很好地解决了上面的两个问题。

先来看打包问题。vite 只启动一台静态页面的服务器,对文件代码不打包,服务器会根据客户端的请求加载不同的模块处理,实现真正的按需加载。

对于热更新问题,vite 采用立即编译当前修改文件的办法。同时 vite 还会使用缓存机制( http 缓存 => vite 内置缓存 ),加载更新后的文件内容。

所以,vite 具有了快速冷启动、按需编译、模块热更新等优良特质。

综上所述,vite 构建项目与 vue-cli 构建的项目在开发模式下还是有比较大的区别:

  • Vite 在开发模式下不需要打包可以直接运行,使用的是 ES6 的模块化加载规则;Vue-CLI 开发模式下必须对项目打包才可以运行。
  • Vite 基于缓存的热更新,Vue-CLI 基于 Webpack 的热更新。