此页面需要javascript支持,请在浏览器中启用javascript

如何利用 promise 影响代码的执行顺序?

promise
执行顺序
共1105个字,阅读时间 6 分钟
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://icebreaker.top/articles/2022/9/19-how-to-lifestyle

Image

如何利用 promise 影响代码的执行顺序?

我们写代码的时候,经常会遇到这样的场景。2个不同组件,它们的生命周期本身都是相互独立,毫无关联的,但是它们调用的接口,存在相互依赖的情况。

我举个例子:

开发小程序时候,里面 App 有一个 onLaunchhook,在小程序初始化时调用,而 Page 里也有一个 onLoadhook,在页面加载时被调用。正常的执行顺序为:

// 应用启动
onLaunch(options)
// do sth....
// 页面加载
onLoad(query)

但是,我们往往也经常遇到这种 case:

async onLaunch(){
  store.dispatch('set-token',await getToken())
}
async onLoad(){
  // getUserInfo 依赖 token
  setUserInfo(await getUserInfo())
}

现在问题来了,依据上面的执行顺序,getTokengetUserInfo 请求实际上是并发执行的。而我们的预期是,先执行 getToken 并设置好全局 token 值之后,才调用 getUserInfo,这样后端才能依据请求携带的,用户 token 信息,来给我们返回指定的数据,不然那就只有一个 401 了。

那么我们如何让它们之间产生调用的依赖关系呢? 实际上很简单 promiseevent-emitter 都是方法之一。接下来我们来构建一个最小化模型。

最小化模型

我们想要 onLoad一部分的代码的执行在 onLaunch特定代码之后。即把一部分并行跑的代码,变更为串行的顺序,同时也允许原先并行运行的方式。

根据描述,我们天然的就想到了 Microtask,它运行在每个事件循环的执行代码,和运行Task后,Rerender 前。

接下来为了实现期望,我们就需要在 onLaunch 中去产生 Promise,然后到 onLoad 中依据 Promise 状态的变化,执行代码。

那么我们就很容易在一个文件中,构建出一个最小化模型,见下方代码:

let promise

function producer () {
  console.log('producer start!')
  promise = new Promise((resolve) => {
    setTimeout(() => {
      console.log('promise resolved')
      resolve(Math.random())
    }, 2_000)
  })
  console.log('producer end!')
}

async function consumer () {
  console.log('consumer start!')
  console.log(await promise)
  console.log('consumer end!')
}

producer()
consumer()

这段代码中,我在 producer 创建了一个 promise,在 2sresolve 一个随机数,然后再在 consumer 中,去 await 它的状态,变为 fulfilled 后打印 consumer end!。 当然 async/await 只是语法糖,你用 then/catch 也是可以的,不过使用 await 有一个好处就是,它在面对非 Promise 对象的时候,它会自动把值进行包裹转化成 Promise,即 Promise.resolve(value)

接着,让我们把这个模型进行扩充,变为多文件模型。

// ref.js 创建一个引用
export default {
  promise: undefined
}
// producer.js
import ref from './ref.js'

export default () => {
  console.log('producer start!')
  ref.promise = new Promise((resolve) => {
    setTimeout(() => {
      console.log('promise resolved')
      resolve(Math.random())
    }, 2_000)
  })
  console.log('producer end!')
}
// consumer.js
import ref from './ref.js'

export default async () => {
  console.log('consumer start!')
  console.log(await ref.promise)
  console.log('consumer end!')
}
// index.js
import producer from './producer.js'
import consumer from './consumer.js'

producer()
consumer()

执行结果同理。

移花接木

根据上述的代码,我们就可以对小程序的开发,进行一系列劫持的操作。我们以 uni-app vue2/3 和原生为例。

// vue2
Vue.mixin({
  created () {
    if (Array.isArray(this.$options.onLoad) && this.$options.onLoad.length) {
      this.$options.onLoad = this.$options.onLoad.map(fn => {
        return async (params:Record<string, any>) => {
          await ref.promise
          fn.call(this, params)
        }
      })
    }
  }
})

// vue3
const app = createSSRApp(App)
app.mixin({
  created () {
    if (this.$scope) {
      const originalOnLoad = this.$scope.onLoad
      this.$scope.onLoad = async (params:Record<string, any>) => {
        await ref.promise
        originalOnLoad.call(this, params)
      }
    }
  }
})

// native
const nativePage = Page
Page = function (options: Parameters<typeof Page>[0]) {
  if (options.onLoad && typeof options.onLoad === 'function') {
    const originalOnLoad = options.onLoad
    options.onLoad = async function (params: Record<string, any>) {
      await ref.promise
      originalOnLoad.call(this, params)
    }
  }
  nativePage(options)
}

思路其实都差不多。

增强

上述的方法,虽然达到了目的,但是实在太简陋了,扩展性也很差。

我们以 ref.js 为例,里面只放了一个 promise 太浪费了,为什么不把它放入全局状态里去呢?这样随时可以取出来进行观察。

为什么不创建多个 Promise queue 呢? 这样还能循环往复地利用不同的队列,来作为代码执行的信道,同时又能够自定义并发度,超时,执行事件间隔等等。p-queue 就是不错的选择。

当然,这些也只是抛砖引玉,这些相信大家各自有各自的看法,反正先做到满足当前的需求,再根据进阶的需求进行适当的改造,做出来的才是最适合自己的。