vue.js3.0 Composition API

357 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、vue3.0介绍

1、vue不同的构建版本

vue3不再构建UMD模块化的方式,因为umd需要支持多种模块化方式,代码有些冗余

1. cjs-commonJs-完整版vue

  • vue.cjs.js-开发版,没有被压缩
  • vue.cjs.prod.js-生产版,代码被压缩

2. global-全局,可以直接在浏览器中使用script导入,导入后增加一个全局vue对象

  • vue.global.js-完整版的开发版,没有被压缩
  • vue.global.prod.js-完整版的生产版,代码被压缩
  • vue.runtime.global.js-运行时开发版,没有被压缩
  • vue.runtime.global.prod.js-运行时的生产版,代码被压缩

3. browser-esModule,浏览器原生模板化方式,通过引入<script type='module'>的方式

  • vue.esm-browser.js
  • vue.esm-browser.prod.js-完整版
  • vue.runtime.esm-browser.js
  • vue.runtime.esm-browser.prod.js-运行时

4. bundler-没有打包所有代码,需要配合打包工具使用

  • vue.esm-bundle.js-完整版
  • vue.runtime.esm-bundle.js-脚手架默认使用,只导入运行时,vue的打包最小版本,只会打包使用到的代码

2、vue3新增的composition API

学习composition的方式: 查看RFC,vue2到vue3的大改动基本都是在RFC确认,官方给出提案,手机社区的反馈并讨论,最后确认

1. composition API组合API设置的初衷

在vue2中开发中小项目,已经很合适了,但是在开发大型项目时,options有不好拆分和重用问题

Options API:

  • 包含描述组件选项(data,methods,props,watch等)的对象
  • 开发复杂组件,一个功能逻辑分别拆分在好几个选项中,如果其他人写的看大型项目的代码,可能不好理解

Composition API:

  • vue.js3.0新增的一组API
  • 基于函数的API
  • 可以更灵活的组织组件的逻辑

3、性能提升

1. vue3使用proxy对象重写了响应式系统

  1. vue2中响应式核心是Object.defineProperty
  • 初始化的时候就执行,如果没有使用这个属性也进行了响应式处理
  • 初始化的时候遍历所有的成员,通过defineProperty把对象的属性转换成getter和setter,如果子元素也是对象需要递归处理
  • vue2中动态新增属性只能通过vue.set()
  • vue2中监听不到删除属性,删除的话只能通过vue.delete()
  • vue2中监听不到数组的索引和length的属性 vue2 响应式源码解读可查看:vue2 响应式源码解析
  1. vue3使用proxy对象重写响应式
  • proxy的性能本来就比defineproperty好
  • 代理属性可以拦截对象的访问,赋值,删除等操作
  • 不需要初始化的时候遍历属性,如果有属性嵌套,只有访问的时候才会进行递归处理
  • 可以监听到动态新增属性
  • 可以监听到删除属性
  • 可以监听数组的索引和length的属性

2. 同时优化编译过程,重写虚拟DOM提升渲染功能

  1. vue2中通过标记静态根节点,来优化diff操作
  • 在构建的时候,将模板编译为render函数,需要编译静态根节点,静态节点
  • 当组件的状态发生变化时,会通知watch
  • 触发watch的undate,最终执行虚拟DOM的patch操作
  • 遍历所有虚拟节点,找到差异最终更新到真实DOM上

vue2 虚拟DOM源码解读可查看:vue2 虚拟DOM源码解析

  1. vue3中标记和提升所有的静态根节点,diff的时候只需要对比动态节点内容
  • vue3中添加了Fragments片段,在vscode中需要升级vetur,否则会报错
  • 静态提升,提升静态节点,只需要在初始化的时候创建一次,以后不需要再创建
  • patch flag标记动态节点,会跳过静态节点,直接比较动态节点,例如,<div>{{count}}</div>这个会标记动态节点1,等下次直接比较动态内容就行;<div :id='id'>{{count}}</div>会标记为动态节点9,下次比较动态内容count,及动态属性id
  • 缓存事件处理函数

3. 优化源码体积,更好的tree shaking,减少打包体积

  • 在vue3中,移除了不常用的API,例如:inline-template,filter等
  • vue3中的API基本添加了Tree shaking,按需引入,如果不需要vue3新增的API,打包的时候是不会打包的

4、vite打包工具

