前端异步发展流程总结

同步与异步

我们都知道javascript是单线程的,一次只能执行一个任务,当前任务没有完成,下面的任务是不进行处理的。 当浏览器在执行我们js代码的时候,首先会开辟一个全局作用域,然后代码自上而下一行一行地执行,如果遇到定时器或ajax请求,当前任务不会执行,被放到了一个等待队列中,继续完成下面的任务,当下面的任务完成后,而且也到达等待的时间了,才去完成当前的任务。 定时器和ajax就是异步的,包括我们给元素绑定一个点击事件,这个事件不会立刻执行,而是等到我们点击的时候才会触发,回调函数一般也是异步的(难免会有二班的),除了这些其他都是同步的。

js解决异步的方案

1.回调函数

所谓回调函数,就是把函数当做实参传给一个方法,在这个方法里面,我们可以执行回调函数。如果我们在某个阶段,要做的事情不固定,就可以传一个回调函数,在回调函数里面写我们的逻辑。

//setTimeout 参数cb为回调函数
function fn(cb){
    setTimeout(cb, 3000);
}
fn(()=>{
	console.log('这里的代码不会立刻执行, 3s后才会执行');
})

//ajax success传入的是回调函数
$.ajax({
	url: 'xx',
	...
	success(data)=>{
		console.log('ajax 成功之后才会执行这里的代码', data);
	}
});

//同步的回调函数 forEach 所以不一定所有的回调都是异步的
let arr = [1, 2, 3];
arr.forEach(item => console.log(item));
复制代码
// 哨兵模式 优化前
let fs = require('fs');
let obj = {};
fs.readFile('./file1.js', 'utf8', (err, data)=>{
    obj['a'] = data;
    finallyCb();
});
fs.readFile('./file2.js', 'utf8', (err, data)=>{
    obj['b'] = data;
    finallyCb();
});
fs.readFile('./file3.js', 'utf8', (err, data)=>{
    obj['c'] = data;
    finallyCb();
});
let finallyCb = ()=>{
    if(Object.keys(obj).length == 3){
        console.log(obj);
    }else{
        console.log('数据未获取完成');
    }
}

复制代码
// 哨兵模式 优化后
let fs = require('fs');
let obj = {};
// 模拟loadash after方法
let after = (times, cb)=>()=>{--times == 0 && cb()};

fs.readFile('./file1.js', 'utf8', (err, data)=>{
    obj['a'] = data;
    finallyCb();
});
fs.readFile('./file2.js', 'utf8', (err, data)=>{
    obj['b'] = data;
    finallyCb();
});
fs.readFile('./file3.js', 'utf8', (err, data)=>{
    obj['c'] = data;
    finallyCb();
});
let finallyCb = after(3, ()=>{
    console.log(obj); // 最终得到的数据
});

复制代码

缺点:如果多个请求会出现回调地狱,逻辑多层嵌套,代码不易维护,如果每个请求都需要判断是否出现错误等,代码冗余等问题,最终哨兵模式可解决部分问题,但对比promise,promise更方便调用维护代码。

2.发布订阅

这里我们引用一下jquery中的Callbacks与node核心模块events分别举例子。

// 使用发布订阅解决回调地狱的问题 3个请求参数分别基于前一个请求,最终请求成功执行cb方法
let cb = $.Callbacks();
let fn1 = function (){
	$.ajax({
		url: 'xx',
		// ...
		success(data)=>{
		    // 触发 data2 事件 并且将参数传过去
			cb.fire('data2', data);
		}
	})		
};
let fn2 = function (url){
	$.ajax({
	    // 根据data1 触发的数据得到fn2 需要的参数
		url: url,
		// ...
		success(data)=>{
			cb.fire('done', data);
		}
	})		
};
let callback = function (data){
	console.log(data);
}
// 订阅 data1 data2 done这些事件 订阅但不触发
cb.add('data1', fn1);
cb.add('data2', fn2);
cb.add('done', callback);
// 发布 data1事件 就会触发 fn1 里面的代码
cb.fire('data1');
复制代码
// 使用发布订阅模式手写一个相对完整的http服务器 可处理静态文件 get/post/cookie/session 数据 有请求接口等 逻辑为
// 解析get数据->解析post数据->解析cookie数据->解析session数据->处理接口请求->处理静态资源->写回session->响应结束
const http = require('http');
const querystring = require('querystring');
const urlLib = require('url');
const fs = require('fs');
const EventEmitter = require('events').EventEmitter;

var E = new EventEmitter();

http.createServer((req, res)=>{
     //解析get数据 并将参数向下传递
     E.emit('parseGetData', req, res);
}).listen(3000);

E.on('parseGetData', (req, res)=>{
     req.get = urlLib.parse(req.url, true).query;
     req.url = urlLib.parse(req.url, true).pathname;
     //解析post
     E.emit('parsePostData', req, res);
});

E.on('parsePostData', (req, res)=>{
     let arr = [];
     req.on('data', (chunk)=>{
          arr.push(chunk);
     });
     req.on('end', ()=>{
          req.post = querystring.parse(arr);
          //解析cookie
          E.emit('parseCookieData', req, res);
     });
});

E.on('parseCookieData', (req, res)=>{
     req.cookie=querystring.parse(req.headers.cookie, '; ');
     //解析session
     E.emit('parseSessionData', req, res);
});

E.on('parseSessionData', (req, res)=>{
     if(!req.cookie.sessid){
          req.cookie.sessid = Date.now().toString() + Math.random();
     }
     E.emit('readSessionData', req, res);
});

E.on('readSessionData', (req,res)=>{
     fs.readFile(`/session/${req.cookie.sessid}`, (err,data)=>{
          req.session = err ? {} : JSON.parse(data.toString());
          E.emit('apis', req, res);
     });
});

E.on('apis', (req,res)=>{
     res.setHeader('set-cookie', `sessid=${req.cookie.sessid}`);
     // do something
     console.log(
          req.get,
          req.post,
          req.cookie,
          req.session,
          req.url
     );
     // 会返回一个布尔值 如果有接口监听 就是true 否则就是 false 去读取文件
     !E.emit(req.url, req, res) ? E.emit('readStaticFile', req, res) : null;
     // 设置浏览次数
     req.session.visite ? req.session.visite ++ : req.session.visite = 1;
     console.log(`浏览 ${req.session.visite} 次`);
});

E.on('readStaticFile', (req, res)=>{
     // 读取静态文件 这里的路径需处理
     fs.readFile(req.url.substring(1), (err, data)=>{
          if(err){
               res.writeHeader(404, null);
               res.write('404 NOT FOND');
          }else{
               res.write(data);
          }
          res.end();
     });
});
// 接口往下直接列
E.on('/api1', (req, res)=>{
     res.write(JSON.stringify([
          {code: 'xxx', data: {}}
     ]));
     E.emit('writeSession', req, res);
});
// 写回session
E.on('writeSession', (req, res)=>{
     fs.writeFile('session/' + req.cookie.sessid,JSON.stringify(req.session), err=>{
          E.emit('endOver', req, res);
     });
});

E.on('endOver', (req,res)=>{
     res.end();
});
复制代码
3.promise

Promise 是一个类/构造函数,高版本浏览器自带Promise,不兼容低版本浏览器。接收一个函数作为参数,函数内代码为同步,函数有resolve, reject两个参数,分别代表成功失败,根据不同情况调用即可。每个Promise实例都有一个then方法,then方法接收两个函数作为参数,分别为成功失败,两个方法第一个参数分别为成功返回的数据以及err对象。promise有三个状态 pending resolved rejected,pending可转为成功或者失败,一旦成功或者失败,就不会再改变状态。 支持链式调用,解决回调地狱。catch一个回调可同时处理多个then出错问题。

试想,如果工作中有一个需求,第3个接口需要依赖于第2个接口返回的参数,2依赖于1,我们的代码就要写成

//ajax success传入的是回调函数
$.ajax({
	url: 'xx',
	success(data)=>{
	    // 第一次请求成功
		$.ajax({
			url: 'xx',
			data,
			...
			success(data)=>{
			    // 第二次请求成功
				$.ajax({
					url: 'xx',
			        data,
					...
					success(data)=>{
					    // 第三次请求成功
						console.log(data);
					}
				});
			}
		});
	}
});
复制代码

回调地狱的滋味如何?这样编码很不易于维护,我们想一个办法能不能把代码拉平。promise的出现,就可以解决回调地狱。Promise.all方法可用于解决上述需求中的函数多层嵌套。通过下面代码我们最终可以得到第三个接口返回的数据。

let obj = {};
function getData(url, data){
    return new Promise((resolve, reject) => {
        $.ajax({
            url,
            data,
            success: (res)=>{
                resolve(res);
            },
            error: (e)=>{
                reject(e);
            }
        });
    });
}
getData('url1', {}).then(data=>{
    obj.data1 = data;
    return getData('url2', obj.data1);
}).then(data =>{
    obj.data2 = data;
    return getData('url3', obj.data2);
}).then(data =>{
    obj.data3 = data;
    console.log(obj);
}).catch( e =>{
    console.log(e);
});
复制代码

