从 Event Loop 到 Promise (常见问题分析)

写在最前面

  • promise 做为前端经常使用的工具,今天从底了解一下 promise 的使用和基础知识。
    • 其中有出入或者错误的地方但愿朋友们指出。

导航

  • 1、同步和异步
  • 2、单线程和多线程
  • 3、evet loop
  • 4、实战,promise 题目分析

Promise

  • 什么是 promise?html

    • 咱们先明确:Promise 对象用于表示一个异步操做的最终完成 (或失败), 及其结果值.
  • 什么是 async 和 await前端

    • async/await 使得异步代码看起来像·同步代码·,一句话总结,async 函数就是 Generator 函数的语法糖,返回了一个 promise.resolve() 的结果。阮一峰老师的 async 教程
  • 上面提到了一个异步的问题,咱们前端er都知道 JavaScript - 是单线程的,若是存在多个任务的时候,就会有任务队列进行排队,而后一一执行任务。git

不着急介绍 promise 的详情,首先咱们从最开始的同步和异步讲起:github

1、同步和异步

1.1 同步

简单的理解ajax

  • 若是函数在返回结果的时候,调用者可以拿到预期的结果(即便会等待可是依然能拿到预期的结果),那么这个函数就是同步的。
console.log('synchronous'); //咱们能当即获得 synchronous
复制代码

1.2 异步

简单的理解chrome

  • 若是函数返回的时候,不能当即获得预期的结果,而是经过必定的手段获得的(好比回调函数 callback()), 这就是异步,好比经常使用的 promise 和 ajax 操做等。

来看一个图数组

image

2、单线程和多线程

  • 简单的了解了同步和异步的概念后,咱们看看什么是单线程和多线程?

2.1 浏览器常驻线程

一个浏览器一般由如下几个常驻的线程:promise

  1. 渲染引擎线程,负责页面的渲染
  2. js引擎线程,负责js的解析和执行
  3. 定时触发器线程,处理setInterval和setTimeout
  4. 事件触发线程,处理DOM事件
  5. 异步http请求线程,处理http请求
  • 要注意其中渲染引擎js引擎线程是不能同时进行的,渲染线程在执行任务的时候,js引擎线程会被挂起。由于如果在渲染页面的时候,js处理了DOM,浏览器就不知道该听谁的了

2.2 JS 引擎

  1. 渲染引擎:Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trdent引擎,FireFox用的是Gecko引擎。不一样的引擎对同一个样式的实现不一致,就致使浏览器的兼容性问题。
  2. JS引擎:js引擎能够说是js虚拟机,负责解析js代码的解析和执行。一般有如下步骤:
    • 词法解析:将源代码分解位有意义的分词
    • 语法分析:用语法分析器将分词解析成语法树
    • 代码生成:生成机器能运行的代码
    • 代码执行
  • 固然不一样浏览器的JS引擎也是不一样的:Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。

总结一点:JavaScript是单线程的,可是浏览器不是单线程的。一些I/O操做,定时器的计时和事件监听是由其余线程完成的。浏览器

3、消息队列和事件循环

开局一张图bash

image

导图要表达的内容用文字来表述的话: 1.同步和异步任务分别进入不一样的执行"场所" 2.同步的进入主线程,异步的进入Event Table并注册回调函数到 Event Queue 中。 3.当主线程执行完毕之后,而后会去 Event Queue 查询,时候若是存在的函数,放进主线程中继续执行。 4.上述就是event loop的执行

说了这么多文字,不如直接一段代码更直白:

console.log('script start');

new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function cb() {
    console.log('promise2');
});

console.log('script end');

// script start
// promise1
// script end
// promise2

复制代码

分析这段代码:

首先执行,打印 script start

而后进入 promise 函数打印 promise1,执行 resolve()
在 then 执行的时候咱们把异步回调放进了 event table 中注册相关的回调函数。
new promise 执行完毕,回调函数cb() 进入Event Queue。

执行 打印 script end;

主线程从Event Queue读取回调函数 cb 并执行。
复制代码

3.1 宏任务和微任务

  • 记住一点,当同一个 event queue 中有 微任务 的时候,优先执行 微任务

macro-task(宏任务):包括总体代码script,setTimeout,setInterval micro-task(微任务):Promise,process.nextTick

image

看一个栗子

3.2 思考一下代码执行顺序

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(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});

console.log('script end');
    /** * script start * async1 start * async2 * promise1 * script end * async1 end * promsise2 * setTimerout */
复制代码

注意几个点

一、js是单线程的。
二、promise被定义后是当即执行的,可是他的resolve是异步的。
三、promise的异步优先级高于setTimeout。
四、async会返回一个promise对象,await关键字会让出线程。
复制代码
  • 分析
- 定义异步函数 async1, 异步函数 async2
1. console.log('script start'); 执行 (1)`script start`

2. setTimeout 执行,异步放入异步队列中,注意这是一个宏任务(咱们标记为 macro1)

3. 执行 async1(), 打印 (2)`async1 start`, 执行 async1() 中的 await async2(): 打印 (3)`async2`;
遇到 await 后面的函数进入任务队列,这里又注册一个微任务(咱们标记为 mico1);到这里 async1() 就执行完了

4. 执行 new Promise:打印 (4)`promise1`,执行 resolve();
而后在 then 中注册回调函数,console.log('promise2') 函数进入任务队列;
注册 event queue(咱们标记为 mico2).这里 new Promise 就执行完了。

5. 执行 console.log('script end');, 打印 (5) `script end`;

6. 上面👆五步把主线程都执行完毕了,而后去event queue 查找有没有注册的函数;
咱们发现了(macro 1, mico1, mico2),按照优先执行微任务的原则,咱们按照这样的顺序执行 mico1 > mico2 > macro1。
 打印:(6) `async1 end` (7) `promise2` (8) `setTimeout`

