7

从 Composition API 源码分析 getCurrentInstance() 为何返回 null

 3 years ago
source link: https://4ark.me/post/87ba8d8b.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从 Composition API 源码分析 getCurrentInstance() 为何返回 null

2021-01-16

如果同学们已经开始使用 Composition API,那么我们可能都会遇到类似的问题,那就是有时候我们调用 getCurrentInstance() ,它返回的居然是 null ,举个例子:

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increase">Increase</button>
  </div>
</template>

<script>
import {defineComponent, ref, getCurrentInstance} from '@vue/composition-api'

function useIncrease() {
  const count = ref(0)

  function increase() {
    count.value++

    // 假设我们还要访问当前实例来做些什么
    getCurrentInstance().doSomething // 但 getCurrentInstance() 返回的是 null
  }

  return {
    count,
    increase,
  }
}

export default defineComponent({
  setup() {
    const {count, increase} = useIncrease()

    return {
      count,
      increase,
    }
  },
})
</script>
复制代码

或许有些同学已经知道,只要我们把该方法的调用摆到外面,那它就能正常工作了:

function useIncrease() {
+  const vm = getCurrentInstance()
  const count = ref(0)

  function increase() {
    count.value++

-   getCurrentInstance().doSomething
+   vm.doSomething // 这时能够正常访问
  }

  return {
    count,
    increase,
  }
}
复制代码

另外其实不止 getCurrentInstance() ,还有诸如: useStoreuseRouter 这些跟 Vue 实例沾边的都会有这个问题,不过本文只讨论 getCurrentInstance(),至于其它这些我不太清楚,所以不敢乱下结论。

我之前对此一直没怎么深究,满足于知道这样做能使它正常工作就足够了。但是,当我今天看到这篇文章《使用Vue3的CompositionAPI来优化代码量》的时候,里面也提到了一样的问题,只是这位同学认为像 getCurrentInstance() 方法只能在 setup() 中调用,而无法在外部的 hooks 方法中调用时,我意识到这个问题必须得讲清楚,否则会造成很多误解。

但当我想在评论区提醒一波时,我发现其实我也不清楚背后的缘由,索性就趁此机会,好好了解一下为什么会有这么神奇的事情。毕竟,只知道能工作的解决方法是不够的,还要知道它为什么会这样。

但 Google 一番,发现没有对这个问题做太多解读(难道这件事不值得疑惑吗?),从 Composition API 官方仓库的 issue 中,也只是给出了与上面类似的解决方案,但并没有说为什么要这样,对此我就更加疑惑了。

所以,本文就来分析一下,到底为什么在不同的位置调用 getCurrentInstance() 得到的结果会不一样呢?

为了不浪费同学们宝贵的时间,我先把重点放到前面,首先是到底什么时候才能访问通过 getCurrentInstance() 当前实例:

  1. 只要在 setup() 内部调用的任何方法,都能获取到当前实例,无论函数调用栈有多深
  2. 但只能在同步代码中才能访问,其它的诸如 setTimeoutDOM 事件Promise 这类异步代码均无法访问(建议再回头看看上面的例子

无法访问的原因是:

  1. Composition API 会在调用组件的 setup() 前,先拿一个变量存放当前实例,以供调用 getCurrentInstance() 时返回,源码:mixin.ts#L95instance.ts#L116
  2. 执行完 setup() 以后,会把用于存放当前实例的变量值恢复到以前的模样(也就是 null ),源码:instance.ts#L126
  3. 所以,当 Composition API 内部执行到我们组件的 setup() 时,所有的同步代码都能访问到当前实例,但那些异步代码再去访问时,它已经恢复成 null 了。

好了,就是这么简单,相信同学们下次再遇到这种问题时,就不会疑惑为什么 getCurrentInstance() 会返回 null ,因为我们知道背后的运行逻辑。

结论说完了,下面开始分享一下我分析这个问题的整个过程,也希望能对同学们有一些启发。

想知道问题的所在,最直接的方法就是看源码,但是直接 clone 项目下来从头看,显然不及通过打断点来得高效,毕竟我们现在的目的还不是学习它的源码设计,只是想知道它其中一部分的运行逻辑。

我们用下面的代码来做例子:

<script>
import {defineComponent, getCurrentInstance} from '@vue/composition-api'

export default defineComponent({
  setup() {
    debugger
    console.log(getCurrentInstance())

    setTimeout(() => {
      debugger
      console.log(getCurrentInstance())
    })
  },
})
</script>
复制代码

可以看到,我们在两处调用的地方之前都加了一个断点, 先看看它们执行起来有何不同。

第一次调用的内部是这样的,这时候它能够访问:

而第二次调用的内部是这样的,这时候它已经变成了 null

可以看到 getCurrentInstance() 的内部非常简单,只是将 currentInstance 给返回出来了,而这个变量显然是在外部定义的,那为什么两次调用,它的值就发生了变化呢?

我们留意到断点进来的文件是:vue-composition-api.esm.js,那我们直接在 node_modules 下打开这个文件,先搜索一下有哪些地方修改了 currentInstance 这个变量,根据上图就能看到是通过 setCurrentInstance() 方法来修改,于是我们搜索一下这个方法的调用:

可以看到一共有四处(其中一处是定义),但是光靠肉眼看也很难看出来什么,所以还是老方法,在每个调用之前加一个断点(是的,我们可以直接修改 node_modules 下的代码来进行调试),然后我们发现第一次调用是在 activateCurrentInstance

这时候我们仔细看看这个方法的内部,就能看出些端倪:

  1. 上图显示 preVm 是一个 null
  2. vm 是一个 Vue 的实例
  3. 执行完 fn(vm) 后,又将 preVm 传入到 setCurrentInstance() 去了

所以,我们看看这个 fn(vm) 内部是什么,搜索 activateCurrentInstance() 的调用,发现一个很可疑的地方:

很明显这里就是执行组件 setup() 函数的地方,再结合上图来看,我们得出结论:

  1. 在执行组件 setup() 函数前,先把当前实例存放起来
  2. 然后执行组件 setup() 函数时, setup() 函数内部自然就能通过 getCurrentInstance() 访问当前实例了
  3. 等执行完 setup() 后,又将 currentInstance 重置回 null 去了(注意是同步执行 setup()
  4. 后面 setup() 内部的异步代码再去调用 getCurrentInstance() ,其实已经是 null

如果还不太理解的同学,建议先补一下 JavaScript 的:事件循环、同步、异步(分为宏任务、微任务)这些概念。

至此,整个分析过程就结束了,你可能也会觉得很简单,我觉得你上你也行,所以不要觉得这些底层库的运行原理很难捉摸,出了问题也先别一味抱怨或者尝试各种 HACK,为何不直接看看它的运行逻辑呢?

所以,大胆 debug 吧,少年。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK