Promiseオブジェクトを徹底的に理解する--es 5文法で自分のPromiseを実現する(前編)


この文書では、自己の個人ブログを同期します.http://mly-zju.github.io/
Javascript言語の大きな特色は非同期であることはよく知られており、これはその利点であり、場合によってはいくつかの問題をもたらしている.最大の問題の1つは,非同期操作が多すぎると,コード内に多くのコールバック関数があふれ,コールバックピラミッドが形成されることである.コールバック関数がもたらす問題を解決するために,Promiseはより優雅な非同期ソリューションとして提案され,最初はインタフェース仕様の実現にすぎなかったが,es 6になると言語面でPromiseオブジェクトを原生的にサポートした.
最初にPromiseに触れたとき、抽象的で困惑していたと思いますが、多くの人も同じような感じを持っていると信じています.しかし、その後の熟知の過程で、私はゆっくりとその優雅さを体得し、Promiseオブジェクトの実現の原理を考え始め、最終的にes 5文法で基本的な機能を備えた自分のPromiseオブジェクトを実現した.この文章の中で、自分が実現した過程と構想を逐次漸進的に記録して、みんなが見終わった後に、Promiseオブジェクトの運行の原理を徹底的に理解することができることを信じて、そして後の開発の中で、更に熟練してそれを使うことができます.
githubソースアドレス:https://github.com/mly-zju/Js-practice
1.過去に戻る:resolve,reject,then
まずPromiseの使用例を見てみましょう.
var fn=function(resolve, reject){
  console.log('begin to execute!');
  var number=Math.random();
  if(number<=0.5){
    resolve('less than 0.5');
  }else{
    reject('greater than 0.5');
  }
}

var p=new Promise(fn);
p.then(function(data){
  console.log('resolve: ', data);
}, function(data){
  console.log('reject: ', data);
})

この例では,fnに0~1の乱数を生成し,0.5以下であればresolve関数,0.5以上であればreject関数を呼び出す.関数を定義したら、この関数をPromiseで包み、Promiseオブジェクトを返し、resolveとreject関数をそれぞれ定義するオブジェクトのthenメソッドを呼び出します.ここでresolveとrejectは比較的簡単で、伝達されたパラメータに接頭辞を付けて出力を印刷することです.
ここでは、p=new Promise(fn)という文を実行すると、fn関数はすでに実行されていますが、p.thenという方法は後でresolveとrejectを定義します.では、なぜfn関数はresolveとreject関数が何であるかを知ることができますか?
すなわち,resolveとreject関数はどのように過去に戻り,先に実行されたfn関数の中に現れるのだろうか.これはPromiseの中で最も重要な概念の一つです.
実はこの「ブラックテクノロジー」を実現するには、方法も非常に簡単で、主にsettimeoutという方法を運用し、fnにおけるresolveとrejectの実行を遅らせる.このアイデアを利用して、私たちは自分の初級版Promiseを初歩的に書くことができます.ここではMyPromiseと命名します.
function MyPromise(fn) {
  this.value;
  this.resolveFunc = function() {};
  this.rejectFunc = function() {};
  fn(this.resolve.bind(this), this.reject.bind(this));
}

MyPromise.prototype.resolve = function(val) {
  var self = this;
  self.value=val;
  setTimeout(function() {
    self.resolveFunc(self.value);
  }, 0);
}

MyPromise.prototype.reject = function(val) {
  var self=this;
  self.value=val;
  setTimeout(function() {
    self.rejectFunc(self.value);
  }, 0);
}

MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
  this.resolveFunc = resolveFunc;
  this.rejectFunc = rejectFunc;
}

var fn=function(resolve, reject){
  console.log('begin to execute!');
  var number=Math.random();
  if(number<=0.5){
    resolve('less than 0.5');
  }else{
    reject('greater than 0.5');
  }
}

var p = new MyPromise(fn);
p.then(function(data) {
  console.log('resolve: ', data);
}, function(data) {
  console.log('reject: ', data);
});

