阅读 303
深入浅出Vue3 Composition API

深入浅出Vue3 Composition API

在这里插入图片描述

vue3改动的地方

  • 使用Typescript
  • 放弃class采用function-based API(vue2是new Vue,vue3是createApp)
  • option API => Composition API
  • 重构complier
  • 重构virtual DOM
  • 新的响应式机制

Vue2中的跨组件重用代码方案

  1. Mixin - 混入

    代码混入其实就是设计模式中的混合模式,缺点也非常明显。可以理解为多重继承,简单的说就是一个人如何有两个父亲

    缺点是:无法避免属性名冲突 - 比如长鼻子、眼睛、嘴巴随谁;继承关系不清晰;不清楚这些mixin是什么以及如何交互

  2. Mixin Factory - 混入工厂

    特点:通过function返回mixin的定制版本,引用时通过namespace重新命名。代码重用方便;继承关系清洗;命名空间需要强大的约定和规则来做正确的事情;我们仍然需要查看每个mixin内部,并查看它暴露了哪些属性。

  3. ScopeSlots - 作用域插槽

    可读性不高

    配置复杂,需要在模版中进行配置

    性能低,每个插槽相当于一个实例

mixins混入

为什么要选择CompositionAPI

vue2的局限性

  • 组件逻辑膨胀导致的可读性变差
  • 无法跨组件重用代码
  • vue2对TS的支持有限

CompositionAPI解决了什么问题

面对vue2的局限性最佳的解决方法是将逻辑聚合就可以很好的代码可读性。

CompositionAPI是一个完全可选的语法与原来的OptionAPI并没有冲突之处。它可以让我们将相同功能的代码组织在一起,而不需要散落到optionsAPI的各个角落。

CompositionAPI优点

  • composition API是根据逻辑相关性组织代码的,提高可读性和维护性
  • 代码量少,更好的重用逻辑代码
  • 没有引入新的语法,只是单纯函数
  • 异常灵活
  • 工具语法提示友好,因为是单纯函数所以很容易实现语法提示、自动补偿
  • 更好的Typescript支持
  • 在复杂功能组件中可以实现根据特性组织代码,代码内聚性强
  • 组件间代码复用

了解CompositionAPI

参考掘金上一篇动画看下:

回顾Option Api

在传统的OptionsAPI中我们需要将逻辑分散到以下六个部分:

  • components
  • props
  • data
  • computed
  • methods
  • lifecycle methods

在这里插入图片描述

随着业务复杂度越来越高,代码量会不断的加大;由于相关业务的代码需要遵循option的配置写到特定的区域,导致后续维护非常的复杂,代码可复用性也不高

在这里插入图片描述

CompositionAPI

可以更加优雅的组织代码,函数。让相关功能的代码更加有序的组织在一起.

在这里插入图片描述

在这里插入图片描述

总结

Options APIComposition-API
不利于复用方便代码复用,关注点分离
潜在命名冲突,数据源来源不清晰数据来源清晰
上下文丢失提供更好的上下文
有限类型支持更好的TypeScript支持
按API类型支持按功能/逻辑组织,方便代码复用
响应式数据必须在组件的data中定义可独立vue组件使用

简单看一段vue3代码

// index.vue
<template>
  <h1>Vue2的data选项已经被ref、reactive这样的API给替换了</h1>
  <div>num:{{num}}</div>
  <div>refNum:{{refNum}}</div>
  <div>state对象:名字:{{state.name}},年龄:{{state.age}}</div>
  <!-- 这里不再需要state包裹了,属性已经被展开return出来了
  <div>state对象:名字:{{name}},年龄:{{age}}</div> -->
  <h1>computed写法有变化了</h1>
  <div>{{newCount}}</div>
  <div>{{list}}</div>
  <h1>watch与watchEffect监听</h1>
  <div>newCountVal:{{newCountVal}}</div>
  <div>person对象:名字:{{person.name}},年龄:{{person.age}}</div>
