整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

开发者成长Vue.js 中有哪些性能陷阱

内心深处对游戏的热爱,让我一直渴望能自己制作一些电子游戏。几个月前我开始将这种梦想变为现实,并第一次参加了全球游戏大赛(Global Game Jam)。我和我的团队使用 Vue.js 构建了一个名为“ ZeroDaysLeft ”的游戏,其形式是 Web 端的单页面应用程序。这款游戏的主题是环境保护,我们考虑到商业活动对地球环境的影响,希望就这个话题做一些有益的探讨。使用 Vue.js 制作的游戏并不多。我的团队迟到了一天,然后用猜拳的方式选择了我们要用的框架;我们飞快地写完了代码,并在周末结束时做出了游戏的可运行版本。在本地测试时一切都很顺利。自然,我们为自己第一次写出来的游戏作品感到自豪,并希望与世界分享它。

可是问题出现了——当我们构建好应用并开始查询域时,内存占用爆表了。它几乎没法正常运行,不管换什么机器都会卡住不动,即使在强大的基于 Intel i7 处理器的系统上程序也会崩溃。游戏大赛的时间限制把我们拉回了现实,我们决定搁置生产性能问题,这样起码我们能做出一款能在自己的设备上运行的完整游戏。就像大部分的“已完成”项目一样,第二天我们就把它抛在脑后了。

但我自己没法释怀。它一直困扰着我。问题是出在 Vue.js 上吗?是 Netlify 吗?还是因为我们的取巧代码?我必须找出答案。

调查性能下降的原因

我首先使用 Lighthouse 进行了快速测试。所幸 Firefox 为此提供了一个浏览器插件。下面就是我得到的结果。

89%的数字挺不错的。实际上,与许多流行的网站相比,这个表现相当出色。这个测试指出了一些潜在问题,例如速度指数和第一次有意义且有内容的绘制步骤等。从理论上讲,解决这些问题会进一步提高分数,但不一定能解决应用面临的严重性能问题。

我们的游戏中有一些图像和音频素材资源,但是两者都不至于让游戏卡死在那里。我们也可以对这些已经优化过的资源再过度优化一遍,但这可能根本就无济于事。

这个测试无法让我们真正找出可能导致这一性能问题的原因。于是我开始想:“该不会是 Vue 的问题吧?”这种想法会冒出来也没什么理由,但要是不检查一下就是蠢了。我检查了已部署站点的控制台,结果空白一片。但警告往往不会在生产中显示。当我在本地进行相同操作时,一堆 Vue 警告让我吃了一惊。

像大多数开发人员一样,我对控制台警告没那么在意,觉得它们只是警告,而不代表错误;所以我一般会把注意力集中在其他地方。或许消除这些警告可以解决我的生产问题,我决定深入研究每个问题并修复它们。

所有这些警告均来自我创建的、用来显示名为 Cards.vue 选项的组件,因此这个组件可能需要大量重写。

我决定按顺序解决这些控制台警告。

> [Vue warn]: Avoid using non-primitive value as key, use string/number value instead.
 found in 
 ---> <Cards> at src/components/Cards.vue

Vue.js 有很多指令,让我们能更直观地使用框架,比如说 v-for 就可以快速将数组渲染为列表。使用它时,我们需要一个 :key 才能有效地重渲染组件。但我们将一个对象用作了一个键,这是非原始值,因此导致了这个错误。我决定将 index.description 用作一个新键,因为它是一个字符串,并且在值发生更改时可以更好地重新渲染。

> [Vue warn]: Duplicate keys detected: '[object Object]'. This may cause an update error.
found in
---> <Cards> at src/components/Cards.vue

将 :key 更改为一个字符串(index.description)来解决上一个错误,就能解决这个重复键的错误。我们只能将字符串类型写入 DOM,因此当我们传递一个要渲染的对象时,该对象将转换为等效的字符串(即 [object Object]);并且因为这以前是我们的键,所以每个对象都将转换为 [object Object](除非对象有不同的值),进而会出现重复键警告。现在既然键不是对象,警告就会消失,效率也会提升。

> [Vue warn]: You may have an infinite update loop in a component render function.
found in
---> <Cards> at src/components/Cards.vue

就一个非常模糊的警告来说,这个警告似乎是最重要的:无限循环意味着内存消耗。这条消息并没有告诉我们可能出了什么问题,但它确实暗示了问题与组件中的 render 函数有关。也许是因为我们写的代码比较取巧,因此触发了不间断的更新,并占用了大量的计算能力,以至于使浏览器和设备崩溃。

