函數(shù)式編程與 JS 異步編程、手寫 Promise

Study Notes

本博主會持續(xù)更新各種前端的技術(shù),如果各位道友喜歡,可以關(guān)注、收藏、點贊下本博主的文章。

函數(shù)是一等公民(First-class Function)

特性

可將函數(shù)賦值給變量,即函數(shù)可存儲在變量中

函數(shù)可作為參數(shù)

函數(shù)可作為返回值

說明

在 JavaScript 中函數(shù)就是一個普通的對象 (可以通過 new Function() ),我們可以把函數(shù)存儲到變量/數(shù)組中,它還可以作為另一個函數(shù)的參數(shù)和返回值,甚至我們可以在程序運行的時候通過 new Function('alert(1)') 來構(gòu)造一個新的函數(shù)。

demo

// 可將函數(shù)賦值給變量,即函數(shù)可存儲在變量中
const foo = function () {
  console.log('foobar');
};
// 用變量來調(diào)用它
foo();
// 函數(shù)可作為參數(shù)
function sayHello() {
  return 'Hello, ';
}
function greeting(helloMessage, name) {
  console.log(helloMessage() + name);
}
// 傳遞 `sayHello` 作為 `greeting` 函數(shù)的參數(shù)
greeting(sayHello, 'JavaScript!'); // Hello, JavaScript!
// 函數(shù)可作為返回值
function sayHello() {
  return function () {
    console.log('Hello!');
  };
}

高階函數(shù)(Higher-order function)

特性

可以把函數(shù)作為參數(shù)傳遞給另一個函數(shù)

可以把函數(shù)作為另一個函數(shù)的返回結(jié)果

使用高階函數(shù)的意義

抽象可以幫我們屏蔽細節(jié),只需要關(guān)注我們的目標

高階函數(shù)是用來抽象通用的問題

demo

可以把函數(shù)作為參數(shù)傳遞給另一個函數(shù)

/**
 * forEach 遍歷數(shù)組
 * @param array {Array} 所需遍歷的數(shù)組
 * @param fn {Function} 返回值
 */
const forEach = (array, fn) => {
  for (let val of array) {
    fn(val);
  }
};

// 測試
let array = [1, 343, 5, 7, 8345, 8];
forEach(array, (val) => console.log(val));
/* 輸出
 * 1
 * 343
 * 5
 * 7
 * 8345
 * 8
 * */
/**
 * filter 數(shù)組過濾,并返回新的數(shù)組
 * @param array {Array} 所需過濾的數(shù)組
 * @param fn {Function} 過濾處理函數(shù)
 * @returns {Array} 返回值
 */
const filter = (array, fn) => {
  let result = [];
  for (let val of array) {
    if (fn(val)) {
      result.push(val);
    }
  }
  return result;
};
// 測試
let result = filter(array, (val) => val > 100);
console.log('filter:', result);
/* 輸出
 * filter: [ 343, 8345 ]
 * */

可以把函數(shù)作為參數(shù)傳遞給另一個函數(shù)

/**
 * makeFn 函數(shù)生成
 * @returns {function(): void}
 */
const makeFn = () => {
  const msg = 'Hello World';
  return () => console.log(msg);
};

// 測試
//調(diào)用方式一
const fn = makeFn();
fn();
// 調(diào)用方式二
// makeFn()();
/* 輸出
 * Hello World
 * */
/**
 * once 只執(zhí)行一次
 * @param fn {Function} 執(zhí)行函數(shù)
 * @returns {Function} 返回值
 */
const once = (fn) => {
  let done = false;
  // 因為下面使用this,這里切不可使用箭頭函數(shù),箭頭函數(shù)里this的指向是上下文里對象this指向,如果沒有上下文對象,this則指向window
  return function () {
    if (!done) {
      done = true;
      return fn.apply(this, arguments);
    }
  };
};
//測試
const pay = once((money) => {
  console.log(`支付:¥${money}`);
});
pay(100);
pay(100);
pay(100);
// 這里調(diào)用了三次函數(shù),但是結(jié)果只輸出一次,所以我們的once達到了預(yù)期效果
/* 輸出
 * 支付:¥100
 * */

模擬常用高階函數(shù) map、every、some

/**
 * map 遍歷數(shù)組,對其進行處理并返回新的數(shù)組
 * @param array {Array} 所需遍歷數(shù)組
 * @param fn {Function} 處理函數(shù)
 * @returns {Array} 返回值
 */
const map = (array, fn) => {
  let result = [];
  for (let val of array) {
    result.push(fn(val));
  }
  return result;
};

// 測試
let newArr = map(array, (val) => val * val);
console.log(newArr);
/* 輸出
 * [ 1, 117649, 25, 49, 69639025, 64 ]
 * */
/**
 * every 遍歷數(shù)組,判斷數(shù)組所有元素是否全部滿足指定條件,并返回結(jié)果
 * @param array {Array} 所需遍歷的數(shù)組
 * @param fn {Function} 指定條件函數(shù)
 * @returns {boolean} 返回值
 */
const every = (array, fn) => {
  let result = true;
  for (let val of array) {
    result = fn(val);
    if (!result) {
      break;
    }
  }
  return result;
};

// 測試
let result1 = every(array, (val) => val > 0);
let result2 = every(array, (val) => val > 1);
console.log(result1);
console.log(result2);
/* 輸出
 * true
 * false
 * */
/**
 * some 遍歷數(shù)組,判斷數(shù)組所有元素是否有滿足指定條件的元素,并返回結(jié)果
 * @param array {Array} 所需遍歷的數(shù)組
 * @param fn {Function} 指定條件函數(shù)
 * @returns {boolean} 返回值
 */
const some = (array, fn) => {
  let result = false;
  for (let val of array) {
    result = fn(val);
    if (result) {
      break;
    }
  }
  return result;
};
// 測試
let result3 = some(array, (val) => val > 0);
let result4 = some(array, (val) => val > 10000);
console.log(result3);
console.log(result4);
/* 輸出
 * true
 * false
 * */

閉包(Closure)

描述

閉包 (Closure):函數(shù)和其周圍的狀態(tài)(詞法環(huán)境)的引用捆綁在一起形成閉包。

可以在另一個作用域中調(diào)用一個函數(shù)的內(nèi)部函數(shù)并訪問到該函數(shù)的作用域中的成員

閉包的本質(zhì):函數(shù)在執(zhí)行的時候會放到一個執(zhí)行棧上當函數(shù)執(zhí)行完畢之后會從執(zhí)行棧上移除,但是堆上的作用域成員因為被外部引用不能釋放,因此內(nèi)部函數(shù)依然可以訪問外部函數(shù)的成員

瀏覽器調(diào)試工具使用

Call Stack 函數(shù)調(diào)用棧(在匿名函數(shù)中調(diào)用) 一個函數(shù)執(zhí)行后,會從函數(shù)調(diào)用棧中移除

Scope 作用域

demo

/**
 * makePower 生成冪數(shù)函數(shù)
 * @param power {Number} n次方
 * @returns {function(*=): number} 返回值
 */
const makePower = (power) => {
  return (number) => {
    return Math.pow(number, power);
  };
};
// 測試
const power2 = makePower(2);
const power3 = makePower(3);
console.log(power2(2));
console.log(power2(3));
console.log(power3(2));
/**
 * 輸出
 * 4
 * 9
 * 8
 */
