之前在百度前端学院遇到一道题:
实现快速排序的可视化
当时的实现要么是
- 深度递归嵌套
- 把中间结果保存在中间数组中,全部执行完之后输出到dom
第一种实现方式逻辑难度太大,第二种实现方式有点不地道。
但是最新一不小心看到ES6的 Generator Function之后,就可以写简单顺序代码却可以实现异步编程啦。
0x00 实现一个异步编程demo
JavaScript 的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,在事件队列中加一个延时,指定自己执行成功需要的时间。
下面代码实现了一个简单的异步编程的方式。
var Q = {
// 保存队列信息
a: [],
// 添加到队列 queue
q: function(d){
// 添加到队列如果不是函数或者数字则不处理
if(!/function|number/.test(typeof d)) return;
Q.a.push(d);
// 返回对自身的引用
return Q;
},
// 执行队列 dequeue
d: function(){
var s = Q.a.shift();
// 如果已经到了队列尽头则返回
if(!s) return;
// 如果是函数,直接执行,然后继续 dequeue
if(typeof s === "function") {
s(), Q.d();
return;
}
// 如果是数字,该数字作为延迟时间,延迟 dequeue
setTimeout(function(){
Q.d();
}, s);
}
};
使用:
// 进程记录函数
function record(s){
var div = document.createElement("div");
div.innerHTML = s;
console.log(s);
document.body.appendChild(div);
}
Q
.q(function(){
record("0 <i style='color:blue'>3s 之后搞定,0 把 1 叫进来</i>");
})
.q(3000) // 延时 3s
.q(function(){
record("1 <i style='color:blue'>2s 之后搞定,1 把 2 叫进来</i>");
})
.q(2000) // 延时 2s
.q(function(){
record("2 <span style='color:red'>后面没人了,OK,厕所关门~</span>");
})
.d(); // 执行队列
实现了一个简单的异步编程啦。
0x01 浅析javascript中的几种异步编程的方式
1. setTimeout 函数
缺点:setTimeout 是存在一定时间间隔的,并不是设定 n 毫秒执行,他就是 n 毫秒执行,可能会有一点时间的延迟(2ms左右)。
2. ajax 或者 插入节点 的 readyState 变化
这是数据传输方式。ajax在open时设置async为true的时候,也就是异步加载,当 state 发生改变的时候,xhr 立即响应,触发相关的函数。
3.高阶函数篡改回调函数
高阶函数篡改回调函数的方法也可以实现异步编程。
假设需要等待所有异步回调执行完成后,才能执行某个逻辑,这时通过高阶函数篡改回调函数的方式就大为受用,也相当简单。
例子:
var pending = (function () {
var count = 0;
return function (callback) {
count++;
return function () {
count--;
if (count === 0) {
callback();
}
};
};
}());
var done = pending(function () {
console.log('all is over');
});
fs.readFile('file1.txt', 'utf8', done());
fs.readFile('file2.txt', 'utf8', done());
总觉得高阶函数代码逻辑会非常混乱。或者说我智商驾驭不了吧。
4 事件监听
JS 和 浏览器提供的原生方法基本都是基于事件触发机制的,耦合度很低,不过事件不能得到流程控制。
5 观察者模式
发布/订阅( Pub/Sub )
把事件全部交给 控制器管理,可以完全掌握事件被订阅的次数,以及订阅者的信息,管理起来特别方便。
6 Promise 对象(deferred 对象)
这不就是iqianyiduanshijian分析的jQuery全局使用的defered模型嘛。
在Promises/A规范中,每个任务都有三种状态:默认(pending)、完成(fulfilled)、失败(rejected)。
默认状态可以单向转移到完成状态,这个过程叫resolve,对应的方法是deferred.resolve(promiseOrValue);
默认状态还可以单向转移到失败状态,这个过程叫reject,对应的方法是deferred.reject(reason);
默认状态时,还可以通过deferred.notify(update)来宣告任务执行信息,如执行进度;
状态的转移是一次性的,一旦任务由初始的pending转为其他状态,就会进入到下一个任务的执行过程中。
7 Web worker
JavaScript 中的 Worker 对象可以创建一个独立线程来处理数据,很自然的处理了阻塞问题。我们可以把繁重的计算任务交给 Worker 去倒腾,等他处理完了再把数据 Post 过来。
var worker = new Worker("./outer.js");
worker.addEventListener("message", function(e){
console.log(e.message);
});
worker.postMessage("data one");
worker.postMessage("data two");
// outer.js
self.addEventListener("message", function(e){
self.postMessage(e.message);
});
0x02 ES6 Generator Function
1.generator function
var compute = function* (a, b) {
var sum = a + b;
yield console.log(sum);
var c = a - b;
yield console.log(c);
var d = a * b;
yield console.log(d);
var e = a / b;
console.log(e);
};
function* (a, b)
这里*代表是一个generator函数。generator自带一个next()方法。yield
代表函数中每一个断点,generator函数在运行到断点时停止,每一次调用next函数向后执行到下一个断点,实现了函数内部的任务拆解。
使用:
var generator = compute(4, 2);
generator.next(); // 6
generator.next(); // 2
generator.next(); // 8
generator.next(); // 2
yield
还有一个特性。看下面代码:
var compute = function* (a, b) {
var foo = yield a + b;
console.log(foo);
};
var generator = compute(4, 2);
generator.next();//undefined
generator.next("Hello world!"); // Hello world!
我们调用第一次next时,foo此时声明未赋值,输出undefined。
第二次调用next时,传入的hello world可以赋值给yield的变量。
利用这个特性,竟然神奇的在一个函数中停下,传入了参数,对函数的发展做了更改。
2.generator与异步编程
var readFile = helper(fs.readFile);
var flow = function* () {
var txt = yield readFile('file1.txt', 'utf8');
console.log(txt);
};
var generator = flow();
var ret = generator.next(); // 执行readFile('file1.txt', 'utf8');
上面代码成功执行读了一个文件
var generator = flow();
var ret = generator.next(); // 执行readFile('file1.txt', 'utf8');
ret.value(function (err, data) {
if (err) {
throw err;
}
generator.next(data);
});
稍微修改,发现我们可以在读入每个文件之后,拿到中间结果,对错误进行处理,再读入下个文件。
3.添加流程控制的Generator
网上大神的一段精简的代码,完美实现顺序控制复杂异步编程!
var co = function (flow) {
var generator = flow();
var next = function (data) {
var result = generator.next(data);
if (!result.done) {
result.value(function (err, data) {
if (err) {
throw err;
}
next(data);
});
}
};
next();
};
调用示例:
co(function* () {
var txt = yield readFile('file1.txt', 'utf8');
console.log(txt);
var txt2 = yield readFile('file2.txt', 'utf8');
console.log(txt2);
});
运行结果:
I am file1.
I am file2.
貌似还是没觉得有多强大,大牛的扩充版本如下:
4.扩展的并行执行的Generator
var co = function (flow) {
var generator = flow();
var next = function (data) {
var ret = generator.next(data);
if (!ret.done) {
if (Array.isArray(ret.value)) {
var count = 0;
var results = [];
ret.value.forEach(function (item, index) {
count++;
item(function (err, data) {
count--;
if (err) {
throw err;
}
results[index] = data;
if (count === 0) {
next(results);
}
});
});
} else {
ret.value(function (err, data) {
if (err) {
throw err;
}
next(data);
});
}
}
};
next();
};
调用方式:
co(function* () {
var results = yield [readFile('file1.txt', 'utf8'), readFile('file2.txt', 'utf8')];
console.log(results[0]);
console.log(results[1]);
var file3 = yield readFile('file3.txt', 'utf8');
console.log(file3);
});
运行结果:
I am file1.
I am file2.
I am file3.