</template>

// script
import { defineComponent, reactive, ref, toRefs, computed, watch, watchEffect } from "vue"
import { useStore } from 'vuex'
export default defineComponent({
  setup() {
    const num = 1  //不具备响应式
    const refNum = ref(2)
    const state = reactive({
      name: '小黄',
      age: 18
    })
    console.log(state, 'state---'); // Proxy对象
    console.log(state.name) // 小黄
    console.log(state.age) // 18
    // 对数据更改
    setTimeout(() => {
      state.age = 20
      console.log(state.age) // => 20
    }, 1000)

    //  ref声明的数据可以直接使用.value这种形式更新数据,视图不需要.value,其实是vue内部做了操作
    const count = ref(1)
    console.log(count, 'count--');
    console.log(count.value) // 1
    count.value++
    console.log(count.value) // 2

    const str = ref('小黄')
    console.log(str.value) //小黄


    ////// computed
    const cnt = ref(3)
    console.log(cnt.value) // 3
    const newCount = computed(() => cnt.value + 1)
    console.log(newCount.value) // 4
    // 计算属性中获取Vuex的数据的话,可以使用Vuex提供的 useStore 模块获取到store的实例
    const store = useStore()
    const list = computed(() => store.state.list)

    //// watch
    const newCountVal = ref(10)
    setTimeout(() => {
      newCountVal.value = 20
    }, 2000)
    watch(newCountVal, (newValue, oldValue) => {
      console.log(oldValue, newValue) // watch监听到count变化
    })

    const person = reactive({
      name: '前端发现',
      age: 18
    })
    setTimeout(() => {
      person.name = '我是reactive定义的name属性更改后的数据';
      person.age = 22
    }, 2000)
    // 单个属性监听变化
    watch(() => person.name, (newValue, oldValue) => {
      console.log(oldValue)
      console.log(newValue)
    })
    // 多个属性监听变化
    // watch([count, () => person.name, () => person.age], ([newCount, newName, newAge], [oldCount, oldName, oldAge
    // ]) => {
    //   console.log(oldCount, oldName, oldAge)
    //   console.log(newCount, newName, newAge)
    // })

    //// watchEffect
    watchEffect(() => console.log(newCountVal.value, '我是watchEffect执行结果')) 
    // 监听多个变化
    // watchEffect(() => {
    //   console.log(newCountVal.value)
    //   console.log(person.name)
    // })
    setTimeout(() => {
      count.value = 20 // 立即执行watchEffect方法
    }, 1000)

    return {
      num,
      refNum,
      state,
      // ...toRefs(state),
      newCount,
      list,
      newCountVal,
      person
    }
  }
})
复制代码

CompositionAPI

setup

是什么

一个组件选项,在创建组件之前执行,并作为Composition API的入口。setup中是没有this上下文的。

可以简单理解为组件的一个配置项,值是一个函数。组件中所使用到的数据,方法,计算属性等都要配置在setup里。

参数

使用setup时,它接受两个参数:

  • props:组件传入的属性,是一个响应式对象,不能使用ES6解构,和Vue2.x中的props一致
  • context:上下文对象
setup (props,context) {
	console.log(props, 'props');
  console.log(context, 'context');
  // context.attrs; //Attributes
  // context.slots; //slots
  // context.emit; //tirgger event
  // context.listeners; // events
  // context.root; // root component instance
  // context.parent; // parent component isntance
  // context.refs; // all refs
	const {attrs,slots,emit} = context // 分别对应Vue2.x中的$attr属性、slot插槽和$emit发射事件。并且这几个属性都是自动同步最新的值,所以我们每次使用拿到的都是最新值
}
复制代码

在这里插入图片描述

context属性说明

  • attrs:组件外部传递过来的,但是没有在props配置中声明的属性,内部组件实例上对应项的代理
  • slots:收到的插槽内容。内部组件实例上对应项的代理,默认名是default
  • emit:分发自定义事件的函数
  • root:根组件的实例