/**
 * makeSalary 工資生成器
 * @param base {Number} 基本工資
 * @returns {function(*): *}
 */
const makeSalary = (base) => {
  return (performance) => {
    return base + performance;
  };
};
// 測試
const getSalaryLevel1 = makeSalary(10000);
const getSalaryLevel2 = makeSalary(12000);
console.log(getSalaryLevel1(2000));
console.log(getSalaryLevel1(3000));
console.log(getSalaryLevel2(2000));
/**
 * 輸出
 * 12000
 * 13000
 * 14000
 */

純函數(shù)(Pure Functions)

純函數(shù)概念

純函數(shù):相同的輸入永遠會得到相同的輸出,而且沒有任何可觀察的副作用

  • 純函數(shù)就類似數(shù)學(xué)中的函數(shù)(用來描述輸入和輸出之間的關(guān)系),y = f(x)

lodash 是一個一致性、模塊化、高性能的 JavaScript 實用工具庫(lodash 的 fp 模塊提供了對函數(shù)式編程友好的方法),提供了對數(shù)組、數(shù)字、對象、字符串、函數(shù)等操作的一些方法

數(shù)組的 slice 和 splice 分別是:純函數(shù)和不純的函數(shù)

  • slice 返回數(shù)組中的指定部分,不會改變原數(shù)組
  • splice 對數(shù)組進行操作返回該數(shù)組,會改變原數(shù)組
// 純函數(shù)
let array = [1, 2, 3, 4, 5];
console.log('slice: ', array.slice(0, 3));
console.log('slice: ', array.slice(0, 3));
console.log('slice: ', array.slice(0, 3));

// 不純的函數(shù)
console.log('splice: ', array.splice(0, 3));
console.log('splice: ', array.splice(0, 3));
console.log('splice: ', array.splice(0, 3));
// slice:  [ 1, 2, 3 ]
// slice:  [ 1, 2, 3 ]
// slice:  [ 1, 2, 3 ]
// splice:  [ 1, 2, 3 ]
// splice:  [ 4, 5 ]
// splice:  []

函數(shù)式編程不會保留計算中間的結(jié)果,所以變量是不可變的(無狀態(tài)的)

我們可以把一個函數(shù)的執(zhí)行結(jié)果交給另一個函數(shù)去處理

純函數(shù)的好處

可緩存

  • 因為純函數(shù)對相同的輸入始終有相同的結(jié)果,所以可以把純函數(shù)的結(jié)果緩存起來
const _ = require('lodash');
const add = (a, b) => {
  console.log(a, b);
  return a + b;
};

const result = _.memoize(add);
console.log(result(1, 2));
console.log(result(1, 2));
console.log(result(1, 2));

const memoize = (fn) => {
  let cache = {};
  // 箭頭函數(shù)沒有arguments,因為下面使用了arguments,所以這里不能使用箭頭函數(shù)
  return function () {
    let key = JSON.stringify(arguments);
    cache[key] = cache[key] || fn.apply(fn, arguments);
    return cache[key];
  };
};

const result1 = memoize(add);
console.log(result1(1, 2));
console.log(result1(1, 2));
console.log(result1(1, 2));
// 輸出結(jié)果 從結(jié)果可以看出來,參數(shù)只被打印一次,說明函數(shù)緩存成功
// 1 2
// 3
// 3
// 3
// 1 2
// 3
// 3
// 3

可測試

  • 純函數(shù)讓測試更方便

并行處理

  • 在多線程環(huán)境下并行操作共享的內(nèi)存數(shù)據(jù)很可能會出現(xiàn)意外情況
  • 純函數(shù)不需要訪問共享的內(nèi)存數(shù)據(jù),所以在并行環(huán)境下可以任意運行純函數(shù) (Web Worker)

副作用

// 不純的
let mini = 18;
function checkAge(age) {
  return age >= mini;
}
// 純的(有硬編碼,后續(xù)可以通過柯里化解決)
function checkAge(age) {
  let mini = 18;
  return age >= mini;
}

副作用讓一個函數(shù)變的不純(如上例),純函數(shù)的根據(jù)相同的輸入返回相同的輸出,如果函數(shù)依賴于外部的狀態(tài)就無法保證輸出相同,就會帶來副作用。

副作用來源:

  • 配置文件
  • 數(shù)據(jù)庫
  • 獲取用戶的輸入
  • ……

所有的外部交互都有可能帶來副作用,副作用也使得方法通用性下降不適合擴展和可重用性,同時副作用會給程序中帶來安全隱患給程序帶來不確定性,但是副作用不可能完全禁止,盡可能控制它們在可控范圍內(nèi)發(fā)生。

柯里化 (Haskell Brooks Curry)

柯里化 (Currying):

  • 當一個函數(shù)有多個參數(shù)的時候先傳遞一部分的參數(shù)調(diào)用它(這部分參數(shù)以后永遠不變)
  • 然后返回一個新的函數(shù)來接收剩余的參數(shù),返回結(jié)果

lodash 中的柯里化函數(shù)

_.curry(func)

  • 功能:創(chuàng)建一個函數(shù),該函數(shù)接收一個或多個 func 的參數(shù),如果 func 所需要的參數(shù)都被提供則執(zhí)行 func 并返回執(zhí)行的結(jié)果。否則繼續(xù)返回該函數(shù)并等待接收剩余的參數(shù)。
  • 參數(shù):需要柯里化的函數(shù)
  • 返回值:柯里化后的函數(shù)
// lodash 中的柯里化函數(shù)
const _ = require('lodash');
const getSum = (a, b, c) => {
  return a + b + c;
};
const curried = _.curry(getSum);
console.log(curried(1, 2, 3));
console.log(curried(1)(2, 3));
console.log(curried(1, 2)(3));
/**
 * 輸出結(jié)果
 * 6
 * 6
 * 6
 */
// 案例
const match = _.curry((res, str) => {
  return str.match(res);
});

const haveSpace = match(/\s+/g);
const haveNumber = match(/\d+/g);

const filter = _.curry((fun, array) => {
  return array.filter(fun);
});

const findSpace = filter(haveSpace);
const findNumber = filter(haveNumber);

console.log(haveSpace('ss ss'));
console.log(haveNumber('ss 12'));
console.log(findSpace(['ss12', 's ss']));
console.log(findNumber(['ss12', 'sss']));
/**
 * 輸出結(jié)果
 * [ ' ' ]
 * [ '12' ]
 * [ 's ss' ]
 * [ 'ss12' ]
 */

模擬 _.curry() 的實現(xiàn)

// 模擬 curry
const getSum = (a, b, c) => {
  return a + b + c;
};

const curry = (func) => {
  return (curryFn = (...args) => {
    if (args.length < func.length) {
      return function () {
        return curryFn(...args.concat(Array.from(arguments)));
      };
    }
    return func(...args);
  });
};

const curried = curry(getSum);
console.log(curried(1, 2, 3));
console.log(curried(1)(2, 3));
console.log(curried(1, 2)(3));

/**
 * 輸出結(jié)果
 * 6
 * 6
 * 6
 */

總結(jié)

柯里化可以讓我們給一個函數(shù)傳遞較少的參數(shù)得到一個已經(jīng)記住了某些固定參數(shù)的新函數(shù)

這是一種對函數(shù)參數(shù)的'緩存'

讓函數(shù)變的更靈活,讓函數(shù)的粒度更小

