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 更新后在执行回调。