In the last article we talked about the concept of synchronous chain processing data functors. In this video, we’re going to talk about asynchrony. The concepts used are simple and do not require a foundation in functional programming. Of course, it will be easier to understand if you read the article by your side – Functional Programming don’t Worry. In this article we will finish writing a Promise code. This article starts with implementing a simple Promise with only a dozen lines of code that solves the problem of asynchronous chain calls. Then gradually improve and add features.
- Implement a simple asynchronous Promise functor
- The ability to call the same Promise functor at the same time
- Add the reject callback function
- Adding Promise states
The code for this article is on my Github
Implement a simple Promise functor
Let’s review synchronous chain calls first.
class Functor{
constructor (value) {
this.value = value ;
}
map (fn) {
return Functor.of(fn(this.value))
}
}
Functor.of = function (val) {
return new Functor(val);
}
Functor.of(100).map(add1).map(add1).map(minus10)
// var a = Functor.of(100);
// var b = a.map(add1);
// var c = b.map(add1);
// var d = c.map(minus10);
Copy the code
- The core of functors is that each functor is a new object
- The data is processed by the function fn passed in from the map
- Use the resulting value to generate a new functor
So if a is generated asynchronously, how do we pass this.value?
function executor(resolve){
setTimeout(()=>{resolve(100)},500)
}
Copy the code
So let’s simulate this by going through settime out for 500 milliseconds and getting the data 100. We can simply pass in a resolve callback to handle the data.
class Functor {
constructor (executor) {
let _this = this;
this.value = undefined;
function resolve(value){
_this.value = value;
}
executor(resolve)
}
}
var a = new Functor(executor);
Copy the code
- We say executor comes in and executes immediately
- We get the value in the resolve callback
- We define the resolve callback to assign the value of value to this.value
So we can easily complete the assignment of a. So how do we process this data with methods?
- Obviously after we get the callback value, we should be able to get fn in the map to continue processing the data
- After processing this data, we pass it to the next function resolve to continue processing
- So we define a callback function,
- When a map is called, it is assigned a function that contains fn to process the data and perform the resolve of the next object
- We then execute the callback after our resolve gets the value
class Functor {
constructor (executor) {
let _this = this;
this.value = undefined;
this.callback = null;
function resolve(value){
_this.value = value;
_this.callback()
}
executor(resolve)
}
map (fn) {
let self = this;
return new Functor((resolve) => {
self.callback = function() {let data = fn(self.value)
resolve(data)
}
})
}
}
new Functor(executor).map(add1).map(add1)
Copy the code
Now that we’ve implemented asynchronous chain calls, let’s take a look at what’s going on.
- (1) When a = new Functor(executor), we initialized it and executor(resolve) started
- (2) execute executor(resolve) initialize new Functor () when b = a.ap (add1)
- (3) The executor(resolve) function in b is assigned to the callback in A
New Functor((resolve) => {} is a Functor of B
- (4) Return a new functor b
- (5) when c = B.map(add1), similarly, assign a value to callback in B
- (6) Then return a new functor c, in which no map is called and callback in C is null
Let’s look again at how resolve is executed in the callback function after asynchrony ends.
- (1) _this.value = value; Change the value in a
- (2) After executing _this.callback(), let data = fn(self.value) calculate the processed data
- (3) Call the resolve function in B to continue processing
- (4) In b, the value is assigned first and then the data is processed
- (5) Call resolve in C and pass the processed data to it
- (6) Assign value to C first, and then process data. Finally, an error will be reported when callback is not a function, which will be solved later
Code for this section: promise1.js
Well, that’s the rationale behind the asynchronous operations that Promise implements as a functor. It already solves the problem of simple asynchronous calls. There’s not much code, but this is the heart of how Promise handles asynchronous calls. We’ll continue to implement other features as we go along.
Call the same Promise functor simultaneously
If we call the functor A at the same time as below. And you can see that it actually only performs C.
var a = new Functor(executor);
var b = a.map(add);
var c = a.map(minus);
Copy the code
The reason for this is simple, because as we learned above, B first assigns a callback, and c then assigns a callback. So it’s not going to execute if it overwrites b. To solve this problem, we simply need to make the callback an array.
class MyPromise {
constructor (executor) {
let _this = this;
this.value = undefined;
this.callbacks = [];
function resolve(value){
_this.value = value;
_this.callbacks.forEach(item => item())
}
executor(resolve)
}
then (fn) {
return new MyPromise((resolve) => {
this.callbacks.push (()=>{
let data = fn(this.value)
console.log(data)
resolve(data)
})
})
}
}
var a = new MyPromise(executor);
var b = a.then(add).then(minus);
var c = a.then(minus);
Copy the code
- We define the callbacks array every time we call a’s then method. Store it in the Callbacks array.
- When the callback function gets a value, each function is iterated through in Resolve.
- If the callbacks are empty, forEach will not execute, which also fixes the error
- Then we further changed the name of the functor (MyPromise), changing map to then
- Let self = this;
Add reject callback function
As we all know, when making an asynchronous call, we often don’t get the data and return an error message. In this section, we handle errors.
function executor(resolve,reject){
fs.readFile('./data.txt',(err, data)=>{
if(err){
console.log(err)
reject(err)
}else {
resolve(data)
}
})
}
Copy the code
- We now use Node to read a file asynchronously
- Resolve (data) on success, reject(err) on failure
Now we define reject
class MyPromise {
constructor (executor) {
let _this = this;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
function resolve(value){
_this.value = value;
_this.onResolvedCallbacks.forEach(item => item())
}
function reject(reason){
_this.reason = reason;
_this.onRejectedCallbacks.forEach(item => item());
}
executor(resolve, reject);
}
then (fn,fn2) {
return new MyPromise((resolve,reject) => {
this.onResolvedCallbacks.push (()=>{
let data = fn(this.value)
console.log(data)
resolve(data)
})
this.onRejectedCallbacks.push (()=>{
let reason = fn2(this.reason)
console.log(reason)
reject(reason)
})
})
}
}
Copy the code
- It’s as simple as passing in one more reject at executor
- Determine whether to execute resolve or reject based on the result of asynchronous execution
- We then define the same method for Reject and resolve in MyPromise
- And then we should pass in two parameters,fn,fn2, for then
This section code: promise3.js
The executor function is then wrapped in an asyncReadFile function that reads files asynchronously
function asyncReadFile(url){
return new MyPromise((resolve,reject) => {
fs.readFile(url, (err, data) => {
if(err){
console.log(err)
reject(err)
}else {
resolve(data)
}
})
})
}
var a = asyncReadFile('./data.txt');
a.then(add,mismanage).then(minus,mismanage);
Copy the code
This is how we normally encapsulate asynchronous Promise functions. But it’s a process that doesn’t feel like we’ve seen it before. If the ‘./data.txt’ of executor was passed in as an argument, then the process would be the same as the Currization we talked about in the last section.
This section is promise4.js
Let’s summarize the above process again.
- We initialize the executor function passed in and push the function into the callback array
- When reslove or Reject executes, we execute the function in callback
- We can see that the same functor A has different states at different times.
- Obviously, adding then () after reslove () or reject() won’t work
So how do we solve the then call of a functor after reslove, but after reslove, we already have value, so that’s just the chain call of a regular functor that we started with? So now all we have to do is label the state of the functor, and then decide how to call then, okay
4 Add the Promise state
- We define the ongoing state as pending
- After the successful implementation, there is a big pity
- Failure is rejected
class MyPromise {
constructor (executor) {
let _this = this;
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
function resolve(value){
if (_this.status === 'pending') {
_this.status = 'fulfilled';
_this.value = value;
_this.onResolvedCallbacks.forEach(item => item())
}
}
function reject(reason){
if (_this.status === 'pending') {
_this.status = 'rejected';
_this.reason = reason;
_this.onRejectedCallbacks.forEach(item => item());
}
}
executor(resolve, reject);
}
then (fn,fn2) {
return new MyPromise((resolve,reject) => {
if(this.status === 'pending'){
this.onResolvedCallbacks.push (()=>{
let data = fn(this.value)
console.log(data)
resolve(data)
})
this.onRejectedCallbacks.push (()=>{
let reason = fn2(this.reason)
console.log(reason)
reject(reason)
})
}
if(this.status === 'fulfilled') {let x = fn(this.value)
resolve(x)
}
if(this.status === 'rejected') {let x = fn2(this.value)
reject(x)
}
})
}
}
var a = asyncReadFile('./data.txt');
a.then(add,mismanage).then(add,mismanage).then(add,mismanage);
Copy the code
Let’s analyze this process
It’s just one more parameter, and it’s easy. So now let’s analyze the execution process when we call A in the depressing state
setTimeout(()=>{ d = a.then(add); } ,2000) value:"1"
Copy the code
- (1) First execute new MyPromise (), initialize d
- Execute executor(resolve, reject); Fn starts executing and evaluates the new value x
- (3) Execute resolve to d,
- (4) Modify the state of stauts and value
- (5) Return the new functor, can continue the chain call
If (2) fn is an asynchronous operation and d continues to call then, the pending state will not change until resolve. The then method is added to the callback. We’re back to where we were with asynchrony. So is that why Promise solves callback hell
Reference code: promise5.js
Ok, so now we’re going to pass fn(this.value), and we need to filter it with the Maybe functor from last time.
Maybe functor optimization
then (onResolved,onRejected) {
onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}
onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}
return new MyPromise((resolve,reject) => {
if(this.status === 'pending'){
this.onResolvedCallbacks.push (()=>{
let x = onResolved(this.value)
resolve(x)
})
this.onRejectedCallbacks.push (()=>{
let x = onRejected(this.reason)
reject(x)
})
}
if(this.status === 'fulfilled') {let x = onResolved(this.value)
resolve(x)
}
if(this.status === 'rejected') {let x = onRejected(this.value)
reject(x)
}
})
}
Copy the code
- The Maybe is simple, filter onResolved and onRejected
Reference code: promise6.js
I’ll stop here for this one. In conclusion, Promise is very powerful, just like Life of PI. Although the journey is gorgeous, but always accompany you only that tiger. Promise is no different. Once you understand the concept of its core functor, other issues become easier to understand. A simple Promise is implemented here, and more powerful features will be added slowly.