可以把多元函數(shù)轉(zhuǎn)換成一元函數(shù),可以組合使用函數(shù)產(chǎn)生強大的功能

函數(shù)組合(compose)

純函數(shù)和柯里化很容易寫出洋蔥代碼 h(g(f(x)))

函數(shù)組合可以讓我們把細粒度的函數(shù)重新組合生成一個新的函數(shù)

  • 獲取數(shù)組的最后一個元素再轉(zhuǎn)換成大寫字母, .toUpper(.first(_.reverse(array)))

函數(shù)組合

函數(shù)組合 (compose):如果一個函數(shù)要經(jīng)過多個函數(shù)處理才能得到最終值,這個時候可以把中間過程的函數(shù)合并成一個函數(shù)

  • 函數(shù)就像是數(shù)據(jù)的管道,函數(shù)組合就是把這些管道連接起來,讓數(shù)據(jù)穿過多個管道形成最終結(jié)果

  • 函數(shù)組合默認是從右到左執(zhí)行

lodash 中的組合函數(shù)

  • lodash 中組合函數(shù) flow() 或者 flowRight(),他們都可以組合多個函數(shù)
  • flow() 是從左到右運行
  • flowRight() 是從右到左運行,使用的更多一些
const _ = require('lodash');
// lodash 中組合函數(shù) flowRight(),將數(shù)組的最后一個元素轉(zhuǎn)換為大寫

/**
 * first 獲取字符串第一個字符或數(shù)組第一個元素
 * @param str {String || Array} 字符串或數(shù)組
 * @returns {*} 返回值
 */
const first = (str) => {
  return _.first(str);
};

/**
 * reverse 顛倒字符串
 * @param str {String} 字符串
 * @param isReverse {Boolean} 是否顛倒
 * @returns {*} 返回值
 */
const reverse = (str, isReverse) => {
  let array = str.split('');
  array = isReverse ? array.reverse() : array;
  return array;
};

/**
 * toUpperCase 將字母轉(zhuǎn)換為大寫
 * @param str {String} 字符串
 * @returns {string} 返回值
 */
const toUpperCase = (str) => {
  return str.toUpperCase();
};

const fn = _.flowRight(toUpperCase, first, reverse);

console.log(fn('abc', true));
/**
 * 輸出結(jié)果
 * C
 */

模擬實現(xiàn) lodash 的 flowRight 方法

// 模擬 flowRight
/**
 * compose 函數(shù)組合
 * @param args 參數(shù)
 * @returns {function(...[*]=): *}
 */
const compose = (...args) => {
  let first = false;
  return (...args1) => {
    return args.reverse().reduce((val, fn) => {
      if (!first) {
        first = true;
        return fn(...val);
      }
      return fn(val);
    }, args1);
  };
};

const fn = compose(toUpperCase, first, reverse);

console.log(fn('abc', false));
/**
 * 輸出結(jié)果
 * A
 */

函數(shù)的組合要滿足結(jié)合律 (associativity):

  • 我們既可以把 g 和 h 組合,還可以把 f 和 g 組合,結(jié)果都是一樣的
// 滿足結(jié)合律

const fn1 = compose(toUpperCase, first, reverse);
const fn2 = compose(compose(toUpperCase, first), reverse);
const fn3 = compose(toUpperCase, compose(first, reverse));

console.log(fn1('abc', true));
console.log(fn2('abc', true));
console.log(fn3('abc', true));
/**
 * 輸出結(jié)果
 * C
 * C
 * C
 */

lodash/fp

  • lodash 的 fp 模塊提供了實用的對函數(shù)式編程友好的方法

  • 提供了不可變 auto-curried iteratee-first data-last 的方法

Point Free

Point Free:我們可以把數(shù)據(jù)處理的過程定義成與數(shù)據(jù)無關(guān)的合成運算,不需要用到代表數(shù)據(jù)的那個參數(shù),只要把簡單的運算步驟合成到一起,在使用這種模式之前我們需要定義一些輔助的基本運算函數(shù)。

  • 不需要指明處理的數(shù)據(jù)

  • 只需要合成運算過程

  • 需要定義一些輔助的基本運算函數(shù)

demo

/// 非 Point Free 模式
// Hello World => hello_world
function f(word) {
  return word.toLowerCase().replace(/\s+/g, '_');
}
console.log(f('Hello World'));
// Point Free
const fp = require('lodash/fp');
const firstLetterToUpper = fp.flowRight(
  fp.join('. '),
  fp.map(fp.flowRight(fp.first, fp.toUpper)),
  fp.split(' '),
);
console.log(firstLetterToUpper('world wild web'));
// => W. W. W

函子(Functor)

概念

容器:包含值和值的變形關(guān)系(這個變形關(guān)系就是函數(shù))

函子:是一個特殊的容器,通過一個普通的對象來實現(xiàn),該對象具有 map 方法,map 方法可以運行一個函數(shù)對值進行處理(變形關(guān)系)

總結(jié)

函數(shù)式編程的運算不直接操作值,而是由函子完成

函子就是一個實現(xiàn)了 map 契約的對象

我們可以把函子想象成一個盒子,這個盒子里封裝了一個值

想要處理盒子中的值,我們需要給盒子的 map 方法傳遞一個處理值的函數(shù)(純函數(shù)),由這個函數(shù)來對值進行處理

最終 map 方法返回一個包含新值的盒子(函子)

demo

// Functor
class Container {
  /**
   * constructor 構(gòu)造函數(shù)
   * @param value 入?yún)?   */
  constructor(value) {
    this._value = value;
  }

  /**
   * of 實例化
   * @param value 入?yún)?   * @returns {Container} 返回Container對象
   */
  static of(value) {
    return new Container(value);
  }

  /**
   * map 方法
   * @param fn {function} 處理函數(shù)
   * @returns {Container} 返回Container對象
   */
  map(fn) {
    return Container.of(fn(this._value));
  }
}

let r = Container.of(5)
  .map((v) => v * v)
  .map((v) => v + 1);
console.log(r);
/**
 * 輸出結(jié)果
 * Container { _value: 26 }
 */

MayBe 函子

我們在編程的過程中可能會遇到很多錯誤,需要對這些錯誤做相應(yīng)的處理

MayBe 函子的作用就是可以對外部的空值情況做處理(控制副作用在允許的范圍)

demo

// MayBe 函子
class MayBe {
  /**
   * constructor 構(gòu)造函數(shù)
   * @param value 入?yún)?   */
  constructor(value) {
    this._value = value;
  }

  /**
   * of 實例化
   * @param value 入?yún)?   * @returns {MayBe} 返回MayBe對象
   */
  static of(value) {
    return new MayBe(value);
  }

  /**
   * map 方法
   * @param fn {function} 處理函數(shù)
   * @returns {MayBe} 返回MayBe對象
   */
  map(fn) {
    return this.isNothing() ? MayBe.of(this._value) : MayBe.of(fn(this._value));
  }

  /**
   * isNothing 判斷是否為null或undefined
   * @returns {boolean}
   */
  isNothing() {
    return this._value === null || this._value === undefined;
  }
}

let r = MayBe.of('abc')
  .map((val) => val.toUpperCase())
  .map((val) => val.split(''));

let r1 = MayBe.of(null)
  .map((val) => val.toUpperCase())
  .map((val) => val.split(''));

let r2 = MayBe.of(undefined)
  .map((val) => val.toUpperCase())
  .map((val) => val.split(''));

