Event Loop 事件循环

JS从诞生之日起,就是一门单线程的非阻塞的脚本语言。这是由其最初的用途所决定的:与浏览器交互。基本上所有语言和UI相关的部分都是单线程的,比如安卓的UI线程绘制。如果是多线程的话,两个线程同时操作一个元素的属性,就会造成后续一系列问题。

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
js引擎执行异步代码而不用等待,是因有为有 消息队列和事件循环。

消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息。
事件循环:事件循环是指主线程重复从消息队列中取消息、执行的过程。

实际上,主线程只会做一件事情,就是从消息队列里面取消息、执行消息,再取消息、再执行。当消息队列为空时,就会等待直到消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。
事件循环用代码表示大概是这样的:

  1. while(true) {
  2. var message = queue.get();
  3. execute(message);
  4. }

那么,消息队列中放的消息具体是什么东西?消息的具体结构当然跟具体的实现有关,但是为了简单起见,我们可以认为:

消息就是注册异步任务时添加的回调函数。

再次以异步AJAX为例,假设存在如下的代码:

  1. $.ajax('http://segmentfault.com', function(resp) {
  2. console.log('我是响应:', resp);
  3. });
  4. // 其他代码
  5. ...
  6. ...
  7. ...

主线程在发起AJAX请求后,会继续执行其他代码。AJAX线程负责请求segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构造一条消息:

  1. // 消息队列中的消息就长这个样子
  2. var message = function () {
  3. callbackFn(response);
  4. }

其中的callbackFn就是前面代码中得到成功响应时的回调函数。
主线程在执行完当前循环中的所有代码后,就会到消息队列取出这条消息(也就是message函数),并执行它。到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,AJAX线程在收到HTTP响应后,也就没必要通知主线程,从而也没必要往消息队列放消息。
用图表示这个过程就是:
image.png
从上文中我们也可以得到这样一个明显的结论,就是:

异步过程的回调函数,一定不在当前这一轮事件循环中执行。

事件循环进阶:macrotask与microtask(宏观任务与微观任务)

一张图展示JavaScript中的事件循环:
image.png

一次事件循环:先运行macroTask队列中的一个,然后运行microTask队列中的所有任务。接着开始下一次循环(只是针对macroTask和microTask,一次完整的事件循环会比这个复杂的多)。
JS中分为两种任务类型:macrotask和microtask,在ECMAScript中,microtask称为jobs,macrotask可称为task
它们的定义?区别?简单点可以按如下理解:
macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
每一个task会从头到尾将这个任务执行完毕,不会执行其它
浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
(task->渲染->task->…)
microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务
也就是说,在当前task任务后,下一个task之前,在渲染之前
所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)
那么什么样的场景会形成macrotask和microtask呢?

macroTask: 主代码块, setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering(可以看到,事件队列中的每一个事件都是一个macrotask)

microTask: process.nextTick, Promise, Object.observe, MutationObserver (其实都是ES6内容)
补充:在node环境下,process.nextTick的优先级高于Promise,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。
另外,setImmediate则是规定:在下一次Event Loop(宏任务)时触发(所以它是属于优先级较高的宏任务),(Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面),所以setImmediate如果嵌套的话,是需要经过多个Loop才能完成的,而不会像process.nextTick一样没完没了。

实践:上代码
我们以setTimeout、process.nextTick、promise为例直观感受下两种任务队列的运行方式。

  1. console.log('main1');
  2. process.nextTick(function() {
  3. console.log('process.nextTick1');
  4. });
  5. setTimeout(function() {
  6. console.log('setTimeout');
  7. process.nextTick(function() {
  8. console.log('process.nextTick2');
  9. });
  10. }, 0);
  11. new Promise(function(resolve, reject) {
  12. console.log('promise');
  13. resolve();
  14. }).then(function() {
  15. console.log('promise then');
  16. });
  17. console.log('main2');

别着急看答案,先以上面的理论自己想想,运行结果会是啥?
最终结果是这样的:

  1. main1
  2. promise
  3. main2
  4. process.nextTick1
  5. promise then
  6. setTimeout
  7. process.nextTick2

process.nextTick 和 promise then在 setTimeout 前面输出,已经证明了macroTask和microTask的执行顺序。但是有一点必须要指出的是。上面的图容易给人一个错觉,就是主进程的代码执行之后,会先调用macroTask,再调用microTask,这样在第一个循环里一定是macroTask在前,microTask在后。
但是最终的实践证明:在第一个循环里,process.nextTick1和promise then这两个microTask是在setTimeout这个macroTask里之前输出的,这是为什么呢?
因为主进程的代码也属于macroTask(这一点我比较疑惑的是主进程都是一些同步代码,而macroTask和microTask包含的都是一些异步任务,为啥主进程的代码会被划分为macroTask,不过从实践来看确实是这样,而且也有理论支撑:【翻译】Promises/A+规范)。
主进程这个macroTask(也就是main1、promise和main2)执行完了,自然会去执行process.nextTick1和promise then这两个microTask。这是第一个循环。之后的setTimeout和process.nextTick2属于第二个循环
别看上面那段代码好像特别绕,把原理弄清楚了,都一样 ~
requestAnimationFrame、Object.observe(已废弃) 和 MutationObserver这三个任务的运行机制大家可以从上面看到,不同的只是具体用法不同。重点说下UI rendering。在HTML规范:event-loop-processing-model里叙述了一次事件循环的处理过程,在处理了macroTask和microTask之后,会进行一次Update the rendering,其中细节比较多,总的来说会进行一次UI的重新渲染。

事件循环机制进一步补充

这里就直接引用一张图片来协助理解:(参考自Philip Roberts的演讲《Help, I’m stuck in an event-loop》)
image.png

上图大致描述就是:

  • 主线程运行时会产生执行栈,栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)
  • 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
  • 如此循环
  • 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

    原型链

    当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
    几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

    基于原型链的继承

    继承属性

    JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

这里演示当尝试访问属性时会发生什么:

  1. // 让我们从一个自身拥有属性a和b的函数里创建一个对象o:
  2. let f = function () {
  3. this.a = 1;
  4. this.b = 2;
  5. }
  6. /* 这么写也一样
  7. function f() {
  8. this.a = 1;
  9. this.b = 2;
  10. }
  11. */
  12. let o = new f(); // {a: 1, b: 2}
  13. // 在f函数的原型上定义属性
  14. f.prototype.b = 3;
  15. f.prototype.c = 4;
  16. // 不要在 f 函数的原型上直接定义 f.prototype = {b:3,c:4};这样会直接打破原型链
  17. // o.[[Prototype]] 有属性 b 和 c
  18. // (其实就是 o.__proto__ 或者 o.constructor.prototype)
  19. // o.[[Prototype]].[[Prototype]] 是 Object.prototype.
  20. // 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null
  21. // 这就是原型链的末尾,即 null,
  22. // 根据定义,null 就是没有 [[Prototype]]。
  23. // 综上,整个原型链如下:
  24. // {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null
  25. console.log(o.a); // 1
  26. // a是o的自身属性吗?是的,该属性的值为 1
  27. console.log(o.b); // 2
  28. // b是o的自身属性吗?是的,该属性的值为 2
  29. // 原型上也有一个'b'属性,但是它不会被访问到。
  30. // 这种情况被称为"属性遮蔽 (property shadowing)"
  31. console.log(o.c); // 4
  32. // c是o的自身属性吗?不是,那看看它的原型上有没有
  33. // c是o.[[Prototype]]的属性吗?是的,该属性的值为 4
  34. console.log(o.d); // undefined
  35. // d 是 o 的自身属性吗?不是,那看看它的原型上有没有
  36. // d 是 o.[[Prototype]] 的属性吗?不是,那看看它的原型上有没有
  37. // o.[[Prototype]].[[Prototype]] 为 null,停止搜索
  38. // 找不到 d 属性,返回 undefined

继承方法

JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。
当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

  1. var o = {
  2. a: 2,
  3. m: function(){
  4. return this.a + 1;
  5. }
  6. };
  7. console.log(o.m()); // 3
  8. // 当调用 o.m 时,'this' 指向了 o.
  9. var p = Object.create(o);
  10. // p是一个继承自 o 的对象
  11. p.a = 4; // 创建 p 的自身属性 'a'
  12. console.log(p.m()); // 5
  13. // 调用 p.m 时,'this' 指向了 p
  14. // 又因为 p 继承了 o 的 m 函数
  15. // 所以,此时的 'this.a' 即 p.a,就是 p 的自身属性 'a'