执行时机

在beforeCreate之前执行

export default defineComponent({
  beforeCreate() {
    console.log("----beforeCreate----");
  },
  created() {
    console.log("----created----");
  },
  setup() {
    console.log("----setup----");
  },
});
// 输出结果
----setup----
----beforeCreate----
----created----
复制代码

注意

由于setup()是在beforeCreate,created之前执行,因此:

  • 不能在setup()函数中使用this,因为此时组件并没有完全实例化
  • 不能在setup()函数中使用beforeCreatecreated两个组合生命周期

生命周期钩子

可以使用直接导入的 onX 函数注册生命周期钩子,这些函数接受一个回调函数,当钩子被组件调用时将会被执行:

import { onMounted, onUpdated, onUnmounted } from 'vue'

setup() {
  onMounted(() => {
  	console.log('mounted!')
  })
  onUpdated(() => {
   console.log('updated!')
  })
  onUnmounted(() => {
   console.log('unmounted!')
  })
}
复制代码

这些生命周期钩子注册函数只能在setup 期间同步使用,因为它们依赖于内部全局状态来定位当前活动实例 (此时正在调用其setup()的组件实例)。在没有当前活动实例的情况下调用它们将导致错误。

组件实例上下文也是在生命周期钩子的同步执行期间设置的,因此在生命周期钩子内同步创建的侦听器和计算属性也会在组件卸载时自动删除。

Vue2生命周期选项和Vue3之间的映射

选项式 APIHook inside setup
beforeCreateNot needed*
createdNot needed*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

因为setup是围绕beforeCreatecreated生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup函数中编写。

Provide / Inject

提供依赖注入,实现祖孙组件间通信,类似于vue2中provideinject, vue3提供了对应的provideinject API,两者都只能在当前活动组件实例的 setup() 中调用。父组件有一个provide选项来提供数据,子组件有一个inject选项来开始使用这些数据。下图来自官网:

在这里插入图片描述

provide用法

provide接受两个参数,第一个参数是provide唯一名称,最好用Symbol,避免重复。第二个参数是需要暴露的数据。

provide(ThemeSymbol, 'dark')
复制代码

inject 用法

inject接收两个参数,第一个参数是provide名称,第二个参数是默认数据。如果provider没有暴露自己的数据,那么使用inject默认数据

inject(ThemeSymbol, 'light' /* optional default value */)
复制代码

例子

// 提供者:
const ThemeSymbol = Symbol()
setup() {
	// ref注入响应式对象
	const themeRef = ref('dark')
	provide(ThemeSymbol, themeRef)
}

// 使用者
setup() {
	const theme = inject(ThemeSymbol, ref('light'))
}
复制代码

getCurrentInstance

获取当前执行setup函数的组件实例。 需要注意的是,getCurrentInstance只能在setup中执行或者在生命周期钩子中执行。

import {getCurrentInstance} from 'vue';

setup(props, ctx){
  onMounted(()=>{
   const vm =  getCurrentInstance();
   console.log(vm);
  });
}

复制代码

在这里插入图片描述

Composition API带来的变化

响应式基础API

reactive

reactive函数内部机遇ES6Proxy实现

reactive接收一个普通的对象,返回出一个响应式对象。 在Vue2.x的版本中,我们只需要在data中定义一个数据就能将它变为响应式数据,在Vue3.0中,需要用reactive函数或者ref来创建响应式数据。reactive的响应式数据是深层次的。

例子
<template>
  <div>person对象:名字:{{person.name}},年龄:{{person.age}}</div>
</template>