console.log(r);
console.log(r1);
console.log(r2);

/**
 * 輸出結(jié)果
 * MayBe { _value: [ 'A', 'B', 'C' ] }
 * MayBe { _value: null }
 * MayBe { _value: undefined }
 */

Either 函子

Either 兩者中的任何一個,類似于 if...else...的處理

異常會讓函數(shù)變的不純,Either 函子可以用來做異常處理

demo

// Either 函子
class Either {
  /**
   * constructor 構(gòu)造函數(shù)
   * @param value 入?yún)?   */
  constructor(value) {
    this._value = value;
  }

  /**
   * of 實例化
   * @param value 入?yún)?   * @returns {Either} 返回Either對象
   */
  static of(value) {
    return new Either(value);
  }

  /**
   * map 方法
   * @param fn {function} 處理函數(shù)
   * @returns {Either} 返回Either對象
   */
  map(fn) {
    return Either.of(fn(this._value));
  }
}

class Error extends Either {
  /**
   *
   * @param fn
   * @returns {Error} 返回Error對象的this
   */
  map(fn) {
    return this;
  }
}

/**
 * parseJson 解析字符串JSON
 * @param str {string} 入?yún)? * @returns {Either} 返回Either對象
 */
const parseJson = (str) => {
  try {
    return Either.of(JSON.parse(str));
  } catch (e) {
    return Error.of({ error: e.message });
  }
};

const r = parseJson('{"name": "zs"}');
const r1 = parseJson('{name: "zs"}');

console.log(r.map((val) => val.name.toUpperCase())); // 正常
console.log(r1); // 異常
/**
 * 輸出結(jié)果
 * Either { _value: 'ZS' }
 * Either { _value: { error: 'Unexpected token n in JSON at position 1' }}
 */

IO 函子

IO 函子中的 _value 是一個函數(shù),這里是把函數(shù)作為值來處理

IO 函子可以把不純的動作存儲到 _value 中,延遲執(zhí)行這個不純的操作(惰性執(zhí)行),包裝當前的操作純

把不純的操作交給調(diào)用者來處理

demo

// IO 函子
const _ = require('lodash');

class IO {
  /**
   * 構(gòu)造函數(shù)
   * @param fn {function} 函數(shù)
   */
  constructor(fn) {
    this._value = fn;
  }

  /**
   * 實例化
   * @param value
   * @returns {IO} IO對象
   */
  static of(value) {
    return new IO(function () {
      return value;
    });
  }

  /**
   * map 方法
   * @param fn {function} 處理函數(shù)
   * @returns {IO} 返回IO對象
   */
  map(fn) {
    return new IO(_.flowRight(fn, this._value));
  }
}

const r = IO.of(process).map((p) => p.execPath);

console.log(r._value());
/**
 * 輸出結(jié)果
 * D:\Development\nodeJS\node.exe
 */

IO 函子的問題

  • 嵌套函子時,需要多次獲取 value
/**
 * 讀取文件內(nèi)容
 * @param filename {string} 文件名
 * @returns {IO} 返回IO對象
 */
const readFile = (filename) => {
  return new IO(() => {
    return fs.readFileSync(filename, 'utf-8');
  });
};

/**
 * 打印
 * @param value {string} 入?yún)? * @returns {IO} 返回IO對象
 */
const print = (value) => {
  return new IO(() => {
    return value;
  });
};
/**
 * 讀取文件并打印
 */
const cat = _.flowRight(print, readFile);
// 因為這里是嵌套函子,所以這邊打印出來的是IO(IO())
console.log(cat('package.json'));
// 所以需要連續(xù)兩次獲取value才能取到值
console.log(cat('package.json')._value()._value());

Task 異步執(zhí)行

folktale 一個標準的函數(shù)式編程庫

  • 使用 folktale 里的 Task 函數(shù)實現(xiàn)異步

  • 使用 fs 里的 readFile 函數(shù),讀取文件內(nèi)容

// Task 異步執(zhí)行
const { task } = require('folktale/concurrency/task');
const fs = require('fs');

/**
 * 讀取文件內(nèi)容
 * @param filename {String} 文件名
 * @returns {*} 返回task對象
 */
const readFile = (filename) => {
  return task((resolve) => {
    fs.readFile(filename, 'utf-8', (err, data) => {
      if (err) resolve.reject(err);
      resolve.resolve(data);
    });
  });
};

readFile('package.json')
  .map((v) => JSON.parse(v))
  .run()
  .listen({
    onRejected: (err) => {
      console.log('異常', err);
    },
    onResolved: (data) => {
      console.log(data.version);
    },
  });

/**
 * 輸出結(jié)果
 * 1.0.0
 */

Monad 函子

Monad 函子是可以變扁的 Pointed 函子,IO(IO(x))

一個函子如果具有 join 和 of 兩個方法并遵守一些定律就是一個 Monad

// Monad 函子
const _ = require('lodash');
const fs = require('fs');

class IO {
  /**
   * 構(gòu)造函數(shù)
   * @param fn {function} 函數(shù)
   */
  constructor(fn) {
    this._value = fn;
  }

  /**
   * 實例化
   * @param value
   * @returns {IO} IO對象
   */
  static of(value) {
    return new IO(function () {
      return value;
    });
  }

  /**
   * map 方法
   * @param fn {function} 處理函數(shù)
   * @returns {IO} 返回IO對象
   */
  map(fn) {
    return new IO(_.flowRight(fn, this._value));
  }

  join() {
    return this._value();
  }

  flatMap(fn) {
    return this.map(fn).join();
  }
}

/**
 * 讀取文件內(nèi)容
 * @param filename {string} 文件名
 * @returns {IO} 返回IO對象
 */
const readFile = (filename) => {
  return new IO(() => {
    return fs.readFileSync(filename, 'utf-8');
  });
};

/**
 * 打印
 * @param value {string} 入?yún)? * @returns {IO} 返回IO對象
 */
const print = (value) => {
  return new IO(() => {
    return value;
  });
};
/**
 * 讀取文件并打印
 */
const cat = readFile('package.json')
  .map((v) => JSON.parse(v))
  .map((v) => v.version)
  .flatMap(print)
  .join();
console.log(cat);
/**
 * 輸出結(jié)果
 * 1.0.0
 */

異步模式

異步任務(wù)

異步任務(wù)是指不進入主線程,而進入任務(wù)隊列的任務(wù),只有任務(wù)隊列通知主線程,某個異步任務(wù)可以執(zhí)行了,該任務(wù)才會進入主線程,當我們打開網(wǎng)站時,像圖片的加載,音樂的加載,其實就是一個異步任務(wù)

