Skip to content

Instantly share code, notes, and snippets.

@yangfch3
Last active January 17, 2020 07:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yangfch3/4f737064c777f761bbae725ab01cb144 to your computer and use it in GitHub Desktop.
Save yangfch3/4f737064c777f761bbae725ab01cb144 to your computer and use it in GitHub Desktop.
JavaScript 异步编程分享使用代码
/**
* 依次读取文件 A、B、C
*/
/**
* 4. async-await
*/
const fs = require('fs'),
promisedReadFile = util.promisify(fs.readFile); // util.promisify: Node v8.x.x 新增 API
async function readFiles() {
let fileData;
fileData = await promisedReadFile('path/to/fileA', 'utf8');
console.log(fileData);
fileData = await promisedReadFile('path/to/fileB', 'utf8');
console.log(fileData);
fileData = await promisedReadFile('path/to/fileC', 'utf8');
console.log(fileData);
}
readFiles().catch(function (err) {
console.log(err,stack);
})
## 刚加入聚完项目组时的情况:
1. 使用了 Pomelo 这个 Node.js 游戏服务器框架 —— Callback Hell
2. 代码全部以 ES5 为主
3. 开发初期,单个文件两千行……
4. 魔术数值与魔术字符严重影响新人接手项目
(gate、connector、room 服务器,RPC 机制简介)
(Pomelo 回调演示)
(Pomelo 回调 + 频繁 MySQL/Redis CRUD 回调 => 回调地狱)
(上两千行的文件)
(魔术字符串与魔术数值)
--------------------------------------
## 重构的时机选择
1. 1.0.3 版本开发完
2. 项目进入进入下一步思考阶段,有较多空闲时间
---------------------------
## 重构的方向
1. 从 ES5 到 ES6
(node.green 查看 Node.js LTS 版本对 ES6 的支持度)
2. 消除开发初期的魔术字面量
3. 面向对象的重新设计,使用设计模式来组织改进代码
4. 消除冗杂的 if…else…
(不同的消息码消息的处理带来的 if...else...)(策略模式)
5. 消除回调地狱
(Pomelo、MySQL 等都采用了回调的方式来进行异步处理)
(演示:wbapp v1.0 回调地狱)
(演示:快的回调地狱 http://www.kuaidadi.com/assets/js/animate.js)
**重点**:使用新的异步解决方案改进项目
---------------------------
## 1-4
(refactor 分支源码,对比 1.0.3 源码)
---------------------------
## Node.js 异步 —— 双刃剑
1. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.
—— 非阻塞模型的实现以来底层的时间循环
2. Node.js 单线程执行、事件循环与任务队列
3. https://segmentfault.com/img/bVyNKc libuv:事件循环(Event Loop)、一个线程池、文件系统 I/O、DNS 相关和网络 I/O
4. 具体到项目中异步产生的问题:
1. 思路差异(异步使得我们不能像简单地同步模式那样来组织代码)
2. **回调带来的调用栈丢失,排错困难**
2. **回调地狱,文件超长**
3. 太多异步导致了程序状态的不确定性(这部分是难以避免的,有异步就有这些问题,只能减轻,不能杜绝)
* 许多同步的操作依赖前置异步操作的正确返回,然而 **等待异步返回的过程中可能房间的状态已经被其他同步、异步代码改变**
  * 例如:加入房间需要 Read 数据库(异步操作),在 read 完之后可能房间状态已经被其他代码改变
  * 例如:房间仅剩的玩家退出房间与新玩家进入房间同时触发时,房间状态在 Redis 回收是异步
解决异步带来程序不确定性的思路:
  * 程序设计上针对异步做相应的改变
   * 代码组织上适应异步的特性
  * 锁/占用-释放占用
  * 异步操作前、异步操作后都进行状态校验
--------------------------
## 解决回调地狱
目的:改变代码的流动方向(→ 到 ↓)
方案:Promise 化 —— then、catch
  * 越来越多 API 都支持默认返回 Promise 对象(例如:fetch)
  * 可以自己封装 API 成 Promise-Based
新的问题:
1. 一堆 .then()
2. 多个相同/相似的异步操作按照一路 .then 的写法太繁琐
Promise 是 ES 异步解决方案的基石
--------------------------
## 流程控制
需要流程控制的情境:异步操作 n+1 执行依赖异步操作 n 的成功返回
1. 类似/相同异步操作的流程控制(读取文件1 -> 读取文件2 -> ...)
* 递归的回调
  * 递归的 Promise.then
  * Generator 函数 + Thunkify 回调 + 流程控制器(即一个递归函数)
    Generator 函数 + Promise + 流程控制器(即一个递归函数)
(原理也是递归)
2. 差异较大的异步操作的流程控制(读数据库 -> 文件读取 -> ...)
  * async-await(Node.js >= 7.6.0)
* co(Node.js < 7.6.0)
--------------------------
兼容性考量:
1. caniuse
2. node.green
--------------------------
思考题:见问题.md
/**
* 依次读取文件 A 和 B
*/
/**
* 1. 普通回调版
*/
const fs = require('fs');
fs.readFile('path/to/fileA', 'utf8', (err, data) => {
if (err) {
console.error(err.stack);
return;
}
console.log(data);
fs.readFile('path/to/fileB', 'utf8', (err, data) => {
if (err) {
console.error(err.stack);
return;
}
console.log(data);
});
});
/**
* 依次读取文件 A、B、C
*/
/**
* 5. co
*/
const fs = require('fs'),
promisedReadFile = util.promisify(fs.readFile);
co(function* () {
let fileData;
fileData = yield promisedReadFile('path/to/fileA', 'utf8');
console.log(fileData);
fileData = yield promisedReadFile('path/to/fileB', 'utf8');
console.log(fileData);
fileData = yield promisedReadFile('path/to/fileC', 'utf8');
console.log(fileData);
}).catch((err) => {
console.log(err.stack);
})
/**
* 依次读取文件 A、B、C
*/
/**
* 3. Generator 函数对相同/同类异步 API 的流程控制
* thunkReadFile('path/to/fileA', 'utf8')
*/
const fs = require('fs'),
thunkReadFile = thunkify(fs.readFile);
// 3.1 Generator 函数 + Thunkify 回调
function* readFiles() {
let fileAData;
fileAData = yield thunkReadFile('path/to/fileA', 'utf8');
console.log(fileAData);
fileAData = yield thunkReadFile('path/to/fileB', 'utf8');
console.log(fileAData);
fileAData = yield thunkReadFile('path/to/fileC', 'utf8');
console.log(fileAData);
}
const readFilesGen = readFiles();
// 递归执行器
let run = (fn) => {
let gen = fn();
let next = (err, data) => {
let result = gen.next(data);
if (result.done) {
return
} else {
result.value(next);
}
}
next();
}
run(readFiles);
// 3.2 Generator 函数 + Promise
function* readFiles() {
let fileAData;
fileAData = yield promisedReadFile('path/to/fileA', 'utf8');
console.log(fileAData);
fileAData = yield promisedReadFile('path/to/fileB', 'utf8');
console.log(fileAData);
fileAData = yield promisedReadFile('path/to/fileC', 'utf8');
console.log(fileAData);
}
const readFilesGen = readFiles();
// 递归执行器
let run = (fn) => {
let gen = fn();
let next = (err, data) => {
let result = gen.next(data);
if (result.done) {
return result.value;
}
result.value.then((data) => {
next(data);
})
}
next();
}
run(readFiles);
/**
* 依次读取文件 A 和 B
*/
/**
* 2. Promise 版
*/
const fs = require('fs');
// fs.readFile() 默认使用的是回调机制,而非返回一个 Promise 对象
// 所以需要先经过一层包装
const promisedReadFile = (pathToFile, option) => {
return new Promise(function (resolve, reject) {
fs.readFile(pathToFile, option, (err, data) => {
err ? reject(err) : resolve(data);
})
})
}
// 也可以使用 8.x.x 版的 util.promisify() 快速包装
promisedReadFile('path/to/fileA').then((data) => {
console.log(data);
return promisedReadFile('path/to/fileB');
}).then((data) => {
console.log(data);
}).catch((err) => {
console.log(err.stack);
})

浏览器

Event Loop -> MicroQueue -> MacroQueue

macrotasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering

microtasks: Promises, Object.observe, MutationObserver

https://ruiming.me/archives/158 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ https://i.loli.net/2017/08/25/59a01ccba2bbe.png

setTimeOut 0ms 并非真的是 0ms,在浏览器中会转为浏览器支持的最小值(Chrome 是 1ms) setTimeout(fn, 0) ===> setTimeout(fn, minValue)

Node.js

http://voidcanvas.com/setimmediate-vs-nexttick-vs-settimeout/ https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/ http://voidcanvas.com/wp-content/uploads/2017/02/event-loop.png


setTimeOut 0ms 并非真的是 0ms,在浏览器中会转为引擎支持的最小值(V8 是 1ms) setTimeout(fn, 0) ===> setTimeout(fn, minValue)

问题1:尝试说出下面代码的打印输出

setTimeout(function() {
    console.log(1)
}, 0);
new Promise(function executor(resolve) {
    console.log(2);
    for( var i=0 ; i<10000 ; i++ ) {
        i == 9999 && resolve();
    }
    console.log(3);
}).then(function() {
    console.log(4);
});
console.log(5);

2 3 5 4 undefined 1 见 底层.md

问题2:setTimeOut(fn, 0)、setImmediate(fn) 以及 process.nextTick(fn) 执行的先后顺序

见 底层.md

问题3:现我为某一 DOM 元素的点击事件绑定了一个 handler,然后用代码模拟一个点击事件,那么 handler 是否会异步执行?

不会,handler 会立即执行。

问题4:DOM 的突变事件(见 F12>Source>Event Listener Breakpoints>DOM Mutation)在浏览器中不会以异步事件的方式对待,而是立即处理这些突变事件的回调,思考浏览器为什么要这样做

浏览器布局、渲染高度依赖当前 DOM 的状态,异步会带来 DOM 的不确定性

问题5:Promise.resolve(Promise.reject(new Error('err'))) 的返回值

Promise.resolve 如果接收的是一个 Promise 对象,那么它会将这个 Promise 对象原封不动地返回。所以上面代码的返回值是一个 value 为 Error 对象,状态为 rejected 的 Promise 对象。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment