事件循环与异步


线程和异步

进程

当一个应用程序运行时,需要使用内存和 CPU 资源,这些资源需要向操作系统申请。
操作系统以进程的方式来分配这些资源,一个进程就代表着一块独立于其他进程的内存空间。
一个应用程序要运行,必须至少有一个进程启动。
进程的最大特点是独立,一个进程不能随意的访问其他进程的资源。这就保证了多个程序在操作系统上运行互不干扰。

线程

任何一个进程在启动的时候,操作系统都会给其分配一个线程,应用程序的入口函数在主线程中运行。
在应用程序的运行过程中,可能有多个任务需要同时执行,于是可以向操作系统申请分配更多的线程来执行不同的任务。
比如,浏览器启动后,会开启多个线程来处理不同的事情。

两者区别:
和进程不一样的是,线程之间的资源不是隔离的,它们可以共享数据,并且线程可以被调度。
比如浏览器中的执行线程和 GUI 线程就是被调度为互斥的,当 GUI 线程执行渲染时,执行线程会被阻塞,反之亦然。所以在下面的代码中你是看不到元素内容被改变的:

<h1 id="title">Monica</h1>
<button onclick="test()">click me</button>
<script>
  function test() {
    title.innerHTML = "莫妮卡";
    while (true) {}
  }
</script>
<!--点击之后JS执行引擎在执行,test()运行,下一步才渲染。但是进入了死循环,执行线程结束不了了-->

我们所说的「JS 中是单线程」的语言,是指在宿主环境中,执行 JS 代码的线程只有一个,不代表宿主环境是单线程
为什么要互斥?先执行完后渲染,效率高
为什么渲染的时候不能执行 JS 代码?执行混乱

异步

单线程的主要优势是不需要考虑线程调度,降低了程序的复杂性
但在单线程中如果要处理需要等待的任务时,就必须要考虑阻塞的问题。
考虑下面的伪代码:

var dom = document.getElementById("name"); // 获取某个dom元素
var name = syncConnect("http://server/getname"); // 以同步的方式向服务器获取名字
dom.innerHTML = name;
otherTask(); // 其他无关任务


因此,JS 引入异步来处理该问题

var dom = document.getElementById("name"); // 获取某个dom元素
asyncConnect("http://server/getname", function callback(result) {
  //以异步的方式向服务器获取名字
  dom.innerHTML = result;
});
otherTask(); // 其他无关任务

执行栈

要想执行必须有执行上下文
JS 执行引擎只会执行栈顶端的东西
实例 1

function A() {
  console.log("A"); // 函数调用,新建log上下文,入栈,执行完出栈
  B(); // 建立B的上下文
}
function B() {
  console.log("B"); // log上下文
}
A(); // 创建A的上下文,入栈。JS执行引擎只会执行栈顶端的东西,所以不执行下一句,而是执行A
console.log("global");
// 答案:A B global

事件循环

事件循环是 JS 处理异步函数的具体方法

具体的做法是:

  1. 执行 执行栈 中的代码
  2. 遇到一些特殊代码交给浏览器的其他线程处理
  3. 将执行栈中的代码全部执行完毕
  4. 从事件队列中取出第一个任务放入执行栈,然后重复第 1 步

事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:

  • 宏任务(队列):macroTask,计时器结束的回调、事件回调、http 回调等等绝大部分异步函数进入宏队列
  • 微任务(队列):microTask,Promise.then, MutationObserver

当执行栈清空时,JS 引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务。

<div id="app"></div>
<script>
  var app = document.getElementById("app");
  setTimeout(function B() {
    console.log(1);
    app.innerHTML = "1";
  });
  var observer = new MutationObserver(function A() {
    // 该函数将在微队列中
    console.log(2);
    setTimeout(function C() {
      console.log(3);
    });
  });
  observer.observe(app, { childList: true }); // 观察 app 的变化
  app.innerHTML = "2";
  // 2 1 2 3 3
</script>

