前言
去年前端界最轰动的事无非是React Hook的发布,上到react-router、react-redux等生态库,下到React应用开发者都从class component开发方式积极拥抱Hook。那今年我认为最值得关注的是Vue3.0,目前我们可以从vue-next看到Vue3.0以及vuex、vue-router等生态的开发进度,大概7、8月份Vue3.0正式版本就要落地,到时又会引起前端界的广泛讨论,这里我整理了一波Vue3的亮点,供大家参考。
Composition API
什么是composition api?先补一波英语知识,composition是构成、作文的意思,动词是compose,看到compose估计很多人会想到koa、redux等库的源码中有一个compose核心函数,这个函数像桥梁一样连接中间件(middleware)的调用,是构成这些优秀库中间件机制的关键!想想compose都那么牛,那composition api可以带给我们什么?能解决什么问题?这里我们从下面几个现象入手,理解composition api的设计理念。
-
现象一:当你维护前同事长达1000多行的祖传代码,为理解代码含义在不断的
template、data、methods等代码中痛苦跳转时 -
现象二:当你苦苦找不到
this.xxx()方法定义,却不敢删掉,最终在各种mixin文件中找到它的真身时
这个时候真的需要composition api这样的开发方式来组织你的代码了。
功能分割
以组合式 API 征求意见稿中的Vue CLI UI 文件浏览器为例,这个组件中有这样几个功能:
-
追踪监听当前文件夹的状态并展示其中的内容
-
处理文件夹的操作(打开、关闭、刷新...)
-
处理新建文件夹的创建
-
是否只展示收藏文件夹
-
是否只展示隐藏文件夹
-
处理当前工作目录的变化
就单个功能而言,它的代码所在位置比较分散(功能所需要的属性在data或props中,处理数据的方法在methods中),这势必要求开发者在完成或阅读代码时上下反复跳转,同一个功能的代码不够聚合,那使用composition api要怎么写?继续引用组合式 API 征求意见稿中创建新文件夹代码例子:
function useCreateFolder(openFolder) {
// 原来的数据 property
const showNewFolder = ref(false)
const newFolderName = ref('')
// 原来的计算属性
const newFolderValid = computed(() => isValidMultiName(newFolderName.value))
// 原来的一个方法
async function createFolder() {
if (!newFolderValid.value) return
const result = await mutate({
mutation: FOLDER_CREATE,
variables: {
name: newFolderName.value,
},
})
openFolder(result.data.folderCreate.path)
newFolderName.value = ''
showNewFolder.value = false
}
return {
showNewFolder,
newFolderName,
newFolderValid,
createFolder,
}
}
使用composition api有几个亮点
- 整个函数就是一个功能
- 函数包含
创建新文件夹所依赖的数据和逻辑 - 函数完全独立,功能可以复用
再通过一张图更直接的与options api进行对比:
这里相同的色块代表相同的功能,composition api(组合式api)让开发者就像搭积木一样将独立的功能一层一层组合成一个组件,所有的逻辑和功能都一目了然,但基于options api不能让我们这样组织代码,造成一个功能中逻辑的分散。
逻辑复用
当两个或多个组件的逻辑相同或相似时,在vue2.x中我们考虑用mixin、HOC(高阶组件,Vue较少用到)、slot插槽来做逻辑复用。但这几种方式都有各自的弊端:
- 不知道代码引用来源
- 与引入的组件属性或方法命名冲突
HOC和slot需要额外的有状态的组件实例,从而使得性能有所损耗。
使用composition api能做到更清晰的逻辑复用,继续引用组合式 API 征求意见稿中追踪鼠标位置的例子:
import { ref, onMounted, onUnmounted } from 'vue'
export 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 './mouse'
export default {
setup() {
const { x, y } = useMousePosition()
// 其他逻辑...
return { x, y }
},
}
这里:
- 所有的数据来源都非常清晰
- 可以通过解构重命名,不存在命名冲突
- 不再需要仅为逻辑复用而创建的组件实例
告别$set
Vue2.x使用Object.defineProperty拦截数据实现响应式系统,到了Vue3.0,响应式系统的核心api使用了Proxy。我们先看Vue2.x中的一个例子
let vm = new Vue({
data() {
return {
name: 'jay'
}
}
})
vm.age = 20 // 并不会触发vue的响应系统
Vue2.x对新增属性是无感知的,依赖收集发生在初始化组件的过程中,Vue不能响应后来新增的属性,这是Object.defineProperty天生的特性:
const object1 = {};
// 只能对对象上特定属性做数据拦截
Object.defineProperty(object1, 'property1', {
get() {
return object1['property1'];
}
set(value) {
object1['property1'] = value;
}
});
Vue2.x为了让新增的数据具有响应性,加入$set api来兼容无法响应新增属性的情况。对于Proxy api,它天生能够拦截所有的属性。
const object1 = {};
let p = new Proxy(object1, {
get(target, prop, receiver) {
},
set(target, prop, value) {
console.log(target); // {}
console.log(prop); // property1
console.log(value); // 1
target[prop] = value;
}
})
// 能拦截到新增的属性
p.property1 = 1;
Object.defineProperty与Proxy的区别:
-
Object.defineProperty针对对象上特定属性(不能拦截新增属性),Proxy针对handler对象(不论你是否为新增属性) -
Proxy除了get、set外还有其他多种操作符 -
Proxy不兼容IE 11,相当于IE家族不能使用Vue3.0的应用了(也许未来Vue或社区有优雅降级的方案)
有了Proxy,就不用考虑新增属性的响应行为了,是时候要跟$set说声再见了👋。
VDom的性能优化
尤大在此前直播中表示,Vue3.0在性能优化后VDom的更新(计算diff)性能提升1.3-2倍,SSR的速度提升了2-3倍。来看看Vue3在VDom优化实现。
静态标记
<div id="app">
<h1>我来组成静态节点</h1>
<p>{{name}}</p>
</div>
可以在这里查看编译结果:
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", { id: "app" }, [
_createVNode("h1", null, "我来组成静态节点"),
_createVNode("p", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}
// Check the console for the AST
这里h1是静态节点,p为动态节点,在Vue2.x中若name发生变化,整个模板都需要重新渲染一遍,Vue3.0在p标签vdom多传了参数1 /* TEXT */,意味着在diff时略过静态节点,只追踪有这参数的动态节点的更新,Vue3.0源码中还有其他类型的标记位:
export const enum PatchFlags {
TEXT = 1,// 表示具有动态textContent的元素
CLASS = 1 << 1, // 表示有动态Class的元素
STYLE = 1 << 2, // 表示动态样式(静态如style="color: red",也会提升至动态)
PROPS = 1 << 3, // 表示具有非类/样式动态道具的元素。
FULL_PROPS = 1 << 4, // 表示带有动态键的道具的元素,与上面三种相斥
HYDRATE_EVENTS = 1 << 5, // 表示带有事件监听器的元素
STABLE_FRAGMENT = 1 << 6, // 表示其子顺序不变的片段(没懂)。
KEYED_FRAGMENT = 1 << 7, // 表示带有键控或部分键控子元素的片段。
UNKEYED_FRAGMENT = 1 << 8, // 表示带有无key绑定的片段
NEED_PATCH = 1 << 9, // 表示只需要非属性补丁的元素,例如ref或hooks
DYNAMIC_SLOTS = 1 << 10, // 表示具有动态插槽的元素
}
静态节点提升
<div id="app">
<h1>我来组成静态节点</h1>
<p>{{name}}</p>
</div>
编译结果:
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = { id: "app" }
const _hoisted_2 = /*#__PURE__*/_createVNode("h1", null, "我来组成静态节点", -1 /* HOISTED */)
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", _hoisted_1, [
_hoisted_2,
_createVNode("p", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
]))
}
// Check the console for the AST
使用静态提升时,所有静态节点被提升到render方法外,这表明这些节点只会在初始化中创建一次,在更新时进行复用。
添加事件缓存
<div id="app">
<p @click="onClick">hello</p>
</div>
编译结果:
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createBlock("div", { id: "app" }, [
_createVNode("p", {
onClick: _cache[1] || (_cache[1] = ($event, ...args) => (_ctx.onClick($event, ...args)))
}, "hello")
]))
}
// Check the console for the AST
编译后会产生一个内联函数,使用内联函数对绑定事件做缓存,更新时事件处理器没有发生变化,则这个节点被认为一个静态节点。
Typescript
Vue2.x是基于options(选项)的框架,也就是用object去编写组件,但typescript是类型检查能力范围限于class/function,因此Vue2.x中我们使用Vue + typescript + vue-class-component + vue-property-decorator 的方式开发Vue应用。但typescript在Vue3.0源码中占比90%以上,Vue3.0将对tsx、class component等有更好的支持。
Tree Shaking
Vue3.0做到了按需引入,更好支持tree shaking,有时候并不需要Vue全部的功能,打包时可以将无用的代码剪掉
Fragment
Vue3.0支持模板添加多个根节点,意味着render函数也可以返回数组了
结束
好了,Vue3.0的新特性还是很多的,当然该学的还是得学,别做Vue2.x钉子户了🐶