这条警告至少告诉我们要检查 Cards.vue,所以我的第一个想法是检查组件中的反应属性,因为这可能会导致错误。反应属性在更改后会触发重新渲染。

我们正在显示 index.days 和 index.description 中的数据。但我们不会更改这些数据,我们从 cardInfo 数组获得 index。

我们使用这段代码对数组中的元素进行随机排序,然后将前四个元素显示为玩家选择的选项。当用户单击一个选项时将调用 effects() 函数,它除了会计算一个动作如何影响游戏状态外,还使用 cardInfo 上的拼接原型删除前四个元素。

在 Vue 这种使用虚拟 DOM 的框架里,用上诸如 cardInfo 之类的反应属性后,每当数据属性的值更改时都会触发重新渲染。在我们的应用里,我们会直接使用 sort() 原型来更改它,然后删除元素来重新排序。所有这些都会触发“无限”的重新渲染,从而引发警告。

我决定更改数据过滤的逻辑,并停止对反应属性 cardInfo 的多次更改。我安装了 lodash.shuffle 并定义了一个计算属性 shuffledList(),它将创建一个名为 list 的 cardInfo 副本。我对其应用了随机排序操作,并返回了一个“frozen”结果,然后拆分开来显示四张卡片。我们使用了 Object.freeze(),它将使我们返回的对象不可变,从而完全停止了所有重新渲染操作。

至此,问题解决了。

掉进框架的坑

老实说,当我刚开始调查性能下降原因的时候,还觉得我肯定要优化很多资源才能解决问题。最后这个结果说明,在使用许多框架抽象时我们都必须非常小心——特别是在 Vue 中更是如此,只有在必要时才使用某条指令,而且用法一定不能出错,因为它们绝对有自己的代价。

这还让我开始思考自己做过的其他工作,其中应用程序可能会因为框架而出现不必要的性能问题。大多数现代的前端框架都有很多抽象,使我们能更轻松地为 Web 制作应用程序。但我们应该牢记一点,那就是使用这些东西可能会引发潜在的性能问题。

我经常使用 Vue.js,所以决定探索一些我以前用过的指令,以前我用这些指令的时候完全没考虑过它们可能对应用程序带来的性能影响。其中有三条非常流行的指令进入了我的视线。

  • v-if 和 v-show

这两条指令都是用来有条件地渲染元素的,但是它们背后的工作机制却大不相同,因此用法也大相径庭。v-if 一开始不会渲染组件,而只在条件为真时才渲染组件。这意味着当你多次切换组件的可见性时,就会不断重新渲染。如果你要多次更改组件的可见性,那就不要使用这个功能。这会影响你的性能。

v-show 是一个很好的替代品。不管你是否启用 CSS 都会渲染你的组件,但是只会根据条件是 true 还是 false 来决定组件是否可见。这种方法确实有其缺点,因为它不会将非必要组件的渲染推迟到你需要它们在屏幕上实际出现的时候。如果你的初始渲染没那么复杂,那么它就很合适。

  • v-for

这条指令通常用来从数组中渲染列表。它有一个特殊的语法,形式为 item in list,其中 list 是源数据数组,而 item 是要迭代的数组元素的别名。默认情况下,Vue 在源数据数组上添加 watchers,每当发生更改时它就会触发重新渲染。这种持续的重新渲染可能会对应用程序性能产生不利影响。如果你只想可视化对象,那么 Object.freeze() 是一个很好的解决方案,可以大大提高性能。但是请务必记住,你将无法更新组件或编辑对象数据。

在这个研究过程中我还意识到,Lighthouse 可能检查的是以更直接的方式影响用户体验的应用性能指标,所以接下来我的疑问就是如何跟踪服务器上的应用程序性能。

我们是不是太依赖直觉,是不是在假设开发人员知道自己在做什么,假设他们遵循的是最佳实践?不管怎样,这次经历让我对单页应用程序的性能产生了不同的看法。大家可以在 GitHub 上查看上述项目的存储库,也欢迎大家在 Twitter 上和我打招呼。

JavaScript 中,Vue 是一种流行的前端框架,它具有简洁的语法、高效的渲染能力和rich的组件库。Vue 的设计目标是为开发者提供一种简单易用的方式来构建用户界面,同时保持高性能和可扩展性。