/**
 * 任何函數(shù)的聲明和任何變量的聲明都不會壓入調(diào)用棧(Call Stack)
 * 將console.log(1)壓入調(diào)用棧執(zhí)行,執(zhí)行后移除
 * 將setTimeout壓入調(diào)用棧執(zhí)行,執(zhí)行setTimeout,因為setTimeout是異步,所以將a()壓入Web APIs后,從調(diào)用棧中移除,倒計時等待
 * 將console.log(5)壓入調(diào)用棧執(zhí)行,執(zhí)行后移除
 * 將setTimeout壓入調(diào)用棧執(zhí)行,執(zhí)行setTimeout,因為setTimeout是異步,所以將c()壓入Web APIs后,從調(diào)用棧中移除,倒計時等待
 * 將console.log(6)壓入調(diào)用棧執(zhí)行,執(zhí)行后移除
 * 1秒倒計時結(jié)束后,將a()壓入消息隊列,Event Loop(事件循環(huán))監(jiān)聽到后,將a()壓入調(diào)用棧
 * 將console.log(2)壓入調(diào)用棧執(zhí)行,執(zhí)行后移除
 * 將a()里的setTimeout壓入調(diào)用棧執(zhí)行,執(zhí)行setTimeout,因為setTimeout是異步,所以將b()壓入Web APIs后,從調(diào)用棧中移除,倒計時等待
 * 0.5秒倒計時結(jié)束后,將b()壓入消息隊列,Event Loop(事件循環(huán))監(jiān)聽到后,將b()壓入調(diào)用棧
 * 將console.log(3)壓入調(diào)用棧執(zhí)行,執(zhí)行后移除
 * 2秒倒計時結(jié)束后,將c()壓入消息隊列,Event Loop(事件循環(huán))監(jiān)聽到后,將c()壓入調(diào)用棧
 * 將console.log(4)壓入調(diào)用棧執(zhí)行,執(zhí)行后移除
 */

// 異步代碼
console.log(1);
setTimeout(
  (a = () => {
    console.log(2);
    setTimeout(
      (b = () => {
        console.log(3);
      }),
      500,
    );
  }),
  1000,
);
console.log(5);
setTimeout(
  (c = () => {
    console.log(4);
  }),
  2000,
);
console.log(6);

/**
 * 輸出結(jié)果打印
 * 1
 * 5
 * 6
 * 2
 * 3
 * 4
 */

同步模式

單線程

概念

JavaScript 是一門單線程的語言,因此,JavaScript 在同一個時間只能做一件事,單線程意味著,如果在同個時間有多個任務(wù)的話,這些任務(wù)就需要進行排隊,前一個任務(wù)執(zhí)行完,才會執(zhí)行下一個任務(wù)

使用單線程原因

JavaScript 的單線程,與它的用途是有很大關(guān)系,我們都知道,JavaScript 作為瀏覽器的腳本語言,主要用來實現(xiàn)與用戶的交互,利用 JavaScript,我們可以實現(xiàn)對 DOM 的各種各樣的操作,如果 JavaScript 是多線程的話,一個線程在一個 DOM 節(jié)點中增加內(nèi)容,另一個線程要刪除這個 DOM 節(jié)點,那么這個 DOM 節(jié)點究竟是要增加內(nèi)容還是刪除呢?這會帶來很復(fù)雜的同步問題,因此,JavaScript 是單線程的

同步任務(wù)

同步任務(wù)是指在主線程上排隊執(zhí)行的任務(wù),只有前一個任務(wù)執(zhí)行完畢,才能繼續(xù)執(zhí)行下一個任務(wù),當我們打開網(wǎng)站時,網(wǎng)站的渲染過程,比如元素的渲染,其實就是一個同步任務(wù)

// 同步代碼
/**
 * 任何函數(shù)的聲明和任何變量的聲明都不會壓入調(diào)用棧(Call Stack)
 * 將console.log(1)壓入調(diào)用棧執(zhí)行,執(zhí)行后移除
 * 將console.log(3)壓入調(diào)用棧執(zhí)行,執(zhí)行后移除
 * 將a()壓入調(diào)用棧執(zhí)行,執(zhí)行console.log(2),將console.log(2)壓入調(diào)用棧執(zhí)行,執(zhí)行后依次移除
 */

console.log(1);
const a = () => console.log(2);
console.log(3);
a();
/**
 * 輸出結(jié)果打印
 * 1
 * 3
 * 2
 */

副作用

因為 JavaScript 是單線程,因此同個時間只能處理同個任務(wù),所有任務(wù)都需要排隊,前一個任務(wù)執(zhí)行完,才能繼續(xù)執(zhí)行下一個任務(wù),但是,如果前一個任務(wù)的執(zhí)行時間很長,比如文件的讀取操作或 ajax 操作,后一個任務(wù)就不得不等著,拿 ajax 來說,當用戶向后臺獲取大量的數(shù)據(jù)時,不得不等到所有數(shù)據(jù)都獲取完畢才能進行下一步操作,用戶只能在那里干等著,嚴重影響用戶體驗

Promise

Promise 對象用于表示一個異步操作的最終完成 (或失敗), 及其結(jié)果值.

參數(shù)

executor

executor 是帶有 resolve 和 reject 兩個參數(shù)的函數(shù) 。Promise 構(gòu)造函數(shù)執(zhí)行時立即調(diào)用 executor 函數(shù), resolve 和 reject 兩個函數(shù)作為參數(shù)傳遞給 executor(executor 函數(shù)在 Promise 構(gòu)造函數(shù)返回所建 promise 實例對象前被調(diào)用)。resolve 和 reject 函數(shù)被調(diào)用時,分別將 promise 的狀態(tài)改為 fulfilled(完成)或 rejected(失敗)。executor 內(nèi)部通常會執(zhí)行一些異步操作,一旦異步操作執(zhí)行完畢(可能成功/失敗),要么調(diào)用 resolve 函數(shù)來將 promise 狀態(tài)改成 fulfilled,要么調(diào)用 reject 函數(shù)將 promise 的狀態(tài)改為 rejected。如果在 executor 函數(shù)中拋出一個錯誤,那么該 promise 狀態(tài)為 rejected。executor 函數(shù)的返回值被忽略。

描述

Promise 對象是一個代理對象(代理一個值),被代理的值在 Promise 對象創(chuàng)建時可能是未知的。它允許你為異步操作的成功和失敗分別綁定相應(yīng)的處理方法(handlers)。 這讓異步方法可以像同步方法那樣返回值,但并不是立即返回最終執(zhí)行結(jié)果,而是一個能代表未來出現(xiàn)的結(jié)果的 promise 對象

一個 Promise 有以下幾種狀態(tài):

  • pending: 初始狀態(tài),既不是成功,也不是失敗狀態(tài)。
  • fulfilled: 意味著操作成功完成。
  • rejected: 意味著操作失敗。

pending 狀態(tài)的 Promise 對象可能會變?yōu)?fulfilled 狀態(tài)并傳遞一個值給相應(yīng)的狀態(tài)處理方法,也可能變?yōu)槭顟B(tài)(rejected)并傳遞失敗信息。當其中任一種情況出現(xiàn)時,Promise 對象的 then 方法綁定的處理方法(handlers )就會被調(diào)用(then 方法包含兩個參數(shù):onfulfilled 和 onrejected,它們都是 Function 類型。當 Promise 狀態(tài)為 fulfilled 時,調(diào)用 then 的 onfulfilled 方法,當 Promise 狀態(tài)為 rejected 時,調(diào)用 then 的 onrejected 方法, 所以在異步操作的完成和綁定處理方法之間不存在競爭)。

因為 Promise.prototype.then 和 Promise.prototype.catch 方法返回 promise 對象, 所以它們可以被鏈式調(diào)用。

demo

// Promise基本使用
let promise = new Promise((resolve, reject) => {
  // resolve('成功');
  reject('失敗');
});

promise.then(
  (res) => {
    console.log(res);
  },
  (e) => {
    console.log(e);
  },
);
const Ajax = (url) => {
  // promise方式Ajax使用
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open('get', url);
    xhr.responseType = 'json';
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    xhr.send();
  });
};
Ajax('fed-e-task-01-01/notes/promise/api/user.json').then(
  (value) => {
    console.log(value);
  },
  (e) => {
    console.log(e);
  },
);
// promise鏈式調(diào)用

