oliver1204 / randomNotes

用来记录平时的日常总结
1 stars 0 forks source link

promise 原理 #74

Closed oliver1204 closed 5 years ago

oliver1204 commented 6 years ago

promise的基本用法

var promise = new Promise(function(resolve, reject) {
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
promise
.then(function(value) {
  // 如果调用了resolve方法,执行此函数
}, function(value) {
  // 如果调用了reject方法,执行此函数
});

其实Promise函数的使命,就是构建出它的实例,并且负责帮我们管理这些实例。而这些实例有以下三种状态:

  1. pending: 初始状态,位履行或拒绝
  2. fulfilled: 意味着操作成功完成
  3. rejected: 意味着操作失败

new Promise 只接受一个函数为参数,这个函数在创建的时候就立即执行了。

promise原理

promise 从设计模式上来讲,是观察者模式

观察者模式

观察者模式是基于订阅-发布模式的,但是又有些许区别, 发布 emit 和订阅 on 两者没有直接关系, 观察者模式把 观察者 放到被观察者中,二者都是 一种一对多的关系 [fn,fn,fn]。

举个生活中的例子: 一天 A 去售楼处看房子,到了售楼处才发现楼盘都售光了,但是值得高兴的是下周可能会有一批尾楼盘放出,于是 A 请售楼小姐记下他的信息,等楼盘放出后给他发信息。 不久后 B 、C 、D 都来看房子都遇见同样的问题,售楼小姐依次记录了他们的信息。一周后,楼盘放出了,售楼小姐拿出手机依次给 A、B 、C 、D... 客人发了信息(发完崩溃了。。。)。

这就是订阅-发布模式。

后来售楼小姐认识了一个程序猿小哥哥,和他说了这件事,程序猿小哥哥说,你把 A、B 、C 、D... 的信息给我,我帮你操作,这样你就不用一个一个发信息了。

下面看看程序猿小哥哥的操作:

// 观察者模式: 把 观察者 放到被观察者中

class Subject { // 被观察者
    constructor() {
      this.stack = [];
      this.name = '';
    }
    attach = (observer) => {
      this.stack.push(observer);
    }
    setName = (newName) => {
      this.name = newName;
      this.stack.map( o => o.mobile(newName))
    }
}

class Observer { // 观察者
    constructor(username) {
      this.username = username;
    }
    mobile = (name) => {
      console.log('尊敬的用户' + this.username + '您好!您关注的'+ name + '开售了,快来看看吧')
    }
}

let o1 = new Observer('A');
let o2 = new Observer('B');// 收集  A、B ... 的信息
let s = new Subject();
s.attach(o1);
s.attach(o2); // 将 A、B ... 的信息放入待执行队列中
s.setName('万科楼盘') // 当楼盘信息出来后,主动给 A、B... 发信息

手写一版 promise

下面我们手写一版 promise,如下:


const SUCCESS = 'fulfilled'
const FAIL = 'rejected';
const PENDING = 'pending'

class Promise {
  constructor(executor) {
     this.status = PENDING;
    this.value = undefined;  // 成功返回
    this.reason = undefined; // 失败返回
    this.onResolvedCallbacks = []; // 存储成功的所有的回调 只有pending的时候才存储
    this.onRejectedCallbacks = []; // 存储所有失败的

    try {
        executor(this.resolve, this.reject);
    } catch(e){
        reject(e);
    }
  }

  then = (onFulfilled, onRejected) => {
    if(this.status === SUCCESS){
        onFulfilled(this.value);
    }
    if(this.status === FAIL){
        onRejected(this.reason);
    }
    if(this.status === PENDING){
        this.onResolvedCallbacks.push(()=>{
            onFulfilled(this.value);
        });
        this.onRejectedCallbacks.push(()=>{
            onRejected(this.reason);
        })
    }
  }

  resolve = (value) => {
        if(this.status === PENDING){
             this. value = value;
             this.status = SUCCESS;
            this.onResolvedCallbacks.forEach((fn) => {
              fn(value)
            })
        }
  }

  reject = (reason) => {
        if(this.status === PENDING){
            this. reason = reason;
            this.status = FAIL;
             this.onRejectedCallbacks.forEach((fn) => {
              fn(reason)
            })
        }
  }
}

promise 和 fetch 简单结合一下:

let fetchData = new Promise((resovle, reject) => {
  fetch(url)
  .then(data => data.json())
  .then(data => {
     if(data.success) {
        return resovle(data)
     }
     else {
       return reject(data)
     }
  })
}) 

当我们使用链式调用法,如下:


fetchData
.then(res => {
  console.log(res)
})
.then(res => {
  console.log(this.userInfo)
})

当进去第二个then后,我们发现,程序因为找不到then方法而导致报错。

image

promise的链式操作关键是: Promise.prototype.then方法接受一个函数为参数,并返回的是一个新的Promise对象,因此可以采用链式写法。

想让then方法支持链式调用,其实也是很简单的:

then(onFulfilled, onRejected) {
    ....

    return this
}

没错,就是这么简单的一句,就可以很魔法的实现链式调用。

问题1:

如果用户再then函数里面注册的仍然是一个Promise,该如何解决?


Page({
  onShow: function() {
    HTTP.GET('v2/user/invest/info')
    .then(res => {
      this.userInfo = res
      console.log(this.userInfo)
      return HTTP.GET('v2/report/invest/do/list')
    })
    .then(data => {
      console.log(data)
    })
  }
})

如果继续使用上面的promise,HTTP.GET('v2/report/invest/do/list')返回内容无法是无法获取的:

callback 2x

如图,data 和 res 返回内容一致,这显然不是我们想要的。

链式Promise是指在当前promise达到fulfilled状态后,即开始进行下一个promise(后邻promise)。那么我们如何衔接当前promise和后邻promise呢?

我们可以说尝试着在 then 方法里面 return 一个 promise, 事实上Promises/A+规范中的2.2.7就是这么说哒~

继续改造我们的promise:

// 在Promise类外添加一个resolvePromise方法
function resolvePromise(promise2, x,resolve,reject) { // 考虑的非常全面
    if(promise2 === x){
       return reject(new TypeError('TypeError: Chaining cycle detected for promise #<Promise>'));
    }
    // 判断x的类型
    // promise 有n种实现 都符合了这个规范 兼容别人的promise

    // 怎么判断 x是不是一个promise 看他有没有then方法
    if(typeof x === 'function' || (typeof x === 'object' && x != null)){
      try{
        let then = x.then; // 去then方法可能会出错
        if(typeof then === 'function'){ // 我就认为他是一个promise
           then.call(x,y=>{ // 如果promise是成功的就把结果向下传,如果失败的就让下一个人也失败
              resolvePromise(promise2,y,resolve,reject); // 递归
           },r=>{
              reject(r);
           }) // 不要使用x.then否则会在次取值
        }else{ // {then:()=>{}}
          resolve(x);
        }
      }catch(e){
        reject(e);
      }
    }else{ // x是个? 常量 
      resolve(x);
    }
}

...
then(onFulfilled, onRejected) {
    let promise2; 
    // 可以不停的调用then方法,返还了一个新的promise
    // 异步的特点 等待当前主栈代码都执行后才执行
    promise2 = new Promise((resolve, reject) => {
      if (this.status === SUCCESS) {
        setTimeout(() => {
          try {
            // 调用当前then方法的结果,来判断当前这个promise2 是成功还是失败
            let x = onFulfilled(this.value);
            // 这里的x是普通值还是promise
            // 如果是一个promise呢?
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        });
      }
      if (this.status === FAIL) {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            reject(err);
          }
        });
      }
      if (this.status === PENDING) {
        this.onResolvedCallbacks.push(()=>{
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          });
        });
        this.onRejectedCallbacks.push(()=> {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (err) {
              reject(err);
            }
          });
        });
      }
    });
    return promise2; // 之前例子中的this
  }

