I have introduced the use of Vue3 and mitt.js in another article

Some Vue3 points (550+👍)

Demand is introduced

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

The page is similar to the welfare page of the personal center. Each floor displays the picture of the corresponding gift package. After the user enters the page, a popup window for receiving the gift package will pop up automatically if the conditions are met.

Control pop-ups for each package to show the hidden state written separately in the respective component, now required

💡 can only show one popover at a time

💡 No matter click ok or cancel, after closing the previous popover, the second popover will open automatically

💡 can control the pop-up display order

The solution

Technology stack

  • Vue3
  • mitt.js
  • Promise

Train of thought

Each popup is treated as an asynchronous task, and a task queue is constructed in a preset order. Then, click the button to manually change the status of the current asynchronous task and enter the next asynchronous task.

Step one

Let’s write two components to simulate the real world

Parent component (page component)

<template>
  <div>
    <h1>I'm the parent!</h1>
    <child-one></child-one>
    <child-two></child-two>
    
    <div class="popup"
     v-if="showPopp">
      <h1>I'm the parent component popover</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 the popup display
    const showPopp = ref(false)
    return {
      showPopp,
    }
  },
})
</script>
Copy the code

A child components

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

    <div class="popup"
       v-if="showPopp">
        <h3>I'm popover 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 the popup display
    const showPopp = ref(false)
    // Cancel logic
    const cancle = () = > {
      showPopp.value = false
      //do something
    }
    // The validation logic
    const confirm = () = > {
      showPopp.value = false
      //do something
    }
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>
Copy the code

Child component 2

The logic of the popover is exactly the same as that of the subcomponent. In fact, the logic should be extracted into a hook. Here, for the convenience of demonstration, I will write it directly.

<template>
    <div>I'm floor two</div>
    
    <div class="popup"
       v-if="showPopp">
        <h3>I'm popover 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 the popup display
    const showPopp = ref(false)
    // Cancel logic
    const cancle = () = > {
      showPopp.value = false
      //do something
    }
    // The validation logic
    const confirm = () = > {
      showPopp.value = false
      //do something
    }
    return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>
Copy the code

The result is shown below

Step 2

Instead of using popovers, let’s go through the timer andconsole.logTo simulate asynchronous tasks

The parent component

