javascript执行机制,机制详解与

JavaScript Event Loop 机制详解与 Vue.js 中实践应用

2017/09/09 · CSS · Event Loop, Vue

原文出处: 王下邀月熊   

JavaScript Event Loop 机制详解与 Vue.js 中实践应用归纳于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章。本文依次介绍了函数调用栈、MacroTask 与 MicroTask 执行顺序、浅析 Vue.js 中 nextTick 实现等内容;本文中引用的参考资料统一声明在 JavaScript 学习与实践资料索引。

一.js是一门单线程的语言,js是按照语句出现的顺序执行的

文的目的就是要保证你彻底弄懂javascript的执行机制,如果读完本文还不懂,可以揍我。

为什么JavaScript是单线程?

1. 事件循环机制详解与实践应用

JavaScript 是典型的单线程单并发语言,即表示在同一时间片内其只能执行单个任务或者部分代码片。换言之,我们可以认为某个同域浏览器上下中 JavaScript 主线程拥有一个函数调用栈以及一个任务队列(参考 whatwg 规范);主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。当函数调用栈为空时,运行时即会根据事件循环(Event Loop)机制来从任务队列中提取出待执行的回调并执行,执行的过程同样会进行函数帧的入栈出栈操作。每个线程有自己的事件循环,所以每个 Web Worker有自己的,所以它才可以独立执行。然而,所有同属一个 origin 的窗体都共享一个事件循环,所以它们可以同步交流。

Event Loop(事件循环)并不是 JavaScript 中独有的,其广泛应用于各个领域的异步编程实现中;所谓的 Event Loop 即是一系列回调函数的集合,在执行某个异步函数时,会将其回调压入队列中,JavaScript 引擎会在异步代码执行完毕后开始处理其关联的回调。

星彩彩票app下载 1

在 Web 开发中,我们常常会需要处理网络请求等相对较慢的操作,如果将这些操作全部以同步阻塞方式运行无疑会大大降低用户界面的体验。另一方面,我们点击某些按钮之后的响应事件可能会导致界面重渲染,如果因为响应事件的执行而阻塞了界面的渲染,同样会影响整体性能。实际开发中我们会采用异步回调来处理这些操作,这种调用者与响应之间的解耦保证了 JavaScript 能够在等待异步操作完成之前仍然能够执行其他的代码。Event Loop 正是负责执行队列中的回调并且将其压入到函数调用栈中,其基本的代码逻辑如下所示:

JavaScript

while (queue.waitForMessage()) { queue.processNextMessage(); }

1
2
3
while (queue.waitForMessage()) {
  queue.processNextMessage();
}

完整的浏览器中 JavaScript 事件循环机制图解如下:星彩彩票app下载 2

在 Web 浏览器中,任何时刻都有可能会有事件被触发,而仅有那些设置了回调的事件会将其相关的任务压入到任务队列中。回调函数被调用时即会在函数调用栈中创建初始帧,而直到整个函数调用栈清空之前任何产生的任务都会被压入到任务队列中延后执行;顺序的同步函数调用则会创建新的栈帧。总结而言,浏览器中的事件循环机制阐述如下:

  • 浏览器内核会在其它线程中执行异步操作,当操作完成后,将操作结果以及事先定义的回调函数放入 JavaScript 主线程的任务队列中。
  • JavaScript 主线程会在执行栈清空后,读取任务队列,读取到任务队列中的函数后,将该函数入栈,一直运行直到执行栈清空,再次去读取任务队列,不断循环。
  • 当主线程阻塞时,任务队列仍然是能够被推入任务的。这也就是为什么当页面的 JavaScript 进程阻塞时,我们触发的点击等事件,会在进程恢复后依次执行。

二.Javascript事件循环

不论你是javascript新手还是老鸟,不论是面试求职,还是日常开发工作,我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。因为javascript是一门单线程语言,所以我们可以得出结论:

Javascript引擎是单线程机制,首先我们要了解Javascript语言为什么是单线程。

2. 函数调用栈与任务队列

在变量作用域与提升一节中我们介绍过所谓执行上下文(Execution Context)的概念,在 JavaScript 代码执行过程中,我们可能会拥有一个全局上下文,多个函数上下文或者块上下文;每个函数调用都会创造新的上下文与局部作用域。而这些执行上下文堆叠就形成了所谓的执行上下文栈(Execution Context Stack),便如上文介绍的 JavaScript 是单线程事件循环机制,同时刻仅会执行单个事件,而其他事件都在所谓的执行栈中排队等待:星彩彩票app下载 3

