最近在开发一套组件库,期间在实现InputNumber 组件时候碰到一个诡异卡顿的现象,用了时间来排除这个问题,涉及到一些问题定位的方法,记录下来已备后用。
1. 发现问题
在实现 InputNumber 组件的时候,有一个功能是按住 + 或 - 按钮时,组件的值在不断的自增或者自减,具体如下图
当组件的值自增到一定数量之后,组件会开始卡顿,并且页面上下滚动也会有明显的延迟。
2. 定位问题
<script>
export default {
name: "input-number",
...
methods: {
handleClick(type) {
const { step } = this;
const period = 10;
const timerHandle = () => {
const { addDisabled, decDisabled } = this;
if (!addDisabled && type === "add") this.inputNumberValue += step;
if (!decDisabled && type === "dec") this.inputNumberValue -= step;
};
const timer = setInterval(timerHandle, period);
const startTime = new Date();
const handler = () => {
const endTime = new Date();
if (endTime - startTime < period) timerHandle();
clearInterval(timer);
document.removeEventListener("mouseup", handler, false);
};
document.addEventListener("mouseup", handler, false);
}
...
};
</script>
复制代码
首先定位问题发生的位置,直观上感受应该是点击之后不无端自增发生的卡顿,对应代码中的 handleClick
函数,它将 click
事件分为 mousedown
以及 mouseup
,当触发 mousedown
事件时候,调用一个 setInterval
定时执行组件值变化的函数。
初步定位问题应该就发生在 timerHandle
之后,当 inputNumberValue
发生变化之后,它会按照一定的规则来改变 inputValue
的值,从而触发 $emit(input, this.inputValue)
来完成 v-model
。
computed: {
inputNumberValue: {
get() {
return this.inputValue;
},
set(value) {
// ...一定规则
this.inputValue = limits.find(limit => limit.need(value)).value;
}
}
},
watch: {
value: {
handler(newVal) {
console.timeEnd()
this.inputNumberValue = newVal;
},
immediate: true
},
inputValue(newVal) {
this.$emit("input", newVal);
}
}
复制代码
利用 console.time
以及 console.timeEnd
来排查,那一步发生的卡顿,检测整个 v-model
变化的流程。
也就是在 timerHandle
以及 watch value handler
内添加 console.time
以及 console.timeEnd
,具体如下
const timerHandle = () => {
const { addDisabled, decDisabled } = this;
if (!addDisabled && type === "add") this.inputNumberValue += step;
if (!decDisabled && type === "dec") this.inputNumberValue -= step;
console.time();
};
watch: {
value: {
handler(newVal) {
console.timeEnd()
this.inputNumberValue = newVal;
},
immediate: true
}
}
复制代码
然后运行,发现运行时间是在不断地增加的,这时候问题的可以归类为,inputNumber 组件的值在不断地变动,导致的 update
的时间会不断地增长。
接下来要判断具体是哪一句js导致整个页面的 update
时间不断地变长,利用 Chrome 的 JavaScript Profiler 来完成该工作。打开开发者工具
利用这个面板你可以追踪网页程序的内存泄漏问题,进一步提升程序的JavaScript执行性能,点击Start 按钮,然后去复现刚才的操作,得到结果如下
图中标识处有三个模式:
- Chart 按时间先后顺序显示的火焰图;
- Heavy(Bottom Up) 根据对性能的消耗影响列出所有的函数,并可以查看该函数的调用路径;
- Tree(Top Down) 从调用栈的顶端(最初调用的位置)开始,显示调用结构的总体的树状图情况。
选择 Tree(Top Down) 模式,得到结果如下
可以看出 flushCallbacksvue
函数占用了74.66%的 Total Time,所以需要对它进行分析
在它的调用栈中,关键的一步是 Vue._update
,它的主要功能是将 Vnode
渲染成真实DOM,所以上述的卡顿问题果然出现在渲染这一步。
继续分析,发现主要问题在与 updateDirctives
这个函数内,看来问题和指令的更新相关。
最后,发现原来是 highlightBlock
的锅,因为要完成页面中代码高亮的需求,开发了一个指令
import hljs from 'highlight.js/lib/highlight';
Vue.directive ('highlight', function (el) {
let blocks = el.querySelectorAll ('code');
Array.prototype.forEach.call (blocks, block => {
hljs.highlightBlock (block);
});
});
复制代码
当 InputNumber 组件 v-model
所绑定的父组件 data
变动时候,会导致 v-highlight
指令不断地更新,使得页面卡顿。
3. 解决问题
只需要将该指令的高亮代码的函数写在 bind
里面,这样就只调用一次,指令第一次绑定到元素时调用。
Vue.directive ('highlight', {
bind (el) {
let blocks = el.querySelectorAll ('code');
Array.prototype.forEach.call (blocks, block => {
hljs.highlightBlock (block);
});
}
});
复制代码
原创声明: 该文章为原创文章,转载请注明出处。