面试题

  1. 怎样理解 JS 的异步?(必考)JS 是一个单线程的语言,意味着宿主仅为其分配了一个执行线程。而在实际的开发中,JS 有时需要执行一些耗时的操作,比如等待一个 DOM 事件发生、等待网络通信完成、等待计时结束等等。如果在执行线程上去等待,就浪费线程的宝贵执行时间,阻塞后续操作。更可怕的是,由于浏览器的GUI 线程和 JS 执行线程是互斥的,这就导致浏览器界面会因为 JS 的等待处于卡死状态。因此,JS 通过异步来解决这个问题,当需要等待的时候,通知宿主的其他线程去做处理,执行线程则继续后续执行。当其他线程完成处理后,会发出通知,此时执行线程转而去执行事先定义好的回调函数即可。异步的方式充分了解放了执行线程,让执行线程可以毫无阻塞的运行,也就避免了浏览器宿主因为等待操作完成出现的卡死现象。
  2. 下面的哪个函数执行会导致报错?如果报错,会报什么错误?为什么会出现这种情况?
function A() {
  A();
}
function B() {
  var n = 0;
  while (n >= 0) {
    n++;
  }
}
// 执行上下文 A Maximum call stack size exceeded 栈溢出
// B 死循环 不报错 栈空间不变化,一直是B

下面的代码输出结果是什么?

setTimeout(function func1() {
  // func1先进入事件队列
  console.log(1);
  a();
}, 0);
function a() {
  setTimeout(function func2() {
    console.log(2);
  }, 0);
  console.log(3);
}
a();
console.log(4);
// 3 4 1 3 2 2

!考点:GUI 线程怎么渲染的?

ES6 异步机制

面试常考:JS 基础里面的事件循环 原形原型链 作用域链

异步处理的演化

JS 实现异步的代码模型主要依托于回调

dom.addEventListener(
  "click",
  function (e) {
    // 回调函数作为第二个参数传递,函数可接收一个参数
  },
  {}
);
dom.onclick = function (e) {
  // 回调函数作为属性传递,函数可接收一个参数
};
fs.readFile("./txt", function (err, buffer) {
  // 回调函数作为最后一个参数传递,函数可接收两个参数
});

这种设计实际上是有缺陷的

  1. 没有统一的标准
  2. 容易陷入回调地狱(callback hell)
/*
异步任务:依次发送7次网络请求,拿到服务器数据
*/
asyncConnect("地址1", (resp1) => {
  // to do something
  asyncConnect("地址2", (resp2) => {
    // to do something
    asyncConnect("地址3", (resp3) => {
      // to do something
      asyncConnect("地址4", (resp4) => {
        // to do something
        asyncConnect("地址5", (resp5) => {
          // to do something
          asyncConnect("地址6", (resp6) => {
            // to do something
            asyncConnect("地址7", (resp7) => {
              // to do something
            });
          });
        });
      });
    });
  });
});

后来,JS 社区提出了Promise A+规范,希望把异步规范化,并消除回调地狱
再后来,ES6 官方标准中提出了 Promise API 来处理异步,它满足Promise A+规范
由于异步处理变得标准了,就给 ES 官方提供了进一步改进的空间,于是在 ES7 中出现了新的语法async await,它更加完美的解决了异步处理问题

Promise 的概念

一个 promise 就是一个对象,它表示一个异步任务
异步任务内部保存了它的进展状态,规范中约定有三种状态,不同的状态属于不同的阶段

状态的转换

任务开始时,始终处于未决阶段的挂起状态
任务在未决阶段的时候,有能力将其推向已决。比如,当从服务器拿到数据后,我们就从未决阶段推向已决的 resolved 状态,如果网络不好,导致出错了,我们就从未决阶段推向已决的 rejected 状态
我们把从未决推向已决的 resolved 状态的过程,叫做 resolve从未决推向已决的 rejected 状态的过程,叫做 reject

这种状态和阶段的变化是不可逆的,也就是说,一旦推向了已决,就无法重新改变状态

任务完成后附带的数据

任务从未决到已决时,可能附带一些数据,比如:跑步完成后的用时、网络请求后从服务器拿到的数据

任务已决后(有了结果),可能需要进一步做后续处理
针对 resolved 的后续处理,称之为 thenable,针对 rejected 的后续处理,称之为 catchable

Promise 的基本使用

ES6 提供了一套 API 来适配上面提到的异步模型,这个 API 即Promise

