基于项目实战阐述vue3.0新型状态管理和逻辑复用方式

前言

背景:

2019年2月6号,React 发布 16.8.0 版本,vue紧随其后,发布了vue3.0 RFC

Vue3.0受React16.0 推出的hook抄袭启发(咳咳...),提供了一个全新的逻辑复用方案。使用基于函数的 API,我们可以将相关联的代码抽取到一个 "composition function"(组合函数)中 —— 该函数封装了相关联的逻辑,并将需要暴露给组件的状态以响应式的数据源的方式返回出来。

本文目的

本文会简略得总结vue3.0带来的所有新特性,之后会介绍Vue3.0组合api的用法和注意点。最后会用一个 Todolist 的项目实战,向大家介绍Vue3.0的逻辑复用写法以及借用provide和inject的新型状态管理方式

本文提纲:

  • vue3.0主要特点
  • 如何新建一个使用vue3.0的项目
  • conposition api
  • 逻辑复用(hook)和状态管理(provide+inject)

    • 结合项目实战,做一个todo list

正文

vue3.0主要特点

  • 更小;

常驻代码体积在10kb左右。

  • Object.defineProperty 替换为 Proxy;
  • 支持typescript;

vue3.0本身就是用typescript重写的,完美的支持了tsx,且不会影响不使用ts的用户。

  • 优化vdom渲染函数。

通过模版静态分析,将模版分类(if for slot),记录动态节点的位置,更新时仅遍历动态节点,vdom的性能从原先的与模版大小相关变成和动态节点的数量相关。

  • 去除class api,改成function api;

了解react的同学可能这时候会闻到一股熟悉的闻到吧~其实就是撤销类写法,将所有逻辑放在一个**纯函数** 里面。

 setup (props) {

const name = reactive({

name: 'hello 番茄'

})

return { name }

}

以及只是简单的总结了一些重要的新特性。

如何新建一个使用vue3.0的项目

接下来向大家简单介绍下如何尝鲜 -- 自己创建一个vue3.0的项目。

  1. 安装vue0-cli

我这边使用的是最新版本的vue-cli 4.4.0

npm install -g @vue/cli

# OR

yarn global add @vue/cli

  1. 将vue升级到bata版本

vue add vue-next

ok了。就这么简单!

conposition api

#### 目录

  • 基本例子
  • setup()
  • reactive
  • ref
  • computed
  • watchEffect
  • watch
  • 生命周期
  • 依赖注入

基本例子

<template>

<div>

<div>count is {{ count.count }}</div>

<div>plusOne is {{ plusOne }}</div>

<button @click="increment">count++</button>

</div>

</template>

<script>

// eslint-disable-next-line no-unused-vars

import { reactive, computed, watch, onMounted } from 'vue'

export default {

name: 'HelloWorld',

props: {

msg: String

},

setup () {

// reactive state

const count = reactive({ count: 0 })

console.log("setup -> count", count.count)

// computed state

const plusOne = computed(() => count.count + 1)

// method

const increment = () => { count.count++ }

// watch

watch(() => count.count * 2, val => {

console.log(`count * 2 is ${val}`)

})

// lifecycle

onMounted(() => {

console.log(`mounted`)

})

// expose bindings on render context

return {

count,

plusOne,

increment

}

}

}

</script>

setup

该setup功能是新的组件选项。它是组件内部暴露出所有的属性和方法的统一API。

调用时机

创建组件实例,然后初始化 props ,紧接着就调用setup 函数。从生命周期钩子的视角来看,它会在 beforeCreate 钩子之前被调用

模板中使用

如果 setup 返回一个对象,则对象的属性将会被合并到组件模板的渲染上下文

<template>

<div>{{ count }} {{ object.foo }}</div>

</template>

<script>

import { ref, reactive } from 'vue'

export default {

setup() {

const count = ref(0)

const object = reactive({ foo: 'bar' })

// 暴露给模板

return {

count,

object,

}

},

}

</script>

setup 参数

  1. props

    第一个参数接受一个响应式的props,这个props指向的是外部的props。如果你没有定义props选项,setup中的第一个参数将为undifined。

    props和vue2.x并无什么不同,仍然遵循以前的原则;

  • 不要在子组件中修改props;如果你尝试修改,将会给你警告甚至报错。

  • 不要解构props。解构的props会失去响应性。

2.上下文对象

第二个参数提供了一个上下文对象,从原来 2.x 中 this 选择性地暴露了一些 property。