vite是快的意思,对比webpack,更快

  1. ES Module:
  • 现代浏览器都支持ES Module(IE 不支持)
  • 通过<script type='module' src='..'></script>加载模块
  • 支持模块的script默认延迟加载
    • 相当于script标签设置defer属性
    • 在文档解析完成,加载DOM树后,加载DOMContentLoaded事件前执行
  1. Vite与Vue-cli的区别:
  • Vite在开发模式下不需要打包可以直接运行,打开页面秒开
  • Vite在生产环境下使用rollup打包,不用babel等转换,基于ES Module的方式打包
  • Vue-Cli在开发模式下必须打包才能运行,如果项目很大加载时间长
  • Vue-Cli使用webpack打包
  1. Vite特点:
  • 快速冷启动
  • 按需加载
  • 模块热更新
  1. Vite创建项目
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

二、composition API

1. composition API按需加载

新建项目composition-api,使用npm init初始化package.json,安装vue3

1. createApp创建对象属性

  • 新建01-composition.html
  • 使用browser-esModule,浏览器原生模板化方式,通过引入<script type='module'>
  • 按需引入,引入creatApp创建vue对象;在createApp中,data必须是一个函数;createApp创建的vue对象,比vue2中的对象属性要少

image.png

  • mount挂载,相当于vue2中的$mount
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    x: {{position.x}}
    y: {{position.y}}
  </div>
  <script type="module">
    import {createApp} from './node_modules/vue/dist/vue.esm-browser.js'
    const app = createApp({
      // data必须是函数
      data () {
        return {
          position: {
            x: 0,
            y: 0
          }
        }
      }
    })
    console.log(app)

    // 挂载,相当于vue2中的$mount('#app')
    app.mount('#app')
  </script>
</body>
</html>

使用composition

  • setup()是composition的入口
  • setup()有两个参数:第一个参数:props,介绍外部传入参数,是响应式对象,不能被解构;第二个参数:context是一个对象,context有三个属性:attrs,emit,slots
  • setup返回一个对象,可以用在模板,生命周期,props等
  • setup执行时机是props解析完毕,组件实例被创建之前执行beforecreate与created之间,所以无法使用this,不能找data等
  • setup中的值不是响应式的
<script type="module">
  import {createApp} from './node_modules/vue/dist/vue.esm-browser.js'
  //  setup()是composition API的入口
  const app = createApp({
    setup() {
      const position = {
        x: 0,
        y: 0
      }
      return {
        position
      }

    },
    mounted () {
      this.position.x = 100
    }
  })


  // 挂载,相当于vue2中的$mount('#app')
  app.mount('#app')
</script>

注意:此时改变x的值,在页面中未触发改变,不上响应式的

2. reactive响应式数据

因为setup中的值不是响应式的,所以需要使用reactive变为响应式,reactive的核心是proxy

  • setup中的值不是响应式的
<script type="module">
  import {createApp, reactive} from './node_modules/vue/dist/vue.esm-browser.js'
  //  setup()是composition API的入口
  const app = createApp({
    setup() {
      const position = reactive({
        x: 0,
        y: 0
      })
      return {
        position
      }

    },
    mounted () {
      this.position.x = 100
    }
  })


  // 挂载,相当于vue2中的$mount('#app')
  app.mount('#app')
</script>

此时改变x的值,页面中有更改,此时的position是相应书数据

3. 生命周期钩子函数

在setup中可以写生命周期钩子函数,与vue2的钩子函数基本一致,例如mounted改为onMounted();在vue3中destoryed的钩子函数是Unmounted,setup中写法为onUnmounted

image.png

setup生命周期钩子函数示例:例如写一个移动鼠标将坐标值在x,y中呈现

  • 在一个函数中写入移动鼠标需要的逻辑,挂载时添加事件,销毁时移除事件
  • setup中调用函数,返回一个响应式对象
  • setup返回一个对象,在组件中使用
<script type="module">
  import {createApp, reactive, onMounted, onUnmounted} from './node_modules/vue/dist/vue.esm-browser.js'
  function useMousePosition() {
    const position = reactive({
      x: 0,
      y: 0
    })
    const update = e => {
      position.x = e.pageX;
      position.y = e.pageY;
    }
    onMounted(() => {
      window.addEventListener('mousemove', update);
    })
    onUnmounted(() => {
      window.removeEventListener('mousemove', update);
    })

    return position;
  }

  // composition API setup()是com的入口
  const app = createApp({
    setup() {
      const position = useMousePosition();
      return {
        position
      }

    },
    mounted () {
      this.position.x = 100
    }
  })
  // 挂载,相当于vue2中的$mount('#app')
  app.mount('#app')
