你不知道的JavaScript 中卷 第二部分 - 异步和性能
异步:现在与将来
js程序的执行顺序整体上是从头到尾执行的,但不是从头到尾一成不变的,比如两个ajax请求。
分块的程序
-
异步控制台
var a = { index: 1}; console.log(a.index); // 1 console.log(a); // 在浏览器控制台中查看为 {index: 2} a.index++;
- 原因:
浏览器会认为需要把控制台I/O延迟到后台,这种情况下,等到浏览器控制台输出对象内容时,a.index++可能已经执行了,因此会显示 {index: 2}
- 原因:
事件循环
setTimeout(...)
定时器精度不高原因
setTimeout(…) 并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的 tick 会摘下并执行这个回调。
如果这时候事件循环中已经有 20 个项目了会怎样呢?你的回调就会等待。它得排在其他项目后面——通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么setTimeout(…) 定时器的精度可能不高。大体说来,只能确保你的回调函数不会在指定的时间间隔之前运行,但可能会在那个时刻运行,也可能在那之后运行,要根据事件队列的状态而定。
并行线程
- 异步:指的是关于现在和将来的时间间隙
- 并行:指的是关于能够同时发生的事情
var a = 0; function foo(){ a += 1; } function bar(){ a += 2; } // 两个方法会并行运行(实际上并不是同时发生的,先1后2,但异步时间的间隙无法确定先后),但最终的结果无法预测 ajax('http://xxxx.url.1', foo); ajax('http://xxxx.url.2', bar);
setTimeout
相关任务执行顺序
console.log('A');
setTimeout(()=>{
console.log('B');
}, 0);
setTimeout(()=>{
console.log('C');
});
(()=>{
console.log('D');
(()=>{
console.log('E');
})()
console.log('F');
})()
console.log('G');
// A D E F G B C
Promise
Promise 用法
function foo(name) {
return new Promise((resolve, reject) => {
name ? resolve({
name }) : reject({
error: 'error' });
})
}
foo('ProsperLee')
.then(
// 成功
value => {
console.log(value); // ProsperLee
},
// 失败 - 优先调用
reason => {
console.log('error', reason);
}
)
// 失败
.catch(reason => {
console.log('reason', reason);
})
// 完成
.finally(_ => {
console.log('Hello ProsperLee');
})
foo()
.then(
// 成功
value => {
console.log(value);
},
// 失败 - 优先调用
reason => {
console.log('error', reason); // error {error: 'error'}
}
)
// 失败
.catch(reason => {
console.log('reason', reason);
})
// 完成
.finally(_ => {
console.log('Hello Error');
})
// 最终输出结果
// ProsperLee
// error {error: 'error'}
// Hello ProsperLee
// Hello Error
Promise.all 和 Promise.race 的区别和使用
function foo(name) {
return new Promise((resolve, reject) => {
name ? resolve({
name }) : reject({
error: 'error' });
})
}
function bar(name) {
return new Promise((resolve, reject) => {
setTimeout(_ => {
name ? resolve({
name }) : reject({
error: 'error' });
}, 3000);
})
}
// 等待然后返回两个函数成功的返回值
Promise.all([foo('Lee'), bar('Tom')])
.then(values => {
// [{name: 'Lee'}, {name: 'Tom'}]
console.log(values);
}, reason => {
console.log(reason);
})
// 等待然后返回函数错误的返回值
Promise.all([foo('Lee'), bar()])
.then(values => {
console.log(values);
}, reason => {
// {error: 'error'}
console.log(reason);
})
// 返回出结果快的那个函数的返回值,不论成功或失败
Promise.race([foo(), bar('Tom')])
.then(value => {
console.log(value);
}, reason => {
// {error: 'error'}
console.log(reason);
})
链式流
调用 Promise 的 then(…) 会自动创建一个新的 Promise 从调用中返回
function foo(name) {
return new Promise((resolve, reject) => {
name ? resolve({
name }) : reject({
error: 'error' });
})
}
// foo('Lee')
foo('Lee').then(res => {
console.log(res); // {name: 'Lee'}
return res;
}).then(res => {
console.log(res); // {name: 'Lee'}
})
Promise.resolve({
a: 123}).then(res => {
console.log(res); // {a: 123}
return res;
}).then(res => {
console.log(res); // {a: 123}
})
Promise - finally
function foo(name) {
return new Promise((resolve, reject) => {
setTimeout(_ => {
name ? resolve({
name }) : reject({
error: 'error' });
}, 3000);
})
}
foo('Lee')
.finally(_ => {
console.log('A');
})
.then(res => {
console.log(res);
return res;
})
.finally(_ => {
console.log('B');
})
.then(res => {
console.log(res);
})
.finally(_ => {
console.log('C');
})
// 最终输出结果:
// A
// {name: 'Lee'}
// B
// {name: 'Lee'}
// C
Promise性能
Promise 稍慢一些,但是作为交换,你得到的是大量内建的可信任性,Promise 非常好,请使用。
它们解决了我们因只用回调的代码而备受困扰的控制反转问题。
它们并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任的中介机制。
Promise 链也开始提供(尽管并不完美)以顺序的方式表达异步流的一个更好的方法,这有助于我们的大脑更好地计划和维护异步 JavaScript 代码。
ES6中的生成器
创建生成器的方法
function *foo(){}
特点:
暂停程序运行(注意是暂停而不是停止)
打破完整运行
function* foo() {
for (let i = 0; i < 3; i++) {
console.log(i);
yield `i ---> ${
i}`;
}
console.log('PLee');
return 'Lee';
}
let it = foo();
console.log(it.next()); // 0 {value: 'i ---> 0', done: false}
console.log(it.next()); // 1 {value: 'i ---> 1', done: false}
console.log(it.next()); // 2 {value: 'i ---> 2', done: false}
console.log(it.next()); // PLee {value: 'Lee', done: true}
console.log(it.next()); // {value: undefined, done: true}
- 分析:
let it = foo();
并没有执行生成器*foo()
,而只是构成了一个迭代器(iterator),这个迭代器会控制它的执行- 第一个
it.next()
启动了生成器*foo()
, 并运行了*foo()
的第一次循环,最终会打印0
*foo()
在yield
语句处暂停,并返回yield
语句的值,在这一点上第一个it.next()
调用结束,此时*foo()
仍在运行并且是活跃的,但处于暂停状态- 以此类推,
第二次循环(it.next())
,第三次循环(it.next())
第四次调用(it.next())
执行了PLee
,最终返回Lee
并完成了生成器*foo()
的执行最后一次
执行生成器因生成器已经完成并且没有找到值,故返回undefined
输入和输出
- 生成器分析:
foo(1, 2);
并没有运行生成器,而是创建了一个迭代器,然后调用it.next()
,指示生成器 *foo(…) 从当前位置开始继续运行,停在下一个 yield 处或者直到生成器结束
function *foo(a, b){ return a + b; } var it = foo(1, 2); it.next();
- 迭代消息传递
- 第一个
next(..)
总是启动一个生成器,并运行到第一个yield
处 - 第二个
next(..)
调用完成第一个被暂停的yield
表达式 - 第三个
next(..)
调用完成第二个yield
, 以此类推
/** * 1. 在 *foo(..) 内部,开始执行语句 var y = x ..,但随后就遇到了一个 yield 表达式 * 2. 它就会在这一点上暂停 *foo(..)(在赋值语句中间!),并在本质上要求调用代码为 yield 表达式提供一个结果值 * 3. 接下来,调用 it.next( 2 ),这一句把值 2 传回作为被暂停的 yield 表达式的结果 * 4. 所以,这时赋值语句实际上就是 var y = 6 * 2。 * 5. 现在,return y 返回值 12 作为调用 it.next(2) 的结果 */ // 生成器函数 function* foo(x) { var y = x * (yield); return y; } // 迭代器 let it = foo(6); // 启动生成器 it.next(); // {value: undefined, done: false} it.next(2); // {value: 12, done: true} it.next(); // {value: undefined, done: false}
- 第一个
- 消息是双向传递的
yield..
作为一个表达式可以发出消息响应next(..)
调用,next(..)
也可以向暂停的yield
表达式发送值- 如:
yield 'Hello Lee!!!'
向第一次it.next();
传递了字符串参数it.next(2);
向等待的yield..
传递了2
- 注意:
return
不是必须的!
// 生成器函数 function* foo(x) { var y = x * (yield 'Hello Lee!!!'); return y; } // 迭代器 let it = foo(6); // 启动生成器 it.next(); // {value: 'Hello Lee!!!', done: false} it.next(2); // {value: 12, done: true} // 结束 it.next(); // {value: undefined, done: false}
多个迭代器
var z = 1;
function* foo() {
var x = yield 2;
z++;
var y = yield (x * z);
console.log(x, y, z);
}
var it1 = foo();
var it2 = foo();
var {
value: val1 } = it1.next(); // 2 执行到 var x = yield 2; 暂停,此时还没有执行到 z++,所以 z = 1, x = 2, val1 = 2;
var {
value: val2 } = it2.next(); // 2 执行到 var x = yield 2; 暂停,此时还没有执行到 z++,所以 z = 1, x = 2, val2 = 2;
val1 = it1.next(val1 * 10).value; // 40 从 var x = yield 2; 执行到 var y = yield (x * z); 暂停,此时 x = val1 * 10 = 2 * 10 = 20, z = z++ = 2, y = x * z = 20 * 2 = 40, val1 = 40;
val2 = it2.next(val1 * 5).value; // 600 从 var x = yield 2; 执行到 var y = yield (x * z); 暂停,此时 x = val1 * 5 = 40 * 5 = 200, z = z++ = 3, y = x * z = 200 * 3 = 600, val2 = 600;
it1.next(val2 / 2); // 20 300 3 从 var y = yield (x * z); 执行到 console.log(x, y, z),这时 var y = yield (x * z); 被看作整体 var y = yield ...,此时 x = 20, z = 3, y = val2 / 2 = 600 / 2 = 300;
it2.next(val1 / 4); // 200 10 3 从 var y = yield (x * z); 执行到 console.log(x, y, z),这时 var y = yield (x * z); 被看作整体 var y = yield ...,此时 x = 200, z = 3, y = val1 / 4 = 40 / 4 = 10;
生成器产生值
迭代器
假定你要产生一系列值,其中每个值都与前面一个有特定的关系。要实现这一点,需要一个有状态的生产者能够记住其生成的最后一个值
- 闭包解决方案:
const gimmeSomething = (function () { let nextVal = 0; return function () { nextVal += 2; return nextVal; } }()); console.log(gimmeSomething()); // 2 console.log(gimmeSomething()); // 4 console.log(gimmeSomething()); // 6 console.log(gimmeSomething()); // 8
- 迭代器来解决方案:
// 迭代器解决方案 const something = (function () { let nextVal = 0; return { // for..of循环需要 [Symbol.iterator]: function () { return this; }, // 标准迭代器接口方法 next: function () { nextVal += 2; return { done: false, value: nextVal }; } } }()); // 因为迭代器 something 总是返回 done:false for (const iterator of something) { console.log(iterator); // 2 4 6 8 返回对象中的value, 等价于 this.next().value, this指向当前对象 if(iterator >= 8) break; } console.log(something.next()); // {done: false, value: 10} console.log(something.next()); // {done: false, value: 12} console.log(something.next()); // {done: false, value: 14} console.log(something.next()); // {done: false, value: 16}
- 数组创建迭代器
var arr = [1, 2, 3, 4, 5, 6]; var it = arr[Symbol.iterator](); for (const iterator of it) { console.log(iterator); // 1 2 3 4 5 6 }
var arr = [1, 2, 3, 4, 5, 6]; var it = arr[Symbol.iterator](); console.log(it.next()); // {value: 1, done: false} console.log(it.next()); // {value: 2, done: false} console.log(it.next()); // {value: 3, done: false} console.log(it.next()); // {value: 4, done: false} console.log(it.next()); // {value: 5, done: false} console.log(it.next()); // {value: 6, done: false} console.log(it.next()); // {value: undefined, done: true}
生成器
- 无限生成器
function* foo() { let nextVal = 0; while (true) { nextVal += 1; yield nextVal; } } let it = foo(); console.log(it.next()); // {value: 1, done: false} console.log(it.next()); // {value: 2, done: false} console.log(it.next()); // {value: 3, done: false}
异步迭代生成器(解决回调地狱问题-回调中带有回调)
- 回调:
// 回调方式 function request(params, callback) { $.ajax({ method: 'GET', url: './response.json', // { "code": 200, "msg": "ok", "data": {} } data: params, success(res, status) { let data = { ...res, data: params }; callback(data, status); }, error(err, status) { callback(err, status); } }) } request({ name: 'Lee' }, (data1, status1) => { // {"code":200,"msg":"ok","data":{"name":"Lee"}} success console.log(JSON.stringify(data1), status1); request({ name: 'Tom'}, (data2, status2) => { // {"code":200,"msg":"ok","data":{"name":"Tom"}} success console.log(JSON.stringify(data2), status2); }); });
- 如果想要通过生成器来表达同样的回调任务流程控制,怎么实现?
let it = null; function request(params) { $.ajax({ method: 'GET', url: './response.json', // { "code": 200, "msg": "ok", "data": {} } data: params, success(res, status) { let data = { ...res, data: params }; it.next(data); }, error(err, status) { it.throw(err); } }) } // 等价于回调 function* main() { try { let data1 = yield request({ name: 'Lee'}); console.log(data1); // {"code":200,"msg":"ok","data":{"name":"Lee"}} let data2 = yield request({ name: 'Tom'}); console.log(data2); // {"code":200,"msg":"ok","data":{"name":"Tom"}} let data3 = yield request({ name: '张三'}); console.log(data3); // {"code":200,"msg":"ok","data":{"name":"张三"}} } catch (error) { console.log(error); } } it = main(); it.next();
- 分析;
it = main();
定义一个迭代器it.next();
启动了生成器*main()
,并运行到了第一个yield
(let data1 = yield request({name: 'Lee'});
)- 这时
it
已经存在值了,接下来调用了request
函数,并将成功或失败结果作为参数传给了下次调用生成器(it.next(data);
) - 接下来程序走到了第二个
yield
(let data2 = yield request({name: 'Tom'});
) - 以此类推···
- 分析;
- 以上代码还有一个特点:
异步请求的方法,变成了同步请求
- 另一种
异步转同步
的解决方案async
await
function request(params) { return new Promise((resolve, reject) => { $.ajax({ method: 'GET', url: './response.json', // { "code": 200, "msg": "ok", "data": {} } data: params, success(res, status) { let data = { ...res, data: params }; resolve(data); }, error(err, status) { reject(err); } }) }) } async function main() { let data1 = await request({ name: 'Lee' }); console.log(data1); // {"code":200,"msg":"ok","data":{"name":"Lee"}} let data2 = await request({ name: 'Tom' }); console.log(data2); // {"code":200,"msg":"ok","data":{"name":"Tom"}} let data3 = await request({ name: '张三' }); console.log(data3); // {"code":200,"msg":"ok","data":{"name":"张三"}} } main();
- 另一种
生成器+Promise
生成器
+Promise
实现分页调用接口问题
function request(params) {
return new Promise((resolve, reject) => {
$.ajax({
method: 'GET',
url: './response.json', // { "code": 200, "msg": "ok", "data": {} }
data: params,
success(res, status) {
let data = {
...res, data: params };
resolve(data);
},
error(err, status) {
reject(err);
}
})
})
}
function* main() {
let [pageNum, pageSize] = [1, 10];
while (true) {
yield request({
pageNum, pageSize });
pageNum++;
}
}
let it = main();
// 第一页
it.next().value // Promise
.then(res => {
// { code: 200, data: {pageNum: 1, pageSize: 10}, msg: "ok" }
console.log(res);
})
.catch(err => {
console.log(err);
})
// 第二页
it.next().value // Promise
.then(res => {
// { code: 200, data: {pageNum: 2, pageSize: 10}, msg: "ok" }
console.log(res);
})
.catch(err => {
console.log(err);
})
生成器委托
function request(params) {
return new Promise((resolve, reject) => {
$.ajax({
method: 'GET',
url: './response.json', // { "code": 200, "msg": "ok", "data": {} }
data: params,
success(res, status) {
let data = {
...res, data: params };
resolve(data);
},
error(err, status) {
reject(err);
}
})
})
}
function* foo() {
var r2 = yield request({
name: 'Lee' });
var r3 = yield request({
name: 'Tom' });
}
function* bar() {
var r1 = yield request({
name: '张三' });
// 通过 yeild* "委托"给*foo()
yield* foo();
}
let it = bar();
/**
* // {value: Promise, done: false}
* it.next().value.then(res => console.log(res.data.name)); // 张三
*
* // {value: Promise, done: false}
* it.next().value.then(res => console.log(res.data.name)); // Tom
*
* // {value: Promise, done: false}
* it.next().value.then(res => console.log(res.data.name)); // Lee
*
* // {value: undefined, done: true}
* it.next();
*
*/
for (const iterator of it) {
iterator.then(res => console.log(res.data.name)); // 张三 Tom Lee
}
- 为什么用委托
yield 委托的主要目的是代码组织,以达到与普通函数调用的对称
程序性能
Web Worker
JavaScript
当前并没有任何支持多线程执行的功能。但是,像你的浏览器这样的环境,很容易提供多个
JavaScript
引擎实例,各自运行在自己的线程上,这样你可以在每个线程上运行不同的程序。程序中每一个这样的独立的多线程部分被称为一个
(Web)Worker
。这种类型的并行化被称为任务并行,因为其重点在于把程序划分为多个块来并发运行。
- 可以从
JavaScript
主程序(或另一个Worker
)中,可以这样实例化一个Worker
:var w1 = new Worker( "http://some.url.1/mycoolworker.js" );
- 这个
URL
应该指向一个JavaScript
文件的位置(而不是一个HTML
页面!),这个文件将被加载到一个Worker
中。然后浏览器启动一个独立的线程,让这个文件在这个线程中作为独立的程序运行。
Worker
之间以及它们和主程序之间,不会共享任何作用域或资源,那会把所有多线程编程的噩梦带到前端领域,而是通过一个基本的事件消息机制相互联系。Worker w1
对象是一个事件侦听者和触发者,可以通过订阅它来获得这个Worker
发出的事件以及发送事件给这个Worker
。- 示例:
- 主线程
<!-- index.html --> <button onclick="send()">发送消息</button> <script> let worker = new Worker('./index.js'); console.log(worker); // 发送消息 function send(){ worker.postMessage("向子js发送消息"); } // 接受消息 worker.addEventListener("message", function (e) { console.log('接收子js的消息:', e, e.data); }); </script>
- 子线程
/* index.js */ // 接受消息 addEventListener("message", function (e) { console.log('接收父js的消息:', e, e.data); }); // 发送消息 postMessage("向父js发送消息!!!");
- 主线程
Worker 环境
在
Worker
内部是无法访问主程序的任何资源的。这意味着你不能访问它的任何全局变量,也不能访问页面的 DOM 或者其他资源。
记住,这是一个完全独立的线程
。
- Worker可以访问全局变量和功能的本地复本有哪些:
- 执行
网络操作(Ajax、WebSockets)
以及设定定时
navigator
、location
、JSON
和applicationCache
importScripts
向 Worker 加载额外的 JavaScript 脚本- 比如:
importScripts( "foo.js", "bar.js" );
- 这些脚本加载是同步的,也就是说,importScripts(…) 调用会阻塞余下 Worker 的执行,直到文件加载和执行完成
- 比如:
- 执行
let worker = new Worker('./index.js');
// 发送消息
worker.postMessage("向子js发送消息");
// 接受消息
addEventListener("message", function (e) {
console.log(this);
console.log(this.location);
console.log(this.navigator);
console.log(this.setTimeout);
console.log(this.setInterval);
console.log(JSON);
console.log(this.WebSocket);
console.log(XMLHttpRequest);
console.log(importScripts);
// console.log('接收父js的消息:', e, e.data);
});
Web Worker
通常应用于哪些方面呢?- 处理密集型数学计算
- 大数据集排序
- 数据处理(压缩、音频分析、图像处理等)
- 高流量网络通信
共享 Worker - SharedWorker
<button onclick="send()">发送消息</button>
<script>
let sw = new SharedWorker('./index.js');
console.log(sw);
// 发送消息
function send(){
sw.port.postMessage({
name: 'Lee'});
}
// 接受消息
sw.port.addEventListener("message", function (e) {
console.log('接收子js的消息:', e, e.data);
});
sw.port.start();
</script>
addEventListener("connect", function (ev) {
// 这个连接分配的端口
var port = ev.ports[0];
port.addEventListener("message", function (e) {
setTimeout(() => {
port.postMessage(`处理后的数据:${
JSON.stringify(e.data)}`);
}, 3000)
});
// 初始化端口连接
port.start();
});
性能测试
测试程序性能库
Benchmark.js