vue hbulider 关闭项目命令yarn 项目 停止vue项目_ide


作者:Mingle

前言

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

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

本文目的

本文会介绍Vue3.0「组合api的用法和注意点」。最后会用一个 Todolist 的项目实战,向大家介绍「Vue3.0的逻辑复用写法以及借用provide和inject的新型状态管理方式」

本文提纲:

  • 如何新建一个使用vue3.0的项目
  • conposition api
  • 逻辑复用(hook)和状态管理(provide+inject)
  • 结合项目实战,做一个todo list

正文

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

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

  1. 安装vue0-cli

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

npm install -g @vue/cli# ORyarn global add @vue/cli
  1. 将vue升级到bata版本
vue add vue-next


vue hbulider 关闭项目命令yarn 项目 停止vue项目_生命周期_02


ok了。就这么简单!

conposition api

#### 目录

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

基本例子

count is {{ count.count }}   
      
    plusOne is {{ plusOne }}   
       count++  

setup

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

调用时机

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

模板中使用

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

{{ count }} {{ object.foo }}


setup 参数

  1. 「props」第一个参数接受一个响应式的props,这个props指向的是外部的props。如果你没有定义props选项,setup中的第一个参数将为undifined。props和vue2.x并无什么不同,仍然遵循以前的原则;
  • 不要在子组件中修改props;如果你尝试修改,将会给你警告甚至报错。


vue hbulider 关闭项目命令yarn 项目 停止vue项目_生命周期_03


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


vue hbulider 关闭项目命令yarn 项目 停止vue项目_ide_04


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,那么就会产生冲突。


vue hbulider 关闭项目命令yarn 项目 停止vue项目_vue项目的停止_05


reactive

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

const obj = reactive({ count: 0 })

ref

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

const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1

tip:

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


{{ count }}


  1. 当 ref 作为 reactive 对象的 property 被访问或修改时,也将自动解套 value 值,其行为类似普通属性。
const count = ref(0)const state = reactive({  count,})console.log(state.count) // 0state.count = 1console.log(count.value) // 1
  1. 注意当嵌套在 reactive Object 中时,ref 才会解套。从 Array 或者 Map 等原生集合类中访问 ref 时,不会自动解套:
const arr = reactive([ref(0)])// 这里需要 .valueconsole.log(arr[0].value)const map = reactive(new Map([['foo', ref(0)]]))// 这里需要 .valueconsole.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) // 2plusOne.value++ // 错误!
  • 或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。
const count = ref(1)const plusOne = computed({  get: () => count.value + 1,  set: (val) => {    count.value = val - 1  },})plusOne.value = 1console.log(count.value) // 0

watchEffect

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

注册监听

import {watchEffect}from 'vue' // 导入apiconst count = ref(0) // 定义响应数据watchEffect(() => console.log(count.value)) // 注册监听函数// -> 打印出 0setTimeout(() => {  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 中多个状态改变导致的不必要的重复调用。在核心的具体实现中, 组件的更新函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时, 会在所有的组件更新后执行:

{{ count }}

在这个例子中:

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

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

// 同步运行watchEffect(  () => {    /* ... */  },  {    flush: 'sync',  })// 组件更新前执行watchEffect(  () => {    /* ... */  },  {    flush: 'pre',  })

watch

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

  • 对比 watchEffect,watch 允许我们:
  • 懒执行副作用;
  • 更明确哪些状态的改变会触发侦听器重新运行副作用;
  • 访问侦听状态变化前后的值。
  • 侦听单个数据源

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

// 侦听一个 getterconst state = reactive({ count: 0 })watch(  () => state.count,  (count, prevCount) => {    /* ... */  })// 直接侦听一个 refconst 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 的解决方案function useMouse() {  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: `  
   {{ x }} {{ y }} {{ z }}  
  `}

项目预览

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

项目介绍

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

项目src目录


vue hbulider 关闭项目命令yarn 项目 停止vue项目_生命周期_06


hooks文件夹是专门放hook的


vue hbulider 关闭项目命令yarn 项目 停止vue项目_ide_07


context文件夹以模块划分


vue hbulider 关闭项目命令yarn 项目 停止vue项目_ide_08


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

import { provide, ref, Ref, inject, computed, } from 'vue' //vue apiimport { getListApi } from 'api/home' // mock的api// 以下为定义的ts类型,你也可以单独建一个专门定义类型的文件。type list = listItem[]interface listItem {  title: string,  context: string,  id: number,  status: number,}interface ListContext {  list: Ref,  getList: () => {},  changeStatus: (id: number, status: number) => void,  addList: (item: listItem) => void,  delList: (id: number) => void,  finished: Ref,  unFinish: Ref,  setContext: (id: number, context: string) => void,  setActiveItem: () => void,}

provide名称,推荐用Symbol

const listymbol = Symbol()

提供provide的函数

export const useListProvide = () => {  // 全部事件   const list = ref([]);  // 当前查看的事件id  const activeId = ref(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  {    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(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,不管项目大小)。

作者:Mingle