Ajax('fed-e-task-01-01/notes/promise/api/user.json')
  .then((value) => {
    console.log(value);
    return Ajax('fed-e-task-01-01/notes/promise/api/class.json');
  })
  .then((value) => {
    console.log(value);
    return 'abc';
  })
  .then((value) => console.log(value));

promise 靜態(tài)方法使用

  • all 方法返回一個 Promise 實例,此實例在 iterable 參數(shù)內(nèi)所有的 promise 都“完成(resolved)”或參數(shù)中不包含 promise 時回調(diào)完成(resolve);如果參數(shù)中 promise 有一個失敗(rejected),此實例回調(diào)失敗(reject),失敗的原因是第一個失敗 promise 的結(jié)果
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});

/**
 * 輸出結(jié)果
 * [3, 42, "foo"]
 */
  • race 返回一個 promise,一旦迭代器中的某個 promise 解析或拒絕,返回的 promise 就會解析或拒絕。
const promise4 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise5 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise4, promise5]).then((value) => {
  console.log(value);
  // 都解析了,但是promise2更快
});

/**
 * 輸出結(jié)果
 * two
 */
  • reject 方法返回一個帶有拒絕原因的 Promise 對象
Promise.reject(new Error('fail')).catch((e) => console.error(e.message()));

/**
 * 輸出結(jié)果
 * fail
 */
  • resolve 方法返回一個以給定值解析后的 Promise 對象。如果這個值是一個 promise ,那么將返回這個 promise ;如果這個值是 thenable(即帶有"then" 方法),返回的 promise 會“跟隨”這個 thenable 的對象,采用它的最終狀態(tài);否則返回的 promise 將以此值完成。此函數(shù)將類 promise 對象的多層嵌套展平
Promise.resolve('success').then((res) => console.log(res));

/**
 * 輸出結(jié)果
 * success
 */

Event Loop(事件循環(huán))

Event Loop 它最主要是分三部分:主線程、宏任務(wù)(macrotask)、微任務(wù)(microtask)

宏任務(wù)

宏任務(wù),macrotask,也叫 tasks。 一些異步任務(wù)的回調(diào)會依次進入 macro task queue,等待后續(xù)被調(diào)用,這些異步任務(wù)包括:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI rendering

微任務(wù)

微任務(wù),microtask,也叫 jobs。 另一些異步任務(wù)的回調(diào)會依次進入 micro task queue,等待后續(xù)被調(diào)用,這些異步任務(wù)包括:

  • Promise
  • Object.observe
  • MutationObserver
  • process.nextTick

執(zhí)行順序

主線程 > 微任務(wù) > 宏任務(wù)

Generator(生成器)

生成器對象是由一個 generator function 返回的,并且它符合可迭代協(xié)議和迭代器協(xié)議。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

let g = gen();
// "Generator { }"

方法

Generator.prototype.next()

  • 返回一個由 yield 表達式生成的值。
const arr = [1, 2, 3];
function* idMaker() {
  for (let val of arr) {
    yield val;
  }
}

let gen = idMaker(); // "Generator { }"

console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
/**
 * 輸出結(jié)果
 * 1
 * 2
 * 3
 */

Generator.prototype.return()

  • 返回給定的值并結(jié)束生成器。
const arr = [1, 2, 3];
function* idMaker() {
  for (let val of arr) {
    yield val;
  }
}

let gen = idMaker(); // "Generator { }"

console.log(gen.next().value);
console.log(gen.return('結(jié)束').value);
console.log(gen.next().value);
console.log(gen.next().value);
/**
 * 輸出結(jié)果
 * 1
 * Uncaught Error: error
 * undefined
 * undefined
 */

Generator.prototype.throw()

  • 向生成器拋出一個錯誤。
const arr = [1, 2, 3];
function* idMaker() {
  for (let val of arr) {
    yield val;
  }
}

let gen = idMaker(); // "Generator { }"

console.log(gen.next().value);
// 阻止生成器函數(shù)往下運行,并拋出異常
gen.throw(new Error('error'));
console.log(gen.next().value);
console.log(gen.next().value);
/**
 * 輸出結(jié)果
 * 1
 * Uncaught Error: error
 */

demo

// generator配合promise使用
const Ajax = (url) => {
  // promise方式Ajax使用
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open('get', url);
    xhr.responseType = 'json';
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    xhr.send();
  });
};
function* main() {
  try {
    const result = yield Ajax('fed-e-task-01-01/notes/promise/api/user.json');
    console.log(result);
    const result1 = yield Ajax('fed-e-task-01-01/notes/promise/api/class.json');
    console.log(result1);
  } catch (e) {
    console.error(e);
  }
}

const co = (generator) => {
  const g = generator();
  function handelResult(result) {
    if (result.done) return;
    result.value.then(
      (res) => {
        handelResult(g.next(res));
      },
      (e) => {
        handelResult(g.throw(e));
      },
    );
  }
  handelResult(g.next());
};

co(main);

Async 函數(shù)

async function 用來定義一個返回 AsyncFunction 對象的異步函數(shù)。異步函數(shù)是指通過事件循環(huán)異步執(zhí)行的函數(shù),它會通過一個隱式的 Promise 返回其結(jié)果。如果你在代碼中使用了異步函數(shù),就會發(fā)現(xiàn)它的語法和結(jié)構(gòu)會更像是標準的同步函數(shù)。

語法

async function name([param[, param[, ... param]]]) { statements }

描述

一個 async 異步函數(shù)可以包含 await 指令,該指令會暫停異步函數(shù)的執(zhí)行,并等待 Promise 執(zhí)行,然后繼續(xù)執(zhí)行異步函數(shù),并返回結(jié)果。

記住,await 關(guān)鍵字只在異步函數(shù)內(nèi)有效。如果你在異步函數(shù)外使用它,會拋出語法錯誤。

注意,當異步函數(shù)暫停時,它調(diào)用的函數(shù)會繼續(xù)執(zhí)行(收到異步函數(shù)返回的隱式 Promise)

const Ajax = (url) => {
  // promise方式Ajax使用
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open('get', url);
    xhr.responseType = 'json';
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    xhr.send();
  });
};
async function main() {
  try {
    const result = await Ajax('fed-e-task-01-01/notes/promise/api/user.json');
    console.log(result);
    const result1 = await Ajax('fed-e-task-01-01/notes/promise/api/class.json');
    console.log(result1);
  } catch (e) {
    console.error(e);
  }
}

main();

Promise

