This is the 11th day of my participation in the August More text Challenge. For details, see: August More Text Challenge

analogy

For example, when we go to McDonald’s during rush hour and order food, we tell the waiter to order a hamburger, and the waiter gives you an order receipt with 2204 printed on it and tells you to wait for your food first. The order number is a Promise that I will get my hamburger at the end. At this point, you should take your order receipt and use it to trade him and the burger; And while you’re waiting in line for a meal, you might swipe your phone or tell a friend you want a burger. You don’t think about the burger when you get the order number, even though you’d like to, because you’re already using the order number as a placeholder for the burger. Basically, this placeholder, this value is no longer dependent on time, this is a future value. Eventually, with the order number 2204 printed on the screen, you take the receipt, hand it over to the cashier, and get the burger. That is, once the value you need (the burger) is ready, you need to exchange the promised value (the order receipt) for the value itself.

But there’s also a situation where you go to the register to pick up a burger and the waiter says the burger you ordered is sold out. The future value has an important property: it can succeed or fail. Every time you order a burger, you get either a burger or a sell-out message.

Present value and future value

var x,y=2
x+y // NaN x was not resolved
Copy the code

The + operator does not wait for both x and y to be ready before performing the operation. If there is a way to determine the readiness of two values, if either is not ready, wait until both are ready and then proceed to the next calculation. Promise makes all operations asynchronous in order to deal with the present and future uniformly.

Promise value

Rewrite the above code with a PROMISE so that both values are ready before adding.

function add(x,y){
	return Promise.all([x,y])
	.then(values= >values[0]+values[1])}// fetchX(),fetchY() returns the corresponding promise, and the ready state is unknown
add(fetchX(),fetchY())
.then(sum= >console.log(sum))
Copy the code

FetchX and fetchY are called directly first, returning a promise, which is passed to Add. Add creates and returns a Promise and waits for the Promise by calling THEN. When add is complete, sum is ready (resolve) and will be printed.

The burgers can be sold out, the program can go wrong, and the promise’s resolution status is rejected rather than done (either as set directly by the program logic or as an implicit runtime exception).

Externally, since a Promise encapsulates a time-dependent state (waiting for the underlying value to complete or reject, the promise itself is time-independent), it can be composed in a predictable manner, without requiring the developer to care about timing or the underlying outcome. Once a promise has been decided, it is now an external immutable value.

Complete event

Promises can be described as a process control mechanism in asynchronous tasks. We don’t need to know when he’s going to start and when he’s going to end; We just need to initiate a notification when he’s done, and then we can move on to the next task. After foo completes, a listener is created to notify the processing object of the time. Then set up two event listeners, one for “completion” and one for “Failure”.

function foo(){
	/ /... Time consuming work
	return listener
}
const evt=foo(20)
evt.on('completion'.() = >{
	// Proceed to the next step
})
evt.on('failure'.err= >{
	// There is an error in foo
})
Copy the code

We can provide this event listener (EVT) to multiple separate parts of the code, which can be independently notified to proceed to the next step

const evt=foo(20)
bar(evt) // bar() to listen for foo completion
baz(evt) // Baz () can also listen for foo completion
Copy the code

Foo () doesn’t need to know whether bar() and baz() exist, and the EVT object is a neutral third-party negotiation mechanism between separate concerns, and a simulation of a Promise.

