vue nextTick原理
后面谈到了vue2.x的响应式原理,vue.js在视图更新采纳的是异步更新策略,咱们来看看它是怎么做到的。
/** ? */
for(let i = 0; i < 100; i++) {
this.count++;
}
/** ? */
在dom更新后执行一些操作
this.$nextTick(fn)
先抛出两个问题:
- for循环更新count数值,dom会被更新100次吗?
- nextTick是如何做到监听dom更新结束的?
异步更新波及到js的运行机制,具体的可看这里
【event loop机制】
这篇文章呢咱们次要从源码角度来剖析nextTick的原理实现。
这是咱们响应式外面的watcher类
<!--观察者Watcher类-->
class Watcher {
constructor () {
Dep.target = this // new Watcher的时候把观察者寄存到Dep.target外面
}
update () {
queueWatcher(this) // 异步更新策略
}
run () {
// dom在这里执行真正的更新
}
}
watcher对象在进行更新执行update,外部次要执行了一个queueWatcher函数,将watcher对象作为this进行传递,所以咱们便从queueWatcher这个口子开始。
queueWatcher
queueWatcher函数在scheduler文件外面
/** queueWatcher函数*/
let has = {};
let queue = [];
let waiting = false;
function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 避免queue队列wachter对象反复
if (has[id] == null) {
has[id] = true
queue.push(watcher)
// 传递本次的更新工作
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
/** flushSchedulerQueue函数 */
function flushSchedulerQueue () {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
// 执行更新
watcher.run();
}
// 更新结束复原标记位
waiting = false;
}
- queue外面寄存着咱们本次要更新的watcher对象,queueWatcher函数做了一个判重操作,雷同的watcher对象只会被退出到queue队列一次。
- flushSchedulerQueue函数顺次调用了wacther对象的run办法执行更新。并作为回调传递给了nextTick函数。
- waiting这个标记位代表咱们是否曾经向nextTick函数传递了更新工作,nextTick会在以后task完结后再去解决传入的回掉,只须要传递一次,更新结束再重置这个标记位。
next-tick
let callbacks = [];
let pending = false;
let timerFunc;
/**----- nextTick -----*/
function nextTick (cb) {
// 把传进来的回调函数放到callbacks队列里
callbacks.push(cb);
// pending代表一个期待状态 等这个tick执行
if (!pending) {
pending = true
timerFunc()
}
// 如果没传递回调 提供一个Promise化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
/**----- timerFunc ----*/
// 1、优先思考Promise实现
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]')) {
// 2、降级到MutationObserver实现
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)) {
// 3、降级到setImmediate实现
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 4、如果以上都不反对就用setTimeout来兜底了
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
function flushCallbacks () {
// 将callbacks中的cb顺次执行
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
-
传进来的回调函数会被保留到callbacks队列外面,这里应用callbacks 而没有在nextTick中间接执行回调函数,是因为这样能够保障在同一个tick 内屡次执行nextTick,在一个tick外面实现渲染,不会开启多个异步工作。
// 举个栗子???? // 如果咱们间接在nexttick外面间接执行回调 function nextTick (cb) { setTimeout(cb) } nextTick(cb1) nextTick(cb2) 这种状况下就会开启两个异步工作,也就是两次事件循环,造成了页面不必要的渲染
- timerFunc是实现的外围,它会优先应用Promise等microtask,保障在同一个事件循环外面执行,这样页面只须要渲染一次。切实不行的话用setTimeout来兜底,尽管会造成二次渲染,但这也是最差的状况。vue在这里用了降级解决的策略。
$nextTick
最初再把nexttick函数挂到Vue原型上就OK了
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
}
小结
vue异步更新,实质上是js事件机制的一种使用,优先思考了具备高优先级的microtask,为了兼容,又做了降级策略。
当初再回头看结尾的那两个问题
- for循环更新count数值,dom会被更新100次吗?
不会,因为queueWatcher函数做了过滤,雷同的watcher对象不会被反复增加。
- nextTick是如何做到监听dom更新结束的?
vue用异步队列的形式来管制DOM更新和nextTick回调先后执行,保障了能在dom更新后在执行回调。
发表回复