最近面试总是会被问到这么一个问题:在使用vue的时候,将for循环中声明的变量i从1增加到100,然后将i展示到页面上,页面上的i是从1跳到100,还是会怎样?答案当然是只会显示100,并不会有跳转的过程。
怎么可以让页面上有从1到100显示的过程呢,就是用setTimeout或者Promise.then等方法去模拟。
讲道理,如果不在vue里,单独运行这段程序的话,输出一定是从1到100,但是为什么在vue中就不一样了呢?
for(let i=1; i<=100; i++){ console.log(i); }
这就涉及到Vue底层的异步更新原理,也要说一说nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。
众所周知,JS是基于事件循环的单线程的语言。 执行的步骤大致是:
我们把主线程执行一次的过程叫一个tick,所以nextTick就是下一个tick的意思,也就是说用nextTick的场景就是我们想在下一个tick做一些事的时候。
所有的异步任务结果都是通过任务队列来调度的。而任务分为两类:宏任务(macro task)和微任务(micro task)。它们之间的执行规则就是每个宏任务结束后都要将所有微任务清空。 常见的宏任务有setTimeout/MessageChannel/postMessage/setImmediate
,微任务有MutationObsever/Promise.then
。
想要透彻学习事件循环,推荐Jake在JavaScript全球开发者大会的演讲,保证讲懂!
大家都知道vue的响应式的靠依赖收集和派发更新来实现的。在修改数组之后的派发更新过程,会触发setter的逻辑,执行dep.notify():
// src/core/observer/watcher.js class Dep { notify() { //subs是Watcher的实例数组 const subs = this.subs.slice() for(let i=0, l=subs.length; i<l; i++){ subs[i].update() } } }
遍历subs
里每一个Watcher
实例,然后调用实例的update
方法,下面我们来看看update
是怎么去更新的:
class Watcher { update() { ... //各种情况判断之后 else{ queueWatcher(this) } } }
update
执行后又走到了queueWatcher
,那就继续去看看queueWatcher
干啥了(希望不要继续套娃了:
//queueWatcher 定义在 src/core/observer/scheduler.js const queue: Array<Watcher> = [] let has: { [key: number]: ?true } = {} let waiting = false let flushing = false let index = 0 export function queueWatcher(watcher: Watcher) { const id = watcher.id //根据id是否重复做优化 if(has[id] == null){ has[id] = true if(!flushing){ queue.push(watcher) }else{ let i=queue.length - 1 while(i > index && queue[i].id > watcher.id){ i-- } queue.splice(i + 1, 0, watcher) } if(!waiting){ waiting = true //flushSchedulerQueue函数: Flush both queues and run the watchers nextTick(flushSchedulerQueue) } } }
这里queue
在pushwatcher时是根据id和flushing
做了一些优化的,并不会每次数据改变都触发watcher
的回调,而是把这些watcher
先添加到⼀个队列⾥,然后在nextTick
后执⾏flushSchedulerQueue
。
flushSchedulerQueue
函数是保存更新事件的queue的一些加工,让更新可以满足Vue更新的生命周期。
这里也解释了为什么for循环不能导致页面更新,因为for是主线程的代码,在一开始执行数据改变就会将它push到queue里,等到for里的代码执行完毕后i的值已经变化为100时,这时vue才走到nextTick(flushSchedulerQueue)
这一步。
接着打开vue2.x的源码,目录core/util/next-tick.js,代码量很小,加上注释才110行,是比较好理解的。
const callbacks = [] let pending = false export function nextTick (cb?: Function, ctx?: Object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() }
首先将传入的回调函数cb(上节的flushSchedulerQueue
)压入callbacks
数组,最后通过timerFunc
函数一次性解决。
let timerFunc 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) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { 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)) { timerFunc = () => { setImmediate(flushCallbacks) } } else { timerFunc = () => { setTimeout(flushCallbacks, 0) } }
timerFunc
下面一大片if else
是在判断不同的设备和不同情况下选用哪种特性去实现异步任务:优先检测是否原生⽀持Promise
,不⽀持的话再去检测是否⽀持MutationObserver
,如果都不行就只能尝试宏任务实现,首先是setImmediate
,这是⼀个⾼版本 IE 和 Edge 才⽀持的特性,如果都不⽀持的话最后就会降级为 setTimeout 0。
这⾥使⽤callbacks
⽽不是直接在nextTick
中执⾏回调函数的原因是保证在同⼀个 tick 内多次执⾏nextTick
,不会开启多个异步任务,⽽把这些异步任务都压成⼀个同步任务,在下⼀个 tick 执⾏完毕。
nextTick
不仅是vue的源码文件,更是vue的一个全局API。下面来看看怎么使用吧。
当设置 vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环tick中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用数据驱动的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)
。这样回调函数将在 DOM 更新完成后被调用。
官网用例:
<div id="example">{{message}}</div>
var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改数据 vm.$el.textContent === 'new message' // false Vue.nextTick(function () { vm.$el.textContent === 'new message' // true })
并且因为$nextTick()
返回一个 Promise
对象,所以也可以使用async/await
语法去处理事件,非常方便。
以上就是详解Vue的异步更新实现原理的详细内容,更多关于vue 异步更新的资料请关注站长技术其它相关文章!
学个技术或者搞副业什么靠谱? 学姐告诉你:答案是Python Python语言是所有语言中...
前言 从毕业开始学习windows UI编程,工作中总是和一些API打交道,但是从没有做...
spring中使用注解时配置文件的写法: xml version="1.0" encoding="UTF-8" span ...
简介 saltstack是由thomas Hatch于创建的一个开源项目,设计初衷是为了实现一个...
昨天有位刚入行数据分析的朋友跟我吐槽,自己入门到现在只会用excel做做分析图表...
从MySQL源码看其网络IO模型 前言 MySQL是当今最流行的开源数据库,阅读其源码是...
HelloWorld Java 文章目录 HelloWorld Java 一、HelloWorld程序 二、逐行分析Hel...
content相当于你例子中的FCKeditor1。 复制代码 代码如下: //获取格式化的编辑器...
php扩展安装方法极简单. 也遵循3大步.但多出一个phpize的步骤. 1.pecl.php.net ...
1、挂接事件,比如onkeydown事件,要在FCKeditor_OnComplete里实现: 复制代码 ...