Fork me on GitHub

JS-异步编程

JS 异步编程以及面试题

并发(concurrency)和并行(parallelism)区别

  • 并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
    通常在多处理器系统中存在。
    并行
  • 并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
    并发可以在单处理器和多处理器系统中都存在。
    在单处理器系统中存在是因为并发是并行的假象,并行要求程序能够同时执行多个操作,而并发只是要求程序假装同时执行多个操作(每个小时间片执行一个操作,多个操作快速切换执行)
    并发

当有多个线程在操作时,如果系统只有一个 CPU,则它根本不可能真正同时进行一个以上的线程,它只能把 CPU 运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent)。
当系统有一个以上 CPU 时,则线程的操作有可能非并发。当一个 CPU 执行一个线程时,另一个 CPU 可以执行另一个线程,两个线程互不抢占 CPU 资源,可以同时进行,这种方式我们称之为并行(Parallel)。

同步和异步

  • 同步编程,即是一种典型的请求-响应模型,当请求调用一个函数或方法后,需等待其响应返回,然后执行后续代码。
  • 异步编程,不同于同步编程的请求-响应模式,其是一种事件驱动编程,请求调用函数或方法后,无需立即等待响应,可以继续执行其他任务,而之前任务响应返回后可以通过状态、通知和回调来通知调用者。

回调函数(Callback)

把一个函数作为参数传递给另一个函数,并在此函数中执行。

1
2
3
ajax('/test.html',function(data){
// 处理逻辑
})

回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:

1
2
3
4
5
6
7
8
9
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})

回调地狱的根本问题是:

  1. 嵌套函数存在耦合性,一旦有所改动,就会影响其他嵌套函数
  2. 嵌套函数难以处理错误

定时器

  • setTimeout,
    setTimeOut产生一个宏任务,加入宏任务队列,等待当前宏任务执行完毕并且清空了微任务才会从宏任务队列中按照先进先出原则执行下一个宏任务。
    因 JavaScript 是单线程的,所以setTimeout不一定会按照设置的延迟时间准时执行,如果当前宏任务和微任务执行花费的时间大于设置的延迟时间,可能延后。
  • setInterval
    每隔一段时间执行一次回调函数。
    通常不建议使用setInterval,第一,不能保证在预期的时间执行任务。第二,存在执行累积的问题。

    1
    2
    3
    4
    5
    6
    7
    function demo() {
    setInterval(function(){
    console.log(2)
    },1000)
    sleep(2000)
    }
    demo()

以上代码在浏览器环境中,如果定时器执行过程中出现了耗时操作,多个回调函数会在耗时操作结束以后同时执行,这样可能就会带来性能上的问题。

  • requestAnimationFrame
    参数:回调函数。
    返回值:一个整数,表示定时器的编号。可以传递给cancelAnimationFrame用于取消动画。
    告诉浏览器你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
    注意,如果想在浏览器下次重绘之前继续更新下一帧动画,回调函数自身必须再次调用window.requestAnimationFrame()
    setTimeoutsetInterval 的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行。
    requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

    特点

    1. 把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
    2. 在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流。
    3. requestAnimationFrame是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //html
    <div id='process' style="background-color: lightblue;width: 0;height: 20px;line-height: 20px;">0%</div>
    <button id='loginBtn'>loginBtn</button>

    //js
    let process = document.getElementById('process');
    let btn = document.getElementById('loginBtn');
    let timer = null;
    btn.onclick = () => {
    process.style.width = '0';
    window.cancelAnimationFrame(timer);
    timer = window.requestAnimationFrame(loop)
    }

    function loop() {
    if (parseInt(process.style.width) < 500) {
    process.style.width = parseInt(process.style.width) + 5 + 'px';
    process.innerHTML = parseInt(process.style.width) / 5 + '%';
    timer = requestAnimationFrame(loop)
    } else {
    window.cancelAnimationFrame(timer)
    }
    }

Generator

执行过程

执行步骤:

  1. Generator 函数执行后,生成一个指向内部状态的指针,也就是遍历器对象Iterator
  2. 第一次调用next函数,函数参数会被忽略,函数会执行到yield就暂停,并且将yield后面跟的表达式的值作为value返回。{value:value,done:true/false},但是yield返回的永远是undefined
  3. 后续调用next函数,函数就会从上一次停下来的位置执行,并且next的参数会代替yield的返回值undefined,继续执行到下一个yield。返回{value:value,done:true/false}
  4. 如果没有 yield,就会一直执行,直到遇到 return,将 return 后面的表达式的值返回,{value:value ,done:true }
  5. 如果没有 return,则返回的对象{value:undefined ,done:false }
1
2
3
4
5
6
7
8
9
10
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)

}
let it = foo(5)//->1, 生成遍历器
console.log(it.next()) // ->2 执行x+1=6 返回{value: 6, done: false},停止执行。即使执行it.next(11),参数也会被忽略
console.log(it.next(12)) //->3 参数12作为上次yield的返回值,y=2*12=24,然后返回 => {value: 8, done: false},如果没有参数,就是y=2*undefined=NaN
console.log(it.next(13)) // ->4 z=13 5+24+13 返回{value: 42, done: true}

可以把上面的回调地狱的例子改成如下代码:

1
2
3
4
5
6
7
8
9
function *fecth(){
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

详情见 ES6—16、Generator 函数的语法
详情见 ES6—17、Generator 函数的异步应用

Promise

1
2
3
4
5
6
7
8
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))

Promise

async/await

async/await
一个函数如果加上 async ,那么该函数就会返回一个 Promise

1
2
3
4
async function test() {
return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}

async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then中处理返回值一样,并且 await 只能配套 async 使用
asyncawait是目前异步的终极解决方案,优点在于语义化更强,能够解决回调地狱问题。
缺点:在于滥用await可能会导致性能问题,因为await会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。

1
2
3
4
5
6
7
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch(url)
await fetch(url1)
await fetch(url2)
}

执行顺序
await 返回一个 Promise 对象,await 后面如果是函数会立即执行,await 会产生一个微任务(Promise.then 是微任务)。
执行完 await 后面的表达式或函数,将 await 后面的代码全部放到微任务队列中。跳出 async 函数,执行其他代码,宏任务执行完毕,执行微任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let a = 0
let b = async () => {
console.log(0,a);
a = a + await 10
console.log('2', a) // -> '2' 10
a=(await 10)+a;
console.log('3',a)// -> '3' 20
}

b()
a++
console.log('1', a) // -> '1' 1
// 0 0
// 1 1
// 2 10
// 3 20

在 await 前面的值都会同步执行, await 后面的才会异步执行,所有等待 await 返回之前,a 的值早就确定

  1. 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为在 await 内部实现了 generators ,generators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来,
  2. 因为 await 是异步操作,遇到 await 就会立即返回一个 pending 状态的 Promise 对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log(‘1’, a)
  3. 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
-------------本文结束感谢阅读-------------