函數語言程式設計與 JS 非同步程式設計、手寫 Promise

2020-08-09 02:46:40

Study Notes

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

特性

可將函數賦值給變數,即函數可儲存在變數中

函數可作爲參數

函數可作爲返回值

說明

在 JavaScript 中函數就是一個普通的物件 (可以通過 new Function() ),我們可以把函數儲存到變數/陣列中,它還可以作爲另一個函數的參數和返回值,甚至我們可以在程式執行的時候通過 new Function(‘alert(1)’) 來構造一個新的函數。

demo

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

高階函數(Higher-order function)

特性

可以把函數作爲參數傳遞給另一個函數

可以把函數作爲另一個函數的返回結果

使用高階函數的意義

抽象可以幫我們遮蔽細節,只需要關注我們的目標

高階函數是用來抽象通用的問題

demo

可以把函數作爲參數傳遞給另一個函數

/**
 * forEach 遍歷陣列
 * @param array {Array} 所需遍歷的陣列
 * @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 陣列過濾,並返回新的陣列
 * @param array {Array} 所需過濾的陣列
 * @param fn {Function} 過濾處理常式
 * @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 ]
 * */

可以把函數作爲參數傳遞給另一個函數

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

// 測試
//呼叫方式一
const fn = makeFn();
fn();
// 呼叫方式二
// makeFn()();
/* 輸出
 * Hello World
 * */
/**
 * once 只執行一次
 * @param fn {Function} 執行函數
 * @returns {Function} 返回值
 */
const once = (fn) => {
  let done = false;
  // 因爲下面 下麪使用this,這裏切不可使用箭頭函數,箭頭函數裡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);
// 這裏呼叫了三次函數,但是結果只輸出一次,所以我們的once達到了預期效果
/* 輸出
 * 支付:¥100
 * */

模擬常用高階函數 map、every、some

/**
 * map 遍歷陣列,對其進行處理並返回新的陣列
 * @param array {Array} 所需遍歷陣列
 * @param fn {Function} 處理常式
 * @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 遍歷陣列,判斷陣列所有元素是否全部滿足指定條件,並返回結果
 * @param array {Array} 所需遍歷的陣列
 * @param fn {Function} 指定條件函數
 * @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 遍歷陣列,判斷陣列所有元素是否有滿足指定條件的元素,並返回結果
 * @param array {Array} 所需遍歷的陣列
 * @param fn {Function} 指定條件函數
 * @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):函數和其周圍的狀態(詞法環境)的參照捆綁在一起形成閉包。

可以在另一個作用域中呼叫一個函數的內部函數並存取到該函數的作用域中的成員

閉包的本質:函數在執行的時候會放到一個執行棧上當函數執行完畢之後會從執行棧上移除,但是堆上的作用域成員因爲被外部參照不能釋放,因此內部函數依然可以存取外部函數的成員

瀏覽器偵錯工具使用

Call Stack 函數呼叫棧(在匿名函數中呼叫) 一個函數執行後,會從函數呼叫棧中移除

Scope 作用域

demo

/**
 * makePower 生成冪數函數
 * @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
 */

純函數(Pure Functions)

純函數概念

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

  • 純函數就類似數學中的函數(用來描述輸入和輸出之間的關係),y = f(x)

lodash 是一個一致性、模組化、高效能的 JavaScript 實用工具庫(lodash 的 fp 模組提供了對函數語言程式設計友好的方法),提供了對陣列、數位、物件、字串、函數等操作的一些方法

陣列的 slice 和 splice 分別是:純函數和不純的函數

  • slice 返回陣列中的指定部分,不會改變原陣列
  • splice 對陣列進行操作返回該陣列,會改變原陣列
// 純函數
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));

// 不純的函數
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:  []

函數語言程式設計不會保留計算中間的結果,所以變數是不可變的(無狀態的)

我們可以把一個函數的執行結果交給另一個函數去處理

純函數的好處

