Vue手写实现异步更新详解
作者:夏日
前言:
本文将详细讲解具体的更新过程,并手写实现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
到此这篇关于Vue手写实现异步更新详解的文章就介绍到这了,更多相关Vue异步更新内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!