[solution] Vue3 multi-component asynchronous task queue

Posted by drayfuss on Sat, 15 Jan 2022 21:25:35 +0100

About Vue3 and MIT The usage of JS is introduced in another article

Some Vue3 knowledge points sorted out (550)+ 👍)

Demand introduction

Recently, the company has a demand for a mobile page. A page contains multiple floors, each of which is a separate component. Each component has its own logic.

The page is similar to the welfare page of the personal center. Each floor displays the picture of the corresponding gift bag. After entering the page, the pop-up window for receiving the gift bag will pop up automatically on the premise of meeting the conditions.

Control the pop-up window display of each gift bag. The hidden status is written in their respective components. Now the demand is

💡 Only one pop-up window can be displayed at a time

💡 Whether you click confirm or cancel, the second pop-up window will open automatically after closing the previous pop-up window

💡 You can control the order of pop-up display

Solution

Technology stack

  • Vue3
  • mitt.js
  • Promise

thinking

Each pop-up window is regarded as an asynchronous task. Build a task queue according to the preset order, and then manually change the status of the current asynchronous task by clicking the button to enter the next asynchronous task.

Step 1

First write two components to simulate the actual situation

Parent component (page component)

<template>
  <div>
    <h1>I am the parent component!</h1>
    <child-one></child-one>
    <child-two></child-two>
    
    <div class="popup"
     v-if="showPopp">
      <h1>I am the parent component pop-up</h1>
    </div>
  </div>
  
</template>
<script>
import { defineComponent } from 'vue'

import ChildOne from './components/Child1.vue'
import ChildTwo from './components/Child2.vue'

export default defineComponent({
  name: '',
  components: {
    ChildOne,
    ChildTwo,
  },
  setup() {
    //Control pop-up display
    const showPopp = ref(false)
    return {
      showPopp,
    }
  },
})
</script>

Sub assembly I

<template>
    <div>
      I'm floor one
    </div>

    <div class="popup"
       v-if="showPopp">
        <h3>I'm pop-up one</h3>
        <div>
          <button @click='cancle'>cancel</button>
          <button @click='confirm'>determine</button>
        </div>
    </div>
</template>
<script>
import { defineComponent, ref } from 'vue'


export default defineComponent({
  name: '',
  setup() {
    //Control pop-up display
    const showPopp = ref(false)
    //Logic of cancellation
    const cancle = () => {
      showPopp.value = false
      //do something
    }
    //Logic of confirmation
    const confirm = () => {
      showPopp.value = false
      //do something
    }
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>

Sub assembly II

Basically, the logic as like as two peas is used to handle the pop up window. In fact, the logic should be extracted into a hook.

<template>
    <div>
      This is floor two
    </div>
    
    <div class="popup"
       v-if="showPopp">
        <h3>I'm pop-up two</h3>
        <div>
          <button @click='cancle'>cancel</button>
          <button @click='confirm'>determine</button>
        </div>
    </div>
</template>
<script>
import { defineComponent, ref } from 'vue'


export default defineComponent({
  name: '',
  setup() {
    //Control pop-up display
    const showPopp = ref(false)
    //Logic of cancellation
    const cancle = () => {
      showPopp.value = false
      //do something
    }
    //Logic of confirmation
    const confirm = () => {
      showPopp.value = false
      //do something
    }
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>

The results are shown in the figure below

Step 2

We don't use pop-up window first. We use timer and console Log to simulate asynchronous tasks

Parent component

//Omit some of the code that appears above
setup() {
    .......
    //Asynchronous tasks to be processed separately by the parent component
    const taskC = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('Asynchronous task of parent component')
        }, 1000)
      })
    }
    
    onMounted(() => {
      taskC()
    })
    ......
  },

Sub assembly I

//Omit some of the code that appears above
setup() {
    .......
    const taskA = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('Asynchronous task of sub component 1')
        }, 1000)
      })
    }
    onBeforeMount(() => {
      taskA()
    })
    ......
  },

Sub assembly II

//Omit some of the code that appears above
setup() {
    .......
    const taskB = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('Asynchronous task of sub component 2')
        }, 1000)
      })
    }
    onBeforeMount(() => {
      taskB()
    })
    ......
},

Take a look at the results. Because the task queue has not been built and all asynchronous tasks are performed at the same time, the log s of the three components are printed at the same time

Step 3

Use MIT JS to collect asynchronous tasks

Let's start with MIT JS is encapsulated into a tool function

//mitt.js
import mitt from 'mitt'
const emitter = mitt();

export default emitter;

Before the child component is mounted, the add async tags event is triggered to notify the parent component to collect asynchronous tasks. The parent component listens to the add async tags event and stores the child component's tasks in the array.

Parent component

