Javascript六:异步

一些概念

同步和异步

连续的执行一个任务就叫做同步。不连续的执行一个任务,就叫做异步。

并发(concurrency)和并行(parallelism)

并发是宏观概念,在一段时间内通过任务间的切换完成了多个任务,这种情况就可以称之为并发。

并行是微观概念,假设 CPU 中存在两个核心,那么就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

进程与线程

本质上来说,两个名词都是 CPU 工作时间片的一个描述。

进程描述了 CPU 在运行指令及加载和保存上下文所需的时间,放在应用上来说就代表了一个程序。

线程是进程中的更小单位,描述了执行一段指令所需的时间。

在浏览器中来说,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

在 JS 运行的时候可能会阻止 UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能安全的渲染 UI。

锁,假如当我读取一个数字的时候,同时有两个操作对数字进行了加减,这时候结果就出现了错误。解决这个问题只需要在操作的时候加锁,直到操作完毕之前都不能再进行写入操作。

回调

容易造成回调地狱,代码可读性及可维护性变差。

console.log(1111);
function test(cb){
    setTimeout(()=>{
        console.log("setTimeout");
        cb && cb();
    },1000)
}
test(function(){
    console.log(2222);
    
});

自定义事件

    let eventObj = new EventTarget();
    (function t() {
        setTimeout(() => {
            console.log(1);
            eventObj.dispatchEvent(new CustomEvent("myEvent"));
        },1000)
    })()

    eventObj.addEventListener("myEvent", function () {
        console.log(2);
    })

promise

//Promise 新建后就会立即执行
let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve(1);
  console.log(2);
});
//Promise
// 2

then的返回值

会返回一个新的Promise 对象, 但是状态会有几种情况

then 的回调函数中没有返回值,返回一个新的resolved状态的Promise对象

then 的回调函数返回值非Promise的值, 返回一个新的resolved状态的Promise对象,并且把返回值,传递给下一个 then

then 的回调函数返回值是Promise对象,then 就直接返回这个Promise对象,具体的状态和值由可以由Promise对象定义

    let p = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(1);
            resolve(2)
        }, 1000)
    })
    p.then((res) => {
        console.log(res);
    }).then(() => {
        console.log('没有return,默认成功');
    })

如果直接return的是非Promise的值,会直接执行回调函数

  new Promise((resolve, reject) => {
    resolve('success1')
  }).then(res => {
    console.log(res)
    return Promise.resolve(res + '111')
  }).then(res => {
    console.log(res)
  })
  new Promise((resolve, reject) => {
    resolve('success2')
  }).then(res => {
    console.log(res)
    return res + '222'
  }).then(res => {
    console.log(res) //这个函数会立即执行不会等下次事件循环再执行
  })
//success1
//success2
//success2222
//success1111

reject与catch

then 中的 rejected 函数用来捕获最近的异常。而catch捕获整个promise异常

如果rejected状态被 reject函数处理了,不会再进入catch中。如果没有 reject函数会进入catch中。

如果是代码错误,只有catch能处理。reject函数接受不到。

静态方法

resolve
Promise.resolve(2).then(res => console.log(res))
reject
Promise.reject(2).then(()=>{},err=>console.log(err))
all

Promise.all可以将多个Promise实例包装成一个新的Promise实例

接受一个Promise数组,返回一个新的Promise,

如果Promise都成功,按数组顺序返回成功状态,

若有一个失败,返回最先被reject的值。

allSettled

接受一个Promise数组,返回一个新的Promise,

不管成功和失败都会返回在res函数里,数据结构如下

  //成功:
  {
  status: "fulfilled"
  value: "success"
  }
  //失败
  {
  reason: "err"
  status: "rejected"
  }
race

返回最快响应的Promise

Generator

每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。

value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;

done属性是一个布尔值,表示是否遍历结束。

调用next方法传入的值表示上一个yield的值。

function *foo(x) {
  let y = 2 * (yield (x + 1))
  let z = yield (y / 3)
  return (x + y + z)
}
let t = foo(5)
console.log(t.next())   // => {value: 6, done: false}  执行x+1=5+1=6
console.log(t.next(12)) // => {value: 8, done: false}  传入的参数12等于yield (x + 1)值,执行y=2*12=24 y/3=24/3=8 如果不传参,`yield` 会返回 `undefined`。会输出NAN.
console.log(t.next(13)) // => {value: 42, done: true}  传入的参数13等于yield (y / 3)值 z=13,之前x=5,y=24,总和42

async及await

同步形式书写代码
try及catch方法捕获错误
    let fn1 = function () {
        return new Promise(resolve => {
            setTimeout(() => {
                console.log("fn1");
                resolve(111);
            }, 2000)
        })
    }
    let fn2 = function () {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log("fn2");
                reject("err222");
            }, 1000)
        })
    }

    // fn1().then(res=>{
    //     console.log(res);
    //     return fn2();
    // }).then(res=>{
    //     console.log(res);
    // },err=>{
    //     console.log(err);
    // })

    async function fn() {
        try {
            let res1 = await fn1();
            console.log(res1);
            let res2 = await fn2();
            console.log(res2);
        } catch(e) {
            console.log(e);
        }
    }
    fn()

直接return值会被包装成一个promise

    async function fn(){
        return "value";
    }
    console.log(fn().then(res=>{console.log(res);}));

循环执行await