而从 JavaScript 内存模型的角度,我们可以将内存划分为调用栈(Call Stack)、堆(Heap)以及队列(Queue)等几个部分:星彩彩票app下载 4

其中的调用栈会记录所有的函数调用信息,当我们调用某个函数时,会将其参数与局部变量等压入栈中;在执行完毕后,会弹出栈首的元素。而堆则存放了大量的非结构化数据,譬如程序分配的变量与对象。队列则包含了一系列待处理的信息与相关联的回调函数,每个 JavaScript 运行时都必须包含一个任务队列。当调用栈为空时,运行时会从队列中取出某个消息并且执行其关联的函数(也就是创建栈帧的过程);运行时会递归调用函数并创建调用栈,直到函数调用栈全部清空再从任务队列中取出消息。换言之,譬如按钮点击或者 HTTP 请求响应都会作为消息存放在任务队列中;需要注意的是,仅当这些事件的回调函数存在时才会被放入任务队列,否则会被直接忽略。

譬如对于如下的代码块:

JavaScript

function fire() { const result = sumSqrt(3, 4) console.log(result); } function sumSqrt(x, y) { const s1 = square(x) const s2 = square(y) const sum = s1 s2; return Math.sqrt(sum) } function square(x) { return x * x; } fire()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fire() {
    const result = sumSqrt(3, 4)
    console.log(result);
}
function sumSqrt(x, y) {
    const s1 = square(x)
    const s2 = square(y)
    const sum = s1 s2;
    return Math.sqrt(sum)
}
function square(x) {
    return x * x;
}
 
fire()

其对应的函数调用图(整理自这里)为:星彩彩票app下载 5

这里还值得一提的是,Promise.then 是异步执行的,而创建 Promise 实例 (executor) 是同步执行的,譬如下述代码:

JavaScript

(function test() { setTimeout(function() {console.log(4)}, 0); new Promise(function executor(resolve) { console.log(1); for( var i=0 ; i<10000 ; i ) { i == 9999 && resolve(); } console.log(2); }).then(function() { console.log(5); }); console.log(3); })() // 输出结果为: // 1 // 2 // 3 // 5 // 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()
// 输出结果为:
// 1
// 2
// 3
// 5
// 4

我们可以参考 Promise 规范中有关于 promise.then 的部分:

JavaScript

promise.then(onFulfilled, onRejected) 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1]. Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

1
2
3
4
5
promise.then(onFulfilled, onRejected)
 
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
 
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

规范要求,onFulfilled 必须在执行上下文栈(Execution Context Stack) 只包含 平台代码(platform code) 后才能执行。平台代码指引擎,环境,Promise 实现代码等。实践上来说,这个要求保证了 onFulfilled 的异步执行(以全新的栈),在 then 被调用的这个事件循环之后。

因为js是单线程,所有js任务要一个一个顺序执行,任务分为:

  • javascript是按照语句出现的顺序执行的

JavaScript的主要用途主要是用户互动,和操作DOM。如果JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时这两个节点会有很大冲突,为了避免这个冲突,所以决定了它只能是单线程,否则会带来很复杂的同步问题。此外HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程(UI线程, 异步HTTP请求线程, 定时触发器线程...),但是子线程完全受主线程控制,这个新标准并没有改变JavaScript单线程的本质。

3. MacroTask(Task) 与 MicroTask(Job)

在面试中我们常常会碰到如下的代码题,其主要就是考校 JavaScript 不同任务的执行先后顺序:

JavaScript

// 测试代码 console.log('main1'); // 该函数仅在 Node.js 环境下可以使用 process.nextTick(function() { console.log('process.nextTick1'); }); setTimeout(function() { console.log('setTimeout'); process.nextTick(function() { console.log('process.nextTick2'); }); }, 0); new Promise(function(resolve, reject) { console.log('promise'); resolve(); }).then(function() { console.log('promise then'); }); console.log('main2'); // 执行结果 main1 promise main2 process.nextTick1 promise then setTimeout process.nextTick2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 测试代码
console.log('main1');
 
// 该函数仅在 Node.js 环境下可以使用
process.nextTick(function() {
    console.log('process.nextTick1');
});
 
setTimeout(function() {
    console.log('setTimeout');
    process.nextTick(function() {
        console.log('process.nextTick2');
    });
}, 0);
 
new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('promise then');
});
 
console.log('main2');
 
// 执行结果
main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

