Open nitroge opened 3 years ago
关于生成器,我这里不做过多介绍,可以通过ECMAScript 6 入门快速入门。
高级教程可以看Exploring ES6。
我对生成器的第一印象来自于php与python,写后台程序的时候,时不时有请求别的服务器资源的情况,这时候无论是在 python 下使用request,还是在 php 下使用curl,都是同步阻塞的,类似于在前端用XMLHttpRequest做 ajax 请求时,设置open方法的第三个参数为false,在这种情况下,在请求结果返回之前,其它代码是不会被执行的。
request
curl
open
false
那么,有什么办法可以解决这种等待着啥都干不了干着急的状态呢?一般去搜索都会给出两种常用的方案:多线程、多进程,稍微再广泛点找资料,就会发现另一个关键词:协程,也有叫纤程。
多线程
多进程
协程
纤程
简单来讲,协程是用户态的线程,何时执行,执行几行代码,何时停止,都由我们的代码来控制,对应的,我们常说的线程是由系统调度的,多个线程之间相互切换都是系统自动调度的。
线程
这三个概念,想进一步了解的看《进程、线程和协程的概念》。
要实现用户调度线程(伪),就需要请出我们今天的主角生成器。
线程(伪)
生成器
PS: 生成器与迭代器的关系也是暧昧不清的,请自行探索。
我们先用一个简单的例子一窥生成器的强大:
// 声明一个生成器函数 function* fn() { var ret = yield 123; console.log('recv==>', ret); } // 开始调用 var gen = fn(); // 得到生成器 var ret = gen.next(); // ret 为 {value: 123, done: false} var ret1 = gen.next(456); // 输出 recv==> 456 // ret1 为 {value: undefined, done: true}
从上面的例子,我们可以看到:
fn
yield
next
这套流程有木有很熟悉的感觉???
熟悉就对了,我们现在每天打交道的异步函数就是生成器的一个特殊情况(特殊在 await 后面一般是一个 Promise 对象),在那个没有异步函数的年代,大神tj就用生成器造出了co,实现了与我们现在使用的异步函数一样的编程体验。(墙裂推荐阅读其源码,就一个文件,加上注释、空行都不到 250 行代码)。
异步函数
tj
既然已经跑题了,就借力再跑远点。上面搬出co来镇场子,只是让你对生成器的强大抱有更大的信心。还有两个知名的对生成器用得很 6 的库是redux-saga、js-csp;
co
无论各路大神是如何使用生成器的,我们始终记住一个概念就好了:我们可以通过生成器决定一个函数何时运行、运行多少行、何时终止。
同步的业务流程,去中断一个函数的执行意义不大(至少我没想到这样的场景),但是涉及到异步的场景:计时、网络请求、事件...等等,配合生成器,我们可以把原来剪不断理还乱的异步流程,用人脑更容易理解的同步代码写出来。
不知道你能否一口气实现一个基本没什么 bug 的拖拽,反正我是做不到。
逻辑上其实简单:
就是一个这样看似简单的逻辑,先来看原生 js 版本:
在线运行
var box = document.getElementById('box'); var offset = {}; var draging = false; function move(x, y) { box.style.left = x + 'px'; box.style.top = y + 'px'; } box.addEventListener('mousedown', function(e) { draging = true; offset.x = e.offsetX; offset.y = e.offsetY; }); document.addEventListener('mouseup', function() { draging = false; }); document.addEventListener('mousemove', function(e) { if (draging) { move(e.x - offset.x, e.y - offset.y); } });
代码量其实并不大,但有两个问题不能忍受:
再来看看 rxjs 的版本:
const box = document.querySelector('#box'); const md$ = Rx.Observable.fromEvent(box, 'mousedown'); const mm$ = Rx.Observable.fromEvent(document, 'mousemove'); const mu$ = Rx.Observable.fromEvent(document, 'mouseup'); function move(x, y) { box.style.left = x + 'px'; box.style.top = y + 'px'; } // 核心逻辑 md$ .flatMap(() => mm$.takeUntil(mu$)) .withLatestFrom(md$, (e1, e2) => [e1.x - e2.offsetX, e1.y - e2.offsetY]) .subscribe(([x, y]) => move(x, y));
干净清爽多了!理解 rxjs 关于流的思想,一切也变得简单了。
流
最后看看,我用生成器实现的逻辑抽象版本:
首先,我折腾了两个晚上,封装了一个库,暴露了两个接口:go与listener,go接受一个生成器函数作为参数,生成器函数内部通过yield声明需要接收的信号,而listener则用于绑定事件并产生信号。然后,代码的逻辑就如下了:
go
listener
信号
const box = document.getElementById('box'); const mouseDown = listener(box, 'mousedown'); const mouseMove = listener(document, 'mousemove'); const mouseUp = listener(document, 'mouseup'); function move(x, y) { box.style.left = x + 'px'; box.style.top = y + 'px'; } // 核心逻辑 go(function*() { while (true) { let [{ offsetX, offsetY }] = yield mouseDown; // 现有mousedown 才有后面的事件 while (true) { let [{ type, x, y }] = yield [mouseMove, mouseUp]; // mousedown之后监听mousemove与mouseup if (type === 'mouseup') break; // 如果是mouseup 结束本次拖拽 move(x - offsetX, y - offsetY); // 否则计算最新拖拽位置 } } });
是不是线性的思考?
这个库的代码并不多,除开考虑比较好看的处理yield [mouseMove, mouseUp]这样的语法而增加的自定义数据结构linerDict,核心代码就那么几行:
yield [mouseMove, mouseUp]
linerDict
const signalMap = linerDict(); function linerDict() { const map = new Map(); function getAll(key) { return [...map.keys()].reduce((acc, cur) => { if (cur === key || (Array.isArray(cur) && ~cur.indexOf(key))) { acc.push(map.get(cur)); } return acc; }, []); } function get(key) { return map.get(key); } function has(key) { let val = get(key); return val && val.length > 0; } function set(key, value) { map.set(key, value); } return { set, get, getAll, has, }; } // 核心函数 dispatch 与 run function dispatch(signal, ...args) { let arr = signalMap.getAll(signal); arr.forEach(tempArr => tempArr.splice(0).forEach(gen => run(gen, ...args))); } function run(gen, ...args) { let { value: signal } = gen.next(args); if (!signalMap.has(signal)) signalMap.set(signal, []); signalMap.get(signal).push(gen); } function go(fn) { run(fn()); } // 生成事件信号的工具函数 function listener(dom, eventName) { const eventSymbol = Symbol(eventName); dom.addEventListener(eventName, e => dispatch(eventSymbol, e)); return eventSymbol; }
这个思路还有很大的扩展空间,后面再抽时间完善(打算按这种方式实现下 rxjs 的部分功能),有兴趣的小伙伴也可以一起来探讨。
就此打住,希望我的此次尝试能勾起你对生成器的兴趣,优化那曾经写成一团糟的异步流程。
探索生成器(Generator)之拖拽从未如此简单
生成器
关于生成器,我这里不做过多介绍,可以通过ECMAScript 6 入门快速入门。
高级教程可以看Exploring ES6。
我对生成器的第一印象来自于php与python,写后台程序的时候,时不时有请求别的服务器资源的情况,这时候无论是在 python 下使用
request
,还是在 php 下使用curl
,都是同步阻塞的,类似于在前端用XMLHttpRequest做 ajax 请求时,设置open
方法的第三个参数为false
,在这种情况下,在请求结果返回之前,其它代码是不会被执行的。那么,有什么办法可以解决这种等待着啥都干不了干着急的状态呢?一般去搜索都会给出两种常用的方案:
多线程
、多进程
,稍微再广泛点找资料,就会发现另一个关键词:协程
,也有叫纤程
。简单来讲,
协程
是用户态的线程,何时执行,执行几行代码,何时停止,都由我们的代码来控制,对应的,我们常说的线程
是由系统调度的,多个线程之间相互切换都是系统自动调度的。这三个概念,想进一步了解的看《进程、线程和协程的概念》。
要实现用户调度
线程(伪)
,就需要请出我们今天的主角生成器
。我们先用一个简单的例子一窥生成器的强大:
从上面的例子,我们可以看到:
fn
可以通过yield
关键字自行决定函数执行到哪行代码就交出控制权next
方法取得内部传出的值next
方法被调用后,会重新将代码执行权交还给fn
,并且回到之前yield
调用的那个位置next
方法时,可以把外部的值传入fn
内部,作为对应的yield
表达式的结果。这套流程有木有很熟悉的感觉???
熟悉就对了,我们现在每天打交道的
异步函数
就是生成器的一个特殊情况(特殊在 await 后面一般是一个 Promise 对象),在那个没有异步函数的年代,大神tj
就用生成器造出了co,实现了与我们现在使用的异步函数
一样的编程体验。(墙裂推荐阅读其源码,就一个文件,加上注释、空行都不到 250 行代码)。既然已经跑题了,就借力再跑远点。上面搬出
co
来镇场子,只是让你对生成器的强大抱有更大的信心。还有两个知名的对生成器用得很 6 的库是redux-saga、js-csp;无论各路大神是如何使用生成器的,我们始终记住一个概念就好了:我们可以通过生成器决定一个函数何时运行、运行多少行、何时终止。
同步的业务流程,去中断一个函数的执行意义不大(至少我没想到这样的场景),但是涉及到异步的场景:计时、网络请求、事件...等等,配合生成器,我们可以把原来剪不断理还乱的异步流程,用人脑更容易理解的同步代码写出来。
拖拽
不知道你能否一口气实现一个基本没什么 bug 的拖拽,反正我是做不到。
逻辑上其实简单:
原生版本
就是一个这样看似简单的逻辑,先来看原生 js 版本:
在线运行
代码量其实并不大,但有两个问题不能忍受:
Rxjs 版本
再来看看 rxjs 的版本:
在线运行
干净清爽多了!理解 rxjs 关于
流
的思想,一切也变得简单了。生成器版本
最后看看,我用生成器实现的逻辑抽象版本:
在线运行
首先,我折腾了两个晚上,封装了一个库,暴露了两个接口:
go
与listener
,go
接受一个生成器函数作为参数,生成器函数内部通过yield
声明需要接收的信号
,而listener
则用于绑定事件并产生信号
。然后,代码的逻辑就如下了:是不是线性的思考?
这个库的代码并不多,除开考虑比较好看的处理
yield [mouseMove, mouseUp]
这样的语法而增加的自定义数据结构linerDict
,核心代码就那么几行:这个思路还有很大的扩展空间,后面再抽时间完善(打算按这种方式实现下 rxjs 的部分功能),有兴趣的小伙伴也可以一起来探讨。
就此打住,希望我的此次尝试能勾起你对生成器的兴趣,优化那曾经写成一团糟的异步流程。