</script>

4. reactive-toRefs-ref

  1. 如上,在setup中调用useMousePosition得到positionconst position = useMousePosition();,如果想直接使用position中的x,y,那么直接使用解构赋值可以吗?

不可以。因为使用解构赋值是直接将值赋值给x,y,没有进行响应式处理。

  • 在useMousePosition函数中,position是通过reactive创建的,也就是代理对象
  • 当访问x,y的时候会调用getter收集依赖,等数据变化时调用setter拦截触发更新,是响应式;
  • 当使用解构赋值时,是直接将x,y复制一份,没有调用getter,setter,所以不具备响应式
  • toRefs可以将响应式数据中的属性变为响应式:
    • toRefs需要传入一个代理对象,否则会报错;
    • 然后toRefs内部创建一个新的对象来,遍历传入对象的所有属性,将属性值转为响应式对象,然后挂载到新创建的对象,最后将对象返回;
    • 内部会为代理对象的每一个属性创建一个具有value属性的对象,该对象是响应式的,value属性具有getter和setter,所以返回的每个属性都是响应式的
<script type="module">
  import {createApp, reactive, onMounted, onUnmounted} from './node_modules/vue/dist/vue.esm-browser.js'
  function useMousePosition() {
    const position = reactive({
      x: 0,
      y: 0
    })
    const update = e => {
      position.x = e.pageX;
      position.y = e.pageY;
    }
    onMounted(() => {
      window.addEventListener('mousemove', update);
    })
    onUnmounted(() => {
      window.removeEventListener('mousemove', update);
    })

    return toRefs(position);
  }

  // composition API setup()是com的入口
  const app = createApp({
    setup() {
      const { x, y} = useMousePosition();
      return {
        x,
        y
      }
    },
  })
  // 挂载,相当于vue2中的$mount('#app')
  app.mount('#app')
</script>
  1. ref,将基本类型数据转为响应式对象 创建一个简单案例,点击按钮,进行+1;

如果直接定影count = 0,这是赋值,不是一个响应式数据,使用ref(0),传入基本类型,相当于创建一个具有value属性的对象,value:0,value会在获取数据时调用getter收集依赖,数据改变时调用setter拦截触发更新

注意:使用count时,在模板中可以直接写count,但是方法中自加,需要count.value++

ref如果创建的值是一个对象,ref内部是调用reactive;

<div id="app">
  <span>{{ count }}</span>
  <button @click="increase">点击+1</button>
</div>
<script type="module">
  import {createApp, ref} from './node_modules/vue/dist/vue.esm-browser.js'
  
  function useCount() {
    const count = ref(0);
    return {
      count,
      increase: () => {
        count.value++
      }
    }
  }

  const app = createApp({
    setup() {
      return {
        ...useCount()
      }
    }
  })

  app.mount('#app')
</script>

5. computed-计算属性

computed简化模板中的代码,缓存计算结果,当数据发生变化时重新计算。

computed的用法:

  1. 传入函数
const count = ref(1)
computed(() => {
    return count.value++;
})
  1. 传入对象,需要有get,set
const count = ref(1)
computed({
    get: () => count.value++,
    set: (val) => {
        count.value = val - 1;
    }
})

示例: 显示未完成事件数,点击添加开会按钮,事件数加1

<div id="app">
  <span>未完成事件:</span>
  <ul v-for="item in activeData">
    <li>{{item.text}}</li>
  </ul>
  <span>未完成事件数:{{ activeCount }}</span><br>
  <button @click="push">添加开会的未完成事项</button>
</div>
<script type="module">
  import {createApp, reactive, computed} from './node_modules/vue/dist/vue.esm-browser.js'

  const data = [
    {text: '看书', completed: false},
    {text: '写代码', completed: false},
    {text: '锻炼', completed: true},
  ]

  const app = createApp({
    setup() {
      const todos = reactive(data);

      const activeData = computed(() => {
        const data = todos.filter(item => !item.completed);
        return data;
      })
      const activeCount = computed(() => {
        return todos.filter(item => !item.completed).length;
      })

      return {
        activeCount,
        activeData,
        push() {
          todos.push({
            text: '开会',
            completed: false
          })
        }
      }
    }
  })
  app.mount('#app')