我们在前文中已经介绍过 JavaScript 的主线程在遇到异步调用时,这些异步调用会立刻返回某个值,从而让主线程不会在此处阻塞。而真正的异步操作会由浏览器执行,主线程则会在清空当前调用栈后,按照先入先出的顺序读取任务队列里面的任务。而 JavaScript 中的任务又分为 MacroTask 与 MicroTask 两种,在 ES2015 中 MacroTask 即指 Task,而 MicroTask 则是指代 Job。典型的 MacroTask 包含了 setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering 等,MicroTask 包含了 process.nextTick, Promises, Object.observe, MutationObserver 等。 二者的关系可以图示如下:星彩彩票app下载 6

参考 whatwg 规范 中的描述:一个事件循环(Event Loop)会有一个或多个任务队列(Task Queue,又称 Task Source),这里的 Task Queue 就是 MacroTask Queue,而 Event Loop 仅有一个 MicroTask Queue。每个 Task Queue 都保证自己按照回调入队的顺序依次执行,所以浏览器可以从内部到JS/DOM,保证动作按序发生。而在 Task 的执行之间则会清空已有的 MicroTask 队列,在 MacroTask 或者 MicroTask 中产生的 MicroTask 同样会被压入到 MicroTask 队列中并执行。参考如下代码:

JavaScript

function foo() { console.log("Start of queue"); bar(); setTimeout(function() { console.log("Middle of queue"); }, 0); Promise.resolve().then(function() { console.log("Promise resolved"); Promise.resolve().then(function() { console.log("Promise resolved again"); }); }); console.log("End of queue"); } function bar() { setTimeout(function() { console.log("Start of next queue"); }, 0); setTimeout(function() { console.log("End of next queue"); }, 0); } foo(); // 输出 Start of queue End of queue Promise resolved Promise resolved again Start of next queue End of next queue Middle of queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function foo() {
  console.log("Start of queue");
  bar();
  setTimeout(function() {
    console.log("Middle of queue");
  }, 0);
  Promise.resolve().then(function() {
    console.log("Promise resolved");
    Promise.resolve().then(function() {
      console.log("Promise resolved again");
    });
  });
  console.log("End of queue");
}
 
function bar() {
  setTimeout(function() {
    console.log("Start of next queue");
  }, 0);
  setTimeout(function() {
    console.log("End of next queue");
  }, 0);
}
 
foo();
 
// 输出
Start of queue
End of queue
Promise resolved
Promise resolved again
Start of next queue
End of next queue
Middle of queue

上述代码中首个 TaskQueue 即为 foo(),foo() 又调用了 bar() 构建了新的 TaskQueue,bar() 调用之后 foo() 又产生了 MicroTask 并被压入了唯一的 MicroTask 队列。我们最后再总计下 JavaScript MacroTask 与 MicroTask 的执行顺序,当执行栈(call stack)为空的时候,开始依次执行:

《这一段在我笔记里也放了好久,无法确定是否拷贝的。。。如果有哪位发现请及时告知。。。(*ฅ́˘ฅ̀*)♡》

  1. 星彩彩票app下载,把最早的任务(task A)放入任务队列
  2. 如果 task A 为null (那任务队列就是空),直接跳到第6步
  3. 将 currently running task 设置为 task A
  4. 执行 task A (也就是执行回调函数)
  5. 将 currently running task 设置为 null 并移出 task A
  6. 执行 microtask 队列
  • a: 在 microtask 中选出最早的任务 task X
  • b: 如果 task X 为null (那 microtask 队列就是空),直接跳到 g
  • c: 将 currently running task 设置为 task X
  • d: 执行 task X
  • e: 将 currently running task 设置为 null 并移出 task X
  • f: 在 microtask 中选出最早的任务 , 跳到 b
  • g: 结束 microtask 队列
  1. 跳到第一步

  2. 浅析 Vue.js 中 nextTick 的实现


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

《因为本人失误,原来此处内容拷贝了 https://www.zhihu.com/question/55364497 这个回答,造成了侵权,深表歉意,已经删除,后续我会在 github 链接上重写本段》

而当我们希望在数据更新之后执行某些 DOM 操作,就需要使用 nextTick 函数来添加回调:

JavaScript

// HTML <div id="example">{{message}}</div> // JS var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改数据 vm.$el.textContent === 'new message' // false Vue.nextTick(function () { vm.$el.textContent === 'new message' // true })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// HTML
<div id="example">{{message}}</div>
 
// JS
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue ,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

JavaScript

