Vue3.0 体验(API 及 TodoList Demo)

3,510 阅读6分钟

前言

记得有段时间特别想在Vue中玩TypeScript,但是 Vue2.x对 TypeScript 的支持并不是特别好,一种比较好的实现方式是通过 Vue Class Component 库去实现,主要是通过装饰器来对组件的行为进行修改,通过写Class组件的方式去写组件,从而加上 TypeScript。但是装饰器目前还只是提案,并没有正式纳入规范中。

终于,2020.9.19凌晨,Vue3.0发布了,它的源码98%都是通过TypeScript写的,并且,通过组合式的方式去编写代码,几乎完美契合TypeScript的编码风格,我们可以随意的在Vue中写TypeScript,而不需要通过各种修饰或者插件去强行辅助我们写TS了。

Vue3.0 部分 API 尝鲜

这里就不过多的去介绍新的 API 了,官网里面最详细了。

createApp

返回一个提供应用上下文的应用实例。应用实例挂载的整个组件树共享同一个上下文。可以在 createApp 之后链式调用其它方法。该函数第一个参数接收一个根组件选项的对象,第二个参数可以将 props 传递给应用程序

createApp(
  App,
  {
    username: 'alex cheng' // 提供 props,App 组件内部通过 props: ['username'] 接收
  }
).mount('#app')

provide / inject

provide 和 inject 启用依赖注入。只有在使用当前活动实例的 setup() 期间才能调用这两者。

// 父组件
setup() {
  const globalData = reactive({
    name: 'alex.cheng'
  })
  
  provide('globalData', globalData)
}

// 子/孙 组件
setup() {
  const globalData = inject('globalData')
}

注意: 可以在 setup 中获取,也可以在生命周期(onMounted/onBeforeMount)中获取。但是不能在方法中获取,比如点击事件在方法中打印,会是 undefined,并且Vue会给出警告。

function showInject () {
  // 获取顶层组件的数据
  const globalObj = inject('globalObj')
  console.log(globalObj)
}

setup

一个组件的选项,在创建组件之前执行,代替了beforeCreatecreated钩子函数。它有两个参数,第一个是 props,第二个是当前上下文context,用来代替以前的thisemit方法就挂载在context上。(优先于 data 方法执行)

并作为组合式 API 的入口点,后续都会在这里做数据处理。

defineComponent

顾名思义,这是一个定义组件的方法,传递一个包含组件选项的对象

import { defineComponent, h } from 'vue'

const DefineComp = defineComponent({
  data () {
    return {
      count: 11
    }
  },
  render () {
    return h(
      'h1',
      {
        class: 'define-comp'
      },
      `${this.count}`
    )
  }
})

或者是一个setup函数(函数名将作为组件名来使用)

import { defineComponent, ref } from 'vue'

const HelloWorld = defineComponent(function HelloWorld() {
  const count = ref(0)
  return { count }
})

getCurrentInstance

getCurrentInstance 支持访问内部组件实例,用于高阶用法或库的开发。只能在 setup 或生命周期钩子中调用。

注意: 如需在 setup 或生命周期钩子外使用,请先在 setup 中调用 getCurrentInstance() 获取该实例然后再使用。

const MyComponent = {
  setup() {
    const internalInstance = getCurrentInstance() // works

    const id = useComponentId() // works

    const handleClick = () => {
      getCurrentInstance() // doesn't work
      useComponentId() // doesn't work

      internalInstance // works
    }

    onMounted(() => {
      getCurrentInstance() // works
    })

    return () =>
      h(
        'button',
        {
          onClick: handleClick
        },
        `uid: ${id}`
      )
  }
}

// 在组合式函数中调用也可以正常执行
function useComponentId() {
  return getCurrentInstance().uid
}

watch

Vue3中的 watch 方法和 Vue2 中的watch不太一样,它的第一个参数是要监听的对象,第二个参数是回调函数((newVal, oldVal) => {}),第三个参数是监听属性({ immediate, deep })

  watch(
    () => state.list.length, // 监听 list 数组的长度
    (v = 0) => {
      state.nums = v // 当数组的长度发生变化,将 nums 的值修改
    },
    { immediate: true } // 立即执行
  )

computed

使用 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。

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

plusOne.value++ // error

如果需要修改计算属性,就需要加上 setter 函数

onst count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    // 直接计算 count.value
    count.value = val - 1
  }
})

watchEffect

在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。

reactive | ref

创建响应式对象或者基础类型变量

toRef | toRefs | unRef

快速使用Vue3最新的15个常用API

TodoList Demo

每次学习一门新的语言或者一些新的 API, TodoList demo 肯定是跑不掉的。

看看如何用 Vue3 的 Composition API 来实现一个 todolist demo 吧!

需求

一个 todolist,它应该是包含输入框,添加任务按钮,过滤(状态:全部,已完成,未完成),显示任务列表,还有删除任务按钮。

界面大概是这样的!

