Vue异步更新
夏日 人气:1前言:
本文将详细讲解具体的更新过程,并手写实现Vue
的异步更新逻辑相关代码。
收集去重后的watcher进行更新
这里先回顾一下依赖收集的相关知识:
- 页面首次挂载,会从
vm
实例上获取data
中的值,从而调用属性的get
方法来收集watcher
- 当
vm
实例上的属性更新它的值时,会执行收集到的watcher
的update
方法
看下之前完成的代码:
class Watcher { // some code ... update () { // 直接执行更新操作 this.get() } }
那么watcher
的update
到底应该如何被执行呢?这就是本文的重点。
watcher
的更新操作主要分为如下俩步:
- 将
watcher
去重后放到队列中 - 在异步任务中执行存放的所有
watcher
的run
方法
代码如下:
class Watcher { // some code update () { queueWatcher(this); } run () { this.get(); } } export default Watcher; let queue = []; let has = {}; // 使用对象来保存id,进行去重操作 let pending = false; // 如果异步队列正在执行,将不会再次执行 function flushSchedulerQueue () { queue.forEach(watcher => { watcher.run(); if (watcher.options.render) { // 在更新之后执行对应的回调: 这里是updated钩子函数 watcher.cb(); } }); // 执行完成后清空队列 queue = []; has = {}; pending = false; } function queueWatcher (watcher) { const id = watcher.id; if (!has[id]) { queue.push(watcher); has[id] = true; if (!pending) { pending = true; // 异步执行watcher的更新方法 setTimeout(flushSchedulerQueue) } } }
此时已经实现了视图的异步更新,但是Vue
还为用户提供而了$nextTick
方法,让用户可以在DOM
更新之后做些事情。即$nextTick
中的方法会在flushSchedulerQueue
执行后才能执行,下面就来看下$nextTick
和视图更新之间的逻辑。
实现nextTick方法
在queueWatcher
中其实并不是直接调用setTimeout
来进行视图更新的,而是会调用内部的nextTick
方法。为用户提供的$nextTick
方法,也会调用nextTick
方法。
该方法实现如下:
let callbacks = []; let pending = false; function flushCallbacks () { callbacks.forEach(cb => cb()); callbacks = []; pending = false; } export function nextTick (cb) { callbacks.push(cb); if (!pending) { pending = true; timerFunc(); } }
nextTick
会接收一个回调函数,并将回调函数放到callbacks
数组中,之后会通过timerFunc
来异步执行callbacks
中的每一个函数:
let timerFunc; if (Promise) { timerFunc = function () { return Promise.resolve().then(flushCallbacks); }; } else if (MutationObserver) { timerFunc = function () { const textNode = document.createTextNode('1'); const observer = new MutationObserver(() => { flushCallbacks(); observer.disconnect(); }); const observe = observer.observe(textNode, { characterData: true }); textNode.textContent = '2'; }; } else if (setImmediate) { timerFunc = function () { setImmediate(flushCallbacks); }; } else { timerFunc = function () { setTimeout(flushCallbacks); }; }
timerFunc
对异步API
进行了兼容处理,分别会先尝试使用微任务Promise.then
、MutationObserver
、setImmediate
,如果这些API
浏览器都不支持的话,那么会使用宏任务setTimeout
。
在queueWatcher
里我们将flushSchedulerQueue
作为参数执行nextTick
:
function queueWatcher (watcher) { const id = watcher.id; if (!has[id]) { queue.push(watcher); has[id] = true; if (!pending) { pending = true; nextTick(flushSchedulerQueue); } } }
在Vue
原型上,也要增加用户可以通过实例来调用的$nextTick
方法,其内部调用nextTick
:
Vue.prototype.$nextTick = function (cb) { nextTick(cb); };
$nextTick
会将用户传入的回调函数也放到callbacks
中,通过异步API
来执行。
测试demo详解
上面已经讲解了视图更新和$nextTick
的实现代码,接下来写一个demo
来实践一下。
下面是实际开发中可能会用到的一段代码:
<div id="app">{{name}}</div> <script> const vm = new Vue({ el: '#app', data () { return { name: 'zs' }; } }); vm.name = 'ls'; console.log('$el', vm.$el); vm.$nextTick(() => { console.log('next tick $el', vm.$el); }); </script>
其输出结果如下:
在了解了$nextTick
的具体实现后,我们详细分析下代码的执行流程:
- 在修改值之后,我们将要更新的
watcher
队列放到了flushSchedulerQueue
函数中来执行 - 而
nextTick
将flushSchedulerQueue
放到了callbacks
中,通过异步任务来执行flushCallbacks
- 由于异步任务要等到主线程中的代码执行完毕后才会执行,所以此时先打印
vm.$el
,视图尚未更新 - 接下来会继续执行
vm.$nextTick
,将vm.$nextTick
中的回调函数也放到了callbacks
中,但是其位置在flushSchedulerQueue
后边 - 主线程中的代码执行完毕,开始执行异步任务
flushCallbacks
。首先执行flushSchedulerQueue
更新DOM
,然后再执行$nextTick
中的回调函数,此时回调函数中可以获取到最新的DOM
加载全部内容