Vue3 Composition API 杂记

1,407 阅读2分钟

- 其他

  • 在 props/emits 选项中设置了的,都不会再出现在 $attrs 中。
  • inheritAttrs: false 设置后,所有属性都不会在根节点上继承
  • PropType 用于协助定义 props 以便支持 ts 类型提示
  • getCurrentInstance 需要显示导出。是获取当前调用位置的实例。只能在 setup 或 生命周期内,其他地方无效。
    • 在 this.xxx 上设置的内容,可以用 getCurrentInstance().ctx.xxx 获取
  • composables 组合化思想类似 React 的 Hook 方式

- setup(props, ctx) 函数

  • props 对应外部传入的 props 属性,它是响应式的。所以不建议解构
  • context 包含 { root, parent, attrs, slots, emit, expose } 属性,类似 this
    • 非响应式,可以用es6结构
    • 只有 expose 出的才能在实例上访问到
  • 需要将响应式的内容 return { ... }
  • beforeCreate 前被调用,无法访问 this
  • 注意: setup 和 render 是可以同时使用的,但是要处理好数据

- 响应式系统 api / 工作集

  • reactive 接收普通对象/Ref返回响应对像。
    • 使用方式 js/模版 都一样,使用返回的 值.属性
    • 可以包含计算属性
    • 不能对返回值解构展开,不然会失去响应式。可以通过 toRefs 解决
    • shallowReactive 只对数据第一层进行响应式化。(性能优化)
    • 注意:
      • 整体替换 obj = Object.assign(obj, {xxx})
      • 内部调用的是 proxy
  • ref 接收基础类型返回一个响应式值。(也支持对像类型)
    • 内部将值指向单一属性 xxx.value 。js 操作时使用 xxx.value,模版直接使用 xxx
    • shallowRef/triggerRef 配合使用(性能优化)
    • 注意:
      • 如果把 ref 放入 reactive 中时,操作方式安正常对象方式。
      • 如果把 refObj.value 赋值另一个值A,这时改变 refObj.value 的值并不会触发A的改变。因为之间是常量赋值。
      • 对于基本类型调用的是 Object.defineProperty 而对象是 proxy
  • unref 返回参数本身
  • isRef 检查值是否为一个 ref 对象
  • readonly 只读的响应式对象
  • toRef/toRefsreactive 的某一属性或整体转为 **ref **类型
    • toRef(响应式对象,propName)
  • toRaw 获取响应式对象的原始数据,修改它不会触发刷新
    • 对 ref 的数据要 xxx.value

- 模版 Refs

  • 通过响应式 ref模版 ref 进行绑定。并且** return 出的名称**要与模版上的 ref 相同
  • 注意:获取值需要在 onMounted 时,或者通过 watchEffect 监听获取

- 响应式 computed 和 watch

  • computed 计算属性
    • 它接受()=>{}getter 函数,返回一个默认不可手动修改的 ref 对象。
    • 也可以使用一个带有 get/set 函数对象来创建可读写 ref 对象
    • js 内使用也是x.value方式
  • watch(source, callback(prev, cur, onInvalidate), { immediate, deep }) 数据源可以是一个拥有返回值的 getter 函数
    • 可以直接是 ref 传入时不用传入 ref.value
    • 只想对 reactive某一个属性监听,传入个 getter 函数 () => state/state.count 或者使用 toRef(obj, 'key')
    • 可以使用数组来同时侦听多个源
    • 和 watchEffect 的区别:
      • 懒执行副作用
      • 需要更具体说明什么状态应该触发重新运行
      • 可访问监听状态的变化的前后值
  • watchEffect( (onInvalidate) => {} [, {flush: 'pre(default)/post/sync'}] ) 立即执行的监听函数
    • options.flush前/后/同步指的是在组件 update 时它触发的时机
    • 清理副作用 onInvalidate() 函数的触发时机
      • 重新执行时前触发。(可用于清理上一次的一些结果)
      • 侦听器被停止/组件卸载时触发。
    • 类似 React 的 useEffect 函数
  • 注意
    • watchEffect/watch 默认是组件卸载时自动停止,也可以调用_返回值_手动停止。停止方式 const stop = watchEffect(); stop();

- 依赖注入

  • provide 功能类似 2.x 的
    • 需要显示导入,使用方式为 (name, value) 形式
    • 也可用于函数的提供
  • inject 接受一个可选的的默认值作为_第二个参数_
  • InjectionKey 是个 interface 用于把 provide/inject 两者关联起来,是定义用的继承自 symbol 。
  • 需要从 vue 中引入

- 渲染函数 h

  • h 现在是作为全局导入的。可以在 render 选项中返回,也可以 setup 中已经函数形式返回(主要形式)
setup() {
	return () => h('div', null, 'abc')
}
  • h(tagName, {options}, []) 函数
    • 属性更加扁平,静/动态 class/style 都合并了。其他属性也不再用单独的属性名。
  • v-model:扩展为 modelValue 和 onUpdate:modelValue 方式在模板编译过程中。支持多个绑定
    • modelValue 为使用 v-model 的默认名称。如果自定义改为 v-model:title 名称,函数调用时也改为 update:title 就好。都需要 emits 中定义