Vue 的核心库是极小的,只关注视图层,而且可以与其他库或框架很好地集成。这使得 Vue 适合用于构建单页应用程序(SPA)和Progressive Web Apps(PWA)。

Vue 的组件系统是它的核心特性之一,允许开发者使用小型、独立且易于重用的代码块来构建用户界面。Vue 组件可以传递数据和方法,同时还支持两向数据绑定和异步组件加载。

Vue 还提供了一套丰富的直接指令,例如v-if、v-for和v-model,使得开发者能够方便地操作 DOM 元素。

Vue 的响应式系统是它的另一个重要特性,允许开发者根据数据的变化来实时更新 DOM,从而达到高效的数据驱动开发。

Vue 还提供了一个强大的工具链,包括官方的命令行界面(CLI)、开发工具(DevTools)和丰富的社区支持。

总之,Vue 是一个优秀的前端框架,适合于构建富互动的用户界面,同时具有简单易学的特点,并且拥有强大的社区和文档支持。

ue的使用相信大家都很熟练了,使用起来简单。但是大部分人不知道其内部的原理是怎么样的,今天我们就来一起实现一个简单的vue

Object.defineProperty()

实现之前我们得先看一下Object.defineProperty的实现,因为vue主要是通过数据劫持来实现的,通过get、set来完成数据的读取和更新。

var obj = {name:'wclimb'}
var age = 24
Object.defineProperty(obj,'age',{
 enumerable: true, // 可枚举
 configurable: false, // 不能再define
 get () {
 return age
 },
 set (newVal) {
 console.log('我改变了',age +' -> '+newVal);
 age = newVal
 }
})
 
> obj.age
> 24
 
> obj.age = 25;
> 我改变了 24 -> 25
> 25

从上面可以看到通过get获取数据,通过set监听到数据变化执行相应操作,还是不明白的话可以去看看Object.defineProperty文档。

流程图



image

html代码结构<div id="wrap">
 <p v-html="test"></p>
 <input type="text" v-model="form">
 <input type="text" v-model="form">
 <button @click="changeValue">改变值</button>
 {{form}}
</div>

js调用

new Vue({
 el: '#wrap',
 data:{
 form: '这是form的值',
 test: '<strong>我是粗体</strong>',
 },
 methods:{
 changeValue(){
 console.log(this.form)
 this.form = '值被我改变了,气不气?'
 }
 }
 })

Vue结构

class Vue{
 constructor(){}
 proxyData(){}
 observer(){}
 compile(){}
 compileText(){}
}
class Watcher{
 constructor(){}
 update(){}
}
  • Vue constructor 构造函数主要是数据的初始化
  • proxyData 数据代理
  • observer 劫持监听所有数据
  • compile 解析dom
  • compileText 解析dom里处理纯双花括号的操作
  • Watcher 更新视图操作

Vue constructor 初始化

class Vue{
 constructor(options = {}){
 this.$el = document.querySelector(options.el);
 let data = this.data = options.data; 
 // 代理data,使其能直接this.xxx的方式访问data,正常的话需要this.data.xxx
 Object.keys(data).forEach((key)=> {
 this.proxyData(key);
 });
 this.methods = options.methods // 事件方法
 this.watcherTask = {}; // 需要监听的任务列表
 this.observer(data); // 初始化劫持监听所有数据
 this.compile(this.$el); // 解析dom
 }
}

上面主要是初始化操作,针对传过来的数据进行处理

proxyData 代理data

class Vue{
 constructor(options = {}){
 ......
 }
 proxyData(key){
 let that = this;
 Object.defineProperty(that, key, {
 configurable: false,
 enumerable: true,
 get () {
 return that.data[key];
 },
 set (newVal) {
 that.data[key] = newVal;
 }
 });
 }
 }

上面主要是代理data到最上层,this.xxx的方式直接访问data

observer 劫持监听

class Vue{
 constructor(options = {}){
 ......
 }
 proxyData(key){
 ......
 }
 observer(data){
 let that = this
 Object.keys(data).forEach(key=>{
 let value = data[key]
 this.watcherTask[key] = []
 Object.defineProperty(data,key,{
 configurable: false,
 enumerable: true,
 get(){
 return value
 },
 set(newValue){
 if(newValue !== value){
 value = newValue
 that.watcherTask[key].forEach(task => {
 task.update()
 })
 }
 }
 })
 })
 }
 }

同样是使用Object.defineProperty来监听数据,初始化需要订阅的数据。