可快取

  • 因爲純函數對相同的輸入始終有相同的結果,所以可以把純函數的結果快取起來
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 = {};
  // 箭頭函數沒有arguments,因爲下面 下麪使用了arguments,所以這裏不能使用箭頭函數
  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));
// 輸出結果 從結果可以看出來,參數只被列印一次,說明函數快取成功
// 1 2
// 3
// 3
// 3
// 1 2
// 3
// 3
// 3

可測試

  • 純函數讓測試更方便

並行處理

  • 在多執行緒環境下並行操作共用的記憶體數據很可能會出現意外情況
  • 純函數不需要存取共用的記憶體數據,所以在並行環境下可以任意執行純函數 (Web Worker)

副作用

// 不純的
let mini = 18;
function checkAge(age) {
  return age >= mini;
}
// 純的(有寫死,後續可以通過柯裡化解決)
function checkAge(age) {
  let mini = 18;
  return age >= mini;
}

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

副作用來源:

  • 組態檔
  • 數據庫
  • 獲取使用者的輸入
  • ……

所有的外部互動都有可能帶來副作用,副作用也使得方法通用性下降不適合擴充套件和可重用性,同時副作用會給程式中帶來安全隱患給程式帶來不確定性,但是副作用不可能完全禁止,儘可能控制它們在可控範圍內發生。

柯裡化 (Haskell Brooks Curry)

柯裡化 (Currying):

  • 當一個函數有多個參數的時候先傳遞一部分的參數呼叫它(這部分參數以後永遠不變)
  • 然後返回一個新的函數來接收剩餘的參數,返回結果

lodash 中的柯裡化函數

_.curry(func)

  • 功能:建立一個函數,該函數接收一個或多個 func 的參數,如果 func 所需要的參數都被提供則執行 func 並返回執行的結果。否則繼續返回該函數並等待接收剩餘的參數。
  • 參數:需要柯裡化的函數
  • 返回值:柯裡化後的函數
// lodash 中的柯裡化函數
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));
/**
 * 輸出結果
 * 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']));
/**
 * 輸出結果
 * [ ' ' ]
 * [ '12' ]
 * [ 's ss' ]
 * [ 'ss12' ]
 */

模擬 _.curry() 的實現

// 模擬 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));

/**
 * 輸出結果
 * 6
 * 6
 * 6
 */

總結

柯裡化可以讓我們給一個函數傳遞較少的參數得到一個已經記住了某些固定參數的新函數

這是一種對函數參數的’快取’

讓函數變的更靈活,讓函數的粒度更小

可以把多元函數轉換成一元函數,可以組合使用函數產生強大的功能

函數組合(compose)

純函數和柯裡化很容易寫出洋蔥程式碼 h(g(f(x)))

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

  • 獲取陣列的最後一個元素再轉換成大寫字母, .toUpper(.first(_.reverse(array)))

函數組合

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

  • 函數就像是數據的管道,函數組合就是把這些管道連線起來,讓數據穿過多個管道形成最終結果

  • 函數組合預設是從右到左執行

lodash 中的組合函數

  • lodash 中組合函數 flow() 或者 flowRight(),他們都可以組合多個函數
  • flow() 是從左到右執行
  • flowRight() 是從右到左執行,使用的更多一些
const _ = require('lodash');
// lodash 中組合函數 flowRight(),將陣列的最後一個元素轉換爲大寫

/**
 * first 獲取字串第一個字元或陣列第一個元素
 * @param str {String || Array} 字串或陣列
 * @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 將字母轉換爲大寫
 * @param str {String} 字串
 * @returns {string} 返回值
 */
const toUpperCase = (str) => {
  return str.toUpperCase();
};

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

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

模擬實現 lodash 的 flowRight 方法

// 模擬 flowRight
/**
 * compose 函數組合
 * @param args 參數
 * @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));
/**
 * 輸出結果
 * A
 */

函數的組合要滿足結合律 (associativity):

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

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));
/**
 * 輸出結果
 * C
 * C
 * C
 */