MyPromiseはfn関数を受信し,自分のthis.resolveとthis.rejectメソッドをfnのresolveとrejectパラメータとしてfnに渡して実行することがわかる.MyPromiseのresolveメソッドを観察すると,settimeoutを用いてresolveFuncを0秒遅らせることが主な動作であることが分かった.
then法を観察すると,ここでは比較的簡単で,2つの関数を受け入れ,それぞれ自分のthis.resolveFuncとthis.rejectFuncに与えることが分かる.
ここで論理は明らかで,fn関数はまず実行されるがresolveとrejectを呼び出す際にsettimeoutが用いられる.0秒遅延実行ですが、jsは単一スレッド+メッセージキューであり、メッセージキュー内のコードの実行を開始するには、プライマリスレッドコードの実行が完了するまで待たなければならないことがわかります.したがって、thenという方法を最初に実行し、resolveFuncとrejectFuncに値を割り当てます.then実行が完了してからsettimeout内のメソッドを実行すると、この時点でresolveFuncとrejectFuncが割り当てられているので、スムーズに実行できます.これが「過去に戻る」奥義です.
2.加入状態:pending,resolved,rejected
前節では,実行可能に見えるMyPromiseを初歩的に実現したが,問題は多い.次のコードを見てみましょう.
var fn=function(resolve, reject){
  resolve('hello');
  reject('hello again');
}

var p1=new Promise(fn);
p1.then(function(data){
  console.log('resolve: ',data)
}, function(data){
  console.log('reject: ',data)
});
//'resolve: hello'

var p2=new MyPromise(fn);
p2.then(function(data){
  console.log('resolve: ',data)
}, function(data){
  console.log('reject: ',data)
});
//'resolve: hello '
//'reject: hello again'

p 1はオリジナルPromise,p 2は私たちが自分で書いたもので,resolveを呼び出した後にrejectを呼び出すと,p 1はresolveのみを実行し,私たちは両方とも実行することがわかる.実際、Promise仕様では、Promiseが初期pending状態からresolvedまたはrejected状態にしかならないことを規定しており、一方向に変化している.つまり、resolveを実行してもrejectは実行されず、逆も同様である.
そのため、MyPromiseにステータスを追加し、必要な場所で判断し、繰り返し実行を防止する必要があります.
function MyPromise(fn) {
  this.value;
  this.status = 'pending';
  this.resolveFunc = function() {};
  this.rejectFunc = function() {};
  fn(this.resolve.bind(this), this.reject.bind(this));
}

MyPromise.prototype.resolve = function(val) {
  var self = this;
  if (this.status == 'pending') {
    this.status = 'resolved';
    this.value=val;
    setTimeout(function() {
      self.resolveFunc(self.value);
    }, 0);
  }
}

MyPromise.prototype.reject = function(val) {
  var self = this;
  if (this.status == 'pending') {
    this.status = 'rejected';
    this.value=val;
    setTimeout(function() {
      self.rejectFunc(self.value);
    }, 0);
  }
}

MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
  this.resolveFunc = resolveFunc;
  this.rejectFunc = rejectFunc;
}

これにより、上記のインスタンスを再実行してもresolveとrejectが実行されることはありません.
3.チェーンコール
Promiseの使用では、チェーン呼び出しが可能であることに注意してください.
var fn=function(resolve, reject){
  resolve('hello');
}

var p1=new Promise(fn);
p1.then(function(data){
  console.log(data);
  return 'hello again';
}).then(function(data){
  console.log(data);
});
//'hello'
//'hello again'

チェーン呼び出しを実現するには、thenメソッドの戻り値もPromiseオブジェクトでなければなりません.これにより、thenを再び後で呼び出すことができます.そこでMyPromiseのthenメソッドを修正します.
MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
  var self = this;
  return new MyPromise(function(resolve_next, reject_next) {
    function resolveFuncWrap() {
      var result = resolveFunc(self.value);
      resolve_next(result);
    }
    function rejectFuncWrap() {
      var result = rejectFunc(self.value);
      resolve_next(result);
    }

    self.resolveFunc = resolveFuncWrap;
    self.rejectFunc = rejectFuncWrap;
  })
}