把需要订阅的数据到push到watcherTask里,等到时候需要更新的时候就可以批量更新数据了。下面就是;

遍历订阅池,批量更新视图。

set(newValue){
 if(newValue !== value){
 value = newValue
 // 批量更新视图
 that.watcherTask[key].forEach(task => {
 task.update()
 })
 }
}

compile 解析dom

class Vue{
 constructor(options = {}){
 ......
 }
 proxyData(key){
 ......
 }
 observer(data){
 ......
 }
 compile(el){
 var nodes = el.childNodes;
 for (let i = 0; i < nodes.length; i++) {
 const node = nodes[i];
 if(node.nodeType === 3){
 var text = node.textContent.trim();
 if (!text) continue;
 this.compileText(node,'textContent') 
 }else if(node.nodeType === 1){
 if(node.childNodes.length > 0){
 this.compile(node)
 }
 if(node.hasAttribute('v-model') && (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA')){
 node.addEventListener('input',(()=>{
 let attrVal = node.getAttribute('v-model')
 this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'value'))
 node.removeAttribute('v-model')
 return () => {
 this.data[attrVal] = node.value
 }
 })())
 }
 if(node.hasAttribute('v-html')){
 let attrVal = node.getAttribute('v-html');
 this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML'))
 node.removeAttribute('v-html')
 }
 this.compileText(node,'innerHTML')
 if(node.hasAttribute('@click')){
 let attrVal = node.getAttribute('@click')
 node.removeAttribute('@click')
 node.addEventListener('click',e => {
 this.methods[attrVal] && this.methods[attrVal].bind(this)()
 })
 }
 }
 }
 },
 compileText(node,type){
 let reg = /\{\{(.*?)\}\}/g, txt = node.textContent;
 if(reg.test(txt)){
 node.textContent = txt.replace(reg,(matched,value)=>{
 let tpl = this.watcherTask[value] || []
 tpl.push(new Watcher(node,this,value,type))
 if(value.split('.').length > 1){
 let v = null
 value.split('.').forEach((val,i)=>{
 v = !v ? this[val] : v[val]
 })
 return v
 }else{
 return this[value]
 }
 })
 }
 }
 }

这里代码比较多,我们拆分看你就会觉得很简单了

首先我们先遍历el元素下面的所有子节点,node.nodeType === 3 的意思是当前元素是文本节点,node.nodeType === 1 的意思是当前元素是元素节点。因为可能有的是纯文本的形式,如纯双花括号就是纯文本的文本节点,然后通过判断元素节点是否还存在子节点,如果有的话就递归调用compile方法。下面重头戏来了,我们拆开看:

if(node.hasAttribute('v-html')){
 let attrVal = node.getAttribute('v-html');
 this.watcherTask[attrVal].push(new Watcher(node,this,attrVal,'innerHTML'))
 node.removeAttribute('v-html')
}

上面这个首先判断node节点上是否有v-html这种指令,如果存在的话,我们就发布订阅,怎么发布订阅呢?只需要把当前需要订阅的数据push到watcherTask里面,然后到时候在设置值的时候就可以批量更新了,实现双向数据绑定,也就是下面的操作

that.watcherTask[key].forEach(task => {
 task.update()
})

然后push的值是一个Watcher的实例,首先他new的时候会先执行一次,执行的操作就是去把纯双花括号 -> 1,也就是说把我们写好的模板数据更新到模板视图上。

最后把当前元素属性剔除出去,我们用Vue的时候也是看不到这种指令的,不剔除也不影响

至于Watcher是什么,看下面就知道了

Watcher

class Watcher{
 constructor(el,vm,value,type){
 this.el = el;
 this.vm = vm;
 this.value = value;
 this.type = type;
 this.update()
 }
 update(){
 this.el[this.type] = this.vm.data[this.value]
 }
}

之前发布订阅之后走了这里面的操作,意思就是把当前元素如:node.innerHTML = '这是data里面的值'、node.value = '这个是表单的数据'

那么我们为什么不直接去更新呢,还需要update做什么,不是多此一举吗?

其实update记得吗?我们在订阅池里面需要批量更新,就是通过调用Watcher原型上的update方法。

最后

以下是总结出来最全前端框架视频,包含: javascript/vue/react/angualrde/express/koa/webpack 等学习资料。

【领取方式】

关注头条 前端全栈架构丶第一时间获取最新前端资讯学习

手机用户可私信关键词 【前端】即可获取全栈工程师路线和学习资料!