改造mixins,我释放了20倍终端性能

1,920 阅读4分钟

前言

彦祖们,今天分享一个笔者遇到的真实项目场景, 做了一个项目肿瘤切除术,直接把性能提升 20 倍

认真看完,帮你简历上亮点。阅读本文前,默认彦祖们已经了解 vue.mixins

眼见为实,彦祖们先看下优化前后的性能对比

  • 优化前 WechatIMG142.jpg

  • 优化后 WechatIMG143.jpg

项目背景

开始之前,让我们来简述一下项目背景

笔者的项目业务是工业互联网,简而言之就是帮助工厂实现数字化

其中的终端叫做工控机(性能较我们 PC 会相差几十倍),理解一下 就是工业操控机器,说白了就是供工人操作业务的一个终端

类似于我们去医院自助挂号/打印报告的那种终端

技术栈

  • vue2

问题定位

在笔者接手项目(历时三年的老项目,实在是非常痛苦)的时候,发现其中一个页面过一段时间就奔溃无响应,导致现场屡次投诉

这种依附于终端的界面属实不好调试

经过各种手段摸排,我们定位到了问题所在

其实就是 vue mixins 内容部添加了重复的 websocket 事件监听器

导致页面重复渲染,接口重复调用

在线 Demo

老规矩先上 demo

stackblitz.com/edit/vue-74…

现场场景复现

下面笔者简单模拟一下线上的真实代码场景

代码结构

因为线上的组件结构非常复杂,子组件数量达到了 20 个甚至 30 个以上

笔者就抽象了主要问题,模拟了一下 5 个子组件的情况

image.png

总结一下图中的两个关键信息

1.child 子组件可能 会被多个父组件引用

2.child 子组件的层级是不固定

代码目录结构大致如下

  • Parent.vue // 主页面
  • mixins
    • index.js // 核心的 mixin 文件
  • component
    • child1.vue // 子组件
      • grandchild1.vue // 孙子组件
    • child2.vue
    • child3.vue
    • child4.vue
    • child5.vue

代码说明

接下来让我们简单来看下项目中各个代码文件的主要作用

  • mixins.js

剥离业务逻辑后,核心就是增加了一个onmessage事件监听器

最后通过各自子组件自定义的onWsMessage去处理对应的业务逻辑

export const wsMixin = {
  created() {
    window.addEventListener('onmessage', this.onmessage)
  },
  beforeDestory() {
    window.addEventListener('onmessage', this.onmessage)
  },
  methods: {
    // ... 省略其他业务方法
    async onmessage(e) {
      // 开始处理业务逻辑,这里用 fetch 接口代替,当然实际业务比这复杂太多
      fetch(`https://api.example.com/${Date.now()}`)
      // ...
      
      // 开始处理对应的业务逻辑
      this.onWsMessage(e.detail)
    }
  }
}

  • Parent.vue

引入子组件,并且模拟了 websocket 推送消息行为

<template>
  <div id="app">
    <Child1 />
    <Child2 />
    <Child3 />
    <Child4 />
    <Child5 />
  </div>
</template>
<script>
import Child1 from './components/Child1.vue'
import Child2 from './components/Child2.vue'
import Child3 from './components/Child3.vue'
import Child4 from './components/Child4.vue'
import Child5 from './components/Child5.vue'
import { wsMixin } from './mixins'
// 模拟 websocket 1s 推送一次消息
setInterval(() => {
  const event = new CustomEvent('onmessage', {
    detail: { currentTime: new Date() }
  })
  window.dispatchEvent(event)
}, 1000)

export default {
  name: 'Parent',
  components: { Child1, Child2, Child3, Child4, Child5 },
  mixins: [wsMixin],
  methods: {
    onWsMessage(data) {
      console.log('parent onWsMessage', data)
    }
  }
}
</script>
  • child.vue

child.vue 核心逻辑都非常相似,此处以 child1.vue 举例,其他不再赘述

<template>
  <div>
    child1
  </div>
</template>
<script>
import { wsMixin } from '../mixins'
export default {
  mixins: [wsMixin],
  methods: {
    onWsMessage(data) {
      console.log('child1 onWsMessage', data)
      // 处理业务逻辑
    }
  }
}
</script>

现场预览

彦祖们,让我们来看一下模拟的现场

我们期望的效果应该是 onmessage 收到消息后,会发送一次请求

但是目前来看显然是发送了 6 次请求

实际线上更为复杂可能高达 20 倍,30 倍...这是非常可怕的事

2023-11-26 11.13.35.gif

开始动刀

接下来让我们一步步来切除这个监听器肿瘤,让终端变得更轻松

定位重复的监听器

现象已经比较明显了

彦祖们大致能猜想到是因为绑定了过多的 onmessage 监听器导致过多的重复逻辑.

我们可以借助 getEventListeners API 来看下指定对象的绑定事件

这个 API 只能在浏览器中调试,无法在代码中使用

