梳理 | JavaScript 单线程

本文旨在进行学习过程中的知识梳理,如有问题还望多多指教。

1、JavaScript 执行为何单线程

最直观的解释:如果多线程的话,操作DOM的时候可能会出现这样的情况,一个线程读取DOM节点数据的同时,另一个线程把那个DOM节点删了。因此JS一个线程就足够。

梳理 | JavaScript 单线程_第1张图片
Event Loop.png

我们经常碰到还有这些线程:浏览器事件触发线程;定时触发器线程;异步HTTP请求线程等

任务队列:
  • 所有同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,还存在一个任务队列。在异步任务有了运行结果后,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  • 主线程不断重复上面的第三步。
function test(){
    console.log(1)
    setTimeout(() => {
        console.log(2);
        setTimeout(() => {
            console.log(3)
            setTimeout(() => {
                console.log(4)   
            });   
        });
    });
    console.log(5)
}
console.log(6)
test() // 输出6 1 5 2 3 4

上述代码中:同步任务放入执行栈console->(6/1/5) ,遇到异步任务settimeout 有运行结果后console->(2) 放入任务队列,后续依次放入console->(3/4),等执行栈中同步人物执行完,依次执行人物队列里面的

2、Nodejs
  • Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,让JavaScript+ 的执行效率与低端的C语言的相近的执行效率。。
  • Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。
  • Node.js 的包管理器 npm,是全球最大的开源库生态系统。
梳理 | JavaScript 单线程_第2张图片
Nodejs EventLoop.png
流程
  • 首先V8引擎解析JavaScript脚本
  • 解析后的代码,调用Node API
  • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎
  • V8引擎再将结果返回给用户

其中,NodeJS的工作原理其实就是事件循环。可以理解为每一条NodeJS的逻辑都是写在回调函数里面的,在回调函数有返回之后进行异步执行。NodeJS实现这些的基础是单线程

但是,NodeJS不是没有阻塞,而是阻塞不发生在后续回调的流程中,而会发生在NodeJS本身对逻辑的计算和处理。NodeJS的分发能力强大,可以循环事件进行异步回调。但是如果在循环事件时遇到复杂的逻辑运算,那么单薄的单线程怎么支撑得起上百万的逻辑+并发呢?NodeJS它的所有I/O、网络通信等比较耗时的操作,都可以交给worker threads执行再回调,所以很快。但CPU的正常操作,它就只能自己抗了。

NodeJS处理并发的能力强,但处理计算和逻辑的能力反而很弱,因此,如果我们把复杂的逻辑运算都搬到前端(客户端)完成,而NodeJS只需要提供异步I/O,这样就可以实现对高并发的高性能处理。比如:聊天服务器,电子商务网站

function read(){
    console.log(1);
    setTimeout(() => {
        console.log(2);
    });

    // nextTick是把这个回调函数放在当前栈的尾部
    process.nextTick(function(){
        console.log(3);
        process.nextTick(function(){
            console.log(4);
            process.nextTick(function(){
                console.log(5);
            })
        })
    })

    console.log(6)
}

read()//1 6 3 4 5 2 
3、同步与异步,阻塞与非阻塞

同步与异步取决于被调用者,它来决定是马上给你答案,还是回头再给,关注的是消息的通知方式

  • 同步:调用者主动等待这个调用的结果(就是等待被调用者返回结果)
  • 异步:当一个异步过程调用发出后,调用者不会立刻得到结果,而是调用发出后,被调用者通过状态、通知或回调函数处理这个调用(就是调用的结果不会立刻返回,后续返回)

阻塞与非阻塞取决于调用者,在等待的过程中,调研方是否可以干别的事情,关注的是程序等待调用结果时的状态

  • 阻塞:调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回(就是我一直等待调用返回的结果)
  • 非阻塞:不能立刻得到结果之前,该调用不会阻塞当前线程(就是我不等待调用返回的结果,比如读取文件太久,我会去执行其他任务)

