Written first: The goal of this article
The implementation principle of Vue3's life cycle is relatively simple, but to understand the entire life cycle of Vue3, you must also combine the operation principle of Vue. Because some of the execution mechanisms of Vue3's life cycles are completed through the scheduler of Vue3, you must also combine the implementation principle of Vue3's scheduler to fully understand the life cycle principle of Vue3. At the same time, through understanding the Vue3 scheduler, we can deepen our understanding of some design principles and rules of Vue. Therefore, the goal of this article is to understand the principles of Vue3 life cycle Hooks and understand the principles of Vue3 scheduler through the operation of Vue3 life cycle Hooks.
The implementation principle of Vue3 life cycle
The implementation principle of the life cycle Hooks function of Vue3 is relatively simple. It is to mount or register the functions of each life cycle on the component instance, and then wait until the component runs to a certain moment, then go to the component instance to take out the corresponding life cycle functions to execute.
Let's take a look at the implementation of the specific code
Life cycle type
// packages/runtime-core/src/ export const enum LifecycleHooks { BEFORE_CREATE = 'bc', // Before creation CREATED = 'c', // Create BEFORE_MOUNT = 'bm', // Before mount MOUNTED = 'm', // After mount BEFORE_UPDATE = 'bu', // Before update UPDATED = 'u', // After the update BEFORE_UNMOUNT = 'bum', // Before uninstall UNMOUNTED = 'um', // After uninstall // ... }
Creation of Hooks functions in each life cycle
// packages/runtime-core/src/ export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT) export const onMounted = createHook() export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE) export const onUpdated = createHook() export const onBeforeUnmount = createHook(LifecycleHooks.BEFORE_UNMOUNT) export const onUnmounted = createHook()
You can see that the Hooks function for each life cycle is created through the createHook function.
Create life cycle function createHook
// packages/runtime-core/src/ export const createHook = (lifecycle) => (hook, target = currentInstance) => injectHook(lifecycle, hook, target)
createHook is a closure function that caches the current life cycle of Hooks. Target indicates which component instance the Hooks function is bound to. By default, it is the current working component instance. The underlying layer of createHook calls another injectHook function, so let's continue to take a look at this injectHook function.
injectHook function
injectHook is a closure function that binds the corresponding life cycle Hooks to the corresponding component instance through the closure cache.
// packages/runtime-core/src/ export function injectHook(type, hook, target) { if(target) { // Mount the Hooks function of each life cycle to the component instance and is an array, because you may call the same life cycle function of the same component multiple times const hooks = target[type] || (target[type] = []) // Package the life cycle function and cache the wrapper function on __weh const wrappedHook = hook.__weh || (hook.__weh = (...args: unknown[]) => { if () { return } // When the life cycle is called, ensure that the currentInstance is correct setCurrentInstance(target) // Execute the life cycle Hooks function const res = args ? hook(...args) : hook() unsetCurrentInstance() return res }) // Bind the lifecycle wrapper function to the hooks corresponding to the component instance (wrappedHook) // Return to the wrapper function return wrappedHook } }
Calls of life cycle Hooks
= effect(() => { if (!) { const { bm, m } = instance // Life cycle: beforeMount hook if (bm) { invokeArrayFns(bm) } // This will be executed when the component is initialized // Why do you need to call the render function here? // It is because the call to render within the effect can trigger dependency collection // This function will be triggered again after the value of the responsive formula changes. const subTree = ( = renderComponentRoot(instance)) patch(null, subTree, container, instance, anchor) = = true // Life cycle: mounted if(m) { // mounted needs to be called through Scheduler's function queuePostFlushCb(m) } } else { // The logic will be executed from here after the responsive value changes // It mainly takes the new vnode and compares it with the previous vnode // Get the latest subTree const { bu, u, next, vnode } = instance // If next is present, it means that the component's data needs to be updated (props, slots, etc.) // First update the component's data, and then after the update is completed, continue to compare the child elements of the current component if(next) { = updateComponentPreRender(instance, next) } // Life cycle: beforeUpdate hook if (bu) { invokeArrayFns(bu) } const subTree = renderComponentRoot(instance) // Replace the previous subTree const prevSubTree = = subTree // Handle the old vnode and new vnode to patch to handle patch(prevSubTree, subTree, container, instance, anchor) // Life cycle: updated hook if (u) { // updated needs to be called through Scheduler's function queuePostFlushCb(u) } } }, { scheduler() { queueJobs() } })
The above is after instantiating the Vue3 component, and wrapping an updated side effect function through effect to perform dependency collection with responsive data. There are two branches in this side effect function. The first one is executed before the component is mounted, which is where the life cycle functions beforeMount and mount are called, and the second branch is executed when the component is updated after the component is mounted, which is where the life cycle functions beforeUpdate and updated are called. Specifically, before mounting, before generating the virtual DOM, the beforeMount function is executed, and after the virtual DOM is generated, the component has been mounted on the page, that is, the view is displayed on the page, and then the mount function is executed; when updating, before obtaining the updated virtual DOM, then obtaining the updated virtual DOM, then patching, updating the view, and then performing update. It should be noted that beforeMount and beforeUpdate are executed synchronously, and are both called through invokeArrayFns. invokeArrayFns function
export const invokeArrayFns = (fns: Function[], arg?: any) => { for (let i = 0; i < ; i++) { fns[i](arg) } }
Component mounts and updates are asynchronous and need to be handled through Scheduler.
Vue3 Scheduler Principle
Some APIs in Vue3, such as the component's life cycle API, watch API, and component update callback functions are not executed immediately, but are placed in the asynchronous task queue and then executed according to certain rules. For example, the task queue also exists at the same time, the watch task, the component update task, and the life cycle task, what is its execution order? This is determined by the scheduler's scheduling algorithm. At the same time, the scheduling algorithm only schedules the order of execution and is not responsible for specific execution. The advantage of this design is that even if Vue3 adds a new asynchronous callback API in the future, there is no need to modify the scheduling algorithm, which can greatly reduce the coupling between Vue API and queues. Vue3's Scheduler provides three APIs for listing:
queuePreFlushCb API: Join Pre queue Execute before component update
export function queuePreFlushCb(cb: SchedulerJob) { queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex) }
queueJob API: Join queue queue component update execution
export function queueJob(job: SchedulerJob) { }
queuePostFlushCb API: Join Post Queue and execute after component update
export function queuePostFlushCb(cb: SchedulerJobs) { queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex) }
Since Vue3 only provides an API for inclusion and does not provide an API for delisting, we can only control when to enroll, and when to delist is controlled by the Vue3 scheduler itself.
So how does the Vue3 scheduler control the delisting method? Actually, it's very simple.
function flushJobs(seen?) { isFlushPending = false // Queue execution before component update flushPreFlushCbs(seen) try{ // Component update queue execution let job while (job = ()) { job &amp;&amp; job() } } finally { // The queue is executed after the component is updated flushPostFlushCbs(seen) // If a new queue is generated during the execution of the asynchronous task, then continue to callback execution if ( || || ) { flushJobs(seen) } } }
The execution order of the life cycle of Vue parent-child components
There are two concepts that need to be clarified here: one: the execution order of parent-child components, and two: the execution order of parent-child components life cycles. These two are different
The execution order of parent-child components
This is to execute the parent component first and then execute the child component, first instantiate the parent component, then obtain the virtual DOM of the parent component, and then in the patch process, if there is a virtual DOM of component type, that is, a child component, then the component initialization process will be carried out in the patch branch, and this cycle will be performed.
The execution order of parent-child component life cycle
The execution order of the parent-child component life cycle is executed according to Vue's rules through the scheduling algorithm under the execution order of the parent-child component. First, the parent component is instantiated and executed. Through the above life cycle call instructions, we can know that when the parent component is updated for the first time, that is, when the component is initialized, it first executes the beforeMount of the parent component, and then obtains the virtual DOM of the parent component. Then when the virtual node is component type during patching, it will go through the component initialization process. At this time, it is actually the child component initialization. Then the child component also needs to go through all the components processes. When the child component updates the update for the first time, it first executes the beforeMount of the child component, then obtains the virtual DOM of the child component, and then patches the virtual DOM of the child component. If the node is component type in the process, go through the component initialization process until the child component patch is completed, then executes the mounted life cycle function of the child component, and then return to the parent component's execution stack to execute the mounted life cycle of the parent component.
Therefore, when initializing the creation, it is a process of deeply recursively creating child components. The execution order of the life cycle of parent and child components is:
- Parent component -> beforeMount
- Subcomponents -> beforeMount
- Subcomponents -> mounted
- Parent component -> mounted
The parent-child component update order is also a process of deep recursive execution:
- If the parent and child components do not pass data through props, then when updating, each will execute its own update lifecycle function.
- If the parent and child component exists to pass data through props, the parent component must be updated first before the child component is updated. Because before the parent component DOM is updated, the props of the child component need to be modified, and the props of the child component are the correct value.
Let's look at the source code below
if (next) { = // Before the component is updated, update some data first updateComponentPreRender(instance, next, optimized) } else { next = vnode }
For example, update props and slots
const updateComponentPreRender = ( instance: ComponentInternalInstance, nextVNode: VNode, optimized: boolean ) =&gt; { = instance const prevProps = = nextVNode = null // Update props updateProps(instance, , prevProps, optimized) // Update slots updateSlots(instance, , optimized) // ... }
Therefore, when the parent-child component is updated, the life cycle execution order of the parent-child component is:
- Parent component -> beforeUpdate
- Subcomponents -> beforeUpdate
- Subcomponents -> updated
- Parent component -> updated
When uninstalling, the parent-child component is also a process of deep recursive traversal execution:
- Parent component -> beforeUnmount
- Subcomponents -> beforeUnmount
- Subcomponents -> unmounted
- Parent component -> unmounted
What are you uninstalling when the component is uninstalled?
When uninstalling components, the main thing is to uninstall template references, clear the side effect functions of the update functions of related components saved in effect. If it is a cache component, clear the related cache, and finally remove the relevant nodes on the real DOM.
In addition, component DOM update() is stored in the scheduler's task queue. When component uninstallation, the relevant component update() setting needs to be invalidated.
In the unmountComponent function in the source code, there is a paragraph:
if (update) { // Update the component function's active setting false = false unmount(subTree, instance, parentSuspense, doRemove) }
Then when Scheduler executes queue queue tasks, those whose active jobs are false will not be executed
const job = queue[flushIndex] // Those jobs whose active is false will not be executedif (job &amp;&amp; !== false) { callWithErrorHandling(job, null, ) }
So when will the component DOM update() be deleted?
The deleted settings can be found in the updateComponent function in the source code
invalidateJob() // Execute the update task immediately()
Scheduler deletes tasks
export function invalidateJob(job: SchedulerJob) { // Find the index of the job const i = (job) if (i &gt; flushIndex) { // Delete Job (i, 1) } }
From this we can know that when a component is updated, the update task of the component in the scheduler will be deleted first. Because component update is also a process of recursive execution of updates, and when the subcomponent update is performed during the recursive process, the subcomponent update task in the scheduler's task queue does not need to be executed anymore, so it must be deleted. In the future, the responsive data dependent on by the subcomponent occurs, and the subcomponent update task is put again in the scheduler's task queue.
The difference between invalidation and deletion of queue tasks in the scheduler for component updates
Through the above introduction to component uninstallation, we can summarize the difference between failure and deletion of queue tasks in the scheduler for component updates.
Ineffective
- When component uninstallation, set the job to invalidate, and when the job is removed from the queue, it will no longer be executed.
- Can't join the queue again because it will be dereduced
- The uninstalled component will not update no matter how the responsive variable it depends on.
delete
- When a component is updated, delete the job of the component in the scheduler task queue.
- You can join the queue again
- Delete the task, because it has been updated, no repeated updates are required. If the dependent responsive variable is modified again, it still needs to be added to the scheduler's task queue, waiting for update
The relationship between parent-child component execution order and scheduler
Suppose there is a scenario where there is a pair of parent-child components. The child component uses the watch API to listen to the responsive data of a child component and then modify the responsive data of N parent components. Then the update functions of N parent components will be placed in the scheduler's task queue waiting for execution. In this case, how can the scheduler ensure that the update function of the top-level parent component is executed first?
Let's first look at the data structure of the Job in the task queue of the scheduler
export interface SchedulerJob extends Function { id?: number // Used to sort jobs in the queue, execute first if the id is small active?: boolean computed?: boolean allowRecurse?: boolean ownerInstance?: ComponentInternalInstance }
A Job is a function with some properties. Where id represents priority, is used to implement queue queue insertion. If the id is small, execute first. Active Through the above, we can know whether the active indicates whether the job is valid. If the invalid job is not executed, the job will be invalidated. If the component uninstallation occurs, it will cause the job to be invalid.
Data structure of scheduler task queue
const queue: SchedulerJob[] = []
is an array
Execution of scheduler task queue
// Sort by task id size((a, b) =&gt; getId(a) - getId(b)) try { for (flushIndex = 0; flushIndex &lt; ; flushIndex++) { const job = queue[flushIndex] if (job &amp;&amp; !== false) { // Use an error handling function with Vue internals to execute the job callWithErrorHandling(job, null, ) } } } finally { // Clear the queue queue flushIndex = 0 = 0 }
So how do you ensure that the task id of the update function of the parent component is the smallest?
By looking at the source code, we can see that there is a uid property in the createComponentInstance function that creates component instances, and its initial value is 0, followed by ++
let uid = 0 // Initialize to 0export function createComponentInstance( vnode parent suspense ) { const instance: ComponentInternalInstance = { uid: uid++, // ... }
Then when creating the component update function, you can see that the id of the component update function is the uid of the component instance
const update = ( = (effect) as SchedulerJob) =
The process of component creation is a process of deeply recursively creating child components, so the first parent component is 0, and the subsequent child components go up all the way, which ensures that the task id of the child component's update function must be greater than the id of the parent component's update function. Therefore, when there are many component update functions in the scheduler's task queue, through priority sorting, it can be ensured that the update functions of the parent component are executed first.
You can also go to the queue in the middle of the journey
export function queueJob(job: SchedulerJob) { // If there is no id, push to the end if ( == null) { (job) } else { // Perform queue processing (findInsertionIndex(), 0, job) } queueFlush() }
The nature of Hooks
Finally, let’s discuss the nature of Hooks
Vue's Hooks design is borrowed from React's Hooks. The essence of React's Hooks is to store state variables and side-effect functions on the fiber object of the function component. When the state variable changes in the future, the relevant function component fiber will be updated again. The implementation principle of Vue3 is similar. Through the implementation principle of Hooks in the life cycle above, we can know that the Hooks in the life cycle of Vue3 are bound to specific component instances, and state variables, because Vue variables are responsive, state variables will be collected through effect and specific component update functions for dependency, and then bound. When the state variables change in the future, the corresponding component update function will re-enter the scheduler's task queue for dispatching and execution.
Therefore, the essence of Hooks is to bind those state variables or life cycle functions to components, and the components run to the corresponding moment to execute the corresponding bound life cycle functions. When those bound variables change, the corresponding components will also be updated again.
at last
In the next article, I will write about the implementation principles of the watch API. At the same time, the watch API also needs to be understood in combination with the scheduler. Only by understanding it in series can the underlying design and implementation principles of Vue3 be understood more thoroughly.
Finally, I recommend a library to learn vue3 source code. It is based on the open source library mini-vue of Cui Xiaorui. It implements more core functions of vue3 on the basis of mini-vue, and is used to deeply learn vue3, allowing you to understand the core logic of vue3 more easily.
Source code address/amebyte/mini-vue3-plus
The above is the detailed content of the relationship between the Vue3 life cycle Hooks principle and the scheduler Scheduler. For more information about the relationship between the Vue3 Hooks and Scheduler, please pay attention to my other related articles!