前言
熟悉Vue的前端,想必对Vue里的nexTick也很熟悉了,用的时候都知道他是延迟回调,有时候用起来甚至和setTimeout看起来是同样的效果。但他和setTimeout到底有什么区别呢?它是如何实现的?
本文就nextTick的实现引入,来讨论一下js中的异步和同步,微任务和宏任务。
nexTick
用法
在下一次DOM更新循环结束之后执行延迟回调(例如,操作更新后的 DOM 元素)。在修改数据之后立即使用这个方法,获取更新后的DOM。
nextTick 方法有两种用法:
使用回调函数:
你可以将一个回调函数传递给 this.$nextTick,这个回调函数将在 DOM 更新之后执行。这可以用于访问已经更新的 DOM 元素或执行其他需要等待 DOM 更新完成的操作。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
this.$nextTick(function () {
// 在 DOM 更新之后执行操作
});
使用 Promise:
从 Vue.js 2.1.0 开始,nextTick 还返回一个 Promise 对象,可以用于更方便的异步操作管理。
this.$nextTick().then(function () {
// 在 DOM 更新之后执行操作
});
完整例子:
{{ message }}
更新消息
export default {
data() {
return {
message: '初始消息'
};
},
methods: {
updateMessage() {
this.message = '新消息';
// 使用 nextTick 确保 DOM 已经更新后再操作
this.$nextTick(() => {
// 此时可以安全地访问更新后的 DOM 元素
const paragraph = this.$el.querySelector('p');
paragraph.style.color = 'red';
});
}
}
};
在上面的示例中,当点击按钮时,updateMessage 方法首先更新 message 数据,然后使用 this.$nextTick 来确保在 DOM 更新后再修改段落的文字颜色。这是因为 Vue.js 异步更新 DOM,如果不使用 nextTick,则在修改 DOM 元素之前可能会尝试访问尚未更新的 DOM。
源码实现
// 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 && 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, 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 (typeof setImmediate !== '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)
}
}
可以看到上面有几个条件判断,如果支持 Promise 就用 Promise
如果不支持就用 MutationObserver MDN-MutationObserver
MutationObserver 它会在指定的DOM发生变化时被调用
如果不支持 MutationObserver 的话就用 setImmediate MDN-setImmediate
但是这个特性只有最新版IE和node支持,然后是最后一个条件
如果这些都不支持的话就用setTimeout。
看完这一段其实也很懵,为什么要这样设计呢?为什么要这样一个顺序来判断呢?说到这里就不得不讨论JavaScript 运行机制(Event Loop)&微任务宏任务了。
JavaScript 运行机制(Event Loop)
单线程
JavaScript 是一门单线程编程语言,这意味着它在任何给定时刻只能执行一个任务。单线程的特点如下:
「顺序执行:」 JavaScript 代码按照它们在程序中的顺序依次执行,每个操作都需要等待前一个操作完成后才能开始。这确保了代码的执行顺序是可控的。
「阻塞问题:」 由于 JavaScript 是单线程的,如果执行某个操作需要一段时间(例如,执行一个耗时的计算、等待网络请求返回等),它将会阻塞整个主线程,导致页面不响应。这种情况可能会对用户体验产生负面影响。
为了解决阻塞问题,JavaScript 使用了异步编程模型。通过使用回调函数、Promise、async/await 等机制,可以在不阻塞主线程的情况下处理耗时操作。这意味着即使 JavaScript 是单线程的,它仍然可以执行异步操作,以确保应用程序保持响应性。
一些典型的异步操作包括:
定时器:例如 setTimeout 和 setInterval,用于延时执行代码或周期性执行代码。
网络请求:例如使用 XMLHttpRequest 或 Fetch API 发起HTTP请求。
事件处理:例如点击、键盘输入等事件的处理。
文件操作:例如读写文件。
数据库访问:例如使用 IndexedDB 进行本地数据库操作。
虽然 JavaScript 是单线程的,但由于异步编程模型的存在,它能够有效地处理并发任务,确保应用程序的响应性,同时也需要开发者小心处理异步操作,以避免潜在的问题,例如回调地狱(Callback Hell)或异步并发问题。
同步和异步
JavaScript 中的同步(Synchronous)和异步(Asynchronous)是关于代码执行的方式。之间的区别:
「同步(Synchronous)」:
同步代码按顺序执行:在同步代码中,每行代码按照它们在程序中的顺序依次执行,一个操作必须在另一个操作完成之后才能开始。这种行为称为阻塞(Blocking),因为在执行某个操作时,程序会等待其完成才继续执行下一个操作。
同步代码会阻塞主线程:在浏览器环境中,同步操作会阻塞 JavaScript 主线程,导致页面在执行耗时的任务时变得不响应,这可能会引发用户体验问题。
同步代码适用于一些简单的操作,但不适用于需要长时间执行的任务,因为它会阻塞应用程序的响应性。
「异步(Asynchronous)」:
异步代码不按顺序执行:在异步代码中,操作不一定会按照它们出现在代码中的顺序执行。相反,异步操作通常会在后台执行,不会阻塞主线程,允许主线程继续执行其他任务。
异步代码适用于耗时操作:异步代码适用于需要执行较长时间操作(例如网络请求、文件读写、定时器等)的情况,因为它们不会阻塞主线程。
异步操作通常使用回调函数、Promise、async/await 等方式来处理结果或在操作完成时执行特定的代码。
以下是一个使用异步操作的示例,使用了异步回调函数:
console.log("开始");
setTimeout(function() {
console.log("定时器回调函数执行");
}, 2000);
console.log("结束");
在上面的示例中,setTimeout 是一个异步操作,它会在2秒后执行回调函数,但不会阻塞主线程。因此,"开始" 和 "结束" 日志会立即显示,然后在2秒后才会显示 "定时器回调函数执行"。
JavaScript 中的异步操作是非常常见的,用于处理许多与用户界面、网络通信、文件操作等相关的任务,以确保应用程序保持响应性。
宏任务和微任务
JS任务又分为宏任务和微任务。
「宏任务(macrotask)」:
宏任务代表一组异步操作,它们通常比微任务的优先级低。宏任务包括:
用户交互事件(例如,点击、滚动等)
文件 I/O 操作(例如,读写文件)
定时器操作(例如,setTimeout、setInterval、setImmediate)
网络请求(例如,Ajax 请求)
原生事件(例如,页面加载、资源加载)
requestAnimationFrame(用于执行动画操作)
宏任务是在事件循环的主循环中执行的,每个宏任务完成后,JavaScript 引擎会检查是否有微任务需要执行。
「微任务(microtask)」:
微任务通常具有比宏任务更高的执行优先级。微任务包括:
Promise 的回调函数
async/await 中 await 后面的代码
MutationObserver 的回调函数
微任务队列会在当前宏任务执行完毕之后立即执行,确保了微任务会在下一个宏任务之前执行完。这对于处理异步操作的结果,以及确保一些特定的回调尽早执行非常有用。
以下是一个示例,演示宏任务和微任务的执行顺序:
console.log('Start');
setTimeout(() => {
console.log('Timeout (macro task)');
}, 0);
Promise.resolve().then(() => {
console.log('Promise (micro task)');
});
console.log('End');
上述代码会产生以下输出:
Start
End
Promise (micro task)
Timeout (macro task)
领取专属 10元无门槛券
私享最新 技术干货