...

总结

现在回顾下Promise的实现过程,其主要使用了设计模式中的观察者模式:

  1. 通过Promise.then和Promise.catch方法将观察者方法注册到被观察者Promise对象中,同时返回一个新的Promise对象,以便可以链式调用。

  2. 被观察者管理内部pending、fulfilled和rejected的状态转变,同时通过构造函数中传递的resolve和reject方法以主动触发状态转变和通知观察者。

oliver1204 commented 5 years ago

一个Promise面试题


oliver1204 commented 5 years ago

红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用Promse实现)三个亮灯函数已经存在:

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

解析

红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次,意思就是3秒,执行一次 red 函数,2秒执行一次 green 函数,1秒执行一次 yellow 函数,不断交替重复亮灯,意思就是按照这个顺序一直执行这3个函数,这步可以就利用递归来实现。

答案

var light = function (timmer, cb) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            cb();
            resolve();
        }, timmer);
    });
};

var step = function () {
    Promise.resolve().then(function () {
        return light(3000, red);
    }).then(function () {
        return light(2000, green);
    }).then(function () {
        return light(1000, yellow);
    }).then(function () {
        step();
    });
}

step();
oliver1204 commented 5 years ago

实现 mergePromise 函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组 data 中。

const timeout = ms => new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve();
    }, ms);
});