//Omit some of the code that appears above
setup() {
    .......
    // Declare an empty array to hold all asynchronous tasks
    let asyncTasks = []
    
    //Add asynchronous tasks to the array and collect all asynchronous tasks
    const addAsyncTasts = (item) => {
      asyncTasks.push(item)
      console.log('🚀🚀~ asyncTasks:', asyncTasks)
    }
    
    // Listen for the add async tags event. When an asynchronous task is triggered, add the asynchronous task to the array
    emitter.on('add-async-tasts', addAsyncTasts)
    
    // When the component is unloaded, the listening event is removed and the array is reset to null
    onUnmounted(() => {
      emitter.off('add-async-tasts', addAsyncTasts)
      asyncTasks = []
    })
    .......

Sub assembly I

//Omit some of the code that appears above
setup() {
    .......
    onBeforeMount(() => {
      //Conditional judgment if If the conditions are met here, the parent component will be notified of the collection task
      emitter.emit('add-async-tasts', taskA)
    })
    .......

Sub assembly II

//Omit some of the code that appears above
setup() {
    .......
    onBeforeMount(() => {
      //Conditional judgment if If the conditions are met here, the parent component will be notified of the collection task
      emitter.emit('add-async-tasts', taskB)
    })
    .......

Look at the results. I log ged in the collection function of the parent component. You can see that the collection function was triggered twice

Click to see that there are two pieces of data, taskA and taskB. That means our mission has been collected.

Step 4

Custom task order

The way I implement this is to pass in a digital parameter when collecting tasks, and finally sort the task queue according to the number.

Parent component

//Omit some of the code that appears above
setup() {
    .......
    //Sorting function
    const compare = (property) => {
      return (a, b) => {
        let value1 = a[property]
        let value2 = b[property]
        return value1 - value2
      }
    }
    
    //Add asynchronous tasks to the array and collect all asynchronous tasks
    const addAsyncTasts = (item) => {
      asyncTasks.push(item)
      //Sort by order field
      asyncTasks = asyncTasks.sort(compare('order'))
      console.log('🚀🚀~ asyncTasks:', asyncTasks)
    }
    .......

Sub assembly I

//Omit some of the code that appears above
setup() {
    .......
    onBeforeMount(() => {
      //Conditional judgment if If the conditions are met here, the parent component will be notified of the collection task
      emitter.emit('add-async-tasts', { fun: taskA, order: 1 })
    })
    .......

Sub assembly II

//Omit some of the code that appears above
setup() {
    .......
    onBeforeMount(() => {
      //Conditional judgment if If the conditions are met here, the parent component will be notified of the collection task
      emitter.emit('add-async-tasts', { fun: taskB, order: 2 })
    })
    .......

Looking at the results, you can see that two tasks are still collected and sorted according to order

We modify the order of sub component 1 to 3, and then verify whether the result is correct

You can see that taskA ranks behind taskB, indicating that the order of our custom asynchronous tasks has also been realized.

Step 5

After the tasks are collected, the next step is to build a task queue

Parent component

//Omit some of the code that appears above
setup() {
    .......
    //The instance is mounted and then called to ensure that all tasks are collected, we execute the queue in the onMounted cycle.
    //Mounted does not guarantee that all sub components are mounted together. If you want to wait until the whole view is rendered, you can use nextTick inside mounted
    onMounted(() => {
      nextTick(() => {
        // Build queue
        const queue = async (arr) => {
          for (let item of arr) {
            await item.fun()
          }
          //Return a promise in completion status before continuing the chain call
          return Promise.resolve()
        }

        // Execution queue
        queue(asyncTasks)
          .then((data) => {
            //After the tasks of all child components are completed, perform the tasks of the parent component
            //If you want to perform the task of the parent component first, you can define order as 0 and save it into the task queue
            return taskC()
          })
          .catch((e) => console.log(e))
      })
    })
 })
    .......

Looking at the results, you can see that all the tasks are carried out in order.

Step 6

Use the real pop-up scene to modify the code

Let's take a brief look at promise. The use of promise is not the content of this article

The state of Promise objects is not affected by the outside world.

Promise object represents an asynchronous operation and has three states:

  • Pending (in progress)
  • Resolved (completed, also known as completed)
  • Rejected (failed)

Only the result of asynchronous operation can determine the current state, and no other operation can change this state. This is also the origin of the name Promise. Its English meaning is "commitment", which means that other means cannot be changed.

However, through practice, it is found that Promise status can be manually modified externally

Refer to the following article for details 👉
How to control its status outside Promise

Since it can be modified, we can add code that can manually modify Promise status in the button click event of the sub component

//Omit some of the code that appears above
setup() {
    .......
 //Used to change the state of promise externally
    let fullfilledFn
    //Asynchronous task
    const taskA = () => {
      return new Promise((resolve, reject) => {
        showPopp.value = true
        fullfilledFn = () => {
          resolve()
        }
      })
    }
    //Logic of cancellation
    const cancle = () => {
      showPopp.value = false
      fullfilledFn()
    }
    //Logic of confirmation
    const confirm = () => {
      showPopp.value = false
      fullfilledFn()
    }
 })
    .......

Finally, let's look at the results

All codes

Finally, post all the codes

Parent component

<!--
 * @Description: 
 * @Date: 2021-06-23 09:48:13
 * @LastEditTime: 2021-07-07 10:34:04
 * @FilePath: \one\src\App.vue
-->
<template>
  <div>
    <h1>I am the parent component!</h1>
    <child-one></child-one>
    <child-two></child-two>
  </div>
  <div class="popup"
       v-if="showPopp">
    <h1>I am the parent component pop-up</h1>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onMounted, onUnmounted, nextTick, ref } from 'vue'

import ChildOne from './components/Child1.vue'
import ChildTwo from './components/Child2.vue'

import emitter from './mitt'

export default defineComponent({
  name: '',
  components: {
    ChildOne,
    ChildTwo,
  },
  setup() {
    //Control pop-up display
    const showPopp = ref(false)
    //Sorting function
    const compare = (property) => {
      return (a, b) => {
        let value1 = a[property]
        let value2 = b[property]
        return value1 - value2
      }
    }

    //Asynchronous tasks to be handled separately by the component
    const taskC = () => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          showPopp.value = true
          resolve()
        }, 1000)
      })
    }

    // Declare an empty array to hold all asynchronous tasks
    let asyncTasks = []

    //Add asynchronous tasks to the array and collect all asynchronous tasks
    const addAsyncTasts = (item) => {
      asyncTasks.push(item)
      asyncTasks = asyncTasks.sort(compare('order'))
      console.log('🚀🚀~ asyncTasks:', asyncTasks)
    }

    // Listen to the addAsyncTasts event. When an asynchronous task is triggered, add the asynchronous task to the array
    emitter.on('add-async-tasts', addAsyncTasts)

    //When an instance is mounted, calling mounted does not guarantee that all subcomponents are also mounted together. If you want to wait until the whole view is rendered, you can use nextTick inside mounted
    onMounted(() => {
      nextTick(() => {
        // Build queue
        const queue = async (arr) => {
          for (let item of arr) {
            await item.fun()
          }
          return Promise.resolve()
        }

        // Execution queue
        queue(asyncTasks)
          .then((data) => {
            return taskC()
          })
          .catch((e) => console.log(e))
      })
    })

    // When the component is unloaded, the listening event is removed and the array is reset to null
    onUnmounted(() => {
      emitter.off('add-async-tasts', addAsyncTasts)
      asyncTasks = []
    })

    return {
      showPopp,
    }
  },
})
</script>


