前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

Vue API 盲点解析 performance errorHandler nextTick 与 watch

首页2019-06-02 00:40:12Front-End
VueVue API前端性能

接手一个跑了两年的 Vue 项目,最常遇到的两件事:一是页面进去要等一秒多,但不知道慢在哪个组件;二是某个操作偶尔白屏,控制台干干净净,window.onerror 里什么都没有。这两个问题都不是写业务代码能解决的,得靠 Vue 全局配置上那几个平时没人翻的 API。这篇把 config.performance、config.errorHandler、nextTick、watch 的 deep 和 immediate 这四个盲点逐个讲透,讲用法,也讲它们背后是怎么实现的、什么时候会失效。

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 用 config.performance 把每个组件的渲染耗时打到浏览器时间线上
  • 为什么 window.onerror 抓不到 Vue 里的报错,errorHandler 又能覆盖到哪一层
  • nextTick 到底在等什么,它的降级实现链和 MutationObserver 的关系
  • watch 的 deep 和 immediate 怎么用,以及它们各自的代价
  • 这四个 API 在 Vue 3 里对应的写法

# 一、用 performance 开启性能追踪

performance 是 Vue 全局配置里的一项,用来做组件级别的渲染耗时追踪。在入口文件里加上这段就开了:

if (process.env.NODE_ENV !== 'production') {
    Vue.config.performance = true;
}
@前端进阶之旅: 代码已经复制到剪贴板

这个 API 是 2.2.0 新增的,只在开发模式并且浏览器支持 performance.mark 时生效。生产包里它是被直接编译掉的,所以不用担心线上开销,也别指望在生产环境靠它测数据。

开起来之后,浏览器时间线上就能看到每个组件在 init、compile、render、patch 各阶段花了多少毫秒。哪个组件的 patch 特别长,优化的靶子就有了。

原文这里推荐的是 Vue Performance Devtool 这个 Chrome 插件。它现在已经很久没更新了,我自己更习惯直接开 Chrome DevTools 的 Performance 面板录一段,Vue 打的这些标记会落在 Timings(有的版本叫 User Timing)轨道上,一目了然,也不用额外装东西。这条我只在 Chrome 上试过,Firefox 的面板叫法不太一样。

那 Vue 是怎么做到的?它用的就是浏览器原生的 window.performance,核心是两个方法:

  • performance.mark 用于创建标记
  • performance.measure 用于记录两个标记之间的时间间隔
performance.mark('start'); // 创建 start 标记
performance.mark('end'); // 创建 end 标记

performance.measure('output', 'start', 'end'); // 计算两者时间间隔

performance.getEntriesByName('output'); // 获取标记,返回值是一个数组,包含了间隔时间数据
@前端进阶之旅: 代码已经复制到剪贴板

Vue 干的事情就是在每个组件的关键节点上按这个套路打点,标记名带上组件名,所以你在时间线上看到的是 <UserList> render 这种可读的条目,而不是一堆数字。

知道原理之后,这套打点你自己也能用。接口请求前后、一段复杂计算的首尾、一个大列表渲染完成的时刻,全都可以 mark 一下再 measure。比 console.time 强的地方在于,这些数据会进浏览器的性能时间线,能和网络、脚本执行、绘制放在同一张图上对齐着看。

除了上面这两个方法,原文还提到可以用 performance.timing 计算页面各个阶段的加载情况。这条现在要打个补丁:performance.timing 在规范里已经被标记为废弃,虽然浏览器还留着,但新代码建议改用 performance.getEntriesByType('navigation')[0] 拿到的 PerformanceNavigationTiming 对象,字段名基本能对上,而且时间基准更干净。

Vue 3 里这个开关搬到了应用实例上,写成 app.config.performance = true,语义和生效条件没变,还是只在开发模式起作用。

# 二、用 errorHandler 兜住框架吞掉的异常

浏览器里捕获异常,大家最熟的是 try...catch 和 window.onerror,这是原生 JavaScript 给的两条路。但在 Vue 2 项目里,你照着老经验挂一个 window.onerror,会发现组件里的报错一条都收不到。

原因是 Vue 自己在渲染和生命周期调用的外面包了一层 try...catch。它把错误接住了,走自己的错误处理流程,所以异常根本没冒泡到 window 上。想拿到这些信息,得用 errorHandler:

