JS 链式调用和流程控制
2021年4月13日 · 预计阅读时间: 6 分钟
博主在某工作室担任前端导师 (知识搬运工) 的时候,遇到了其他导师出了一道 JS 题目,要求实现以下输出
Student('fxy')
// =>输出:
// Hi! This is fxy!
Student('fxy').sleep(3).study('javascript')
// =>输出:
// Hi! This is fxy!
//等待 3 秒
// Wake up after 3
// Study javascript~
Student('fxy').study('javascript').study('Vue')
// =>输出:
// Hi! This is fxy!
// Study javascript~
// Study Vue~
Student('fxy').sleepFirst(5).study('Ajax')
// =>输出
// //等待 5s
// Wake up after 5
// Hi! This is fxy!
// Study Ajax
本篇文章不叙述实现过程,主要分析一下执行过程和原理,实现如下:
function Student(name) {
Student.cbs = []
Student.cbs.push(() => {
console.log(`Hi! This is ${name}!`)
Student.next()
})
setTimeout(() => {
Student.next()
}, 0)
return Student
}
Student.next = function () {
const cb = Student.cbs.shift()
cb && cb()
}
Student.sleep = function (time) {
Student.cbs.push(() => {
setTimeout(() => {
console.log(`Wake up after ${time}`)
Student.next()
}, time * 1000)
})
return Student
}
Student.sleepFirst = function (time) {
Student.cbs.unshift(() => {
setTimeout(() => {
console.log(`Wake up after ${time}`)
Student.next()
}, time * 1000)
})
return Student
}
Student.study = function (thing) {
Student.cbs.push(() => {
console.log(`Study ${thing}`)
Student.next()
})
return Student
}
// Student("fxy")
// Student("fxy").sleep(3).study("javascript")
// Student("fxy").study("javascript").study("Vue")
// Student("fxy").sleepFirst(5).study("Ajax")
原理分析
首先初始化的时候定义了一个数组Student.cbs = []
,保存函数队列,我们之后只需要操作这个数组就行。
next()
方法控制函数出队列并执行,由Student()
中的
setTimeout(() => {
Student.next()
}, 0)
启动函数执行
我们来根据实例讲讲执行流程,
第一个很简单,
Student('fxy')
讲函数 push 进队列后,经过 setTimeout 调用 next() 方法执行函数,打印信息
第二个
Student('fxy').sleep(3).study('javascript')
// Hi! This is fxy!
// 等待 3 秒
// Wake up after 3
// Study javascript~
首先提醒一点,以上链式调用等同于
Student('fxy')
Student.sleep(3)
Student.study('javascript')
三个同步任务执行,所以在 cbs 里应该有[Student,sleep,study]
(用函数名代替输出)
Student
首先出队执行调用执行next()
,sleep
函数出队执行,3s 后进入宏任务队列输出,继续调用next()
函数,让study
函数出队执行,也会调用一次next()
函数,但此时队列已空。
第三个经过上面的解释很简单了,我们直接来看第四个
Student('fxy').sleepFirst(5).study('Ajax')
// 等待 5s
// Wake up after 5
// Hi! This is fxy!
// Study Ajax
sleepFirst()
很神奇的在Student()
之前先输出了,我们来看看sleepFirst()
函数里面究竟干了什么。
Student.sleepFirst = function (time) {
Student.cbs.unshift(() => {
setTimeout(() => {
console.log(`Wake up after ${time}`)
Student.next()
}, time * 1000)
})
return Student
}
sleepFirst()
函数将一个函数用unshift
方法放入队列顶部,实现了sleepFirst()
的首先输出
上面的问题就这样轻松解决了
意外的输出
一个学员看了我上面的实现之后,执行了下面这段输出
Student('fxy')
Student('fxy').sleep(3).study('javascript')
输出结果也很让人迷惑
// Hi! This is fxy!
// Study javascript
// Wake up after 3
小朋友,你是否有很多小问号?
为什么Student
怎么只输出了一次?study
的输出怎么跑到了sleep
后面?
不急,我们一个一个问题来看
首先时为什么Student
怎么只输出了一次?
很简单,因为我们的cbs
是定义在Student()
函数里面的,两次调用Student()
函数,相当于初始化了两次cbs
,所以有一个Student
里的输出被漏掉了。如果我们将cbs
定义在Student()
函数外面就会输出两次。
// Hi! This is fxy!
// Hi! This is fxy!
// Study javascript
// Wake up after 3
那为什么study
的输出怎么跑到了sleep
后面?
这个问题就得说到事件循环了,当一个宏任务执行完后,会检查微任务队列中有没有待执行的函数,显然我们这里没有微任务,那么就会执行下一个宏任务。
而我们Student
触发用的的实际上是第一个Student
中的next()
,第二个Student
中的next()
还在宏任务队列中等待执行。第一个Student
调用next()
让sleep()
出队执行,但是有 3s 的延时才会进入宏任务队列,第二个Student
也调用next()
让study()
出队执行,没有延时直接进入宏任务队列执行,之后sleep()
才进入宏任务队列执行,所以study
的输出跑到了sleep
后面