lodash/fp

  • lodash 的 fp 模組提供了實用的對函數語言程式設計友好的方法

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

Point Free

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

  • 不需要指明處理的數據

  • 只需要合成運算過程

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

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)

概念

容器:包含值和值的變形關係(這個變形關係就是函數)

函子:是一個特殊的容器,通過一個普通的物件來實現,該物件具有 map 方法,map 方法可以執行一個函數對值進行處理(變形關係)

總結

函數語言程式設計的運算不直接操作值,而是由函子完成

函子就是一個實現了 map 契約的物件

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

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

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

demo

// Functor
class Container {
  /**
   * constructor 建構函式
   * @param value 入參
   */
  constructor(value) {
    this._value = value;
  }

  /**
   * of 範例化
   * @param value 入參
   * @returns {Container} 返回Container物件
   */
  static of(value) {
    return new Container(value);
  }

  /**
   * map 方法
   * @param fn {function} 處理常式
   * @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);
/**
 * 輸出結果
 * Container { _value: 26 }
 */

MayBe 函子

我們在程式設計的過程中可能會遇到很多錯誤,需要對這些錯誤做相應的處理

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

demo

// MayBe 函子
class MayBe {
  /**
   * constructor 建構函式
   * @param value 入參
   */
  constructor(value) {
    this._value = value;
  }

  /**
   * of 範例化
   * @param value 入參
   * @returns {MayBe} 返回MayBe物件
   */
  static of(value) {
    return new MayBe(value);
  }

  /**
   * map 方法
   * @param fn {function} 處理常式
   * @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);

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

Either 函子

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

異常會讓函數變的不純,Either 函子可以用來做例外處理

demo

// Either 函子
class Either {
  /**
   * constructor 建構函式
   * @param value 入參
   */
  constructor(value) {
    this._value = value;
  }

  /**
   * of 範例化
   * @param value 入參
   * @returns {Either} 返回Either物件
   */
  static of(value) {
    return new Either(value);
  }

  /**
   * map 方法
   * @param fn {function} 處理常式
   * @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} 入參
 * @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); // 異常
/**
 * 輸出結果
 * Either { _value: 'ZS' }
 * Either { _value: { error: 'Unexpected token n in JSON at position 1' }}
 */

IO 函子

IO 函子中的 _value 是一個函數,這裏是把函數作爲值來處理

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

把不純的操作交給呼叫者來處理

demo

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

class IO {
  /**
   * 建構函式
   * @param fn {function} 函數
   */
  constructor(fn) {
    this._value = fn;
  }

  /**
   * 範例化
   * @param value
   * @returns {IO} IO物件
   */
  static of(value) {
    return new IO(function () {
      return value;
    });
  }

  /**
   * map 方法
   * @param fn {function} 處理常式
   * @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());
/**
 * 輸出結果
 * D:\Development\nodeJS\node.exe
 */

IO 函子的問題

  • 巢狀函子時,需要多次獲取 value
/**
 * 讀取檔案內容
 * @param filename {string} 檔名
 * @returns {IO} 返回IO物件
 */
const readFile = (filename) => {
  return new IO(() => {
    return fs.readFileSync(filename, 'utf-8');
  });
};

/**
 * 列印
 * @param value {string} 入參
 * @returns {IO} 返回IO物件
 */
const print = (value) => {
  return new IO(() => {
    return value;
  });
};
/**
 * 讀取檔案並列印
 */
const cat = _.flowRight(print, readFile);
// 因爲這裏是巢狀函子,所以這邊列印出來的是IO(IO())
console.log(cat('package.json'));
// 所以需要連續兩次獲取value才能 纔能取到值
console.log(cat('package.json')._value()._value());

Task 非同步執行

folktale 一個標準的函數語言程式設計庫

  • 使用 folktale 裡的 Task 函數實現非同步