看到这个界面,大概可以拆分成三个组件,一个是包含输入框和添加按钮的组件(TodoAdd.vue),然后是过滤的组件(TodoFilter.vue),最后是列表组件(TodoList.vue),不过 Todolist 还可以继续拆分,将每条任务当成一个组件(TodoListItem.vue)。

毫无疑问,这三个组件理应被容器组件(App.vue)包裹起来,并且初始化各功能组件的数据。代码如图:

Vue3 中最重要的思想就是 Composition API,通过组合的方式,更容易抽离出逻辑处理,并拆分逻辑与结构,降低代码的耦合性。

如何初始化界面的数据呢 ?

我们已经知道 setup 方法相当于 Vue2 中的 beforeCreatecreated 钩子,那么在这里我们可以做数据初始化的工作。

首先,我们在 src 下面创建 composables 文件夹,文件夹下面放组件可能需要用到的逻辑或者方法。接着我们创建一个 useTodo.js 文件,它的作用是获取任务列表,操作列表(添加,删除)。

// src/composables/useTodos.js

export default function useTodos() {
  const todos = ref([]);
  // 添加 todo
  const addTodo = (todo) => todos.value.push(todo);

  // 删除 todo
  const deleteTodo = (id) => {
    const Index = todos.value.findIndex(todo => todo.id === id)
    todos.value.splice(Index, 1)
  }

  // 获取远程 todos
  const fetchTodos = async () => {
    const response = await fetch(
      "https://jsonplaceholder.typicode.com/todos?_limit=5"
    );
    const rawTodos = await response.json()
    todos.value = rawTodos.map((todo) => ({
      id: todo.id,
      content: todo.title,
      completed: todo.completed,
    }));
  };

  onMounted(() => {
    fetchTodos();
  });

  // 返回列表数据,以及操作方法,这里返回的东西,将会在 setup 方法中通过 return 暴露出去
  return {
    todos,
    addTodo,
    deleteTodo
  };
}

接着回到 App.vue 中,在 setup 方法中,使用 useTodos.js 中的数据和方法。

setup() {
  // 拿到 todo 清单,以及向 todo 清单添加任务的方法
  const {todos, addTodo, deleteTodo} = useTodos()

  return {
    todos,
    addTodo,
    deleteTodo
  }
}

todos 数据 和 deleteTodo 方法就可以当作 props 传递给 todolist 组件了,addTodo 方法就传递给 todoAdd 组件。

// todoAdd.vue

setup(props, context) {
  return useEmitAddTodo(context.emit);
}

// 我们还可以创建组件专属的 composable logic
function useEmitAddTodo(emit) {
  const todoContent = ref("");

  const emitAddTodo = () => {
    if (todoContent.value === '') {
      return alert('请填写明确的任务!')
    }
    const todo = {
      id: Math.random(),
      content: todoContent.value,
      completed: false,
    };
    
    // 通过 emit 方法,调用传递给当前组件的方法
    emit("add-todo", todo);
    todoContent.value = "";
  };

  return {
    todoContent,
    emitAddTodo,
  };
}

接着,我们在 composables 文件夹下创建 useFilteredTodo.js 文件,里面存放处理过滤列表的方法和过滤之后的数据。

// src/composables/useFilteredTodo.js


export default function useFilteredTodos(todos) {
  // 通过 ref 创建一个响应式的变量 filter
  const filter = ref("all");

  // 过滤 todo
  const filteredTodos = computed(() => {
    switch (filter.value) {
      case "done":
        return todos.value.filter((todo) => todo.completed);
      case "todo":
        return todos.value.filter((todo) => !todo.completed);
      default:
        return todos.value;
    }
  });

  return { filter, filteredTodos };
}

然后在 TodoFilter.vue 组件中导入进去,代码如下:

结构如下:

<template>
  <div class="filters">
    <span
      v-for="filter in filters"
      :key="filter.value"
      @click="$emit('change-filter', filter.value)"
      class="filter"
      :class="{ active: selected == filter.value }"
      >{{ filter.label }}</span
    >
  </div>
</template>

逻辑部分:通过 emit 调用 change-filter 方法,将当前点击的 state 传递出去,改变 useFilteredTodos 中的 filter,由于过滤后的数据,是通过计算属性来处理的,所以当 filter 发生改变,那么计算属性就会重启计算,拿到新的 todo list 数据!

export default {
  name: "TodoFilter",
  props: ["selected"],
  setup() {
    const filters = reactive([
      { label: "全部", value: "all" },
      { label: "已完成", value: "done" },
      { label: "未完成", value: "todo" },
    ]);

    return { filters };
  },
};

到此,基本上就完成了TodoList Demo 了,通过 Composition API 的方式,我们可以更方便的去组合代码。以前我们将方法都放在 methods 里面,当我们的代码量越来越大的时候,我们就只能通过搜索的方式,在代码里面反复横跳。特别是接手了别人的代码,这种现象尤为明显。

源码链接:戳这里

推荐阅读