从函数式组件引发的性能思考
简介
vue
函数式组件大部分人在开发过程中用到的不多,就连官方文档位置放置的也比较隐晦,但是在我们对项目做性能优化时,却是一个不错的选择。本文将对函数式组件初始化过程做一个系统性的阐述,通过本文,你将了解到以下内容:
- 什么是函数式组件
- 函数式组件与普通组件间的差异
vue
相似性能优化点
什么是函数式组件
函数式组件即无状态组件,没有data
、computed
、watch
,也没有生命周期方法,组件中也没有this
上下文,只有props
传参。在开发中,有很多组件仅仅只用到了props
和插槽,这部分组件就可以提炼为函数式组件。借用官网demo
,最简单的函数式组件如下:
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
}
})
函数式组件与普通组件间的差异
组件实例化过程大致分为四步,状态初始化 --> 模板编译 --> 生成VNode
--> 转换为真实DOM
。接下来对比普通组件与函数式组件常用配置项,比较下差异。
功能点名称 | 普通组件 | 函数式组件 | 描述 |
---|---|---|---|
vm | Y | N | 组件作用域 |
hooks | Y | N | 生命周期钩子 |
data | Y | N | 数据对象声明 |
computed | Y | N | 计算属性 |
watch | Y | N | 侦听器 |
props | Y | Y | 属性 |
children | Y | Y | VNode 子节点的数组 |
slots | Y | Y | 一个函数,返回了包含所有插槽的对象 |
scopedSlots | Y | Y | 作用域插槽的对象 |
injections | Y | Y | 依赖注入 |
listeners | Y | Y | 事件监听 |
parent | Y | Y | 对父组件的引用 |
从上表中可以看出,普通组件与函数式组件最大的差别在于函数式组件没有独立作用域,没有响应式数据声明。没有独立作用域,会有以下优点:
-
没有组件实例化(
new vnode.componentOptions.Ctor(options)
),函数式组件获取VNode
仅仅是普通函数调用- 无公共属性、方法拷贝
- 无生命周期钩子调用
-
函数式组件直接挂载到父组件中,缩短首次渲染、
diff
更新路径- 函数式组件在父组件生成
VNode
时,函数式组件render
方法会被调用,生成VNode
挂载到父组件children
中,patch
阶段可直接转换成真是DOM
,普通组件则在createElm
时,走组件初始化流程。 diff
更新时,函数式组件调用render
,直接创建普通VNode
,而普通组件创建的VNode
的是包含组件作用域的,diff
操作时,还有额外调用updateChildComponent
更新属性、自定义事件等,调用链路会比较长。
- 函数式组件在父组件生成
vue性能优化点
函数式组件带来的性能提升主要体现在缩短渲染路径与减少组件嵌套层级,前者与浏览器重绘回流有异曲同工之处,后者可以降低时间复杂度。
无论何种性能优化,能从代码层面做优化的,无疑是代价最小的,虽然有时效果不是很明显,但是积少成多。在vue
中,有不少与上述相似的点,可以提升代码执行效率。
合理声明data
中数据,确保data
中声明数据都是必须的
很多时候有一些数据没必要声明在data
中,比如需要组件内共享,但不需要响应式处理的数据。data
中的数据,对象都会对其深度优先用Object.defineProperty
声明,数组也会拦截基本操作方法。不必要的声明会造成无意义的数据劫持。
合理使用computed
与watch
computed
与watch
最大的区别在于computed
是惰性加载的。惰性加载主要体现在两个方面:
- 依赖状态发生改变时,不会立即触发,只是改变当前
Watcher
实例的dirty
属性值为true
- 当对计算属性值取操作时,当且仅当
watcher.dirty === true
时,才会触发计算
以上两点特性,能够避免一些不必要的代码执行,具体代码如下所示:
// src\core\instance\state.js
function createComputedGetter (key) {
return function computedGetter () {
// 获取实例上的computed属性的watcher实例
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 当且仅当computed依赖属性发生变化 && 对计算属性进行取操作,才会调用Watcher的update方法,将dirty置为true
if (watcher.dirty) {
// 调用get方法,获取到computed的值
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
// src\core\observer\watcher.js
update () {
// computed watcher lazy === true
if (this.lazy) {
// 标记懒执行是否可执行状态,false不执行计算属性计算
this.dirty = true
} else if (this.sync) { // 同步执行
this.run()
} else {
// 将当前watcher放入到watcher队列
queueWatcher(this)
}
}
v-for绑定key值
v-for
循环定义key
值目的是便于精准找到diff
比对节点,避免一些无意义的比对。
普通diff: 从头尾开始,新旧节点头尾分别比较,游标向中间靠拢,当且仅当一个节点遍历结束后,diff
流程结束
带有key值diff: 根据key
值维护一个hash
表,每次循环精准定位到更新目标节点,当且仅当一个节点遍历结束后,diff
流程结束
思考
在vue
中,很多性能优化点都是缩短代码执行路径,尤其在存在大量计算逻辑中,性能的提升会有肉眼可见的效果。实际开发中,也有不少场景可以用到此类优化方法,举个最简单的例子,关键词高亮匹配。实现这个操作,需要以下几步:
- 获取匹配关键词,将关键词进行格式化(对正则表达式中有意义的字符串进行转义)
- 动态生成匹配的正则表达式
- 根据正则表达式进行
replace
操作
有些时候,第一, 二步我们可以省略,直接执行第三步即可,因为输入关键字可能存在相同的,因此我们可以将字符串与正则表达式缓存在Map
中,下次匹配时,如果存在缓存,直接从缓存中拿即可。
vue
模板编译用到的就是这个特性,每次会把编译的模板字符串作为key
值,render
方法作为value
,缓存起来,如果遇到一样的模板,可以省去编译流程,带来一定的性能提升。
小结
养成良好的编码习惯,对于个人能力,也是一个不错的提升。