Vue.component('example', { template: '<span>{{ message }}</span>', data: function () { return { message: '没有更新' } }, methods: { updateMessage: function () { this.message = '更新完成' console.log(this.$el.textContent) // => '没有更新' this.$nextTick(function () { console.log(this.$el.textContent) // => '更新完成' }) } } })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '没有更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '更新完成'
      console.log(this.$el.textContent) // => '没有更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '更新完成'
      })
    }
  }
})

src/core/util/env

JavaScript

/** * 使用 MicroTask 来异步执行批次任务 */ export const nextTick = (function() { // 需要执行的回调列表 const callbacks = []; // 是否处于挂起状态 let pending = false; // 时间函数句柄 let timerFunc; // 执行并且清空所有的回调列表 function nextTickHandler() { pending = false; const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i ) { copies[i](); } } // nextTick 的回调会被加入到 MicroTask 队列中,这里我们主要通过原生的 Promise 与 MutationObserver 实现 /* istanbul ignore if */ if (typeof Promise !== 'undefined' && isNative(Promise)) { let p = Promise.resolve(); let logError = err => { console.error(err); }; timerFunc = () => { p.then(nextTickHandler).catch(logError); // 在部分 iOS 系统下的 UIWebViews 中,Promise.then 可能并不会被清空,因此我们需要添加额外操作以触发 if (isIOS) setTimeout(noop); }; } else if ( typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]') ) { // 当 Promise 不可用时候使用 MutationObserver // e.g. PhantomJS IE11, iOS7, Android 4.4 let counter = 1; let observer = new MutationObserver(nextTickHandler); let textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = () => { counter = (counter 1) % 2; textNode.data = String(counter); }; } else { // 如果都不存在,则回退使用 setTimeout /* istanbul ignore next */ timerFunc = () => { setTimeout(nextTickHandler, 0); }; } return function queueNextTick(cb?: Function, ctx?: Object) { let _resolve; callbacks.push(() => { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } // 如果没有传入回调,则表示以异步方式调用 if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve; }); } }; })();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* 使用 MicroTask 来异步执行批次任务
*/
export const nextTick = (function() {
  // 需要执行的回调列表
  const callbacks = [];
 
  // 是否处于挂起状态
  let pending = false;
 
  // 时间函数句柄
  let timerFunc;
 
  // 执行并且清空所有的回调列表
  function nextTickHandler() {
    pending = false;
    const copies = callbacks.slice(0);
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i ) {
      copies[i]();
    }
  }
 
  // nextTick 的回调会被加入到 MicroTask 队列中,这里我们主要通过原生的 Promise 与 MutationObserver 实现
  /* istanbul ignore if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    let p = Promise.resolve();
    let logError = err => {
      console.error(err);
    };
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError);
 
      // 在部分 iOS 系统下的 UIWebViews 中,Promise.then 可能并不会被清空,因此我们需要添加额外操作以触发
      if (isIOS) setTimeout(noop);
    };
  } else if (
    typeof MutationObserver !== 'undefined' &&
    (isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]')
  ) {
    // 当 Promise 不可用时候使用 MutationObserver
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    let counter = 1;
    let observer = new MutationObserver(nextTickHandler);
    let textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = () => {
      counter = (counter 1) % 2;
      textNode.data = String(counter);
    };
  } else {
    // 如果都不存在,则回退使用 setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0);
    };
  }
 
  return function queueNextTick(cb?: Function, ctx?: Object) {
    let _resolve;
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
 
    // 如果没有传入回调,则表示以异步方式调用
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve;
      });
    }
  };
})();


             同步任务

看到这里读者要打人了:我难道不知道js是一行一行执行的?还用你说?稍安勿躁,正因为js是一行一行执行的,所以我们以为js都是这样的:

任务队列

5. 延伸阅读

  • 深入浅出 Node.js 全栈架构 – Node.js 事件循环机制详解与实践

    1 赞 3 收藏 评论

星彩彩票app下载 7


let a = '1';
console.log(a);
let b = '2';
console.log(b);

单线程一个一个完成任务,前一个任务完成了,才会执行下一个任务,就是排队一样,不能插队,只能前面的人完成才能轮到后一个。那么问题来了,加入一个人在那办理很多任务,一时半会办不完,难道就一直卡在那里吗,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

              异步任务***

然而实际上js是这样的:

所有同步任务都在主线程上执行,形成一个执行栈


流程图:

setTimeout(function() {

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


任务进入执行栈——>同步任务还是异步任务?

  console.log('定时器开始啦')
});
new Promise(function(resolve) {

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


同步任务
——>主线程——>任务全部执行完毕——>读取任务队列中的结果,进入主线程执行**

  console.log('马上执行for循环啦');
  for (var i = 0; i < 10000; i ) {
    i == 99 && resolve();
  }
}).then(function() {

主线程不断重复上面的第三步。


    console.log('执行then函数啦')
});

Event Loop(事件循环)

异步任务——>event table——>注册回调函数——>event queue——>**读取任务队列中的结果,进入主线程执行**

console.log('代码执行结束');

主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)


依照 js是按照语句出现的顺序执行 这个理念,我自信的写下输出结果:

星彩彩票app下载 8

上面的过程不断重复——就是常说的Event  Loop(事件循环)**

//"定时器开始啦"//"马上执行for循环啦"//"执行then函数啦"//"代码执行结束"

JS引擎的执行机制


去chrome上验证下,结果完全不对,瞬间懵了,说好的一行一行执行的呢?

上图中,主线程运行的时候,产生堆(heap)和栈(stack),。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针,栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

let data = [];

$.ajax({

url:www.javascript.com,

data:data,

success:() => { console.log('发送成功!'); } })

console.log('代码执行结束')

我们真的要彻底弄明白javascript的执行机制了。

function read() {

console.log(1);

setTimeout(function () {

console.log(2);

})

console.log(3);

}

read()

//输出的是132*/

分析:1.ajax进入Event Table,注册回调函数 success()

1.关于javascript

setTimeout()的就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

2.执行同步任务 console.log('代码执行结束')

javascript是一门 单线程 语言,在最新的HTML5中提出了Web-Worker,但javascript是单线程这一核心仍未改变。所以一切javascript版的"多线程"都是用单线程模拟出来的,一切javascript多线程都是纸老虎!

除了广义的同步任务和异步任务,我们对任务有更精细的定义:

3.ajax事件完成,回调函数 进入event queue

2.javascript事件循环

Macrotask  (宏任务):

etImmediate:把回调函数放在事件队列的尾部

setTimeout:定时器

setInterval:定时器

Microtask 微任务):

process.nextTick:把回调函数放在当前执行栈的底部

Promise:

4.主线程 从event  queue中读取 回调函数success 并执行

既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。以下测试可以得到结论 


  • 同步任务

  • 异步任务

console.log('main1');

process.nextTick(function() {

    console.log('process.nextTick1');

});

setTimeout(function() {

    console.log('setTimeout');

    process.nextTick(function() {

        console.log('process.nextTick2');

    });

}, 0);

new Promise(function(resolve, reject) {

    console.log('promise');

    resolve();

}).then(function() {

    console.log('promise then');

});

console.log('main2');

输出结果是:  main1 、 promise 、 main2 、 process.nextTick1 、 promise then 、

setTimeout  、 process.nextTick2

setTimeout

setTimeout(() => { task() },3000)

sleep(10000000)

分析:1.task()进入event  table并注册,计时开始

2.执行同步任务 sleep()

3.3秒到了,但是同步任务未完成,所以event queue中仍需等待

4.执行完,task()从 event queue进入主线程执行,延迟时间大于3秒

setTimeout(fn,0)

指某个任务在主线程最早可得的空闲时间执行,即主线程执行栈中的同步任务完成后,立即执行fn,0毫秒是达不到的,最低是4毫秒


当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。关于这部分有严格的文字定义,但本文的目的是用最小的学习成本彻底弄懂执行机制,所以我们用导图来说明:

代码分析

setInterval

会每隔指定的时间将注册的函数置入 event  queue


setInterval(fn, ms):  不是每过ms 会执行一次fn,而是,没过ms,会有fn进入event  queue,一旦fn执行时间超过了延迟时间ms,那就看不出来有时间间隔了


星彩彩票app下载 9

首先执行main1是毫无疑问的,接下来看到process.nextTick这个函数,把他排到执行栈的底部(微任务) (未执行),在往下是一个setTimeout函数,应该放到宏任务队里排列(未执行),在看到的,promise函数,执行console.log('promise');then里面要判断排到nextTick的后面(未执行),最后看到输出的main2;微任务执行完,宏任务在实行

Promise 与 process.nextTick(callback)

process.nextTick(callback)类似 node.js版的“setTimeout”,在事件循环的下一次循环中调用callback回调函数

任务更精细的定义:

宏任务: 包括整体代码script, setTimeout, setInterval

微任务:promise, process.nextTick

事件循环循序:进入整体代码(宏任务)后,开始第一次循环,接着执行所有的微任务,然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务

setTimeout(function() { console.log('setTimeout'); })

new Promise(function(resolve) { console.log('promise');

}).then(function() { console.log('then'); })

console.log('console')

// promise 

//console

//undefined

//setTimeout

分析:1.这段代码作为宏任务,进入主线程

2.先遇到settimout,那么将其回调函数注册后分发到宏任务event queue

3.接下来promise,立即执行,并将then函数分发到微任务event queue

4.遇到 console.log并执行

5.整体代码script作为第一个宏任务执行结束,看看有哪些微任务?then函数在微任务中,并执行

6.第一轮事件循环结束后,开始第二轮循环,从宏任务event queue开始,发现 了宏任务 setimout对应的回调函数,立即执行

7.结束

console.log('1');

setTimeout(function() {

console.log('2');

process.nextTick(function() { console.log('3'); })

new Promise(function(resolve) {

console.log('4');

resolve();

}).then(function() { console.log('5') }) })

process.nextTick(function() { console.log('6'); })

new Promise(function(resolve) {

console.log('7');

resolve(); }).

then(function() { console.log('8') })

setTimeout(function() { console.log('9');

process.nextTick(function() { console.log('10'); })

new Promise(function(resolve) { console.log('11');

resolve();

}).then(function() { console.log('12') }) })

分析

第一轮事件循环流程分析如下:

整体script作为第一个宏任务进入主线程,遇到console.log,输出1。

遇到setTimeout,其回调函数被分发到宏任务Event Queue中。我们暂且记为setTimeout1。

遇到process.nextTick(),其回调函数被分发到微任务Event Queue中。我们记为process1。

遇到Promise,new Promise直接执行,输出7。then被分发到微任务Event Queue中。我们记为then1。

又遇到了setTimeout,其回调函数被分发到宏任务Event Queue中,我们记为setTimeout2。

宏任务Event Queue微任务Event Queue

setTimeout1process1

setTimeout2then1

上表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。

我们发现了process1和then1两个微任务。

执行process1,输出6。

执行then1,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从setTimeout1宏任务开始:

首先输出2。接下来遇到了process.nextTick(),同样将其分发到微任务Event Queue中,记为process2。new Promise立即执行输出4,then也分发到微任务Event Queue中,记为then2。

宏任务Event Queue微任务Event Queue

setTimeout2process2

then2

第二轮事件循环宏任务结束,我们发现有process2和then2两个微任务可以执行。

输出3。

输出5。

第二轮事件循环结束,第二轮输出2,4,3,5。

第三轮事件循环开始,此时只剩setTimeout2了,执行。

直接输出9。

将process.nextTick()分发到微任务Event Queue中。记为process3。

直接执行new Promise,输出11。

将then分发到微任务Event Queue中,记为then3。

宏任务Event Queue微任务Event Queue

process3

then3

第三轮事件循环宏任务执行结束,执行两个微任务process3和then3。

输出10。

输出12。

第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

(请注意,node环境下的事件监听依赖libuv与前端环境不完全相同,输出顺序可能会有误差)


导图要表达的内容用文字来表述的话:

总结:1. process.nextTick会比setTimeout先执行

2.setTimeout和setImmediate谁先执行,答案是不确定。

总结

1.js的异步

js是一门单线程的语言,无论是什么新框架新语法实现的所谓异步,都是用同步的方法去模拟的

2.事件循环event  Loop

事件循环是js实现异步的一种方法,也是js的执行机制

3.js的执行与运行

js在不同的环境下,不如node,浏览器等,执行方式是不同的

运行大多指js解析引擎,是统一的


作者:ssssyoki

链接:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。

  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。

  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。

  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

Node.js的Event Loop

我们不禁要问了,那怎么知道主线程执行栈为空啊?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。

说了这么多文字,不如直接一段代码更直白:

星彩彩票app下载 10

let data = [];
$.ajax({
  url: www.javascript.com,
  data: data,
  success: () = >{
    console.log('发送成功!');
  }
}) console.log('代码执行结束');

Node.js的Event Loop

上面是一段简易的 ajax 请求代码:

  1. V8引擎解析JavaScript脚本。

  2. 解析后的代码,调用Node API。

  3. libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

  4. V8引擎再将结果返回给用户。

ajax进入Event Table,注册回调函数 success 。

执行 console.log('代码执行结束') 。

ajax事件完成,回调函数 success 进入Event Queue。

主线程从Event Queue读取回调函数 success 并执行。

相信通过上面的文字和代码,你已经对js的执行顺序有了初步了解。接下来我们来研究进阶话题:setTimeout。

3.又爱又恨的setTimeout

大名鼎鼎的 setTimeout 无需再多言,大家对他的第一印象就是异步可以延时执行,我们经常这么实现延时3秒执行:

setTimeout(() => {

  console.log('延时3秒');

},3000)

渐渐的 setTimeout 用的地方多了,问题也出现了,有时候明明写的延时3秒,实际却5,6秒才执行函数,这又咋回事啊?

先看一个例子:

setTimeout(() = >{
  task();
  },
3000) console.log('执行console');

 

根据前面我们的结论, setTimeout 是异步的,应该先执行 console.log 这个同步任务,所以我们的结论是:

 

/执行console//task()

 

去验证一下,结果正确!

 

然后我们修改一下前面的代码:

setTimeout(() = >{
  task()
  },
3000) sleep(10000000)

 

乍一看其实差不多嘛,但我们把这段代码在chrome执行一下,却发现控制台执行 task() 需要的时间远远超过3秒,说好的延时三秒,为啥现在需要这么长时间啊?

 

这时候我们需要重新理解 setTimeout 的定义。我们先说上述代码是怎么执行的:

 

task() 进入Event Table并注册,计时开始。

 

执行 sleep 函数,很慢,非常慢,计时仍在继续。

 

3秒到了,计时事件 timeout 完成, task() 进入Event Queue,但是 sleep 也太慢了吧,还没执行完,只好等着。

 

sleep 终于执行完了, task() 终于从Event Queue进入了主线程执行。

 

上述的流程走完,我们知道setTimeout这个函数,是经过指定时间后,把要执行的任务(本例中为 task() )加入到Event Queue中,又因为是单线程任务要一个一个执行,如果前面的任务需要的时间太久,那么只能等着,导致真正的延迟时间远远大于3秒。

 

我们还经常遇到 setTimeout(fn,0) 这样的代码,0秒后执行又是什么意思呢?是不是可以立即执行呢?

 

答案是不会的, setTimeout(fn,0) 的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。举例说明:

//代码1console.log('先执行这里');setTimeout(() => { console.log('执行啦')},0);

//代码2console.log('先执行这里');setTimeout(() => { console.log('执行啦')},3000);

代码1的输出结果是:

//先执行这里//执行啦

代码2的输出结果是:

//先执行这里// ... 3s later// 执行啦

关于 setTimeout 要补充的是,即便主线程为空,0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。有兴趣的同学可以自行了解。

4.又恨又爱的setInterval

上面说完了 setTimeout ,当然不能错过它的孪生兄弟 setInterval 。他俩差不多,只不过后者是循环的执行。对于执行顺序来说, setInterval 会每隔指定的时间将注册的函数置入Event Queue,如果前面的任务耗时太久,那么同样需要等待。

唯一需要注意的一点是,对于 setInterval(fn,ms) 来说,我们已经知道不是每过 ms 秒会执行一次 fn ,而是每过 ms 秒,会有 fn 进入Event Queue。一旦 setInterval 的回调函数 fn 执行时间超过了延迟时间 ms ,那么就完全看不出来有时间间隔了 。这句话请读者仔细品味。

5.Promise与process.nextTick(callback)

传统的定时器部分我们已经研究过了,接下来我们看下 Promise 与 process.nextTick(callback) 的表现。

Promise 的定义和功能本文不再赘述,不了解的读者可以学习一下阮一峰老师的Promise。而 process.nextTick(callback) 类似node.js版的"setTimeout",在事件循环的下一次循环中调用 callback 回调函数。

我们进入正题,除了广义的同步任务和异步任务,我们对任务有更精细的定义:

macro-task(宏任务):包括整体代码script,setTimeout,setInterval

micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的Event Queue,比如 setTimeout 和 setInterval 会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。听起来有点绕,我们用文章最开始的一段代码说明:

 

 

setTimeout(function() {
  console.log('setTimeout');
}) new Promise(function(resolve) {
  console.log('promise');
}).then(function() {
  console.log('then');
}) console.log('console');

这段代码作为宏任务,进入主线程。

先遇到 setTimeout ,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)

接下来遇到了 Promise , new Promise 立即执行, then 函数分发到微任务Event Queue。

遇到 console.log() ,立即执行。

好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了 then 在微任务Event Queue里面,执行。

ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中 setTimeout 对应的回调函数,立即执行。

结束。

事件循环,宏任务,微任务的关系如图所示:

星彩彩票app下载 11

我们来分析一段较复杂的代码,看看你是否真的掌握了js的执行机制:

console.log('1');
setTimeout(function() {
   console.log('2');
   process.nextTick(function() {
    console.log('3');
   }) new Promise(function(resolve) {
    console.log('4');
    resolve();
   }).then(function() {
    console.log('5')
  })
}) process.nextTick(function() {
  console.log('6');
}) new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8')
}) setTimeout(function() {
  console.log('9');
  process.nextTick(function() {
  console.log('10');
}) new Promise(function(resolve) {
  console.log('11');
  resolve();
}).then(function() {
  console.log('12')
  })
})

 