举例来理解:A打电话向B表白

  • 同步阻塞:A给B打电话表白,A啥也不干,等B的3s回复,不去干别的事。等待过程中不干别的,那就是调用者阻塞了
  • 同步非阻塞:A给B打电话表白,B在思考3s后回复,B的电话没有挂掉,A此时可以向C打电话表白
  • 异步阻塞:A给B打电话表白后,B思考下,两天后通知你,这两天内A啥也不干,静静的等待
  • 异步非阻塞:A给B打电话表白后,B思考下,两天后通知你,这两天内A又向C打电话表白
4、并发&并行(区别在于:是否同时)

并发:在一个时间段的快速的切换不同的线程代码运行(比如:接了电话,接完后继续吃饭)
并行:当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行(比如:边接电话边吃饭)

延伸阅读

ES6 let和作用域 FROM 前端达人

文中简单结:

  • 当一个块或函数嵌套在另一个函数时,就发生了作用域嵌套。使用var声明变量时,如果在函数外声明,就是全局变量,任何函数都可以进行使用,这就是全局作用域查找。如果在函数内使用var声明变量,就是函数作用域查找,只能在函数内部进行访问,外部不能进行访问
var a = 12; // 全局作用域都能访问
function myFunction() {
    alert(a); // alerts 12
    var b = 13;
    if (true) {
        var c = 14; // 函数内部可以访问
        alert(b); // alerts 13
    }
    alert(c); // alerts 14
}
myFunction();
alert(b); // alerts undefined
  • 用var在同一个作用域重复定义变量,后者将会覆盖前者声明的变量的值。ES6引入了let,避免了有var声明变量的一些问题,让变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常是{...}内部),有一点需要强调,在块级作用域定义的变量,块级作用域外是无法访问的
var a = 0;
var a = 1;
alert(a); // alerts 1
function myFunction() {
    var b = 2;
    var b = 3;
    alert(b); // alerts 3
}
myFunction();
let a = 12; // 全局作用域,可以访问
function myFunction() {
    console.log(a); // alerts 12
    let b = 13;
    if (true) {
        let c = 14; // this is NOT accessible throughout the function!
        alert(b); // alerts 13
    }
    alert(c); // alerts undefined  {}外,因此无法访问
}
myFunction();
alert(b); // alerts undefined {}外,因此无法访问

如果你在嵌套作用域里进行重新定义变量,虽然变量名相同,但是不是同一变量

var a = 1;
let b = 2;
function myFunction() {
    var a = 3; // different variable
    let b = 4; // different variable
    if(true) {
        var a = 5; // overwritten
        let b = 6; // different variable
        console.log(a); // 5
        console.log(b); // 6
    }
    console.log(a); // 5
    console.log(b); // 4
}
myFunction();
console.log(a);
console.log(b);
  • 函数声明和变量声明都会被提升(使用var声明变量,let声明变量将不会被提升)。函数首先会被提升,然后才是变量提升。
bookName("ES8 Concepts");
function bookName(name) {
    console.log("I'm reading " + name);
    //I'm reading ES8 Concepts
}
bookName("ES8 Concepts");
//TypeError: bookName is not a function
var bookName = function (name) {
    console.log("I'm reading " + name);
}

JavaScript引擎只会先提升函数,在提升变量声明,引擎将会对上述代码这样调整,代码如下:

var bookName; // 变量声明提升至最上面
bookName("ES8 Concepts"); 
// bookName is not function 
// because bookName is undefined
bookName = function (name) { // 变量赋值不会被提升
    console.log("I'm reading " + name);
}

如果使用let替换var声明函数呢?将会有什么提示输出呢?

bookName("ES8 Concepts"); 
// ReferenceError: bookName is not defined
let bookName = function (name) {
    console.log("I'm reading " + name);
}
//nextTick
function Clock(){
    this.listener;
    // this.listener(); 
    process.nextTick(()=>{
        this.listener();
    }) //等下面的走完了 才会执行这里
}

Clock.prototype.add = function(listener){
    this.listener = listener;
}

let c = new Clock(); 
//这里的时候 Clock里面的 会执行
//此时会报错 listener还是undefined 下面添加新的箭头函数为时已晚
//因为this.listener() 提前执行了 可以改为process.nextTick
c.add(()=>{console.log('ok')});

延伸拓展
Node.js的event loop及timer/setImmediate/nextTick
理解 Node.js 事件循环
理解 js 事件循环

你可能感兴趣的:(梳理 | JavaScript 单线程)