How to use promise s to affect the order of execution of code?

How to use promise s to affect the order of execution of code?

When we write code, we often encounter such scenarios. Two different components, their life cycles are independent of each other and have nothing to do with each other, but the interfaces they call are interdependent.

I give an example:

When developing the applet, there is an onLaunch hook in the App, which is called when the applet is initialized, and there is also an onLoad hook in the Page, which is called when the page is loaded. The normal execution sequence is:

// app launch
onLaunch(options)
// do sth....
// page load
onLoad(query)

However, we often encounter this kind of case:

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

Now comes the problem, according to the above execution order, the getToken and getUserInfo requests are actually executed concurrently. Our expectation is to call getUserInfo first after executing getToken and setting the global token value, so that the backend can return the specified data to us based on the user token information carried by the request, otherwise there will only be a 401.

So how do we make a call dependency between them? It's actually very simple promise, event-emitter is one of the methods. Next we build a minimization model.

Minimize the model

We want the execution of a part of the code in onLoad after the specific code in onLaunch. That is to say, a part of the code that runs in parallel is changed to a serial order, and the original parallel operation is also allowed.

According to the description, we naturally think of Microtask, which runs the execution code of each event loop, and after running the Task, before the Renderer.

Next, in order to achieve the expectation, we need to generate Promise in onLaunch, and then go to onLoad to execute the code according to the change of Promise state.

Then we can easily build a minimal model in one file, see the code below:

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()

In this code, I create a promise in the producer, resolve a random number after 2s, and then in the consumer, go to await its state, and print consumer end! after it becomes fulfilled.
Of course, async/await is just grammatical sugar, and you can use then/catch as well, but one advantage of await is that when it comes to non-Promise objects, it automatically wraps the values into Promise, or Promise.resolve (value)

Next, let's extend this model into a multi-file model.

// ref.js creates a reference
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()

The execution result is the same.

graft

According to the above code, we can perform a series of hijacking operations on the development of the applet. Let's take uni-app vue2/3 and native as an example.

// 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)
}

The ideas are actually the same.

enhance

The above method, although it achieves its purpose, is too simple and has poor scalability.

Let's take ref.js as an example. It's too wasteful to put only one promise in it, why not put it in the global state? This can be taken out for observation at any time.

Why not create multiple Promise queue s? In this way, different queues can be used repeatedly as a channel for code execution, and at the same time, concurrency, timeout, execution event interval, etc. can be customized. p-queue is a good choice.

Of course, these are just suggestions. I believe that everyone has their own opinions. Anyway, we should first meet the current needs, and then make appropriate transformations according to the advanced needs.

Tags: Javascript Vue.js Front-end

Posted by feckless on Tue, 20 Sep 2022 22:33:28 +0530