// omit some of the previous code
setup(){...// An asynchronous task to be handled separately by the parent component
    const taskC = () = > {
      return new Promise((resolve, reject) = > {
        setTimeout(() = > {
          console.log('Parent component's asynchronous task')},1000)
      })
    }
    
    onMounted(() = > {
      taskC()
    })
    ......
  },
Copy the code

A child components

// omit some of the previous code
setup(){...const taskA = () = > {
      return new Promise((resolve, reject) = > {
        setTimeout(() = > {
          console.log('Asynchronous task for child component one')},1000)
      })
    }
    onBeforeMount(() = > {
      taskA()
    })
    ......
  },
Copy the code

Child component 2

// omit some of the previous code
setup(){...const taskB = () = > {
      return new Promise((resolve, reject) = > {
        setTimeout(() = > {
          console.log('Asynchronous task for child component two')},1000)
      })
    }
    onBeforeMount(() = > {
      taskB()
    })
    ......
},
Copy the code

Take a look at the result. Since the task queue has not been built yet, all asynchronous tasks are executed at the same time, so the logs of three components are printed out at the same time

Step 3

usemitt.jsTo collect asynchronous tasks

Start by wrapping mit.js as a utility function

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

export default emitter;
Copy the code

The add-Async-tasts event is raised before the child component is mounted, notifies the parent component to collect the asynchronous task, listens for the Add-Async-tasts event in the parent component, and stores the child component’s task into an array.

The parent component

// omit some of the previous code
setup(){...// Declare an empty array to hold all asynchronous tasks
    let asyncTasks = []
    
    // Add asynchronous tasks to the array collect all asynchronous tasks
    const addAsyncTasts = (item) = > {
      asyncTasks.push(item)
      console.log('🚀 🚀 ~ asyncTasks:', asyncTasks)
    }
    
    // Listen for the add-async-tasts event and add the asynchronous task to the array when it is triggered
    emitter.on('add-async-tasts', addAsyncTasts)
    
    // The listening event is removed when the component is uninstalled, and the array is reset to empty
    onUnmounted(() = > {
      emitter.off('add-async-tasts', addAsyncTasts)
      asyncTasks = []
    })
    .......
Copy the code

A child components

// omit some of the previous code
setup(){... onBeforeMount(() = > {
      // if... The parent component is notified of the collection task if the condition is met here
      emitter.emit('add-async-tasts', taskA)
    })
    .......
Copy the code

Child component 2

// omit some of the previous code
setup(){... onBeforeMount(() = > {
      // if... The parent component is notified of the collection task if the condition is met here
      emitter.emit('add-async-tasts', taskB)
    })
    .......
Copy the code

Looking at the result, I have logged the parent component’s collection function, and you can see that the collection function is triggered twice

If you click on it, you can see that there are two data points in it, namely taskA and taskB. It means our missions are already collected.

Step 4

Customize the task order

The way I implement this is to pass in an extra numeric parameter when collecting tasks, and finally sort the task queue by numeric size.

The parent component

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

A child components

// omit some of the previous code
setup(){... onBeforeMount(() = > {
      // if... The parent component is notified of the collection task if the condition is met here
      emitter.emit('add-async-tasts', { fun: taskA, order: 1})})...Copy the code

Child component 2

// omit some of the previous code
setup(){... onBeforeMount(() = > {
      // if... The parent component is notified of the collection task if the condition is met here
      emitter.emit('add-async-tasts', { fun: taskB, order: 2})})...Copy the code

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

Let’s change the order of child component 1 to 3 to verify that the result is correct

You can see that taskA is behind taskB, indicating that our custom order of asynchronous tasks has also been implemented.

Step 5

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

The parent component

// omit some of the previous code
setup(){...To ensure that all tasks are collected, we execute the queue in the onMounted cycle
    //mounted Does not ensure that all child components are mounted together. If you want to wait until the entire view is rendered, you can use nextTick inside Mounted
    onMounted(() = > {
      nextTick(() = > {
        // Build the queue
        const queue = async (arr) => {
          for (let item of arr) {
            await item.fun()
          }
          // Return a completed state promise to continue the chain call
          return Promise.resolve()
        }

        // Execute queue
        queue(asyncTasks)
          .then((data) = > {
            // After all the child component tasks are completed, the parent component tasks are performed
            // If you want to start the parent component's task first, you can define order as 0 and store it in the task queue
            return taskC()
          })
          .catch((e) = > console.log(e))
      })
    })
 })
    .......
Copy the code

Looking at the result, you can see that all the tasks are done in order.

Step 6

Modify the code with a real popover scenario

Let’s start with a quick look at promises, the use of which is not covered in this article

The state of the Promise object is unaffected.

The Promise object represents an asynchronous operation with three states:

  • Pending
  • Resolved, also known as a pity
  • Rejected (failed)

Only the result of an asynchronous operation can determine the current state, and no other operation can change the state. That’s where the name “Promise” comes from. Its English name means “Promise,” indicating that nothing else can change it.

But practice has shown that you can manually change the state of a Promise externally

See the following article for details 👉 How to control the state of a Promise externally

Now that you can change it, add code to manually change the Promise state in the button click event of the child component

// omit some of the previous code
setup(){...// Used externally to change the state of the promise
    let fullfilledFn
    // Asynchronous tasks
    const taskA = () = > {
      return new Promise((resolve, reject) = > {
        showPopp.value = true
        fullfilledFn = () = > {
          resolve()
        }
      })
    }
    // Cancel logic
    const cancle = () = > {
      showPopp.value = false
      fullfilledFn()
    }
    // The validation logic
    const confirm = () = > {
      showPopp.value = false
      fullfilledFn()
    }
 })
    .......
Copy the code

Finally, let’s look at the results

All the code

Post the entire code at the end

The 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'm the parent!</h1>
    <child-one></child-one>
    <child-two></child-two>
  </div>
  <div class="popup"
       v-if="showPopp">
    <h1>I'm the parent component popover</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 the popup display
    const showPopp = ref(false)
    // Sort function
    const compare = (property) = > {
      return (a, b) = > {
        let value1 = a[property]
        let value2 = b[property]
        return value1 - value2
      }
    }

    // An asynchronous task to be processed 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 collect all asynchronous tasks
    const addAsyncTasts = (item) = > {
      asyncTasks.push(item)
      asyncTasks = asyncTasks.sort(compare('order'))
      console.log('🚀 🚀 ~ asyncTasks:', asyncTasks)
    }

    // Listen for the addAsyncTasts event to add an asynchronous task to the array when it is fired
    emitter.on('add-async-tasts', addAsyncTasts)

    // Mounted does not guarantee that all child components will be mounted at once. If you want to wait until the entire view is rendered, you can use nextTick inside Mounted
    onMounted(() = > {
      nextTick(() = > {
        // Build the queue
        const queue = async (arr) => {
          for (let item of arr) {
            await item.fun()
          }
          return Promise.resolve()
        }

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

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

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


Copy the code

A child components

<! -- * @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 popover 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 the popup display
    const showPopp = ref(false)
    // Used externally to change the state of the promise
    let fullfilledFn
    // Asynchronous tasks
    const taskA = () = > {
      return new Promise((resolve, reject) = > {
        showPopp.value = true
        fullfilledFn = () = > {
          resolve()
        }
      })
    }
    // Cancel logic
    const cancle = () = > {
      showPopp.value = false
      fullfilledFn()
    }
    // The validation logic
    const confirm = () = > {
      showPopp.value = false
      fullfilledFn()
    }
    onBeforeMount(() = > {
      // if...
      emitter.emit('add-async-tasts', { fun: taskA, order: 1})})return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>


Copy the code

Child component 2

<! -- * @Description: * @Date: 2021-06-23 18:46:29
 * @LastEditTime: 2021-07-07 10:33:11
 * @FilePath: \one\src\components\Child2.vue
-->
<template>
  <div>I'm floor two</div>
  <div class="popup"
       v-if="showPopp">
    <h3>I'm popover 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 externally to change the state of the promise
    let fullfilledFn

    // Control the popup display
    const showPopp = ref(false)

    // Asynchronous tasks
    const taskB = () = > {
      return new Promise((resolve, reject) = > {
        showPopp.value = true
        fullfilledFn = () = > {
          resolve()
        }
      })
    }
    // Cancel logic
    const cancle = () = > {
      showPopp.value = false
      fullfilledFn()
    }
    // The validation logic
    const confirm = () = > {
      showPopp.value = false
      fullfilledFn()
    }
    onBeforeMount(() = > {
      // if...
      emitter.emit('add-async-tasts', { fun: taskB, order: 2})})return {
      showPopp,
      cancle,
      confirm,
    }
  },
})
</script>


Copy the code

This plan is the first time to meet such a need, beat the head to come up with, certainly not the best plan, but it is a move. I hope you can give me a better and simpler plan.

reference

How do I control the state of a Promise externally