复制代码

[!warning]可能你会在不一样浏览器发现不一样结果,这是由于不一样浏览器和版本的不一样遵循的 promise 规则不一样。这里是按照较新版本的 chrome(68+) 执行的结果,具体参考(www.w3.org/2001/tag/do…

4、回到 promise

  • 这里直接看几道常见的题目来认识 promise 具体是什么?

4.1 理解常见的状态变化

  • 理解 resolve
const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
})
promise.then(() => {
    console.log(3);
})
console.log(4);
复制代码

分析

首先Promise新建后当即执行,因此会先输出1,2,而Promise.then()内部的代码在当次事件循环的结尾当即执行,因此会先输出4,最后输出3.

QA:1 2 4 3

  • 理解状态变化
const promise = new Promise((resolve, reject) => {
    resolve('success1');
    reject('error');
    resolve('success2');
});
promise.then((res) => {
    console.log('then:', res);
}).catch((err) => {
    console.log('catch:', err);
})
复制代码

分析

resolve函数Promise对象的状态从“未完成”变为“成功”(即从pending变为resolved),在异步操做成功时调用,并将异步操做的结果,做为参数传递出去; reject函数将Promise对象的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操做失败时调用,并将异步操做报出的错误,做为参数传递出去。 而一旦状态改变,就不会有再变

因此代码中的reject('error');不会有做用。 Promise只能resolve一次,剩下的调用都会被忽略。 因此第二次resolve('success');也不会有做用。

QA:then:success1

4.2 手写 promise

(1) promise 对象初始化状态为 pending

(2) 当调用resolve(成功),会由pending => fulfilled

(3) 当调用reject(失败),会由pending => rejected

  • 基础版
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(executor) {
    this.state = PENDING;
    this.value = null;
    this.reason = null;

    const resolve = value => {
        if (this.state === PENDING) {
            this.state = FULFILLED;
            this.value = value;
        }
    };

    const reject = reason => {
        if (this.state === PENDING) {
            this.state = REJECTED;
            this.reason = reason;
        }
    };

    try {
        executor(resolve, reject);
    } catch (reason) {
        reject(reason);
    }
}
复制代码
  • 添加 then 手法
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(executor) {
    this.state = PENDING;
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = value => {
        if (this.state === PENDING) {
            this.state === FULFILLED;
            this.value === value;
            this.onFulfilledCallbacks.forEach(fuc =>{
                fuc();
            });
        }
    };

    const reject = reason => {
        if (this.state === PENDING) {
            this.state = REJECTED;
            this.reason === reason;
            this.onRejectedCallbacks.forEach(fuc =>{
                fuc();
            })
        }
    };

    try {
        executor(resolve, reject);
    } catch (reason) {
        reject(reason);
    }
}

/* - then方法接受两个参数onFulfilled、onRejected,它们分别在状态由PENDING改变为FULFILLED、REJECTED后调用 - 一个promise可绑定多个then方法 - then方法能够同步调用也能够异步调用 - 同步调用:状态已经改变,直接调用onFulfilled方法 - 异步调用:状态仍是PENDING,将onFulfilled、onRejected分别加入两个函数- - 数组onFulfilledCallbacks、onRejectedCallbacks, - 当异步调用resolve和reject时,将两个数组中绑定的事件循环执行。 */

MyPromise.prototype.then = function(onFulfilled,onRejected){
    switch(this.state){
        case FULFILLED:
            onFulfilled(this.value);
            break;
        case REJECTED:
            onRejected(this.reason);
            break;
        case PENDING:
            this.onFulfilledCallbacks.push(()=>{
                onFulfilled(this.value);
            });
            this.onRejectedCallbacks.push(() => {
                onRejected(this.reason);
            })
            break;
            
    }
}

// 因为catch方法是then(null, onRejected)的语法糖,因此这里也很好实现
MyPromise.prototype.catch = function(onRejected){
    return this.then(null, onRejected);
}
复制代码

4.3 promsie.all

  • 所有成功,返回一个成功res的数组,若是有失败就返回那个失败的err
Promise.all = function(promises) {
    return new Promise(function(resolve, reject) {
      var resolvedCounter = 0
      var promiseNum = promises.length
      var resolvedValues = new Array(promiseNum)
      for (var i = 0; i < promiseNum; i++) {
        (function(i) {
          Promise.resolve(promises[i]).then(function(value) {
            resolvedCounter++
            resolvedValues[i] = value
            if (resolvedCounter == promiseNum) {
              return resolve(resolvedValues)
            }
          }, function(reason) {
            return reject(reason)
          })
        })(i)
      }
    })
复制代码
  • 咱们想实现一个不论是成功仍是失败都返回怎么处理?
    • 优先在 all 处理前去处理一下咱们数组的值,错误的catch 错误,成功的捕获成功就是
Promise.all([a,b,c].map(p => p.catch(e => {...})))
  .then(res => {...})
  .catch(err => {...});
复制代码

4.4 图片异步加载封装

  • 实现一个图片异步的封装,成功就返回图片,失败返回错误提示,有一个属性值 src
function loadImageAsync(url) {
    return new Promise(function(resolve,reject) {
        var image = new Image();
        image.onload = function() {
            resolve(image) 
        };
        image.onerror = function() {
            reject(new Error('Could not load image at' + url));
        };
        image.src = url;
     });
}   
复制代码

衍生:这里须要先并发请求3张图片,当一张图片加载完成后,又会继续发起一张图片的请求,让并发数保持在3个,直到须要加载的图片都所有发起请求。咱们应该怎么作?

4.5 待续,promise 有不少有趣的实现,后续会继续补充

参考

相关文章
相关标签/搜索