function foo(){
    / /... Time consuming work
    // Construct and return the promise
	return new Promise((resolve,reject) = >{
		// The function is executed immediately})}const p=foo()
bar(p)
baz(p)
Copy the code

Instead of p being passed to bar() and baz(), p is used to control when these two functions are executed.

Then method

Note that there must be no custom then method on any delegate or value, otherwise this value will be mistaken for a thenable in the Promise system, causing untraceable bugs!!

Are promises trustworthy

Here are some of the problems we encountered during development and how to deal with promises

Call early

When the promise has been decided, the callback provided to then() is always called asynchronously, without inserting setTimeout() yourself.

Call too late

Once the Promise is resolved, all callbacks registered via then() on the Promise are called in turn at the next asynchronous point in time. None of these callbacks affects the invocation of the other callbacks, as follows:

p.then(() = >{
	p.then(() = >{
		console.log('C') // C cannot interrupt B
	})
	console.log('A')
})
p.then(() = >{console.log('B')})
// A B C
Copy the code

The callback was not called

Nothing could prevent Promise from informing him of his decision; If you have both a complete and a reject callback registered for a Promise, one will always be called when the Promise is decided.

So what if the promise itself never decides? We can create a Promise utility for timeouts and set race callbacks for timeouts, which we’ll see later when we discuss the Promise API.

Too few or too many calls

The normal number of calls is 1, too little is 0, it is not called; Too many calls, such as more than one resolve() or reject(), then the promise will accept only the first resolution and ignore any subsequent calls!!

Failed to pass parameter/environment value

If you do not explicitly decide in the promise with any value (i.e. no call to resolve or reject), this value is undefined; It is passed to all registration callbacks.

Or if you want to pass multiple values, just put them in an array.

Swallow the error or exception

If a javascript error occurs during the promise creation, this exception is caught and the promise is rejected. As follows:

const p=new Promise((resove,reject) = >{
	foo.bar() // Foo is not defined, so an error is thrown
	resolve(20)
})
p.then((num) = >{
	// Will not be output
	console.log(num)
},(err) = >{
	// Err will be a TypeError exception object
})
Copy the code

Whether the value passed in is a trusted promise

As mentioned in the last section, do not pass values that contain then()! So what do we do when we’re not sure?

Here’s a counterexample:

const p={
	then(cb,errcb){
		cb(20)
		errcb('this is err')
	}
}

p.then(
(val) = >{console.log(val)}, / / 20
// This should not be run!
(err) = >{console.log(err)} // this is err
)
Copy the code

It works like a normal function, and is not a promise mechanism. But we can encapsulate promise.resove () and get the desired result.

Promise.resolve(p)
	.then(
		(val) = >{console.log(val)},
		// Never get here!
		(err) = >{console.log(err)} 
		)
Copy the code

Chain flow

We can wire multiple promises together to represent a series of asynchronous steps, based on two inherent behavior characteristics of promises:

Each time a THEN call is made to a Promise, a new promise is created and returned, which we can link together. Whatever value is returned from the completion callback (first argument) of the call to THEN () is automatically set to the completion of the linked promise.

It’s easy to link promises together as follows:

const p=Promise.resolve(10)
	p.then((val) = >{return val*2})
	p.then((val) = >{console.log(val)}) / / 20
Copy the code

And, no matter how many asynchronous steps we want, each one can wait for the next one as needed; Of course, if a value is not returned explicitly, undefined is returned implicitly.

const p=Promise.resolve(10)
	p.then((val) = >{
		return new Promise((resolve,reject) = >{
			setTimeout(() = >{
				resolve(val*2)},500)
		})
	}).then((val) = >{console.log(val)}) / / 20
Copy the code

In a chained call, if there is an error on a step, it is dealt with on the nearest reject, and if there is none, an error is thrown.

const p=Promise.resolve(10)
	p.then((val) = >{ // The first then
		return new Promise((resolve,reject) = >{
			resolve(val*2)
			foo.bar() // This will not be executed, so no error will be thrown, and 60 will be printed
		})
	})
	.then((val) = >{ // The second then
		return val*3
	},
	() = >{
		console.log('oops!! ')
	})
	.then(val= >{ // The third then
		console.log(val)
	},
	() = >{
		console.log('oops!! 2 ')})Copy the code

If you change the position of resolve(val*2) and foo.bar(), foo. Bar is not declared, and the Promise resolution is rejected, passing undefined down because no value is set. The first reject encountered down (reject in the second then) is captured, output Oops! ; Since this THEN node has no errors and does not return a value, it is passed down until it is caught by resolve in the third THEN, output undefined.

If you put foo.bar() in the second then, it prints ‘oops!! 2 ‘. Putting foo.bar() into the third then throws an error if the chain after it has no reject.

Error handling

We are familiar with try.. Catch can only be synchronous and cannot be used in asynchronous code patterns.

But we also know from the last section that a PROMISE can catch an asynchronous error, but only if there is Reject in the next chain to the error, otherwise the error can only be thrown. To this end, many developers use the following practices to solve the above problems:

const handleErrors=(err) = >{console.log(err)}
const p=Promise.resolve(10)
p.then((val) = >{
	foo.bar()
	return val*2
})
.catch(handleErrors)
Copy the code

However, this approach is not comprehensive, and if there are errors in handleErrors, the errors in the promise will not be caught successfully.

Another solution is to set a timer that will start on rejection. If the promise is rejected and no error handler is registered before the timer goes off, it will not be registered to handle the error, and thus the error will not be caught. This method works well in many libraries, but the timing is arbitrary and may not be reliable if certain requests are truly pending for a long time.

Promise model

1.Promise.all([..] )

Perform more operations after completing multiple tasks. In the Promise chain, only one asynchronous task can be executing at any one time, and the desire to perform two or more steps simultaneously (in parallel) must be created using a gate.

The gate waits for two or more parallel/concurrent tasks to complete before continuing. The order of completion is not important, but they must all be completed in order for the door to open and for process control to continue.

Promise.all([p1,p2])
.then(msgs= >{
    // The MSG is also an array of completion messages for P1 and P2
})
Copy the code

Note that in P1 and P2, if either Promise is rejected, the primary promise.all () is rejected immediately and all the consequences from all the other promises are discarded.

2.Promise.race([])

This API refers to race mode. The traditional mode is called a latch, which only responds to the first promise that completes execution.

Similar to promise.all (), promise.race() is completed as soon as any Promise is decided to be completed; As soon as any promise was decided to refuse, he would refuse. If an empty array is passed, it will never be resolved.

We can use it to check if a request has timed out:

Promise.race([
request(),
timeoutPromise()
])
.then(() = >{
    // The request is completed on time
},
err= >{
    // Request () was rejected or timed out!!})Copy the code

Concurrent iteration

Sometimes you need to iterate through a list of Promises and perform a task for all of them. It’s like a synchronous array iteration, but asynchronous. Iterate over the promise (or any other value) using a map, and run a function on each value as an argument. The map itself returns a promise whose completion value is an array that preserves the mapping order and holds the asynchronous completion value after the task is executed:

Promise.map((vals,cb) = >{
    return Promise.all(
        vals.map(val= >
            new Promise(resolve= >cb(val,resolve))
        )
    )
})
Copy the code

In the map implementation above, you cannot send an asynchronous reject reject signal, but if a synchronous exception or error occurs in the map callback CB, the Promise returned by the primary promise.map () will be rejected.

Limitations of Promises

Promise seems like a magic solution to many of the asynchronous callbacks and makes the code clean and reliable. But nothing is 100% perfect, and promises have their limitations.

1. Sequence error handling

Because of the way promises are linked, it’s easy to inadvertently ignore errors in promises. If you build a promise chain with no error handling, anywhere in the chain is passed along the chain until it is viewed (by registering a rejection handler at some step).

2. A single value

A promise can only have one completion value or one rejection reason. In simple applications, this is not a problem; But in complex scenarios, you’ll find that this is a limitation. If you need to handle multiple values, encapsulate them directly as Promises, put them in an array, and use promise.all () for processing.

3. Single resolution

A Promise can only be decided once. If an event is bound to a Promise, the following is an example. This only works if the button response is clicked once. Click the second time, and the Promise is already resolved, so the second call to resolve() will be ignored. In the following example, only 50 will be printed on the first click, and subsequent clicks on resolve will be ignored.

function click(ele,event){
	document.getElementById(ele).onclick=event
}

const request=() = >new Promise((resolve,rej) = >{
	resolve(50)})const p=new Promise((resolve,reject) = >{
	click('test',resolve)
})

p.then(evt= >request())
.then(text= >{console.log(text)})
Copy the code

4. Promise you can’t cancel

If you set up a promise and register a complete or reject handler for it, there is no way to stop the process from the outside if something happens that makes the task unresolved.

Consider the previous timeout scenario: use promise.race () to set a timer that throws an error.

5. Promise’s performance issues

Promise worked more slowly than the callback plans it had built. But promise is worthy of trust, the loss of small performance but can make the whole system more credible, capricious and combinable; The code is much clearer.