美文网首页前端技术js
深入理解nextTick()

深入理解nextTick()

作者: HelenYin | 来源:发表于2017-12-04 18:12 被阅读150次

    这篇文章主要讲一下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()

        本文链接:https://www.haomeiwen.com/subject/gckxixtx.html