this.a = 1 this.b = 2 this.c = 3 this.a = 5 this.a = 6想想嘛,根据 Vue 响应式原理,响应式数据在赋值时运行 setter,会触发视图更新,但是就像上面例子中连续赋值的场景可以说十分常见了,在 React 倒是可以写在一个 setState 里,但是 Vue 没有这种方法。总不能每次更新数据都立即给你刷新一次页面,如果每次赋值都跑一趟 render、patch,那整个页面不是卡得不行吗?解决这个问题的关键就是先把需要运行的更新函数都存入队列(而且,在加入队列时,相同的函数不会重复加入),再异步运行队列。
var circular = {} // 开发环境检测循环引用 var queue = [] // watcher 队列 var activatedChildren = [] // 堆代码 duidaima.com var has = {} // 是否已经加入队列 var waiting = false // 可以理解为是否在等待 flushing var flushing = false // 是否已经开始处理队列 var index = 0 // 队列当前运行到哪里你也许十分好奇,为什么会有 waiting 和 flushing 这么相似的两个变量呢?开始我也很疑惑,是否正在 flushing,是否等待 flushing(waiting),虽然还是有细微的区别:
2.在 flushing 时,已经确定开始队列任务
function queueWatcher(watcher) { var id = watcher.id if (has[id] == null) { has[id] = true // 标记 watcher 已添加到队列 if (!flushing) { // 队列还没开始更新,直接推入队列即可 queue.push(watcher) } else { // 队列正在处理中时,Vue 的做法是直接把新的 watcher 插到运行到的最新位置 // 这蕴含着隐藏逻辑 // 已经在前面运行过的 watcher 这时候会立即再在下一个运行 var i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }最后的 nextTick(flushSchedulerQueue) 就是把运行 watcher 队列放入 nextTick 队列中(虽然没有传参,但是 flushSchedulerQueue 可以读取 queueWatcher 处理的 queue)。短短的这一句函数调用,连接了两个重点:flushSchedulerQueue 是处理 watcher 队列的核心,而 nextTick 是 Vue 异步渲染的核心。
function flushSchedulerQueue() { flushing = true var watcher, id queue.sort(function(a, b) { return a.id - b.id }) // queue 可变长,queue.length 不能缓存 for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id has[id] = null // 已经处理的 watcher 从 has 去除 watcher.run() } // keep copies of post queues before resetting state var activatedQueue = activatedChildren.slice() var updatedQueue = queue.slice() resetSchedulerState() // 重置队列相关变量 callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) }简单来说 flushSchedulerQueue 就是用于处理 queueWatcher 编排好的 queue。这里提出一个简单的实践问题,在 watch 某个值的回调函数中,可以访问最新的 DOM 吗?
var nextTick = (function() { var callbacks = [] var pending = false var timerFunc function nextTickHandler() { pending = false var copies = callbacks.slice(0) callbacks.length = 0 for (var i = 0; i < copies.length; i++) { copies[i]() } } if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { timerFunc = function() { setImmediate(nextTickHandler) } } else if ( typeof MessageChannel !== 'undefined' && (isNative(MessageChannel) || // PhantomJS MessageChannel.toString() === '[object MessageChannelConstructor]') ) { var channel = new MessageChannel() var port = channel.port2 channel.port1.onmessage = nextTickHandler timerFunc = function() { port.postMessage(1) } } else if (typeof Promise !== 'undefined' && isNative(Promise)) { // use microtask in non-DOM environments, e.g. Weex var p = Promise.resolve() timerFunc = function() { p.then(nextTickHandler) } } else { // fallback to setTimeout timerFunc = function() { setTimeout(nextTickHandler, 0) } } return function queueNextTick(cb, ctx) { var _resolve callbacks.push(function() { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } if (!cb && typeof Promise !== 'undefined') { return new Promise(function(resolve, reject) { _resolve = resolve }) } } })()先解释一下 Vue 制造异步运行的方法吧——
Vue.prototype.$nextTick = function(fn) { return nextTick(fn, this) }
2.nextTick 的 callbacks