早在六月初,vue作者尤雨溪在知乎上发布了一篇vue3.0的RFC [Vue Function-based API RFC], 这篇文章指明了在19年末即将发布的vue3.0的初步路线。
RFC (Request For Comments),中文翻译为 "意见征求稿"
一. 设计目的
我们可以思考一下,为什么 vue团队 要选择function-based API?
1. 减少面条代码,提高灵活性。
-
众所周知, vue.js 的 api 对开发者十分的友好。在开发中,vue.js 的API强制要求开发者将组件代码基于选项切分开来。
-
理想很美好,现实很骨感。缺乏经验的新手在项目不断迭代中可能会将逻辑写在同一个文件,使得代码逻辑非常不易阅读及抽离。
-
新的API制约很少,它提倡开发者根据逻辑去抽离成函数,通过返回值将逻辑数据返回回来,也不只是可以根据逻辑去抽离代码,也可以为了写出更好的漂亮代码而抽离函数。
2. 减少mixin,提高代码重复利用率。
- 在我们的日常开发中,我们发现一个组件变的很大的时候,会将组件拆分成各个小组件,逻辑也会拆成一个个的 mixin 来复用(
表格分页mixin,对话框mixin等)。但在引入了多个 mixin 的情况下,会出现引用赋值混乱/命名重复的困扰,虽然解决了快速开发的问题,但这使得事情更加的糟糕。
// mixin-mouse.js
export default {
data () {
return {
x: 0,
y: 0
}
},
methods: {
update(e) {
x = e.pageX;
y = e.pageY;
}
},
mounted() {
window.addEventListener('mousemove', this.update)
},
destroyed() {
window.removeEventListener('mousemove', this.update)
}
}
// mouse.js
import { binding, onMounted, onUnmouted } from "vue";
export const useMouse = () => {
const x = binding(0)
const y = binding(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 }
}
// app.js
import { binding } from "vue";
import { useMouse } from "./mouse";
export default {
setup () {
const { x, y } = useMouse();
return { x, y };
}
}
引入的时候,开发者能够更清晰的识别及处理合并进来的值,这样更容易维护。
二. 方案
1. setup函数
- 这个函数将会是我们 setup 我们组件逻辑的地方,它会在一个组件实例被创建时,初始化了 props 之后调用。setup() 会接收到初始的 props 作为参数:
import { reactive, toBindings } from "vue";
export default {
// props, ctx不一定会有
setup(props) {
const state = reactive({
name: "lxs"
});
return {
...toBindings(state)
}
}
}
-
尤大大指出,setup 函数里面可以使用 this 获取到当前上下文,但没必要用。所以
liximomo的vue-function-api版本,setup 函数里面的 this 为 undefined,而且不仅仅有 props 参数,还有 context 参数,里面有以下参数:attrs: Object
emit: ƒ ()
parent: VueComponent
refs: Object
root: Vue
slots: Object
export default {
props: {
visible: {
type: Boolean,
default: false
}
},
setup(props, { attrs, emit, parent, refs, root, slots }) {
return {}
}
}
2. Binding函数 -> value
binding()返回的是一个包装对象(value wrapper), 里面只有一个 .value 属性,该属性指向内部被包装的值。
import { reactive, binding, isBinding, toBindings } from "vue";
export default {
setup() {
const name = binding("lxs");
console.log(name);
// ValueWrapper {
// value: Object
// _internal: {__ob__: Observer}
// __proto__: AbstractWrapper
// }
const state = reactive({
name: "test"
})
const changeName = () => {
if (isBinding(name)) {
name.value = "new lxs";
}
}
return {
...toBindings(state),
name,
changeName
}
}
}
binding函数附带了两个功能性函数:
isBinding: 判断是否为包装对象
toBindings: 将原始值转化成包裹对象
这里引发两个思考
为什么需要包装对象?
-
我们知道在 JavaScript 中,
原始值类型如 string 和 number 是只有值,没有引用的。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值,是无法追踪原始变量后续的变化的。 -
因此,包装对象的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。这有点像
React Hooks 中的 useRef—— 但不同的是 Vue 的包装对象同时还是响应式的数据源。有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新) -
声明数据和更新数据更加清晰。
在template下时候需要使用.value来展开数据?
- 否。虽然在
setup()中返回的是一个包装对象,但是在模版渲染的过程或者嵌套在另一个包装对象的时候,如果判断类型为包装对象,都会被自动展开为内部的值。
3. Reactive函数 -> state
reactive()返回一个没有包装的响应式对象,等同于 vue 2.6 版本以后的Vue.observable()函数。Vue.observable()提供了让data块里面的值能够在外面定义的功能。
import { reactive } from 'vue'
const object = reactive({
count: 0
})
object.count++
当一个包装对象被作为另一个响应式对象的属性引用的时候也会被自动展开:
const count = binding(0)
const obj = reactive({
count
})
console.log(obj.count) // 0
obj.count++
console.log(obj.count) // 1
console.log(count.value) // 1
count.value++
console.log(obj.count) // 2
console.log(count.value) // 2
- 以上这些关于包装对象的细节可能会让你觉得有些复杂,但实际使用中你只需要记住一个基本的规则:只有当你直接以变量的形式引用一个包装对象的时候才会需要用
.value去取它内部的值 ——在模版中你甚至不需要知道它们的存在。
4. Computed value
除了直接包装一个可变的值,我们也可以包装通过计算产生的值:
import { binding, computed } from "vue";
import dayjs from "dayjs";
export const timeHook = () => {
const time = binding(new Date().getTime());
const formatTime = computed(() => time.value, val => {
return dayjs(val).format("YYYY-MM-DD hh:mm:ss");
});
return {
time,
formatTime
}
}
-
计算值的行为跟计算属性 (
computed property) 一样:只有当依赖变化的时候它才会被重新计算。 -
computed()返回的是一个只读的包装对象,它可以和普通的包装对象一样在setup()中被返回 ,也一样会在渲染上下文中被自动展开。默认情况下,如果用户试图去修改一个只读包装对象,会触发警告。 -
双向计算值可以通过传给
computed第二个参数作为setter来创建:
import { binding, computed } from "vue";
import dayjs from "dayjs";
export const timeHook = () => {
const time = binding(new Date().getTime());
const formatTime = computed(
// read
() => time.value + 2000,
// write
val => {
return dayjs(val).format("YYYY-MM-DD hh:mm:ss");
});
return {
time,
formatTime
}
}
5. Watchers
watch()函数与旧API的 $watch一样提供了观察状态变化的能力。它的作用类似React Hooks 的 useEffect,但实现原理和调用时机其实完全不一样。
不同之处:
1. 它可以接收多种数据源:
一个返回任意值的函数
一个包装对象
一个包含上述两种数据源的数组
2. watch()函数的回调,会在创建时就执行一次,相当于2.x的 watcher 的immediate: true。
3. 默认情况下,watch()的回调在触发时,DOM总会在一个更新过的状态。
export default {
props: {
tableHeight: {
type: [String, Number],
default: 200
}
},
setup(props) {
watch(
() => props.tableHeight,
val => {
console.log("DOM render flush")
},
{
flush: 'post', // default, fire after renderer flush
flush: 'pre', // fire right before renderer flush
flush: 'sync' // fire synchronously
}
)
}
}
4. 观察多个数据源
- 任意一个数据源发生变化时,回调函数都会被触发
watch(
[valueA, () => valueB.value],
([a, b], [prevA, prevB]) => {
console.log(`a is: ${a}`)
console.log(`b is: ${b}`)
}
)
5. 停止观察
const stop = watch(...)
// stop watching
stop()
- 如果
watch()是在一个组件的setup()或是生命周期函数中被调用的,那么该watcher会在当前组件被销毁时也一同被自动停止,否则将要自己自动停止。
6. 清理副作用
-
有时候当观察的数据源变化后,我们可能需要对之前所执行的副作用进行清理。
举例来说,一个异步操作在完成之前数据就产生了变化,我们可能要撤销还在等待的前一个操作。为了处理这种情况,watcher 的回调会接收到的第三个参数是一个用来注册清理操作的函数。调用这个函数可以注册一个清理函数。清理函数会在下属情况下被调用,我们经常需要在 watcher 的回调中用async function来执行异步操作:在回调被下一次调用前 在 watcher 被停止前
watch(idValue, (id, oldId, onCleanup) => {
const token = performAsyncOperation(id)
onCleanup(() => {
// id 发生了变化,或是 watcher 即将被停止.
// 取消还未完成的异步操作。
token.cancel()
})
})
const data = value(null)
watch(getId, async (id) => {
data.value = await fetchData(id)
}
生命周期函数
- 所有现有的生命周期钩子都会有对应的
onXXX函数(只能在setup()中使用)->destroyed 调整为 unmounted:
import { onMounted, onUpdated, onUnmounted } from 'vue'
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
// destroyed 调整为 unmounted
onUnmounted(() => {
console.log('unmounted!')
})
}
}
- 现有的
vue-function-api是在install里面 设置Vue.config.optionMergeStrategies.setup,让setup函数在beforeCreate中注入。
// setup.ts
Vue.mixin({
beforeCreate: functionApiInit,
});
依赖注入
// hook.js
import { provide, binding } from 'vue'
export const randomKeyHook = () => {
const randomKey = binding(
Math.random()
.toString(36)
.substr(2)
)
provide({
randomKey
})
return {
randomKey
}
}
import { inject } from 'vue';
export default {
setup() {
const randomKey = inject('randomKey')
return {
randomKey
}
}
}
- 如果注入的是一个包装对象,则该注入绑定会是响应式的
Typescript支持
正确类型推导
-
3.0 的一个主要设计目标是增强对
TypeScript的支持。原本vue团队期望通过Class API来达成这个目标,但是经过讨论和原型开发,vue团队认为 Class 并不是解决这个问题的正确路线,基于 Class 的 API 依然存在类型问题。 -
基于函数的 API 天然对类型推导很友好,
因为TS对函数的参数、返回值和泛型的支持已经非常完备。更值得一提的是基于函数的 API 在使用 TS 或是原生 JS 时写出来的代码几乎是完全一样的。
三. 调整
标准版剔除以下选项
- el (应用将不再由
new Vue()来创建,而是通过新的createApp来创建,)
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
- 疑似要废除
.sync, 添加v-model指令参数
可替代项
data(由 setup() + binding) + reactive) 取代)
computed(由 computed 取代)
methods( 由 setup() 中声明的函数取代)
watch (由 watch() 取代)
provide/inject(由 provide() 和 inject() 取代)
mixins (由组合函数取代)
extends (由组合函数取代)
- 所有的生命周期选项 (由 onXXX 函数取代)
四. vue-router 的 Functional API 猜想
以依赖注入的形式
// main.ts
import { provideRouter, useRouter } from 'vue-router'
import router from './router'
new Vue({
setup() {
provideRouter(router)
}
})
// ... in a child component
export default {
setup() {
const { route /*, router */ } = useRouter()
const isActive = computed(() => route.name === 'myAwesomeRoute')
return {
isActive
}
}
}
以hook的形式导给上下文
import router from './router'
new Vue({
setup() {
router.use()
}
})
五. vuex 的 Functional API 猜想
采用函数转化成module
// useState, useGetter, useMutation, useAction, useModule
import { useModule } from 'vuex'
import { value, computed } from 'vue';
export const itemsModule = () => {
const items = value([])
const size = computed(() => items.value.length);
const addItem = (content) => {
items.value.push(content)
}
return {
items,
size,
addItem
}
}
useModule(itemsModule)
效仿nuxt.js,在vue-cli4中提供统一的 store 目录,如果store 目录存在,程序将自己做以下事情
引用 vuex 模块
将 vuex 模块 加到 vendors 构建配置中去
设置 Vue 根实例的 store 配置项
六. 和React的区别
-
尤大大文章刚出的时候,很多技术帖子对于
vue3.0的语法更新表示不满,他们表示,如果是大规模模仿react,倒不如去学习react,这样也是学习新的语法。 -
这样大错特错,
vue和react的实现有很大的差别,vue实际上模仿的是hooks,而不是react。
Template 机制还是没变
- vue还是将
Template和Setup分开,react则是写在了一起。
减少 GC 压力
-
vue的setup函数只在初始化之前执行一次,而React在更新数据的时候,都会执行一次render。 -
vue将hooks和mutable深度结合,在数据更新的时候,通过包装对象的obj.value,在obj的数据变更的时候,引用保持不变,只有值改变了,vue 通过 新的API Proxy监听数据的变化,可以做到 setup 函数不重新执行。而Template 的重渲染则完全继承 Vue 2.0的依赖收集机制,它不管值来自哪里,只要用到的值变了,就可以重新渲染了。 -
React Hooks 存在的问题:
所有Hooks都在渲染闭包中执行,每次重渲染都有一定性能压力,而且频繁的渲染会带来许多闭包,虽然可以依赖 GC 机制回收,但会给 GC 带来不小的压力。
v8的垃圾回收算法
从宏观上来看,V8的堆分为3部分,年轻分代/年老分代/大对象空间。对于各种类型,v8有对应的处理方式:
1. 年轻分代(短暂)
分为两堆,一半使用,一半用于垃圾清理。在堆中分配而内容不够时,会将超出生命期的垃圾对象清除出去,释放掉空间。
2. 年老分代
主要类型:
(1) 从年轻分代中移动过来的对象
(2) JIT (即时编辑器) 之后产生的代码
(3) 全局对象
标记清除和标记整理算法将可清除的垃圾对象和有效的对象区分开来,但超过分配给年老分代但空间时,V8会清除垃圾代码,形成了碎片的内存块,然后压缩内存块。当然,这个过程是走走停停的。(上述问题)
3. 大对象空间
整块分配,一次性回收。
七. 总结
-
vue3.0对 vue 的主要3个特点:响应式、模板、对象式的组件声明方式,进行了全面的更改,底层的实现和上层的api都有了明显的变化,基于Proxy重新实现了响应式,基于treeshaking内置了更多功能,提供了类式的组件声明方式。而且源码全部用Typescript重写。以及进行了一系列的性能优化。 -
取其精华,去其糟糠, vue团队希望剔除掉代码灵活性差的帽子,大胆的改变了原有的开发模式,更加亲和于
vue生态及javascript生态。vue一直在不断的进步,让开发者减少框架的约束,放飞开发者的思想。 -
感谢 vue 团队一贯的高出产率~
八. 观察
2019-07-29在github上的截的路线图

1. vue对兼容模式的开发目前完成了80%;
2. 尚未进行破坏性更新的讨论;
3. vue3.0会伴着vue-cli4的出现 -> webpack5;
4. 各生态目前正在同步更新;
5. Q3季度将要开始vue3.0的测试;
等
期待吧~
参考文章:
尤雨溪 - Vue Function-based API RFC (https://zhuanlan.zhihu.com/p/68477600)
黄子毅 - 精读《Vue3.0 Function API》(https://zhuanlan.zhihu.com/p/71667382)
vuejs - rfcs (https://github.com/vuejs/rfcs/issues)
ps: 个人公众号求加个阅读量哈,二维码如下:
