12

image.png

background

Knowing it and knowing why, Vue is one of the most mainstream front-end MVVM frameworks. It is very meaningful to deeply understand its implementation principle on the basis of skilled use.

Reading Vue source code is a good way to learn, it not only allows us to help us solve the problems encountered in our work faster, but also learn from the experience of excellent source code, learn the ideas of masters to find, think and solve problems, and learn how to Write high-quality code that is well-maintained and well-maintained.

Part of the text and code snippets in this article are mainly from the official Vue documentation and the rare earth nuggets community, and the references cited are marked at the end of the article. If there is any infringement, please contact to delete it.

💡Warm reminder: The full text of this article 3277 words, the recommended reading time 15分钟 , come on, old iron!

1. Asynchronous update queue

In case you haven't noticed, Vue executes asynchronously when updating the DOM. As soon as it hears a data change, Vue will open a queue and buffer all data changes that happen in the same event loop. If the same Watcher is fired multiple times, it will only be pushed into the queue once. This deduplication during buffering is important to avoid unnecessary computation and DOM manipulation. Then, in the next event loop "tick", Vue flushes the queue and does the actual (de-duplicated) work.

Vue 在内部对异步队列尝试使用原生的Promise.thenMutationObserver 5605dd69ebf9d788b8769a2ea2a990bc setImmediate ,如果执行环境不支持, setTimeout(fn, 0) replace.

For example, when you set vm.someData = 'new value' , the component will not re-render immediately. When the queue is flushed, the component is updated on the next event loop "tick". Most of the time we don't need to care about this process, but if you want to do something based on the updated DOM state, this can be a little tricky.

While Vue.js generally encourages developers to think in a "data-driven" way and avoid touching the DOM directly, sometimes we have to. To wait for Vue to finish updating the DOM after a data change, use Vue.nextTick(callback) immediately after the data change. This way the callback function will be called after the DOM update is complete. E.g:

 <div id="example">{{message}}</div>
 var vm = new Vue({
    el: '#example',
    data: {
        message: '123'
    }
});
vm.message = 'new message'; // 更改数据
vm.$el.textContent === 'new message'; // false
Vue.nextTick(function () {
    vm.$el.textContent === 'new message'; // true
});

Using the vm.$nextTick() instance method inside a component is especially convenient because it doesn't require a global Vue, and the this in the callback function is automatically bound to the current Vue instance:

 Vue.component('example', {
    template: '<span>{{ message }}</span>',
    data: function () {
        return {
            message: '未更新'
        };
    },
    methods: {
        updateMessage: function () {
            this.message = '已更新';
            console.log(this.$el.textContent); // => '未更新'
            this.$nextTick(function () {
                console.log(this.$el.textContent); // => '已更新'
            });
        }
    }
});

Because $nextTick() returns a Promise object, you can accomplish the same thing using the new ES2017 async/await syntax:

 methods: {
        updateMessage: async function () {
            this.message = '已更新';
            console.log(this.$el.textContent); // => '未更新'
            await this.$nextTick();
            console.log(this.$el.textContent); // => '已更新'
        }
    }

nextTick Receives a callback function as a parameter, and delays the callback function to be executed until the DOM is updated; nextTick is to execute the delayed callback after the end of the next DOM update loop, after modifying the data With nextTick, you can get the updated DOM in the callback

Usage scenario : When you want to operate the generated DOM based on the latest data , put this operation in the callback of nextTick ;

2. Preliminary knowledge

nextTick The function of the function can be understood as asynchronous execution of the incoming function. Here, let's first introduce what asynchronous execution is, starting from the JS operating mechanism.

2.1 JS operating mechanism

The execution of JS is single-threaded. The so-called single-threaded means that the event task needs to be queued for execution. After the previous task ends, the next task will be executed. This is a synchronous task. In the case where the next task cannot be executed, the concept of asynchronous tasks is introduced. The JS operating mechanism can be simply described in the following steps.

  • All synchronization tasks are executed on the main thread, forming an execution context stack.
  • In addition to the main thread, there is also a task queue. As long as the asynchronous task has a running result, its callback function will be added to the task queue as a task.
  • Once all synchronization tasks in the execution stack are executed, the task queue is read to see what tasks are in it, added to the execution stack, and execution begins.
  • The main thread keeps repeating the third step above. Also known as the event loop (Event Loop).

2.2 Types of asynchronous tasks

nextTick The function executes the incoming function asynchronously, which is an asynchronous task. There are two types of asynchronous tasks.

The execution process of the main thread is a tick , and all asynchronous tasks are executed one by one through the task queue. The task queue stores the tasks one by one. The specification stipulates that tasks are divided into two categories, namely macro tasks and micro tasks, and after each macro task ends, all must be cleared micro task .

Use a piece of code to visualize the execution order of tasks.

 for (macroTask of macroTaskQueue) {
    handleMacroTask();
    for (microTask of microTaskQueue) {
        handleMicroTask(microTask);
    }
}

In the browser environment, the common methods of creating macro task are

  • setTimeoutsetIntervalpostMessageMessageChannel (队列优先setTimeiout )
  • 网络请求IO
  • Page interaction: DOM, mouse, keyboard, scroll events
  • page rendering

Common way to create micro task

  • Promise.then
  • MutationObserve
  • process.nexttick

In the nextTick function, these methods are used to process the incoming function through the parameter cb into an asynchronous task.

Three, nextTick implementation principle

Wrap the incoming callback function into an asynchronous task, which is divided into micro-tasks and macro-tasks. In order to execute as quickly as possible, micro-tasks are preferred;
nextTick Promise.thenMutationObserversetImmediatesetTimeOut(fn,0)

3.1 Vue.nextTick internal logic

In the execution initGlobalAPI(Vue) initialize the Vue global API, so define
Vue.nextTick :

 function initGlobalAPI(Vue) {
    //...
    Vue.nextTick = nextTick;
}

It can be seen that the nextTick function is directly assigned to Vue.nextTick, which is very simple.

3.2 vm.$nextTick internal logic

 Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
};

It can be seen that vm.$nextTick also calls the nextTick function internally.

3.3 Source code interpretation

The source code of ---93d7a0590b7421913ae064ebda65e8b3 nextTick is located at src/core/util/next-tick.js
nextTick source code is mainly divided into two parts:

 import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

//  上面三行与核心代码关系不大,了解即可
//  noop 表示一个无操作空函数,用作函数默认值,防止传入 undefined 导致报错
//  handleError 错误处理函数
//  isIE, isIOS, isNative 环境判断函数,
//  isNative 判断是否原生支持,如果通过第三方实现支持也会返回 false

export let isUsingMicroTask = false     // nextTick 最终是否以微任务执行

const callbacks = []     // 存放调用 nextTick 时传入的回调函数
let pending = false     // 标识当前是否有 nextTick 在执行,同一时间只能有一个执行