描述

  1. Promise 是一個類,參數(shù)是一個執(zhí)行器函數(shù), 執(zhí)行器函數(shù)自執(zhí)行。

  2. Promise 有 3 個狀態(tài) Pending 默認等待態(tài) 、Fulfilled 成功態(tài) 、Rejected 失敗態(tài) 狀態(tài)一改變就不能再次修改 Pending -> Fulfilled || Pending -> Rejected

  3. 執(zhí)行器函數(shù)參數(shù)有 resolve 方法和 reject 方法 resolve 方法將 Pending 3.> Fulfilled reject 方法將 Rejected -> Rejected resolve 方法的參數(shù)將作為 then 方法成功回調(diào)的值, reject 方法的參數(shù)將作為 then 方法失敗回調(diào)的原因。

  4. then 方法有兩個參數(shù) 一個是成功回調(diào)的函數(shù) successCallback,一個是失敗回調(diào)的函數(shù) failCallback。 promise 的成功態(tài)會將成功的值傳給成功的回調(diào)并且執(zhí)行,失敗態(tài)會將失敗的原因傳遞給失敗的回調(diào)并執(zhí)行。

  5. 執(zhí)行器中 resolve 和 reject 在異步中執(zhí)行的時候,當前狀態(tài)還是等待態(tài) 需要把 then 方法的成功回調(diào)和失敗回調(diào)存起來,等異步調(diào)用 resolve 的時候調(diào)用成功回調(diào),reject 的時候調(diào)用失敗回調(diào)

  6. then 方法

    a. then 方法多次調(diào)用添加多個處理函數(shù);

    b. 實現(xiàn) then 的鏈式調(diào)用: then 方法鏈式調(diào)用識別 promise 自返回 then 鏈式調(diào)用 返回的是一個新的 promise 對象

    c. 判斷 then 方法成功回調(diào)和失敗回調(diào)的返回值 x x 返回的是一個 pormise,判斷 x 和當前 then 返回是不是同一個 promise,如果是同一個 promise 就報錯。x 和 then 返回的不是同一個 promise,將 x 的 then 方法執(zhí)行返回給下一個 then。 x 是常量直接當作下一個 then 的成功回調(diào)的參數(shù)。后面 then 方法的回調(diào)函數(shù)拿到值的是上一個 then 方法的回調(diào)函數(shù)的返回值。

    d.捕獲錯誤及 then 鏈式調(diào)用其他狀態(tài)代碼補充

    e. then 方法的參數(shù)可選 ?

    f. then 方法的值穿透

  7. executor 執(zhí)行器函數(shù) / then 方法可能有錯誤,有錯誤直接調(diào)用 reject 方法

  8. all 和 race 方法的實現(xiàn)

    all 和 race 方法不管 promise 成功還是失敗都不會影響其他 promise 執(zhí)行 ? all 方法返回一個新的 promise? all 參數(shù)里面每一項執(zhí)行完成,才把所有結(jié)果依次按原順序 resolve 出去 ? all 方法只要一個 promise 失敗就 reject 失敗的 promise 結(jié)果 ? Promise.race 方法,誰執(zhí)行的快就返回誰

  9. resolve 和 reject 方法的實現(xiàn)

  10. resolve 方法: 相當于實例化 Promise 對象,并且調(diào)用了 resolve 方法 reject 方法:相當于實例化 Promise 對象,并且調(diào)用了 reject 方法

  11. finally 方法的實現(xiàn)

  12. finally 的實現(xiàn),不管是成功狀態(tài)還是失敗態(tài)都會進這個 finally 方法 (等待態(tài)不會進)。finally 方法會返回一個新的 promise,它拿不到上次 then 執(zhí)行的結(jié)果(所以沒有參數(shù)),內(nèi)部會手動執(zhí)行一次 promise 的 then 方法。finally 方法有錯誤會把錯誤作為下次 then 方法的失敗回調(diào)的參數(shù)。

  13. catch 方法的實現(xiàn)

    catch 方法相當于執(zhí)行 then 方法的失敗回調(diào)

demo

自定義 promise

class Promise {
  /**
   * 構(gòu)造函數(shù)
   * @param executor {Function} 執(zhí)行器
   */
  constructor(executor) {
    /**
     * 初始狀態(tài),既不是成功,也不是失敗狀態(tài)
     * @type {string}
     */
    this.PENDING = 'pending';
    /**
     * 意味著操作成功完成
     * @type {string}
     */
    this.FULFILLED = 'fulfilled';
    /**
     * 意味著操作失敗
     * @type {string}
     */
    this.REJECTED = 'rejected';

    /**
     * Promise 狀態(tài)
     * @type {string} 默認值PENDING
     */
    this.status = this.PENDING;
    /**
     * 成功回調(diào)函數(shù)入?yún)⒌闹?     * @type {string} 默認值undefined
     */
    this.value = undefined;
    /**
     * 失敗回調(diào)函數(shù)入?yún)⒌闹?     * @type {string} 默認值undefined
     */
    this.reason = undefined;
    /**
     * successCallback 成功回調(diào)函數(shù)
     * @type {Array} 默認值[]
     */
    this.successCallback = [];
    /**
     * failCallback 失敗回調(diào)函數(shù)
     * @type {Array} 默認值[]
     */
    this.failCallback = [];

    /**
     * privateResolve 解析
     * @param value 成功回調(diào)的入?yún)?     */
    this.privateResolve = (value) => {
      // 在promise的狀態(tài)為PENDING時,允許改變其狀態(tài)
      if (this.status === this.PENDING) {
        // 觸發(fā)resolve,將promise的狀態(tài)改為fulfilled(完成)
        this.status = this.FULFILLED;
        // 將resolve傳遞的參數(shù),保存在Promise對象的value里
        this.value = value;
        // 如果successCallback成功回調(diào)函數(shù)存在,則執(zhí)行
        this.successCallback.forEach((callback) => {
          callback();
        });
      }
    };

    /**
     * privateReject 駁回
     * @param reason 失敗回調(diào)入?yún)?     */
    this.privateReject = (reason) => {
      // 在promise的狀態(tài)為PENDING時,允許改變其狀態(tài)
      if (this.status === this.PENDING) {
        // 觸發(fā)reject,將promise的狀態(tài)改為rejected(失敗)
        this.status = this.REJECTED;
        // 將reject傳遞的參數(shù),保存在MyPromise對象的reason里
        this.reason = reason;
        // 如果failCallback失敗回調(diào)函數(shù)存在,則執(zhí)行
        this.failCallback.forEach((callback) => {
          callback();
        });
      }
    };
    // 捕獲執(zhí)行器異常,并從reject拋出
    try {
      executor(this.privateResolve, this.privateReject);
    } catch (e) {
      this.privateReject(e);
    }
  }

