Javascript异步编程

JavaScript是天生异步的,其可以先执行程序的主要逻辑,将耗时的操作推迟执行,相对同步来说,这样可以优化体验和提高性能。

1. 回调函数

回调函数我们接触过很多了,它非常简单、容易理解和部署,但确定也十分明显:不利于代码的阅读和维护,各个部分之间高度耦合,流程混乱。

function xxx(param){
    console.log('我是高阶函数');
    // 上面的代码执行完,去调用另一个函数
    param();
}
function yyy() {
    console.log('我是回调函数');
}
xxx(yyy);

回调函数是使用比较频繁的,比如:ajax、Node的文件操作等。其实回调函数大多数情况下并没有减少执行时间,只是将更耗时的放到后面执行了而已。

2. 事件监听(发布/订阅)

采用事件驱动模式,使任务的执行不取决代码的顺序,而取决于某一个事件是否发生。好处是比较容易理解,耦合度低。缺点就是使整个程序都要变成事件驱动型,运行流程会变得不清晰。

2.1 浏览器中的JavaScript的事件
element.onclick=function(){
   //处理函数
}
2.2 Node中的events模块
// 简单使用
const events = require('events');
let e1 = new events.EventEmitter();
// 注册一个叫xxx的事件监听器
e1.on('xxx', function() {
    console.log('监听器开始处理');
});
console.log('监听器已打开');
// 触发监听器执行
e1.emit('xxx');

3. Promise对象

我们前面介绍的回调函数和事件,其实是有很多问题的,比如:

  1. 函数的return无效了,为了完成一些相互依赖的逻辑,只能一层一层的再嵌套回调函数回调
  2. 多次嵌套,进入回调地狱,代码耦合度加深
  3. 代码可读性差,维护困难
  4. ……

为了解决上面的问题,方便地获取异步操作消息,并且为异步编程提供统一的API,社区很早就提出了Promise,最终在ES6中增加了Promise规范。

3.1 Promise的状态

Promise对象有3中状态:

  1. Pending:进行中 / 未完成 (初始就是这个状态)
  2. Resolved:已完成
  3. Rejected:已失败
3.2 基本语法
/**
 * Promise对象传入的函数有两个参数,这两个参数都必须是函数对象
 * 参数1:状态由“未完成” 变成 “已完成” 时要执行的函数,可以理解为成功时要执行的函数对象
 * 参数2:状态由“未完成” 变成 “已失败” 时要执行的函数,可以理解为失败时要执行的函数对象
 */
let p = new Promise(function (resolve, reject) {
    let data = {name: '嗨!小子'};
    if(1 > 2){
        // 调用成功时的回调函数
        data.status = 'success';
        resolve(data);
    }else{
        // 调用失败时的回调函数
        data.status = 'error';
        reject(data);
    }
});

/**
 * 使用实例的then方法分别指定resolved状态和rejected状态的回调函数
 * 参数1(必选):Promise对象状态由“未完成” 变成 “已完成” 时执行的回调函数
 * 参数2(可选):Promise对象状态由“未完成” 变成 “已失败” 时执行的回调函数
 */
p.then(
    function (value) {
        console.log('这是成功时要执行的内容');
        console.log(value);
    },
    function (value) {
        console.log('这是失败时要执行的内容');
        console.log(value);
    }
)

从语法中我们可以看到,我们的基本逻辑要写到Promise对象中,我们的回调函数定义到then()方法中了。和我们之前直接使用回调函数差不多。不过,then()方法返回的是一个新的Promise对象,因此,then()方法可以多次链式调用。

3.3 例子讲解

我们看一个例子:

需求:
1.txt中读取文本,然后在末尾加个*号,然后写到2.txt中,如果上一步写入成功,则在log.txt中记录写入成功,否则提示错误。

const fs = require('fs');
// 我们有一个需求:
// 从1.txt中读取文本,然后在末尾加个*号,然后写到2.txt中
// 如果上一步写入成功,则在log.txt中记录写入成功,否则提示错误
fs.readFile('./1.txt', 'utf-8', function (err, data) {
    if(!err){
        data += '*';
        fs.writeFile('./2.txt', data, function (err) {
            if(err){
                console.log('文件写入失败');
            }else{
                fs.writeFile('./log.txt', `数据处理完成并写入成功,共写入${data.length}个字符`, function (err) {
                    if(err){
                        console.log('日志记录失败');
                    }else{
                        console.log('OK');
                    }
                })
            }
        })
    }else{
        console.log('文件读取失败');
    }
})

我们可以看出,上面的需求,每一步都依赖上一步的操作,使用回调函数是可以处理的,不过代码写的嵌套太多了,倘若一个需求有十来步,每一步都需要依赖前一步的结果,那这样写,能把人写疯。接下来我们尝试用Promise去处理:

let p = new Promise(function (successCallback, errorCallback) {
    fs.readFile('./1.txt', 'utf-8', function (err, data) {
        if (err) {
            // 执行错误的
            errorCallback(err);
        } else {
            successCallback(data);
        }
    });
})

p.then(
    function (data) {
        // 使用return返回普通数据的话,返回的数据会被下一个then()接收
        // 读取成功,处理数据,并将处理后的数据返回
        return data += '*';

    },
    function (err) {
        console.log(err);
    }
	)
    .then(
    // 这里可以只有一个参数
    function (data) {
        // 接收上一步的数据,并做下一步处理
        // 如果这里的是一个Promise对象的话,会自动调用下一个then()
        return new Promise(function (successFun, errorFun) {
            fs.writeFile('./2.txt', data, function (err) {
                if (err) {
                    errorFun(err);
                } else {
                    successFun(data.length);
                }
            });
        });
    }
).then(
    function (data) {
        fs.writeFile('./log.txt', `数据处理完成并写入成功,共写入${data}个字符`, function (err) {
            if (err) {
                console.log('日志记录失败');
            } else {
                console.log('OK');
            }
        })
    },
    function (data) {
        console.log(data);
        console.log('文件写入失败');
    }
)

我们可以看到,Promise可以将多层的回调函数改造成链式的操作,不过仍然需要写一些回调函数。

3.4 其它方法
3.4.1 实例方法:

.then():获取异步任务的正确结果(第二个参数可以获取异常结果)

.catch():获取异步任务的异常结果

.finally():无论结果成功或者失败都会执行(部分运行环境,可能不一定支持)

3.4.2 对象(静态)方法

Promise.all(): 并发处理多个异步任务(一次性启动多个任务),所有任务执行完毕后得到结果。

Promise.race(): 并发处理多个异步任务(一次性启动多个任务),只要一个任务执行完毕就得到结果。

这两个方法的使用方式一模一样,只是返回值返回的时机不同。

Promise.all([new Promise(function(x, y){}), 
   new Promise(function(x, y){}), ……])
   .then(function(result){
  
});

Promise.race([new Promise(function(x, y){}),
   new Promise(function(x, y){}), ……])
  .then(function(result){
  
});
3.5 使用Promise简单改造ajax操作
function ajaxGet(url) {
    return new Promise(function (success, error) {
        let xhr = new XMLHttpRequest();
        // 监听readyState属性值的变化
        xhr.onreadystatechange = function () {
            // 请求响应完毕
            if (xhr.readyState === 4) {
                if (xhr.status == 200) {
                  	success(xhr.response);
                } else {
                 	 	error(xhr.status)
                }
            }
        }
        // 创建请求
        xhr.open('GET', url);
        xhr.responseType = 'json';
        // 发送请求
        xhr.send()
    });
}

ajaxGet('http://127.0.0.1')
  .then(
      function(data){
        	return parseInt(data) + 7777;
      }
	)
  .then(
    function(x){
    	console.log(x);
  	}
	)

4.async/await 语法糖

该语法是ES7新增的,async/await是基于生成器和promise实现的。是目前最简洁的编写异步代码的方式了。

// 基本语法
async function DoSomething() {
    let data  = await doFirst();     // 第1步操作 执行一个异步操作得到结果
    let data2 = await doSecond(data);  // 第2步操作 执行一个异步操作得到结果
    let data3 = await odthird(data2); // 第3步操作 执行一个异步操作得到结果
    return data3
}

上面的代码可以看出,如果都是异步操作,下一步的操作依赖上一步的结果,只要编写的时候加上asyncawait关键词,感觉就像写同步代码一样。

从语法上讲:async 关键字用来修饰函数,这种函数的返回值是一个promise对象,await关键字用在 async function 函数内容,表示得到异步操作的结果。

async function xxx(){

}
// 是个Promise对象
console.log(xxx());
async function xxx(){
  	return '你长得真好看,向天边的花一样';
}
// 返回值依然是个Promise对象
console.log(xxx());
4.1 例子

看前面使用我们封装的ajaxGet函数进行的操作:我们先远程请求一个数据,然后用异步返回来的数据加上 8888, 得到结果打印出来,我们应该这样写代码:

ajaxGet('http://127.0.0.1')
.then(
  	// 使用回调函数进行后续操作
    function (data) {
      	console.log(parseInt(data) + 7777);
    }
)

我们发现,我们仍然要写一个回调函数,业务逻辑写到回调函数中。如果我们使用async/await 语法糖的话,可以怎么处理呢?看代码:

async function xxx(){
    // 获得异步操作的结果
    let tmp = await ajaxGet('http://127.0.0.1');
    console.log(tmp + 8888);
}

xxx();

我们发现,使用async/await 语法糖,我们不用手动写回调函数了,是不是很方便。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值