阅读本文你可以了解到
Vue3.0 的 RFC 提出来也有一段时间了。从刚开始的 function-base-api 到现在的 composition-api。这两者都有一个共同的目标就是将 2.x 中与逻辑相关的选项都以函数形式抽离出来
这两个都能定义响应式数据,在这里其实有那么点区别,先举个例子
const x = ref<number>(0) const y = ref<string>("Hello World") // x y的类型是 Ref<number> | Ref<string> const state = reactive({ text:"Hello World" }) 复制代码
这里的 Ref 类型是包装类型的意思。以前的 Vue2.x 直接监听的是字符串,还有数字的变化。因为这些值都是作为 data 的属性值存在,监听的其实是 data 对象的属性值。但是现在单独在函数中返回的字符串,和数字等基本数字类型,不存在引用关系,是无法追踪原始变量后续的变化的。所以包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。容器的存在可以通过引用的方式传回给组件。
state 跟原来的 Vue2.x 的就很很类似,存在于 state 对象上,监听的是 state 属性的变化。
怎么去选择定义数据的方法?其实很简单,都可以,看个人习惯问题。
const state = reactive({ user:{ name:"A", age:10, where:"china" } }) // 相当于 const state = { name :"A", age:10, where:"china" } const userName = ref<string>("A") const userAge = ref<number>(10) const userWhere = ref<string>("china") // 相当于 const name = "A"; const age = 10; const where = 'china' 复制代码
⚠️ 注意在使用 Ref 的时候,在模板会自动拆箱,但是在函数里面,传递的是对象。所以要取值的时候记得要加 ref.value 才能取值
2.watch
watch 用过的同学都知道是用来观察数据变化进行对应的操作的。这次的改动是将原来的 Options 的写法换成函数的形式进行调用。
watch 的第一个参数可以是返回任意值的函数,一个包装对象,还有是包含两种数据的数组
... const state = reactive<{count:number}>({ count:1, }) watch( () => state.count, (cur,pre) => { // 观察的是 state对象里面的一个值在这里进行操作 }, ); ... ... const name = ref<string>("Hello World") watch( () => name, (cur,pre) => { // do other things }, ); ... 复制代码
⚠️ 注意的是包装对象不能用函数返回,因为是两种不同类型的观察值,所对应的操作也是不一样的。如果用函数返回,则没有监听的效果。(虽然你看到的值是改变了,但是你.value 过后仍然是初始的值)
⚠️ 注意 watch 的回调会在创建的时候就使用,类似于 2.x 设置了immediate: true
,watch 的触发总是会在 DOM 更新完的情况下。所以说如果想要在 DOM 更新前就得设置flush
选项
✏️ watch 还能够停止监听,这是 2.x 没有的
const stop = watch(()=>state.count) // watch函数返回的是一个 停止观察数据的函数。 调用一下就能停止观察了 复制代码
❤️ 特别说明一下:如果 watch 实在setup
或者是生命周期函数里面调用。watch
会在销毁组件的时候自动停止
3.setup 函数和生命周期
Vue3.x 的生命周期发生了些变化,废弃了原来的created
和beforeCreated
的生命周期,在这两者之间新增了一个组件选项setup
函数。
// setup是在初始化props后面调用的,所以会接受props为参数。context是整个组件的上下文。以前都是用this类指向Vue组件,现在换成函数就用参数的形式传递上下文。 ... setup(props:Props,context:SetupContext){ onMounted() onUpdate(); onUnMounted(); } ... 复制代码
生命周期都改了名字。并且以函数 Api 的形式,更重要的是只能在 setup 函数里面调用~!
⚠️ 注意的 props 不能进行结构赋值,也不能进行扩展运算符等破坏监听值的操作。因为这些操作都会有中间量的生成导致破坏原有存在的监听系统。要用的话就得在state
上面使用toRefs
方法。这个方法可以让对象不会破坏原来的监听。同时 Props 也是不可修改的。
在用 Composition-api 的同时,也可以用原来基于选项的 options 的做法。composition-api 是为了解决一些 2.x 存在的问题。
vue 在应对简单的业务的时候确实很游刃有余。但是在大型项目,涉及很多组件,或者是组件之间的逻辑复用的问题的时候,可能就有点棘手了。 在这之前可能都知道有 Mixins,HOC 等解决逻辑复用的手段。但是他们都或多或少有点问题。譬如:来源不清。命名空间冲突。需要额外的开销等等。
composition-api 可以将组件的逻辑封装到一个函数里面进行复用。
// 举一个封装的例子 判断是否下拉到底 // useIsEnd.ts ... const isEnd = ref<boolean>(false) // 初始化未到最底下 export function getEnd(node:Ref<HTMLElement>){ // 已经封装好的三个函数 判断是否到底的 -10 是为了有些情况下会出现小数(在移动端内嵌的时候) if ( getClientHeight(node) + (getScrollTop(node) as number) >= getScrollHeight(node) - 10 ) { isEnd.value = true; } else { isEnd.value = false; } return { isEnd } } // 这里将isEnd放在外面是为了在触发触摸事件的时候(touch),不会每次都重新创建一个新的对象导致判断失误 // demo.vue ... import { getEnd } from "useIsEnd"; export default defineComponent({ ... setup(){ const node = ref<HTMLElement>('node') // 获取一个node ... // 这是@touchmove 事件 function handleGetIsEnd(){ const { isEnd } = getEnd(node) // 判断当前是否到底 // do other things like ajax } ... return { // 记得return node, handleGetIsEnd } } ... }) ... 复制代码
✏️ 不存在来源不清晰,返回值还可以重新定义,没有命名空间的冲突。把所有的逻辑封装成一个函数没有额外的组件的开销。还有可以更好的组织代码
Vue3.x 用 TypeScript 重写之后,对的类型推导天然支持。再加上是函数的 APi 的缘故,类型推导更上一层楼。
也是因为函数的原因。对 tree shakeing 友好。每个函数都可以单独引入,并且也因为函数的 API 的原因可以压缩函数名字达到极致的最小尺寸
以前 Vue 项目假如要用 TypeScript,就得引入对应的插件(例如 vue-class-component or vue-property-decorator)。但是通过 TypeScript 重写以后,就可以直接使用 TypeScript。
用 TypeScript 也是想要 TypeScript 的类型推导,这里不多赘述 TypeScript 的概念
// 这里的interface可以让Ts提供提示 interface Props{ name:string; age:number; } interface State{ title:string; head:string; } ... // 类型推导需要用defineCompoent or createComponent export default defineComponent({ ... props:{ name:String; age:Number; } ... setup(props:Props,context:SetupContext){ ... const value = ref<string>(""); // ref 的类型是Ref<T> ... const state = reactive<State>({ title:"", head:"" }) ... const { demo } = useXXX(...) demo(); ... onMounted(async ()=>{ const res = await fetchData(...); state.XXX = res.data.XXX }) ... return { value, // 这样的话 就可以直接 使用 title / head ...toRefs(state) } } ... }) 复制代码
在使用 Composition-api 的时候我们需要知道我们用的目的是什么。当我们的逻辑很复杂的时候,可以考虑使用它来帮我们抽离逻辑复用。当我们想更好的组织代码的时候,我们可以使用它让我们的代码更有条理性。
💪 实践 1: 将 data(state)状态跟方法抽离到一个文件中
// 以前可能是会这样写 export default { ... data(){ // 当数据很大的时候 这里可能会很长 对应的每一条也需要写长长的注释 return { ... key1:value1, ... } } ... methods:{ // 方法这里 可能会这样写, 假如方法过多 也会出现很冗余的情况,注释虽然可以帮助我们很快的找到对应的代码。但是 可能会出现几百行的情况 handleXXX(){ ... do otherthings } } } 复制代码
👍 现在我们可能会这样写(一个函数包裹该函数的自身的状态,仅仅维护本函数自身状态)
// index.ts interface InitState{ key1:string; key2:number; key3:{ 'key3-1':string; 'key3-2':boolean; } } export function init(){ ... const initState = reactive<InitState>({ key1:""; key2:0; key3:{ 'key3-1':""; 'key3-2':false; } }) // 这里也可以不watch 直接 外部 async 或者是 promise的then继续执行后续操作也行 const stop = watch(async ()=>{ const res = await initData(params); const { key1,key2,key3 } = res.data; state.key1 = key1; state.key2 = key2; state.key3 = key3; }) stop(); // 假如不放在setup 里面的 或者是生命周期的 不会自动回收 ... return { // 记得加上toRefs就不会丢失响应式了 ...toRefs(initState) } } // index.vue template ... <script lang="ts"> import { defineComponent, reactive, SetupContext, toRefs } from '@vue/composition-api'; import { init } from "./index" export default defineComponent({ ... // 在某些情况下 context 可以进行解构 拿出你想要的 或者只是一个ui组件没有逻辑可以不传 /** 常见的解构的类型 * {{ root }: { root: ComponentInstance } * { root: { $router } }: { root: { $router: VueRouter } } * { root: { $store } }: { root: { store: Store } } */ setup(props:Props,context:SetupContext){ const initState = init(); return { // 这里是为了更方便的取值 ...toRefs(initState) } } ... }) <script> ... style 复制代码
✏️ 这里只有一种情况,就是仅仅是初始化的时候的 state。这里可以解构赋值,但是我个人认为 直接用一个对象包裹之后,减少不必要的命名冲突问题。这里表示的是 init 的时候的 state,后面可能会还有别的 state。
💪 实践 2: 别的请求的情况(value 值不会马上拿到的情况)
// index.ts // 这里的value不能直接拿到,需要等待的话 都需要用watch 等待改变之后再进行请求 ... function getSomeData(value) { const xxxState = reactive<XXXState>({ key1:'' }) const stop = watch( () => value, async (cur, pre) => { const res = await fetchData(cur); xxxState.key1 = res.data }, ); // 这里的假如value值会变的 不会只用一次的,后续可能会变化的 就不需要 stop了 return { ...toRefs(xxxState) } } ... 复制代码
💪 实践 3: 初始化后或者其他方法初始化的 state 后续别的方法用到,该如何修改
// index.ts /** * 这里可能有三种方法。第一将初始化的state放到全局上 * 就好像操作全局的变量一样操作全局的state,这样的做法是简单,这样做的后果就是后面需要查 * 现的问题的时候就不知道哪些函数有操作对应的state,导致难以定位问题。 */ const initState = reactive({ key1:1; }) async function init(){ const { key1 } = await fetchData(); initState.key1 = key1; } // 这个方法我想改变初始化的state function handleChangeInitState(){ const { key1 } = await fetchData(); initState.key1 = key1; } 复制代码
// index.ts /** * 第二种 将初始化的state返回后,需要修改的将state作为参数进行传入修改 * 这样做比第一种好一点。知道是哪些函数修改,就直接看对应的函数就行,但是假如后续有很多需要修改的情况,就得传很多次参数造成冗余。 */ async function init(){ const initState = reactive({ key1:1; }) const { key1 } = await fetchData(); initState.key1 = key1; return { initState, } } function handleChangeInitState(initState){ const { key1 } = await fetchData(); initState.key1 = key1; } 复制代码
// index.ts /** * 最后一种 借鉴react-hook的思想,对外暴露操作改state中某个属性的方法 * 比较简洁,没有多余的参数传递 */ async function init(){ const initState = reactive({ key1:1; }) const { key1 } = await fetchData(); initState.key1 = key1; function changeKey1(value){ initState.key1 = value; } return { changeKey1, ...toRefs(initState), } } // index.vue import { init } from "./index" ... setup(props,context){ const {changeKey1,initState} = init(); ... // 这里可以做操作state的方法 const { xxxState } = useXXX(); changeKey1(xxxState.key1) ... return { ...toRefs(initState) } } ... 复制代码
这是个人觉得 composition-api 的最佳实践。组合函数就是能更好的组织代码,将多层嵌套抽离出来分成一个个组合函数,将其组合之后,就能让代码更加有条理了。