最近在一个项目中, 遇到这么一个需求: 一个页面中, 大概有四五个元素需要按一定次序依次进场, setTimeout 来实现吧, 仔细一想, 那样的代码实在是写不下去, 大概是这样的:
- setTimeout(()=>{
- this.setState({view1Visible: true})
- setTimeout(()=>{
- this.setState({view2Visible: true})
- setTimeout(()=>{
- this.setState({view3Visible: true})
- setTimeout(()=>{
- // 没完没了的 setTimout...
- },500)
- },500)
- },500)
- },100)
明显的回调地狱, 对症下药, 用 Promise 来简单封装一下:
- const timer=(task,ms)=>{
- return new Promise((resolve,reject)=>{
- setTimeout(()=>{
- task && task()
- resolve()
- },ms)
- })
- }
然后之前的代码大致可以写成这样:
- timer(()=>this.setState({view1Visible: true}),100)
- .then(()=>timer(()=>this.setState({view2Visible: true}),500))
- .then(()=>timer(()=>this.setState({view3Visible: true}),500))
- .then(()=>timer(()=>this.setState({view4Visible: true}),500))
到这里基本已经满足我的需求了, 如果对不喜欢用 then, 或者只是对它有意见, 也可以用 async/await 来改写一下:
- async layout(){
- await timer(()=>this.setState({view1Visible: true}),100)
- await timer(()=>this.setState({view2Visible: true}),500)
- await timer(()=>this.setState({view3Visible: true}),500)
- await timer(()=>this.setState({view4Visible: true}),500)
- }
写到这里, 已经足够了, 不过我个人对 timer 的两个参数不喜欢, 而且我更喜欢写链式风格的代码, 理想的代码是这样的:
- new Schedule()
- .delay(100).task(()=>this.setState({view1Visible:true}))
- .task(()=>this.setState({view1Visible:true}))
- .task(()=>this.setState({view2Visible:true}))
- .task(()=>this.setState({view3Visible:true}))
首先 task 和 delay 分别用两个方法传参, 语义化嘛, 一眼就能看出这个参数指的是什么; 然后 delay 要能够复用, 很多情下我们任务之间的间隔是相等的, 就不用每次都传了.
实现方法嘛, 在 Schedule 类中, 要有个 promise 来处理这些任务, 然后需要一个变量来保存 delay, 来达到复用的目的, 然后就是 delay 和 task 两个方法, 都返回 this 来实现链式调用. 最后把上面那个 timer 方法拿过来, 解决回调地狱. 先看看最后的代码吧:
- export default class Schedule{
- constructor(){
- this._delay=0
- this.p = null
- }
- timer(task,ms){
- return new Promise((resolve,reject)=>{
- setTimeout(()=>{
- task && task()
- resolve()
- },ms)
- })
- }
- task(task){
- this.p = this.p ?
- this.p.then(()=>this.timer(task,this._delay)) :
- this.timer(task,this._delay)
- return this
- }
- delay(_delay){
- this._delay = _delay
- return this
- }
- }
也没啥特别的, 要注意的一点是, 第一次调用 task 的时候, p 为空, 直接给他赋值即可. 或者你一可以给 p 一个初始的 promise, 之后就不用考虑是否为空了, 直接 p.then() 就可以了.
对于一般的需求, 现在这个 Schedule 应该完全能够搞定, 可能你想这样做: 先把任务队列定义好, 到了特定的时机再去触发它执行, 那我们要怎么做呢?
其实也不难, 每次调用 task 的时候, 不放到 promise 里面, 而是把 task 和当前 delay 先保存到一个数组里面, 最后再写一个方法, 在调用的时候遍历这个数组, 把他们放到 promise 里面去, 直接上代码好了:
- export default class Schedule{
- constructor(){
- this._delay=0
- this.tasks=[]
- }
- timer(task,ms){
- return new Promise((resolve,reject)=>{
- setTimeout(()=>{
- task && task()
- resolve()
- },ms)
- })
- }
- task(task){
- this.tasks.push({task,delay:this._delay})
- return this
- }
- delay(_delay){
- this._delay = _delay
- return this
- }
- exec(){
- this.tasks.length>0 && this.tasks.reduce(
- (p,t)=>p.then(()=>this.timer(t.task,t.delay)),
- Promise.resolve()
- )
- }
- }
一个小小的技巧就是用数组的 reduce 方法来把这些 task 依次放到 promise 中, 在 reduce 的第二个参数传入一个空的 Promise, 就避免了判断是否有初始 Promise 的问题. 用的时候需要手动去调用 exec 方法, 整个队列才回开始执行:
- new Schedule()
- .delay(100).task(()=>this.setState({view1Visible:true}))
- .task(()=>this.setState({view1Visible:true}))
- .task(()=>this.setState({view2Visible:true}))
- .task(()=>this.setState({view3Visible:true}))
- .exec() // 可以在任何你需要的时候调用
需要介绍的就这些了, 最后其实有不少可以改进的地方, 比如上面说的两种情况, 完全可以写在一起, 构造方法中传个参数来决定是否是需要延迟执行的队列. 又或者引入 cron 表达式, 来决定在特定的时间点执行任务...... 当然这些不在本文讨论的范畴, 感兴趣的朋友可以去试试.
来源: http://www.jianshu.com/p/20721dd82dea