美文网首页
说说异步

说说异步

作者: Jason_Shu | 来源:发表于2019-03-06 15:52 被阅读0次

我们都知道Javascript是「单线程」的,所谓「单线程」就是指一次只能完成一个任务,如果有多个任务,则必须排队,等上一个任务完成后,才能轮到下一个任务执行。

但是这样的模式就有一个问题了,如果上一个任务的执行时间很长,甚至造成死循环的话,那下一个任务是不是要等到天荒地老?

为了解决这个问题,于是乎Javascript就将任务的执行模式分为了:「同步模式」和「异步模式」。

「同步模式」,即上述的模式,一个任务执行完再执行下一个,程序的执行是有顺序的。

「异步模式」则是每一个任务都有一个或者多个「回调函数」,前一个任务执行完,不是执行下一个任务,而是执行「回调函数」,后一个任务也不是等前面那个任务执行完才开始执行,所以程序的执行顺序与任务的排列顺序不是一致的。比如在耗时很长的任务中,我们就需要使用异步,最好的例子就是Ajax操作,因为执行环境是单线程的,如果同步执行所有http请求,那么服务器性能会大幅下降,很快失去响应。

一.回调函数
这是处理异步的最基本的方式,所谓「回调函数」,就是把一个函数A传给函数B调用,则函数A就是「回调函数」

假如有两个函数f1和f2(f2等待f1的执行结果),如果f1是需要花费很长时间,可以考虑把f2写成f1的回调函数。

function f1(callback) {
  setTimeout(() => {
    // f1的任务代码
    f2();
  }, 1000)
}

f1(f2);

采用这种方式,我们就可以把同步操作变成异步操作,f1不会堵塞程序,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。

回调函数的优点是简单,容易理解和部署,不过缺点是不利于代码维护,高耦合,流程混乱,并且每个任务只能有一个回调函数。

所说回调的几种写法:
(1)具体回调写法

function getInfo(callback) {
    callback.call(null, 'name: Jason');
}

function userInfo(info) {
    console.log(info)
}


getInfo(userInfo)

(2)匿名回调写法

function getInfo(callback) {
    callback.call(null, 'name: Jason');
}


getInfo.call(null, function(info) {
    console.log(info)
})

(3)回调地狱

getInfo(function(info) {
    console.log(info);
    saveInfo(info, function() {
        getAnotherInfo(function(anotherInfo) {
            console.log(anoterInfo)
            saveInfo(function() {});
        })
    })
})

像这种多层匿名函数的嵌套,就会让人很难理解,这种多层嵌套称为「回调地狱」。

二. 事件监听

另一种思路是采取事件驱动的模式,任务的执行不取决于代码顺序,而取决于这个事件是否发生。

f1.done('done', f2);

上面代码的意思就是在f1完成后在调用f2。

我们对上面代码改写。

function f1() {
    setTimeout(function() {
        // f1代码

        f1.trigger('done');

    },1000);
}

上述代码是指当f1完成后trigger('done'),触发done事件,从而开始执行f2。

这种方法比较容易理解,可以绑定多个事件,一个事件可以指定多个回调函数,去耦合,有利于代码模块化。缺点是整个程序编程事件驱动,运行流程不清晰。

三. 发布/订阅模式

上一节的「事件」我们可以理解为「信号」。我们假设有一个「信号中心」,来把控这些信号,当一个任务完成后,向「信号中心」“发布”一个信号,其他任务可以向这个「信号中心」“订阅”这个信号,从而知道自己的任务什么时候开始执行。

首先我们用f2向「Jquery订阅中心」订阅“done”信号。

jQuery.subscribe("done", f2);

然后对f1进行改写:

function f1() {
    setTimeout(function() {
        // f1代码
        jQuery.publish('done');
    }, 1000)
}

jQuery.publish('done')的意思就是在f1执行完“发布”一个“done”信号,从而引发f2的执行。

此外我们还可以取消“订阅”。

jQuery.unsubscribe("done", f2);

这种方式跟「事件监听」方式类似,但是优于后者,因为我们可以看到「消息中心」,有多少「信号」。

四. Promise
简单来说,Promise思想就是在每一个异步任务后返回一个Promise对象,该对象有个一个then方法,允许有一个回调函数。

// f1和f2可以写成
f1.then(f2)

使用Promise我们解决了上述什么问题呢?
(1)解决了不知道该如何使用回调函数
promise前面是操作,.then里面就是对上一步操作的结果进行处理,then的第一个参数是上一步操作成功后调用的回调函数,第二个参数是上一步操作失败后调用的操作。

f1.then(function() {}, function() {})

(2)解决了回调地狱
我们可以通过不断的then来比较清晰地看出逻辑,不存在“嵌套”一说。

f1.then(function() {}, function() {}).then(function() {}, function() {})

我们使用Promise来解决一下上面的回调地狱问题。

function getInfo() {
    return new Promise((resolve, reject) => {
        console.log('第一次获取信息');
        resolve('name: Jason');
    });
}

function printInfo(info) {
    return new Promise((resolve, reject) => {
        console.log(info);
        resolve();
    })
}

