浅谈Vuejs中nextTick()异步更新队列源码解析

vue官网关于此解释说明如下:

vue2.0里面的深入响应式原理的异步更新队列

官网说明如下:

只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际(已去重的)工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MutationObserver,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

例如,当你设置 vm.someData = ‘new value' ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。例如

源码解析

方法原型以及解析注释如下:

var nextTick = (function () {
    var callbacks = []; // 存储需要触发的回调函数
    var pending = false; // 是否正在等待的标识(false:允许触发在下次事件循环触发callbacks中的回调, true: 已经触发过,需要等到下次事件循环)
    var timerFunc; // 设置在下次事件循环触发callbacks的 触发函数

    //处理callbacks的函数
    function nextTickHandler () {
      pending = false;// 可以触发timeFunc
      var copies = callbacks.slice(0);//复制callback
      callbacks.length = 0;//清空callback
      for (var i = 0; i < copies.length; i++) {
        copies[i]();//触发callback回调函数
      }
    }

    //如果支持Promise,使用Promise实现
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      var p = Promise.resolve();
      var logError = function (err) { console.error(err); };
      timerFunc = function () {
        p.then(nextTickHandler).catch(logError);
        // ios的webview下,需要强制刷新队列,执行上面的回调函数
        if (isIOS) { setTimeout(noop); }
      };

      //如果Promise不支持,但是支持MutationObserver(h5新特性,异步,当dom变动是触发,注意是所有的dom都改变结束后触发)
    } else if (typeof MutationObserver !== 'undefined' && (
        isNative(MutationObserver) ||
        // PhantomJS and iOS 7.x
        MutationObserver.toString() === '[object MutationObserverConstructor]'
      )) {
      // use MutationObserver where native Promise is not available,
      // e.g. PhantomJS IE11, iOS7, Android 4.4
      var counter = 1;
      var observer = new MutationObserver(nextTickHandler);
      //创建一个textnode dom节点,并让MutationObserver 监视这个节点;而 timeFunc正是改变这个dom节点的触发函数
      var textNode = document.createTextNode(String(counter));
      observer.observe(textNode, {
        characterData: true
      });
      timerFunc = function () {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
      };
    } else {// 上面两种不支持的话,就使用setTimeout

      timerFunc = function () {
        setTimeout(nextTickHandler, 0);
      };
    }
    //nextTick接受的函数, 参数1:回调函数 参数2:回调函数的执行上下文
    return function queueNextTick (cb, ctx) {
      var _resolve;//用于接受触发 promise.then中回调的函数
      //向回调数据中pushcallback
      callbacks.push(function () {
        //如果有回调函数,执行回调函数
        if (cb) { cb.call(ctx); }
        if (_resolve) { _resolve(ctx); }//触发promise的then回调
      });
      if (!pending) {//是否执行刷新callback队列
        pending = true;
        timerFunc();
      }
      //如果没有传递回调函数,并且当前浏览器支持promise,使用promise实现
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(function (resolve) {
          _resolve = resolve;
        })
      }
    }
  })();

我在注释中解释了nextTick()函数的逻辑

上面处理回调的三个方式的使用优先级的原因:因为Promise和MutationObserver和触发的事件在同一个事件循环里面(只不过是运行在微观队列里面),但是setTimeout的回调函数是运行在下次时间循环里面。

优先使用Promise的原因是MutationObserver在ios9.3.3以上版本的UIWebview中运行一段时间后就停止了。
上面代码的注释已经完全说明了代码逻辑。简单理解:将callback 推到队列里面,如果还没有执行过在下次事件循环执行触发callback函数。

注意: 如果使用nextTick()不设置回调函数,而是使用Promise的方式设置回调函数,里面this并不是指向当前的Vue实例,而是指向window(严格模式是undefined);
但是通过上面的分析可知:执行上下文是通过Promise.then()里的回调函数的第一个参数传递的。

nextTick()被使用的地方

1、他是全局Vue的一个函数,因此我们可以通过vue直接调用。

2、Vue系统中,用于处理dom更新的操作

Vue中有一个watcher,用于观察数据的变化,然后更新dom。前面我们就知道Vue里面不是每一次数据改变都会触发更新dom,而是将这些操作都缓存在一个队列,在一个事件循环结束之后,刷新队列,统一执行dom更新操作。

function queueWatcher (watcher) {
    var id = watcher.id;
    if (has[id] == null) {
      has[id] = true;
      if (!flushing) {
        queue.push(watcher);
      } else {
        // if already flushing, splice the watcher based on its id
        // if already past its id, it will be run next immediately.
        var i = queue.length - 1;
        while (i >= 0 && queue[i].id > watcher.id) {
          i--;
        }
        queue.splice(Math.max(i, index) + 1, 0, watcher);
      }
      // queue the flush
      if (!waiting) {
        waiting = true;
        nextTick(flushSchedulerQueue);
      }
    }
  }

简单说明上面代码的逻辑,因为是watcher那里的代码,以后会分析到。这里nextTick()的作用,是在此次事件循环结尾的时候刷新watcher检查的dom更新操作。

3、局部Vue触发$nextTick(),在dom更新后执行相应逻辑。

Vue.prototype.$nextTick = function (fn) {
  return nextTick(fn, this)// 设置nextTick回调函数的上下文环境是当前Vue实例
};

上面是renderMinxin中的一段代码,也就是render模块初始化的代码。

总结

如果不了解它的代码,我们会产生理解误区。

1、nextTick()并不会重绘当前页面,并且它也不是在页面重绘才执行,而是在此次事件循环结束后一定会执行的。

2、此方法的触发并不是在页面更新完成才执行,第一条已经说了,但是为什么能在此方法中取到更新后的数据,那是因为dom元素的属性已经在watcher执行flush队列的时候改变了,因此是可以在此时获取的。