这么写并不能实现依次执行函数,因为每个await都是在独立的async函数中。

    let arrFn = [fn1,fn2];
    arrFn.forEach(async fn=>{
       await fn();
    })

应该如下写,这样await都在fn函数中

    let arrFn = [fn1,fn2];
    async function fn(){
       for(let i=0;i<arrFn.length;i++){
           await arrFn[i]();
       }
    }
    fn();

setTimeout,setInterval,requestAnimationFrame。

开启定时器前,记得先关闭定时器,防止多次开启定时器。

定时器第三个以后的参数会作为第一个函数的参数。

function add(x,y){
  console.log(x+y)
}
setTimeout(add,1000,1,2)//3

requestAnimationFrame 自带函数节流功能,回调函数执行次数通常是每秒60次(不掉帧的情况下),并且该函数的延时效果是精确的,没有其他定时器时间不准的问题。

Event Loop

宏队列,macrotask,也叫tasks。

  • setTimeout
  • setInterval
  • setImmediate (Node独有)
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

微队列,microtask,也叫jobs.

  • process.nextTick (Node独有)
  • Promise
  • Object.observe
  • MutationObserver

浏览器的Event Loop

1.执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
2.全局Script代码执行完毕后,调用栈Stack会清空;
3.从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
4.继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
5.microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
6.取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
7.执行完毕后,调用栈Stack为空;
重复第3-7个步骤;
重复第3-7个步骤;
......

示例:
console.log('script start');

setTimeout(function () {
    console.log('setTimeout---0');
}, 0);

setTimeout(function () {
    console.log('setTimeout---200');
    setTimeout(function () {
        console.log('inner-setTimeout---0');
    });
    Promise.resolve().then(function () {
        console.log('promise5');
    });
}, 200);

Promise.resolve().then(function () {
    console.log('promise1');
}).then(function () {
    console.log('promise2');
});
Promise.resolve().then(function () {
    console.log('promise3');
});
console.log('script end');
运行结果
script start
script end
promise1
promise3
promise2
setTimeout---0
setTimeout---200
promise5
inner-setTimeout---0
代码步骤

首先顺序执行完主进程上的同步任务,第一句和最后一句的console.log。

遇到setTimeout 0,它的作用是在 0ms 后将回调函数放到宏任务队列中。

遇到setTimeout 200,它的作用是在 200ms 后将回调函数放到宏任务队列中。

遇到两个promise放入微任务队列中。

同步任务执行完之后,首先检查微任务队列, 发现此队列不为空,执行第一个promise的then回调,输出 'promise1',将promise2加入微任务队列末尾,然后执行第二个promise的then回调,输出'promise3',执行第一个promise加入的promise2,输出 'promise2';

此时微任务队列为空,进入下一个事件循环, 检查宏任务队列,取出第一个宏任务 setTimeout的回调函数,立即执行回调函数输出 'setTimeout---0',检查微任务队列,队列为空,进入下一次事件循环.

检查宏任务队列,取出第一个宏任务 setTimeout的回调函数, 立即执行同步任务输出'setTimeout---200'。

遇到inner-setTimeout---0,放到宏任务队列中,遇到promise5放入微任务队列中。检查微任务队列,发现此队列不为空,执行promise的then回调,输出'promise5'。

此时微任务队列为空,进入下一个事件循环,检查宏任务队列,发现有 setTimeout 的回调函数,立即执行回调函数输出,输出'inner-setTimeout---0'。

代码执行结束.

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

setTimeout(() => {
  console.log(9);
})

console.log(10);

//
1
4
10
5
6
7
2
3
9
8
//

NodeJS的Event Loop

  1. 执行全局Script的同步代码
  2. 执行microtask微任务,先执行所有Next Tick Queue中的所有任务,再执行Other Microtask Queue中的所有任务
  3. 开始执行macrotask宏任务,共6个阶段,从第1个阶段开始执行相应每一个阶段macrotask中的所有任务,注意,这里是所有每个阶段宏任务队列的所有任务(在浏览器的Event Loop中是只取宏队列的第一个任务出来执行),每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2
  4. Timers Queue -> 步骤2 -> I/O Queue -> 步骤2 -> Check Queue -> 步骤2 -> Close Callback Queue -> 步骤2 -> Timers Queue ......
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,616评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,020评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,078评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,040评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,154评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,265评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,298评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,072评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,491评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,795评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,970评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,654评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,272评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,985评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,815评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,852评论 2 351

推荐阅读更多精彩内容

  • 1. 前言 JavaScript是一门单线程语言,在执行一些比较耗时的操作(比如常见的Ajax请求)时,为了不阻塞...
    cbw100阅读 7,075评论 0 7
  • [回顾] 事件循环 JS运行的环境称之为宿主环境 执行栈:call stack,一个数据结构,用于存放各种函数的执...
    Darkdreams阅读 306评论 0 0
  • 学习了异步编程的过程之后,我们接着整理一下常用的异步编程方式。常见的异步编程方法包含以下几种: 回调函数 事件发布...
    隽小吚阅读 214评论 0 2
  • 前言 我们知道Javascript语言的执行环境是"单线程"。也就是指一次只能完成一件任务。如果有多个任务,就必须...
    浪里行舟阅读 14,280评论 1 27
  • 异步: 一段不连续的程序流,在未来不确定的事件发生的事情。问题:编程表述 在一件异步事件 A 发生后执行程序 B发...
    YjWorld阅读 138评论 0 0