梳理 | JavaScript 单线程

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

1、JavaScript 执行为何单线程

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

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,是全球最大的开源库生态系统。
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 事件循环

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,081评论 1 32
  • 一. 操作系统概念 操作系统位于底层硬件与应用软件之间的一层.工作方式: 向下管理硬件,向上提供接口.操作系统进行...
    月亮是我踢弯得阅读 5,948评论 3 28
  • 进程和线程 进程 所有运行中的任务通常对应一个进程,当一个程序进入内存运行时,即变成一个进程.进程是处于运行过程中...
    胜浩_ae28阅读 5,084评论 0 23
  • iOS多线程编程 基本知识 1. 进程(process) 进程是指在系统中正在运行的一个应用程序,就是一段程序的执...
    陵无山阅读 5,995评论 1 14
  • Normal Flow 即浏览器默认的文档布局方式。定位就是通过设置 position 属性的值来覆盖默认的布局方...
    饥人谷_易燃阅读 224评论 0 0