props: ['abc'],
emits: ['update:modelValue/cname'], // 注意这里需要写明
render() {
  return h(SomeComponent, {
    modelValue: this.abc,
    'onUpdate:modelValue/cname': value => this.$emit('update:modelValue/cname', value)
  })
}
  • v-on:以 on 开头使用驼峰写法,修饰符只有 .passive/capture/once 生效。其他的用等价函数去操作
  • 插槽:API 形式获取和对象形式设置,两种形式都可以
    • 使用 this.$slots.default/slotName({ ...data }) 。如果传递数据就是作用域插槽
    • 对象形式更多用于给**子组件(本身还是一个组件)**传递内容的
    • 注意Transition 组件插槽用对象形式设置
// parent 使用函数形式性能更好
h(resolveComponent('cmpChild'), null, {
  // 这里的 props 是子组件传递过来的。即下面的 { text: 'header' }
	header: (props) => slots.header(props),
  default: (props) => slots.default(props)
});

// cmpChild
h('div', null, [
	h('div', { class: 'cmp-header' }, slots.header({ text: 'header' })),
  h('div', { class: 'cmp-body' }, slots.default({ text: 'body' })),
]);
  • 使用组件:只需要对全局注册的组件使用 resolveComponent。而对于局部注册的却可以直接使用。
  • 和 is 动态组件:使用 resolveDynamicComponent(name) 去生成一个
  • 自定义指令:使用 resolveDirective 去生成指令,使用 withDirectives 去包裹 h 函数使用指令
  • 内置组件:在模版中使用时会自动导入。但在 render 函数中需要显示的自行导入

- 生命周期

  • setup 内,改为函数调用方式
    • :调用但并不用 return返回
  • ~~beforeCreate~~ -> 使用 setup()
  • ~~created~~ -> 使用 setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured
  • 新增 onRenderTracked/onRenderTriggered 调试钩子函数

- 新组件

  • Teleport 将特定的 html 传送到 Dom 的任意位置。
    • to="cssSelecter" 属性传入 css 选择器,然后进内部的 html 放到该节点下
    • disabled 用于控制 teleport 的作用
  • Suspense 异步内容展示
    • 需要返回 promise 对像,支持 async/await 语法
    • 支持两个插槽 default/fallback。加载时展示 fallback 中内容,最总展示 default 的内容

- 全局配置

  • globalProperties 具体用法 app.config.globalProperties.xxx = 123
    • 配合 getCurrentInstance() 函数在 setup 里获取实例(当前),类似之前的 this 对象 internalInstance.appContext.config.globalProperties.xxx
      • 只能在 setup 或生命周期钩子中调用
    • internalInstance.ctx 是当前实例,也就是 this

- 全局 API

  • createApp({ 组件选项对象, propsData }) 返回一个实例。原本挂在 Vue 上的全局方法/属性,现在要挂到这个返回值 app 上。类似 Vue.extend() 可用于创建组件实例
    • app.mount(string | Element)
    • app.unmount(string | Element) 卸载,对应之前的 $destroy()
  • nextTick 和之前一样
  • defineComponent 返回传递给它的对象。在创建组件时用,主要是用于类型推断。
  • defineAsyncComponent 创建异步组件,可支持 promise 对象
    • 高级用法(配置加载中组件、错误组件等)和 2.0 的相似
  • resolveComponent(name) 解析组件
  • resolveDirective(name) 解析自定义指令,没找到返回 undefined
  • withDirectives(vnode[, [directive, value, arg, modifiers]]) 指令应用于 VNode。注意是二维数组。每个指令本身又是一个数组。
  • useCssModule 允许在 setup 内访问 css 模块

- 单文件组件

  • 顶层的绑定会被暴露给模板,包括 import 导入的内容。组件也是直接导入即可使用
  • 自定义指令:必须是 vNameOfDirective 命名方式的一个对象
  • 属性转 API 方式:defineProps/defineEmits/defineExpose
  • useSlots/useAttrs:是 $slots/$attrs 的 API 形式,返回 setupContext.slots 和 setupContext.attrs
  • 注意
    • 不支持 inheritAttrs 属性只能通过 <script> 选项式 API 实现
    • 不支持 render 选项,需要配合 <script> 使用