  • 使用 fs 裡的 readFile 函數,讀取檔案內容

// Task 非同步執行
const { task } = require('folktale/concurrency/task');
const fs = require('fs');

/**
 * 讀取檔案內容
 * @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);
    },
  });

/**
 * 輸出結果
 * 1.0.0
 */

Monad 函子

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

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

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

class IO {
  /**
   * 建構函式
   * @param fn {function} 函數
   */
  constructor(fn) {
    this._value = fn;
  }

  /**
   * 範例化
   * @param value
   * @returns {IO} IO物件
   */
  static of(value) {
    return new IO(function () {
      return value;
    });
  }

  /**
   * map 方法
   * @param fn {function} 處理常式
   * @returns {IO} 返回IO物件
   */
  map(fn) {
    return new IO(_.flowRight(fn, this._value));
  }

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

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

/**
 * 讀取檔案內容
 * @param filename {string} 檔名
 * @returns {IO} 返回IO物件
 */
const readFile = (filename) => {
  return new IO(() => {
    return fs.readFileSync(filename, 'utf-8');
  });
};

/**
 * 列印
 * @param value {string} 入參
 * @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);
/**
 * 輸出結果
 * 1.0.0
 */

非同步模式

非同步任務

非同步任務是指不進入主執行緒,而進入任務佇列的任務,只有任務佇列通知主執行緒,某個非同步任務可以執行了,該任務纔會進入主執行緒,當我們開啓網站時,像圖片的載入,音樂的載入,其實就是一個非同步任務

/**
 * 任何函數的宣告和任何變數的宣告都不會壓入呼叫棧(Call Stack)
 * 將console.log(1)壓入呼叫棧執行,執行後移除
 * 將setTimeout壓入呼叫棧執行,執行setTimeout,因爲setTimeout是非同步,所以將a()壓入Web APIs後,從呼叫棧中移除,倒計時等待
 * 將console.log(5)壓入呼叫棧執行,執行後移除
 * 將setTimeout壓入呼叫棧執行,執行setTimeout,因爲setTimeout是非同步,所以將c()壓入Web APIs後,從呼叫棧中移除,倒計時等待
 * 將console.log(6)壓入呼叫棧執行,執行後移除
 * 1秒倒計時結束後,將a()壓入訊息佇列,Event Loop(事件回圈)監聽到後,將a()壓入呼叫棧
 * 將console.log(2)壓入呼叫棧執行,執行後移除
 * 將a()裡的setTimeout壓入呼叫棧執行,執行setTimeout,因爲setTimeout是非同步,所以將b()壓入Web APIs後,從呼叫棧中移除,倒計時等待
 * 0.5秒倒計時結束後,將b()壓入訊息佇列,Event Loop(事件回圈)監聽到後,將b()壓入呼叫棧
 * 將console.log(3)壓入呼叫棧執行,執行後移除
 * 2秒倒計時結束後,將c()壓入訊息佇列,Event Loop(事件回圈)監聽到後,將c()壓入呼叫棧
 * 將console.log(4)壓入呼叫棧執行,執行後移除
 */

// 非同步程式碼
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);

/**
 * 輸出結果列印
 * 1
 * 5
 * 6
 * 2
 * 3
 * 4
 */

同步模式

單執行緒

概念

JavaScript 是一門單執行緒的語言,因此,JavaScript 在同一個時間只能做一件事,單執行緒意味着,如果在同個時間有多個任務的話,這些任務就需要進行排隊,前一個任務執行完,纔會執行下一個任務

使用單執行緒原因

JavaScript 的單執行緒,與它的用途是有很大關係,我們都知道,JavaScript 作爲瀏覽器的指令碼語言,主要用來實現與使用者的互動,利用 JavaScript,我們可以實現對 DOM 的各種各樣的操作,如果 JavaScript 是多執行緒的話,一個執行緒在一個 DOM 節點中增加內容,另一個執行緒要刪除這個 DOM 節點,那麼這個 DOM 節點究竟是要增加內容還是刪除呢?這會帶來很複雜的同步問題,因此,JavaScript 是單執行緒的

同步任務

同步任務是指在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能 纔能繼續執行下一個任務,當我們開啓網站時,網站的渲染過程,比如元素的渲染,其實就是一個同步任務

// 同步程式碼
/**
 * 任何函數的宣告和任何變數的宣告都不會壓入呼叫棧(Call Stack)
 * 將console.log(1)壓入呼叫棧執行,執行後移除
 * 將console.log(3)壓入呼叫棧執行,執行後移除
 * 將a()壓入呼叫棧執行,執行console.log(2),將console.log(2)壓入呼叫棧執行,執行後依次移除
 */

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

副作用

因爲 JavaScript 是單執行緒,因此同個時間只能處理同個任務,所有任務都需要排隊,前一個任務執行完,才能 纔能繼續執行下一個任務,但是,如果前一個任務的執行時間很長,比如檔案的讀取操作或 ajax 操作,後一個任務就不得不等着,拿 ajax 來說,當使用者向後台獲取大量的數據時,不得不等到所有數據都獲取完畢才能 纔能進行下一步操作,使用者只能在那裏乾等着,嚴重影響使用者體驗

Promise

Promise 物件用於表示一個非同步操作的最終完成 (或失敗), 及其結果值.

參數

executor

executor 是帶有 resolve 和 reject 兩個參數的函數 。Promise 建構函式執行時立即呼叫 executor 函數, resolve 和 reject 兩個函數作爲參數傳遞給 executor(executor 函數在 Promise 建構函式返回所建 promise 範例物件前被呼叫)。resolve 和 reject 函數被呼叫時,分別將 promise 的狀態改爲 fulfilled(完成)或 rejected(失敗)。executor 內部通常會執行一些非同步操作,一旦非同步操作執行完畢(可能成功/失敗),要麼呼叫 resolve 函數來將 promise 狀態改成 fulfilled,要麼呼叫 reject 函數將 promise 的狀態改爲 rejected。如果在 executor 函數中拋出一個錯誤,那麼該 promise 狀態爲 rejected。executor 函數的返回值被忽略。

描述

Promise 物件是一個代理物件(代理一個值),被代理的值在 Promise 物件建立時可能是未知的。它允許你爲非同步操作的成功和失敗分別系結相應的處理方法(handlers)。 這讓非同步方法可以像同步方法那樣返回值,但並不是立即返回最終執行結果,而是一個能代表未來出現的結果的 promise 物件

一個 Promise 有以下幾種狀態:

  • pending: 初始狀態,既不是成功,也不是失敗狀態。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失敗。

pending 狀態的 Promise 物件可能會變爲 fulfilled 狀態並傳遞一個值給相應的狀態處理方法,也可能變爲失敗狀態(rejected)並傳遞失敗資訊。當其中任一種情況出現時,Promise 物件的 then 方法系結的處理方法(handlers )就會被呼叫(then 方法包含兩個參數:onfulfilled 和 onrejected,它們都是 Function 型別。當 Promise 狀態爲 fulfilled 時,呼叫 then 的 onfulfilled 方法,當 Promise 狀態爲 rejected 時,呼叫 then 的 onrejected 方法, 所以在非同步操作的完成和系結處理方法之間不存在競爭)。

因爲 Promise.prototype.then 和 Promise.prototype.catch 方法返回 promise 物件, 所以它們可以被鏈式呼叫。

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鏈式呼叫

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 靜態方法使用

  • all 方法返回一個 Promise 範例,此範例在 iterable 參數內所有的 promise 都「完成(resolved)」或參數中不包含 promise 時回撥完成(resolve);如果參數中 promise 有一個失敗(rejected),此範例回撥失敗(reject),失敗的原因是第一個失敗 promise 的結果
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);
});

/**
 * 輸出結果
 * [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更快
});

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

/**
 * 輸出結果
 * fail
 */
  • resolve 方法返回一個以給定值解析後的 Promise 物件。如果這個值是一個 promise ,那麼將返回這個 promise ;如果這個值是 thenable(即帶有"then" 方法),返回的 promise 會「跟隨」這個 thenable 的物件,採用它的最終狀態;否則返回的 promise 將以此值完成。此函數將類 promise 物件的多層巢狀展平