const MyComponent = {

setup(props, context) {

context.attrs

context.slots

context.emit

},

}

Tip:

由于vue3.x向下兼容vue2.x,所以我在尝试之后发现,一个vue文件中你可以同时写两个版本的东西。

import { reactive, computed, watch, onMounted } from 'vue'

export default {

name: 'HelloWorld',

props: {

count: Number,

},

data () {

return {

msg: "我是vue2.x中的this"

}

},

methods: {

test () {

console.log(this.msg)

}

},

mounted () {

console.log('vue2.x mounted')

},

// eslint-disable-next-line no-unused-vars

setup (props, val) {

console.log(this, 'this') // undefined

onMounted(() => {

console.log('vue3.x mounted')

})

return {

...props

}

}

}

当然这边不推荐你在项目中这么用,但是抱着尝鲜和探究的态度,我们势必要弄清如果这么写要注意哪些?

  1. 如果我写了mounted(2.x),在setup函数中又写了onMounted(3.x),谁先执行?

setup中的先执行。因为setup() 在解析 2.x 选项前被调用;

  1. 我在vue2.x选项中中定义在this上的变量,在setup上可以通过this访问吗?可以重复定义吗?可以return吗?

首先在setup中的this将不再指向vue,而是undefined;所以在setup函数内部自然无法访问到vue实例上的this。

setup内部定义的变量和外表的变量并无冲突;

但是如果你要将其return 暴露给template,那么就会产生冲突。

reactive

接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()

const obj = reactive({ count: 0 })

ref

接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 value。

const count = ref(0)

console.log(count.value) // 0

count.value++

console.log(count.value) // 1

tip:

  1. ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive.
  2. 当 ref 作为渲染上下文的属性返回(即在setup() 返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value;

<template>

<div>{{ count }}</div>

</template>

<script>

export default {

setup() {

return {

const count = ref(0)

count: count, // 而不是 count.value

}

},

}

</script>

  1. 当 ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 value 值,其行为类似普通属性。

const count = ref(0)

const state = reactive({

count,

})

console.log(state.count) // 0

state.count = 1

console.log(count.value) // 1

  1. 注意当嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套:

const arr = reactive([ref(0)])

// 这里需要 .value

console.log(arr[0].value)

const map = reactive(new Map([['foo', ref(0)]]))

// 这里需要 .value

console.log(map.get('foo').value)

computed

computed和vue2.x版本保持一致,支持getter和setter

  • 传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。

const count = ref(1)

const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误!

  • 或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。

const count = ref(1)

const plusOne = computed({

get: () => count.value + 1,

set: (val) => {

count.value = val - 1

},

})

plusOne.value = 1

console.log(count.value) // 0

watchEffect

传入的一个函数,并且立即执行,响应式追踪其依赖,并在其依赖变更时重新运行该函数。

注册监听

import {watchEffect}from 'vue' // 导入api

const count = ref(0) // 定义响应数据

watchEffect(() => console.log(count.value)) // 注册监听函数

// -> 打印出 0

setTimeout(() => {

count.value++

// -> 打印出 1

}, 100)

注销监听

- 默认情况下是在**组件卸载**的时候停止监听;

- 也可以显式**调用返回值**以停止侦听;

const stop = watchEffect(() => {

/* ... */

})

// 之后

stop()

清除副作用

> 有时副作用函数会执行一些异步的副作用, 这些响应需要在其失效时清除(即完成之前状态已改变了)。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参, 用来注册清理失效时的回调。

当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时
  • 侦听器被停止

const count = ref(0)

watchEffect(

(onInvalidate) => {

console.log(count.value, '副作用')

const token = setTimeout(() => {

console.log(count.value, '副作用')

}, 4000)

onInvalidate(() => {

// id 改变时 或 停止侦听时

// 取消之前的异步操作

token.cancel()

})

}

)

副作用刷新时机

> Vue 的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个 tick 中多个状态改变导致的不必要的重复调用。在核心的具体实现中, 组件的更新函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时, 会在所有的组件更新后执行:

<template>

<div>{{ count }}</div>

</template>

<script>

export default {

setup() {

const count = ref(0)

watchEffect(() => {

console.log(count.value)

})

return {

count,

}

},

}

</script>

在这个例子中:

  • count 会在初始运行时同步打印出来
  • 更改 count 时,将在组件更新后执行副作用。

如果副作用需要同步或在组件更新之前重新运行,我们可以传递一个拥有 flush 属性的对象作为选项(默认为 'post'):

// 同步运行

watchEffect(

() => {

/* ... */

},

{

flush: 'sync',

}

)

// 组件更新前执行

watchEffect(

() => {

/* ... */

},

{

flush: 'pre',

}

)

watch

> watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。

  • 对比 watchEffect,watch 允许我们:

    • 懒执行副作用;
    • 更明确哪些状态的改变会触发侦听器重新运行副作用;
    • 访问侦听状态变化前后的值。
  • 侦听单个数据源

侦听器的数据源可以是一个拥有返回值的 getter 函数,也可以是 ref:

// 侦听一个 getter

const state = reactive({ count: 0 })

watch(

() => state.count,

(count, prevCount) => {

/* ... */

}

)

// 直接侦听一个 ref

const count = ref(0)

watch(count, (count, prevCount) => {

/* ... */

})

  • 侦听多个数据源

watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {

/* ... */

})

  • 与 watchEffect 共享的行为

watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机 和 侦听器调试 等方面行为一致.

生命周期钩子函数

可以直接导入 onXXX 一族的函数来注册生命周期钩子,这些生命周期钩子注册函数只能在 setup() 期间同步使用,在卸载组件时,在生命周期钩子内部同步创建的侦听器和计算状态也将自动删除。

  • 与 2.x 版本生命周期相对应的组合式 API

    • beforeCreate -> 使用 setup()
    • created -> 使用 setup()
    • beforeMount -> onBeforeMount
    • mounted -> onMounted
    • beforeUpdate -> onBeforeUpdate
    • updated -> onUpdated
    • beforeDestroy -> onBeforeUnmount
    • destroyed -> onUnmounted
    • errorCaptured -> onErrorCaptured
  • 新增的钩子函数

    • onRenderTracked
    • onRenderTriggered

两个钩子函数都接收一个DebuggerEvent,与 watchEffect 参数选项中的 onTrack 和 onTrigger 类似:

export default {

onRenderTriggered(e) {

debugger

// 检查哪个依赖性导致组件重新渲染

},

}

依赖注入

provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。

这是本篇文章的重点。结合项目实战以此来探索一下未来的 Vue 状态管理模式和逻辑复用模式。

用法

provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。

import { provide, inject } from 'vue'

const ThemeSymbol = Symbol()

const Ancestor = {

setup() {

provide(ThemeSymbol, 'dark')

},

}

const Descendent = {

setup() {

const theme = inject(ThemeSymbol, 'light' /* optional default value */)

return {

theme,

}

},

}

inject 接受一个可选的的默认值作为第二个参数。如果未提供默认值,并且在 provide 上下文中未找到该属性,则 inject 返回 undefined。

  • 注入的响应性

可以使用 ref 来保证 provided 和 injected 之间值的响应:

// 提供者:

const themeRef = ref('dark')

provide(ThemeSymbol, themeRef)

// 使用者:

const theme = inject(ThemeSymbol, ref('light'))

watchEffect(() => {

console.log(`theme set to: ${theme.value}`)

})

如果注入一个响应式对象,则它的状态变化也可以被侦听。

逻辑组合与复用

引出问题:

我们通常会基于一堆相同的数据进行花样呈现,有列表展示、有饼图占比、有折线图趋势、有热力图说明频次等等,这些组件使用的是相同的一些数据和数据处理逻辑。对于数据处理逻辑,目前vue有

  • Mixins
  • 高阶组件 (Higher-order Components, aka HOCs)
  • Renderless Components (基于 scoped slots / 作用域插槽封装逻辑的组件)

但是上面的方案始存在一些弊端:

  1. 模版中的数据来源不清晰
  2. 命名空间冲突。
  3. 需要额外的组件实例嵌套来封装逻辑(性能问题);

##### 基于组合api 的解决方案

functionuseMouse() {

const x = ref(0)

const y = ref(0)

const update = e => {

x.value = e.pageX

y.value = e.pageY

}

onMounted(() => {

window.addEventListener('mousemove', update)

})

onUnmounted(() => {

window.removeEventListener('mousemove', update)

})

return { x, y }

}

// 在组件中使用该函数

const Component = {

setup() {

const { x, y } = useMouse()

// 与其它函数配合使用

const { z } = useOtherLogic()

return { x, y, z }

},

template: `<div>{{ x }} {{ y }} {{ z }}</div>`

}