function getAnotherInfo() {
    return new Promise((resolve, reject) => {
        console.log('第二次获取信息')
        resolve('name: Jack');
    })
}

getInfo().then(printInfo)
    .then(getAnotherInfo)
    .then(printInfo)

// 第一次获取信息
// name: Jason
// 第二次获取信息
// name: Jack

.then里第一个参数,是成功函数(resolve),这个函数的参数就是上一个resolve中传入的实参

function f1() {
    return new Promise((resolve, reject) => {
        resolve('我是f1中的实参')
    })
}

function f2(data) {
    console.log(data);
}

f1().then(f2);
log结果

同样的,.then后面的第二个参数是失败函数,失败函数的参数也是由上一个reject函数
传入的实参。

如果不给resolve和reject传值,那么你通过then得到的参数就是undefined。

function f1() {
    return new Promise((resolve, reject) => {
        resolve(); // 我们不传参
    })
}

function f2(data) {
    console.log(data);
}

f1().then(f2);
log结果

如果我们想用多个then操作,那么我们就要在每一个resolve或者reject中传入参数。就像我们小时候玩的「击鼓传花」的游戏一样。

function f1() {
    return new Promise((resolve, reject) => {
        resolve('这是f1的实参');
    })
}

function f2(data) {
    return new Promise((resolve, reject) => {
        resolve(data)
    })
}

function f3(data) {
    return new Promise((resolve, reject) => {
        console.log(data)
        resolve(data)
    })
}

f1().then(f2).then(f3);
log结果

上述代码,f1传入的实参「这是f1的实参」可以通过then传到f3,然后在f3中log出来。

then里面不管调用的是成功函数(resolve)还是失败函数(reject),下一个then都会调用成功函数,除非在reject函数里直接reject。

function f1(name) {
    return new Promise((resolve, reject) => {
        if(name === "Jason") {
            resolve('success');
        } else {
            reject('fail')
        }
    })
}

function f2(data) {
    return new Promise((resolve, reject) => {
        console.log('这里是f2')
    })
}

function f3(data) {
    console.log('这里是f3')
}

function f4(data) {
    console.log('这里是f4')
    return new Promise((resolve, reject) => {
        resolve()
    })
}

f1('Jack').then(f2, f3).then(f4);
log结果

如图,第一个.then进入f3这个失败函数后,没有返回任何东西,浏览器就默认处理完毕了,然后就直接到了第二个.then的f4这个成功函数了。那么如何让浏览器知道我们还没有搞定这个失败,不让下一个.then的成功函数不执行呢?

我们在第一个.then的失败函数f3增加reject。

function f1(name) {
    return new Promise((resolve, reject) => {
        if(name === "Jason") {
            resolve('success');
        } else {
            reject('fail')
        }
    })
}

function f2(data) {
    return new Promise((resolve, reject) => {
        console.log('这里是f2')
    })
}

function f3(data) {
    console.log('这里是f3')
    // 返回reject
    return new Promise((resolve, reject) => {
        reject()
    })
}

function f4(data) {
    console.log('这里是f4')
    return new Promise((resolve, reject) => {
        resolve()
    })
}

f1('Jack').then(f2, f3).then(f4);

还有一个知识点,就是promise里面是「同步」的,.then里面才是「异步」的。

function f1() {
    return new Promise((resolve, reject) => {
        console.log(1)
    })
};
f1();
console.log(2)
promise里的同步
function f1() {
    return new Promise((resolve, reject) => {
        resolve(1)
    })
};

function f2(data) {
    console.log(data);
}
f1().then(f2);
console.log(2)
.then中异步

五.await
上面我们使用promise,通过then来拿到成功或者失败的信息,但是then里面还是有回调,有啥方式可以不是用回调呢?

我们可以使用「await」。
await操作符用于等待一个Promise对象。它只能在异步函数async function中使用

function f1(name) {
    return new Promise((resolve, reject) => {
        if(name === 'Jason') {
            resolve('success!');
        } else {
            reject('fail!!')
        }
    })
}

async function f2() {
    let result = await f1('Jason');
    console.log(result)
}

f2();
image.png

上述代码中,在f2函数中,我们用await得到了f1中promise对象执行成功函数传入的参数“success”。同样await也可以获得失败函数传入的参数。我们把两者整合,应该这么写。

function f1(name) {
    return new Promise((resolve, reject) => {
        if(name === 'Jason') {
            resolve('success!');
        } else {
            reject('fail!!')
        }
    })
}

async function f2() {
    try {
        let result = await f1('Jack'); // 当前是Jack输出“fail”,如果为Jason则输出“success”
        console.log(result)
    }catch(error) {
        console.log(error)
    }

}

f2();

如果await后不是一个promise对象,await 会把该值转换为已正常处理的Promise,然后等待其处理结果。

async function f2() {
  var y = await 20;
  console.log(y); // 20
}
f2();

参考:

  1. http://www.ruanyifeng.com/blog/2012/12/asynchronous%EF%BC%BFjavascript.html
  2. https://zhuanlan.zhihu.com/p/55533549
  3. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await

相关文章

网友评论

      本文标题:说说异步

      本文链接:https://www.haomeiwen.com/subject/wxizuqtx.html