var pro = new Promise((resolve, reject) => {
  //未决阶段的代码,这些代码将立即同步执行,表示任务启动后要做的事情
  //...
  //在合适的时候,将任务推向已决
  //resolve(数据):将任务推向resovled状态,并附加一些数据
  //reject(数据):将任务推向rejected状态,并附加一些数据
});

应用:Promise A+规范:必须有 then,可以有多个参数

function delay(duration) {
  return new Promise((resolve, reject) => {
    console.log("开始计时");
    setTimeout(() => {
      console.log("结束计时");
      if (Math.random() < 0.5) {
        resolve(123);
      } else {
        reject(233);
      }
    }, duration);
  });
}
var pro = delay(1000);
pro.then(
  (data) => {
    console.log("任务完成了", data); // undefined
  },
  (reason) => {
    console.log("任务失败了", reason);
  }
);

Promise A+规范没有规定

pro.then((data) => {
  console.log("任务完成了", data); // undefined
});
pro.catch((reason) => {
  console.log("任务失败了", reason);
});

注意

  1. 任务一旦进入已决后,所有企图改变任务状态的代码都将失效
  2. 以下代码可以让任务到达 rejected 状态
    1. 调用 reject
    2. 代码执行报错
    3. 抛出错误

拿到 Promise 对象后,可以通过 then 方法指定后续处理

pro.then(thenable, catchable);
//或
pro.then(thenable);
pro.catch(catchable);

无论是 thenable 还是 catchable,均是下面格式的函数

function (data){
    //data为状态数据
}

注意:后续处理函数一定是异步函数,并且放到微队列中

面试题

  1. 下面代码输出什么?
const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve("a");
  resolve("b");
  reject("c");
  console.log(2);
});
promise.then(
  (data) => {
    console.log(data);
  },
  (resean) => {
    console.log(reason);
  }
);
console.log(4);
// 1 2 4 a
  1. 下面的代码输出什么?
setTimeout(() => {
  console.log(1);
});
var pro = new Promise((resolve, reject) => {
  console.log(2);
  resolve(3);
  console.log(4);
  throw 5; // 状态不能变,但是依旧执行此代码,即报错,导致后面代码不走了
  console.log(6); // 不走了
});
console.log(7);
pro.then(
  (data) => {
    console.log(data);
  },
  (reason) => {
    console.log(reason);
  }
);
// 2 4 7 3 1

更多知识

Promise 是可以链式调用的

var pro1 = ...; // pro1 是一个异步任务,它完成后会得到一个数字3
pro1
  .then(n=>{
  console.log(n); // 输出:3
  return n * 2;
})
  .then(n=>{
  console.log(n); // 输出:6
  return n * 2;
})
  .then(n=>{
  console.log(n); // 输出:12
})

demo

var pro = new Promise((resolve, reject) => {
  resolve(1);
});
pro
  .then((d) => {
    console.log(d);
    return 2;
  })
  .then((d) => {
    //这个d取决于上一个的返回结果
    console.log(d);
    return 3;
  })
  .then((d) => {
    console.log(d);
  });

如果上一个返回的是一个 Promise 呢

var pro = new Promise((resolve, reject) => {
  resolve(1);
});
pro
  .then((d) => {
    console.log(d);
    return new Promise((resolve, reject) => {
      resolve(5);
    }); //相当于返回的此Promise
  })
  .then((d) => {
    console.log(d);
    return 3;
  })
  .then((d) => {
    console.log(d);
  });
/*
异步任务:依次发送7次网络请求,拿到服务器数据
*/
asyncConnect("地址1", (resp1) => {
  // to do something
  asyncConnect("地址2", (resp2) => {
    // to do something
    asyncConnect("地址3", (resp3) => {
      // to do something
      asyncConnect("地址4", (resp4) => {
        // to do something
        asyncConnect("地址5", (resp5) => {
          // to do something
          asyncConnect("地址6", (resp6) => {
            // to do something
            asyncConnect("地址7", (resp7) => {
              // to do something
            });
          });
        });
      });
    });
  });
});
/*
异步任务:依次发送7次网络请求,拿到服务器数据
*/
asyncConnect("地址1")
  .then((resp) => {
    // to do something
    return asyncConnect("地址2"); // 返回新的Promise
  })
  .then((resp) => {
    // to do something
    return asyncConnect("地址3");
  })
  .then((resp) => {
    // to do something
    return asyncConnect("地址4");
  })
  .then((resp) => {
    // to do something
    return asyncConnect("地址5");
  })
  .then((resp) => {
    // to do something
    return asyncConnect("地址6");
  })
  .then((resp) => {
    // to do something
    return asyncConnect("地址7");
  })
  .then((resp) => {
    // to do something
  });