项目预览

源码:

https://github.com/961998264/todolist-vue-3.0

项目介绍

  1. 已完成事件列表
  2. 未完成事件列表
  3. 查看事件详情
  4. 修改事件完成状态和事件详情

项目src目录

hooks文件夹是专门放hook的

context文件夹以模块划分

先来看下context编写(我这边是用的ts)

import { provide, ref, Ref, inject, computed, } from 'vue' //vue api

import { getListApi } from 'api/home' // mock的api

// 以下为定义的ts类型,你也可以单独建一个专门定义类型的文件。

type list = listItem[]

interface listItem {

title: string,

context: string,

id: number,

status: number,

}

interface ListContext {

list: Ref<list>,

getList: () => {},

changeStatus: (id: number, status: number) => void,

addList: (item: listItem) => void,

delList: (id: number) => void,

finished: Ref<list>,

unFinish: Ref<list>,

setContext: (id: number, context: string) => void,

setActiveItem: () => void,

}

provide名称,推荐用Symbol

const listymbol = Symbol()

提供provide的函数

export const useListProvide = () => {

// 全部事件

const list = ref<list>([]);

// 当前查看的事件id

const activeId = ref<number | null>(null)

// 当前查看的事件

const activeItem = computed(() => {

if (activeId.value || activeId.value === 0) {

const item = list.value.filter((item: listItem) => item.id === activeId.value)

return item[0]

} else {

return null

}

})

// 获取list

const getList = async function () {

const res: any = await getListApi()

console.log("useListProvide -> res", res)

if (res.code === 0) {

list.value = res.data

}

}

// 新增list

const addList = (item: listItem) => {

list.value.push(item)

}

//修改状态

const changeStatus = (id: number, status: number) => {

console.log('status', status)

const removeIndex = list.value.findIndex((listItem: listItem) => listItem.id === id)

if (removeIndex !== -1) {

list.value[removeIndex].status = status

}

};

// 修改事件信息

const setContext = (id: number, context: string) => {

const Index = list.value.findIndex((listItem: listItem) => listItem.id === id)

if (Index !== -1) {

list.value[Index].context = context

}

}

// 删除事件

const delList = (id: number) => {

console.log("delList -> id", id)

for (let i = 0; i < list.value.length; i++) {

if (list.value[i].id === id) {

list.value.splice(i, 1)

break

}

}

}

// 未完成事件列表

const unFinish = computed(() => {

return list.value.filter(item => item.status === 0)

})

// 已完成事件列表

const finished = computed(() => {

return list.value.filter(item => item.status === 1)

})

provide(listymbol, {

list,

unFinish,

finished,

changeStatus,

getList,

addList,

delList,

setContext,

activeItem,

activeId

})

}

在这个函数中定义 待办事件,并且定义一系列增删改查函数,通过provide暴露出去。

提供inject的函数

export const useListInject = () => {

const listContext = inject<ListContext>(listymbol);

if (!listContext) {

throw new Error(`useListInject must be used after useListProvide`);

}

return listContext

};

全局状态肯定不止一个模块,所以在 context/index.ts 下做统一的导出

import { useListProvide, useListInject } from './home/index'

console.log("useListInject", useListInject)

export { useListInject }

export const useProvider = () => {

useListProvide()

}

然后在 App.vue 的根组件里使用 provide,在最上层的组件中注入全局状态。

import { useProvider } from './context/index'

export default {

name: 'App',

setup () {

useProvider()

return {

}

}

}

在组件中获取数据:

import { useListInject } from '../../context/home/index'

setup () {

const { list, changeStatus, getList, unFinish, finished, addList, a ctiveItem, setContext } = useListInject()

}

不管是父子组件还是兄弟组件,或者是嵌关系套更深的组件,我们都可以通过useListInject来获取到响应式的数据。

  1. 逻辑聚合 同一份数据的相关逻辑我们可以写在一个usexxxx的函数中,不再像以前,按照选择将逻辑分开。在methods,computed,watch,created,mounted中来回跳转。

  2. 取代vuex 在比较小的项目中,你可以用这种状态管理的方式取代vuex。(反正我用react基本不用redux,不管项目大小)。

欢迎关注公众号:前端开发指南

以上是 基于项目实战阐述vue3.0新型状态管理和逻辑复用方式 的全部内容, 来源链接: utcz.com/a/28955.html

回到顶部