  then(successCallback, failCallback) {
    // 判斷successCallback和failCallback是否存在,不存在將值向下傳遞
    successCallback = successCallback ? successCallback : (value) => value;
    // failCallback = failCallback ? failCallback : (reason) => reason;
    failCallback = failCallback
      ? failCallback
      : (reason) => {
          throw reason;
        };
    // 鏈式調(diào)用實現(xiàn),返回Promise函數(shù)對象
    let promise = new Promise((resolve, reject) => {
      // 判斷promise的狀態(tài),當其狀態(tài)為FULFILLED時,觸發(fā)成功回調(diào)函數(shù);當其狀態(tài)為REJECTED時,觸發(fā)成功回調(diào)函數(shù)
      // 后面的 then 方法的回調(diào)函數(shù)的入?yún)⑷∽陨弦粋€ then 方法的回調(diào)函數(shù)的返回值
      // promise對象未生成,所以將resolvePromise放到異步里調(diào)用,等待promise對象未生成后執(zhí)行
      if (this.status === this.FULFILLED) {
        // 捕獲異常,并從reject拋出
        setTimeout(() => {
          try {
            resolvePromise(
              promise,
              successCallback(this.value),
              resolve,
              reject,
            );
          } catch (e) {
            reject(e);
          }
        }, 0);
      } else if (this.status === this.REJECTED) {
        // 捕獲異常,并從reject拋出
        setTimeout(() => {
          try {
            resolvePromise(promise, failCallback(this.reason), resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      } else {
        // 當判斷promise的狀態(tài),當其狀態(tài)為PENDING時,存儲回調(diào)函數(shù)
        this.successCallback.push(() => {
          // 捕獲異常,并從reject拋出
          setTimeout(() => {
            try {
              resolvePromise(
                promise,
                successCallback(this.value),
                resolve,
                reject,
              );
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
        this.failCallback.push(() => {
          // 捕獲異常,并從reject拋出
          setTimeout(() => {
            try {
              resolvePromise(
                promise,
                failCallback(this.reason),
                resolve,
                reject,
              );
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
      }
    });
    return promise;
  }

  /**
   * finally 等待Promise對象執(zhí)行完所有任務(wù),并執(zhí)行
   * @param callback 回調(diào)函數(shù)
   * @returns {Promise} 返回Promise對象
   */
  finally(callback) {
    return this.then(
      (value) => {
        return Promise.resolve(callback()).then(() => value);
      },
      (reason) => {
        return Promise.resolve(callback()).then(() => {
          throw reason;
        });
      },
    );
  }

  /**
   * catch 返回失敗回調(diào)函數(shù)
   * @param callback 回調(diào)函數(shù)
   * @returns {Promise} 返回Promise對象
   */
  catch(callback) {
    return this.then(undefined, callback);
  }

  /**
   * 等待所有Promise對象執(zhí)行完畢,并返回結(jié)果
   * @param array {Array} 數(shù)組對象
   * @returns {Promise} 返回Promise對象
   */
  static all(array) {
    // 返回數(shù)組
    let result = [];
    // 計數(shù)器
    let index = 0;
    return new Promise((resolve, reject) => {
      /**
       * addResult 向result數(shù)組添加返回值
       * @param key {Number} key值
       * @param value {Object} value值
       */
      const addResult = (key, value) => {
        result[key] = value;
        index++;
        // 判斷所有異步操作完成后,執(zhí)行resolve
        if (array.length === index) {
          resolve(result);
        }
      };
      // 遍歷
      array.forEach((val, i) => {
        // 判斷當前值,是否屬于Promise對象
        if (val instanceof Promise) {
          // Promise對象
          // 執(zhí)行成功回調(diào)函數(shù),獲取返回值,并添加到result數(shù)組里
          // 執(zhí)行失敗回調(diào)函數(shù),直接通過reject拋出失敗原因
          val.then(
            (value) => addResult(i, value),
            (reason) => reject(reason),
          );
        } else {
          // 普通對象,將當前值添加到result數(shù)組里
          addResult(i, val);
        }
      });
    });
  }

  /**
   * resolve 解析
   * @param value
   * @returns {Promise}
   */
  static resolve(value) {
    // 判斷value是否是Promise對象,是則直接返回,否則創(chuàng)建Promise對象并返回
    if (value instanceof Promise) return value;
    return new Promise((resolve) => resolve(value));
  }

  /**
   * reject 駁回
   * @param value
   * @returns {Promise}
   */
  static reject(value) {
    // 判斷value是否是Promise對象,是則直接返回,否則創(chuàng)建Promise對象并返回
    if (value instanceof Promise) return value;
    return new Promise((undefined, reject) => reject(value));
  }

  /**
   * 返回一個 promise,一旦迭代器中的某個 promise 解析或拒絕,返回的 promise 就會解析或拒絕。
   * @param array {Array} 數(shù)組對象
   * @returns {Promise} 返回Promise對象
   */
  static race(array) {
    return new Promise((resolve, reject) => {
      // 遍歷
      array.forEach((val, i) => {
        // 判斷當前值,是否屬于Promise對象
        if (val instanceof Promise) {
          // Promise對象
          // 執(zhí)行成功回調(diào)函數(shù),獲取返回值,并通過resolve彈出
          // 執(zhí)行失敗回調(diào)函數(shù),直接通過reject拋出失敗原因
          val.then(
            (value) => resolve(value),
            (reason) => reject(reason),
          );
        } else {
          // 普通對象,將當前值通過resolve彈出
          resolve(val);
        }
      });
    });
  }
}

/**
 * resolvePromise 解析Promise
 * @param promise promise對象
 * @param result 上一個then回調(diào)函數(shù)返回值
 * @param resolve 解析函數(shù)
 * @param reject 駁回函數(shù)
 */
const resolvePromise = (promise, result, resolve, reject) => {
  // 判斷promise與result是否相等,如果相等,則是promise循環(huán)調(diào)用,這里應(yīng)該拋出異常,并阻止往下執(zhí)行
  if (promise === result) {
    return reject(new TypeError('Chaining cycle detected for my-promise'));
  }
  // 判斷result是普通對象還是屬于Promise對象
  if (result instanceof Promise) {
    // 查看Promise對象返回結(jié)果,調(diào)用對應(yīng)的resolve或reject
    result.then(resolve, reject);
  } else {
    resolve(result);
  }
};

module.exports = Promise;

調(diào)用

const Promise = require('./promise');

const Ajax = (url) => {
  // promise方式Ajax使用
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open('get', url);
    xhr.responseType = 'json';
    xhr.onload = function () {
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    xhr.send();
  });
};
// Promise基本使用
let promise = new Promise((resolve, reject) => {
  // resolve('成功');
  reject('失敗');
});

promise
  .then((res) => {
    console.log(res);
  })
  .catch((e) => console.error(e));
// promise方式Ajax使用
Ajax('fed-e-task-01-01/notes/promise/api/user.json').then(
  (value) => {
    console.log(value);
  },
  (e) => {
    console.error(e);
  },
);

// promise鏈式調(diào)用
Ajax('fed-e-task-01-01/notes/promise/api/user.json')
  .then((value) => {
    console.log(value);
    return Ajax('fed-e-task-01-01/notes/promise/api/class.json');
  })
  .then((value) => {
    console.log(value);
    return 'abc';
  })
  .then((value) => console.log(value));

// promise靜態(tài)方法使用
// all  方法返回一個 Promise 實例,此實例在 iterable 參數(shù)內(nèi)所有的 promise 都“完成(resolved)”或參數(shù)中不包含 promise 時回調(diào)完成(resolve);如果參數(shù)中  promise 有一個失敗(rejected),此實例回調(diào)失敗(reject),失敗的原因是第一個失敗 promise 的結(jié)果
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
/**
 * 輸出結(jié)果
 * [3, 42, "foo"]
 */

// race返回一個 promise,一旦迭代器中的某個promise解析或拒絕,返回的 promise就會解析或拒絕。
const promise4 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise5 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise4, promise5]).then((value) => {
  console.log(value);
  // Both resolve, but promise5 is faster
});

/**
 * 輸出結(jié)果
 * two
 */

// reject方法返回一個帶有拒絕原因的Promise對象

Promise.reject(new Error('fail')).catch((e) => console.error(e.message));
/**
 * 輸出結(jié)果
 * fail
 */
// resolve方法返回一個以給定值解析后的Promise 對象。如果這個值是一個 promise ,那么將返回這個 promise ;如果這個值是thenable(即帶有"then" 方法),返回的promise會“跟隨”這個thenable的對象,采用它的最終狀態(tài);否則返回的promise將以此值完成。此函數(shù)將類promise對象的多層嵌套展平
Promise.resolve('success').then((res) => console.log(res));
/**
 * 輸出結(jié)果
 * success
 */
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375