<script>
import { defineComponent, reactive, watch } from "vue"
export default defineComponent({
  setup() {
    const person = reactive({
      name: '前端发现',
      age: 18
    })
    setTimeout(() => {
      person.name = '我是reactive定义的name属性更改后的数据'
    }, 2000)

    watch(() => person.name, (newValue, oldValue) => {
      console.log(oldValue)
      console.log(newValue)
    })
    return {
      person
    }
  }
})
</script>
// 输出结果
before person对象:名字:前端发现,年龄:18
after person对象:名字:我是reactive定义的name属性更改后的数据,年龄:22
复制代码
注意
  • 所有setup和组合函数中不能返回reactive的解构,否则会丢失响应式,可以使用toRefsAPI解决

  • 如果将refreactive结合使用,可以通过reactvie方法重新定义ref对象,会自动展开ref对象的原始值,类似与自动解包无需再通过.value方式访问其值。当然,这并不会解构原始 ref 对象

    const foo = ref('');
    const r = reactive({foo});
    r.foo === foo.value;
    // 但是不能通过字面量的形式将一个ref添加到一个响应式对象中
    const foo = ref('');
    const r = reactive({});
    r.foo = foo; // 报错
    复制代码

readonly

获取一个对象 (响应式或纯对象) 或ref并返回原始代理的只读代理。只读代理是深层的:访问的任何嵌套property也是只读的

const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 适用于响应性追踪
  console.log(copy.count)
})

// 变更original 会触发侦听器依赖副本
original.count++

// 变更副本将失败并导致警告
copy.count++ // 警告!
复制代码

toRaw

返回reactive或readonly代理的原始对象。这是一个转义口,可用于临时读取而不会引起代理访问/跟踪开销,也可用于写入而不会触发更改。不建议保留对原始对象的持久引用。请谨慎使用。

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true
复制代码

Refs

ref、toRef和toRefs

