JavaScript单线程

JavaScript单线程

开始这篇文章之前有几个问题

1、为什么JavaScript是单线程的
2、从for循环的面试题来理解JavaScript单线程

JavaScript为什么是单线程。

我们知道JavaScript作为浏览器的脚本语言,主要用途是与用户互动,以及操作DOM。如果以多线程的方式操作这些DOM,则可能出现操作的冲突。假设有两个线程同时操作一个DOM元素,线程1要求浏览器删除DOM,而线程2却要求修改DOM样式,这时浏览器就无法决定采用哪个线程的操作。当然,我们可以为浏览器引入“锁”的机制来解决这些冲突,但这会大大提高复杂性,所以 JavaScript 从诞生开始就选择了单线程执行。

由于JavaScript 是单线程的,在某一时刻内只能执行特定的一个任务,并且会阻塞其它任务执行。那么对于类似I/O等耗时的任务,就没必要等待他们执行完后才继续后面的操作。在这些任务完成前,JavaScript完全可以往下执行其他操作,当这些耗时的任务完成后则以回调的方式执行相应处理。这些就是JavaScript与生俱来的特性:异步与回调。

从for循环的面试题来理解JavaScript单线程

开始之前我们先预热下一些基本概念:

  • 并行: 同一时刻内多任务同时进行,多线程实现;
  • 并发,同一时间段内,多任务同时进行着,但是某一时刻,只有某一任务执行,单线程可实现;
  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  • 异步任务,不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
  • 堆(heap):内存中某一未被阻止的区域,通常存储对象(引用类型);
  • 栈(stack):后进先出的顺序存储数据结构,通常存储函数参数和基本类型值变量(按值访问);
  • 队列(queue):先进先出顺序存储数据结构。
  • 任务队列:一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,”任务队列”上第一位的事件就自动进入主线程。
    • 除了放置异步任务的事件,”任务队列”还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做”定时器”(timer)功能,也就是定时执行的代码。如果有定时器,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。
  • 回调函数(callback): 那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
  • 事件循环: 主线程从”任务队列”中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为事件循环(Event Loop)。

宿主环境为JavaScript创建线程时,会创建堆(heap)和栈(stack):

  • 堆内存储JavaScript对象
  • 栈内存储执行上下文

同步任务:

  • 执行栈内,执行上下文的同步任务按序执行
  • 执行完即退栈

异步任务:

  • 异步任务执行时,该异步任务进入等待状态(不入栈)
  • 与此同时,通知线程“当触发该异步事件的时候(或该异步操作响应返回时),需向任务队列插入一个事件
  • 当实际上异步事件触发或异步操作响应返回时,线程向任务队列插入相应的回调事件
  • 当执行栈清空后,线程从任务队列取出一个事件消息,其对应异步任务(函数)结束等待状态,进入执行栈,执行回调函数
    • 如果该事件消息未绑定回调,则执行完任务后退栈,这个消息会被丢弃

线程空闲(即执行栈清空)时继续拉取消息队列下一轮消息(next tick,事件循环流转一次称为一次tick)

其实上面这些可以用一个图来概括,也就是JavaScript事件循环:


for (var i = 0; i < 5; i++) {
  console.log(i);
}

结果: 0,1,2,3,4

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000 * i);
}

结果: 5,5,5,5,5

for (var i = 0; i < 5; i++) {
  (function(i) {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}

结果:0,1,2,3,4

for (var i = 0; i < 5; i++) {
  (function() {
    setTimeout(function() {
      console.log(i);
    }, i * 1000);
  })(i);
}

结果:5,5,5,5,5

for (var i = 0; i < 5; i++) {
  setTimeout((function(i) {
    console.log(i);
  })(i), i * 1000);
}

结果:0,1,2,3,4

setTimeout(function() {
  console.log(1)
}, 0);
new Promise(function executor(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

promise是微任务,当主线程执行完毕,微任务会排在宏任务前面先去执行

微任务:promise、process.nextTick()

最后看一个复杂的题目

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')
    })
})

结果应该很简单啦

1,7,6,8,2,4,3,5,9,11,10,12

setTimeout(() => {
    console.log(5)
}, 5)

setTimeout(() => {
    console.log(4)
}, 4)

setTimeout(() => {
    console.log(3)
}, 3)

setTimeout(() => {
    console.log(2)
}, 2)

setTimeout(() => {
    console.log(1)
}, 1)

setTimeout(() => {
    console.log(0)
}, 0)

结果是?
0,1,2,3,4,5

3,2,1,0,4,5

1,0,2,3,4,5

In fact, 4ms is specified by the HTML5 spec and is consistent across browsers released in 2010 and onward. Prior to (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.

底层代码:

double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond);

规范有些地方也不是全对啊~

猜你喜欢

转载自blog.csdn.net/lihangxiaoji/article/details/80697419