- 版本迁移内容

  • 已经支持多根节点了
  • 深度选择器 :deep(selecter)
  • composables 组合化:mixin 建议改为函数的方式去处理。也可以将部分 util 也函数化。
  • 指令的变化
    • 生命周期更改为 created/beforeMount/mounted/beforeUpdate/updated/beforeUnmount/unmounted
    • 在组件上使用,只对组件的生效
  • **attrs将包含所有attribute包括id/class/style/attrs** 将包含所有 attribute 包括 id/class/style/listeners 的属性
    • 事件将是以 on 开头的属性
    • 所有在 props 选项中设置的属性都不会出现在其中了
  • 新增 emits 属性类似 props 来定义组件真正可触发的事件(可被 emit('eventName') 的)
    • 选项中列出的事件不会被组件的根元素继承,也将从 $attrs 中移除。其他的都在根上当作原生事件触发,除非子组件设置了 inheritAttrs: false
  • $children 移除,改用 ref 来获取
  • on/on/once/$off 已弃用,可用 mitt 库代替
  • filters 移除,用方法调用或计算属性来替换
  • 全局 app.component/directive/mixin/use 都相同
    • Vue.config.ignoredElements -> app.config.compilerOptions.isCustomElement
    • Vue.prototype -> app.config.globalProperties
  • 渲染函数 API 看上面 渲染函数
  • 过度的 class 名改变
    • 改为 v-enter-from/v-enter-active/v-enter-tov-leave-from/v-leave/active/v-leav-to
  • 组件自定义 v-model:PropName 的实现,去除了 ~~.sync~~ 修饰。具体看上面 渲染函数
  • 组件外部生命周期监听:@hook:updated -> @vnode-updated

- 自定义组件

  • 注册和使用方式。由于在 setup 中不能使用 this 所以和之前有差
// InjectKey 定义
export const CTOAST_KEY = Symbol('c-toast');

export function useCToast(options = {}) {
  return inject(CTOAST_KEY)(options);
};

// install
CToast.install = (app) => {
  app.component(_CToast.name, _CToast);
  
  // 全局挂载。setup 内使用 const { proxy } = getCurrentInstance() 获取,通过 proxy.$ctoast() 使用
  app.config.globalProperties.$ctoast = CToast;
  
  // 用下面的 useCToast() 获取或手动 inject() 使用
  app.provide(CTOAST_KEY, CToast);
  
  // 或者 import 导入 CToast 函数直接运行
};
  • 自定义挂载点可使用 Teleport 或函数处理
let teleport = document.body;
if (options.teleport !== 'body') {
  if (typeof options.teleport === 'string') {
    teleport = document.querySelector(options.teleport);
  } else {
    teleport = options.teleport;
  }
}
  • 创建组件的方式:主要是挂载方式的差异
const root = document.createElement('div');

// 方式一:实例体为 app
const app = createApp(CusComponent, options); // { component, propsData }
app.mount(root);
app.unmount(); // 卸载

// 方式二:实例体为 vm
const vm = createVNode(CusComponent, options); // { component, props }
render(vm, root); // { vnode, container }

document.body.appendChild(root);
  • 由于 setup 需要用于返回 h 函数,内部暴露的信息需要使用 expose 或者如下
// 方式一(推荐)
setup() {
	expose({
  	open() {},
    text: 'abc'
  });
  
  return () => h('div');
}

// 方式二
setup() {
  // 这里的设置的就是 render 配置项
  // 没有用 render 配置,是因为不用再处理响应式数据
  getCurrentInstance().render = () => h('div');
  
	return {
  	open() {},
    text: 'abc'
  }
}
  • vue JSX 插件 链接
    • 按照 @vue/babel-plugin-jsx 插件,配置 { plugins: ["@vue/babel-plugin-jsx"] }

- vue-router 使用

  • 相关 API 只能在 setup 中使用
  • 在组合式 API 内使用函数调用的形式。其他的使用 this.$route/$router
  • 组件内守卫和组合式 API 一样的使用方式
  • useLink 函数配合 RouterLink 属性,可用函数形式获取参数。类似 v-slot API。
    • 通过作用域插槽暴露底层能力
  • scrollBehavior:返回对象改为 { left, top }
// router.js 内
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'

const router = createRouter({
	history: createWebHashHistory("basePath"), // 或 createWebHistory()
  routes: [{
    // 404 页面匹配
    path: '/:patchMatch(.*)*',
    redirect: '/not-found'
  }]
});

// 全局守卫用法和之前相同
router.beforeEach((to, from, next) => {});

// main.js 组册使用
app.use(router);

// login.vue 内
// onBeforeRouteLeave/onBeforeRouteUpdate 组件内守卫
import { useRoute, useRouter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

setup() {
	const route = useRoute(); // 对应 this.$route 提供当前路由信息
  const router = useRouter(); // 对应 this.$router 提供 api
}

// keep-alive 使用方式做了更改
<router-view v-slot="{ Component }">
  <keep-alive>
    <component :is="Component" />
  </keep-alive>
</router-view>

- vuex 使用

  • useStore 只能在 setup 中使用
  • 使用组合函数,好像就不支持 mapXXX 函数了
// store.js
import { createStore } from 'vuex'

const store = createStore({
	state: {/.../},
  getters: {},
  mutations: {},
  actions: {}
})

// main.js 内
app.use(store)

// home.vue
import { useStore } from 'vuex'

setup() {
  const store = useStore() // this.$store
  
  // 具体使用规则和之前一样
	const count = computed(() => store.state.count) // state
  const double = computed(() => store.getters.double) // getters
  
  const increment = () => store.commit('increment') // mutation
  const asyncIncrement = () => store.dispatch('asyncIncrement') // action
  
  // 命名空间
  this.$store.state.a.xxx;
	this.$store.getters['a/isAdmin'];
	this.$store.commit/dispatch('a/increment');
}