>

美洲杯在线投注_2019美洲杯外围投注[投注官网]

热门关键词: 美洲杯在线投注,2019美洲杯外围投注[投注官网]

Promise 异步流程控制

- 编辑:美洲杯在线投注 -

Promise 异步流程控制

Promise 异步流程控制

2017/10/04 · JavaScript · Promise

原文出处: 麦子谷   

var

图片 1前言

最近部门在招前端,作为部门唯一的前端,面试了不少应聘的同学,面试中有一个涉及 Promise 的一个问题是:

网页中预加载20张图片资源,分步加载,一次加载10张,两次完成,怎么控制图片请求的并发,怎样感知当前异步请求是否已完成?

然而能全部答上的很少,能够给出一个回调 计数版本的,我都觉得合格了。那么接下来就一起来学习总结一下基于 Promise 来处理异步的三种方法。

本文的例子是一个极度简化的一个漫画阅读器,用4张漫画图的加载来介绍异步处理不同方式的实现和差异,以下是 HTML 代码:

JavaScript

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Promise</title> <style> .pics{ width: 300px; margin: 0 auto; } .pics img{ display: block; width: 100%; } .loading{ text-align: center; font-size: 14px; color: #111; } </style> </head> <body> <div class="wrap"> <div class="loading">正在加载...</div> <div class="pics"> </div> </div> <script> </script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Promise</title>
  <style>
    .pics{
      width: 300px;
      margin: 0 auto;
    }
    .pics img{
      display: block;
      width: 100%;
    }
    .loading{
      text-align: center;
      font-size: 14px;
      color: #111;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="loading">正在加载...</div>
    <div class="pics">
    </div>
  </div>
  <script>
  </script>
</body>
</html>

1.可以重复声明

单一请求

最简单的,就是将异步一个个来处理,转为一个类似同步的方式来处理。 先来简单的实现一个单个 Image 来加载的 thenable 函数和一个处理函数返回结果的函数。

JavaScript

function loadImg (url) { return new Promise((resolve, reject) => { const img = new Image() img.onload = function () { resolve(img) } img.onerror = reject img.src = url }) }

1
2
3
4
5
6
7
8
9
10
function loadImg (url) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = function () {
      resolve(img)
    }
    img.onerror = reject
    img.src = url
  })
}

异步转同步的解决思想是:当第一个 loadImg(urls[1]) 完成后再调用 loadImg(urls[2]),依次往下。如果 loadImg() 是一个同步函数,那么很自然的想到用__循环__。

JavaScript

for (let i = 0; i < urls.length; i ) { loadImg(urls[i]) }

1
2
3
for (let i = 0; i < urls.length; i ) {
  loadImg(urls[i])
}

当 loadImg() 为异步时,我们就只能用 Promise chain 来实现,最终形成这种方式的调用:

JavaScript

loadImg(urls[0]) .then(addToHtml) .then(()=>loadImg(urls[1])) .then(addToHtml) //... .then(()=>loadImg(urls[3])) .then(addToHtml)

1
2
3
4
5
6
7
loadImg(urls[0])
  .then(addToHtml)
  .then(()=>loadImg(urls[1]))
  .then(addToHtml)
  //...
  .then(()=>loadImg(urls[3]))
  .then(addToHtml)

那我们用一个中间变量来存储当前的 promise ,就像链表的游标一样,改过后的 for 循环代码如下:

JavaScript

let promise = Promise.resolve() for (let i = 0; i < urls.length; i ) { promise = promise .then(()=>loadImg(urls[i])) .then(addToHtml) }

1
2
3
4
5
6
let promise = Promise.resolve()
for (let i = 0; i < urls.length; i ) {
promise = promise
.then(()=>loadImg(urls[i]))
.then(addToHtml)
}

promise 变量就像是一个迭代器,不断指向最新的返回的 Promise,那我们就进一步使用 reduce 来简化代码。

JavaScript

urls.reduce((promise, url) => { return promise .then(()=>loadImg(url)) .then(addToHtml) }, Promise.resolve())

1
2
3
4
5
urls.reduce((promise, url) => {
  return promise
    .then(()=>loadImg(url))
    .then(addToHtml)
}, Promise.resolve())

在程序设计中,是可以通过函数的__递归__来实现循环语句的。所以我们将上面的代码改成__递归__:

JavaScript

