May 31, 2021 Article blog
The following article is from front-end afternoon tea by SHERlocked93
vue is currently one of the three front-end web end of the world, but also as one of my main technology stack, in the daily use of knowledge and curiosity, and the recent community has emerged a large number of vue source reading class articles, in the next opportunity to draw some nutrition from everyone's articles and discussions, while some reading source code ideas to summarize, produce some articles, as an output of their own thinking
Target Vue version:
2.5.17-beta.0
vue Source Note: github.com/SHERlocked93/vue-analysis
Declaration: The syntax of the source code in the article uses Flow, and the source code is abridged as needed (in order not to be confused), if you want to see the full version, please go to the github address above
In the previous article, we had a distributed update
dep.notify()
method in
setter
visitor in
defineReactive
which
subs
on the collection principle, which notifies the subscriptions collected in
dep
subs one by one that the subscriptions it changes to the watchers perform updates.
Let's take a look at the implementation of the
update
method:
// src/core/observer/watcher.js
/* Subscriber接口,当依赖发生改变的时候进行回调 */
update() {
if (this.computed) {
// 一个computed watcher有两种模式:activated lazy(默认)
// 只有当它被至少一个订阅者依赖时才置activated,这通常是另一个计算属性或组件的render function
if (this.dep.subs.length === 0) { // 如果没人订阅这个计算属性的变化
// lazy时,我们希望它只在必要时执行计算,所以我们只是简单地将观察者标记为dirty
// 当计算属性被访问时,实际的计算在this.evaluate()中执行
this.dirty = true
} else {
// activated模式下,我们希望主动执行计算,但只有当值确实发生变化时才通知我们的订阅者
this.getAndInvoke(() => {
this.dep.notify() // 通知渲染watcher重新渲染,通知依赖自己的所有watcher执行update
})
}
} else if (this.sync) { // 同步
this.run()
} else {
queueWatcher(this) // 异步推送到调度者观察者队列中,下一个tick时调用
}
}
If it's not
computed watcher
it's not
sync
that pushes the current watcher that calls update into the scheduler queue, and the next tick call looks at
queueWatcher
// src/core/observer/scheduler.js
/* 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则
* 该watcher将被跳过,除非它是在队列正被flush时推送
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) { // 检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验
has[id] = true
queue.push(watcher) // 如果没有正在flush,直接push到队列中
if (!waiting) { // 标记是否已传给nextTick
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
/* 重置调度者状态 */
function resetSchedulerState () {
queue.length = 0
has = {}
waiting = false
}
A hashmap of
has
is used here to check if the id of the current watcher exists, skip if it already exists, push to the
queue
queue and mark hash table has for the next test to prevent repeated additions.
This is a de-heavy process, than every check to go to the queue to find civilization, in the rendering will not repeat
patch
the same watcher changes, so that even if the synchronization modified a hundred views used in the data, asynchronous
patch
will only update the last modification.
The
waiting
method here is used to mark whether
flushSchedulerQueue
has passed to
nextTick
tag bit, and if it has been passed, only push to the queue does not pass
flushSchedulerQueue
to
nextTick
until
resetSchedulerState
resets the scheduler
waiting
is put back
false
to allow
flushSchedulerQueue
to be passed to the next tick callback, which in short
flushSchedulerQueue
callback in one tick o
nly once is allowed to be passed in.
Take a look at what the callback
flushSchedulerQueue
which was passed to
nextTick
did:
// src/core/observer/scheduler.js
/* nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers */
function flushSchedulerQueue () {
flushing = true
let watcher, id
queue.sort((a, b) => a.id - b.id) // 排序
for (index = 0; index < queue.length; index++) { // 不要将length进行缓存
watcher = queue[index]
if (watcher.before) { // 如果watcher有before则执行
watcher.before()
}
id = watcher.id
has[id] = null // 将has的标记删除
watcher.run() // 执行watcher
if (process.env.NODE_ENV !== 'production' && has[id] != null) { // 在dev环境下检查是否进入死循环
circular[id] = (circular[id] || 0) + 1 // 比如user watcher订阅自己的情况
if (circular[id] > MAX_UPDATE_COUNT) { // 持续执行了一百次watch代表可能存在死循环
warn() // 进入死循环的警告
break
}
}
}
resetSchedulerState() // 重置调度者状态
callActivatedHooks() // 使子组件状态都置成active同时调用activated钩子
callUpdatedHooks() // 调用updated钩子
}
Perform the
flushSchedulerQueue
method in the
nextTick
method, which executes the
run
method of
queue
one by one.
We see that in the first place there is a
queue.sort()
method that orders the watchers in the queue from small to large by id, which guarantees that:
In the for loop in the execution queue one by one,
index < queue.length
is not cached here because more watcher objects may be pushed into the queue while the existing watcher object is being processed.
Then the modification of the data is reflected from the model layer to the view process:
数据更改 -> setter -> Dep -> Watcher -> nextTick -> patch -> 更新视图
Here's a look at what
nextTick
did after the method containing each watcher execution was passed in as a callback to
nextTick
However, the first thing to do is to look at the concepts of
EventLoop
macro task
micro task
in the browser, and to refer to this article on event loops in JS and Node .js, here's a diagram to show the execution relationship between the latter two in the main thread:
Explain that when the main thread completes the synchronization task:
The types of asynchronous tasks common in browser environments, by priority:
macro task
sync code,
setImmediate
MessageChannel
setTimeout/setInterval
micro task
:
Promise.then
、
MutationObserver
Some articles call
micro task
a microtask,
macro task
a macro task, because the two words are spelled too much like -.
- , so the comments that follow are more represented by Chinese
Let's look at the implementation of
micro task
and
macro task
in source code:
macroTimerFunc
microTimerFunc
// src/core/util/next-tick.js
const callbacks = [] // 存放异步执行的回调
let pending = false // 一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送
/* 挨个同步执行callbacks中回调 */
function flushCallbacks() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let microTimerFunc // 微任务执行方法
let macroTimerFunc // 宏任务执行方法
let useMacroTask = false // 是否强制为宏任务,默认使用微任务
// 宏任务
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
MessageChannel.toString() === '[object MessageChannelConstructor]' // PhantomJS
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
// 微任务
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
}
} else {
microTimerFunc = macroTimerFunc // fallback to macro
}
flushCallbacks
this method is to perform callback functions in callbacks one by one, callback functions in callbacks
macro task
are added
flushCallbacks
when
nextTick
is called;
micro task
macroTimerFunc
microTimerFunc
flushCallbacks
F
or example, the macro task method
macroTimerFunc=()=>{ setImmediate(flushCallbacks) }
so that
macroTimerFunc()
can consume these callbacks saved in the callbacks array when the macro task execution is triggered, and the microtasks are the same.
It can also be seen that the asynchronous callback function passed to
nextTick
is pressed into a synchronous task that is performed in one tick, rather than opening multiple asynchronous tasks.
Note that there is a more difficult place to understand, the first time
nextTick
is
pending
false, at this point push to a macro task or microtask task in the browser event loop, if you continue to add to callbacks without flushing, then this placeholder is executed The callbacks that are added after the queue is executed, so
macroTimerFunc
microTimerFunc
is equivalent to the occupation of the task queue, and then
pending
true continues to be added to the duty queue, which will be executed when it is the task queue's turn.
When
flushCallbacks
pending
false allows the next round of
nextTick
execution to take place in the event loop.
You can see that
macroTimerFunc
above has a smooth degradation with
microTimerFunc
under different browser compatibility, or
a downgrade strategy:
macroTimerFunc
:
setImmediate -> MessageChannel -> setTimeout
。 S
tart by detecting whether native support
setImmediate
which is implemented natively only in IE and Edge
setImmediate
and then detect whether MessageChannel is supported, and finally use
setTimeout
if you don't know
MessageChannel
Unlike
MessageChannel
which does not use
setTimeout
directly, because HTML5 dictates that setTimeout performs a minimum delay of 4ms, while nested timeouts behave as 10ms, and the first two with no minimum delay limit are clearly better than the ones that allow callback execution as quickly as possible
setTimeout
。
microTimerFunc
:
Promise.then -> macroTimerFunc
。
First check to see if
Promise
is supported, and if so, call the
flushCallbacks
method through
nextTick
Promise.then
otherwise it will degrade
MutationObserver
to
macroTimerFunc
Finally, let's take a look at how the
nextTick
method we usually use is implemented:
// src/core/util/next-tick.js
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
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
/* 强制使用macrotask的方法 */
export function withMacroTask(fn: Function): Function {
return fn._withTask || (fn._withTask = function() {
useMacroTask = true
const res = fn.apply(null, arguments)
useMacroTask = false
return res
})
}
nextTick
is divided into three parts here, let's take a look;
nextTick
wraps the incoming
cb
callback function in a
try-catch
and pushes it into the callbacks array in an anonymous function, because it prevents a single
cb
from executing errors that prevent the entire JS thread from hanging up, and
cb
wraps to prevent these callback functions from interacting if they are executed incorrectly, such as if the previous one is thrown wrong and the latter can still be executed.
pending
state, which means that the waiting in
queueWatcher
described earlier means that it is a marker bit that starts with
false
being set to
true
before
waiting
macroTimerFunc
microTimerFunc
method, so the next time you call
nextTick
you won't go into
macroTimerFunc
microTimerFunc
method, which will be flushCallbacks at the next
macro/micro tick
flushCallbacks
Asynchronously perform the tasks collected in the callbacks queue, and the
flushCallbacks
method puts
pending
false
at the beginning of execution, so the next time
nextTick
is called, a new round of
macroTimerFunc
microTimerFunc
is turned on, resulting in
event loop
in vue.
cb
is passed in, because
nextTick
also supports promise calls:
nextTick().then(() => {})
so if there is no incoming
cb
a Promise instance is passed directly to the _resolve, so that when the latter executes, it jumps into the method that we call into
then
The
next-tick.js
file in Vue source code also has an important note, which is translated here:
In previous versions of vue2.5, nextTick was basically implemented based on
micro task
but in some casesmicro task
has too high a priority and may be triggered between continuous sequence events (e.g., s4521, #6690) or even between events bubbling of the same event (#6566). B ut if you change it all tomacro task
there will also be a performance impact on scenes that are redrawn and animated, such as #6813. The solution provided after vue 2.5 is to usemicro task
by default, but enforce macro task when needed, such as in v-on attached event handlers.macro task
Why use
micro task
first by default is to take advantage of its high-priority features to ensure that the microtasks in the queue are fully executed in one loop.
The method of forcing
macro task
is to wrap the handler function call with
withMacroTask
method of the callback when binding the DOM
handler = withMacroTask(handler)
which guarantees that changes in the state of the data are encountered throughout the execution of the callback function and are pushed into
macro task
The above implementation is in the
add
method of src/platforms/web/runtime/modules/events .js, and you can take a look at the specific code yourself.
Just as I was writing this article, I wondered if someone had asked a question about the difference between the @input events of vue 2.4 and version 2.5, which was also due to the fact that the DOM events prior to 2.5 used
micro task
and then
macro task
to solve the problem Refer to the < Vue .js Several ways to upgrade the pit tip >, here's a way to add native events to the mounted hook with
addEventListener
see CodePen.
Having said so much, let's take an example and execute see CodePen
<div id="app">
<span id='name' ref='name'>{{ name }}</span>
<button @click='change'>change name</button>
<div id='content'></div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
name: 'SHERlocked93'
}
},
methods: {
change() {
const $name = this.$refs.name
this.$nextTick(() => console.log('setter前:' + $name.innerHTML))
this.name = ' name改喽 '
console.log('同步方式:' + this.$refs.name.innerHTML)
setTimeout(() => this.console("setTimeout方式:" + this.$refs.name.innerHTML))
this.$nextTick(() => console.log('setter后:' + $name.innerHTML))
this.$nextTick().then(() => console.log('Promise方式:' + $name.innerHTML))
}
}
})
</script>
Do the following to see the results:
同步方式:SHERlocked93
setter前:SHERlocked93
setter后:name改喽
Promise方式:name改喽
setTimeout方式:name改喽
Why is this the result, explain:
dep.notify
in the
setter
is triggered
update
rely on this data's render watcher to update,
update
passes
flushSchedulerQueue
function to
nextTick
rendering, watcher at
flushSchedulerQueue
function runtime
watcher.run
diff -> patch
that set of re-rendering-re-view,
re-render
This process re-relies on collection, which is asynchronous, so when we print after we directly modify the name, the asynchronous changes are not
patch
to the view, so get the DOM element on the view or the original content.
nextTick
in the call when the call to push the call back into the callbacks array, and then execute when it is also
for
loop out one by one execution, so it is similar to the concept of queue, first come first after modifying the name, triggers the render watcher to fill in the
schedulerQueue
queue and pass his execution function
flushSchedulerQueue
to
nextTick
at which point the callbacks queue already has the
setter前函数
because this
cb
is in
setter前函数
is then pushed into the callbacks queue, then the first-in, first-in, first-out callback in the callback is performed first, and the
setter前函数
is not executed,
watcher.run
so the dom element is still printed as the original content.
flushSchedulerQueue
has been executed since setter, when Render watcher has put the change
patch
on the view, so getting the DOM at this point is the changed content.
Promise.then
at which point the DOM has changed.
Note that before performing the asynchronous task of
setter前函数
the synchronized code has been executed, the asynchronous task has not been executed, all
$nextTick
functions have been executed, all callbacks have been pushed into the callbacks queue waiting to be executed, so when the
setter前函数
is executed At this point, the callbacks queue is like this: the
setter前函数
flushSchedulerQueue
setter后函数
Promise方式函数
it is a micro-task queue, after execution of the mapro
setTimeout
so print out the results above.
In addition, if the browser's macro task queue
setImmediate
MessageChannel
setTimeout/setInterval
tasks of all types, then they are added to the loop one by one in the order described above, so if the browser
MessageChannel
nextTick
performs
macroTimerFunc
then if there are both
nextTick
tasks in the macrotask queue and tasks of
setTimeout
type added by the user themselves,
nextTick
tasks will be prioritized
Because
MessageChannel
has a higher priority than
setTimeout
setImmediate
is the same.
The above is
W3Cschool编程狮
on
the Vue advanced interview must ask, asynchronous update mechanism and nextTick principle
of the relevant introduction, I hope to help you.