</script>

6. watch侦听器

wath与vue2中侦听器一样 watch传入三个参数:

  • 第一个:要监听的数据,是ref或者reactive返回的对象
  • 第二个:监听到数据变化后执行的函数,函数有两个参数分别是新值和旧值
  • 第三个:参数选项,deep和immediate watch的返回值:取消监听的函数

示例:输入问题,出现yes/no结果,监听结果

<div id="app">
  <span>请问一个yes/no的问题:</span>
  <input v-model="question">
  <p>{{ answer }}</p>
</div>
<script type="module">
  import {createApp, ref, watch} from './node_modules/vue/dist/vue.esm-browser.js'
  const app = createApp({
    setup() {
      const question = ref('');
      const answer = ref('');

      watch(question, async (newValue, oldValue) =>  {
        const response = await fetch('https://www.yesno.wtf/api');
        const data = await response.json();
        answer.value = data.answer;
      })

      return {
        question,
        answer,
      }
    }
  })
  app.mount('#app')
</script>

7. watchEffect,wath函数的简化版本

watchEffect是watch函数的简化版本,也用来监视数据的变化;只能接受一个参数,为函数,监听函数内响应式数据的变化,返回值是取消监听的函数

示例:点击增加,进行+1,点击取消后,不再对增加函数进行监听

<div id="app">
  <button @click="increase">增加</button>
  <button @click="cancel">取消</button>
  <p>{{ count }}</p>
</div>
<script type="module">
  import {createApp, ref, watchEffect} from './node_modules/vue/dist/vue.esm-browser.js'

  const app = createApp({
    setup() {
      const count = ref(0);

      const cancel = watchEffect(() =>  {
        console.log(count.value)
      })

      return {
        count,
        cancel,
        increase: () => {
          count.value++;
        }
      }
    }
  })
  app.mount('#app')
</script>

image.png

8. 使用composition API写一个todolist

主要有:

  • 添加待办事项
  • 删除待办事项
  • 编辑待办事项,双击进行编辑,按esc取消,清空内容相当于删除
  • 切换待办事项,切换选项: 未完成,已完成,所有
  • 存储待办事项,存储到loacalStorage中
  1. 创建vue3项目
vue create [名]
  1. 添加待办事项
  2. 删除待办事项
  3. 编辑待办事项
  • 双击待办事项,展示编辑文本框
  • 按回车或者是编辑文本框失去焦点,修改完数据
  • 按esc取消编辑
  • 把编辑文本框清空数据,删除事项
  • 显示文本框的时候获取焦点(自定义指令)
  1. 切换待办事项
  • 点击checkbox,改变所有待办项状态
  • 切换,all,未完成,已完成
  • 显示未完成待办项个数
  • 移除所有的项目
  • 如果没有待办项,隐藏main和footer
  1. 存储待办事项
  • 将事项存储到localStorage中,刷新后保持上次的操作

image.png App.vue:

<template>
  <div id="app" class="todoapp">
    <header class="header">
      <h1>todos</h1>
      <input 
        class="new-todo"
        placeholder="What needs to be done?"
        autocomplete="off"
        autofocus
        v-model="input"
        @keyup.enter="addTodo"
      >
    </header>
    <div class="main" v-show="count">
      <input type="checkbox" id="toggle-all" class="toggle-all" v-model="allDone">
      <label for="toggle-all">标记所有为完成</label>
      <ul class="todo-list">
        <li
          v-for="todo in filterTodos"
          :key="todo"
          :class="{ editing: todo === isEdit, completed: todo.completed}"
        >
          <div class="view">
            <input type="checkbox" class="toggle" v-model="todo.completed">
            <label @click="editTodo(todo)">{{ todo.text }}</label>
            <button class="destroy" @click="remove(todo)"></button>
          </div>
          <input 
            type="text" 
            class="edit"
            v-model="todo.text"
            v-editing-focus="todo === isEdit"
            @click="editTodo(todo)"
            @keyup.enter="doneEdit(todo)"
            @blur="doneEdit(todo)"
            @keyup.esc="cancelEdit(todo)"
          >
        </li>
      </ul>
    </div>
    <footer class="footer" v-show="count">
      <span class="todo-count">
        <strong>{{ remainCount }}</strong> items left
      </span>
      <ul class="filters">
        <li><a href="#/all">All</a></li>
        <li><a href="#/active">Active</a></li>
        <li><a href="#/completed">completed</a></li>
      </ul>
      <button class="clear-completed" @click="removeCompleted"  v-show="count > remainCount">
        Clear completed
      </button>
    </footer>
  </div>
