Topic:Vue3的设计与改变
Vue3发展历程
- 2018/09/30:尤雨溪在medium个人博客上发布了 Vue 3.0 的开发路线
- 2019年初:采用了RFC(征求意见)流程
- 2019/10/05:Vue 3 源码开放(pre-alpha状态)
- 2020/04/17:Vue 3.0 beta
- 2020/05/28:发表文章「Vue 3 设计过程」
- 2020/07/18:Vue 3.0 RC
- 2020/09/18:Vue 3.0 “One Piece"
Vue3的核心设计与提升
响应式核心原理:Object.defineProperty 切换到 Proxy
- 可检测到新的属性添加
- 支持 Map、Set、WeakMap 和 WeakSet
- 提供更好的性能
- Proxy 作为浏览器的新标准,性能上是一定会得到厂商的大力优化的
- 不需要像Vue2那样递归对所有的子数据进行响应式定义,而是再获取到深层数据的时候再去利用reactive进一步定义响应式,这对于大量数据的初始化场景来说收益会非常大
Vue Composition API
- 旨在解决大规模应用场景中的痛点
- 更灵活的逻辑组织模式(如逻辑提取与复用)
- 更为可靠的类型推断能力
Vue3代码库完全使用typescript编写
- 已支持对模板表达式和 props 的类型检查
- 已全面支持 TSX
内部模块解耦(采用monorepo)
模块之间的依赖关系更加明确, 降低项目贡献壁垒并提高其长期可维护性
内部包有自己的单独API,类型定义和测试
解锁高级用法
编译器支持自定义AST转换,用于在构建时自定义(如,在构建时进行i18n操作)
核心运行时提供了一系列 API,用于针对不同渲染目标(如native moile、WebGL或终端)的自定义容器。默认的 DOM 渲染器也使用这系列 API 构建。
@vue/reactivity 模块——Vue响应式系统;可作为独立包进行使用。也可以与其他模块解决方案配对使用(如 lit-html),甚至是在非 UI 场景中使用。
性能提升
- bundle包大小方面(tree-shaking 减少了 41% 的体积)
- 初始渲染速度方面(快了 55%)
- 更新速度方面(快了 133%)
- 内存占用方面(减少了 54%)
实验特性
-
在 SFC 内使用 Composition API 的语法糖
-
:在 SFC 中支持将状态作为 CSS 变量注入到样式中
-
组件 - 异步组件或带有 async setup() 组件
SFC: Singe-File Components(单文件组件,.vue文件)
Vue3的性能优化
减小包的尺寸:
- 将大多数全局API和内部帮助程序移至ES模块导出(如
@vue/reactivity
、@vue/runtime-core
等) - 利用Tree Shaking,只打包需要使用的代码
渲染策略优化
vue渲染策略:
- HTML的模板 => (编译) => 虚拟DOM树的渲染函数
- 通过递归遍历两个虚拟DOM树,并比较每个节点上的每个属性来确定实际DOM的哪些部分需要更新
瓶颈:
任何一点改变却仍然需要递归整个虚拟DOM树,以了解发生了什么变化,尤其在查看包含大量静态内容且只有少量动态绑定(整个虚拟DOM)的模板时,效率低下。
最终目的:
克服虚拟DOM的瓶颈,最好的方法是消除不必要的虚拟DOM树遍历和属性比较
优化工作:
编译器分析模板并生成带有优化提示的代码,而运行时将拾取提示,并在可能的情况下采用快速路径:
将模版分为动态的和静态的
“块”
,对动态块
进行了扁平化处理
,减少运行时的遍历开销:在树的层面上,我们注意到,节点结构在没有模板指令的时候是完全静态的(例如,v-if和v-for)。如果我们将模板分为动态的和静态的“块”,每个块内的节点结构再次变得完全静态。当我们更新一个块内的节点时,我们不再需要递归遍历树,因为我们可以在平面数组中跟踪该块内的动态绑定。通过将我们需要执行的树遍历量减少一个数量级,从而节约了虚拟DOM的大部分开销。静态树提升与静态prop提升:编译器会主动检测模板中的静态节点,子树甚至数据对象,并将其提升到生成代码中的render函数之外。这样可以避免在每个渲染上重新创建这些对象,从而大大提高了内存使用率并减少了垃圾回收的频率。
生成编译器的优化提示:在元素级别,编译器还会根据需要执行的更新类型为具有动态绑定的每个元素生成一个优化标志。例如,具有动态类绑定和许多静态属性的元素将收到一个标志,指示仅需要进行类检查。运行时将获取这些提示并采用专用的快速路径。
响应式原理:Proxy
Vue会使用带有getter和setter的处理程序遍历其所有property并将其转换为Proxy;
这个Proxy使Vue能够在property被访问或修改时执行依赖项跟踪和更改通知。
const handler = {
get(target, prop, receiver) {
// 追踪函数:依赖追踪
track(target, prop)
const value = Reflect.get(...arguments)
if (isObject(value)) {
return reactive(value)
} else {
return value
}
},
set(target, key, value, receiver) {
// 触发函数:更改通知
trigger(target, key)
return Reflect.set(...arguments)
}
}
// 简化版
function track(target: object, type: TrackOpTypes, key: unknown) {
const depsMap = targetMap.get(target);
// 收集依赖时 通过 key 建立一个 set
let dep = new Set()
targetMap.set(ITERATE_KEY, dep)
// effect可以先理解为更新函数,存放在 dep 里
dep.add(effect)
}
// 简化版
function trigger(target: object, type: TriggerOpTypes, key?: unknown,) {
// 是通过key找到所有更新函数 依次执行
const dep = targetMap.get(target)
dep.get(key).forEach(effect => effect())
}
- 源码地址:@vue/reactivity(opens new window)
- observer-util(opens new window)
Vue Composition API(简称VCA)
什么是Composition API
Composition API是对于现有Option API的补充,构建于响应式API基础之上
setup函数
新的组件选项,VCA的入口点
export default {
props: {
title: String
},
setup(props, context) {
console.log(props.title)
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
return {}
}
}
- 执行时机:在生命周期钩子beforeCreate钩子之前被调用,且执行一次
- 参数:第一个参数:props,第二个参数context(属性有attrs、slots与emit等)
- 返回一个对象:返回的对象属性将会被合并到组件模板的渲染上下文(this)
注意点:
- 参数props不能解构,会失去响应性
- this在 setup() 中不可用
- 在setup函数中,不能访问组件选项data、computed与methods
响应式API
ref
接受一个参数值并返回一个响应式且可改变的 ref 对象,通过.value获取属性值
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
reactive
接收一个普通对象然后返回该普通对象的响应式代理。等同于 2.x 的 Vue.observable()
const state = reactive({ count: 0 })
computed
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // 错误!
或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态。
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1
},
})
plusOne.value = 1
console.log(count.value) // 0
readonly
传入一个对象(响应式或普通)或 ref,返回一个原始对象的只读代理
const original = reactive({ count: 0 })
const copy = readonly(original)
watchEffect(() => {
// 依赖追踪
console.log(copy.count)
})
// original 上的修改会触发 copy 上的侦听
original.count++
// 无法修改 copy 并会被警告
copy.count++ // warning!
watchEffect
立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> 打印出 0
setTimeout(() => {
count.value++
// -> 打印出 1
}, 100)
停止侦听
const stop = watchEffect(() => {
/* ... */
})
// 之后
stop()
清除副作用
watchEffect((onInvalidate) => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id 改变时 或 停止侦听时
// 取消之前的异步操作
token.cancel()
})
})
watch
watch API 完全等效于 2.x this.$watch
侦听单个数据
const count = ref(0)
// 侦听单个数据
watch(count, (count, prevCount) => {
/* ... */
})
// 侦听多个数据
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
count is: {{count}}
生命周期钩子
beforeCreate -> 使用 setup()
created -> 使用 setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured
新增钩子(用于调试)
onRenderTracked
onRenderTriggered
import {
defineComponent, ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted, onErrorCaptured, onRenderTracked, onRenderTriggered
} from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
// 在挂载开始之前被调用调用
onBeforeMount(() => {})
// 在挂载后被调用调用
onMounted(() => {})
// 组件更新前调用
onBeforeUpdate(() => {})
// 组件更新后调用
onUpdated(() => {})
// 组件卸载前调用
onBeforeUnmount(() => {})
// 组件卸载后调用
onUnmounted(() => {})
// 当捕获一个来自子孙组件的错误时被调用
onErrorCaptured(() => {})
onRenderTracked((DebuggerEvent) => {
debugger
// 检查哪个依赖性被组件追踪
})
onRenderTriggered((DebuggerEvent) => {
debugger
// 检查哪个依赖性导致组件重新渲染
})
return { count }
},
})
设计动机
更友好的类型推断(defineComponent结合typescript)
vue2.x对typescript的类型推断不友好:
- vue2.x通过this来暴露property,所有选项(如methords)的this都是指向组件实例,而不是指向mehordes对象。
- vue2.x使用vue-class-component将组件编写为TypeScript class (借助 decorator,不稳定的 stage 2 提,存在不确定性)
2. 实现逻辑提取与复用
// 追踪鼠标位置的例子
import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update)})
return { x, y }
}
import { useMousePosition } from './useMousePosition'
export default {
setup() {
const { x, y } = useMousePosition()
// 其他逻辑...
return { x, y }
},
}
-
更加灵活的代码组织模式,不需要总是通过选项来组织代码:
Option API:通过选项(如methords、computed、watch等)来组织代码,逻辑关注点分散,这种碎片化使得复杂的组件难以维护
Composition API:通过逻辑来组织代码,逻辑关注点内聚,便于复用与维护
对比Mixins、高阶组件和无渲染组件
弊端:
- 渲染上下文中暴露的 property 来源不清晰。例如在阅读一个运用了多个 mixin 的模板时,很难看出某个 property 是从哪一个 mixin 中注入的。
- 命名空间冲突。Mixin 之间的 property 和方法可能有冲突,同时高阶组件也可能和预期的 prop 有命名冲突。
- 性能方面,高阶组件和无渲染组件需要额外的有状态的组件实例,从而使得性能有所损耗。
相比而言,组合式 API:
- 暴露给模板的 property 来源十分清晰,因为它们都是被组合逻辑函数返回的值。
- 不存在命名空间冲突,可以通过解构任意命名
- 不再需要仅为逻辑复用而创建的组件实例。
缺点与槽点
缺点
- 新概念ref的心智负担:
- 不断地区分响应式引用、基础类型值和对象;
- 读写ref的操作更冗余,需要访问.value;
- 需要理解ref和reactive的区别和使用场景。
- setup()返回语句可能会冗长
槽点
- 所有逻辑都堆在setup函数实现,代码可能会过于臃肿
- 担心VCA会让没有经验的人编写出意大利面条式代码(维护性差)——需约束
- 抄袭 React Hook
// 尤雨溪 原话
其实真的用过并且懂 React hooks 的人看到这个都会意识到 Vue Composition API (VCA)跟 hooks 本质上的区别。VCA 在实现上也其实只是把 Vue 本身就有的响应式系统更显式地暴露出来而已。真要说像的话,VCA 跟 MobX 还更像一点。
但对于不懂 React hooks 的人来说,长的像就是一样了,懒得解释。
关于Class API的提议 (opens new window)[Abandoned]
首先,使用Class API(装饰器方案)来支持更好的类型推断;但Class API的合理性存疑
不确定性:装饰器对第二阶段规范的依赖,存在很多不确定性,尤其是当TypeScript的当前实现与TC39提案完全不同步
props的类型推断
- 尴尬的双重声明:
interface Props {
message: string
}
class App extends Component {
@prop message: string
}
- 无法将装饰器声明的props类型暴露给this.$props,这会破坏TSX的支持
- 关于"reserved" methods的命名空间:解决方案不完美
- 实现复杂:
- 涉及许多边缘情况,需要引入其他内部代码;
- 为了使用Proxy this做响应式追踪,this在构造器中的实现会与在其他的地方不同;
- 更多代码:包含类组件与对象组件的申明的转换代码
- 收益不大:除了提供更好的TypeScript集成之外没有提供任何其他功能。
与React Hook对比
React Hook设计动机:
- 在组件之间复用状态逻辑很难:复用不同组件之间的状态逻辑
- 复杂组件变得难以理解(每个生命周期常常包含一些不相关的逻辑,且逻辑分散)
- 难以理解的 class
- 理解this的工作方式;
- 不能忘记绑定事件处理器;
- class 组件会无意中鼓励开发者使用一些让优化措施无效的方案:不好压缩、热重载不稳定。
Vue Composition API与React Hook的用法对比
// Vue Composition API
import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update)})
return { x, y }
}
// React Hook
import { useState, useEffect } from 'react';
export default function usePosition() {
const [ position, setPosition ] = useState({ x: 0, y: 0 })
function update(e) {
setPosition({
x: e.pageX,
y: e.pageY
})
}
useEffect(() => {
window.addEventListener('mousemove', update)
return () => {
window.removeEventListener('mousemove', update)
}
}, [])
return position
}
React Hook的规则(限制,增加心智负担)
- 不要在循环,条件或嵌套函数中调用Hook;
- 只能React函数的最顶层调用(
遵守这条规则,你就能确保Hook在每一次渲染中都按照同样的顺序被调用。这让React能够在多次的useState和useEffect调用之间保持hook状态的正确
); - 只能在React的函数组件中调用Hook。
Vue Composition API的优势
- 与 React Hook不同,setup 函数仅被调用一次,这在性能上比较占优。
- 对调用顺序没什么要求,每次渲染中不会反复调用Hook函数,产生的的 GC 压力较小。
- 不必考虑总是需要useCallback的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。
- React Hook 里的「依赖」是需要你去手动声明的;Vue会自动追踪依赖。
- React Hook有臭名昭著的闭包陷阱问题,如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。
// React Demo
import React, { useState, useEffect, useCallback } from 'react';
// state变化时,重新执行
function Example() {
const [ count, setCount ] = useState(0)
const [ val, setVal ] = useState('')
// count 或 val变化时都会执行
useEffect(() => console.log('count -', count, 'val -', val))
// 申明依赖count:只有count变化时执行,val变化时不执行
useEffect(() => console.log('count -', count), [ count ])
// 执行一次
useEffect(() => {
console.log('mounted!')
return () => console.log('unmounted!')
}, [])
// count变化时,updateVal的引用不变,子组件Child不会重新渲染
const updateVal = useCallback((val) => setVal(val), [ val ])
// count变化时,updateVal2的引用改变,子组件Child重新渲染
const updateVal2 = (val) => setVal(val)
return (
count is {count}
);
}
count is: {{count}}
使用 react hooks 带来的收益抵得过使用它的成本吗?(opens new window)
如何在Vue2中使用Composition API
# 安装插件
npm install @vue/composition-api
// 入口文件中
import VueCompositionAPI from '@vue/composition-api'
// 挂载Composition API
Vue.use(VueCompositionAPI)
// vue组件
import { computed, ref, reactive, watchEffect, onMounted } from '@vue/composition-api'
export default {
setup(props, ctx) {
// 创建响应式数据对象
const count = ref(0)
const state = reactive({
count
double: computed(() => count * 2)
})
watchEffect(() => console.log(state.count))
onMounted(() => console.log('mounted'))
return {
state,
increment() { state.count += 1 }
}
}
}
注意点:
- Vue2与Vue3响应式对象的核心原理不同;vue2数据观测仍存在缺陷:
- 无法检测属性的添加和删除;
- 无法检测数组索引和长度的变更
- 随着提案的更新,它也可能会做一些不兼容的变更,所以我们不建议这个阶段在生产环境中使用它。
相关链接
- @vue/composition-api(opens new window)
- 一篇文章上手Vue3中新增的API(opens new window)
兼容性
与Vue2兼容,但有重大更改与小改变:
全局API
全局API已更改为使用应用程序实例
2.x 全局 API | 3.x 实例 API (app) |
---|---|
Vue.config | app.config |
Vue.config.productionTip | removed |
Vue.config.ignoredElements | app.config.isCustomElement |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use |
// vue2
import Vue from 'vue'
import App from './App.vue'
import VueCompositionAPI from '@vue/composition-api'
Vue.use(VueCompositionAPI)
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
// vue3
import { createApp } from 'vue'
import App from './App.vue'
import HelloWorld from './components/HelloWorld.vue';
import Counter from './components/Counter.vue';
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [ ... ]
})
const app = createApp(App)
app.use(router)
app.mount('#app')
全局和内部 API 已经被重构为可 tree-shakable:
Vue 2.x 中的这些全局 API 受此更改的影响:
Vue.nextTick
Vue.observable (用 Vue.reactive 替换)
Vue.version
Vue.compile (仅全构建)
Vue.set (仅兼容构建)
Vue.delete (仅兼容构建)
// vue2
import Vue from 'vue'
Vue.nextTick(() => {
// 一些和DOM有关的东西
})
// vue3
import { nextTick } from 'vue'
nextTick(() => {
// 一些和DOM有关的东西
})
模版指令
- 组件上 v-model 用法已更改
-
和非 - v-for 节点上 key 用法已更改
- 在同一元素上使用的 v-if 和 v-for优先级已更改(v-if优先)
- v-bind="object" 现在排序敏感
- v-for 中的 ref 不再注册 ref 数组
组件
- 只能使用普通函数创建功能组件
- functional 属性在单文件组件 (SFC)
和 functional 组件选项被抛弃
- 异步组件现在需要 defineAsyncComponent 方法来创建
渲染函数
- 渲染函数 API 改变
- slots 作为函数暴露
- 自定义指令 API 已更改为与组件生命周期一致
- 一些转换 class 被重命名了:
- v-enter -> v-enter-from
- v-leave -> v-leave-from
- 组件 watch 选项和实例方法 $watch 不再支持点分隔字符串路径,请改用计算函数作为参数
- 在 Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器的 innerHTML。
移除API
- keyCode 支持作为 v-on 的修饰符
- off 和 $once 实例方法
- 过滤
- 内联模板 attribute
- $destroy 实例方法。用户不应再手动管理单个 Vue 组件的生命周期。
脚手架
vite
- 一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析
,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用(冷启动快)
- 不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢(即时的模块热更新)。
- 针对生产环境则可以把同一份代码用 rollup 打包(按需编译)。
缺点: 支持转译ts文件,但不执行类型检查
Vite App
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
# 创建Vite App项目
npm init vite-app
cd
npm install
npm run dev
vue-cli
一个基于 Vue.js 进行快速开发的完整系统
# 安装
npm install -g @vue/cli
# 创建vue3项目
vue create
# 选择`vue3 preview`
? Please pick a preset: (Use arrow keys)
❯ Default ([Vue 2] babel, eslint)
Default (Vue 3 Preview) ([Vue 3] babel, eslint)
Manually select features
# 添加typescript插件
vue add typescript
Vue插件
Vue-router
安装
npm install vue-router@next --save
Vue3中的使用方式
// 1. Define route components.
const Home = { template: 'Home' }
const About = { template: 'About' }
// 2. Define some routes
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
]
// 3. Create the router instance and pass the `routes` option
const router = VueRouter.createRouter({
// 4. Provide the history implementation to use.
history: VueRouter.createWebHashHistory(),
routes,
})
// 5. Create and mount the root instance.
const app = Vue.createApp({})
app.use(router)
app.mount('#app')
Vuex
安装
npm install vuex@next --save
vue3中的使用方式
import { createApp } from 'vue'
import { createStore } from 'vuex'
const app = createApp({ ... })
const store = createStore({ ... })
app.use(store)