blog

手書きPromiseについての考察

小さなダーリンの手書きのPromiseを見ていると、多かれ少なかれPromiseの使い方や役割を認識していると思います。そこで、ここではPromiseの歴史的背景や役割を詳しく紹介するのではなく、簡単...

Oct 12, 2020 · 7 min. read
シェア

前置き

手書きのプロミスのカワイイ姿を見て、多かれ少なかれプロミスの使い方や役割を認識されたことと思います。そこで、ここでは、プロミスの歴史的背景や役割について詳しく紹介するのではなく、簡単な紹介と、様々な使い方、そして、学習の記録として、自分の手書きのプロミスのプロセスを紹介します。

メソッドの使用

Promiseテストファイルを書きます。node の上位バージョンはすでに Promise の書き込みをサポートしているので、デバッグのためにこのファイルを node から直接実行できます。

// Promise.js const fs = require("fs") function asyncReadFile() { return new Promise((resolve, reject) => { fs.readFile('./demo.txt', (err, data) => { if(err) { reject(err) } else { resolve(data) } }) }) } function eventError() { return new Promise((resolve, reject) => { fs.readFile('./demo.txt', (err, data) => { reject("エラー") }) }) } function _setTimeout() { return new Promise((resolve, reject) => { setTimeout(() => { resolve("setTimeout") }, 1500) }) } function sync() { return new Promise((resolve, reject) => { resolve(88) }) } // then成功メソッドを受け入れる asyncReadFile() .then(ret => console.log(ret.toString())) // ドームを出力する.txt // thenエラーメソッドを受け入れる eventError() .then(null, err => console.log(err)) // エラー // catch eventError() .catch(err => console.log(err)) // エラー // resolve sync() .then(err => console.log(err)) // エラー // thenこれは、前のthenからの返り値を受け取り、新しいpromiseオブジェクトとなる。 _setTimeout .then(ret => console.log(ret)) // 1.5印刷後 setTimeout .then(ret => { console.log(ret); return 'aaa' }) // undefined .then(ret => console.log(ret)) // aaa // all静的メソッド。Promiseインスタンスの配列を受け取る。すべてのインスタンスが非同期に終了したら、それを実行し、対応する結果を返す。 Promise .all([asyncReadFile(), _setTimeout()]) .then(ret => { console.log(ret) // ['demo.txtファイルの内容', 'setTimeout'] }) // race静的メソッド。Promiseインスタンスの配列を受け取る。インスタンスが非同期に完了すると、そのインスタンスを実行し、結果を返す。 Promise .race([asyncReadFile(), _setTimeout()]) .then(ret => { console.log(ret) }) Promise.resolve(3).then(ret => console.log(ret)) // then を実装し、Promiseオブジェクトを返し、Promiseの実行が終わるまで待ってから実行し、Promiseのresolveパラメータを受け取る。/rejectの値を指定する。この手書きでは検証されていない // 印刷順序'demo.txtファイルの内容' -> 'setTimeout' asyncReadFile().then(ret => { console.log(ret.toString()) return _setTimeout() }) .then(ret => { console.log(ret) }) // new Promise((resolve) => { setTimeout(() => resolve(1), 500) setTimeout(() => resolve(2), 1500) }) .then(ret => console.log(ret))

プロミスの基本的な仕組みの一部を要約します:

  1. プロミスはクラスです。
  2. コンストラクターの引数は関数fnで、成功呼び出し用と失敗呼び出し用の2つの引数を取ります。
  3. promiseオブジェクトには、thenメソッド、成功コールバック、失敗コールバックがあります。
  4. メソッドは連鎖呼び出しをサポートします。
  5. を返します。
  6. promiseオブジェクトは、rejectによって投げられたエラーをキャッチするcatchメソッドを持っています。
  7. プロミス静的メソッドすべて、レース。
  8. プロミスの状態は、保留から再恋愛か拒絶にしか移行できません。

プロミスはクラスです

	class Promise {
	}

関数の最初のパラメータ用のコンストラクタ。この関数は2つの関数をパラメータとして公開し、最初のパラメータは呼び出しの成功、2番目のパラメータは呼び出しの失敗を表します。

class Promise { constructor(fn) { const resolve = () => { console.log('sucess!') } const reject = () => { console.log('reject!!!') } fn(resolve, reject) } }

基本構造の実装と非同期呼び出し -- サブスクリプションの発行

上記の機能を組み合わせて、シンプルなプロミスを実装します。


