Let's build Promise from scratch.
My current understanding
The use of promises is basically to add a function to javascript's task queue once a value has been obtained by a long-running process (that process is usually carried out in a different thread or other part of the os, like when we request some value over the network or from hard disk).
let p = new Promise((resolve,reject) => {
try {
const value = //obtain some value from a long running task.
resolve(value)
} catch (e) {
reject(e)
}
})
Now p is container for the value we actually want. Later in our code, when we want to do something with that value we can do that like so:
p.then((val) => {
// do something with this value
})
what we've done above is basically we've added the function (given to .then
as an argument) to javascript's task queue. The event loop will pick it up when it gets time and add it to the call stack to be executed.
Breaking it down
So a promise is container for a value. It's constructor takes in a function. That function gets two arguments - a resolve
function and a reject
function. A promise also has a state indicating whether it is pending
, fulfilled
(resolve has been called), or rejected
(reject has been called).
Each time we call .then
on the promise we are adding a function to a list. When the promise is fulfilled
we need to execute all the functions in that list.
We'll implement the constructor first:
class MyPromise {
constructor(func) {
this.value = undefined
this.state = "pending"
this.fulfillmentTasks = []
this.rejectionTasks = []
this.resolve = this.resolve.bind(this)
this.reject= this.reject.bind(this)
if (func) {
func(this.resolve,this.reject)
}
}
resolve(value) {
if (this.state !== "pending") return this
this.state = "fulfilled"
this.value = value
this.rejectionTasks = []
this.fulfillmentTasks.map(t => setTimeout(t, 0))
this.fullfillmentTasks = []
return this
}
reject(value) {
if (this.state !== "pending") return this
this.state = "rejected"
this.value = value
this.fullfillmentTasks = []
this.rejectionTasks.map(t => setTimeout(t, 0))
this.rejectionTasks = []
return this
}
}
Implementing .then
MyPromise.prototype.then = function(onFulfilled, onRejected) {
switch (this.state) {
case "pending": {
this.fulfillmentTasks.push(() => onFulfilled(this.value))
this.rejectionTasks.push(() => onRejected(this.value))
break
}
case "fulfilled":
setTimeout(() => onFulfilled(this.value), 0)
break
case "rejected":
setTimeout(() => onRejected(this.value), 0)
break
}
}
const p = new MyPromise((res,rej) => {
res(2)
})
p.then((val) => console.log(val))
p.then((val) => console.log(val*2))
p.then((val) => console.log(val*3))
p.then((val) => console.log(val*4))
Chaining .then
callbacks:
const p = new Promise((res,rej) => {
res(2)
})
p.then((val) => val*2).then((val) => val*2).then((val) => val*2).then((val) => console.log(val))
// should print 16
For this we need to return a promise from .then
function
MyPromise.prototype.then = function(onFulfilled, onRejected) {
const newPromise = new MyPromise()
const fullfillmentTask = () => {
const newValue = onFulfilled(this.value)
newPromise.resolve(newValue)
}
switch (this.state) {
case "pending": {
this.fulfillmentTasks.push(fullfillmentTask)
this.rejectionTasks.push(() => onRejected(this.value))
break
}
case "fulfilled":
setTimeout(fullfillmentTask, 0)
break
case "rejected":
setTimeout(() => onRejected(this.value), 0)
break
}
return newPromise
}
const newp = new MyPromise((res,rej) => {
res(2)
})
newp.then((val) => val*2).then((val) => val*2).then((val) => val*2).then((val) => console.log("val ->", val))
module.exports.MyPromise = MyPromise
Flattening the .then
const p = new MyPromise((res) => {
const p1 = new MyPromise((r) => r(2))
res(p1)
})
p.then((val) => val.then(v => console.log(v)))
//prints 2
// But what we want is :
p.then(val => console.log(val))
//prints 2
//so in effect p "becomes" p1
To handle the above scenario:
- p needs to "become" p1
- p's
value
is p1's value - p's state is p1's state
- we can't call
p.resolve()
once p1resolves
const imp = require("./main.js")
const MyPromise = imp.MyPromise
function isThenable(val) {
return typeof val === "object" && val !== null && typeof val.then === "function"
}
MyPromise.prototype.fulfill = function(val) {
if (isThenable(val)) return
this.value = val
this.state = "fulfilled"
this.rejectionTasks = []
this.fulfillmentTasks.map(t => setTimeout(t, 0))
this.fullfillmentTasks = []
}
MyPromise.prototype.resolve = function(value) {
if (this.settled) return this
this.settled = true
if (isThenable(value)) {
value.then((v) => this.fulfill(v))
} else {
this.fulfill(value)
}
return this
}
//testing the flattening pattern.
const p0 = new MyPromise((res) => {
const p1 = new MyPromise((r) => r(2))
res(p1)
})
p0.then(val => console.log("flattened val -> ", val))