chrome devTools 执行一下 getEventListeners(window)

很明显有 6 个重复的监听器(1个 Parent.vue + 5个 Child.vue)

image.png

getEventListeners 介绍

彦祖们这个 API 对于事件监听类的代码优化还是蛮有效的

我们还可以右键 listener 定位到具体的赋值函数 2023-11-26 11.25.18.gif

切除重复的监听器

目标已经很明确了,我们只需要一个 onmessage 监听器就足够了

那么把 child.vuemixins的监听器移除不就好了吗?

彦祖们可能会想到最简单的方案,就是把 mixins 改成函数形式,通过传参判断是否需要添加监听器

但是因为实际业务的复杂性,上文中也提到了 mixins 同时也被其他多个文件所引用,最终这个方案被 pass 了

那么我们可以反向思考一下,只给 Parent.vue 添加监听器

需要一个辅助函数来判断是否为 Parent.vue,直接看代码吧

const isSelfByComponentName = (vm, componentName) => {
    // 这里借助了 element 的思路,新增了 componentName 属性,不影响 name 属性
  return vm.$options.componentName === componentName
}

让我们来测试一下,很完美,为什么第一个 true 就能确定是父组件呢?

如果不了解的彦祖,建议你看下父子组件的加载渲染顺序

image.png

此时的

  • mixins.js
const isSelfComponentName = (vm, componentName) => {
  return vm.$options.componentName === componentName
}

export const wsMixin = {
  created() {
    console.log('__SY__🍦 ~ created ~ isSelfComponentName', isSelfComponentName(this, 'Parent'))
    if (isSelfComponentName(this, 'Parent')) window.addEventListener('onmessage', this.onmessage)
  },
  beforeDestory() {
    window.removeEventListener('onmessage', this.onmessage)
  },
  methods: {
    async onmessage(e) {
      // 开始处理业务逻辑,这里用 fetch 接口代替
      fetch(`https://api.example.com/${Date.now()}`)
      console.log('__SY__🍦 ~ onmessage ~ e:', e)
      // 省略处理统一逻辑....
      
      // 开始处理对应的业务逻辑
      this.onWsMessage(e.detail)
    }
  }
}

如何进行子组件的消息分发?

前面我们已经把多余的监听器给切除了,网络请求的确变成了 1s一次, 但是新问题随即出现了

2023-11-26 12.18.50.gif

我们会发现此时只有Parent.vue触发了onWsMessage

child.vue的对应的 onWsMessage 并没有触发

那么此时的核心问题就是 如何从父组件的监听事件中分发消息给多个子组件?

利用观察者模式思想解决消息分发

我们可以借助观察者模式思想来实现这个功能

解决这个问题还有个前提,我们得知道哪些组件是 Parent.vue的子组件

同样我们需要借助一个辅助函数,直接安排

const isChildOf = (vm, componentName) => {
  let parent = vm.$parent
  // 这里为什么要向上遍历呢?因为前面提到了,子组件的层级是不固定的
  while (parent) {
    if (parent.$options.componentName === componentName) return true
    parent = parent.$parent
  }
  return false
}

测试一下,不用看 就是自信

image.png

核心代码

彦祖们核心代码来了!

我们在 mixins.js 初始化一个 observerList=[], 用来存储子组件的 onWsMessage方法

created() {
    if (isSelfComponentName(this, 'Parent')) {
      observerList.push(this.onWsMessage) // 统一由 observerList 管理
      window.addEventListener('onmessage', this.onmessage)
    } else if(isChildOf(this,'Parent') {
      observerList.push(this.onWsMessage)
    }
}

收到消息后进行分发

methods: {
    async onmessage(e) {
      // 开始处理业务逻辑,这里用 fetch 接口代替
      fetch(`https://api.example.com/${Date.now()}`)
      
      // 省略业务逻辑....
      
      // 这里我们就要遍历 observerList
      observerList.forEach(observer=>observer(e.detail))
    }
}

看下优化后的效果 接口 1s一次,各组件也完整的接受到了信息

2023-11-26 12.16.25.gif

当然,除此之外,笔者还做了很多的性能优化手段 比如

1.把大量的 O(n^2) 的算法降维到了 O(n)

2.把非实时性数据做了节流保护

3.大量的template表达式语法迁移到了 computed

4.针对重复的赋值更新逻辑进行了拦截

5.利用 requestIdleCallback 在空闲帧执行 echarts 的渲染

写在最后

之前有彦祖问过笔者,什么才算是面试简历中的亮点

如果笔者是面试官,我觉得 能用最细碎的知识点 解决最复杂的业务问题 绝对算的上是项目亮点

文中的各个知识点,彦祖们应该都非常熟悉

能把你的八股文知识,转换成真正解决业务问题的能力,这是非常难得的

个人能力有限

如有不对,欢迎指正🌟 如有帮助,建议小心心大拇指三连🌟