另一个需求来了,如果第三个接口的数据依赖于前两个接口返回的数据,前两个接口返回数据先后无所谓,我们可以使用Promise.all来实现。

function getData(url, data){
    return new Promise((resolve, reject) => {
        $.ajax({
            url,
            data,
            success: (res)=>{
                resolve(res);
            },
            error: (e)=>{
                reject(e);
            }
        });
    });
}
Promise.all([  // 接收一个数组,数组里全是请求
    getData('url1', {}),
    getData('url2', {})
]).then(([data1, data2]) => { // 返回的 data 就是一个数组 与之前请求回来的数据的顺序一致
    getData('url3', {
        data1,
        data2
    });
}).then( data =>{
    console.log(data);
}).catch( e => {
    console.log(e);
});
复制代码
4.generator

generrator是es6提供的生成器,生成的是迭代器(生成器返回迭代器) 一般会配合co库一起使用。 redux+saga内部靠generrator实现。koa早期版本是靠generator实现的,而后期改为了async await实现。那什么是迭代器呢?看下面代码:

let arr = {0: 1, 1: 2, length: 2}; // 自定义的类数组
for(let item of arr){
  console.log(item);
}
//这样执行代码会报错  Uncaught TypeError: arr is not iterable arr不能迭代 因为没有迭代器
复制代码

说到类数组,那我们再试试arguments

function fn(){
  console.dir(arguments);
  for(let item of arguments){
    console.log(item);
  }
}
fn(1, 2, 3); 
复制代码

结果为1 2 3 ,我们看 console.dir(arguments)出来的结果,arguments内部有一个内置属性 Symbol(Symbol.iterator),就是因为这个属性的方法,我们才得以实现for of循环。那么接下来我们自己实现一个自定义类数组的迭代器。首先要知道迭代器必须返回一个对象, 对象里有一个next方法,每调用一次next方法就可以返回一个对象,对象里面有两个属性一个是done是否迭代完成 value是迭代的结果。

let arr = {0: 1, 1: 2, length: 2, [Symbol.iterator]: function(){
  let index = 0,
      that = this;
  // 迭代器返回一个对象 对象内部有一个next方法
  return {
    // 每次调用next方法 就可以返回一个包含done 和 value 的对象
    next(){
      return {done: that.length === len, value: that[index++]};
    }
  }
}}; // 类数组
for(let item of arr){
  console.log(item);
}
复制代码

下面我们分析一下generator与yield配合使用。yield是一个关键字,被*修饰的方法内使用,yield有暂停功能,后面的值会当做返回值中对象的value属性输出。next方法执行后返回的是一个含有done 和 value的对象 done为true的时候代表迭代已完成。在我们调用next的时候可以传值,第一次next传入的值毫无意义,第二次传入的值直接赋值给了value1,第三次传值的之后,直接赋值给了value2。

function * fn(){
  console.log('fn info'); // 第一次fn()的时候不会打印'fn info' 第一次next() 才会到这里
  let value1 = yield 1;
  console.log(value1);
  let value2 = yield 2;
  console.log(value2);
  return 2;
}
let interator = fn();
console.log(interator.next());
console.log(interator.next('aaa'));
console.log(interator.next('bbb'));
复制代码

执行过程有些绕,这样的语法,在我们实际工作中有什么用处?我们再回到之前那个ajax请求,第三个接口依赖于前两个接口,我们使用读写文件来代替异步操作,使用generator+co语法简化代码如下:

// co需要单独下载 npm install co
const co = require('co');
const fs = require('fs');
const read = require('util').promisify(fs.readFile);
function * readFile() {
  let data1 = yield read('filename', 'utf8');
  let data2 = yield read(data1, 'utf8');
  let data3 = yield read(data2, 'utf8');
  return data3;
}
co(readFile()).then(data => {
  console.log(data);
});
复制代码
5.async await

async await其实是基于上述generator+co库的一个语法糖,用babel在线转义一下,其实还是上述语法。用法如下:使用async修饰的函数,内部可使用await,等待后面代码执行完成后,才会向下执行,写起来感觉像同步代码,其内部执行机制其实还是异步的,这样更有助于我们逻辑清晰。

let fs = require('fs');
let read = require('util').promisify(fs.readFile);
async function read() {
  let data1 = await read('1.txt', 'utf8');
  let data2 = await read(data1, 'utf8');
  let data3 = await read(data2, 'utf8');
  return data3;
}
read().then(data=>{
  console.log(data);
});
复制代码

转载于:https://juejin.im/post/5b3728ade51d4558e27d053c

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值