const ajax1 = () => timeout(2000).then(() => {
    console.log('1');
    return 1;
});

const ajax2 = () => timeout(1000).then(() => {
    console.log('2');
    return 2;
});

const ajax3 = () => timeout(2000).then(() => {
    console.log('3');
    return 3;
});

const mergePromise = ajaxArray => {
    // 在这里实现你的代码

};

mergePromise([ajax1, ajax2, ajax3]).then(data => {
    console.log('done');
    console.log(data); // data 为 [1, 2, 3]
});

// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]

解析

首先 ajax1 、ajax2、ajax3 都是函数,只是这些函数执行后会返回一个 Promise,按题目的要求我们只要顺序执行这三个函数就好了,然后把结果放到 data 中,但是这些函数里都是异步操作,想要按顺序执行,然后输出 1,2,3并没有那么简单,看个例子。

function A() {
    setTimeout(function () {
        console.log('a');
    }, 3000);
}

function B() {
    setTimeout(function () {
        console.log('b');
    }, 1000);
}

A();
B();

// b
// a

例子中我们是按顺序执行的 A,B 但是输出的结果却是 b,a 对于这些异步函数来说,并不会按顺序执行完一个,再执行后一个。 这道题就是考用 Promise 控制异步流程,我们要想办法,让这些函数,一个执行完之后,再执行下一个,看答案吧。

答案

// 保存数组中的函数执行后的结果
var data = [];

// Promise.resolve方法调用时不带参数,直接返回一个resolved状态的 Promise 对象。
var sequence = Promise.resolve();

ajaxArray.forEach(function (item) {
    // 第一次的 then 方法用来执行数组中的每个函数,
    // 第二次的 then 方法接受数组中的函数执行后返回的结果,
    // 并把结果添加到 data 中,然后把 data 返回。
    // 这里对 sequence 的重新赋值,其实是相当于延长了 Promise 链
    sequence = sequence.then(item).then(function (res) {
        data.push(res);
        return data;
    });
})

// 遍历结束后,返回一个 Promise,也就是 sequence, 他的 [[PromiseValue]] 值就是 data,
// 而 data(保存数组中的函数执行后的结果) 也会作为参数,传入下次调用的 then 方法中。
return sequence;
oliver1204 commented 5 years ago

Promise.resolve()
.then(() => {
     return new Error('error!!!')
})
.then((res) => {
    console.log('then: ', res)
})
.catch((err) => {
    console.log('catch: ', err)
})

上面的代码输出什么?

解析

.then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获,需要改成其中一种:

return Promise.reject(new Error('error!!!'))
throw new Error('error!!!')

因为返回任意一个非 promise 的值都会被包裹成 promise 对象,即 return new Error(‘error!!!’) 等价于 return Promise.resolve(new Error(‘error!!!’))。

答案

then:  Error: error!!!
    at <anonymous>:3:8
Promise {<resolved>: undefined}
oliver1204 commented 5 years ago

如果向Promise.all()和Promise.race()传递空数组,运行结果会有什么不同?

all会将传入的数组中的所有promise全部决议以后,将决议值以数组的形式传入到观察回调中,任何一个promise决议为拒绝,那么就会调用拒绝回调。

race会将传入的数组中的所有promise中第一个决议的决议值传递给观察回调,即使决议结果是拒绝。 all会立即决议,决议结果是fullfilled,值是undefined

race会永远都不决议,程序卡死……

oliver1204 commented 5 years ago

[Promise-Polyfill源码解析(1)] (https://juejin.im/post/5ba8a19d6fb9a05d0045aff5) [Promise-Polyfill源码解析(2)] (https://juejin.im/post/5bb49e8a5188255c9e02e5f3)

oliver1204 commented 5 years ago

async/await 函数

oliver1204 commented 5 years ago
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next();
hw.next();
hw.next();
hw.next();
// co 库

function co(it){
    return new Promise((resolve,reject)=>{
        // 异步迭代 next
        function next(data){
           let {value,done} = it.next(data);
           if(!done){
               Promise.resolve(value).then(data=>{
                  next(data)
               },reject);
           }else{
                resolve(value);
           }
        }
        next();
    });
}
co(read()).then(data=>{
    console.log(data);
});

发展趋势: // generator-runtime搜索 generator简单实现 // async + await 就是 generator + co 库 // 回调 -》 promise -》 generator -》 async + await