Implementation principle of nextTick
When developing with vue.js, if we want to manipulate the correct DOM based on data state, we must have dealt with the nextTick() method, which is one of the core methods in vue.js. In this section, we introduce how nextTick is implemented in vue.js.
Asynchronous knowledge
Because nextTick involves a lot of asynchrony, we’ll introduce asynchrony to make it easier to learn.
Event Loop
We all know that JavaScript is single-threaded and is executed based on an Event Loop that follows certain rules: All synchronous tasks are executed in the main thread, forming an execution stack. All asynchronous tasks are temporarily put into a task queue. When all synchronous tasks are completed, the task queue is read and put into the execution stack to start execution. The above is a single execution mechanism. The main thread repeats this process over and over again to form an Event Loop.
The above is a general introduction to Event Loop, but there are still some details we need to master when executing Event Loop.
We mentioned tick in the update section, so what is tick? The tick is a single execution of the main thread. All asynchronous tasks are scheduled by task queue, which stores tasks. According to the specification, these tasks are divided into Macro task and micro task. There is a subtle relationship between macro tasks and Micro Tasks: After each Macro task is executed, all micro Tasks are cleared.
Macro Task and Micro Task correspond as follows in the browser environment:
macro task
Macro task:MessageChannel
,postMessage
,setImmediate
andsetTimeout
.micro task
Micro tasks:Promise.then
andMutationObsever
.
MutationObserver
It creates and returns a new instance of MutationObserver, which will be called whenever the specified DOM changes.
Let’s write an example according to the documentation:
const callback = () = > {
console.log('text node data change')}const observer = new MutationObserver(callback)
let count = 1
const textNode = document.createTextNode(count)
observer.observe(textNode, {
characterData: true
})
function func () {
count++
textNode.data = count
}
func() // text node data change
Copy the code
Code analysis:
- First of all, we define
callback
The callback function andMutationObserver
Object, where the constructor passes arguments that are ourscallback
. - It then creates a text node and passes in the initial text of the text node, followed by the call
MutationObserver
The instanceobserve
Method, passing in the text node we created and aconfig
Observe the configuration object, wherecharacterData:true
We have to observetextNode
The text of the node changes.config
There are other option properties that you can use in theMDN
You can view it in the document. - And then, let’s define one
func
Function, the main thing that this function does is modifytextNode
The text content in the text node, when the text content changes,callback
Is automatically called, so the outputtext node data change
.
Now that we know how to use MutationObserver, let’s take a look at how the nextTick method uses MutationObserver:
import { isIE, isNative } from './env'
// omit the code
else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () = > {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
}
Copy the code
As you can see, nextTick determines that a non-INTERNET Explorer browser is used only when MutationObserver is available and is a native MutationObserver. For judging the non-ie situation, you can see issue#6466 (Labour of new window) in vue.js to see why.
SetImmediate and setTimeout
SetTimeout is a very common timer method for most people, so we won’t cover it too much.
In the nextTick method implementation, it uses setImmediate, which Can be seen on the Can I Use (New Window) website. This API method is only available in advanced Internet Explorer and low Edge, but not in other browsers.
Then why is this method used? It is because of the issue we mentioned before: MutationObserver is not reliable in Internet Explorer, so in Internet Explorer you level down to using setImmediate, which we can think of as similar to setTimeout.
setImmediate(() = > {
console.log('setImmediate')},0)
/ / is approximately equal to
setTimeout(() = > {
console.log('setTimeout')},0)
Copy the code
NextTick implementation
After introducing the knowledge related to nextTick and asynchrony, let’s analyze the implementation of nextTick method. The first thing to say is: asynchronous degradation.
let timerFunc
// 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 next, $flow-disable-line */
if (typeof Promise! = ='undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () = > {
p.then(flushCallbacks)
// 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)
}
isUsingMicroTask = true
} else if(! isIE &&typeofMutationObserver ! = ='undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () = > {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeofsetImmediate ! = ='undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () = > {
setImmediate(flushCallbacks)
}
} else {
// Fallback to setTimeout.
timerFunc = () = > {
setTimeout(flushCallbacks, 0)}}Copy the code
We introduced the Event Loop in the previous section. Due to the special execution mechanism of Macro Task and Micro Task, we first determine whether the current browser supports promises. If not, we then demoted to determine whether MutationObserver is supported. It continues to demote to determining whether or not setImmediate is supported, and finally to using setTimeout.
After introducing asynchronous degradation, let’s look at the nextTick implementation code:
const callbacks = []
let pending = false
export function nextTick (cb? :Function, ctx? :Object) {
let _resolve
callbacks.push(() = > {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')}}else if (_resolve) {
_resolve(ctx)
}
})
if(! pending) { pending =true
timerFunc()
}
// $flow-disable-line
if(! cb &&typeof Promise! = ='undefined') {
return new Promise(resolve= > {
_resolve = resolve
})
}
}
Copy the code
The real code for nextTick is not complicated. It collects incoming CB’s and then executes the timerFunc method when pending is false, where timeFunc is defined during asynchronous demotion. The nextTick method also makes a final judgment that if no CB is passed in and a Promise is supported, it will return a Promise, so we can use nextTick in two ways:
const callback = () = > {
console.log('nextTick callback')}/ / way
this.$nextTick(callback)
2 / / way
this.$nextTick().then(() = > {
callback()
})
Copy the code
Finally, we’ll look at an implementation of the flushCallbacks method that wasn’t mentioned before:
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
Copy the code
The flushCallbacks method returns the pending state to false and executes the methods in the callbacks array.
Note for change detection
Although the Object.defineProperty() method works well, there are exceptions where changes to these exceptions do not trigger setters. In this case, we classify objects and arrays.
object
Suppose we have the following example:
export default {
data () {
return {
obj: {
a: 'a'
}
}
},
created () {
// 1. Add attribute b, attribute B is not reactive, does not trigger setter for obj
this.obj.b = 'b'
// 2.delete delete existing property, cannot trigger setter for obj
delete this.obj.a
}
}
Copy the code
From the above examples, we can see:
- When a new property is added to a responsive object, the new property is not reactive and cannot be triggered by any subsequent changes to the new property
setter
. To solve this problem,Vue.js
Provides a globalVue.set()
Methods and Examplesvm.$set()
Method, they’re all really the sameset
Method, which we will cover globally in relation to responsiveness in a later sectionAPI
The implementation of the. - This is not triggered when a reactive object deletes an existing property
setter
. To solve this problem,Vue.js
Provides a globalvue.delete()
Methods and Examplesvm.$delete()
Method, they’re all really the samedel
Method, which we will cover globally in relation to responsiveness in a later sectionAPI
The implementation of the.
An array of
Suppose we have the following example:
export default {
data () {
return {
arr: [1.2.3]
}
},
created () {
// 1. Cannot capture array changes through index changes.
this.arr[0] = 11
// 2. Cannot capture array changes by changing the array length.
this.arr.length = 0}}Copy the code
From the above examples, we can see:
- Modifying an array directly through an index does not capture changes to the array.
- Changes to the array cannot be caught by changing the array length.
For the first case, we can use the aforementioned vue.set or vm.$set, and for the second, we can use the array splice() method.
In the latest version of Vue3.0, Proxy is used to replace Object.defineProperty() to achieve responsiveness. All the above problems can be solved after Proxy is used. However, Proxy belongs to ES6, so it has certain requirements for browser compatibility.
Change detection API implementation
In the previous section, we looked at some of the problems with change detection. In this section, we’ll look at how vue.js implements the API to solve these problems.
Vue. Set to achieve
Vue. Set and vm.$set refer to a set method defined in observer/index.js:
export function set (target: Array<any> | Object, key: any, val: any) :any {
if(process.env.NODE_ENV ! = ='production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key intarget && ! (keyin Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if(! ob) { target[key] = valreturn val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
Copy the code
Before analyzing the code, let’s review the use of vue. set or vm.$set:
export default {
data () {
return {
obj: {
a: 'a'
},
arr: []
}
},
created () {
// Add a new attribute to the object
this.$set(this.obj, 'b'.'b')
console.log(this.obj.b) // b
// Add a new element to the array
this.$set(this.arr, 0.'AAA')
console.log(this.arr[0]) // AAA
// Modify array elements by index
this.$set(this.arr, 0.'BBB')
console.log(this.arr[0]) // BBB}}Copy the code
Code analysis:
set
Method first on the incomingtarget
Parameters are verified, whereisUndef
Determine whetherundefined
.isPrimitive
Determine whetherJavaScript
Raw value, an error message is displayed in the development environment if one of the conditions is met.
export default {
created () {
// Error message
this.$set(undefined.'a'.'a')
this.$set(1.'a'.'a')
this.$set('1'.'a'.'a')
this.$set(true.'a'.'a')}}Copy the code
- Then through the
Array.isArray()
Method to determinetarget
Whether it is an array, and if so, pass againisValidArrayIndex
Check if it is a valid array index. If it is, variation is usedsplice
The setValue () method sets a value at the specified location in the array. It also resets the array’slength
Property because the index we pass in May be better than the existing array’slength
Even larger. - It then determines whether it is an object and is currently
key
Is it already on this object? If it already exists, then we just need to copy it again. - Finally, through
defineReactive
Method adds a property to the reactive object,defineReactive
Methods have been introduced before and will not be covered here. indefineReactive
After the execution is completed, an update is immediately distributed to inform the dependency of responsive data to be updated immediately. The following two pieces of code areset
Core of method core:
defineReactive(ob.value, key, val)
ob.dep.notify()
Copy the code
Vue. Delete
Delete and vm.$delete use the same delete method as defined in the observer/index.js file: Vue.
export function del (target: Array<any> | Object, key: any) {
if(process.env.NODE_ENV ! = ='production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)}if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
const ob = (target: any).__ob__
if(target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV ! = ='production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
)
return
}
if(! hasOwn(target, key)) {return
}
delete target[key]
if(! ob) {return
}
ob.dep.notify()
}
Copy the code
Before analyzing the code, let’s review the following use of vue. delete or vm.$delete:
export default {
data () {
return {
obj: {
a: 'a'
},
arr: [1.2.3]
}
},
created () {
// Delete object properties
this.$delete(this.obj, 'a')
console.log(this.obj.a) // undefined
// Delete an array element
this.$delete(this.arr, 1)
console.log(this.arr) / / [1, 3]}}Copy the code
Code analysis:
- The object to be deleted is determined first
target
Can’t forundefined
Or a raw value, if so, an error is displayed in the development environment.
export default {
created () {
// Error message
this.$delete(undefined.'a')
this.$delete(1.'a')
this.$delete('1'.'a')
this.$delete(true.'a')}}Copy the code
- Then through the
Array.isArray()
Method to determinetarget
Whether it is an array, and if so, pass againisValidArrayIndex
Check if it is a valid array index. If it is, variation is usedsplice
Method to remove the element at the specified location. - Then determine whether the current attribute to be deleted is in
target
Object, if it’s not there, it just returns, doing nothing. - Finally, through
delete
The operator deletes an attribute on the object, and thenob.dep.notify()
Notifies dependencies on responsive objects to be updated.
Vue. Observables
Vue.observable is a global method available in Vue2.6+ that makes an object responsive:
const obj = {
a: 1.b: 2
}
const observeObj = Vue.observable(obj)
console.log(observeObj.a) / / triggers the getter
observeObj.b = 22 / / triggers the setter
Copy the code
This global method is defined in the initGlobalAPI, which we’ve already covered, not covered here:
export default function initGlobalAPI (Vue) {
// ...
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T= > {
observe(obj)
return obj
}
// ...
}
Copy the code
Observable implementation is very simple, just calling the observe method inside the method and returning the obj. The code implementation of Observe has been covered in the previous section, so there is no further explanation here:
export function observe (value: any, asRootData: ? boolean) :Observer | void {
if(! isObject(value) || valueinstanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if( shouldObserve && ! isServerRendering() && (Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && ! value._isVue ) { ob =new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
Copy the code