我们写代码的时候,经常会遇到这样的场景。2
个不同组件,它们的生命周期本身都是相互独立,毫无关联的,但是它们调用的接口,存在相互依赖的情况。
我举个例子:
开发小程序时候,里面 App
有一个 onLaunch
的 hook
,在小程序初始化时调用,而 Page
里也有一个 onLoad
的 hook
,在页面加载时被调用。正常的执行顺序为:
// 应用启动
onLaunch(options)
// do sth....
// 页面加载
onLoad(query)
但是,我们往往也经常遇到这种 case
:
async onLaunch(){
store.dispatch('set-token',await getToken())
}
async onLoad(){
// getUserInfo 依赖 token
setUserInfo(await getUserInfo())
}
现在问题来了,依据上面的执行顺序,getToken
和 getUserInfo
请求实际上是并发执行的。而我们的预期是,先执行 getToken
并设置好全局 token
值之后,才调用 getUserInfo
,这样后端才能依据请求携带的,用户 token
信息,来给我们返回指定的数据,不然那就只有一个 401
了。
那么我们如何让它们之间产生调用的依赖关系呢? 实际上很简单 promise
,event-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
,在2s
后resolve
一个随机数,然后再在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 就是不错的选择。
当然,这些也只是抛砖引玉,这些相信大家各自有各自的看法,反正先做到满足当前的需求,再根据进阶的需求进行适当的改造,做出来的才是最适合自己的。