Vue.config.errorHandler = function (err, vm, info) {
    let { 
        message, // 异常信息
        name, // 异常名称
        stack  // 异常堆栈信息
    } = err;

    // vm 为抛出异常的 Vue 实例
    // info 为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
@前端进阶之旅: 代码已经复制到剪贴板

三个参数里最有价值的其实是 info。它会告诉你错误发生在哪个钩子,比如 mounted hook 或者 render。做异常上报的时候把它一起带上,定位效率完全不一样。

errorHandler 的覆盖范围是随版本一点点扩的,这块很多人没注意到:2.2.0 起它开始捕获组件生命周期钩子里的错误,2.4.0 起支持捕获自定义事件处理函数内部的错误,2.6.0 起 v-on DOM 监听器里抛出的错误也会走到这里。所以你要是在一个 2.3 的老项目上验证「为什么自定义事件里的报错收不到」,别怀疑自己写错了,是版本不够。

<template>
    <my-component @eventFn="doSomething"></my-component>
</template>

<script>
export default {
    methods: {
        doSomething() {
            console.log(a); // a is not defined
        }
    }
}
</script>
@前端进阶之旅: 代码已经复制到剪贴板

上面这段就是 2.4.0 之后才能被 errorHandler 接住的典型场景。

这里有个坑要注意:errorHandler 管不到异步回调。setTimeout 里抛的错、Promise 链上没接 catch 的 reject,都跑到了 Vue 的调用栈之外,它接不到。真要做全量的前端异常监控,errorHandler 只是其中一块,还得配上 window.onerror 和 window.addEventListener('unhandledrejection'),三者各管一片。

另一个我自己踩过的问题是,errorHandler 里如果自己又抛了错,会形成套娃。上报函数里做了个 JSON.stringify 循环引用的对象,直接把这个兜底逻辑本身炸了,然后就是满屏刷不完的报错。所以这个函数体内部最好整个包一层 try...catch,并且保留一句 console.error(err),不然开发环境下所有错误都被你静悄悄吃掉了,调试体验会变得非常差。

组件级别的兜底还有 errorCaptured 这个钩子(2.5.0 新增),写在组件上,能拦住后代组件抛上来的错误,返回 false 可以阻止它继续往上冒。做局部降级 UI 的时候很好用,某个卡片崩了就渲染一个「加载失败」占位,不至于整页白屏。

Vue 3 里对应的是 app.config.errorHandler(err, instance, info) 和组合式 API 的 onErrorCaptured,思路一模一样,只是第二个参数从 vm 换成了组件实例,info 的字符串格式也做了调整。

# 三、nextTick 到底在等什么

先看一段一定会报错的代码:

<template>
    <ul ref="box">
        <li v-for="(item, index) in arr" :key="index"></li>
    </ul>
</template>

<script>
export default {
    data() {
        return {
            arr: []
        }
    },
    mounted() {
    	this.getData();
    },
    methods: {
        getData() {
            this.arr = [1, 2, 3];
            this.$refs.box.getElementsByTagName('li')[0].innerHTML = 'hello';
        }
    }
}
</script>
@前端进阶之旅: 代码已经复制到剪贴板

赋值完立刻去取 li,取到的是 undefined,再往上读 innerHTML 就抛错。第一直觉的解释是「数据还没赋上」,但你打个断点会发现 this.arr 明明已经是 [1, 2, 3] 了。

真正的原因是 Vue 的 DOM 更新是异步的。数据变了之后,Vue 不会立刻去改 DOM,而是把这个 watcher 推进一个队列,去重,然后在当前同步代码跑完之后统一 flush。这么设计是为了合并同一轮里的多次修改,你连着改十个字段,DOM 只重排一次。响应式这套依赖收集和队列调度的完整链路,我在 Vue响应式原理总结 里单独梳理过。

回到这块。既然更新排在队列后面,那我们把读 DOM 的操作也排到队列后面就行了,这就是 nextTick:

this.$nextTick(() => {
    this.$refs.box.getElementsByTagName('li')[0].innerHTML = 'hello';
})
@前端进阶之旅: 代码已经复制到剪贴板

$nextTick 不传回调时会返回一个 Promise,所以也可以用 async/await 写平:

methods: {
    async getData() {
        this.arr = [1, 2, 3];
        
        await this.$nextTick();
        
        this.$refs.box.getElementsByTagName('li')[0].innerHTML = 'hello';
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

那 Vue 是靠什么实现这个「下一个 tick」的?源码里它并不是写死一种,而是按环境支持度做能力探测,依次降级:

  • Promise.then 微任务
  • MutationObserver 监听 DOM 变化触发回调
  • setImmediate(只有 IE 和 Node 有)
  • setTimeout(fn, 0) 宏任务兜底

前后两种大家都熟,都能把回调推到当前同步代码之后。中间那个 MutationObserver 是 HTML5 的特性,用一句话讲就是:创建一个观察者对象盯住某个 DOM 节点,它的 DOM 树一发生变化就执行你给的回调。Vue 用它的时候其实是取巧,创建一个游离的文本节点,改一下它的内容来触发回调,借的是这个回调跑在微任务队列里这一点。

fe
  • 一、用 performance 开启性能追踪
  • 二、用 errorHandler 兜住框架吞掉的异常
  • 三、nextTick 到底在等什么
  • 四、watch 的 deep 和 immediate
  • 总结
  • 参考

← React Native 适配 Android 与 iOS 完整总结篇Vue之合理划分容器组件与展示组件 →