The previous article described the core implementation of Vue in detail. Subsequent articles will expand on the details not covered.
The implementation of Vue’s nextTick API is one of the better understood parts of Vue, and is very decoupled from the rest of the code, so there are many related source code parsing articles in this section. I was not going to write a separate blog about this part, but RECENTLY I happened to read someone else’s article: In the event loop of each round, one task is executed at a time, and after all the microtasks in the microTask queue are executed, the UI rendering is performed. But the author doesn’t seem so sure about this conclusion either. My first thought was that Vue’s $nextTick was used in the MutationObserver (MO’s callbacks are placed in the microTask queue). I thought I would look at Vue’s $nextTick to see if this was the case, and also to see if I could prove that UI Render was actually executed after the MicroTask queue emptied.
Conclusion: my previous understanding of the $nextTick source code was completely wrong, and UI Render will be executed after each event loop executes all microtasks.
Task/MacroTask and microtask concepts since last yearThis question was raised on ZhihuSince then, task and microtask have been known to many students, and I also saw the content of Microtask at that time. Now there are many Chinese introduction blogs introducing this part of knowledge, recentlyThis article is popular among nuggets, SF and ZhihuFinally, the concept of microtask was examined. If you haven’t read task/ MicroTask, I recommend this oneEnglish blogIs the content source of most blogs in China.
Let’s start with the implementation of nextTick
120 seconds introduction to MutationObserver: MO is a new API in HTML5 that monitors DOM changes. It can listen for child node deletions, property changes, text content changes, and so on on a DOM object. The call is simple, but a little unusual: you need to bind a callback to it first: var mo = new MutationObserver(callback) You get an instance of mo by passing a callback to the constructor of mo, which is triggered when the instance listens for changes. At this point, you’re just binding the MO instance with a callback. You haven’t set which DOM it listens for, node deletions, or property changes. This is accomplished by calling his Observer method:
Var domTarget = mo.observe(domTarget, {characterData: true // })Copy the code
One detail that needs to be mentioned first is that callbacks to MutationObserver are executed in MicroTask.
Ok, now text changes on the domTarget will be heard by mo, which will trigger the callback you passed in the New MutationObserver(callback).
Now let’s look at vue. nextTick source code:
export const nextTick = (function () { var callbacks = [] var pending = false var timerFunc function nextTickHandler () {pending = false} // The reason why we slice a copy is because some cb execution processes add content to the callbacks // for example, $nextTick callback has $nextTick // These should be implemented in the next nextTick, Var copies = callbacks. Slice (0) callbacks = [] for (var I = 0; i < copies.length; I ++) {copies[I]()}} /* Istanbul ignore if */ / If (typeof MutationObserver! == 'undefined' && ! HasMutationObserverBug) {var counter = 1 / / create a MutationObserver, the observer after listening to the dom changes executed after the callback nextTickHandler var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(counter) // Observe. observe(textNode, {characterData: True}) // timerFunc will switch the contents of the text node between 0 and 1 each time it is executed. TimerFunc = function () {counter = (counter + 1) % 2 TextNode.data = counter } } else { // webpack attempts to inject a shim for setImmediate // if it is used as a global, So we have to work around that to avoid bundling unnecessary code. // Webpack will introduce setImmediate into the code by default Do not use MutationObserver to mediate? Do not use setTimeout const context = inBrowser? window : typeof global ! == 'undefined' ? global : {} timerFunc = context.setImmediate || setTimeout } return function (cb, ctx) { var func = ctx ? function () { cb.call(ctx) } : Cb callbacks. Push (func) // If pending is true, timerFunc(nextTickHandler, 0) if (pending) return pending = true timerFunc(nextTickHandler, 0) } })()Copy the code
The function generated after the above function is executed is the nextTick. The pending function initializes the pending variable and the cb variable. Cb is used to store the callback to be executed. Pending indicates whether the nextTickHandler function that clears the callback should be added to the asynchronous queue.
Then you create a MO that listens for changes in the text content of a newly created text node, and the callback to that change is the nextTickHandler. NextTickHandler iterates through the CB array and executes each cb that needs to be executed.
The function returned as nextTick is simpler:
function (cb, ctx) { var func = ctx ? function () { cb.call(ctx) } : Cb callbacks. Push (func) // If pending is true, timerFunc(nextTickHandler, 0) if (pending) return pending = true timerFunc(nextTickHandler, 0) } }Copy the code
TimerFunc () (nextTickHandler, 0), timerFunc(), timerFunc(), timerFunc() Those are the two parameters that matter. TimerFunc simply changes the content of the text node that the MO is listening to, so that if I change the content of the text, the MO will execute the callback after all the current synchronization code is complete, thus performing the task of updating the data to the DOM.
When I first looked at this code, I forgot that the MutationObserver callback was executed in MicroTask. I had not read the other Vue source code at that time, and after I had a general understanding of the nextTick code flow, I formed the following understanding, which seemed to perfectly explain the code logic:
Watcher will immediately modify the DOM after monitoring the data changes. Then the nextTick in the user-written code will be executed, and the nextTick will also modify the DOM(textNode). When the last textNode modification is complete, The callback to the MutationObserver is triggered, which means that the previous DOM changes have already been made, so that nextTick’s promise to the user to update the DOM before executing the user’s callback is fulfilled.
Damn, now that I’ve read Batcher’s code and reflected on it, I realize that the idea above is totally shit. Totally shit!
First, it is common knowledge that DOM Tree modifications are real-time, whereas modifications Render onto the DOM asynchronously. There is no such thing as waiting for the DOM to change. I know that any time I add an element to the DOM and change the textContent of a DOM in the previous line of code, you can read the new DOM immediately in the next line of code. But I still don’t know why I got the idea of using nextTick to make sure THE DOM changes were done. Maybe I ate a little too much shit that day.
Second, let’s look at the real reasons to use nextTick:
Vue uses the above nextTick in two places:
- Vue.nexttick and vue.prototype.$nextTick both use this nextTick directly
- In the Batcher, what Watcher does when he sees a change in the data
nextTick(flushBatcherQueue)
.flushBatcherQueue
Is responsible for performing all DOM update operations.
The Batcher source code, which I analyzed in detail in the last article, is illustrated here with a diagram to illustrate the detailed processing of it and nextTick.
If we follow the code execution carefully, we can see that the batch update is actually executed in MicroTask, and the nextTick(CB) that the user executed after modifying the data is also executed in cb, they are all executed in the same MicroTask. It’s not at all what I initially thought of as putting the callback into a later event loop.
I don’t want MO to actually listen for DOM changes, I just want an asynchronous API to execute the asynchronous callbacks I want when the current synchronous code is finished executing.
The reason for this is that it is possible for the user’s code to change the data multiple times, and each change will be notified synchronously to all the Watcher subscribing to the data, whereas writing the data to the DOM at once will not work, which is simply adding the Watcher to the array. By the time the current task is completed and all the synchronization code has been completed, this round of data modification has been completed. At this point, I can safely write the data to the DOM of the watcher that listens for the dependency change. So even if you changed a Watcher dependency 100 times in a previous task, I would only end up evaluating the value once and changing the DOM once. On the one hand, unnecessary DOM modification is eliminated; on the other hand, DOM operations are aggregated to improve DOM Render efficiency.
So why use a MutationObserver? No, it doesn’t have to be MO, as long as it’s microtask. Resolve ().then(nextTickHandler) is preferred in the latest version of the Vue source code to place asynchronous callbacks into microtasks (MO is buggy in webviews above IOS9.3). MO is only used when native promises are not available.
This shows that MicroTask is what nextTick is all about, and MO is just a backup. If there was a microTask with a higher priority and better browser compatibility than MO, it would probably take MO down in a minute. Any microTask-type asynchronous callback can fulfill the role of MO in existing code. Even more imaginative, if all browsers now support yield and generator, I could yield the watcher, queue the watcher in the coroutine, and execute a function that triggers a microTask callback. The coroutine then hands back control of the function to the generator, which is also fine. Anyway, the intent is to tell you that Vue only uses MicroTask.
Then again, why microtask? Is task ok? (MacroTask and task are the same thing, and the HTML5 standard doesn’t even have the word macrotask, so don’t bother.)
Ha, now there’s an example where Vue initially changed the implementation of nextTick. Let’s look at these two Jsfiddleds: jsfiddle1 and jsfiddle2.
Both fiddle implement the same fiddle, which gives the absolute position of the yellow element a fixed position by binding the Scroll event to the top property of the absolute position element every time it is rolled. If you want to scroll a few times, you’ll see that the yellow element in the first fiddle is stable, and the fixed element is good. The last fiddle is a problem, because the yellow element is bouncing up and down, and it doesn’t seem to be keeping up with our scroll, which is always a little slower, even though we end up in the right position.
The above two examples are actually found in this issue. The first version of JsFiddle is Vue 2.0.0-RC. 6. The nextTick implementation of this version uses MO. PostMessage, which is Vue 2.0.0-RC.7 used for the latter fiddle. Later, Yu Creek learned that Window. postMessage was the MacroTask queue to put callbacks into. This is the root of the problem.
HTML UI events, network events, and HTML Parsing were all completed using tasks, so after each Scroll event was triggered, In the current task, we just queue the watcher and pass the flushBatcherQueue empting the watcher into nextTick as an asynchronous callback.
If nextTick uses microTasks, all microtasks will be executed immediately after the task is finished, and the flushBatcherQueue will complete immediately. Then, when the microtasks in the current round are all cleaned, Perform UI rendering and actually update things like rearrange and redraw onto the DOM (more on that later). Note that scrolling does not need to be redrawn. Redraw is when you modify the UI style, DOM structure, etc., and the page displays the style, don’t get confused. If nextTick were using tasks, the flushBatcherQueue would be processed in a future task execution only after the current task and all microTasks had finished executing. At that point, DOM changes for each instruction would actually be executed, but by then it would be too late. Missed multiple redraw/render UI triggers. In addition, the browser may have multiple task queues in order to respond more quickly to the user UI:
For example, a user agent could have one task queue for mouse and key events (the user interaction task source), and another for everything else. The user agent could then give keyboard and mouse events preference over other tasks three quarters of the time, keeping the interface responsive but not starving other task queues, and never processing events from any one task source out of order.
The priority of the UI task queue may be higher, so the window.postMessage task adopted by Yuxi may not even execute the Window. postMessage task that has executed the UI for many times, which leads to the delay of DOM update. In the case of heavy CPU calculations and UI rendering tasks, it is entirely possible for this delay to reach the level of 100 milliseconds to 1 second observed in the issue. As a result, implementing nextTick using task was not feasible, and Yu Creek withdrew this change. Subsequent nextTick implementations still use Promise. Then and MO.
Task Microtask and UI Render after each event loop
I recently read the HTML5 specification carefully, but I will talk about the UI rendering process after the completion of task and microtask, and how to Render the UI after each task execution and all microtask execution.
Typical tasks include the following
- Events
Dispatching an Event object at a particular EventTarget object is often done by a dedicated task.
Not all events are dispatched using the task queue, many are dispatched during other tasks.- Parsing
The HTML parser tokenizing one or more bytes, and then processing any resulting tokens, is typically a task.- Callbacks
Calling a callback is often done by a dedicated task.- Using a resource When an algorithm fetches a resource, if the fetching occurs in a non-blocking fashion then the processing of the resource once some or all of the resource is available is performed by a task.
- Reacting to DOM manipulation
Some elements have tasks that trigger in response to DOM manipulation, e.g. when that element is inserted into the document.
In addition, there are setTimeout, setInterval, window.postMessage and so on. Reacting to DOM manipulation above does not mean that you are Reacting to DOM manipulation as a task. 31) It is asynchrony that is being treated as task.
HTML5 standard: Task, MicroTask and UI render implementation process is as follows:
An event loop must continually run through the following steps for as long as it exists:
- Select the oldest task on one of the event loop’s task queues, if any, ignoring, in the case of a browsing context event loop, tasks whose associated Documents are not fully active. The user agent may pick any task queue. If there is no task to select, then jump to the microtasks step below.
- Set the event loop’s currently running task to the task selected in the previous step.
- Run: Run the selected task.
- Set the event loop’s currently running task back to null.
- Remove the task that was run in the run step above from its task queue.
- Microtasks: Perform a microtask checkpoint
- Update the rendering: If this event loop is a browsing context event loop (as opposed to a worker event loop), Then run the following substeps. 7.1 Let now be the value that would be returned by the Performance object’s now() Method.7.2 Let docs be the list of Document objects associated with the event loop in question, sorted arbitrarily except that the following conditions must be met: 7.3 If there are top-level browsing context B that the user agent would not benefit from having their rendering updated at this time, Then remove from docs all Document objects whose browsing context is top-down is in B.7.4 If there are a nested browsing contexts B that the user agent believes would not benefit from having their rendering updated at this time, Then remove from docs all Document objects whose browsing context is in B. 7.5 For each fully active Document in docs, run the resize steps for that Document, Passing in now as the timestamp. [CSSOMVIEW] 7.6 For each fully active Document in docs, run the scroll steps for that Document, Passing in now as the timestamp. [CSSOMVIEW] 7.7 For each fully active Document in docs, evaluate media queries and report changes for that Document, Passing in now as the timestamp. [CSSOMVIEW] 7.8 For each fully active Document in docs, run CSS animations and send events for that Document, Passing in now as the timestamp. [CSSANIMATIONS] 7.9 For each fully active Document in docs, run the fullscreen rendering steps for that Document, Passing in now as the timestamp. [FULLSCREEN] 7.10 For each fully active Document in docs, run the animation frame callbacks for that Document, Passing in now as the timestamp. 7.11 For each fully active Document in docs, run the update intersection observations steps for that Document, Passing in now as the timestamp. [INTERSECTIONOBSERVER] 7.12 For each fully active Document in docs, update the rendering or user interface of that Document and its browsing context to reflect the current state.
- If this is a worker event loop (i.e. one running for a WorkerGlobalScope), but there are no tasks in the event loop’s task queues and the WorkerGlobalScope object’s closing flag is true, then destroy the event loop, aborting these steps, resuming the run a worker steps described in the Web workers section below.
- Return to the first step of the event loop.
The first step is to select the oldest task from one of several task queues. (Because there are multiple task queues, the browser can perform some tasks in the task queue, such as the UI task queue, first and frequently.) Then, in steps 2 to 5, execute the task. Perform a microtask checkpoint. Perform a microtask checkpoint. Perform a microtask checkpoint. The newly added Microtasks will still be executed, of course, there seems to be a limit to how many microtasks can be executed in one round (1000?). If the number is too high, execute some of what’s left and execute it later? I’m not so sure here.
Step 7: Update the rendering: From 7.2 to 7.4, the Document objects associated with the event loop of the current round will maintain some specific order. These document objects will perform UI render, but not all associated documents will need to update the UI. The browser will decide if the document will benefit from UI Render, because the browser only needs to keep the refresh rate at 60Hz, and each event loop is very fast, so there is no need to Render the UI on every document. 7.5 and 7.6 Run the resize steps/run the scroll steps does not mean to perform resize and scroll steps. Each time we scoll, the viewport or DOM will immediately scroll and add the Document or DOM to the pending Scroll Event targets, and run the Scroll steps will iterate over those targets. Trigger scroll event on target. Similarly, run the Resize Steps triggers the resize event. Media Query, RUN CSS animations and Send Events are similar in 7.8 and 7.9. Step 10 and step 11 perform the familiar requestAnimationFrame callback and IntersectionObserver callback (step 10 is critical as raf is performed here!). . 7.12 Rendering UI, this is the key.
Step 9 Continue with the Event loop and execute task, MicroTasks, and UI Render.
Update: Found an image, but highlights the entire Event loop, not UI Render.