是什么
vue2 的升级版, 使用 ts 重构了代码, 带来了 Composition API RFC。 类似于 react hook 的写法。
- ts 重构,代码可读性更强
- vue3.x 使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty
- 实现了 TreeShaking (当 Javascript 项目达到一定体积时,将代码分成模块会更易于管理。但是,当这样做时,我们最终可能会导入实际上未使用的代码。Tree Shaking 是一种通过消除最终文件中未使用的代码来优化体积的方法。)
- 支持 hook 写法 CompositionAPI。受 ReactHook 启发
- 支持 jsx
- Vue 3 的 Template 支持多个根标签,Vue 2 不支持
- 对虚拟 DOM 进行了重写、对模板的编译进行了优化操作
- 在 Vue2.x 中具名插槽和作用域插槽分别使用 slot 和 slot-scope 来实现, 在 Vue3.0 中将 slot 和 slot-scope 进行了合并 v-slot
- 在 Vue 3 中对自定义指令的 API 进行了更加语义化的修改,名称和组件生命周期名称相同
- v-model 变更:在自定义组件上使用 v-model 时,同一组件可以同时设置多个 v-model, 开发者可以自定义 v-model 修饰符
学到什么
- vue3 与 vue2 的核心区别
- Tree-Shaking
- 函数 setup()
- 函数 ref()
- 函数 isRef()
- 函数 toRefs()
- 函数 reactive()
- 函数 computed()、watch()
- LifeCycle Hooks(新的生命周期)
- Template refs
- vue3 的全局配置
- vue3 组件模板结构
- 实现 自定义 Hook
- 组件 teleport 任意门
- 组件 异步组件
vue3 与 vue2 的核心区别
- vue3.x 将使用 Proxy 取代 Vue2.x 版本的 Object.defineProperty
- Object.defineProperty 只能劫持对象的属性, 而 Proxy 是直接代理对象,由于 Object.defineProperty 只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是 Proxy 直接代理对象,不需要遍历操作
- Object.defineProperty 对新增属性需要手动进行 Observe
Tree-Shaking
当 Javascript 项目达到一定体积时,将代码分成模块会更易于管理。但是,当这样做时,我们最终可能会导入实际上未使用的代码。Tree Shaking 是一种通过消除最终文件中未使用的代码来优化体积的方法。
为什么:因为 Vue 实例是作为单个对象导出的,打包器无法分辨出代码中使用了对象的哪些属性。所以,我们需要单独引用
抽离了 一部分 vue2 中的公用函数,需要单独引用。以前的全局 API 现在只能通过具名导入,这一更改会对以下 API 有影响:
- Vue.nextTick
- Vue.observable(用 Vue.reactive 替换)
- Vue.version
- Vue.compile(仅限完整版本时可用)
- Vue.set(仅在 2.x 兼容版本中可用)
- Vue.delete(与上同)
setup 函数
组件提供的新属性,为了使用 vue3 CompositionAPI 新特性而启用的函数,它有自己独立的生命周期
vue3 取消了 beforeCreate 、created 两个钩子函数,统一用 setup 代替
- props 用来接收 props 数据
- context 上下文对象
- return 返回模板中需要使用的函数
setup(props, context) {
context.attrs
context.slots
context.emit
return {
}
}
ref() 函数
组件提供的新特性函数
创建一个响应式的数据对象,这个对象是响应式的,只返回一个 { value: ""} 值
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const name = ref < string > "hello";
// 在js 中获取ref 中定义的值, 需要通过value属性
console.log(name.value);
return {
name,
};
},
});
isRef() 函数
组件提供的新特性函数
isRef() 用来判断某个值是否为 ref() 创建出来的对象
import { defineComponent, isRef, ref } from "vue";
export default defineComponent({
setup(props, context) {
const name: string = "vue";
const age = ref<number>(18);
console.log(isRef(age)); // true
console.log(isRef(name)); // false
return {
age,
name,
};
},
});
toRefs() 函数
组件提供的新特性函数
toRefs() 函数可以将响应式对象,转换为普通的对象。只不过,这个对象上的每个属性节点,都是 ref() 类型的响应式数据
import { defineComponent, reactive, ref, toRefs } from "vue";
export default defineComponent({
setup(props, context) {
let state = reactive({
name: "hello",
});
const age = ref(18);
return {
...toRefs(state),
age,
};
},
});
reactive() 函数
组件提供的新特性函数
reactive() 函数接收一个普通对象,返回一个响应式的数据对象, 想要使用创建的响应式数据也很简单,创建出来之后,在 setup 中 return 出去,直接在 template 中调用即可
import { defineComponent, reactive, ref, toRefs } from "vue";
export default defineComponent({
setup(props, context) {
let state = reactive({
name: "hello",
});
return state;
},
});
computed()、watch() 函数
组件提供的新特性函数
computed() 函数 ,用来计算属性,返回的值是一个 ref 对象。
import { computed, defineComponent, ref } from "vue";
export default defineComponent({
setup(props, context) {
const age = ref<number>(18);
const computedAge = computed({
get: () => age.value + 1,
set: (value) => age.value + value,
});
// 为计算属性赋值的操作,会触发 set 函数, 触发 set 函数后,age 的值会被更新
age.value = 100;
return {
age,
computedAge,
};
},
});
watch() 函数,用来监听属性, 当数据源变化的时候才会被执行。
import { computed, defineComponent, reactive, toRefs, watch } from "vue";
interface Person {
name: string;
age: number;
}
export default defineComponent({
setup(props, context) {
const state = reactive<Person>({ name: "vue", age: 10 });
watch(
[() => state.age, () => state.name],
([newName, newAge], [oldName, oldAge]) => {
console.log(newName);
console.log(newAge);
console.log(oldName);
console.log(oldAge);
}
);
// 修改age 时会触发watch 的回调, 打印变更前后的值, 此时需要注意, 更改其中一个值, 都会执行watch的回调
state.age = 100;
state.name = "vue3";
return {
...toRefs(state),
};
},
});
LifeCycle Hooks 生命周期
组件提供的新特性函数
生命周期组件中新的写法
import { set } from "lodash";
import {
defineComponent,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onErrorCaptured,
onMounted,
onUnmounted,
onUpdated,
} from "vue";
export default defineComponent({
setup(props, context) {
onBeforeMount(() => {
console.log("beformounted!");
});
onMounted(() => {
console.log("mounted!");
});
onBeforeUpdate(() => {
console.log("beforupdated!");
});
onUpdated(() => {
console.log("updated!");
});
onBeforeUnmount(() => {
console.log("beforunmounted!");
});
onUnmounted(() => {
console.log("unmounted!");
});
onErrorCaptured(() => {
console.log("errorCaptured!");
});
return {};
},
});
模板 Template refs
组件提供的新特性函数
通过 refs 来回去真实 dom 元素,onMounted 中可以得到 ref 的 RefImpl 的对象, 通过.value 获取真实 dom
<template>
<div class="mine" ref="elmRefs">
<span>hello</span>
</div>
</template>
<script lang="ts">
import { set } from "lodash";
import { defineComponent, onMounted, ref } from "vue";
export default defineComponent({
setup(props, context) {
// 获取真实dom
const elmRefs = ref<null | HTMLElement>(null);
onMounted(() => {
console.log(elmRefs.value); // 得到一个 RefImpl 的对象, 通过 .value 访问到数据
});
return {
elmRefs,
};
},
});
</script>
vue 的全局配置
Vue3 可以在组件用通过 getCurrentInstance() 来获取全局 globalProperties 中配置的信息
const app = Vue.createApp({});
app.config.globalProperties.$http = "axios";
setup( ) {
const { ctx } = getCurrentInstance();
ctx.$http
}
vue3 组件模板结构
<template>
<div class="mine" ref="elmRefs">
<span>{{ name }}</span>
<br />
<span>{{ count }}</span>
<div>
<button @click="handleClick">测试按钮</button>
</div>
<ul>
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
getCurrentInstance,
onMounted,
PropType,
reactive,
ref,
toRefs,
} from "vue";
interface IState {
count: 0;
name: string;
list: Array<object>;
}
export default defineComponent({
name: "demo",
// 父组件传子组件参数
props: {
name: {
type: String as PropType<null | "">,
default: "vue3.x",
},
list: {
type: Array as PropType<object[]>,
default: () => [],
},
},
components: {
/// TODO 组件注册
},
emits: ["emits-name"], // 为了提示作用
setup(props, context) {
console.log(props.name);
console.log(props.list);
const state = reactive<IState>({
name: "vue 3.0 组件",
count: 0,
list: [
{
name: "vue",
id: 1,
},
{
name: "vuex",
id: 2,
},
],
});
const a = computed(() => state.name);
onMounted(() => {});
function handleClick() {
state.count++;
// 调用父组件的方法
context.emit("emits-name", state.count);
}
return {
...toRefs(state),
handleClick,
};
},
});
</script>
实现 自定义 Hook
功能性组件可以封装成 hook, 以 use 作为前缀,和普通的函数区分
import { ref, Ref, computed } from "vue";
type CountResultProps = {
count: Ref<number>;
multiple: Ref<number>;
increase: (delta?: number) => void;
decrease: (delta?: number) => void;
};
export default function useCount(initValue = 1): CountResultProps {
const count = ref(initValue);
const increase = (delta?: number): void => {
if (typeof delta !== "undefined") {
count.value += delta;
} else {
count.value += 1;
}
};
const multiple = computed(() => count.value * 2);
const decrease = (delta?: number): void => {
if (typeof delta !== "undefined") {
count.value -= delta;
} else {
count.value -= 1;
}
};
return {
count,
multiple,
increase,
decrease,
};
}
使用 hook
<template>
<p>count: {{ count }}</p>
<p>倍数: {{ multiple }}</p>
<div>
<button @click="increase()">加1</button>
<button @click="decrease()">减一</button>
</div>
</template>
<script lang="ts">
import useCount from "../hooks/useCount";
setup() {
const { count, multiple, increase, decrease } = useCount(10);
return {
count,
multiple,
increase,
decrease,
};
},
</script>
组件 teleport 任意门
目的: 即希望继续在组件内部使用 Dialog, 又希望渲染的 DOM 结构不嵌套在组件的 DOM 中
场景: 弹框
我们可以用<Teleport>包裹 Dialog, 此时就建立了一个传送门,可以将 Dialog 渲染的内容传送到任何指定的地方。使用 teleport 组件,通过 to 属性,指定该组件渲染的位置与 <div id="app"></div> 同级,也就是在 body 下,但是 Dialog 的状态 dialogVisible 又是完全由内部 Vue 组件控制.
<body>
<div id="app"></div>
<div id="dialog"></div>
</body>
// Dialog.vue
<template>
<teleport to="#dialog">
<div class="dialog">
<div class="dialog_wrapper">
<div class="dialog_header" v-if="title">
<slot name="header">
<span>{{ title }}</span>
</slot>
</div>
</div>
<div class="dialog_content">
<slot></slot>
</div>
<div class="dialog_footer">
<slot name="footer"></slot>
</div>
</div>
</teleport>
</template>
// Footer.vue 子组件
<div class="footer">
...
<Dialog v-if="dialogVisible"></Dialog>
</div>
组件 异步组件
Vue3 中 使用 defineAsyncComponent 定义异步组件,配置选项 component 替换为 loader ,Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise。
<template>
<!-- 异步组件的使用 -->
<AsyncPage />
</template>
<script>
import { defineAsyncComponent } from "vue";
export default {
components: {
// 无配置项异步组件
AsyncPage: defineAsyncComponent(() => import("./NextPage.vue")),
// 有配置项异步组件
AsyncPageWithOptions: defineAsyncComponent({
loader: () => import(".NextPage.vue"),
delay: 200,
timeout: 3000,
errorComponent: () => import("./ErrorComponent.vue"),
loadingComponent: () => import("./LoadingComponent.vue"),
}),
},
};
</script>
参考
- vue3 官网 https://v3.vuejs.org/
- 构建 vite https://github.com/vitejs/vite
- vue3 源码 https://github.com/vuejs/vue-next
- vue-cli https://cli.vuejs.org/
- vue-router https://github.com/vuejs/vue-router-next