Promise.resolve('success').then((res) => console.log(res));

/**
 * 輸出結果
 * success
 */

Event Loop(事件回圈)

Event Loop 它最主要是分三部分:主執行緒、宏任務(macrotask)、微任務(microtask)

宏任務

宏任務,macrotask,也叫 tasks。 一些非同步任務的回撥會依次進入 macro task queue,等待後續被呼叫,這些非同步任務包括:

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

微任務

微任務,microtask,也叫 jobs。 另一些非同步任務的回撥會依次進入 micro task queue,等待後續被呼叫,這些非同步任務包括:

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

執行順序

主執行緒 > 微任務 > 宏任務

Generator(生成器)

生成器物件是由一個 generator function 返回的,並且它符合可迭代協定和迭代器協定。

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);
/**
 * 輸出結果
 * 1
 * 2
 * 3
 */

Generator.prototype.return()

  • 返回給定的值並結束生成器。
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('結束').value);
console.log(gen.next().value);
console.log(gen.next().value);
/**
 * 輸出結果
 * 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);
// 阻止生成器函數往下執行,並拋出異常
gen.throw(new Error('error'));
console.log(gen.next().value);
console.log(gen.next().value);
/**
 * 輸出結果
 * 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 函數

async function 用來定義一個返回 AsyncFunction 物件的非同步函數。非同步函數是指通過事件回圈非同步執行的函數,它會通過一個隱式的 Promise 返回其結果。如果你在程式碼中使用了非同步函數,就會發現它的語法和結構會更像是標準的同步函數。

語法

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

描述

一個 async 非同步函數可以包含 await 指令,該指令會暫停非同步函數的執行,並等待 Promise 執行,然後繼續執行非同步函數,並返回結果。

記住,await 關鍵字只在非同步函數內有效。如果你在非同步函數外使用它,會拋出語法錯誤。

注意,當非同步函數暫停時,它呼叫的函數會繼續執行(收到非同步函數返回的隱式 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 是一個類,參數是一個執行器函數, 執行器函數自執行。

  2. Promise 有 3 個狀態 Pending 預設等待態 、Fulfilled 成功態 、Rejected 失敗態 狀態一改變就不能再次修改 Pending -> Fulfilled || Pending -> Rejected

  3. 執行器函數參數有 resolve 方法和 reject 方法 resolve 方法將 Pending 3.> Fulfilled reject 方法將 Rejected -> Rejected resolve 方法的參數將作爲 then 方法成功回撥的值, reject 方法的參數將作爲 then 方法失敗回撥的原因。

  4. then 方法有兩個參數 一個是成功回撥的函數 successCallback,一個是失敗回撥的函數 failCallback。 promise 的成功態會將成功的值傳給成功的回撥並且執行,失敗態會將失敗的原因傳遞給失敗的回撥並執行。

  5. 執行器中 resolve 和 reject 在非同步中執行的時候,當前狀態還是等待態 需要把 then 方法的成功回撥和失敗回撥存起來,等非同步呼叫 resolve 的時候呼叫成功回撥,reject 的時候呼叫失敗回撥

  6. then 方法

    a. then 方法多次呼叫新增多個處理常式;

    b. 實現 then 的鏈式呼叫: then 方法鏈式呼叫識別 promise 自返回 then 鏈式呼叫 返回的是一個新的 promise 物件

    c. 判斷 then 方法成功回撥和失敗回撥的返回值 x x 返回的是一個 pormise,判斷 x 和當前 then 返回是不是同一個 promise,如果是同一個 promise 就報錯。x 和 then 返回的不是同一個 promise,將 x 的 then 方法執行返回給下一個 then。 x 是常數直接當作下一個 then 的成功回撥的參數。後面 then 方法的回撥函數拿到值的是上一個 then 方法的回撥函數的返回值。

    d.捕獲錯誤及 then 鏈式呼叫其他狀態程式碼補充

    e. then 方法的參數可選 ​

    f. then 方法的值穿透

  7. executor 執行器函數 / then 方法可能有錯誤,有錯誤直接呼叫 reject 方法

  8. all 和 race 方法的實現

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

  9. resolve 和 reject 方法的實現

  10. resolve 方法: 相當於範例化 Promise 物件,並且呼叫了 resolve 方法 reject 方法:相當於範例化 Promise 物件,並且呼叫了 reject 方法

  11. finally 方法的實現

  12. finally 的實現,不管是成功狀態還是失敗態都會進這個 finally 方法 (等待態不會進)。finally 方法會返回一個新的 promise,它拿不到上次 then 執行的結果(所以沒有參數),內部會手動執行一次 promise 的 then 方法。finally 方法有錯誤會把錯誤作爲下次 then 方法的失敗回撥的參數。

  13. catch 方法的實現

    catch 方法相當於執行 then 方法的失敗回撥

demo

自定義 promise

class Promise {
  /**
   * 建構函式
   * @param executor {Function} 執行器
   */
  constructor(executor) {
    /**
     * 初始狀態,既不是成功,也不是失敗狀態
     * @type {string}
     */
    this.PENDING = 'pending';
    /**
     * 意味着操作成功完成
     * @type {string}
     */
    this.FULFILLED = 'fulfilled';
    /**
     * 意味着操作失敗
     * @type {string}
     */
    this.REJECTED = 'rejected';

    /**
     * Promise 狀態
     * @type {string} 預設值PENDING
     */
    this.status = this.PENDING;
    /**
     * 成功回撥函數入參的值
     * @type {string} 預設值undefined
     */
    this.value = undefined;
    /**
     * 失敗回撥函數入參的值
     * @type {string} 預設值undefined
     */
    this.reason = undefined;
    /**
     * successCallback 成功回撥函數
     * @type {Array} 預設值[]
     */
    this.successCallback = [];
    /**
     * failCallback 失敗回撥函數
     * @type {Array} 預設值[]
     */
    this.failCallback = [];

    /**
     * privateResolve 解析
     * @param value 成功回撥的入參
     */
    this.privateResolve = (value) => {
      // 在promise的狀態爲PENDING時,允許改變其狀態
      if (this.status === this.PENDING) {
        // 觸發resolve,將promise的狀態改爲fulfilled(完成)
        this.status = this.FULFILLED;
        // 將resolve傳遞的參數,儲存在Promise物件的value裡
        this.value = value;
        // 如果successCallback成功回撥函數存在,則執行
        this.successCallback.forEach((callback) => {
          callback();
        });
      }
    };

    /**
     * privateReject 駁回
     * @param reason 失敗回撥入參
     */
    this.privateReject = (reason) => {
      // 在promise的狀態爲PENDING時,允許改變其狀態
      if (this.status === this.PENDING) {
        // 觸發reject,將promise的狀態改爲rejected(失敗)
        this.status = this.REJECTED;
        // 將reject傳遞的參數,儲存在MyPromise物件的reason裡
        this.reason = reason;
        // 如果failCallback失敗回撥函數存在,則執行
        this.failCallback.forEach((callback) => {
          callback();
        });
      }
    };
    // 捕獲執行器異常,並從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;
        };
    // 鏈式呼叫實現,返回Promise函數物件
    let promise = new Promise((resolve, reject) => {
      // 判斷promise的狀態,當其狀態爲FULFILLED時,觸發成功回撥函數;當其狀態爲REJECTED時,觸發成功回撥函數
      // 後面的 then 方法的回撥函數的入參取自上一個 then 方法的回撥函數的返回值
      // promise物件未生成,所以將resolvePromise放到非同步裡呼叫,等待promise物件未生成後執行
      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的狀態,當其狀態爲PENDING時,儲存回撥函數
        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物件執行完所有任務,並執行
   * @param callback 回撥函數
   * @returns {Promise} 返回Promise物件
   */
  finally(callback) {
    return this.then(
      (value) => {
        return Promise.resolve(callback()).then(() => value);
      },
      (reason) => {
        return Promise.resolve(callback()).then(() => {
          throw reason;
        });
      },
    );
  }

  /**
   * catch 返回失敗回撥函數
   * @param callback 回撥函數
   * @returns {Promise} 返回Promise物件
   */
  catch(callback) {
    return this.then(undefined, callback);
  }

  /**
   * 等待所有Promise物件執行完畢,並返回結果
   * @param array {Array} 陣列物件
   * @returns {Promise} 返回Promise物件
   */
  static all(array) {
    // 返回陣列
    let result = [];
    // 計數器
    let index = 0;
    return new Promise((resolve, reject) => {
      /**
       * addResult 向result陣列新增返回值
       * @param key {Number} key值
       * @param value {Object} value值
       */
      const addResult = (key, value) => {
        result[key] = value;
        index++;
        // 判斷所有非同步操作完成後,執行resolve
        if (array.length === index) {
          resolve(result);
        }
      };
      // 遍歷
      array.forEach((val, i) => {
        // 判斷當前值,是否屬於Promise物件
        if (val instanceof Promise) {
          // Promise物件
          // 執行成功回撥函數,獲取返回值,並新增到result數組裏
          // 執行失敗回撥函數,直接通過reject拋出失敗原因
          val.then(
            (value) => addResult(i, value),
            (reason) => reject(reason),
          );
        } else {
          // 普通物件,將當前值新增到result數組裏
          addResult(i, val);
        }
      });
    });
  }

  /**
   * resolve 解析
   * @param value
   * @returns {Promise}
   */
  static resolve(value) {
    // 判斷value是否是Promise物件,是則直接返回,否則建立Promise物件並返回
    if (value instanceof Promise) return value;
    return new Promise((resolve) => resolve(value));
  }

  /**
   * reject 駁回
   * @param value
   * @returns {Promise}
   */
  static reject(value) {
    // 判斷value是否是Promise物件,是則直接返回,否則建立Promise物件並返回
    if (value instanceof Promise) return value;
    return new Promise((undefined, reject) => reject(value));
  }

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

/**
 * resolvePromise 解析Promise
 * @param promise promise物件
 * @param result 上一個then回撥函數返回值
 * @param resolve 解析函數
 * @param reject 駁回函數
 */
const resolvePromise = (promise, result, resolve, reject) => {
  // 判斷promise與result是否相等,如果相等,則是promise回圈呼叫,這裏應該拋出異常,並阻止往下執行
  if (promise === result) {
    return reject(new TypeError('Chaining cycle detected for my-promise'));
  }
  // 判斷result是普通物件還是屬於Promise物件
  if (result instanceof Promise) {
    // 檢視Promise物件返回結果,呼叫對應的resolve或reject
    result.then(resolve, reject);
  } else {
    resolve(result);
  }
};

module.exports = Promise;

呼叫

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鏈式呼叫
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靜態方法使用
// all  方法返回一個 Promise 範例,此範例在 iterable 參數內所有的 promise 都「完成(resolved)」或參數中不包含 promise 時回撥完成(resolve);如果參數中  promise 有一個失敗(rejected),此範例回撥失敗(reject),失敗的原因是第一個失敗 promise 的結果
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);
});
/**
 * 輸出結果
 * [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
});

/**
 * 輸出結果
 * two
 */

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

Promise.reject(new Error('fail')).catch((e) => console.error(e.message));
/**
 * 輸出結果
 * fail
 */
// resolve方法返回一個以給定值解析後的Promise 物件。如果這個值是一個 promise ,那麼將返回這個 promise ;如果這個值是thenable(即帶有"then" 方法),返回的promise會「跟隨」這個thenable的物件,採用它的最終狀態;否則返回的promise將以此值完成。此函數將類promise物件的多層巢狀展平
Promise.resolve('success').then((res) => console.log(res));
/**
 * 輸出結果
 * success
 */