第一轮事件循环流程分析如下:

 

整体script作为第一个宏任务进入主线程,遇到 console.log ,输出1。

 

遇到 setTimeout ,其回调函数被分发到宏任务Event Queue中。我们暂且记为 setTimeout1 。

 

遇到 process.nextTick() ,其回调函数被分发到微任务Event Queue中。我们记为 process1 。

 

遇到 Promise , new Promise 直接执行,输出7。 then 被分发到微任务Event Queue中。我们记为 then1 。

 

又遇到了 setTimeout ,其回调函数被分发到宏任务Event Queue中,我们记为 setTimeout2 。

 

星彩彩票app下载 12

表是第一轮事件循环宏任务结束时各Event Queue的情况,此时已经输出了1和7。

我们发现了 process1 和 then1 两个微任务。

执行 process1 ,输出6。

执行 then1 ,输出8。

好了,第一轮事件循环正式结束,这一轮的结果是输出1,7,6,8。那么第二轮时间循环从 setTimeout1 宏任务开始:

首先输出2。接下来遇到了 process.nextTick() ,同样将其分发到微任务Event Queue中,记为 process2 。 new Promise 立即执行输出4, then 也分发到微任务Event Queue中,记为 then2 。

星彩彩票app下载 13

第二轮事件循环宏任务结束,我们发现有 process2 和 then2 两个微任务可以执行。