class _Promise {
 constructor(executor) {
 this.status = 'pending' // resolved rejected
 this.value = null
 this.reason = null
 this.onResolveCallbacks = [] // 成功したコンテナ
 this.onRejectedCallbacks = [] // 失敗したコンテナ
 const resolve = function(value) { //resolve関数の呼び出しに成功する
 this.status = 'resolved'
 this.value = value
 if(this.onResolveCallbacks.length === 0) return
 this.onResolveCallbacks.forEach(fn => fn(this.value))
 }
 const reject = function(reject) { // リジェクト関数の呼び出しに失敗する
 console.log(this)
 this.status = 'rejected'
 this.reason = reject
 if(this.onRejectedCallbacks.length === 0) {
 console.warn('UnhandledPromiseRejectionWarning:', this.reason)
 console.warn('UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().') 
 return
 }
 this.onRejectedCallbacks.forEach(fn => fn(this.reason))
 }
 executor(resolve.bind(this), reject.bind(this))
 }
 then(onFulfilled, onRejected) { // ハンドラをコンテナにプッシュする内部実装。
 if(this.status === 'pending') {
 onFulfilled && this.onResolveCallbacks.push(onFulfilled)
 onRejected && this.onRejectedCallbacks.push(onRejected)
 }
 if(this.status === 'resolved') {
 onFulfilled && onFulfilled(this.value)
 }
 if(this.status === 'rejected') {
 onRejected && onRejected(this.reason)
 }
 }
 catch(onRejected) { // と同じ原理
 if(this.status === 'pending') {
 this.onRejectedCallbacks.push(onRejected)
 }
 if(this.status === 'rejected' || this.status === 'resolved') {
 onRejected(this.reason)
 }
 }
}
const fs = require("fs")
function asyncReadFile() {
 return new _Promise((resolve, reject) => {
 fs.readFile('./demo.txt', (err, data) => {
 if(err) {
 reject(err)
 } else {
 resolve(data)
 }
 })
 })
}
asyncReadFile()
.then((res) => {
 console.log(res, '----')
})

上記は基本的なメソッドを実装したものですが、呼び出しが連鎖していないなど、まだまだ問題点も多いようです...。executor(resolve.bind(this), reject.bind(this))ここで、thisオブジェクトをバインドする必要があります。「さもないと、reslove/rejectは thisオブジェクトを取得できません。

同期呼び出しの実装--脆弱性1

	 constructor(executor) {
 const resolve = function(value) {
 setTimeout(() => {
 this.status = 'resolved'
 this.value = value
 if(this.onResolveCallbacks.length === 0) return
 this.onResolveCallbacks.forEach(fn => fn(this.value))
 }, 0)
 }
 const reject = function(reject) {
 setTimeout(() => {
 this.status = 'rejected'
 this.reason = reject
 if(this.onRejectedCallbacks.length === 0) {
 console.warn('UnhandledPromiseRejectionWarning:', this.reason)
 console.warn('UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch().') 
 return
 }
 this.onRejectedCallbacks.forEach(fn => fn(this.reason))
 }, 0)
 }
 executor(resolve.bind(this), reject.bind(this))
 }

テストした結果、同期がうまくいかない状況は解決しました。

への呼び出しの連鎖

上記はまだ連鎖した呼び出しを実装していないので、テストするとエラーが報告されます。

	then(onFulfilled, onRejected) {
 return new _Promise((resolve, reject) => {
 if(this.status === 'onFulfilled') {
 this.resolveArr.push(() => {
 const x = onFulfilled(this.value)
 resolve(x)
 })
 }
 if(this.status === 'onRejected') {
 this.rejectArr.push(() => {
 const x = onRejected(this.value)
 reject(x)
 })
 }
 if(this.status === 'pending') {
 this.resolveArr.push(() => {
 const x = onFulfilled(this.value)
 resolve(x)
 })
 this.rejectArr.push(() => {
 const x = onRejected(this.value)
 reject(x)
 })
 }
 })
 }

キャッチ・メソッドの実装

	catch(onRejected) {
 return this.then(null, onRejected)
 }

静的メソッド resolve の実装

_Promise.resolve = value => new _Promise(resolve => resolve(value))

静的メソッドrejectの実装

_Promise.reject = value => new _Promise((resolve, reject) => reject(value))

静的メソッドの実装

	_Promise.all = function(promises) {
 // if(Object.prototype.toString.call(promises) !== '[object Array]') return []
 let res = []
 return new this((resolve, reject) => {
 for (let i = 0; i < promises.length; i++) {
 let promiseItem = promises[i];
 if(typeof promiseItem !== 'object') promiseItem = this.resolve(promiseItem)
 promiseItem.then(ret => res.push(ret))
 }
 resolve(res)
 })
}

_Promise.race = promises => new _Promise((resolve, reject) => promises.forEach(pro => pro.then(resolve, reject)) )
Read next

ダークモードを探る

はじめに\nAndroid 10でDarkModeが追加され、それを適応できるアプリがますます増えています。 実際、DarkModeはAndroid 6.0の開発者プレビューですでに利用可能でしたが、正式版から削除されたばかりです。\n\nダークモードを実装するための第一歩は

Oct 12, 2020 · 3 min read