JS为何有异步?

javascript是单线程的语言,即一次只能完成一个任务,若有多个任务要执行,则必须按照队列排队完成任务,前一个任务执行完才能执行下一个任务。
那么如果上一个任务不结束,下一个任务就永远得不到执行,或者上一个任务执行很久,前后两个任务没有什么必然的联系,白白浪费了时间在等待。
所以需要异步。

开发中常用的异步操作

  • 网络请求
  • IO操作 readfile readDir
  • 定时函数setTimeout setInterval
  • 在Node.js中 还有process.nextTick() setImmediate()

传统的异步解决方案:

  1. 事件的订阅/发布机制
  2. 回调函数
发布/订阅机制

简单示例

// 订阅
emitter.on('eventName', function(message){
   console.log(message)
})
// 发布
emitter.emit('eventName', 'I am a message')

Node.js中 我们可以看到它的应用

var options = {  
    hostname: '127.0.0.1',  
    port: 10086,  
    path: '/pay/pay_callback?' + content,  
    method: 'GET'  
};  
var req = http.request(options, function (res) {  
    console.log('STATUS: ' + res.statusCode);  
    console.log('HEADERS: ' + JSON.stringify(res.headers));  
    res.setEncoding('utf8');  
    res.on('data', function (chunk) {  
        console.log('BODY: ' + chunk);  
    });
    res.on('end', function() {
    })
});  
req.on('error', function (e) {  
    console.log('problem with request: ' + e.message);  
});  
  
req.end(); 

上面是一个请求接口的过程, 作为开发者,我们只需要关注error、data、end这些业务事件点上即可,订阅了这些事件,它在执行内部流程会自动去触发相应的事件

传统的回调函数的方式会造成回调地狱,类似这种

fs.readFile('some1.json', (err, data) => {
    fs.readFile('some2.json', (err, data) => {
        fs.readFile('some3.json', (err, data) => {
            fs.readFile('some4.json', (err, data) => {

            })
        })
    })
})

Promise规避了这一点,使用链式调用的形式,可读性更高

readFilePromise('some1.json').then(data => {
    return readFilePromise('some2.json')
}).then(data => {
    return readFilePromise('some3.json')
}).then(data => {
    return readFilePromise('some4.json')
})

异步变同步

这是我们开发时经常要遇到的场景 异步代码同步执行。
写两个模拟的异步函数

var f1 = function() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('f1 is run');
            resolve('f1 done')
        }, 3000)
    })
}
var f2 = function() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('f2 is run');
            resolve('f2 done')
        }, 2000)
    })
}
  1. Promise

    f1().then(res => {
     return f2()
    }).then(res => {
    })
    // 输出
    3s后输出:f1 is run
    再过2s后输出:f2 is run

    如果异步函数很多的话,会有很多个then

    f1().then(res => {
     return f2()
    }).then(res => {
     return f3()
    }).then...

    可以用更简洁的写法

    var arr = [f1, f2] // 待执行promise数组
    var p = Promise.resolve()
    for(let pro of arr) {
     p = p.then(res => pro(res))
    }
  2. reduce
    其实跟1是一样的

    var arr = [f1, f2]
    arr.reduce((p,c) => {
     return p.then((res) => c(res))
    }, Promise.resolve())
  3. async await

    var arr = [f1, f2]
    async function doFunc() {
     for(let p of arr) {
         await p()
     }
    }
  4. Generator

    function * gen() {
     yield f1()
     yield f2()
    }
    let g = gen()
    g.next()
    g.next()

以上的方式都能达到继发执行的结果.
1.2.3.4都是继发执行 然后输出。如果是并发执行 然后按顺序输出呢? 有点类似Promise.all,其实就是并发执行,然后将执行结果先存起来,再顺序输出.这样对于两个无关联的异步函数 并发执行的效率更高

async function testFunc() {
    var arr = [f1, f2]
    // 并发执行
    var promiseArr = arr.map((fn) => fn())
    for(let result of promiseArr) {
        // 同步返回结果
        console.log('result', await result);
    }
}
// 输出
f2 is run
f1 is run
result f1 done
result f2 done

Promise

对于Promise,我们要按要点来记忆它

    1. 很长的链式调用
    1. Promise没有被resolvereject, 它将一直处于pending的状态
    1. 如果是在pending的状态 无法知道是刚开始还是快要结束
    1. 无法取消Promise 一旦建立它就会立即执行,无法中途取消
    1. 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部(所以一般建议promise对象后面要跟着catch方法)
    1. 一旦状态改变,就永久保持该状态,不会再变了
    1. 如果是链式调用 原promise对象的状态跟新对象保持一致
    1. reject的作用 等同于抛出错误
    1. catch方法返回的也是promise对象(后面可以继续跟then)
    1. 立即resolve()Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时

对于第2点,举个例子:

function pend () {
    return new Promise(resolve => {
        // 没有resolve
        console.log('aaaa');
    })
}
pend().then(() => {
    console.log('bbb');
})
// 输出
aaaa
Promise {<pending>}

bbb将永远不会被打印出来


对于第9点,举个例子:

setTimeout(() => {
    console.log('three');
},0)
Promise.resolve().then(() => {
    console.log('two');
})
console.log('one');
// 输出
one
two
three

one 是立即输出
two 是在“本轮”事件循环的末尾执行然后输出
three 是下一轮事件循环开始时被执行然后输出

写的有点笼统 后续会再补充 有问题欢迎一起讨论


高压郭
961 声望494 粉丝

从简单到难 一步一步