ここでthenはMyPromiseオブジェクトを返していることがわかります.このMyPromiseでは、すぐに実行される関数がパッケージされています.主にresolveFuncとrejectFuncをカプセル化し、前のMyPromiseのresolveFuncとrejectFuncに値を割り当てます.ここの難点はパッケージの目的を理解することです.
ここでは上の例で説明します.上記のチェーン呼び出し例では、2つのPromiseが現れ、1つ目はnew Promiseによって明示的に定義され、Promise 1と呼ばれ、2つ目のPromiseは、Promise 1のthenメソッドによって返される新しいPromise 2と呼ばれています.Promise 1のresolveメソッドが実行されると、resolveの戻り値は、Promise 2のresolveにパラメータとして渡されます.これも、上の2番目のthenに最初のthenが返す文字列が印刷された理由です.
パッケージの目的は、Promise 1のresolveまたはrejectが実行された後に、その戻り値をPromise 2のresolveに渡すことです.私たち自身の実装では、Promise 2のresolveはresolveと名付けられています.next、Promise 1のresolveFuncが実行された後、戻り値resultを取得し、resolve_を呼び出します.next(result)は、Promise 2のresolveにパラメータを渡す.ここで注目すべきは、Promise 1がresolveFuncを実行してもrejectFuncを実行しても、その後呼び出されるのはPromise 2のresolveであり、Promise 2のrejectが何に使われるかについては、以下の章で詳しく説明します.
これで、私たちのMyPromiseはチェーン呼び出しを使用できるようになりました.
しかし、Promise仕様をもう一度観察すると、チェーン呼び出しの状況も2つに分かれていることがわかります.1つの場合、前のPromiseのresolveまたはrejectの戻り値は通常のオブジェクトであり、この場合、現在のMyPromiseは正しく処理できます.しかし、前のPromiseのresolveまたはrejectが実行された後、返される値自体がPromiseオブジェクトである場合もあります.例を挙げます.
var fn=function(resolve, reject){
  resolve('hello');
}

var p1=new Promise(fn);
p1.then(function(data){
  console.log(data);
  return 'hello again';
}).then(function(data){
  console.log(data);
  return new Promise(function(resolve){
    var innerData='hello third time!';
    resolve(innerData);
  })
}).then(function(data){
  console.log(data);
});
//'hello'
//'hello again'
//'hello third time!'

この例では2回のチェーン呼び出しが現れ、1番目のthenは'hello again'文字列を返し、2番目のthenのresolveでは印刷処理が行われる.次に、2番目のthenでは、Promiseオブジェクトが返され、resolveが呼び出されることに注意します.では、問題が来ました.このresolveはどこから来ましたか.答えは3番目のthenで定義されています!この例で3番目のthenで定義されたresolveも簡単で、resolveに渡されるパラメータを直接印刷します.
そのため、ここで私たちのMyPromiseも修正する必要があります.前のresolveまたはrejectの戻り値について判断し、Promiseオブジェクトかどうかを見て、もしそうであれば、異なる処理をして、修正したコードは以下の通りです.
MyPromise.prototype.then = function(resolveFunc, rejectFunc) {
  var self = this;
  return new MyPromise(function(resolve_next, reject_next) {
    function resolveFuncWrap() {
      var result = resolveFunc(self.value);
      if (result && typeof result.then === 'function') {
        //  result MyPromise  ,   then resolve_next reject_next   
        result.then(resolve_next, reject_next);
      } else {
        //  result     ,       resolve_next
        resolve_next(result);
      }
    }
    function rejectFuncWrap() {
      var result = rejectFunc(self.value);
      if (result && typeof result.then === 'function') {
        //  result MyPromise  ,   then resolve_next reject_next   
        result.then(resolve_next, reject_next);
      } else {
        //  result     ,       resolve_next
        resolve_next(result);
      }
    }
    self.resolveFunc = resolveFuncWrap;
    self.rejectFunc = rejectFuncWrap;
  })
}

コードには、resolveFuncまたはrejectFuncの戻り値について、.thenメソッドが含まれているかどうかを判断し、含まれている場合はMyPromiseオブジェクトとして認識し、そのMyPromiseのthenメソッドを呼び出し、resolve_nextとreject_nextがそれに伝えます.そうでなければ、通常のオブジェクトはresultをパラメータとしてresolve_に渡します.next.
このように修正すると、私たちのMyPromiseはチェーン呼び出しで通常のオブジェクトとMyPromiseオブジェクトを正しく処理することができます.
このように,この論文ではまずPromiseの一般的な基本機能,主にthenの呼び出し,状態の制御,チェーン呼び出しを実現した.また、後述する記事では、Promiseのエラーキャプチャ処理など(Promiseの.catchメソッドの原理など)を実現する方法をさらに説明し、MyPromiseを本当に丈夫で利用できるようにします.