Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to understand asynchronously updating DOM Strategy and nextTick from Vue.js Source Code

2025-01-15 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

How to understand the asynchronous update DOM strategy and nextTick from the Vue.js source code? in view of this problem, this article introduces the corresponding analysis and solution in detail, hoping to help more partners who want to solve this problem to find a more simple and feasible method.

Manipulate DOM

When using vue.js, sometimes you have to manipulate DOM because of specific business scenarios, such as:

{{test}} tet export default {data () {return {test: 'begin'};}, methods () {handleClick () {this.test =' end'; console.log (this.$refs.test.innerText); / / print "begin"}

The printed result is begin, why do we get the previous value "begin" instead of the "end" we expected when we set test to "end" and get the innerText of the real DOM node?

Watcher queue

With doubt, we found the Watch implementation of the Vue.js source code. When some responsive data changes, its setter function notifies the Dep,Dep in the closure and calls all the Watch objects it manages. Triggers the update implementation of the Watch object. Let's take a look at the implementation of update.

Update () {/ * istanbul ignore else * / if (this.lazy) {this.dirty = true} else if (this.sync) {/ * synchronization performs run direct rendering view * / this.run ()} else {/ * asynchronously pushed to the viewer queue and called on the next tick. * / queueWatcher (this)}}

We found that Vue.js defaults to performing DOM updates asynchronously.

When update is executed asynchronously, the queueWatcher function is called.

/ * put an observer object push into the observer queue. If the same id already exists in the queue, the observer object will be skipped unless it is pushed * / export function queueWatcher (watcher: Watcher) {/ * get the id*/ const id of watcher = watcher.id / * to verify the existence of id. If it already exists, skip it directly. If it does not exist, mark the hash table has. For next inspection * / if (has [id] = = null) {has [id] = true if (! flushing) {/ * if there is no flush drop, you can directly push to the queue * / queue.push (watcher)} else {/ / if already flushing, splice the watcher based on its id / / if already past its id, it will be run next immediately. Let I = queue.length-1 while (I > = 0 & & queue [I] .id > watcher.id) {iRaq -} queue.splice (Math.max (I, index) + 1,0, watcher)} / / queue the flush if (! waiting) {waiting = true nextTick (flushSchedulerQueue)}

Looking at the source code of queueWatcher, we find that instead of updating the view immediately, Watch objects are put into a queue queue by push, and the state is in the state of waiting. At this time, Watch objects will continue to be push into this queue queue, waiting for the next tick before these Watch objects will be traversed to update the view. At the same time, the repeated Watcher of id will not be added to the queue multiple times, because in the final rendering, we only need to care about the final result of the data.

So, what's the next tick?

NextTick

Vue.js provides a nextTick function, which is actually the nextTick called above.

The implementation of nextTick is relatively simple. The purpose of execution is to push a funtion into microtask or task, and execute the funtion passed in by nextTick after the current stack is executed (or there will be some tasks that need to be executed in front of it). Take a look at the source code:

/ * Defer a task to execute it asynchronously. * / / * delay a task to execute asynchronously, execute on the next tick, execute a function immediately, and return a function. This function is used to push a timerFunc in task or microtask. After the execution of the current call stack is completed, it is executed until the timerFunc is executed. The purpose is to delay the execution of * / export const nextTick = (function () {/ * store the callback of asynchronous execution * / const callbacks = [] / *) to a mark bit. If a timerFunc has been pushed to the task queue, there is no need to repeatedly push * / let pending = false / * a function pointer. The pointing function will be pushed to the task queue, and when the task of the main thread is finished, the timerFunc in the task queue will be called * / let timerFunc / * callback * / function nextTickHandler () {/ * when the next tick is called, marking the waiting status (that is, the function has been pushed into the task queue or the main thread, and is already waiting for the current stack to finish execution) In this way, there is no need to push the timerFunc multiple times into the task queue or the main thread * / pending = false / * to execute all callback*/ const copies = callbacks.slice (0) callbacks.length = 0 for (let I = 0) when multiple push callbacks to callbacks I

< copies.length; i++) { copies[i]() } } // the nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >

= 9.3.3 when triggered in touch event handlers. It / / completely stops working after triggering a few times... So, if native / / Promise is available, we will use it: / * istanbul ignore if * / / * explain here that Promise, MutationObserver and setTimeout give priority to the use of Promise when trying to get timerFunc, and use MutationObserver in the absence of Promise. Both methods will be executed in microtask and will be executed earlier than setTimeout, so they are preferred. If the environment that does not support either of the above methods will use setTimeout, push the function at the end of the task and wait for the call to execute. * / if (typeof Promise! = = 'undefined' & & isNative (Promise)) {/ * use Promise*/ var p = Promise.resolve () var logError = err = > {console.error (err)} timerFunc = () = > {p.then (nextTickHandler) .catch (logError) / / in problematic UIWebViews, Promise.then doesn't completely break, but / / it can get stuck in a weird state where callbacks are pushed into the / / microtask queue but the queue isn't being flushed, until the browser / / needs to do some other work E.g. Handle a timer. Therefore we can / / "force" the microtask queue to be flushed by adding an empty timer. If (isIOS) setTimeout (noop)}} else if (typeof MutationObserver! = = 'undefined' & & (isNative (MutationObserver) | | / / PhantomJS and iOS 7.x MutationObserver.toString () =' [object MutationObserverConstructor]') {/ / use MutationObserver where native Promise is not available, / / e.g. PhantomJS IE11, iOS7, Android 4. 4 / * create a DOM object for textNode, bind the DOM with MutationObserver and specify the callback function A callback is triggered when the DOM changes, and the callback enters the main thread (execution takes precedence over the task queue) That is, when textNode.data = String (counter), the callback will be triggered * / var counter = 1 var observer = new MutationObserver (nextTickHandler) var textNode = document.createTextNode (String (counter)) observer.observe (textNode, {characterData: true}) timerFunc = () = > {counter = (counter + 1)% 2 textNode.data = String (counter)} else {/ fallback to setTimeout / * istanbul ignore next * / / * push the callback to the end of the task queue using setTimeout * / timerFunc = () = > {setTimeout (nextTickHandler) 0)}} / * execute cb callback function ctx context * / return function queueNextTick (cb?: Function, ctx?: Object) {let _ resolve / * cb stored in callbacks * / callbacks.push (() = > {if (cb) {try {cb.call (ctx)} catch (e) {handleError (e, ctx)) when pushed to the next cb in the queue 'nextTick')} else if (_ resolve) {_ resolve (ctx)}) if (! pending) {pending = true timerFunc ()} if (! cb & & typeof Promise! = =' undefined') {return new Promise ((resolve, reject) = > {_ resolve = resolve})}}) ()

It is an immediate execution function that returns a queueNextTick interface.

The incoming cb is stored in the callbacks by push, and the timerFunc is executed (pending is a status flag that ensures that the timerFunc is executed only once before the next tick).

What is timerFunc?

Looking at the source code, it is found that timerFunc will detect the current environment and different implementation, in fact, according to the Promise,MutationObserver,setTimeout priority, which exists to use which, the worst environment to use setTimeout.

To explain here, there are three ways to try to get timerFunc: Promise, MutationObserver, and setTimeout.

Priority is given to Promise, and MutationObserver is used in the absence of Promise. The callback functions of both methods are executed in microtask, and they are executed earlier than setTimeout, so they take precedence.

If the environment that does not support either of the above methods will use setTimeout, push the function at the end of the task and wait for the call to execute.

Why give priority to microtask? I learned from Gu Yiling's answer in Zhihu:

The event loop execution of JS distinguishes between task and microtask. Before the execution of each task is completed, the engine executes the microtask in all microtask queues before taking a task from the queue to execute.

The setTimeout callback will be assigned to a new task, while the resolver of Promise and the callback of MutationObserver will be scheduled to be executed in a new microtask, which will be executed before the task generated by setTimeout.

To create a new microtask, use Promise first, and if the browser doesn't support it, try MutationObserver again.

I really can't. I have no choice but to create task with setTimeout.

Why use microtask?

According to HTML Standard, after each task is run, the UI will re-render, then the data update will be completed in the microtask, and the latest UI will be available at the end of the current task.

On the other hand, if you create a new task to update the data, then the rendering will be done twice.

Refer to the answer of Gu Yiling Zhihu

First, Promise, (Promise.resolve ()). Then () can add its callback to microtask

MutationObserver creates a new DOM object of textNode, binds the DOM with MutationObserver and specifies the callback function. When the DOM changes, the callback will be triggered, and the callback will enter microtask, that is, the callback will be added when textNode.data = String (counter).

SetTimeout is the last alternative, which adds the callback function to the task until it is executed.

To sum up, the purpose of nextTick is to generate a callback function to be added to task or microtask. After the execution of the current stack (there may be other functions in front of it), the callback function is called for the purpose of triggering asynchronously (that is, when the next tick is triggered).

FlushSchedulerQueue

/ * Github: https://github.com/answershuto*//** * Flush both queues and run the watchers. * / / * callback function of nextTick. When flush drops two queues in the next tick, run watchers*/function flushSchedulerQueue () {flushing = true let watcher, id / / Sort queue before flush. / / This ensures that: / / 1. Components are updated from parent to child. (because parent is always / / created before the child) / / 2.A component's user watchers are run before its render watcher (because / / user watchers are created before the render watcher) / / 3.If a component is destroyed during a parent component's watcher run, / / its watchers can be skipped. / * sort queue, which ensures that: 1. The order in which the components are updated is from the parent component to the child component, because the parent component is always created before the child component. two。 The user watchers of a component runs before render watcher, because user watchers tends to create 3. 5% earlier than render watcher. If a component is destroyed while the parent component watcher is running, its watcher execution is skipped. * / queue.sort ((a, b) = > a.id-b.id) / / do not cache length because more watchers might be pushed / / as we run existing watchers / * index = queue.length;index > 0 is not used here; index-- is written because length is not cached, because more watcher objects may be push into queue*/ for (index = 0; index) during execution processing of existing watcher objects

< queue.length; index++) { watcher = queue[index] id = watcher.id /*将has的标记删除*/ has[id] = null /*执行watcher*/ watcher.run() // in dev build, check and stop circular updates. /* 在测试环境中,检测watch是否在死循环中 比如这样一种情况 watch: { test () { this.test++; } } 持续执行了一百次watch代表可能存在死循环 */ if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] >

MAX_UPDATE_COUNT) {warn ('You may have an infinite update loop' + (watcher.user? `in watcher with expression "${watcher.expression}" `: `in a component render function.`) Watcher.vm) break} / / keep copies of post queues before resetting state / * * / * get a copy of the queue * / const activatedQueue = activatedChildren.slice () const updatedQueue = queue.slice () / * reset the state of the scheduler * / resetSchedulerState () / / call component updated and activated hooks / * adapt the state of the subcomponents to active and call the activated hook * / callActivatedHooks (activatedQueue) / * call the updated hook * / callUpdateHooks ( UpdatedQueue) / / devtool hook / * istanbul ignore if * / if (devtools & & config.devtools) {devtools.emit ('flush')}}

FlushSchedulerQueue is the callback function for the next tick. The main purpose is to execute the run function of Watcher to update the view.

Why update the view asynchronously?

Let's take a look at the following code

{{test}} export default {data () {return {test: 0};}, created () {for (let I = 0; I)

< 1000; i++) { this.test++; } }} 现在有这样的一种情况,created的时候test的值会被++循环执行1000次。 每次++时,都会根据响应式触发setter->

Dep- > Watcher- > update- > patch.

If the view is not updated asynchronously at this time, then every time + + will directly manipulate DOM to update the view, which is very performance-consuming.

So Vue.js implements a queue queue, which uniformly executes the run of the Watcher in the queue at the next tick. At the same time, Watcher with the same id will not be added to the queue repeatedly, so the run of Watcher will not be executed 1000 times. In the end, updating the view will only directly change the 0 of the DOM corresponding to test to 1000.

The action to ensure that the update view operation DOM is called on the next tick after the current stack has finished execution greatly optimizes performance.

Access the updated data of the real DOM node

So we need to access the updated data of the real DOM node after modifying the data in data. That's all we need to modify the first example of the article.

{{test}} tet export default {data () {return {test: 'begin'};}, methods () {handleClick () {this.test =' end'; this.$nextTick () = > {console.log (this.$refs.test.innerText); / / print "end"}); console.log (this.$refs.test.innerText); / / print "begin"}

You can get the updated DOM instance in the callback by using the $nextTick method of Vue.js 's global API.

On how to understand from the Vue.js source code to see the asynchronous update DOM strategy and nextTick questions are shared here, I hope the above content can be of some help to you, if you still have a lot of doubts to solve, you can follow the industry information channel to learn more related knowledge.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report