ref
ref是什么呢
  • 可以生成值类型(即基本数据类型) 的响应式数据
  • 可以用于模板reactive
  • 通过 .value 来修改值(注意一定要记得加上 .value
  • 不仅可以用于响应式,还可以用于模板的 DOM 元素
  • ref背后也是通过reactive实现的

总结下就是ref用来定义一个响应式数据,返回一个包含响应式数据的对象。一般用来定义一个基本数据类型,但是也可以定义引用类型。

ref如果传入基本数据类型依然是基于Object.defineProperty()getset完成的。如果传入的是引用类型,内部是调用了reactive函数,是基于Proxy实现对象的代理。

看个例子
// template
<template>
  <!-- 模版不需要.value-->
  <p>{{ msg }}</p>
  <p>{{ person.name }}</p>
  <button @click="handleClick">修改</button>
</template>

<script>
import { ref } from 'vue'
export default {
  name: 'App',
  setup() {
    // 基本数据类型
    const msg = ref('一条消息')
    // 引用类型
    const person = ref({ name: '张三', age: 20 })
    // 修改数据时使用.value操作
    function handleClick() {
      msg.value = '新的消息'
      person.value.name = '李四'
    }
    return {
      msg,
      person,
      handleClick
    }
  }
}
</script>
复制代码
ref作用
  • 实现响应式
  • 渲染模板中的DOM元素
<template>
   <p ref="elemRef">今天是周一</p>
</template>

<script>
import { ref, onMounted } from 'vue'
export default {
    name: 'RefTemplate',
    setup(){
        const elemRef = ref(null)
        onMounted(() => {
          console.log('ref template', elemRef.value.innerHTML, elemRef.value)
        })
        return{
            elemRef
        }
    }
}
</script>
// 输出结果
ref template 今天是周一 <p>今天是周一</p>
复制代码

在这里插入图片描述

为什么需要用ref
  • 值类型(即基本数据类型)无处不在,如果不用ref而直接返回值类型,会丢失响应式
  • 比如在**setup** 、 computed合成函数等各种场景中,都有可能返回值类型
  • Vue 如果不定义ref ,用户将自己制造 ref ,这样反而会更加混乱
为什么ref需要.value属性

我们知道ref 需要通过.value来修改值。看起来好像是挺麻烦的,总是频繁的.value特别琐碎且麻烦。那为什么一定要.value ?我们来看看原因

  • ref是一个对象,这个对象不丢失响应式,且这个对象用value来存储值
  • 因此,通过.value属性的getset来实现响应式
  • 只有当用于模板reactive时,不需要.value来实现响应式,而其他情况则都需要
总结
  • JS :需要通过.value操作对象
  • 模板: 自动拆箱
toRef
toRef是什么?

可以用来为一个reactive对象(源响应式对象上)的属性(property)创建一个ref。这个ref可以被传递并且能够保持响应性

  • toRef针对的是某一个响应式对象reactive封装的属性prop
  • toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
语法

toRef(Object, prop) 的形式来传对象名具体的属性名,达到某个属性数据响应式的效果

对于一个普通对象来说,如果这个普通对象要实现响应式,就需要用到reactive 。用了reactive之后,它就在响应式对象里面。那么在一个响应式对象里面,如果其中有一个属性要拿出来单独做响应式的话,就要用到toRef

看个例子
<template>
  <h1>toRef</h1>
  <p>toRef demo - {{ newAgeRef }} - {{ newState.name }} {{ newState.age }}</p>
</template>

<script>
import { ref, reactive, onMounted, toRef, toRefs } from "vue";
export default {
  name: "Ref",
  setup(props, context) {
    //// toRef
    const newState = reactive({
      age: 18,
      name: "monday",
    });

    // toRef 如果用于普通对象(非响应式对象),结果不具备响应式
    // const newState = {
    //   age: 18,
    //   name: 'monday'
    // }

    //实现某一个属性的数据响应式
    const newAgeRef = toRef(newState, "age");
    setTimeout(() => {
      newState.age = 20;
    }, 1500);

    setTimeout(() => {
      newAgeRef.value = 25; // .value 修改值
    }, 3000);

    //// toRefs
    const toRefsState = reactive({
      age: 18,
      name: "monday",
    });

    return {
      newAgeRef,
      newState,
    };
  },
};
</script>

// 输出结果
toRef demo - 18 - monday 18
toRef demo - 20 - monday 20
toRef demo - 25 - monday 25
复制代码
toRefs
toRefs是什么?

将响应式对象转换为普通对象,其中结果对象的每个property都是指向原始对象相应 property 的ref,不会丢失响应性

  • toRef不一样的是, toRefs是针对整个对象的所有属性,目标在于将响应式对象( reactive 封装)转换为普通对象
  • 普通对象里的每一个属性都对应一个ref
const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)
/*
Type of stateAsRefs:

{
  foo: Ref<number>,
  bar: Ref<number>
}
*/

// ref 和 原始property “链接”
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3
复制代码

当从合成函数返回响应式对象时,toRefs非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2
  })
  // 逻辑运行状态
  // 返回时转换为ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以在不失去响应性的情况下破坏结构
    const { foo, bar } = useFeatureX()
    return {
      foo,
      bar
    }
  }
}
复制代码
看个例子
<template>
    <p>toRefs demo {{ age }} {{ name }}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive } from 'vue'

export default {
    name: 'ToRefs',
    setup() {
        const newState = reactive({
          age: 18,
          name: "monday",
        });

        const stateAsRefs = toRefs(newState); // 将响应式对象,变成普通对象
        setTimeout(() => {
          console.log('newState before--', newState.age, newState.name)
          newState.age = 20,
          newState.name = '周一'
          console.log('newState after--', newState.age, newState.name)
          console.log(stateAsRefs, 'stateAsRefs---');
        }, 1500);

        return {
        	...stateAsRefs
        }
    }
}
</script>
// 输出结果
toRefs demo 18 monday
toRefs demo 20 周一
复制代码
总结

toRefs在setup或者Composition Function(合成函数)的返回值特别有用。

为什么需要toRef和toRefs

