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.ts, main.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 的热更新。