深刻理解JavaScript运行机制

JavaScript单线程机制

JavaScript的一个语言特性(也是这门语言的核心)就是单线程。什么是单线程呢?简单地说就是同一时间只能作一件事,当有多个任务时,只能按照一个顺序一个完成了再执行下一个node

为何JS是单线程的呢?promise

  • JS最初被设计用在浏览器中,做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM
  • 若是浏览器中的JS是多线程的,会带来很复杂的同步问题 - 好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?
  • 因此为了不复杂性,JavaScript从诞生起就是单线程

为了提升CPU的利用率,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此这个标准并无改变JavaScript单线程的本质浏览器

任务队列

同步和异步 同步和异步关注的是消息通知机制bash

  • 同步:发出调用后,没有获得结果以前,该调用不返回,一旦调用返回,就获得返回值了。 简而言之就是调用者主动等待这个调用的结果多线程

  • 异步:调用者在发出调用后这个调用就直接返回了,因此没有返回结果。换句话说当一个异步过程调用发出后,调用者不会马上获得结果,而是调用发出后,被调用者经过状态、通知或回调函数处理这个调用。异步

阻塞和非阻塞async

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态函数

  • 阻塞调用是指调用结果返回以前,当前线程会被挂起。调用线程只有在获得结果以后才会返回
  • 非阻塞调用指在不能马上获得结果以前,该调用不会阻塞当前线程

单线程意味着同一时间只能进行一件事情,前面的事情结束才能执行后面的事件.当碰到须要时间的IO事件的时候问题就来了,必须等到这些结束后才往下进行,但这时CPU是闲着的.这样浪费了不少计算机的性能.oop

JavaScript语言的设计者意识到,这时主线程彻底能够无论IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回告终果,再回过头,把挂起的任务继续执行下去.性能

因而,全部任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。 (1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack) (2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件 (3)一旦"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行 (4)主线程不断重复上面的第三步

Event Loop

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

Alt Text

上图中,主线程运行的时候,产生堆(heap)和栈(stack),堆中可存放对象, 栈中可存放变量,函数,函数指针,代码语句等

栈中的代码调用各类外部API,它们在"任务队列"中加入各类事件(click,load,done) WebAPIs都是单独线程,跟组件中的不同,不会阻塞主线程执行,好比获取后台数据,若同步就阻塞了,好比HTTP请求又开辟了一个线程

当执行栈中的任务完成后,主线程会去读取事件队列(先进先出),执行相应的回调函数

举个例子,查看如下代码

function read(){
	console.log(1);
	setTimeout(function (){
	console.log(2);
	setTimeout(function (){
	console.log(4)
	});
	});
	setTimeout(function (){
	console.log(5)
	})
	console.log(3);
}
read();
复制代码

代码执行结果:1 3 2 5 4

先执行同步代码打印1,3,setTimeout异步代码放到事件队列中,先放的先执行,后放的后执行

定时器

"任务队列"能够放置定时事件,即指定某些代码在多少时间以后执行

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制彻底同样,区别在于前者指定的代码是一次性执行,后者则为反复执行,主要以setTimeout举例说明

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数

setTimeout(function () {
    console.log(3)
}, 2000);
setTimeout(function () {
    console.log(1);
    setTimeout(function () {
        console.log(2);
    }, 1000);
}, 1000);
复制代码

执行结果是:1 3 2

setTimeout()将事件放到等待任务队里中,当主任务队列的任务执行完后,再执行等待任务队列,等待任务队里中先返回的先执行

setTimeout()有时候明明写的延时3秒,实际却5,6秒才执行函数,这是怎么回事呢?

  • setTimeout()只是将事件插入了“任务队列”,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等好久,因此并无办法保证回调函数必定会在setTimeout()指定的时间执行

Promise与process.nextTick(callback)

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

  • macro-task(宏任务):包括总体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick - process.nextTick:在事件循环的下一次循环中调用 callback 回调函数。效果是将一个函数推迟到代码书写的下一个同步方法执行完毕时或异步方法的事件回调函数开始执行时;与setTimeout(fn, 0) 函数的功能相似,但它的效率高多了

不一样类型的任务会进入对应的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里面执行
  • 第一轮事件循环结束了,咱们开始第二轮循环,固然要从宏任务Event Queue开始。咱们发现了宏任务Event Queue中 setTimeout 对应的回调函数,当即执行
  • 结束

咱们再看下一段代码说明:

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)

复制代码

以上代码执行结果:1 2 TIMEOUT FIRED

上面代码中,因为process.nextTick方法指定的回调函数,老是在当前"执行栈"的尾部触发,因此不只函数A比setTimeout指定的回调函数timeout先执行,并且函数B也比timeout先执行。这说明,若是有多个process.nextTick语句(无论它们是否嵌套),将所有在当前"执行栈"执行

咱们再看下一段代码说明:

function a() {
    setTimeout(function () {
        console.log('a2');
    }, 0);
    process.nextTick(function () {
        console.log('a1')
    });
}
function b() {
    process.nextTick(function () {
        console.log('b1');
    })
}
a();
b();
复制代码

一个函数执行会造成一个执行栈,任务队列里的回调函数每次只取一个,它执行的时候会造成一个执行栈,当你第一次运行这个脚本的时候,这个脚本的里全部的同步代码都会在一个执行栈里

  • a的执行和b的执行在一个执行栈里,它们共同在第一个宏任务中
  • a执行时候,会把a2放入宏任务队列,把a1放入微任务队列。
  • b执行的时候,把b1放入微任务队列
  • -------------------第一个宏任务执行完毕-------------------------
  • 宏任务执行完毕后会把微任务队列清空,也就是把a1 和b1都执行,输出a1和b1
  • -------------------第一个微任务队列清空--------------------------
  • 而后从宏任务队列中取出下一个宏任务,也就是a2执行.输出a2

为何一个宏任务要搭配处理一个微任务 由于这样最合理,微任务就是在有空时须要当即执行的任务,宏任务相比微任务能够滞后执行。他们虽然都属于异步任务,可是经过这种优先级的设置达到了控制异步回调执行顺序的目的。值得注意的是:同步代码执行完会先清空微任务,而后取出宏任务队列里的第一个事件对应的回调到执行栈执行,而后再清空一次微任务,如此循环...

经过以上三段代码,您是否对JS的执行顺序有所了解呢

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

console.log('main1');
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');
});
process.nextTick(function () {
    console.log('process.nextTick1');
});
console.log('main2');
复制代码

以上代码的执行结果是:main1=>promise=>main2=>process.nextTick1=>promise then=>setTimeout=>process.nextTick2

  • 系统启动执行脚本,这个脚本就是一个宏任务,执行代码块中全部的同步代码,输出main1
  • next1放入微任务,setTimeout+nextTick2(下一轮)放入宏任务队列
  • promise构造函数部分是同步的,马上执行输出promise,promise then放入微任务
  • 下面同步代码输出main2
  • 接下来执行微任务输出nextTick1,promise then
  • 接下来执行宏任务输出setTimeout,将nexttick2放入微任务队列
  • 接下来执行微任务nexttick2
  • nextTick是由node本身定义并实现的概念,它的回调调用入口在event loop过程当中MakeCallback函数的末尾,驱动调用清空js层的queue,最后再执行microtasks,适当处理下可能触发的promise,明显 process.nextTick1> promise.then
相关文章
相关标签/搜索