ref不一样的是, toReftoRefs 这两个兄弟,它们不创造响应式,而是延续响应式。创造响应式一般由ref或者 reactive 来解决,而toReftoRefs则是把对象的数据进行分解和扩散,其这个对象针对的是响应式对象非普通对象总结起来有以下三点

  • 不丢失响应式的情况下,把对象数据进行 分解或扩散
  • 针对的是响应式对象reactrive 封装的)而非普通对象
  • 不创造响应式,而是延续响应式
总结
  • ref值类型的响应式
  • toRef为源响应式对象上的属性创建一个ref。然后可以将ref传递出去,从而保持对其源属性的响应式连接
  • toRefs将响应式对象转换为普通对象,其中结果对象的每个属性都是指向原始对象相应属性的ref
  • 为了防止误会产生, ref的变量命名尽量都用xxxRef ,这样在使用的时候会更清楚明了
  • 合成函数返回响应式对象时,使用toRefs
  • toRefs方法可以解构为多个Ref对象的引用

customRef

定义

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收tracktrigger函数作为参数,并应返回一个带有getset的对象。

例子

使用v-model使用自定义ref实现debounce的示例

// 模版
<input v-model="text" />

// script
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello')
    }
  }
}
复制代码

computed

computed函数与vue2中computed功能一致,它接收一个函数并返回一个value为getter返回值的不可改变的响应式ref对象。

不同的是computed被抽成一个API,直接从vue中获取,而Vue2.x中,computed是一个对象,在对象中定义一个computed。

有两种编写形式

  • 接受一个getter函数的简写方式,即直接传一个函数,返回你所依赖的值的计算结果,这个值是个包装对象,默认情况下,如果用户试图去修改一个只读包装对象,会触发警告,只能get无法set。
  • 接收getset函数的对象的方式,即传一个对象,对象包含get函数和set函数
// getter简写形式
<template>
	<div>{{newCount}}</div>
</template>

// script
import { computed } from 'vue'
setup{
	const cnt = ref(3)
  console.log(cnt.value) // 3
  const newCount = computed(() => cnt.value + 1)
  console.log(newCount.value) // 4
  
  return{
  	newCount
  }
}

// get和set函数的对象的方式
setup{
	const cnt = ref(3)
  console.log(cnt.value) // 3
  const newCount = computed({
  	get() {
      return cnt.value + 1
    },
    set(newVal) {
    	//赋值
    	cnt.value = newVal - 1
      console.log(newVal)
    }
  })
  console.log(newCount.value) // 4
  
  return{
  	newCount
  }
}
复制代码

总结

  • computed返回值是readonly形式,默认不可更改
  • 如果computed用reactive包裹的话,会自动拆装箱,computed里面就不用.value了

watch和watchEffect

watch

watch API 与选项式 APIthis.$watch (以及相应的watch选项) 完全等效。watch 需要侦听特定的data源,并在单独的回调函数中副作用。默认情况下,它也是惰性的,即回调是仅在侦听源发生更改时调用。

语法
watch(params,handler(newValue, oldValue), { immediate: true, deep: true })
复制代码

watch传入三个参数:

  1. params:一个响应式属性或getter函数
  2. handler:回调函数
  3. object:可选配置项
特点
  • 具有懒执行的特性,并不会立即执行
  • 要明确哪些依赖项的状态改变,触发侦听器的重新执行,支持监听多个依赖
  • 能够获得状态变更前后的值
  • 可以手动停止监听
侦听一个单一源

侦听器 data 源可以是返回值的 getter 函数,也可以是ref

// 侦听一个getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})
复制代码

看个例子

// 侦听一个getter
setup{
		const state = reactive({ nickname: "xiaofan", age: 20 });
    setTimeout(() => {
      state.age++;
    }, 1000);

    //// 监听reactive定义的数据
    // 修改age值时会触发watch的回调
    watch(() => state.age,(curAge, preAge) => {
      console.log("新值:", curAge, "老值:", preAge);
    });
}
// 直接侦听一个ref
setup{
		const year = ref(0);
    setTimeout(() => {
      year.value++;
    }, 1000);
    watch(year, (newVal, oldVal) => {
      console.log("新值:", newVal, "老值:", oldVal);
    });
}
复制代码
侦听多个源