// 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
export function nextTick(cb?: Function, ctx?: Object) {
    let _resolve
    // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
    callbacks.push(() => {
        if (cb) {   // 对传入的回调进行 try catch 错误捕获
            try {
                cb.call(ctx)
            } catch (e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
    
    // 如果当前没有在 pending 的回调,就执行 timeFunc 函数选择当前环境优先支持的异步方法
    if (!pending) {
        pending = true
        timerFunc()
    }
    
    // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}

It can be seen that in the nextTick function, the function passed in through the parameter cb is packaged and then pushed to the callbacks array.

Then use the variable pending to ensure that timerFunc() is executed only once in an event loop.

Finally, execute if (!cb && typeof Promise !== 'undefined') , judge the parameter cb does not exist and the browser supports Promise, then return a Promise class instantiation object. For example nextTick().then(() => {}) , when the _resolve function is executed, the logic of then will be executed.

Let's take a look at the definition of the timerFunc function, first only look at the function that uses Promise to create an asynchronous execution timerFunc function.

 // 判断当前环境优先支持的异步方法,优先选择微任务
// 优先级:Promise---> MutationObserver---> setImmediate---> setTimeout
// setTimeOut 最小延迟也要4ms,而 setImmediate 会在主线程执行完后立刻执行
// setImmediate 在 IE10 和 node 中支持

// 多次调用 nextTick 时 ,timerFunc 只会执行一次

let timerFunc   
// 判断当前环境是否支持 promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {  // 支持 promise
    const p = Promise.resolve()
    timerFunc = () => {
    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
    }
    // 标记当前 nextTick 使用的微任务
    isUsingMicroTask = true
    
    
    // 如果不支持 promise,就判断是否支持 MutationObserver
    // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
    let counter = 1
    // new 一个 MutationObserver 类
    const observer = new MutationObserver(flushCallbacks) 
    // 创建一个文本节点
    const textNode = document.createTextNode(String(counter))   
    // 监听这个文本节点,当数据发生变化就执行 flushCallbacks 
    observer.observe(textNode, { characterData: true })
    timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)  // 数据更新
    }
    isUsingMicroTask = true    // 标记当前 nextTick 使用的微任务
    
    
    // 判断当前环境是否原生支持 setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    timerFunc = () => { setImmediate(flushCallbacks)  }
} else {

    // 以上三种都不支持就选择 setTimeout
    timerFunc = () => { setTimeout(flushCallbacks, 0) }
}

Among them isNative how to define the method, the code is as follows.

 function isNative(Ctor) {
    return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}

Found in it timerFunc function is to use various asynchronous execution methods to call flushCallbacks function.

Take a look at flushCallbacks function

 // 如果多次调用 nextTick,会依次执行上面的方法,将 nextTick 的回调放在 callbacks 数组中
// 最后通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
function flushCallbacks() {
    pending = false
    const copies = callbacks.slice(0)    // 拷贝一份
    callbacks.length = 0    // 清空 callbacks
    for (let i = 0; i < copies.length; i++) {    // 遍历执行传入的回调
        copies[i]()
    }
}

// 为什么要拷贝一份 callbacks

// callbacks.slice(0) 将 callbacks 拷贝出来一份,
// 是因为考虑到 nextTick 回调中可能还会调用 nextTick 的情况,
// 如果 nextTick 回调中又调用了一次 nextTick,则又会向 callbacks 中添加回调,
// nextTick 回调中的 nextTick 应该放在下一轮执行,
// 如果不将 callbacks 复制一份就可能一直循环

Execute pending = false to enable the next event loop to call the ---db749f3521d5654089a364cf14d1cc8d nextTick function in the timerFunc function.
var copies = callbacks.slice(0);callbacks.length = 0 ; 把要callbacks克隆到copies ,然后把callbacks
Then traverse copies execute each function. Going back to nextTick is to wrap the function passed in through the parameter cb and push it to the callbacks collection. Let's see how it is packaged.

 function() {
    if (cb) {
        try {
            cb.call(ctx);
        } catch (e) {
            handleError(e, ctx, 'nextTick');
        }
    } else if (_resolve) {
        _resolve(ctx);
    }
}

The logic is simple. If the parameter cb has a value. Execute cb.call(ctx) in the try statement, and the parameter ctx is the parameter passed into the function. If the execution fails, execute handleError(e, ctx, 'nextTick') .

If parameter cb has no value. Execute _resolve(ctx) , because in the nextTick function how the parameter cb has no value, it will return a Promise class instantiation object, then execute _resolve(ctx) in logic.

At this point nextTick the main line logic of the function is very clear. Define a variable callbacks , wrap the function passed in through the parameter cb with a function, in which the passed-in function will be executed, and the scene where the execution fails and the parameter cb does not exist will be added to the callbacks.

timerFunc函数,在其中遍历callbacks执行每个函数,因为timerFunc函数, pending to ensure that the timerFunc function is called only once in an event loop. In this way, the function of nextTick function asynchronously executes the incoming function.

So the key is how to define timerFunc function. Because the methods for creating asynchronous execution functions are different in each browser, compatibility processing is required. The various methods are introduced below.

3.4 Why use microtasks first:

According to the execution sequence of the above event loop, a UI rendering will be performed before the next macro task is executed, and the waiting time is much longer than that of the micro task. Therefore, micro-tasks are used first when micro-tasks can be used, and macro-tasks are used when micro-tasks cannot be used, which degrades gracefully. *

4. References

Deep Dive into Responsive Principles — Vue.js
NextTick implementation principle, must win! - Nuggets
🚩Vue source code - nextTick implementation principle - Nuggets

I am Cloudy, a young front-end siege lion who loves research, technology, and sharing.
Personal notes are not easy to organize, thank you for reading, liking, following and collecting.
If you have any questions about the article, you are welcome to point it out, and you are also welcome to exchange and learn together!

云鱼
3.2k 声望530 粉丝

感谢阅读、浏览和关注!