function syncLoad (index) { if (index >= urls.length) return loadImg(urls[index]).then(img => { // process img addToHtml(img) syncLoad (index 1) }) } // 调用 syncLoad(0)

1
2
3
4
5
6
7
8
9
10
11
function syncLoad (index) {
  if (index >= urls.length) return
      loadImg(urls[index]).then(img => {
      // process img
      addToHtml(img)
      syncLoad (index 1)
    })
}
 
// 调用
syncLoad(0)

好了一个简单的异步转同步的实现方式就已经完成,我们来测试一下。 这个实现的简单版本已经实现没问题,但是最上面的正在加载还在,那我们怎么在函数外部知道这个递归的结束,并隐藏掉这个 DOM 呢?Promise.then() 同样返回的是 thenable 函数 我们只需要在 syncLoad 内部传递这条 Promise 链,直到最后的函数返回。

JavaScript

function syncLoad (index) { if (index >= urls.length) return Promise.resolve() return loadImg(urls[index]) .then(img => { addToHtml(img) return syncLoad (index 1) }) } // 调用 syncLoad(0) .then(() => { document.querySelector('.loading').style.display = 'none' })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function syncLoad (index) {
  if (index >= urls.length) return Promise.resolve()
  return loadImg(urls[index])
    .then(img => {
      addToHtml(img)
      return syncLoad (index 1)
    })
}
 
// 调用
syncLoad(0)
  .then(() => {
  document.querySelector('.loading').style.display = 'none'
})

现在我们再来完善一下这个函数,让它更加通用,它接受__异步函数__、异步函数需要的参数数组、__异步函数的回调函数__三个参数。并且会记录调用失败的参数,在最后返回到函数外部。另外大家可以思考一下为什么 catch 要在最后的 then 之前。

JavaScript

function syncLoad (fn, arr, handler) { if (typeof fn !== 'function') throw TypeError('第一个参数必须是function') if (!Array.isArray(arr)) throw TypeError('第二个参数必须是数组') handler = typeof fn === 'function' ? handler : function () {} const errors = [] return load(0) function load (index) { if (index >= arr.length) { return errors.length > 0 ? Promise.reject(errors) : Promise.resolve() } return fn(arr[index]) .then(data => { handler(data) }) .catch(err => { console.log(err) errors.push(arr[index]) return load(index 1) }) .then(() => { return load (index 1) }) } } // 调用 syncLoad(loadImg, urls, addToHtml) .then(() => { document.querySelector('.loading').style.display = 'none' }) .catch(console.log)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function syncLoad (fn, arr, handler) {
  if (typeof fn !== 'function') throw TypeError('第一个参数必须是function')
  if (!Array.isArray(arr)) throw TypeError('第二个参数必须是数组')
  handler = typeof fn === 'function' ? handler : function () {}
  const errors = []
  return load(0)
  function load (index) {
    if (index >= arr.length) {
      return errors.length > 0 ? Promise.reject(errors) : Promise.resolve()
    }
    return fn(arr[index])
      .then(data => {
        handler(data)
      })
      .catch(err => {
        console.log(err)              
        errors.push(arr[index])
        return load(index 1)
      })
      .then(() => {
        return load (index 1)
      })
  }
}
 
// 调用
syncLoad(loadImg, urls, addToHtml)
  .then(() => {
    document.querySelector('.loading').style.display = 'none'
  })
  .catch(console.log)

demo1地址:单一请求 – 多个 Promise 同步化

至此,这个函数还是有挺多不通用的问题,比如:处理函数必须一致,不能是多种不同的异步函数组成的队列,异步的回调函数也只能是一种等。关于这种方式的更详细的描述可以看我之前写的一篇文章 Koa引用库之Koa-compose

当然这种异步转同步的方式在这一个例子中并不是最好的解法,但当有合适的业务场景的时候,这是很常见的解决方案。

2.无法限制修改

并发请求

毕竟同一域名下能够并发多个 HTTP 请求,对于这种不需要按顺序加载,只需要按顺序来处理的并发请求,Promise.all 是最好的解决办法。因为Promise.all 是原生函数,我们就引用文档来解释一下。

Promise.all(iterable) 方法指当所有在可迭代参数中的 promises 已完成,或者第一个传递的 promise(指 reject)失败时,返回 promise。
出自 Promise.all() – JavaScript | MDN

那我们就把demo1中的例子改一下:

JavaScript

const promises = urls.map(loadImg) Promise.all(promises) .then(imgs => { imgs.forEach(addToHtml) document.querySelector('.loading').style.display = 'none' }) .catch(err => { console.error(err, 'Promise.all 当其中一个出现错误,就会reject。') })

1
2
3
4
5
6
7
8
9
const promises = urls.map(loadImg)
Promise.all(promises)
  .then(imgs => {
    imgs.forEach(addToHtml)
    document.querySelector('.loading').style.display = 'none'
  })
  .catch(err => {
    console.error(err, 'Promise.all 当其中一个出现错误,就会reject。')
  })

demo2地址:并发请求 – Promise.all

3.没有块级作用域

并发请求,按顺序处理结果

Promise.all 虽然能并发多个请求,但是一旦其中某一个 promise 出错,整个 promise 会被 reject 。 webapp 里常用的资源预加载,可能加载的是 20 张逐帧图片,当网络出现问题, 20 张图难免会有一两张请求失败,如果失败后,直接抛弃其他被 resolve 的返回结果,似乎有点不妥,我们只要知道哪些图片出错了,把出错的图片再做一次请求或着用占位图补上就好。 上节中的代码 const promises = urls.map(loadImg) 运行后,全部都图片请求都已经发出去了,我们只要按顺序挨个处理 promises 这个数组中的 Promise 实例就好了,先用一个简单点的 for 循环来实现以下,跟第二节中的单一请求一样,利用 Promise 链来顺序处理。

JavaScript

let task = Promise.resolve() for (let i = 0; i < promises.length; i ) { task = task.then(() => promises[i]).then(addToHtml) }

1
2
3
4
let task = Promise.resolve()
for (let i = 0; i < promises.length; i ) {
  task = task.then(() => promises[i]).then(addToHtml)
}

改成 reduce 版本

JavaScript

promises.reduce((task, imgPromise) => { return task.then(() => imgPromise).then(addToHtml) }, Promise.resolve())

1
2
3
promises.reduce((task, imgPromise) => {
  return task.then(() => imgPromise).then(addToHtml)
}, Promise.resolve())

demo3地址:Promise 并发请求,顺序处理结果

let 不能重复声明,变量-可以修改,块级作用域

控制最大并发数

现在我们来试着完成一下上面的笔试题,这个其实都__不需要控制最大并发数__。 20张图,分两次加载,那用两个 Promise.all 不就解决了?但是用 Promise.all没办法侦听到每一张图片加载完成的事件。而用上一节的方法,我们既能并发请求,又能按顺序响应图片加载完成的事件。

JavaScript

let index = 0 const step1 = [], step2 = [] while(index < 10) { step1.push(loadImg(`./images/pic/${index}.jpg`)) index = 1 } step1.reduce((task, imgPromise, i) => { return task .then(() => imgPromise) .then(() => { console.log(`第 ${i 1} 张图片加载完成.`) }) }, Promise.resolve()) .then(() => { console.log('>> 前面10张已经加载完!') }) .then(() => { while(index < 20) { step2.push(loadImg(`./images/pic/${index}.jpg`)) index = 1 } return step2.reduce((task, imgPromise, i) => { return task .then(() => imgPromise) .then(() => { console.log(`第 ${i 11} 张图片加载完成.`) }) }, Promise.resolve()) }) .then(() => { console.log('>> 后面10张已经加载完') })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
let index = 0
const step1 = [], step2 = []
 
while(index < 10) {
  step1.push(loadImg(`./images/pic/${index}.jpg`))
  index = 1
}
 
step1.reduce((task, imgPromise, i) => {
  return task
    .then(() => imgPromise)
    .then(() => {
      console.log(`第 ${i 1} 张图片加载完成.`)
    })
}, Promise.resolve())
  .then(() => {
    console.log('>> 前面10张已经加载完!')
  })
  .then(() => {
    while(index < 20) {
      step2.push(loadImg(`./images/pic/${index}.jpg`))
      index = 1
    }
    return step2.reduce((task, imgPromise, i) => {
      return task
        .then(() => imgPromise)
        .then(() => {
          console.log(`第 ${i 11} 张图片加载完成.`)
        })
    }, Promise.resolve())
  })
  .then(() => {
    console.log('>> 后面10张已经加载完')
  })

上面的代码是针对题目的 hardcode ,如果笔试的时候能写出这个,都已经是非常不错了,然而并没有一个人写出来,said…

demo4地址(看控制台和网络请求):Promise 分步加载 – 1

那么我们在抽象一下代码,写一个通用的方法出来,这个函数返回一个 Promise,还可以继续处理全部都图片加载完后的异步回调。

JavaScript

function stepLoad (urls, handler, stepNum) { const createPromises = function (now, stepNum) { let last = Math.min(stepNum now, urls.length) return urls.slice(now, last).map(handler) } let step = Promise.resolve() for (let i = 0; i < urls.length; i = stepNum) { step = step .then(() => { let promises = createPromises(i, stepNum) return promises.reduce((task, imgPromise, index) => { return task .then(() => imgPromise) .then(() => { console.log(`第 ${index 1

  • i} 张图片加载完成.`) }) }, Promise.resolve()) }) .then(() => { let current = Math.min(i stepNum, urls.length) console.log(`>> 总共${current}张已经加载完!`) }) } return step }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function stepLoad (urls, handler, stepNum) {
const createPromises = function (now, stepNum) {
    let last = Math.min(stepNum now, urls.length)
    return urls.slice(now, last).map(handler)
  }
  let step = Promise.resolve()
  for (let i = 0; i < urls.length; i = stepNum) {
    step = step
      .then(() => {
        let promises = createPromises(i, stepNum)
        return promises.reduce((task, imgPromise, index) => {
          return task
            .then(() => imgPromise)
            .then(() => {
              console.log(`第 ${index 1 i} 张图片加载完成.`)
            })
        }, Promise.resolve())
      })
      .then(() => {
        let current = Math.min(i stepNum, urls.length)
        console.log(`>> 总共${current}张已经加载完!`)
      })
  }
return step
}

上面代码里的 for 也可以改成 reduce ,不过需要先将需要加载的 urls 按分步的数目,划分成数组,感兴趣的朋友可以自己写写看。

demo5地址(看控制台和网络请求):Promise 分步 – 2

但上面的实现和我们说的__最大并发数控制__没什么关系啊,最大并发数控制是指:当加载 20 张图片加载的时候,先并发请求 10 张图片,当一张图片加载完成后,又会继续发起一张图片的请求,让并发数保持在 10 个,直到需要加载的图片都全部发起请求。这个在写爬虫中可以说是比较常见的使用场景了。 那么我们根据上面的一些知识,我们用两种方式来实现这个功能。

const 不能重复声明,变量-不可以修改,块级作用域

使用递归

假设我们的最大并发数是 4 ,这种方法的主要思想是相当于 4 个__单一请求__的 Promise 异步任务在同时运行运行,4 个单一请求不断递归取图片 URL 数组中的 URL 发起请求,直到 URL 全部取完,最后再使用 Promise.all 来处理最后还在请求中的异步任务,我们复用第二节__递归__版本的思路来实现这个功能:

JavaScript

function limitLoad (urls, handler, limit) { const sequence = [].concat(urls) // 对数组做一个拷贝 let count = 0 const promises = [] const load = function () { if (sequence.length <= 0 || count > limit) return count = 1 console.log(`当前并发数: ${count}`) return handler(sequence.shift()) .catch(err => { console.error(err) }) .then(() => { count -= 1 console.log(`当前并发数:${count}`) }) .then(() => load()) } for(let i = 0; i < limit && i < sequence.length; i ){ promises.push(load()) } return Promise.all(promises) }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function limitLoad (urls, handler, limit) {
  const sequence = [].concat(urls) // 对数组做一个拷贝
  let count = 0
  const promises = []
 
  const load = function () {
    if (sequence.length <= 0 || count > limit) return
    count = 1
    console.log(`当前并发数: ${count}`)
    return handler(sequence.shift())
      .catch(err => {
        console.error(err)
      })
      .then(() => {
        count -= 1
        console.log(`当前并发数:${count}`)
      })
      .then(() => load())
  }
 
  for(let i = 0; i < limit && i < sequence.length; i ){
    promises.push(load())
  }
  return Promise.all(promises)
}

设定最大请求数为 5,Chrome 中请求加载的 timeline :图片 2

demo6地址(看控制台和网络请求):Promise 控制最大并发数 – 方法1

箭头函数

使用 Promise.race

Promise.race 接受一个 Promise 数组,返回这个数组中最先被 resolve 的 Promise 的返回值。终于找到 Promise.race 的使用场景了,先来使用这个方法实现的功能代码:

JavaScript

function limitLoad (urls, handler, limit) { const sequence = [].concat(urls) // 对数组做一个拷贝 let count = 0 let promises const wrapHandler = function (url) { const promise = handler(url).then(img => { return { img, index: promise } }) return promise } //并发请求到最大数 promises = sequence.splice(0, limit).map(url => { return wrapHandler(url) }) // limit 大于全部图片数, 并发全部请求 if (sequence.length <= 0) { return Promise.all(promises) } return sequence.reduce((last, url) => { return last.then(() => { return Promise.race(promises) }).catch(err => { console.error(err) }).then((res) => { let pos = promises.findIndex(item => { return item == res.index }) promises.splice(pos, 1) promises.push(wrapHandler(url)) }) }, Promise.resolve()).then(() => { return Promise.all(promises) }) }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function limitLoad (urls, handler, limit) {
  const sequence = [].concat(urls) // 对数组做一个拷贝
  let count = 0
  let promises
  const wrapHandler = function (url) {
    const promise = handler(url).then(img => {
      return { img, index: promise }
    })
    return promise
  }
  //并发请求到最大数
  promises = sequence.splice(0, limit).map(url => {
    return wrapHandler(url)
  })
  // limit 大于全部图片数, 并发全部请求
  if (sequence.length <= 0) {
    return Promise.all(promises)
  }
  return sequence.reduce((last, url) => {
    return last.then(() => {
      return Promise.race(promises)
    }).catch(err => {
      console.error(err)
    }).then((res) => {
      let pos = promises.findIndex(item => {
        return item == res.index
      })
      promises.splice(pos, 1)
      promises.push(wrapHandler(url))
    })
  }, Promise.resolve()).then(() => {
    return Promise.all(promises)
  })
}

设定最大请求数为 5,Chrome 中请求加载的 timeline :图片 3

demo7地址(看控制台和网络请求):Promise 控制最大并发数 – 方法2

在使用 Promise.race 实现这个功能,主要是不断的调用 Promise.race 来返回已经被 resolve 的任务,然后从 promises 中删掉这个 Promise 对象,再加入一个新的 Promise,直到全部的 URL 被取完,最后再使用 Promise.all 来处理所有图片完成后的回调。

function 名字(){

写在最后

因为工作里面大量使用 ES6 的语法,Koa 中的 await/async 又是 Promise 的语法糖,所以了解 Promise 各种流程控制是对我来说是非常重要的。写的有不明白的地方和有错误的地方欢迎大家留言指正,另外还有其他没有涉及到的方法也请大家提供一下新的方式和方法。

}

题外话

我们目前有 1 个前端的 HC,base 深圳,一家拥有 50 架飞机的物流公司的AI部门,要求工作经验三年以上,这是公司社招要求的。 感兴趣的就联系我吧,Email: d2hlYXRvQGZveG1haWwuY29t

()=>{}

参考资料

图片 4

1.如果只有一个参数 ()可以省

2.如果只有一个return,{}可以省

let show=a=>a*2

arr.sort((n1,n2)=>n1-n2)

函数的参数

1.参数的扩展和数组展开

(1)收集剩余的参数 

function show(a,b,...args){}

(2)数组的展开

let arr=[1,2,3];

show(...arr);

function show(a,b,c){

alert(a);alert(b);alert(c)

}

let a=[1,1,1]

let b=[2,3,4]

let c=[...a,...b]

2.默认参数

function a(b=3){}

解构赋值

1.左右两边结构一样

2.右边是个东西

3.声明和赋值放一起写

let [a,b,c]=[1,2,3]

let {a,c,d}={a:12,c:5,d:6}

let [{a,b},[n1,n2,n3],num,str]=[{a:12,b:5},[12,3,4],5,'ddf']

数组:

map 映射  一个对一个

let arr=[1,2,3]

let result =arr.map(function(item){

return item*2

})

let result =arr.map(item=>item*2)

let score=[19,34,56,75,43]

let result= score.map(item =>item>=60?'及格':'不及格')

reduce 汇总  一堆出来一个

let arr=[12,23,4,5]

let result=arr.reduce(function(tmp,item,index){

tmp 中间结果

item 第几项的值

index 第几项

})

求平局

let arr =[23,34,54,45]

let result=arr.reduce(function(tmp,item,index){

if(index!=arr.length-1){
    return tmp item;

}else{

      return(tmp item)/arr.length

}

})

alert(result)

filter 过滤器

let arr=[12,32,34,99,45]

let result =arr.filter(item=>item%3==0)

alert (result)

let arr=[

{title:'xie',price:74},

{title:'wazi',price:734344},

{title:'kuzi',price:724}

];

let result=arr.filter(json=>json.price>=10000);

console.log(result)

forEach 迭代

let arr=[12,3,4,5]

arr.forEach((item,index)=>{

alert(index ':' item);

});

字符串

1.startsWith()

2.endsWith()

布尔值

3 字符串模板

let title='标题';

let content='内容';

let str=`<div>

<h1>${title}</h1>

<p>${content}</p>

</div>`;

1.可以直接把东西放在字符串里面 ${东西}

2.可以折行

面向对象

class User{

constructor(name,pass){

this.name=name;

this.pass=pass;

}

showName(){

   alert(this.name);

}

showPass(){

    alert(this.pass)

}

}

var u1=new User('dsfs','22')

u1.showName;

u2.showPass;

1.class关键字 构造器和类分开

2.class里面直接加方法

继承:

本文由计算机教程发布,转载请注明来源:Promise 异步流程控制