侦听器还可以使用数组同时侦听多个源:

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})
复制代码

看个例子

// 同时监听ref定义的多个响应式数据

// template
<el-input type="text" v-model="name" />
<el-input type="text" v-model.number="age" />

// script
setup{
		const name = ref('张三')
    const age = ref(20)
    // newValue和oldValue分别是一个数组,和监听变量的顺序一一对应
    watch([name, age], (newValue, oldValue) => {
      console.log(newValue, '+++++++') // [newName, newAge]
      console.log(oldValue, '=======') // [oldName, oldAge]
    })
    return {
    	name,
    	age
    }
}

// 同时监听reactive定义的多个响应式数据

// template
<div>person.name{{person.name}}</div>
<div>person.age{{person.age}}</div>
<div>person.job.jobName{{person.job.jobName}}</div>

// script
setup{
		const person = reactive({
      name: '张三',
      age: 20,
      job: {
        jobName: '工程师',
        area: {
          areaName: '上海'
        }
      }
    })
    setTimeout(() => {
      person.name = '李四'
      person.age = 30
      person.job.jobName = '设计师'
      console.log('--------');
    }, 1000)
    
    watch([() => person.name, () => person.age, () => person.job],(newValue, oldValue) => {
      console.log(newValue, 'newValue--');
      console.log(oldValue, 'oldValue--');
    },{ deep: true })	
    
    return{
    	person
    }
}
复制代码
清除副作用

为什么需要清除副作用?

有这样一种场景,在watch中执行异步操作时,在异步操作还没有执行完成,此时第二次watch被触发,这个时候需要清除掉上一次异步操作。

watch提供了一个onCleanup的副作用清除函数,该函数接收一个函数,在该函数中进行副作用清除。

onCleanup什么时候执行?

  • watch的callback即将被第二次执行时先执行onCleanup
  • watch被停止时,即组件被卸载之后
  • watch选项(包括lazy、deep、flush)
const getData = (value) => {
	const handler = setTimeout(() => {
		console.log('数据', value)
	}, 5000)
	return handler
}

const inputRef = ref('')
watch(inputRef, (val, oldVal, onCleanup) => {
	const handler = getData(val) // 异步操作
	// 清除副作用
	onCleanup(() => {
		clearTimeout(handler)
	})
})

return {inputRef}
复制代码
停止监听
setup{
	const stopWatch = watch('xxxx');
	// 执行即可停止监听
	// watch返回一个函数 function(){ stop() }
	stopWatch()
	return {};
}
复制代码

watchEffect

watchEffect函数不用指明监听哪个属性,监听的回调中用到哪个属性,就监听哪个属性。在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。默认初始化时会执行一次。

特点
  • 会立即执行副作用方法。并且当内部所依赖的响应式值发生改变时也会重新执行
  • 不需要指定监听属性,可以自动收集依赖
  • 可以通过onInvalidate 取消监听
setup{
		const person = reactive({
      name: '张三',
      age: 20,
      job: {
        jobName: '工程师',
        area: {
          areaName: '上海'
        }
      }
    })
    setTimeout(() => {
      person.name = '李四'
      person.age = 30
      person.job.jobName = '设计师'
      console.log('--------');
    }, 1000)
    
    watchEffect(() => {
      console.log(person.name, 'watchEffect--')
      console.log(person.job.jobName, 'watchEffect++')
      onInvalidate(() => {
        // TODO
      })
    })
}
复制代码
注意

需要注意,当副作用函数中执行的函数,若该函数又改变了响应式的数据,可能会造成死循环问题。