Sub assembly I

<!--
 * @Description: 
 * @Date: 2021-06-23 09:48:13
 * @LastEditTime: 2021-07-07 10:32:52
 * @FilePath: \one\src\components\Child1.vue
-->
<template>
  <div>
    I'm floor one
  </div>

  <div class="popup"
       v-if="showPopp">
    <h3>I'm pop-up one</h3>
    <div>
      <button @click='cancle'>cancel</button>
      <button @click='confirm'>determine</button>
    </div>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onBeforeMount, ref } from 'vue'
import emitter from '../mitt'

export default defineComponent({
  name: '',
  setup() {
    //Control pop-up display
    const showPopp = ref(false)
    //Used to change the state of promise externally
    let fullfilledFn
    //Asynchronous task
    const taskA = () => {
      return new Promise((resolve, reject) => {
        showPopp.value = true
        fullfilledFn = () => {
          resolve()
        }
      })
    }
    //Logic of cancellation
    const cancle = () => {
      showPopp.value = false
      fullfilledFn()
    }
    //Logic of confirmation
    const confirm = () => {
      showPopp.value = false
      fullfilledFn()
    }
    onBeforeMount(() => {
      //Conditional judgment if
      emitter.emit('add-async-tasts', { fun: taskA, order: 1 })
    })
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>


Sub assembly II

<!--
 * @Description: 
 * @Date: 2021-06-23 18:46:29
 * @LastEditTime: 2021-07-07 10:33:11
 * @FilePath: \one\src\components\Child2.vue
-->
<template>
  <div>
    This is floor two
  </div>
  <div class="popup"
       v-if="showPopp">
    <h3>I'm pop-up two</h3>
    <div>
      <button @click='cancle'>cancel</button>
      <button @click='confirm'>determine</button>
    </div>
  </div>
</template>

<script lang='ts'>
import { defineComponent, onBeforeMount, ref } from 'vue'
import emitter from '../mitt'

export default defineComponent({
  name: '',
  setup() {
    //Used to change the state of promise externally
    let fullfilledFn

    //Control pop-up display
    const showPopp = ref(false)

    //Asynchronous task
    const taskB = () => {
      return new Promise((resolve, reject) => {
        showPopp.value = true
        fullfilledFn = () => {
          resolve()
        }
      })
    }
    //Logic of cancellation
    const cancle = () => {
      showPopp.value = false
      fullfilledFn()
    }
    //Logic of confirmation
    const confirm = () => {
      showPopp.value = false
      fullfilledFn()
    }
    onBeforeMount(() => {
      //Conditional judgment if
      emitter.emit('add-async-tasts', { fun: taskB, order: 2 })
    })
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>


This plan is the first time to meet this demand. It's certainly not the best plan, but it's also a move. I hope you guys can point out a better and simpler scheme.

reference resources

How to control its status outside Promise

Topics: Javascript Vue queue