证明上述观点的实例:

h5有一个方法requestFrameAnimation(callback),此方法的回调是在页面重绘之前调用。通过实验,更新dom,nextTick()在此方法之前执行。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 详解从Vue.js源码看异步更新DOM策略及nextTick

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/answershuto/learnVue. 在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助. 可能会有理解存在偏差的地方,欢迎提issue指出,

  • 浅谈Vuejs中nextTick()异步更新队列源码解析

    vue官网关于此解释说明如下: vue2.0里面的深入响应式原理的异步更新队列 官网说明如下: 只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变.如果同一个 watcher 被多次触发,只会一次推入到队列中.这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要.然后,在下一个的事件循环"tick"中,Vue 刷新队列并执行实际(已去重的)工作.Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MutationOb

  • 浅谈Jquery中Ajax异步请求中的async参数的作用

    之前不知道这个参数的作用,上网找了前辈的博客,在此收录到自己的博客,希望能帮到更多的朋友: test.html <a href="javascript:void(0)" onmouseover="testAsync()"> asy.js function testAsync{ var temp; $.ajax({ async: false, type : "GET", url : 'tet.php', complete: functi

  • .Net Core中ObjectPool的使用与源码解析

    一.对象池 运用对象池化技术可以显著地提升性能,尤其是当对象的初始化过程代价较大或者频率较高.下面是ObjectPool源码中涉及的几个类.当你看过.Net Core源码很多时,你会发现,微软的开发很多都是这种模式,通过Policy构建Provider,通过Provider创建最终的类. 二.使用 这个组件的目的主要是将对象保存到对象池,用的时候直接去取,不需要重新创建,实现对象的重复利用.但是有个问题,假如对象池中开始没有对象或者取得数量大于对象池中的数量怎么办?在对象池中对象的数量不足时,此

  • 浅谈Android中使用异步线程更新UI视图的几种方法

    在Android中子线程是不能更新ui的. 所以我们要通过其他方式来动态改变ui视图, 1.runOnUiThread activity提供的一个轻量级更新ui的方法,在Fragment需要使用的时候要用getActivity.runOnUiThread开启线程 这种方法最简单,方便更新一些不需要判断的通知,比如在聊天项目中动态获取未读消息数量. runOnUiThread(new Runnable() { @Override public void run() { sendMessage("[

  • 浅谈java中异步多线程超时导致的服务异常

    在项目中为了提高大并发量时的性能稳定性,经常会使用到线程池来做多线程异步操作,多线程有2种,一种是实现runnable接口,这种没有返回值,一种是实现Callable接口,这种有返回值. 当其中一个线程超时的时候,理论上应该不 影响其他线程的执行结果,但是在项目中出现的问题表明一个线程阻塞,其他线程返回的接口都为空.其实是个很简单的问题,但是由于第一次碰到,还是想了一些时间的.很简单,就是因为阻塞的那个线 程没有释放,并发量一大,线程池数量就满了,所以其他线程都处于等待状态. 附上一段自己写的调

  • 浅谈Android中AsyncTask的工作原理

    概述 实际上,AsyncTask内部是封装了Thread和Handler.虽然AsyncTask很方便的执行后台任务,以及在主线程上更新UI,但是,AsyncTask并不合适进行特别耗时的后台操作,对于特别耗时的任务,个人还是建议使用线程池.好了,话不多说了,我们先看看AsyncTask的简单用法吧. AsyncTask使用方法 AsyncTask是一个抽象的泛型类.简单的介绍一下它的使用方式代码如下: package com.example.huangjialin.myapplication;

  • 浅谈js中的宏任务和微任务

    目录 1.关于JavaScript 2.JavaScript事件循环 3.宏任务和微任务 4.拓展宏任务微任务 下面一道关于宏任务和微任务的题: setTimeout(function(){ console.log('1') }); new Promise(function(resolve){ console.log('2'); resolve(); }).then(function(){ console.log('3') }); console.log('4') 试问一下上面代码的执行顺序是啥

  • 浅谈JavaScript中的对象及Promise对象的实现

    JavaScript 中的所有事物都是对象:字符串.数值.数组.函数.下面小编给大家收集整理些javascript中的对象及promise对象的实现.具体内容如下: 到处都是对象 window对象 常用的属性和方法介绍 location 包含页面的URL,如果改变这个属性,浏览器会访问新的URL status 包含将在浏览器状态去显示的一个串.一般在浏览器左下角 onload: 包含了需要在页面完全加载后调用的函数 document: 包含DOM alert方法: 显示一个提醒 prompt方法

  • 浅谈Java中的atomic包实现原理及应用

    1.同步问题的提出 假设我们使用一个双核处理器执行A和B两个线程,核1执行A线程,而核2执行B线程,这两个线程现在都要对名为obj的对象的成员变量i进行加1操作,假设i的初始值为0,理论上两个线程运行后i的值应该变成2,但实际上很有可能结果为1. 我们现在来分析原因,这里为了分析的简单,我们不考虑缓存的情况,实际上有缓存会使结果为1的可能性增大.A线程将内存中的变量i读取到核1算数运算单元中,然后进行加1操作,再将这个计算结果写回到内存中,因为上述操作不是原子操作,只要B线程在A线程将i增加1的

  • 浅谈Vue3中watchEffect的具体用法

    前言 watchEffect,它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数. 换句话说:watchEffect相当于将watch 的依赖源和回调函数合并,当任何你有用到的响应式依赖更新时,该回调函数便会重新执行.不同于 watch,watchEffect 的回调函数会被立即执行(即 { immediate: true }) 此文主要讲述怎样利用清除副作用使我们的代码更加优雅~ watchEffect的副作用 什么是副作用(side effect),简单的说副作用

随机推荐