总结

  • watchEffect不需要指定监听的属性,他会自动的收集依赖,只要在回调函数中引用到了响应式的属性,那么当这些属性变动的时候,这个回调都会执行,而watch只能监听指定的属性而作出变动(vue3开始能够同时指定多个)
  • watch能够获取到新值与旧值(更新前的值),而watchEffect是拿不到的
  • watchEffect在组件初始化的时候就会执行一次用以收集依赖,收集到的依赖发生变化时再执行。而watch则是直接指定依赖项

methods

基础用法

// 模版
<p>Capacity: {{capacity}}</p>
<button @click="increaseCapacity()">Increase Capacity</button>
  
// script
setup{
	const capacity = ref(3);
  function increaseCapacity(){
   capacity.value++
  }
  return {
  	capacity,
  	increaseCapacity
  }
}
复制代码

存在的问题

当然Composition API的引入也存在一定的弊端,组合式API在代码组织方面提供了更多的灵活性,但它也需要开发人员更多地遵守约定(即正确的使用方法),组合式API会让使用不熟的人编写出面条代码:

什么是面条代码?

代码的控制结构复杂、混乱,逻辑不清,关系耦合,让人一时难以理解

为什么会出现面条代码?

在Options API 中实际上形成了一种强制的约定:

  • props里面设置接收参数

  • data 里面设置变量

  • computed里面设置计算属性

  • watch里面设置监听属性

  • methods里面设置事件方法

我们发现Options API已经约定了我们该在哪个位置做什么事,这在一定程度上也强制我们进行了代码分割。 现在用Composition API,不再这么约定了,于是代码组织非常灵活,如果作为一个新手,或者不深入思考的话,那么在逻辑越来越复杂的情况下,setup代码量越来越多,同样setup里面的return越来越复杂,那么肯定会成为面条代码。

如何避免?

没有了this上下文,没有了Options API的强制代码分离。Composition API给了更加广阔的天地,那么我们需要慎重自约起来。牢记setup()函数现在只是简单地作为调用所有组合函数的入口。即对于复杂的逻辑代码,我们要更加重视起 Composition API的初心,使用Composition API来分离代码,用来切割成各种模块导出。

即我们期望的代码是长下面这样的,这样就算setup内容代码量越来越大,但是始终代码结构清晰。

import useA from './a';
import useB from './b';

export default {
    setup (props) {
        let { a, methodsA } = useA();
        let { b, methodsB } = useB();
        return {
            a,
            methodsA,
            b,
            methodsB,
        }
    }
}
复制代码

Composition API实现逻辑复用

规则

  • Composition API抽离逻辑代码到一个函数
  • 函数的命名约定为useXxxx格式
  • setup中引用useXxx函数

Demo

引用一个非常经典的例子:获取鼠标的定位,我们用Composition API来进行封装演示

// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
    const x = ref(0)
    const y = ref(0)

    function update(e) {
        x.value = e.pageX
        y.value = e.pageY
    }

    onMounted(() => {
        console.log('useMousePosition mounted')
        window.addEventListener('mousemove', update)
    })

    onUnmounted(() => {
        console.log('useMousePosition unMounted')
        window.removeEventListener('mousemove', update)
    })

    return {
        x,
        y
    }
}

export default {
  useMousePosition
}

// .vue文件
<template>
    <p v-if="flag">mouse position {{x}} {{y}}</p>
    <button @click="changeFlagHandler">change flag</button>
</template>

<script>
import { reactive } from 'vue'
import utils from "../../utils/useMousePosition";

export default {
    name: 'MousePosition',
    setup() {
        const { x, y } = utils.useMousePosition();
        let flag = ref(true);
        console.log(flag, "----");
        let changeFlagHandler = () => {
          flag.value = !flag.value;
        };
        return {
            x,
            y,
            flag,
      			changeFlagHandler,
        }
    }
}
</script>
复制代码

参考网址:

www.vue3js.cn/docs/zh/api…

juejin.cn/post/689054… (动画)

www.vuemastery.com/ (Vue mastery)

文章分类
前端
文章标签