原理
- 为什么需要
nextTick,Vue是异步修改DOM的并且不鼓励开发者直接接触DOM,但有时候业务需要必须对数据更改--刷新后的DOM做相应的处理, 这时候就可以使用Vue.nextTick(callback)这个 api 了。 - 理解原理前的准备 首先需要知道事件循环中宏任务和微任务这两个 概念,常见的宏任务有
script, setTimeout, setInterval, setImmediate, I/O, UI rendering常见的微任务有process.nextTick(Nodejs),Promise.then(), MutationObserver; - 理解
nextTick的原理正是Vue通过异步队列控制DOM更新和nextTick回调函数先后执行的方式。如果大家看过这部分的源码,会发现其中 做了很多isNative()的判断,因为这里还存在兼容性优雅降级的问题。可见 Vue 开发团队的深思熟虑,对性能的良苦用心。
使用逻辑
在 Vue.js 里是数据驱动视图变化,由于 JS 执行是单线程的,在一个 tick 的过程中,它可能会多次修改数据,但 Vue.js 并不会傻到每修改一次数据就去驱动一次视图变化,它会把这些数据的修改全部 push 到一个队列里,然后内部调用 一次 nextTick 去更新视图,所以数据到 DOM 视图的变化是需要在下一个 tick 才能完成。
大致分为以下几个步骤:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度被调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。
- 在浏览器环境中,常见的
macro task有setTimeout、MessageChannel、postMessage、setImmediate; - 常见的
micro task有MutationObsever和Promise.then。
使用场景
语法:Vue.nextTick([callback, context])
参数:
{Function} [callback]:回调函数,不传时提供promise调用{Object} [context]:回调函数执行的上下文环境,不传默认是自动绑定到调用它的实例上。
//改变数据
vm.message = 'changed'
//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'
//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
// DOM 更新了
//可以得到'changed'
console.log(vm.$el.textContent)
})
// 作为一个 Promise 使用 即不传回调
Vue.nextTick()
.then(function () {
// DOM 更新了
})
Vue实例方法vm.$nextTick做了进一步封装,把context参数设置成当前Vue实例。
源码
class Dep{
static target=null
constructor(){
this.subs=[];
}
addSubs(watcher){
this.subs.push(watcher)
}
notify(){
for(let i=0;i<this.subs.length;i++){
this.subs[i].update();
}
}
}
class Observer{
constructor(data){
if(typeof data=='object'){
this.walk(data);
}
}
walk(obj){
const keys=Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
this.defineReactive(obj, keys[i])
}
}
defineReactive(obj,key){
if(typeof obj[key]=='object'){
this.walk(obj[key]);
}
const dep=new Dep();
let val=obj[key];
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
//get代理将Dep.target即Watcher对象添加到依赖集合中
get: function reactiveGetter () {
if (Dep.target) {
dep.addSubs(Dep.target);
}
return val;
},
set: function reactiveSetter (newVal) {
val=newVal;
dep.notify()
}
})
}
}
let uid=0
class Watcher{
constructor(vm,key,cb){
this.vm=vm;
this.key=key;
this.uid=uid++;
this.cb=cb;
//调用get,添加依赖
Dep.target=this;
this.value=vm.$data[key];
Dep.target=null;
}
update(){
if(this.value!==this.vm.$data[this.key]){
this.value=this.vm.$data[this.key];
if(!this.vm.waiting){//控制变量,控制每次事件循环期间只添加一次flushUpdateQueue到callbacks
this.vm.$nextTick(this.vm.flushUpdateQueue);
this.vm.waiting=true;
}
//不是立即执行run方法,而是放入updateQueue队列中
if(!has[this.uid]){
has[this.uid]=true;
updateQueue.push(this);
}
}
}
run(){
this.cb(this.value);
}
}
const updateQueue=[];//异步更新队列
let has={};//控制变更队列中不保存重复的Watcher
const callbacks=[];
let pending=false;
class Vue{
constructor(options){
this.waiting=false
this.$el=options.el;
this._data=options.data;
this.$data=this._data;
this.$nextTick=this.nextTick;
new Observer(this._data);
}
//简易版nextTick
nextTick(cb){
callbacks.push(cb);
if(!pending){//控制变量,控制每次事件循环期间只执行一次flushCallbacks
pending=true;
setTimeout(()=>{
//会在同步代码(上一次宏任务)执行完成后执行
this.flushCallbacks();
})
}
}
//清空UpdateQueue队列,更新视图
flushUpdateQueue(){
while(updateQueue.length!=0){
updateQueue.shift().run();
}
has={};
this.waiting=false;
}
//清空callbacks
flushCallbacks(){
while(callbacks.length!=0){
callbacks.shift()();
}
pending=false;
}
}
//======测试=======
let data={
message:'hello',
num:0
}
let app=new Vue({
data:data
});
//模拟数据监听
let w1=new Watcher(app,'message',function(value){
//模拟dom变更
console.log('message 引起的dom变更--->',value);
})
//模拟数据监听
let w2=new Watcher(app,'num',function(value){
//模拟dom变更
console.log('num 引起的dom变更--->',value);
})
data.message='world'//数据一旦更新,会为nextTick的事件队列callbacks中加入一个flushUpdateQueue回调函数
data.message='world1'
data.message='world2'//message的变更push到updateQueue中,只保存最后一次赋值的结果
for(let i=0;i<=100;i++){
data.num=i;//num的变更push到updateQueue中,只保存最后一次赋值的结果
}
//开发者为callbacks添加的异步回调事件
app.$nextTick(function(){
console.log('这是dom更新完成后的操作')
})
//例子中的执行顺序是,先执行同步代码,其中第一次修改数据data.message='world'会把dom更新回调函数push到callbacks队列,并把dom更新操作的cb回调放入updateQueue,后续对message的变更操作