本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、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确认,官方给出提案,手机社区的反馈并讨论,最后确认
-
RFC(Request For Comments) - github.com/vuejs/rfcs
-
Composition API RFC - vue3js.cn/vue-composi…
1. composition API组合API设置的初衷
在vue2中开发中小项目,已经很合适了,但是在开发大型项目时,options有不好拆分和重用问题
Options API:
- 包含描述组件选项(data,methods,props,watch等)的对象
- 开发复杂组件,一个功能逻辑分别拆分在好几个选项中,如果其他人写的看大型项目的代码,可能不好理解
Composition API:
- vue.js3.0新增的一组API
- 基于函数的API
- 可以更灵活的组织组件的逻辑
3、性能提升
1. vue3使用proxy对象重写了响应式系统
- vue2中响应式核心是Object.defineProperty
- 初始化的时候就执行,如果没有使用这个属性也进行了响应式处理
- 初始化的时候遍历所有的成员,通过defineProperty把对象的属性转换成getter和setter,如果子元素也是对象需要递归处理
- vue2中动态新增属性只能通过vue.set()
- vue2中监听不到删除属性,删除的话只能通过vue.delete()
- vue2中监听不到数组的索引和length的属性 vue2 响应式源码解读可查看:vue2 响应式源码解析
- vue3使用proxy对象重写响应式
- proxy的性能本来就比defineproperty好
- 代理属性可以拦截对象的访问,赋值,删除等操作
- 不需要初始化的时候遍历属性,如果有属性嵌套,只有访问的时候才会进行递归处理
- 可以监听到动态新增属性
- 可以监听到删除属性
- 可以监听数组的索引和length的属性
2. 同时优化编译过程,重写虚拟DOM提升渲染功能
- vue2中通过标记静态根节点,来优化diff操作
- 在构建的时候,将模板编译为render函数,需要编译静态根节点,静态节点
- 当组件的状态发生变化时,会通知watch
- 触发watch的undate,最终执行虚拟DOM的patch操作
- 遍历所有虚拟节点,找到差异最终更新到真实DOM上
vue2 虚拟DOM源码解读可查看:vue2 虚拟DOM源码解析
- 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,更快
- ES Module:
- 现代浏览器都支持ES Module(IE 不支持)
- 通过
<script type='module' src='..'></script>加载模块 - 支持模块的script默认延迟加载
- 相当于script标签设置defer属性
- 在文档解析完成,加载DOM树后,加载DOMContentLoaded事件前执行
- Vite与Vue-cli的区别:
- Vite在开发模式下不需要打包可以直接运行,打开页面秒开
- Vite在生产环境下使用rollup打包,不用babel等转换,基于ES Module的方式打包
- Vue-Cli在开发模式下必须打包才能运行,如果项目很大加载时间长
- Vue-Cli使用webpack打包
- Vite特点:
- 快速冷启动
- 按需加载
- 模块热更新
- 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中的对象属性要少
- 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
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
- 如上,在setup中调用useMousePosition得到position
const 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>
- 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的用法:
- 传入函数
const count = ref(1)
computed(() => {
return count.value++;
})
- 传入对象,需要有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>
8. 使用composition API写一个todolist
主要有:
- 添加待办事项
- 删除待办事项
- 编辑待办事项,双击进行编辑,按esc取消,清空内容相当于删除
- 切换待办事项,切换选项: 未完成,已完成,所有
- 存储待办事项,存储到loacalStorage中
- 创建vue3项目
vue create [名]
- 添加待办事项
- 删除待办事项
- 编辑待办事项
- 双击待办事项,展示编辑文本框
- 按回车或者是编辑文本框失去焦点,修改完数据
- 按esc取消编辑
- 把编辑文本框清空数据,删除事项
- 显示文本框的时候获取焦点(自定义指令)
- 切换待办事项
- 点击checkbox,改变所有待办项状态
- 切换,all,未完成,已完成
- 显示未完成待办项个数
- 移除所有的项目
- 如果没有待办项,隐藏main和footer
- 存储待办事项
- 将事项存储到localStorage中,刷新后保持上次的操作
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
}
}