关于 Vue3
6月前10min read

Composition API
以 setup
函数作为入口,提供了一系列的响应式的 API、生命周期,使得不依赖 options API 就可以开发一个组件。可以将同一个功能逻辑组合在一起,不再分散,使代码有更好的可维护和复用性
setup
setup()
在创建组件之前执行,Composition API 入口,等同于beforeCreate
和created
- 组件实例尚未被创建,因此无法访问组件中声明的任何属性(
data
、computed
、methods
) - 参数是
props
和context
props
是响应式的,不能使用解构,可以使用toRefs
context
是一个包含attrs
、slots
、emit
的对象,不是响应式的,可以解构。- 返回一个对象或者一个渲染函数,对象可以在模板中使用。
<script>import { ref, reactive, toRefs, toRef } from "vue";export default { props: { title: String, }, setup(props, { attrs, slots, emit }) { // const { title } = toRefs(props) // title 是可选的 toRef可以自动创建一个 const title = toRef(props, "title"); // 返回一个对象 return { title }; // 返回一个渲染函数 // return () => h("div", [title?.value]) },};</script>
生命周期钩子
beforeMount
>onBeforeMount
mounted
>onMounted
beforeUpdate
>onBeforeUpdate
updated
>onUpdated
beforeUnmount
>onBeforeUnmount
unmounted
>onUnmounted
errorCaptured
>onErrorCaptured
renderTracked
>onRenderTracked
renderTriggered
>onRenderTriggered
从 options 到 Composition
思考一个最简单的场景:
- 根据组件的
props
, 获取列表 - 当组件的
props
更新,重新获取列表 - 通过计算属性获取列表中状态正常的项
import { featchList } from "@/api/list";export default { name: "list", props: { houseType: { type: Number, default: 1, }, }, data() { return { status: 0, list: [], }; }, computed: { filterList() { return this.list.filter((item) => (item.status = this.status)); }, }, watch: { houseType() { this.getList(); }, }, mounted() { this.getList(); }, methods: { async getList() { const params = { houseType: this.houseType, }; const list = await featchList(params); this.list = list; }, },};
import { defineComponent, toRefs } from "vue";import useGetList from "@/assets/useGetList.js";import useFilterList from "@/assets/useFilterList.js";export default defineComponent({ props: { houseType: { type: Number, default: 0, }, }, setup(props) { // setup 使用 Composition API 位置 // toRefs 将响应式对象转化为普通对象, // 其中每个属性都是指向原始对象对应属性的 ref,因此可以使用解构,不会丢失响应性。 const { houseType } = toRefs(props); const { list, getList } = useGetList(houseType); const { status, filterList } = useFilterList(list); return { list: filterList, getList, status, }; },});
// useGetList.jsimport { featchList } from "@/api/list";import { onMounted, ref, watch } from "vue";export default function useGetList(houseType) { // ref 函数创建一个响应式变量 // ref 将值封装在一个对象中,通过.value 访问或者修改响应式变量的值 const list = ref([]); const getList = async () => { const params = { houseType: houseType.value, }; // 通过 .value修改响应式变量的值 list.value = await featchList(params); }; // 生命周期钩子,只能在 setup 中使用,因为需要依赖调用 setup 的组件实例 onMounted(getList); // watch 等同于 this.$watch 或者 options watch // 监听一个响应式引用或者一个 getter 函数 // 如果需要监听多个 watch([one, tow], ([new1,new2], [old1,old2]) => { /* ... */ }) watch(houseType, (newValue, oldValue) => { getList(); }); return { list, getList, };}
// useFilterList.jsimport { computed, ref } from "vue";export default function useFilterList(list) { const status = ref(0); // computed 等同于 options 的 computed // 接受一个 getter函数 返回一个只读的响应式引用 // 也可以接受一个包含 get 和 set 函数的对象来创建可写的响应式引用 const filterList = computed(() => { return list.value.filter((item) => item.status === status); }); return { status, filterList, };}
Teleport
Teleport
,Vue 3 内置组件,顾名思义,它的出现主要解决了可以将某个组件或者组件中的一部分挂载到指定的 DOM 节点上,例如全局弹框。
<!-- to 必须是有效的选择器 --><teleport to="#modals"> <div class="model">弹框</div></teleport>
Fragments
在 Vue 3 中,组件支持了可以多个根节点。(终于可以不用套 <div>
了!)
<template> <div></div> <div></div></template>
Suspense
Suspense
Vue 3 内置组件,主要是配合异步组件使用(要返回一个 Promise
),如果在加载组件中发生了错误可以在 errorCaptured
中捕获错误,进行相应处理。
<Suspense> <!-- 默认 --> <template #default> <!-- 异步组件 --> <async-show></async-show> </template> <!-- 加载中要显示的 --> <template #fallback> <h1>加载中...</h1> </template></Suspense>
JSX 或者 TSX 使用
Vue CLI 中 @vue/babel-plugin-jsx
npm install @vue/babel-plugin-jsx -D
// .babelrc{ "plugins": ["@vue/babel-plugin-jsx"]}
vite 中 @vitejs/plugin-vue-jsx
// vite.config.jsimport vueJsx from '@vitejs/plugin-vue-jsx'export default { plugins: [ vueJsx({ // options are passed on to @vue/babel-plugin-jsx }) ]}
// TestJsx.tsximport { defineComponent, reactive } from "vue";export default defineComponent({ setup(){ const state = reactive({ name:'chang' }) return ()=> { return (<p>{state.name}</p>) } }})
全局配置的修改
Vue3 提供了一个 createApp
函数用来创建应用实例。因为:
- 测试期间,全局配置污染其它测试用例
- 更改配置会同时影响到多个根实例
import { createApp } from 'vue'import App from './App.vue'import router from './router'import './index.css'// 全局 API 的修改// createApp 返回一个应用实例const app = createApp(App)// use全局 API 将不在使用// 必须在实例上显示的使用插件app.use(router)// Vue.config.productionTip 移除 // Vue.config.ignoredElements > config.isCustomElement// Vue.component > app.component// Vue.directive > app.directive// Vue.mixin > app.mixinapp.config.isCustomElement = tag => tag.startsWith('wx-open')app.config.globalProperties.$utils = {}debugger;app.mount('#app')
Vue3 优化了什么?
源码优化:使用了 monorepo 和 TypeScript 管理和开发源码,提升了自身的代码可维护性。 性能优化:源码体积的优化,移除了不用的 API ,引入了tree-shaking,减少打包体积;数据劫持优化 Object.defineProperty 到 Proxy 编译优化:编译阶段对静态模板分析,提升 vnode 更新性能 语法优化: Composition API
源码调试
克隆源码之后使用 vite
或者 vue-cli
,dev
模式下 结合 debugger 或者 源码目录下yarn build -s
开启 sourcemap 调试, html 文件中引入生成文件调试
深入 Vue3
createApp
import { createApp } from 'vue'import App from './App.vue'import router from './router'import './index.css'debugger;const app = createApp(App)app.use(router)app.config.isCustomElement = tag => tag.startsWith('wx-open')app.config.globalProperties.$utils = {}app.mount('#app')
createApp,这个函数主要做了什么?
- 创建了 APP 对象
- 重写了 mount 方法
// vue-next-master/packages/runtime-dom/src/index.tsconst createApp = (...args)=>{ // 如果你仅使用了reactivity响应式, // 没有显示的调用 createApp 那么渲染器代码将不会被打包 const app = ensureRenderer().createApp(...args) app.mount = (containerOrSelector)=> {} return app}
// vue-next-master/packages/runtime-dom/src/index.ts// 创建了一个渲染器对象const ensureRenderer = () => { //...中间省略 const render = (vnode, container, isSVG)=>{ // 组件渲染的逻辑 } function createAppAPI(render) { return function createApp(rootComponent, rootProps = null){ // 创建 app 对象 const app = { _uid: uid++, _component: rootComponent, _props: rootProps, use(){}, mixin(){}, component(){}, ... mount(){} } } } return { render, createApp: createAppAPI(render) }}
// vue-next-master/packages/runtime-core/src/apiCreateApp.ts// 标准的挂载方法const mount = (rootContainer)=>{ // 创建 vnode const vnode = createVNode(rootComponent, rootProps) // 渲染 vnode render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy}// 重写的 mount 方法const mount = (containerOrSelector)=>{ // 对于传入字符串的,将其转化成 dom 对象 const container = normalizeContainer(containerOrSelector) // 挂载前清空 html container.innerHTML = '' // 执行组件的挂载 // 参数依次是容器,isHydate, 是否是 svg return mount(container, false, container instanceof SVGElement)}
组件是如何渲染的?
组件渲染的流程:
// app.vue<template> <div id="app" class="app-class"> <p>hello vue</p> </div></template>
假设我们的组件模板如上,在 .mount
前打上 debugger
,开始调试吧。
那么组件渲染的主流程如下:
- 为传入的 App 组件创建 vnode
createVNode
- 开始渲染 vnode
render
- 进入
patch
,由于 vnode 类型是组件,所以进入处理组件的方法processComponent
- 进入
mountComponent
开始挂载组件,创建组件实例,设置组件实例,运行带副作用的渲染函数 - 进入
setupRenderEffect
,渲染生成子树 vnode - 再次进入
patch
挂载子树 vnode ,由于类型是 DOM 元素 - 进入
processElement
处理 DOM 元素,进入mountElement
,创建元素节点,子元素是数组,进入mountChildren
,遍历子节点pach
挂载到父节点上。 - 进入
hostInsert
将创建的节点插入到#app
上。
源码中的主流程:
// vue-next-master/packages/runtime-core/src/renderer.ts// 渲染 vnodeconst render = (vnode, container) => { if (vnode == null) { // 销毁组件 } else { // 创建更新组件 patch(container._vnode || null, vnode, container) }}
// vue-next-master/packages/runtime-core/src/renderer.ts// 主要参数 旧节点,新节点,DOM 容器const patch = (n1,n2,container)=> { const { type, ref, shapeFlag } = n2 switch (type) { // 根据节点类型分别处理不同 vnode // 例如 processText 处理文本 // processComponent 处理组件 }}
// 处理 vnode 类型是组件const processComponent = (n1,n2,container)=> { if(n1 == null){ // 挂载组件 mountComponent() }else{ // 更新组件 updateComponent() }}// 挂载组件const mountComponent = ()=>{ // 创建组件实例 const instance = createComponentInstance() // 设置组件实例 setupComponent(instance) // 设置带副作用的渲染函数 setupRenderEffect( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized )}// 带副作用的渲染函数,比如当数据修改的时候会执行渲染函数const setupRenderEffect= ()=> { instance.update = effect(function componentEffect(){ if (!instance.isMounted){ // 渲染组件 const subTree = (instance.subTree = renderComponentRoot(instance)) // 挂载子树 vnode,继续 patch patch(null,subTree,container,anchor,instance,parentSuspense,isSVG) }else{ // 更新组件 } })}
// 如果 vnode 类型是元素的情况const processElement = () => { if(n1 == null){ mountElement() }else{ // 更新元素 }}// 挂载元素const mountElement = ()=>{ el = vnode.el = hostCreateElement() // 如果子节点是数组的情况 挂载子节点 mountChildren()}const mountChildren = () => { // for 循环子项再进行 patch挂载 for(){ patch() }}
组件更新
组件更新的主要流程:
- 更新组件的 vnode 节点
- 渲染新的子树 vnode
- 新旧子树 vnode patch
- 如果是组件的话,执行更新组件的方法。
- 判断子组件是否需要更新,如果需要则执行子组件的副作用渲染函数,不需要更新的话只复制属性
- 如果是普通元素的话,则执行更新元素的方法。
- 更新元素的属性和更新元素的子节点
其中一个元素的子节点有三种情况,纯文本、vnode 数组、空。
如果旧是纯文本:
- 新(纯文本)则替换文本
- 新(空)则删除旧
- 新(vnode 数组)清空旧,添加新的子节点
如果旧是空:
- 新(纯文本)添加新文本
- 新(空)无
- 新(vnode 数组)添加新
如果旧是 vnode 数组:
- 新(纯文本)删除旧,添加新
- 新(空)删除旧
- 新(vnode 数组)diff 新旧子节点(核心 diff 算法)
// 设置副作用渲染函数const setupRenderEffect= ()=> { instance.update = effect(function componentEffect(){ if (!instance.isMounted){ // 渲染组件 }else{ // 更新组件 let { next, bu, u, parent, vnode } = instance // next:新的组件 vnode if (next) { next.el = vnode.el // 更新组件 vnode 信息 updateComponentPreRender(instance, next, optimized) } else { next = vnode } // 渲染新的子树 vnode const nextTree = renderComponentRoot(instance) // 旧的子树 vnode const prevTree = instance.subTree // 更新子树 vnode instance.subTree = nextTree // 组件更新 进行新旧子树patch patch( prevTree, nextTree, hostParentNode(prevTree.el!)!, getNextHostNode(prevTree),instance,parentSuspense,isSVG ) } })}// 更新组件的方法const updateComponent = (n1,n2,optimized) => { // 会否需要更新子组件 if (shouldUpdateComponent(n1, n2, optimized)){ instance.next = n2 // 避免子组件重复更新 invalidateJob(instance.update) // 子组件副作用渲染函数 instance.update() }else{ // 不需要更新,复制属性 n2.component = n1.component n2.el = n1.el instance.vnode = n2 }}