Promise与异步执行机制

303 阅读17分钟

一、执行机制

  1. 进程和线程

(1)含义

  1. 进程(process):保存在硬盘上的程序会被载入到内存中运行,在内存空间里面形成一个独立的内存体,这个内存体有自己独立的地址空间,系统也会为应用的每一个进程分配独立的CPU、内存等资源。
  2. 线程(thread):进程中执行的每一个任务指的就是线程,系统不会为其分配内存资源,各个线程共享进程拥有的内存资源(线程也被称为轻量级进程)。

进程是CPU资源分配的最小单位

线程是CPU调度的最小单位

一般操作系统会存在多个进程,他们相互独立,瓜分内存、CPU资源,

一个进程又分为多个线程,他们共享资源,完成各自的任务,同时由于共享资源,可以相互协作

(2)状态

  1. 进程三态
  • 就绪:获取CPU外的所有资源、只要CPU分配资源就可以马上执行
  • 运行:获得处理器分配的资源,程序开始执行
  • 阻塞:正在执行的进程由于某些事件(I/O请求,申请缓存区失败)而暂时无法运行,

进程受到阻塞,在满足请求时进入就绪状态等待系统调用

  1. 线程三态
  • 就绪:指线程具备运行的所有条件,逻辑上可以运行,在等待处理机
  • 运行:指线程占用处理机正在运行
  • 阻塞:线程在等待一个事件,逻辑上不可执行(占用)

(3)关系

  • 一个程序至少有一个进程,一个进程至少有一个线程。
  • 线程不能够脱离进程而独立运行,同一进程的所有线程共享该进程的所有资源;
  • 进程是cpu资源分配的最小的单位,线程是cpu资源调度的最小的单位
  • 当进程运行时只产生一个线程,被称为单线程,否则被称为多线程。

(4)浏览器中的进程和线程

浏览器是电脑中的一个进程,它有一个主进程还有很多的子进程,通过子进程来实现多进程。

当我们打开浏览器的时候,我们的浏览器就会默认的给我们打开至少 1 个浏览器进程,1 个 GPU 进程,1 个网络进程,和 1 个渲染进程 (一个 tag 页就是一个渲染进程)。ps:* 如果你有浏览器的第三方插件也会打开第三方插件进程 * 然后其中,浏览器进程,GPU 进程,网络进程,第三方插件进程这些都是共有的,只有渲染进程会跟着 tag 页的增加而增加。这里的渲染进程还有另外一个名字,就是我们常说的浏览器内核!!!

这里以 chrome 浏览器举例去看我们的进程,设置~~ 更多工具 ~~任务管理器

进程的下一层就是线程,我们常说的 ****js 引擎线程就是在渲染进程中,那么我们的渲染进程中到底包含什么线程呢?

  • GUI 渲染线程:负责渲染页面,解析 HTML 和 CSS, 构建 DOM 树,CSSOM 树,渲染树和绘制页面,重绘重排也是在该线程中执行.
  • js 引擎线程:一个渲染进程中只能有一个 js 引擎线程,负责解析和执行 js. js单线程。
  • 计时器线程:指 setInterval 和 setTimeout,因为 JS 引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作
  • 异步 http 请求线程: XMLHttpRequest 连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待 JS 引擎空闲执行
  • 事件触发线程:主要用来控制事件循环,比如 JS 执行遇到计时器,AJAX 异步请求等,就会将对应任务添加到事件触发线程中,在对应事件符合触发条件触发时,就把事件添加到待处理队列的队尾,等 JS 引擎处理
  • IO线程: 负责和其他的进程IPC通信(一种进程间的通信方式),接收其他进程传进来的消息。

  1. 并发与并行

并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。 并发解决了程序排队等待的问题,如果一个程序发生阻塞,其他程序仍然可以正常执行。

并行: 当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。

区别:是否同时

  1. 并发只是在宏观上给人感觉有多个程序在同时运行,但在实际的单CPU系统中,每一时刻只有一个程序在运行,微观上这些程序是分时交替执行
  2. 在多CPU系统中,将这些并发执行的程序分配到不同的CPU上处理,每个CPU用来处理一个程序,这样多个程序便可以实现同时执行。
  1. JavaScript 协程

