JS Event Loop(VUE nextTick)

前言

js是一个单线程的语言(非阻塞),最初的目的是为了和浏览器交互,也就是事件的输入输出流,计算机根据人类的指令做出不同的反应结果,但是在JS执行的过程环境中 我们有 几个特殊的 “单词” setTimeoutsetIntervalPromise、另外在 Node中还有 process.nextTick。那么他们的执行顺序到底是怎么样的呢,浏览器不应该是按照他们书写的顺序从上往下执行吗?

那你又有疑问了,既然是单线程的,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码。

那不行啊,我们总不能一直等着啊,前端需要调用后端接口取数据,这个过程是需要响应时间的,那执行这个代码的时候浏览器也等着?答案是否定的。

其实还有其他很多类线程(应该叫做任务队列),比如进行ajax请求、监控用户事件、定时器、读写文件的线程(例如在NodeJS中)等等。

这些我们称之为异步事件,当异步事件发生时,将他们放入执行队列,等待当前代码执行完成。就不会长时间阻塞主线程。

等主线程的代码执行完毕,然后再读取任务队列,返回主线程继续处理。如此循环这就是事件循环机制。

JS 在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为

举个栗子

console.log('0');
setTimeout(() => {
  console.log('1');
}, 0);
console.log('2');
//输出 0 , 2 ,1

看起来是setTimeout 设置了时间为0 但是 setTimeout 是一个“异步”的操作,其实真是的情况是 setTimeout 的0 参数是无效的, JS会给他默认一个值为4毫秒。所以结果是 0 2 1 。


image.png

我们刚刚说到了“异步” 那么JS是怎么异步的呢 ,其实在JS执行的时候 不同的任务会分配到不同的队列中,每个任务在制定的时候 已经规定了他的基础要素 也就是他属于哪个队列的 ,任务源可以分为2类 微任务 microtask宏任务 macrotask,微任务又称之为JOBS,宏任务称为TASK。
我们在来看看下面这个例子

setTimeout(function() {
    console.log(1)
}, 0);
new Promise((resolve)=>{
    console.log(2);
    for(var i = 0; i < 10000; i++) {
        i == 9999 && resolve();
    }
    console.log(3);
}).then(function() {
    console.log(4);
});
console.log(5)
// 2 3 5 4  1

为什么是这个结果呢 。这就是 Jobs 和 Task的区别 我们下面仔细梳理下

微任务(Jobs)包括

process.nextTick

Promise

Object.observe(已废弃)

MutationObserver (html5 新特性)

宏任务(Task)包括

setTimeout/setInterval

setImmediate

I/O操作

UI rendering

浏览器中新标准中的事件循环机制与 node.js 类似,其中会介绍到几个nodejs有但是浏览器中没有的 API,大家只需要了解就好。

比如process.nextTicksetImmediate我们称他们为事件源, 事件源作为任务分发器,他们的回调函数才是被分发到任务队列,而本身会立即执行。

例如,setTimeout第一个参数被分发到任务队列,Promise 的 then 方法的回调函数被分发到任务队列(catch方法同理)。
不同源的事件被分发到不同的任务队列,其中 setTimeoutsetInterval 属于同源

整体代码开始第一次循环。全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的job。

当所有可执行的 job 执行完毕之后。循环再次从task开始,找到其中一个任务队列执行完毕,然后再执行所有的 job,这样一直循环下去。

无论是 task 还是 job,都是通过函数调用栈来完成。

这个时候我们是不是有一个大发现,除了首次整体代码的执行,其他的都有规律,先执行task任务队列,再执行所有的 job 并清空 job 队列。

再执行 task—job—task—job……,往复循环直到没有可执行代码。

那我们可不可以这么理解,第一次 script 代码的执行也算是一个task任务呢,如果这么理解那整个事件循环就很容易理解了。

UI rendering是在Task执行之后就运行的 那么我们只要把DOM操作放入Job中就可以提高渲染的性能了

下面我们说说 vue的 nextTick
还是举个栗子

        
  • {{li.name}}
new Vue({
    el: '#app',
    data: {
        list: []
    },
    mounted() {
        this.init()
    },
    methods: {
        init() {
            this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
            this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
                    
        },
    }
})

我们会发现 这样会报错


error.png

如下修改:

new Vue({
    el: '#app',
    data: {
        list: []
    },
    mounted() {
        this.init()
    },
    methods: {
        init() {
                this.list = [{name:"lxl",age:18},{name:"kobe",age:19}]
                this.$nextTick(()=>{
                    this.$refs.list.getElementsByTagName('li')[0].style.color = 'red'
                })
        },
    }
})
image.png

我在获取到数据后赋值给 data 对象的 list 属性,然后我想引用ul元素找到第一个li把它的颜色变为红色,但是事实上,这个要报错的。

我们知道,在执行这句话时,ul 下面并没有 li,也就是说刚刚进行的赋值操作,当前并没有引起视图层的更新。

因为 Vue 的数据驱动视图更新,是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

因此,在这样的情况下,vue 给我们提供了 nextTick 方法,vue 在更新完视图后就会执行我们的函数帮我们做事情。

nextTick 可以让我们在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM。

var callbacks = [];
var pending = false;

function flushCallbacks () {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
var microTimerFunc;
var macroTimerFunc;
var useMacroTask = false;

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = function () {
    setImmediate(flushCallbacks);
  };
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  var channel = new MessageChannel();
  var port = channel.port2;
  channel.port1.onmessage = flushCallbacks;
  macroTimerFunc = function () {
    port.postMessage(1);
  };
} else {
  /* istanbul ignore next */
  macroTimerFunc = function () {
    setTimeout(flushCallbacks, 0);
  };
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  var p = Promise.resolve();
  microTimerFunc = function () {
    p.then(flushCallbacks);
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) { setTimeout(noop); }
  };
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc;
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
function withMacroTask (fn) {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true;
    var res = fn.apply(null, arguments);
    useMacroTask = false;
    return res
  })
}
function nextTick (cb, ctx) {
  var _resolve;
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    pending = true;
    if (useMacroTask) {
      macroTimerFunc();
    } else {
      microTimerFunc();
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    })
  }
}

综合上面的代码我们可以知道
在 Vue 2.4 之前都是使用的 microtasks,但是 microtasks 的优先级过高,在某些情况下可能会出现比事件冒泡更快的情况,但如果都使用 macrotasks 又可能会出现渲染的性能问题。所以在新版本中,会默认使用 microtasks,但在特殊情况下会使用 macrotasks,比如 v-on。

对于实现 macrotasks ,会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout
setImmediate传送门
MessageChannel传送门
event-loops传送门

总结一下今天的知识

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步

你可能感兴趣的:(JS Event Loop(VUE nextTick))