输出3。

输出5。

第二轮事件循环结束,第二轮输出2,4,3,5。

第三轮事件循环开始,此时只剩setTimeout2了,执行。

直接输出9。

将 process.nextTick() 分发到微任务Event Queue中。记为 process3 。

直接执行 new Promise ,输出11。

将 then 分发到微任务Event Queue中,记为 then3 。

星彩彩票app下载 14

第三轮事件循环宏任务执行结束,执行两个微任务 process3 和 then3 。

输出10。

输出12。

第三轮事件循环结束,第三轮输出9,11,10,12。

整段代码,共进行了三次事件循环,完整的输出为1,7,6,8,2,4,3,5,9,11,10,12。

6.写在最后

(1)js的异步

我们从最开头就说javascript是一门单线程语言,不管是什么新框架新语法糖实现的所谓异步,其实都是用同步的方法去模拟的,牢牢把握住单线程这点非常重要。

(2)事件循环Event Loop

事件循环是js实现异步的一种方法,也是js的执行机制。

(3)javascript的执行和运行

执行和运行有很大的区别,javascript在不同的环境下,比如node,浏览器,Ringo等等,执行方式是不同的。而运行大多指javascript解析引擎,是统一的。

(4)setImmediate

微任务和宏任务还有很多种类,比如 setImmediate 等等,执行都是有共同点的,有兴趣的同学可以自行了解。

 

 

 

修改:

JS执行周期 浏览器(1 7 6 8      2 4 3 5      9 11 10 12)

micro-tasks queue macro-tasks queue

micro-tasks queue macro-tasks queue

micro-tasks queue NodeJS(1 7 6 8     2 4 9 11      3 10 5 12)

micro-tasks queue macro-tasks queue

micro-tasks queue macro-tasks queue

说明(优先级由上往下) main(主线程任务) 由上往下顺序执行

micro-task(微任务) Promise,process.nextTick

macro-task(宏任务) script,setTimeout,setInterval

 

console.log('1');
setTimeout(function() {
console.log('2');
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
}) new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
}) setTimeout(function() {
console.log('9');
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})输出:1 7 8 2 4 5 9 11 12

注意:process.nextTick 优先级大于 promise.then;浏览器中未支持 process.nextTick

 

星彩彩票app下载 15

 

本文由星彩网app下载发布于前端技术,转载请注明出处:javascript执行机制,机制详解与

TAG标签:
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。