协程定义:一种用户态的轻量级线程,协程的调度完全由用户控制(进程和线程都是由cpu 内核进行调度,或者可以理解为协程就是线程中可以交替运行的代码片段

发展

  • 同步代码
  • 异步JavaScript: callback hell(回调地狱)
  • ES6引入 Promise/a+, 生成器Generators(语法 function foo(){} * 可以赋予函数执行暂停/保存上下文/恢复执行状态的功能), 新关键词yield使生成器函数暂停.
  • ES7引入 async函数/await语法糖,async可以声明一个异步函数(将Generator函数和自动执行器,包装在一个函数里),此函数需要返回一个 Promise 对象。await 可以等待一个 Promise 对象 resolve,并拿到结果.

与线程的比较

  • 线程是为了解决阻塞和并发的问题(在一段时间内执行更多的程序),协程是为了在一段时间运行更多的“程序”(应该说是函数)并且避免线程阻塞。
  • 线程切换是由操作系统的时间片控制, 而协程是程序自己实现的,让协程不断轮流执行才是实现并发,所以实现协程还必须要有一个类似于时间片的结构,不同于线程的切换,协程的切换不是按照时间来算的,而是按照代码既定分配,就是说代码运行到这一行才启动协程,协程是可以由我们程序员自己操控的。
  • 切换调度开销方面远比线程小。
function* idMaker() {
    let index = 0;
    while (true) 
        yield index++;
}
let gen = idMaker(); 
// "Generator { }" 
console.log(gen.next().value); // 0 
console.log(gen.next().value); // 1 
console.log(gen.next().value); // 2  
// 协程本身是个函数,协程之间的切换本质是函数执行权的转移。 
  1. JS中的同步和异步

(1)JS单线程

所谓单线程,是指在JS 引擎中负责解释和执行 JavaScript 代码的线程只有一个

  1. JS为什么是单线程?

这主要和 js 的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

  1. 计算机的同步和异步解释?

同步: 是阻塞模式,是按顺序执行,执行完一个再执行下一个,需要等待,协调运行;

异步: 是非阻塞模式,无需等待,异步是彼此独立,在等待某事件的过程中,继续做自己的事,不需要等待这一事件完成后再工作。

(2)JS任务

任务

  • Js引擎运行的环境是宿主环境,即Web浏览器 / Node.js,但这些宿主环境都提供了一种机制处理程序中多个块的执行,且执行js语句,这种机制被称为事件循环机制

  • 任务队列:一个先进先出的数据结构,也称事件队列.

    • 宏任务
    • 微任务
  • 一个任务可能引起更多任务被添加

分类-同步和异步

  1. 为什么会有同步和异步任务?
  2. 同步任务:主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务

当我们打开网站时,网站的渲染过程,比如元素的渲染,其实就是一个同步任务。

  1. 异步任务:指不进入主线程,而进入任务队列的任务。 只有任务队列通知主线程,某个异步任务可以执行,该任务才会进入主线程, 当我们打开网站时,像图片的加载,音乐的加载,其实就是一个异步任务(单线程的****JS通过事件循环实现异步)

JS 执行机制(Event loop)

  1. 虽然JS是单线程的但是浏览器的内核是多线程的,在浏览器的内核中不同的异步操作由不同的浏览器内核模块调度执行,异步操作会将相关回调添加到任务队列中.
  2. 不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout, ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含下图中的3种 webAPI,分别是 DOM Binding、network、timer模块

演示执行栈、event table、回调队列、event loop过程:

latentflip.com/loupe/?code…!!!

关于heap和stack:深入理解js数据类型与堆栈内存 - 掘金

  1. JS 的执行机制

  1. 判断js代码是同步还是异步,同步就进入主进程,异步就进入event table

  2. 异步和同步各自对应处理方式执行

    1. 异步任务在event table中注册函数,当满足触发条件后,被推入event queue
    2. 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中

总结

  1. 首先判断js代码是同步还是异步,同步就进入主线程,异步就进入event table
  2. 异步任务在event table中注册函数,当满足触发条件后,被推入event queue
  3. 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主进程中

JS的异步执行机制

两个概念

  • Event Table(回调函数对应表): 用来存储 JavaScript 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表
  • 执行栈: 所有同步任务都在主线程上执行,形成执行栈

blog-事件循环机制

步骤

  1. 文件读取操作(异步任务)会被添加到Event Table中,等IO事件完成后,就会在任务队列中添加一个事件,表示异步任务完成 可以进入执行栈
  2. 判断主线程是否有空,当主线程处理完其它任务有空时,就会读取任务队列,如果该任务指定了回调函数,那么主线程在处理该事件时,就会执行回调函数中的代码,也就是执行异步任务
  3. 单线程从从任务队列中读取任务是不断循环的,每次执行栈被清空后,都会在任务队列中读取新的任务,如果没有任务,就会等待直到有新的任务。

举例

console.log("setTime之前");
setTimeout(() => {
     console.log("timeout callback");
   }, 0);
console.log("setTime之后");

JS中常见的异步

A. 回调函数

  1. 定义:你自己定义的函数、你不会调用、最终执行了
  2. 分类: 同步和异步回调
  • 同步回调:立即执行,完全执行完了才结束,不会放入回调队列中

pop push shift unshift reverse concat splice

高阶函数

  • 异步回调:不会立即执行,会放入回调队列将来执行
/ / 同步回调 (数组遍历相关回调函数)
let arr=['s','i','p','c']
console.log(1)
arr.forEach((v,k)=>{
    console.log(v)
})
console.log(2)

//异步回调  ajax请求/promise的失败成功/setTimout/setInterval
console.log("setTime之前");
setTimeout(() => {
      console.log("timeout callback");
   }, 0);
console.log("setTime之后");
  1. 回调函数产生的问题

    1.   a. 回调地狱

在使用JavaScript时,为了实现某些逻辑经常会写出层层嵌套的回调函数,如果嵌套过多,会极大影响代码可读性和逻辑。(eg:A函数的参数是B函数,B函数的参数C函数......)

var sayhello = function (name, callback) {
  setTimeout(function () {
    console.log(name);
    callback();
  }, 1000);
}
sayhello("first", function () {
  sayhello("second", function () {
    sayhello("third", function () {
      console.log("end");
    });
  });
});

b. 控制反转(IOC)

回调最大的问题,会导致信任链断裂,使用回调需要应用某种逻辑解决所有这些控制反转导致的信任问题.

//A
ajax("/xxx",function(){
    //C

},1000)
//B

问题总结:与大脑方式不同有顺序性问题 控制反转造成信任问题

解决方式:Promise的使用 解决原理

B. 事件监听

任务的执行不取决于代码的顺序,而取决于某个事件是否发生

C.发布订阅模式

/**
*伪代码
*/
// 创建事件管理器实例
const event =new EventEmitter() // 注册一个registerFinish事件监听者
event.on('registerFinish', function() { login()}) 
event.on('registerFinish', function(){ fn2()}) ..
function registe(){ 
    setTimeout(()=>{ // 触发事件
        event.trigger("registerFinish") 
    },2000)
}
function login() {
    console,log("fn2执行了") 
}
registe()

D. Promises对象

理解
  1. Promise是一个构造函数,用来封装一个异步操作并可以获得异步执行结果
  2. Promise的参数是一个回调函数(同步) ,返回值为仍Promise对象
  3. Promise对象必须实现then方法,而且then必须返回一个Promise对象,同一个Promise对象的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致
  4. then方法接受两个参数,第一个参数是成功时的回调(异步),在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调(异步)
状态
  • 1.pending(即将发生的)- - ->resolved/fullfilled

  • 2.pending(即将发生的)- - ->rejected

  • 不能逆向转换,同时“完成”态和“拒绝”态不能相互转换

  • 注意:

    • 只有这两种状态转变,且一个Promise对象只能改变一次状态
    • 无论成功或失败都会有一个结果
//promise简单举例
const p = new Promise((resolve, reject) => {
  console.log("start promise");
  let a=1
  if(a==1){
  resolve("success");
  }else{
  reject("error")
  }
});
p.then((val) => {
  console.log(val);//"success"
});
p.catch(err=>{
console.log(err)
})
p.finally(()=>{
console.log(123)
})
console.log("sync task");
/*
start promise
sync task
success
*/
//进阶(Assignment)
//eg1
 console.log("sync task1");
new Promise(
    (resolve,reject)=>{
        console.log(123)
        setTimeout(() => {
          console.log('setTimeout')
          resolve("成功的数据");
          reject("失败的数据");
        }, 1000);
    }).then(
    value=>console.log("onResolved()1", value)
    )
    .catch(
    reason=>console.log("onRejected()1", reason)
    )
   console.log("sync task2");
   
//eg2
 setTimeout(() => {
      console.log("timeout callback()");
      Promise.resolve(1).then((value) => console.log("onResolved", value));
   }, 0);
 setTimeout(() => {
      console.log("timeout callback2()");
    }, 0);
Promise.resolve(1).then((value) => console.log("onResolved", value));
Promise.resolve(1).then((value) => console.log("onResolved2", value));

//eg3
setTimeout(() => {
      console.log(1);
 }, 0);
new Promise((resolve) => {
      console.log(2);
      resolve();
    })
      .then(() => {
        console.log(3);
      }) 
      .then(() => {
        console.log(4);
      });
console.log(5);
   
Promise执行流程

E. Generator 函数

ECMAScript 6 入门 - 《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack

F.async/await

  1. async是用来声明一个异步方法,await用来等待异步方法的执行
  2. 正常情况下await后边是一个Promise对象,返回该对象的结果。如果不是Promise对象,直接返回封装好的promise。await 都会阻塞函数中后边的代码(即加入微队列)
//eg1
async function f(){
    return await 123;
}

f().then(value=>console.log(value))

//eg2
async function fn1(){
    console.log(1)
    await fn2();
    console.log(2);//阻塞
}

async function fn2(){
    console.log('fn2')
}
fn1();
console.log(3)

二、异步中的宏\微任务执行

为什么引入微任务?

异步任务也有需要紧急执行的,需要给他更高的优先级,例如dom操作如果等待过久,会看起来卡顿

宏任务, 微任务

一个微任务(microtask)就是一个简短的函数,当创建该函数的函数执行之后,并且只有当 Javascript 调用栈为空,而控制权尚未返还给被用户代理用来驱动脚本执行环境的事件循环之前,该微任务才会被执行。

简单的说,先执行一个宏任务(最开始是script),然后执行所有微任务,然后宏、微直到全部执行完。

js 宏任务和微任务 - wangziye - 博客园

体验一下

setTimeout(()=>{
    console.log("1");
},0);
//避免晦涩地使用 promise 去创建微任务
queueMicrotask(()=>{
    console.log("2");
});
queueMicrotask(()=>{
    console.log("3");
});
      setTimeout(() => {
        console.log("我是外部的setTimeout");
        setTimeout(() => {
          console.log("我是内部的setTimeout");
        }, 0);
        new Promise((reject, resolove) => {
          reject("我是定时器内部的promise");
        })
          .then((data) => {
            console.log(data);
          })
          .catch((err) => {
            console.log(err);
          });
      }, 100);
// sleep 函数
   function sleep(numberMillis) {
      var now = new Date();
      var exitTime = now.getTime() + numberMillis;
      while (true) {
        now = new Date();
        if (now.getTime() > exitTime) return;
      }
    }
 // 使用sleep 函数去验证我们对主线任务和异步任务的思考
  function addFunc() {
      console.time();
      setTimeout(() => {
        console.timeEnd();
        console.log(2);
      }, 0);
  }
  sleep(1000)
  addFunc()
  function addProFunc() {
      new Promise((resolve, reject) => {
        console.time();
        resolve();
      })
        .then(() => {
          console.timeEnd();
        })
        .catch((err) => {
          console.log(err);
      });
    }

我们可以看到这个图然后想一想,那些是宏任务,那些是微任务。

宏任务 macro-task(Task)

  • script(主线程上第一个任务)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • requestAnimationFrame
  • UI rendering

微任务 micro-task(Job)

注意:事件循环每次只会入栈****一个 macrotask ,主线程执行完该任务后又会先检查 microtasks 队列并完成里面的所有任务后再执行 macrotask;

宏任务中可以再生成宏任务和微任务。微任务也同理。

那么下面就让我们做一些题来检验

正常的调用

function a1() {
   console.log(1);
}
function a2() {
   console.log(2);
}
function a3() {
   console.log(3);
}
// 输出什么?

添加定时器

function a1() {
        console.log(1);
      }
      function a2() {
        console.log(2);
      }
      function a3() {
        console.log(3);
      }
      setTimeout(() => {
        a3();
        setTimeout(() => {
          a2();
        }, 0);
      }, 0);
      a1();
// A 任务
setTimeout(() => {
    console.log(1)
}, 20)
// B 任务
setTimeout(() => {
    console.log(2)
}, 0)
// C 任务
setTimeout(() => {
    console.log(3)
}, 10)
// D
setTimeout(() => {
    console.log(5)
}, 10)
console.log(4)

4 -》 2 3 5 1

添加promise

console.log(1)
new Promise((resolve, reject) => {
    console.log(2)
    resolve()
}).then(res => {
    console.log(3)
})
console.log(4)
// 输出什么?
1243 
console.log(2);
new Promise((resolve, reject) => {
  console.log(1);
  resolve(3);
  console.log(4);
}).then((data) => {
  console.log(data);
});
2143 

添加 async await

console.log(1);

var pro = new Promise((reject, resolve) => {
  console.log(2);
  reject(3)
});

async function add() {
// 等待, pro reject  返回, mid 
  var mid = await pro
  // 上面是同步的
  console.log(mid);
}

add()

console.log(4);
// 1243
setTimeout(function () {
  console.log('1')
}, 0)
console.log('2')
async function async1() {
  console.log('3')
  await async2()
  console.log('4')
}
async function async2() {
  console.log('5')
}
async1()
console.log('6')
// 235641
console.log('1')
async function async1() {
  console.log('2')
  await 'await的结果'
  console.log('3')
}

async1()
console.log('4')

new Promise(function (resolve) {
  console.log('5')
  resolve()
}).then(function () {
  console.log('6')
})
 // 1 2 4 5 3  6

热身结束

  • 第一题
setTimeout(function () {
    console.log(1)
}, 0);
new Promise(function (resolve, reject) {
    console.log(2)
    for (var i = 0; i < 10000; i++) {
        if (i === 10) {
            console.log(10)
        }
        i == 9999 && resolve();
    }
    console.log(3)
}).then(function () {
    console.log(4)
})
console.log(5);
  • 第二题 boss 副本
console.log("start");
setTimeout(() => {
    console.log("children2")
    Promise.resolve().then(() =>{
        console.log("children3")
    })
}, 0)
new Promise(function(resolve, reject){
    console.log("children4")
    setTimeout(function(){
        console.log("children5")
        resolve("children6")
    }, 0)
}).then(res =>{
    console.log("children7")
    setTimeout(() =>{
        console.log(res)
    }, 0)
})

//s  4  2  3 5  7 6
async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')
// script start   async1 start  async2 promise1  script end async1 end promise2 setTimeout
async function async1() {
    console.log('async1 start');
    await async2();
    setTimeout(function() {
        console.log('setTimeout1')  // 这一部分代码会放入到 promise 的微任务队列中。
    },0)
}
async function async2() {
    setTimeout(function() {
        console.log('setTimeout2')
    },0)
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout3');
}, 0);
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');
//宏队列 setTimeout3 setTimeout2  setTimeout1
//script start  
//async1 start
//promise1
//script end
//promise2
//setTimeout3 setTimeout2  setTimeout1

总结: js整体代码执行,增加宏任务和微任务,在js整体代码结束的时候处理自身的微任务,然后事件循环机制去看任务队列中是否有需要被扔进执行栈的,如果有,那么就扔进去。

看一个例子

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=clo, initial-scale=1.0" />
    <title>Document</title>
    <style>
      div {
        height: 100px;
        width: 100px;
        background-color: cadetblue;
      }
    </style>
  </head>
  <body>
    <div></div>
    <button class="btn">点我就完事了</button>
  </body>
  <script>
    var btn = document.querySelector(".btn");
    btn.addEventListener("click", () => {
      var divNode = document.querySelector("div");
      divNode.style.backgroundColor = "red";
      alert("你看见什么了呢?");
    });
  </script>
</html>

思考题: 为什么会发生这种情况?

alert()是 window 的内置函数,被认为是同步代码