深入理解nextTick()

这篇文章主要讲一下nextTick()的使用,event loop,和vue中nextTick()的原理,以及在使用nextTick()的时候踩到的坑。作为我学习的记录。
首先,nextTick()的用法有两种:

  1. Vue.nextTick([callback, context])
  2. vm.$nextTick([callback])

两个方法的作用都是在DOM更新循环结束之后执行延迟回调。当我们改变了数据的时候,DOM的渲染需要时间,然而我们希望去操作DOM元素,就需要等待渲染完成后再去操作。就需要用到nextTick,将等待DOM渲染完成后需要的操作放在回调函数里。
不同的是,Vue.nextTick([callback, context])是全局的,使用vm.$nextTick([callback])时的回调会自动绑定到调用它的实例上。而这里文档中并没有说明全局的Vue.nextTick([callback, context])context参数是用来做什么的,后面我将通过源码的分析告诉大家这个参数的用法。

好,现在大家应该都知道nextTick是用来做什么的了。这个方法是怎么实现的呢?首先,需要理解一下Event loop。

Event loop

很多时候我们看到别人的代码里有这么一句setTimeout(fn, 0)。额,作为前端小白的我,觉得这段代码很神奇。延时0毫秒,不就是不用延时么,为什么还要这么写一句呢?这里其实就是Event loop的知识点。

首先,JavaScript是一个单线程的语言。
也就是说,在特定的时间只能是特定的代码被执行,要等待上一步的代码执行完成后在执行下一段代码。那么问题来了,如果上一段代码的请求需要等待很长时间,那么后面的代码就得给我等着,用户也得给我等着。最终,用户就会关掉浏览器走人。那我们今天的表演就结束了,欢迎收看,下期再见。
呵呵,其实,JavaScript除了主线程以外,还有一个叫做任务队列的东东。他会把一些需要一定等待时间的操作,放进任务队列里。

JavaScript的执行依靠函数调用栈和任务队列。
首先我们弄懂栈和队列的区别:
栈是先进后出,后进先出。
队列则相反,是先进先出。

函数执行栈

我们的js代码从上到下的执行,当一个函数被执行的时候,都会有一个执行上下文,全局环境也有一个执行上下文,就是全局的上下文。JavaScript将以栈的形式来存储他们。每执行一个函数,就把它上下文存入栈。栈的最底层就是全局上下文,栈顶就是当前正在执行的函数。每当一个函数执行结束,他的执行上下文就从栈中被弹出,释放。最底层的全局上下文,在浏览器关闭的时候才被弹出。

任务队列

任务队列有两种:macro-task(task)和micro-task(job)

macro-task(task):

  • setTimeout/setInterval
  • setImmediate
  • I/O操作
  • UI rendering

micro-task(job):

  • process.nextTick
  • Promise
  • MutationObserve
注意:以上的方法的回调函数会被分发到执行队列中,而他们自身会被直接执行,比如Promise只有then()会被加入到执行队列中,而Promise本身会被直接执行。

JavaScript执行的机制是:首先执行调用栈中的函数,当调用栈中的执行上下文全部被弹出,只剩下全局上下文的时候,就开始执行job的执行队列,job的执行完以后就开始执行task的队列中的。先进入的先执行,后进入的后执行。无论是task还是job都是通过函数调用栈来执行。task执行完成一个,js代码会继续检查是否有job需要执行。就形成了task-job-task-job的循环(其实这里可以将第一次的函数调用栈也看成一个task)。这就形成了event loop.

好了,现在可以来看nextTick的实现原理了

  var nextTick = (function () {
    // 这里存放的是回调函数的队列
    var callbacks = [];
    var pending = false;
    var timerFunc;

    //这个函数就是DOM更新后需要执行的
    function nextTickHandler () {
      pending = false;
       //这里将回调函数copy给copies
      var copies = callbacks.slice(0);
      callbacks.length = 0;
      //进行循环执行回调函数的队列
      for (var i = 0; i < copies.length; i++) {
        copies[i]();
      }
  }
})()

vue用了三个方法来执行nextTickHandler函数,分别是:

  • Promise
//当浏览器支持Promise的时候就是用Promise
p.then(nextTickHandler).catch(logError);
  • MutationObserver
//当浏览器支持MutationObserver的时候就是用MutationObserver
var observer = new MutationObserver(nextTickHandler);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true
  });
  timerFunc = function () {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  • setTimeout
//当以上都不支持的时候就用setTimeout
setTimeout(nextTickHandler, 0);

那么Vue.nextTick([callback, context])的第二个参数是什么呢?来看下面的代码。

  return function queueNextTick (cb, ctx) {
    var _resolve;
    callbacks.push(function () {
    //看这里,其实是可以给cb指定一个对象环境,来改变cb中this的指向
      if (cb) { cb.call(ctx); }
      if (_resolve) { _resolve(ctx); }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise(function (resolve) {
        _resolve = resolve;
      })
    }
  }

看到代码后,我开心的这么写道

Vue.nextTick(()=>{
    this.text()
}, { 
  text(){
    console.log('test')
  }
})

结果报错了,这是为什么呢?
源码中使用的是if (cb) { cb.call(ctx) } 所以不能使用箭头函数,箭头函数的this是固定的,是不可用apply,call,bind来改变的。改成这样:

Vue.nextTick(function () {
    this.text()
}, { 
  text(){
    console.log('test')
  }
})

OK

你可能感兴趣的:(深入理解nextTick())