</template>

<script>
import { ref } from '@vue/reactivity'
import './assets/index.css'
import { computed, onMounted, onUnmounted, watchEffect } from '@vue/runtime-core';
import useLocalStorage from './utils/useLocalStorage';

const storage = useLocalStorage();

// 1. 添加待办事项
const useAdd = todos => {
  const input = ref('');
  const addTodo = () => {
    const text = input.value && input.value.trim();
    if (text.length === 0) return;
    todos.value.unshift({
      text,
      completed: false
    })
    input.value = '';
  }
  return {
    input,
    addTodo
  }
}

// 2. 删除待办事项
const useRemove = todos => {
  const remove = todo => {
    // todo的索引
    const index = todos.value.indexOf(todo);
    todos.value.splice(index, 1)
  }
  const removeCompleted = () => {
    todos.value = todos.value.filter(todo => !todo.completed)
  }
  return {
    remove,
    removeCompleted
  }
}

// 3. 编辑待办事项
const useEdit = remove => {
  // 编辑前文本内容
  let beforeEditText = '';
  // 是否编辑状态
  const isEdit = ref(null);
  
  const editTodo = todo => {
    beforeEditText = todo.text;
    console.log(beforeEditText,1111)
    isEdit.value = todo;
  }

  const doneEdit = todo => {
    if (!isEdit.value) return;
    todo.text = todo.text.trim();
    todo.text || remove(todo);
    isEdit.value = null;
  }

  const cancelEdit = todo => {
    isEdit.value = null;
    console.log(beforeEditText)
    todo.text = beforeEditText;
  }

  return {
    isEdit,
    editTodo,
    doneEdit,
    cancelEdit
  }
}

// 4. 切换待办事项,完成状态
const useFilter = todos => {
  const allDone = computed({
    // get展示事项
    get() {
      return !todos.value.filter(item => !item.completed).length
    },
    // set选中时修改完成状态
    set(value) {
      todos.value.forEach(todo => {
        todo.completed = value
      })
    }
  })

  const filter = {
    all: list => list,
    active: list => list.filter(todo => !todo.completed),
    completed: list => list.filter(todo => todo.completed)
  }

  const type = ref('all');
  const filterTodos = computed(() => filter[type.value](todos.value));
  // 未完成事项个数
  const remainCount = computed(() => filter.active(todos.value).length);
  const count = computed(() => todos.value.length)

  const onHashChange = () => {
    // 获取到地址
    const hash = window.location.hash.replace('#/', '');
    if (filter[hash]) {
      type.value = hash;
    } else {
      type.value = 'all';
      window.location.hash = ''
    }
  }
  onMounted(() => {
    window.addEventListener('hashchange', onHashChange);
    onHashChange();
  })
  onUnmounted(() => {
    window.removeEventListener('hashchange', onHashChange);
  })

  return {
    allDone,
    filterTodos,
    remainCount,
    count
  }
}

// 5. 存储待办事项
const useStorage = () => {
  const KEY = 'TODOKEYS';
  const todos = ref(storage.getItem(KEY) || []);
  watchEffect(() => {
    storage.setItem(KEY, todos.value);
  })
  return todos;
}

export default {
  name: 'App',
  setup() {
    const todos = useStorage();

    const { remove, removeCompleted } = useRemove(todos)
    return {
      todos,
      remove,
      removeCompleted,
      ...useAdd(todos),
      ...useEdit(remove),
      ...useFilter(todos)
    }
  },
  directives: {
    editingFocus: (el, binding) => {
      binding.value && el.focus()
    }
  }
}
</script>

useLocalStorage.js:

function parse(str) {
  let value;
  try {
    value = JSON.parse(str);
  }
  catch {
    value = null;
  }
  return value;
}

function stringify(obj) {
  let value;
  try {
    value = JSON.stringify(obj);
  }
  catch {
    value = null;
  }
  return value;
}

export default function useLocalStorage() {
  function setItem(key, value) {
    value = stringify(value);
    window.localStorage.setItem(key, value)
  }
  function getItem(key) {
    let value = window.localStorage.getItem(key)
    if(value){
      value = parse(value);
    }
    return value;
  }

  return {
    setItem,
    getItem
  }
}