Node.js简单Promise实现

Node.js最常见的callback形式的异步调用API是这个样子的,我们调用fs.readFile,然后在回调函数中分别调用onSuccess和onFail。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const fs = require('fs');

function onSuccess (data) {
console.log(data);
}

function onFail (err) {
console.log('Error: ', err);
}

fs.readFile('./myPromise.js', (err, data) => {
if (err) {
onFail(err);
} else {
onSuccess(data);
}
});

我们也可以这么写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs');

const p = {};

p.resolve = function (data) {
console.log(data);
};

p.reject = function (err) {
console.log('Error: ', err);
};

fs.readFile('./myPromise.js', (err, data) => {
if (err) {
p.reject(err);
} else {
p.resolve(data);
}
});

和上面代码的区别是,我们把onSuccess和onFail封装到了一个Object中,分别叫resolve和reject,我们先不讨论这么写的好处。

然后我们发现其实设置p.resolve和p.reject其实可以在fs.readFile之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fs = require('fs');

const p = {};

fs.readFile('./myPromise.js', (err, data) => {
if (err) {
p.reject(err);
} else {
p.resolve(data);
}
});

p.resolve = function (data) {
console.log(data);
};

p.reject = function (err) {
console.log('Error: ', err);
};

这其实是异步编程的一种特性,而不仅仅是Node.js的特性,用伪代码表示

1
2
3
声明一个对象A
开始一个异步操作
修改对象A的成员

这里的问题是,异步操作完成以后如何访问修改后的A的成员?在Node.js中,我们可以利用closure来实现。上面的例子中,回调函数内部可以引用到p对象。

更进一步,我们发现,把p对象封装在一个函数中返回,然后设置函数返回的对象的resolve和reject也是可以行的,原因同上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const fs = require('fs');

function readFile (filePath) {
const p = {};

function cb (err, data) {
if (err) {
p.reject(err);
} else {
p.resolve(data);
}
}

fs.readFile(filePath, cb);

return p;
}

let p = readFile('./myPromise.js');

p.resolve = function (data) {
console.log(data);
};

p.reject = function (err) {
console.log('Error: ', err);
};

我们现在添加一个MyPromise类来替换p,方便我们扩展p这个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const fs = require('fs');

function MyPromise () {

}

MyPromise.prototype.then = function (resolve, reject) {
this.resolve = resolve;
this.reject = reject;
};

function readFile (filePath) {
const p = new MyPromise();

function cb (err, data) {
if (err) {
p.reject(err);
} else {
p.resolve(data);
}
}

fs.readFile(filePath, cb);

return p;
}

let p = readFile('./myPromise.js');

function resolve (data) {
console.log(data);
}

function reject (err) {
console.log('Error: ', err);
}

p.then(resolve, reject);

我们知道Promise被用来避免多层的callback嵌套,例如下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const fs = require('fs');

function MyPromise () {

}

MyPromise.prototype.then = function (resolve, reject) {
this.resolve = resolve;
this.reject = reject;
};

function readFile (filePath) {
const p = new MyPromise();

function cb (err, data) {
if (err) {
p.reject(err);
} else {
p.resolve(data);
}
}

fs.readFile(filePath, cb);

return p;
}

let p = readFile('./myPromise.js');

function resolve (data) {
console.log(data);
let p2 = readFile('./myPromise2.js'); // 这里嵌套了一层readFile
function resolve2 (data) {
console.log(data);
}
function reject2 (err) {
console.log('Error: ', err);
}
p2.then(resolve2, reject2);
}

function reject (err) {
console.log('Error: ', err);
}

p.then(resolve, reject);

p2也是一个MyPromise对象,那么也许可以把p2返回到最外层,然后在最外层调用then。这需要调整MyPromise的内部实现。思路是then方法会返回一个MyPromise对象,我们把这个对象保存在MyPromise的next属性。我们添加了MyPromise.prototype.resolve方法,这个方法调用了this._resolve,而this._resolve会返回一个MyPromise对象,在MyPromise.prototype.resolve方法中,我们调用this._resolve返回的MyPromise对象的then方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const fs = require('fs');

function MyPromise () {

}

MyPromise.prototype.then = function (resolve, reject) {
this._resolve = resolve;
this._reject = reject;
this.next = new MyPromise();
return this.next;
};

MyPromise.prototype.resolve = function (data) {
let p = this._resolve(data);
if (p) p.then(this.next._resolve, this.next._reject);
};

MyPromise.prototype.reject = function (err) {
this._reject(err);
};

function readFile (filePath) {
const p = new MyPromise();

function cb (err, data) {
if (err) {
p.reject(err);
} else {
p.resolve(data);
}
}

fs.readFile(filePath, {encoding: 'utf-8'}, cb);

return p;
}

let p = readFile('./a.txt');

function resolve (data) {
console.log('a.txt', data);
let p2 = readFile('./b.txt');
return p2;
}

function reject (err) {
console.log('Error: ', err);
}

function resolve2 (data) {
console.log('b.txt', data);
}

function reject2 (err) {
console.log('Error: ', err);
}

p
.then(resolve, reject)
.then(resolve2, reject2);

和上一段代码的最大区别是,不需要在resolve函数中嵌套,p2的resolve和reject函数了。

上面的代码可以写成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let p = readFile('./a.txt');

p
.then(function resolve (data) {
console.log('a.txt', data);
let p2 = readFile('./b.txt');
return p2;
}, function reject (err) {
console.log('Error: ', err);
})
.then(function resolve2 (data) {
console.log('b.txt', data);
}, function reject2 (err) {
console.log('Error: ', err);
});

如果忽略reject函数,那么这段代码已经很像调用Promise对象的代码。

1
2
3
4
5
6
7
8
9
readFile('./a.txt');
.then(function resolve (data) {
console.log('a.txt', data);
let p2 = readFile('./b.txt');
return p2;
})
.then(function resolve2 (data) {
console.log('b.txt', data);
});

这样的代码给人一种顺序调用readFile的感觉,饶了一大圈,我们仅仅是为了把嵌套的代码变成顺序的形式。

这里仅仅实现了嵌套的异步操作看起来像顺序操作,和Promise对象功能还差很多,完整的Promise实现的规范是Promises/A+

Promise其实是一种设计模式,就像面向对象编程带来的一堆设计模式一样,在异步编程领域,Promise被大量使用。

如果看一下Promise规范的话,单单一个then函数的实现就有十几条规则,编程的时候总是要考虑这些规则可不是个好的体验,如果你不注意的话,它就会跳出说”Surprise!”。但愿你的客户不会被”Surprise”。