前言
从2018年开始用Vue开发项目,到如今也有几个年头,这期间也看过很多讲解Vue的书籍,对于Vue这门技术也有了自己的理解。Vue 作为目前最为主流的前端 MVVM 框架之一,在熟练使用的基础上,去深入理解其实现原理是非常有意义的一件事情。
阅读 Vue 源码就是一个很好的学习方式,不仅可以让我们帮助我们更快解决工作中遇到的问题,也能借鉴优秀源码的经验,学习高手发现问题、思考问题、解决问题的思路,学习怎么写出规范又好维护的高质量代码。
异步更新队列
Vue使用数据驱动来更新视图,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 Watcher
被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
例如,当你设置 vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环 “tick” 中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。
虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:
在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:
因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
nextTick
接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新后才执行;nextTick
是在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后使用nextTick,则可以在回调中获取更新后的 DOM。
使用场景:想要操作 基于最新数据的生成DOM 时,就将这个操作放在 nextTick
的回调中;
基础知识
nextTick
函数的作用可以理解为异步执行传入的函数,这里先介绍一下什么是异步执行,从 JS 运行机制说起。
JS运行机制
JS 的执行是单线程的,所谓的单线程就是事件任务要排队执行,前一个任务结束,才会执行后一个任务,这就是同步任务,为了避免前一个任务执行了很长时间还没结束,那下一个任务就不能执行的情况,引入了异步任务的概念。JS 运行机制简单来说可以按以下几个步骤。
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,会把其回调函数作为一个任务添加到任务队列中。
- 一旦执行栈中的所有同步任务执行完毕,就会读取任务队列,看看里面有那些任务,将其添加到执行栈,开始执行。
- 主线程不断重复上面的第三步。也就是常说的事件循环(Event Loop)。
异步任务的类型
nextTick
函数异步执行传入的函数,是一个异步任务。异步任务分为两种类型。
主线程的执行过程就是一个 tick
,而所有的异步任务都是通过任务队列来一一执行。任务队列中存放的是一个个的任务(task)。规范中规定 task 分为两大类,分别是宏任务(macro task)和微任务 (micro task),并且每个 macro task
结束后,都要清空所有的 micro task
。
用一段代码形象介绍 task的执行顺序。
在浏览器环境中, 常见的创建 macro task
的方法有
-
setTimeout
、setInterval
、postMessage
、MessageChannel
(队列优先于setTimeiout
执行) -
网络请求IO
- 页面交互:DOM、鼠标、键盘、滚动事件
- 页面渲染
常见的创建 micro task
的方法
-
Promise.then
-
MutationObserve
-
process.nexttick
在 nextTick
函数要利用这些方法把通过参数 cb
传入的函数处理成异步任务。
nextTick 实现原理
将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务,为了尽快执行所以优先选择微任务;
nextTick
提供了四种异步方法 Promise.then
、MutationObserver
、setImmediate
、setTimeOut(fn,0)
Vue.nextTick 内部逻辑
在执行 initGlobalAPI(Vue)
初始化 Vue 全局 API 中,这么定义
Vue.nextTick
:
可以看出是直接把 nextTick 函数赋值给 Vue.nextTick,就可以了,非常简单。
vm.$nextTick 内部逻辑
可以看出是vm.$nextTick
内部也是调用 nextTick
函数。
源码解读
nextTick
的源码位于 src/core/util/next-tick.js
nextTick
源码主要分为两块:
可以看到在 nextTick
函数中把通过参数 cb 传入的函数,做一下包装然后 push 到 callbacks
数组中。
然后用变量 pending 来保证执行一个事件循环中只执行一次 timerFunc()。
最后执行 if (!cb && typeof Promise !== 'undefined')
,判断参数 cb
不存在且浏览器支持 Promise,则返回一个 Promise 类实例化对象。例如 nextTick().then(() => {})
,当 _resolve
函数执行,就会执行 then 的逻辑中。
来看一下 timerFunc
函数的定义,先只看用 Promise 创建一个异步执行的 timerFunc
函数 。
其中 isNative
方法是如何定义,代码如下。
在其中发现 timerFunc
函数就是用各种异步执行的方法调用 flushCallbacks
函数。
来看一下 flushCallbacks
函数
执行 pending = false
使下个事件循环中能 nextTick
函数中调用 timerFunc
函数。
执行 var copies = callbacks.slice(0);callbacks.length = 0
; 把要异步执行的函数集合 callbacks
克隆到常量 copies
,然后把 callbacks
清空。
然后遍历 copies
执行每一项函数。回到 nextTick
中是把通过参数 cb
传入的函数包装后 push 到 callbacks
集合中。来看一下怎么包装的。
逻辑很简单。若参数 cb 有值。在 try 语句中执行 cb.call(ctx)
,参数 ctx 是传入函数的参数。 如果执行失败执行 handleError(e, ctx, 'nextTick')
。
若参数 cb 没有值。执行 _resolve(ctx)
,因为在 nextTick
函数中如何参数 cb 没有值,会返回一个 Promise 类实例化对象,那么执行 _resolve(ctx)
,就会执行 then 的逻辑中。
到这里 nextTick
函数的主线逻辑就很清楚了。定义一个变量 callbacks
,把通过参数 cb 传入的函数用一个函数包装一下,在这个中会执行传入的函数,及处理执行失败和参数 cb 不存在的场景,然后 添加到 callbacks。
调用 timerFunc
函数,在其中遍历 callbacks
执行每个函数,因为 timerFunc
是一个异步执行的函数,且定义一个变量 pending
来保证一个事件循环中只调用一次 timerFunc
函数。这样就实现了 nextTick
函数异步执行传入的函数的作用了。
那么其中的关键还是怎么定义 timerFunc
函数。因为在各浏览器下对创建异步执行函数的方法各不相同,要做兼容处理,下面来介绍一下各种方法。
为什么优先使用微任务:
按照上面事件循环的执行顺序,执行下一次宏任务之前会执行一次 UI 渲染,等待时长比微任务要多很多。所以在能使用微任务的时候优先使用微任务,不能使用微任务的时候才使用宏任务,优雅降级。*