Vue 修复了 watch 的 BUG

前言

在之前的项目中,需要做全局错误的收集和上报,最后有个头疼的问题就是 Vue watch 中的异步错误无法上报到 errorHandler 里面,然后在某一天我再次阅读 Vue 代码的时候,发现他在 2.6.13 版本上修复了这个问题,开心!!!

例子

大家可以切换 Vue 的版本号,来看看效果,你会发现 <= 2.6.12 版本的 watch 都不会捕获到异步错误

<!-- vue 2.6.12 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>

<div id="app">
    <button @click='num++'>{{ num }}</button>
</div>

<script>
    Vue.config.errorHandler = (err, vm, info) => {
        console.log('收集到错误:', err)
    }

    new Vue({
        el: '#app',
        data: { num: 100 },
        watch: {
            async num() {
                // 加 await 是为了捕获异步错误
                await this.errorFnc()
            }
        },
        methods: {
            errorFnc() {
                return new Promise((resolve, reject) => {
                    reject('promise 错误')
                })
            },
            // 或者 async 函数
            // async errorFnc() {
            //     throw 'async 错误'
            // },
        }
    })
</script>

Vue 是如何解决的

2.6.12

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // Watcher 里面执行回调函数和下面一样,就不贴代码了
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      try {
        // 直接执行回调函数
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {
      watcher.teardown()
    }
}

2.6.13

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // Watcher 里面执行回调函数和下面一样,就不贴代码了
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      // 用该函数去执行回调函数
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()
    }
    return function unwatchFn () {
      watcher.teardown()
    }
}

对比版本

我们发现两个版本不同的是执行回调函数的方式变了,通过 invokeWithErrorHandling 执行回调函数,如果是 promise 的话会被 catch,从而被 handleError 报告上去。

export function invokeWithErrorHandling (
    handler: Function,
    context: any,
    args: null | any[],
    vm: any,
    info: string
) {
    let res
    try {
        res = args ? handler.apply(context, args) : handler.call(context)
        if (res && !res._isVue && isPromise(res) && !res._handled) {
          res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
          // issue #9511
          // avoid catch triggering multiple times when nested calls
          res._handled = true
        }
    } catch (e) {
        handleError(e, vm, info)
    }
    return res
}

思考

有人可能会问,为什么不 try catch 自己上报错误信息,或者这个有什么用?

  1. 自己 try catch,重复工作量极大。

  2. 对 Vue 来说这个是一个很小的修复,但对于一个线上项目来说,如果无法上报你的所有错误,那么有些地方就可能会影响到用户体验,产生用户的流失,甚至让公司财产损失。

Vue 如何进行错误收集和上报

对于我们开发者来说,最好是不用手动上报错误,这会带来很多重复性的工作,我们最好只用关注我们的正常的业务逻辑,而对于 Vue 工程来说,Vue 会自动上报我们的错误,我们只要保证一定的写法,错误就不会丢失。

第一步

我们全局只需要一个上报错误的地方,那就是 Vue 的 errorHandler,Vue 会把所有错误上报到这个函数,你可以直接应用 sentry,或者在这个函数里面调用后台的错误上报接口。

第二步

我们确定了上报错误的地方,下面要做的就是保证所有错误能被 Vue 捕获到,同步任务的错误会被直接捕获,而异步任务的错误,我们必须使用一定的写法。

异步错误

项目中我们最多的是和后台交互,如下

写法一

这个是我在项目中见的最多的写法,一旦使用了 then 来处理异步任务,就意味着我们的错误不会被 Vue 捕获,如果我们 then 回调函数里面出现了错误,我们还得在最后面写一个 .catch 来捕获 then 回调函数里面的错误,这种写法给我们开发者加大了很多的工作量。

mounted() {
    // 不会捕获到错误
    this.getData()
},
methods: {
    getData() {
        http.get('xxx').then(data => {
            // xxx
        }, error => {
            // 只能自己上报异步错误
        })
    }
}

写法二

我们只用换成 async await 来替代我们 then 的写法,所有错误就会被捕获到,而且更加简洁

async mounted() {
    // 使用 await 可以捕获到异步错误
    await this.getData()
},
methods: {
    async getData() {
        const data = await http.get('xxx')
        // xxx
    }
}

如何保证所有人使用 async 语法开发

如果你的项目中大家都可以遵守这种写法,那就不用往下看了。

对于开发项目来说,开发者是不可控的,编码风格也是千变万化,而且就算记住了哪种写法,在实际开发的时候也会有疏忽,还是能用工具解决的就不用口头去约束。

借助 eslint

基于 eslint 我们可以很轻松制定一套规则,但有一些规则是没有的,就需要我们自己开发,我对上面 async 语法的约束写了一个插件:eslint-plugin-leon-rule,大家可以参考下源码或者使用。

如果大家想开发一个属于自己的 eslint 插件,可以参考我这篇文章:教你手写个 eslint 插件

最后

A0BF4632.jpg