如果使用ES7asyncawait,代码会更加优雅,消除了回调

/*
异步任务:依次发送7次网络请求,拿到服务器数据
*/
async function doRequest() {
  var resp1 = await asyncConnect("地址1");
  // to do something
  var resp2 = await asyncConnect("地址2");
  // to do something
  var resp3 = await asyncConnect("地址3");
  // to do something
  var resp4 = await asyncConnect("地址4");
  // to do something
  var resp5 = await asyncConnect("地址5");
  // to do something
  var resp6 = await asyncConnect("地址6");
  // to do something
  var resp7 = await asyncConnect("地址7");
  // to do something
}

简化

function asyncConnect(url) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(Math.random());
    }, Math.floor(Math.random() * 3000));
  });
}

async function doRequest() {
  var urls = ["地址1", "地址2", "地址3"];
  for (var url of urls) {
    var resp = await asyncConnect(url);
    console.log(resp);
  }
}

快手试题

  1. 下面的代码输出什么
var a;
var b = new Promise((resolve, reject) => {
  console.log("promise1");
  setTimeout(() => {
    resolve(); //1000ms后推向成功
  }, 1000);
})
  .then(() => {
    console.log("promise2");
  })
  .then(() => {
    console.log("promise3");
  })
  .then(() => {
    console.log("promise4");
  });
a = new Promise(async (resolve, reject) => {
  console.log(a);
  await b; //等待b,a的赋值不会被卡住  这个代码相当于b.then(()=>{下面一驼}),后面变成异步了
  console.log(a); //pending等待着
  console.log("after1");
  await a;
  resolve(true);
  console.log("after2");
});
console.log("end");
/*
		a undefined
		b 是所有then调用完返回的Promise .promise234放入微队列,b是pending
		开始a赋值,因为赋值给a的Promise还没运行,输出a:undefined,等待b,a运行结束.此时a变成Promise:pending
		输出end, 输出微队列内容,promise234, b完成,a里面的await结束了,输出a,a还是pending,后after1, a永远完不成了(因为要等a完成后才能完成的悖论)
*/
  1. 下面的代码输出什么?
async function async1() {
  console.log('async1 start');
  await async2();//等待
  console.log('async1 end');//进入微队列
}
async function async2() {//有async 返回的一定是promsie
  console.log('async2');
}
// 两个函数没有执行先不看
console.log('script start');

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

async1();

new Promise(function(resolve) {
  console.log('promise1');
  resolve();
}).then(function() {
  console.log('promise2');
});
console.log('script end');
Script start
Async1 start
Async2
Promise1
Script end

宏队列:setTimeout
微队列:async1end promise2

高仿 setTimeout

function delay(duration) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, duration);
  });
}
async function test() {
  console.log(1);
  await delay(1000);
  console.log(2);
  await delay(1000);
  console.log(3);
}
test();

高仿 setInterval

function delay(duration) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, duration);
  });
}
async function test() {
  var count = 0;
  while (true) {
    await delay(1000);
    console.log(count++);
  }
}
test();
const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve("a");
  resolve("b");
  reject("c");
  console.log(2);
});
promise.then(
  (data) => {
    console.log(data);
  },
  (resean) => {
    console.log(reason);
  }
);
console.log(4);
setTimeout(() => {
  console.log(1);
});
var pro = new Promise((resolve, reject) => {
  console.log(2);
  resolve(3);
  console.log(4);
  throw 5; // 相当于  reject(5), 但是throw会中断当前函数的执行
  console.log(6);
});

console.log(7);

pro.then(
  (data) => {
    console.log(data);
  },
  (reason) => {
    console.log(reason);
  }
);

文章作者: Sunny
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Sunny !
  目录