",color=green];
625 | vec5 -> node8;
626 | }
627 | {rank=same; node0;node9;}
628 | {rank=same; node7;node11;node13}
629 | node0:f0 -> node1;
630 | node0:f1 -> node11;
631 | node1:f0 -> node2;
632 | node1:f1 -> node3;
633 | node2:f0 -> node4;
634 | node2:f1 -> node5;
635 | node3:f0 -> node6;
636 | node3:f1 -> node7;
637 | node8:f0 -> node9;
638 | node8:f1 -> node13;
639 | node9:f0 -> node1;
640 | node9:f1 -> node10;
641 | node10:f0 -> node12;
642 | node12:f0 -> node11;
643 | }
644 | #+END_SRC
645 |
646 | #+caption: 添加11
647 | #+RESULTS:
648 | [[file:./images/vecconj11.png]]
649 |
650 | 好了,看到这里,我们已经看到了 Clojure 的向量数据结构完整的添加元素的过程。我们可以看到整个过程并没有做全部数据的拷贝,而只是最多 log_{32}N次,也就是树的高度次的拷贝。总体来说复杂度应该是非常可观的,因为一个 6 层的 32 叉树已经能存放 10亿(1,073,741,824)个元素了,而10亿个元素的添加操作最多也只是 O(6*32),效率是非常不错的。
651 |
652 | 既然学会了看 Clojure 的源码,下来更新元素和弹出元素的过程可以留给读者研究了。类似的,效率也是O(log_{32}N)。
653 |
654 | ** 不可变性
655 |
656 | 在函数式世界里,所有东西在被创建出来之后都应该是不可变的,换句话说, 比如说我买了一个巨无霸汉堡,这汉堡会一直在那里,不对变两个巨无霸,不会变迷你 霸,不会发霉,也不会过期。这个巨无霸不管在任何时候,都是最初服务员给我递上来的那一个色香味恒定的巨无霸。
657 |
658 | *** 致命魔术
659 |
660 | #+BEGIN_QUOTE
661 | ⚠️ 本小节严重剧透,好奇心强的读者请看完电影再回来接着看。
662 | #+END_QUOTE
663 |
664 | 如果你看过克里斯托弗·诺兰的电影《致命魔术》(The Prestige),应该会对里面的安吉尔[fn:3]用特斯拉给的神秘装置复制自己来完成瞬间移动的魔术。虽然安吉尔不停的杀死自己确实做法极端,但是完全又印证了片中开头和结束解释的变魔术的三个步骤:
665 |
666 | 1. 让你看一个小鸟
667 | 2. 让小鸟 *“消失”*
668 | 3. 再把小鸟变 *“回来”* (这也是最难的步骤)
669 |
670 | 注意到“消失”和“回来”我都加了引号,因为小鸟是真的“消失”,而”回来“的其实是另一只几乎一样的小鸟。
671 |
672 | #+CAPTION: 电影《致命魔术》海报
673 | [[./images/The-Prestige.png]]
674 |
675 | 回到我们的话题上来,那么可变操作就像是让小鸟消失再回来,其实永远都找不回来消失的那只小鸟了。
676 |
677 | #+BEGIN_SRC js
678 | var magic = function(cage){
679 | cage[0] = {name:‘翠花’}
680 | }
681 | var birdInACage = [{name:’tweety’}]
682 | magic(birdInACage)
683 | birdInACage// => [{name:‘翠花’}]
684 | #+END_SRC
685 |
686 | 可以看到,经过 magic 函数后,tweety 就消失了,笼子里只有翠花,而这只被 =magic= 变没有的 tweety,不久之后就可能会被 JavaScript 的 GC(垃圾回收器)铲走。
687 |
688 | 但是,函数式编程并不喜欢魔术[fn:12],就像博登在台上把小鸟“变回来”时,台下的小朋友哭着说我要原来那只小鸟一样。函数式编程时,我们更希望在不论何时都可以找回来原来那只小鸟。
689 |
690 | 因此,我们需要一种神奇的模式把 twetty 隐藏起来。
691 | #+BEGIN_SRC js
692 | var anotherBirdInTheCage = magic(birdInACage)
693 | function magic(birdInCage){
694 | return birdInCage.map(function(bird){return bird.name='翠花'})
695 | }
696 | anotherBirdInTheCage// => [{name:‘翠花’}]
697 | birdInACage // => [{name:'tweety'}]
698 | #+END_SRC
699 |
700 | 太好了,twetty 没有“消失”,只是多了一只叫做翠花的小鸟。
701 |
702 | 虽然可变性 给我们编程带来了一些便利,这可能是因为我们的真实世界的所有东西都是可变的,这非常符合我们真实环境的思维方式。但是,这种可变性也能带来类似现实世界一样不可预测性的问题,有可能在不经意间会给我带来一些困扰,而却很难推理出产生这种困扰的原因。
703 |
704 | *** 引用透明性
705 |
706 | 由于所有的对象都是可变的,就像现实世界一样,对象之间靠消息通信,而通过各种消息发来发去之后谁也不知道在某一时间这些对象的状态都是些什么。然而对象的行为又可能依赖于其他对象的状态。这样依赖,如果想推测一个对象某个时间的行为,可能需要先确定其所有有消息通信相关的对象这时的状态,以及通信所发生的先后顺序。
707 |
708 | 在函数式编程中有个概念叫 /引用透明性/ (Referential Transparent),引用透明是指函数对于相同输入一等返回相同的输出,因此如果将其替换成他的输出,也不会影响程序的结果。所以通常纯函数都是引用透明的。而越透明,说明代码越容易推理(reason about)。
709 |
710 | 写过前端 JavaScript 的人都应该非常清楚前端代码是非常难推理的,光看一段代码片段很难推测出其行为。通常,自由变量越多,行为越不确定,而前端的 /自由变量/[fn:6] 太多太多:
711 |
712 | 1. DOM:不管是谁都可以修改。
713 | 2. 全局变量:谁都可以改。
714 | 3. Event:事件是全局的,如果你句柄函数如果不纯会导致结果无法预测,因为谁都可以在任何时候触发相应的事件。
715 | 4. 持久化的数据:比如 localStorage, cookie 之类的,依赖他们比依赖全局变量还难以预测。
716 |
717 | 而通常 JavaScript 或前端一些框架,都或多或少的依赖于这些因素。
718 |
719 | 有意思的是最近火热的 facebook 的 UI 库 ReactJS 就相对更容易推理。因为它使用了单向数据流状态机模型,VirtualDOM 的使用很好的隔离开了 DOM 的状态。React 的成功也充分的诠释了面向对象和函数式编程的完美结合。正常一个 React 控件是这样工作的:
720 |
721 | #+BEGIN_SRC dot :file images/react-flow.png :exports results
722 | digraph{
723 | { rank = same; "初始化";"属性更新";"状态更新"; }
724 | 数据 -> 属性更新-> VirtualDOM
725 | 初始化 -> VirtualDOM
726 | 状态更新 -> VirtualDOM
727 | VirtualDOM -> DOM [label=diff]
728 | DOM -> 状态更新 [label=用户事件, style=dotted]
729 | }
730 | #+END_SRC
731 |
732 | #+caption: React 控件隔离变化
733 | #+RESULTS:
734 | [[file:images/react-flow.png]]
735 |
736 | 所以,React 的模型为更高内聚的模型[fn:4],只有当自己的属性和状态发生变化时,才会重新的返回该状态和属性下的 *全新* 控件。注意是全新的,不同于传统的修改 DOM 的可变性模型,React 的任何操作都是返回全新控件的不可变操作,就像操作 vector 一样,不会去修改,而是再建一个新的。而且,React 把所有可变的部分都隔离了,所有的可变的因素如,用户事件,数据变化,其他上下游控件的影响,都隔离在状态和属性之外。这样做使得我们的控件行为更简单,容易推理,也容易测试。就像接受两个参数(状态,属性)的函数,给定这两个参数 ,那么返回的控件一定是一样的。而可变的 DOM,也被 VirtualDOM 隔离了。所以完全可以把所有 React 的控件编写的像纯函数一样。因此,也可以像纯函数一样轻松的把一个组件替换掉,轻松解耦了组件之间的关系。
737 |
738 | *** 线程不安全
739 |
740 | 前端 JavaScript 虽然说是单线程的,但是基于事件循环的并发模型一样会遇到多线程的线程安全问题。线程不安全是指一个值会被多个线程中的操作同时修改。带来的问题是你很难预测以及重现这个值在某个时间到底是什么。 解决线程安全通常会用到互斥锁,原子操作等等,这些方式大大的增加编程和测试的难度。
741 |
742 | 在前端即使没有多线程同样会遇到一样的问题,比如在期望线程安全的一个事物操作中,某个值突然被修改了:
743 |
744 | #+BEGIN_SRC js
745 | // 假设收钱比如使用第三方支付宝之类的, 这里假设100ms之后知道支付成功,然后调用回调函数
746 | function charge(order,callback){
747 | setTimeout(callback.bind(this,order), 100)
748 | }
749 | // 假设熊孩子喝牛奶只需要99ms(可能熊孩子是闪电侠)
750 | function drinkMilkThenChange(order){
751 | setTimeout(order.push({name:'R2D2',price:99999}),
752 | 99)
753 | }
754 | // 打印发票
755 | function printReceipt(order){console.log(order)}
756 | // 熊孩子买了两个东西
757 | var order = [{name:'kindle',price:99}, {name:'drone', price:299}];
758 | // 熊孩子结账
759 | charge(order, printReceipt)
760 | // 熊孩子喝了杯牛奶后过来修改订单
761 | drinkMilkThenChange(order)
762 | // 这时熊孩子发票上有三个东西
763 | // [{name:'kindle',price:99}, {name:'drone', price:299}, {name: 'R2D2', 99999}]
764 | #+END_SRC
765 |
766 | 这里到底发生了什么?单线程也不安全吗?难道要给 order 加锁吗? 这里的 setTimeout 都是写死的多少秒,如果是真实代码多几个熊孩子而且发 ajax 请求不确定回调时间之类的,你永远猜不到最后打印出来的发票上有些什么。
767 |
768 | 首先,让我来解释一下这里到底发生了什么。使用多线程的思路的话,charge 应该是个 io 操作,通常需要 fork 一个线程来做,这样就不阻塞主线程。于是 printReceipt 就是运行在 fork 出来的另一个线程,意味着我在主线程的操作修改到了子线程依赖的值,导致了线程不安全。
769 |
770 | 但是 JavaScript 在单线程的运行环境下如何做到线程不安全?单线程,说的是 JavaScript 运行的主线程,但是浏览器可以有若干线程处理这样的 IO 操作,也就是维护传说中的 /事件循环/ 。就拿刚才简单的 setTimeout 为例,其实是另一个线程在100毫秒之后把回调函数放入到事件循环的队列中。
771 |
772 | 所以解决方式是加锁吗? 在每次收钱之前,把订单锁上:
773 |
774 | #+BEGIN_SRC js
775 | function charge(order,callback){
776 | Object.freeze(order);
777 | setTimeout(callback.bind(this,order), 100)
778 | }
779 | drinkMilkThenChange(order)
780 | // Uncaught TypeError: Cannot assign to read only property 'length' of [object Array]
781 | #+END_SRC
782 |
783 | 当然加锁可以解决,但是更容易而且无需考虑是多线程的方式则是简单的使用不可变数据结构。简单的把 order 的类型改成 vector 就可以了:
784 |
785 | #+BEGIN_SRC js
786 | function charge(order,callback){
787 | setTimeout(callback.bind(this,order), 100)
788 | }
789 | function drinkMilkThenChange(order){
790 | setTimeout(mori.conj(order,{name:'R2D2',price:99999}),
791 | 99)
792 | }
793 | var order = mori.vector({name:'kindle',price:99}, {name:'drone', price:299})
794 | function printReceipt(order){console.log(order.toString())}
795 | charge(order, printReceipt)
796 | drinkMilkThenChange(order)
797 | // [#js {:name "kindle", :price 99} #js {:name "drone", :price 299}]
798 | #+END_SRC
799 |
800 | 不可变性保证了不管是主线程代码还是回调函数,拿到的值都能一直保持不变,所以不再需要关心会出现线程安全问题。
801 |
802 | ** 惰性序列
803 |
804 | 还记得介绍向量时这个怪怪的返回吗?
805 | #+BEGIN_SRC js
806 | mori.rest(vec) // => (2 3 4)
807 | #+END_SRC
808 |
809 | 我明明是取一个向量的尾部,为什么返回的不是方括号的向量,而是圆括号呢?
810 |
811 | 这个圆括号代表惰性序列(lazy sequence),当然,我接着要来定义 /惰性/ 和 /序列/ 。
812 |
813 | 这一章既介绍了集合 API 又读了 Clojure 源代码,实在是太无聊了,我自己都快写不下去了,所以我们不妨先暂停一下,来一个十分生动的故事稍微提提神。
814 |
815 | *** 改良吃奥利奥法
816 |
817 | 还是吃奥利奥这件事情,如果你已经忘了,我们来回顾一下之前的吃法:
818 |
819 | 1. 掰成两片,一片是不带馅的,一份是带馅的。
820 | 2. 带馅的一半沾一下牛奶。
821 | 3. 舔掉中间夹心的馅。
822 | 4. 合起来吃掉。
823 |
824 | 这是吃一个奥利奥的方法,我要把这个步骤写下来(这个故事的设定是我的记忆力极差,不写下来我会忘了该怎么吃)。既然学过 map 函数,我们试试要怎么将我的吃法 map 到一整包奥利奥上。首先封装一下如何吃一个奥利奥的步骤:
825 |
826 | #+BEGIN_SRC js
827 | function lipMiddle(oreo){
828 | var wetOreo = dipMilk(oreo);
829 | var [top, ...middleBottom] = wetOreo;
830 | var bottom = lip(middleBottom);
831 | return [top, bottom];
832 | }
833 | eat(lipMiddle(oreo));
834 | #+END_SRC
835 |
836 | 然后我们开始吃整包奥利奥(underscore 版吃法):
837 |
838 | #+BEGIN_SRC js
839 | var _ = require('underscore')
840 | var oreoPack = _.range(10).map(function(x){return ["top","middle","bottom"]})
841 | var wetOreoPack = _.map(oreoPack,lipMiddle);
842 | _.each(wetOreoPack, eat)
843 | #+END_SRC
844 |
845 | 1. 按照吃奥利奥步骤,我挨个舔掉一整包奥利奥的馅,然后放回袋子里。
846 | 2. 一个一个吃掉舔过的湿湿的奥利奥。
847 |
848 | 问题是,我其实并不知道自己能不能吃完整包,但是按照这种吃法的话, 我会打开并且着急的把所有奥利奥都沾了下牛奶,把馅舔掉,又塞回了袋子里。
849 |
850 | 假如我吃了两块就发现吃不下去了,我把袋子封好,然后困得不行去睡觉了。过了两天打开袋子发现我的奥利奥全发霉了。于是开始抱怨为什么当初不吃的要手贱去沾一下牛奶,太浪费了不是吗。
851 |
852 | 我是个特别抠门的人,于是开始苦思冥想到底吃奥利奥的方式哪里有问题。
853 |
854 | 很明显我不应该贪心的先吃掉整包奥利奥的馅,我应该吃多少就舔多少奥利奥的馅。但是问题是,我怎么知道我要吃多少呢?
855 |
856 | 又经过一番又一番的苦思冥想,我终于想到了在不知道能吃多少块的情况下怎样完美的吃一包奥利奥(mori 版吃法):
857 |
858 | 1. 把吃的步骤写成10长小条(假设一包有十块奥利奥)
859 | 2. 把小条依次贴到每块奥利奥上
860 | 3. 待吃的时候每拿出来一个,按照奥利奥上的小条的步骤开始吃
861 | 4. 完美!
862 |
863 | 写成代码该是长这样的:
864 | #+BEGIN_SRC js
865 | var oreoPack = mori.repeat(["top","middle","bottom"]);
866 | var wetOreoPack = mori.map(lipMiddle,oreoPack);
867 | // 条都塞好了,现在该吃了,假设我吃3块
868 | mori.each(eat, mori.take(3, wetOreoPack));
869 | #+END_SRC
870 |
871 | 故事就这么圆满的结束了!于是公主和王子......
872 |
873 | 等等,这个实现怎么看着跟前面 underscore 的实现没有什么两样,到底是在哪里把小条塞进去的?
874 |
875 | *** 惰性求值 VS 及早求值
876 |
877 | 那么现在我们来看看 mori 是如何把小条塞进去的。在这之前,我们再来看看 underscore 版本的实现,细心的读者会发现我还没有实现 lip 函数,这个函数具体如何去舔奥利奥我们并不是很关心,暂且简单的打印出来点东西好了:
878 |
879 | #+BEGIN_SRC js
880 | function lip(oreo){
881 | console.log("舔了一下")
882 | return oreo
883 | }
884 | function dipMilk(orea){
885 | console.log("沾一下牛奶")
886 | return oreo
887 | }
888 | #+END_SRC
889 |
890 | 那么, map 我的吃奥利奥方式到整包奥利奥的时候会发生什么呢?
891 | #+BEGIN_SRC js
892 | var wetOreoPack = _.map(oreoPack,lipMiddle);
893 | // => " 沾一下牛奶" “舔了一下” 这两句话被打印10次
894 | #+END_SRC
895 |
896 | 而同样的 mori 版本的 map 却什么也不会打印出来:
897 | #+BEGIN_SRC js
898 | var wetOreoPack = mori.map(lipMiddle,oreoPack) // 无打印信息
899 | #+END_SRC
900 |
901 | 为什么会什么都没打印,难道没 map 上吗?当然不是,map 是成功的,但是 mori 的 map 不会真对每一块奥利奥都执行我的吃奥利奥流程 lipMiddle,它只会在奥利奥上贴上一张描述如何吃奥利奥的流程的小条。因此,什么也不会返回,相当于我把整包奥利奥打开,贴上小条,再放回原位,封好袋子。
902 |
903 | #+caption: 惰性吃奥利奥法
904 | [[./images/lazy-oreo.png]]
905 |
906 | 好了,生动的故事真的要圆满结束了,如果这个故事都听明白了的话,再加上几个学术名词,我想我已经解释完什么是惰性和为什么要使用惰性了。故事中的小条,叫做 /thunk/ (我在第一章提过),而这种贴过条的序列,叫做 /惰性序列/ ,对应的 map 操作方式,叫 /惰性求值/ 。 Underscore 的这种立即执行的 map 方式,叫做 /及早求值/ 。
907 |
908 | *** 惰性求值的实现
909 |
910 | 在了解这一大堆名词之后,我们来进一步研究如何具体实现一个惰性的数据结构。我将继续以吃奥利奥为例子,解释如何实现这个惰性的 map。
911 |
912 | 之前见到的 =mori.map(lipMiddle,oreoPack)= 没有打印出任何信息,按照我的例子的说法是因为“map 只把操作的过程写成小条贴到饼干上”。那么,具体是如何把过程贴到这包奥利奥里的呢?
913 |
914 | 只要是涉及到实现,我必然要贴源代码,因为没有什么文档会比代码更真实。首先我们大眼看一下 map 的实现:
915 |
916 | #+BEGIN_SRC clojure -n -r
917 | ([f coll]
918 | (lazy-seq ;; <= 1 (ref:lazyseq)
919 | (when-let [s (seq coll)]
920 | (if (chunked-seq? s) ;; <= 2 (ref:chunkseq)
921 | (let [c (chunk-first s)
922 | size (int (count c))
923 | b (chunk-buffer size)]
924 | (dotimes [i size]
925 | (chunk-append b (f (.nth c i))))
926 | (chunk-cons (chunk b) (map f (chunk-rest s))))
927 | (cons (f (first s)) (map f (rest s))))))) ;; <= 3 (ref:cons)
928 | #+END_SRC
929 |
930 | 1. [[(lazyseq)][第(lazyseq)行]]中的 lazy-seq 的 macro,其实就是用来 new 一个新的 LazySeq 实例(源码在往上翻几页,在658行)。
931 | 2. 第一个分支处理 chunked-seq 类型的序列,返回一个包含两个元素的序列 =(chunk b)= 和 =(map f (chunk-rest s))= 。
932 | 3. 另外一个分支则处理普通序列,可以看出来返回一个包含两个元素的序列 =(f (first s))= 和 =(map f (rest s))= 。
933 |
934 | 两种分支其实返回的都差不多,都是两个元素, 而第二个元素都是递归的再次调用 =map= 。我们先别看第一个分支,看看第二个简单分支。重要的是,所有的过程都放在一个叫 =lazy-seq= 的 macro 中。如果我们把 =(map lipMiddle oreoPack)= 代换展开的话会得到:
935 |
936 | #+BEGIN_SRC clojure
937 | (lazy-seq (cons (lipMiddle (first oreoPack) (map lipMiddle (rest oreoPack)))))
938 | #+END_SRC
939 |
940 | 其中 =lazy-seq= 做的事情就是阻止 =(cons...)= 被求值,硬生生的把序列从 /应用序/ 变成 /正则序/ 。回到我们的例子,这样一来, =map= 其实就是创建了一个 =lazy-seq= 的对象或者容器,容器内的序列其实还没有被求值。所以在 =map= 之后不会有任何的打印信息,因为所有的东西其实都还没有被求值,也就是我例子中说的,只是给奥利奥贴上了写满过程的小条而已。
941 | 这个例子中,就是在吃奥利奥的时候,我们才真正需要进行这么一个吃奥利奥的过程。所以当我从一包奥利奥中拿一个准备吃的时候,我需要按照条上的过程操作一遍:
942 |
943 | #+BEGIN_SRC clojure
944 | (take 1 (map lipMiddle oreoPack))
945 | #+END_SRC
946 |
947 | 那么 =lazy-seq= 中的序列会被求值,意味着,两个元素都会被求值
948 |
949 | #+BEGIN_SRC clojure
950 | (cons lipedOreo (map lipMiddle (rest oreoPack))))
951 | #+END_SRC
952 |
953 | =(lipMiddle (first oreoPack)= 求值得到 =lipedOreo= 而 =(map lipMiddle (rest oreoPack)= 求值变成又一个 =lazy-seq=
954 | #+BEGIN_SRC clojure
955 | (lazy-seq (cons (lipMiddle (first (rest oreoPack))) (map lipMiddle (rest (rest oreoPack)))))
956 | #+END_SRC
957 |
958 | 以此类推,需要吃第二块奥利奥时,同样的再对上式 =lazy-seq= 容器中的序列求值。
959 |
960 |
961 | * Footnotes
962 |
963 | [fn:12] 程序员更是不喜欢别人代码中的魔术。
964 |
965 | [fn:11] Futurama 是我最爱看的动画片,作者同另一部我最爱看的动画片《辛普森一家》一样都是 Matt Groening。
966 |
967 | [fn:10] 我会在第四节专门介绍序列。
968 |
969 | [fn:9] 这让我想起了“抽象屏障”这个词。
970 |
971 | [fn:8] conjs(https://github.com/jcouyang/conjs) 是我 fork 过来的一个特别版本,不仅对 JavaScript 使用习惯做了改进,而且还加入了 core.async。
972 |
973 | [fn:7] 更多具体的安装和使用步骤可以参考 mori 的 README https://github.com/swannodette/mori
974 |
975 | [fn:6] 第四章会有专门的章节解释自由变量这个词。
976 |
977 | [fn:5] http://stackoverflow.com/questions/7533837/explanation-of-combinators-for-the-working-man
978 |
979 | [fn:4] 也有人认为 React 是紧耦合,不妨再仔细看看我画这张图。
980 |
981 | [fn:1] 源代码在 https://github.com/clojure/clojure/blob/36d665793b43f62cfd22354aced4c6892088abd6/src/jvm/clojure/lang/PersistentVector.java 第34行。
982 |
983 | [fn:2] 按 lisp 语言的传统来说 cons(construct) 代表的是组成包含一个头(car)和一个尾(cdr)的结构体,主要用于创建序列 list,在 Clojure 中就是 sequence。
984 |
985 | [fn:3] 休杰克曼Hugh Jackman饰,大家更熟悉的休杰克曼应该是X战警(X-MEN)里的金刚狼
986 |
987 |
988 |
989 |
--------------------------------------------------------------------------------
/book/zh/第八章.org:
--------------------------------------------------------------------------------
1 | #+OPTIONS: :makeindex nil
2 | #+LANGUAGE: zh-CN
3 | * COMMENT Import
4 | #+BEGIN_SRC emacs-lisp
5 | (require 'ob-ditaa)
6 | #+END_SRC
7 |
8 | #+RESULTS:
9 | : ob-ditaa
10 |
11 | * 并发编程
12 | 并发编程一直是令人头疼的编程方式,直到 Clojure 和 Go 的出现,彻底改变了我们并发编程的方式。而对于单线程的 JavaScript,基于事件循环的 并发模型也一直困扰着我们,到底能从 Clojure 学些什么,可以使我们的前端 并发编程之路更顺畅一些呢?本章将带你熟悉:
13 |
14 | 1. 什么是并发?
15 | 2. JavaScript 的并发模型。
16 | 3. CSP 并发模型。
17 | 4. 前端实践中如何使用 CSP。
18 |
19 | ** 什么是并发
20 | 在介绍 CSP 之前首先有两个概念需要强调一下,那就是并发与并行。 为了便于理解,我会结合现实生活举一个例子。
21 |
22 | 假设我正在上班写代码,老板过来拍着肩膀说明天要发布,加个班吧。于是我发个短信给老婆说晚点回,发完以后继续敲代码。那么请问,发短信和敲代码两个任务是 *并发* 还是并行 ?
23 |
24 | 但如果我还特别喜欢音乐,所以我边听音乐边敲代码,那么写代码和听音乐两个任务是并发还是 *并行* ?
25 |
26 | 为了不侮辱读者的智商,我就不公布答案了。所以说:
27 | - 并发是为了解决如何管理需要同时运行的多个任务。就例子来说就是我要决定的是到底先发短信,还是先写代码,还是写两行代码,发两个字短信呢?对于计算机来说,也就是线程管理。
28 | - 并行是要解决如何让多个任务同时运行。例子中的我享受音乐与写代码所用到的大脑区域可能并不冲突,因此可以让它们同时运行。对于计算机来说,就需要多个 CPU(核)或者集群来实现并行计算。
29 |
30 | 并行与并发的最大区别就是后者任务之间是互相阻塞的,任务不能同时进行,因此在执行一个任务时就得阻塞另外一个任务。
31 |
32 | *** 异步与多线程
33 | 所以说到并发,如果不是系统编程,我们大多关心的只是多线程并发编程。因为进程调度是需要操作系统更关心的事情。
34 |
35 | 继续敲代码这个例子,假如我现在能 fork 出来一只手发来短信,但是我还是只有一个脑袋,在发短信的时候我的脑子还是只能集中在
36 | 如何编一个理由向老婆请假,而另外两只手只能放在键盘上什么也干不了,直到短信发出去,才能继续写代码。
37 |
38 | 所以多线程开销大至需要长出(fork)一只手,结束后缩回去(join),但是这 些代价并没有带来时间上的好处,发短信时其它两只手其实是闲置(阻塞)着的。
39 |
40 | #+BEGIN_SRC ditaa :file images/multithread.png :exports results
41 |
42 |
43 | +---+ +---+ +---+
44 | Thread A ----+a1 +-+a2 +-+a3 +------------------>
45 | +-+-+ +-+-+ +-+-+
46 | : | :
47 | | : +-----+
48 | | | |
49 | v v v
50 | +---+ +---+ +---+ +---+ +---+
51 | CPU ----+a1 +-+a2 +-+b1 +-+a3 +-+b2 +------>
52 | +---+ +---+ +-^-+ +---+ +-^-+
53 | : |
54 | +-----------+ |
55 | | +-----------------+
56 | : :
57 | +-+-+ +-+-+
58 | Thread B ----+b1 +-+b2 +------------------------>
59 | +---+ +-+-+
60 |
61 | #+END_SRC
62 |
63 | #+caption: 线程任务到达 CPU 的不确定顺序
64 | #+RESULTS:
65 | [[file:images/multithread.png]]
66 |
67 | #+INDEX: 事件驱动模型
68 | 因此,另外一种更省资源的处理并发的方式就出现了——异步编程,或者叫 /事件驱动模型/ 。对的,就是我们在 JavaScript 代码里经常发的 Ajax 那个异步。
69 |
70 | 比如我还是两只手,我发完短信继续就敲代码了,这时,老婆给我回了一条短信,那我放下手中的活,拿起手机看看,老牌居然说了“同意”,于是就安心的放下手机继续敲代码了。
71 |
72 | 注意这段动作与之前多线程的区别,相对于多线程的场景下 fork 了第三只手在敲代码时一直呆呆的握着手机,异步编程并不需要增加胳膊,资源利用率更高一些。
73 |
74 | 那么你就要问了,你是怎么知道手机响的,还不是要开一个线程让耳朵监听着。对的,但是异步只需要很少的有限个线程就好了。比如我有十个手机
75 | 要发给十个老婆,我还是两个线程,相比如果是多线程的话我要 fork 出来十只手,却是会省了不少的资源的[fn:4]。
76 |
77 | 所以 JavaScript 的并发模型就是这么实现的,有一个专门
78 | 的事件循环([[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/EventLoop][event loop]])线程,就如同我们的耳朵,不停的检查消息队列中是否还有待执行的任务。
79 |
80 | *** JavaScript 的并发模型
81 | JavaScript 的并发模型主要基于事件循环,运行 JavaScript 代码其实就是从 event loop 里面取任务,队列中任务的来源为函数调用栈与事件绑定。
82 | - 每写一个函数 =f()= ,都会被加到消息队列中,运行该任务直到调用栈全部弹空。
83 | - 而像 =setTimeout(somefunction,0)= 其实是 注册一个事件句柄(event handler), timer 会在“0毫秒”后“立刻”往队列加入 =somefunction= (如果不是 0,则是 n 长时间后加入队列)
84 |
85 | #+BEGIN_SRC ditaa :file images/event-loop-model.png :exports results
86 | +---+ +---+ +---+
87 | Functions----+a1 +-+a2 +-+a3 +------------------>
88 | +-+-+ +-+-+ +-+-+
89 | : | :
90 | | : |
91 | | | |
92 | v v v
93 | +---+ +---+ +---+ +---+ +---+
94 | Queue for----+a1 +-+a2 +-+a3 +-+b1 +-+b2 +------>
95 | Event loop +---+ +---+ +---+ +-^-+ +-^-+
96 | | |
97 | +-----------------+ |
98 | | +-----------------+
99 | : :
100 | +-+-+ +-+-+
101 | Callbacks----+b1 +-+b2 +------------------------>
102 | +---+ +-+-+
103 | #+END_SRC
104 |
105 | #+caption: callback 会加到消息队列末尾
106 | #+RESULTS:
107 | [[file:images/event-loop-model.png]]
108 |
109 | #+BEGIN_SRC javascript
110 | function a(){
111 | console.log('a');
112 | }
113 | function b(){
114 | console.log('b');
115 | }
116 | function timeout(){
117 | console.log('timeout');
118 | }
119 | setTimeout(timeout,0);
120 | a();
121 | b();
122 | // => "a"
123 | // => "b"
124 | // => "timeout"
125 | #+END_SRC
126 |
127 | 这个例子中的 =timeout= 函数并没有在 =a= 或 =b= 之前被调用,因为当时的消息队列应该是这样的(处理顺序从左至右)
128 |
129 | #+BEGIN_SRC ditaa :file images/message-queue.png :exports results
130 | +-----------+-----+-----+--------+
131 | out <- |setTimeout |a |b |timeout | <- in
132 | +-----------+-----+-----+--------+
133 | #+END_SRC
134 |
135 | #+RESULTS:
136 | [[file:images/message-queue.png]]
137 |
138 | 现在,我们可以用的并发模型来再实现一下我们最开始的加班写代码的例子:
139 |
140 | #+BEGIN_SRC javascript
141 | let keepDoing = (something, interval) => {
142 | return setInterval(()=>console.log(something), interval);
143 | };
144 | let notify = function(read, callback, yesno){
145 | console.log('dinglingling')
146 | setTimeout(read.bind(callback), 2000)
147 |
148 | };
149 | let meSendingText = function(callback) {
150 | console.log('Me sending text');
151 | notify(wifeReadingText, callback)
152 | }
153 | let wifeReadingText = function(callback){
154 | console.log('my wife sending text');
155 | notify(callback, null, 'yes')
156 | };
157 |
158 | let working = keepDoing('typing',1000);
159 | let meReadingText = function(msg) {
160 | if(msg!='ok') clearInterval(work);
161 | console.log('I\'m reading text');
162 | }
163 |
164 | meSendingText((msg)=>{
165 | if(msg!='ok') clearInterval(work);
166 | else
167 | console.log('continue working');
168 | });
169 |
170 | #+END_SRC
171 |
172 |
173 | 其中 =notify= 负责往事件循环上放一个任务,当老婆读了短信,并 =notify= 我读回信之后,两秒后短信发到了我的手机上,手机(包含快来阅读短信句柄)的铃声通过我的耳朵传到我的脑回路中,触发我开始读短信。
174 |
175 | 使用事件循环回调的形式看起来还挺高效的,而且 JavaScript 编程中我们也一直也是这么用的。但是当异步调用多了之后,就会出现 /回调地狱/ (Callback Hell)的现象,为什么说是 *地狱* 呢, 可以想象一下前面例子中如果我有十个老婆,要向 五个老婆发短信申请加班,而且都同意后才能继续工作,该是如何实现呢?
176 | #+INDEX 回调地狱
177 | #+BEGIN_SRC js
178 | meSendingText(wife1Reading, (msg)=>{
179 | if(msg=='yes')
180 | metSendingText(wife2Reading, (msg)=>{
181 | if(msg=='yes')
182 | metSendingText(wife3Reading, (msg)=>{
183 | if(msg=='yes')
184 | metSendingText(wife4Reading, (msg)=>{
185 | if(msg=='yes')
186 | metSendingText(wife5Reading, (msg)=>{
187 | if(msg=='yes')
188 | console.log('continue working')
189 | })
190 | })
191 | })
192 | })
193 | })
194 |
195 | #+END_SRC
196 |
197 | 只要有一个异步函数要回调,那么所有依赖于这个异步函数结束的函数都得放到该函数的回调内。这是个比地狱还要深的回调地狱。
198 | 于是前段时间特别火的 Promise,似乎能够缓解一下回调地狱的窘境。但是,Promise 并不是专门用来消除回调地狱的,Promise 更有意义的应该是在于 Monadic 编程。对于回调地狱,Promise 能做的也只是把这些回调平铺开而已。
199 | #+INDEX: Monadic
200 | #+BEGIN_QUOTE
201 | 从乘坐手扶电梯下回调地狱,变成了乘坐直梯下回调地狱。
202 | #+END_QUOTE
203 |
204 | #+BEGIN_SRC js
205 | meSendingText(wife1Reading)
206 | .then(()=>meSendingText(wife2Reading))
207 | .then(()=>meSendingText(wife3Reading))
208 | .then(()=>meSendingText(wife4Reading))
209 | .then(()=>meSendingText(wife5Reading))
210 | #+END_SRC
211 |
212 | 当然,如果是使用 Monadic 编程方式来解决这种问题的话,其实也可以变得非常优雅而且函数式,读者可以尝试用 =when= 实现一下(请回到第七章,如果你忘了 =when= 是什么)。
213 |
214 | 但是本章,我要强调的是一种更有意思的异步编程方式 CSP。
215 |
216 | ** 通信顺序进程(CSP)
217 | 通信顺序进程(Communicating Sequential Processes), 是计算机科学中用于一种描述并发系统中交互的形式语言,简称 CSP,来源于C.A.R Hoare 1978年的论文。没错了,Hoare就是发明(让我们熟悉的大学算法课纠结得快要挂科的) 快排算法的那位计算机科学家了。
218 |
219 | CSP 由于最近 Go 语言的兴起突然复活,[[http://talks.golang.org/2012/concurrency.slide#1][Go]] 给自己的 CSP 实现起名叫 /goroutines and channels/ [fn:3],由于实在是太好用了,Clojure 也加入了
220 | CSP 的阵营,弄了一个包叫做 /core.async/ 。
221 |
222 | CSP 的概念非常简单, 想象一下事件循环,类似的:
223 |
224 | 1. CSP 把这个事件循环的消息队列转换成一个数据队列,并且把这个队列叫做 /channel/
225 | 2. 任务等待队列中的数据
226 |
227 | #+BEGIN_SRC ditaa :file images/csp.png :exports results
228 | +----+ +----+
229 | Process A ----+ +-+ +--------->
230 | +----+ +----+
231 | : put
232 | +-->+----+
233 | Channel ----------+data+------->
234 | +----+
235 | : take
236 | +=---+ +->+----+
237 | Process B ----+ +-----------+ +----->
238 | +----+ +----+
239 |
240 | #+END_SRC
241 |
242 | #+CAPTION: CSP 中的 Channel
243 | #+RESULTS:
244 | [[file:images/csp.png]]
245 |
246 |
247 | 这样就成功的把任务和异步数据成功从回调地狱中分离开来。还是刚才发短信的例子,我们来用 CSP 实现一遍:
248 |
249 | #+BEGIN_SRC clojure -r
250 | (def working (chan))
251 | (def texting (chan))
252 |
253 | (defn boss-yelling []
254 | (go-loop [no 1]
255 | (! working (str "bose say: work " no))
257 | (recur (+ no 1))))
258 |
259 | (defn wife-texting [] (ref:wife)
260 | (go-loop []
261 | (! texting "wife say: come home!")
263 | (recur)))
264 |
265 | (defn reading-text [] (ref:reading)
266 | (go-loop []
267 | (println (JS Bin
282 |
283 | - 可以看出 boss yelling,wife texting,me working 和 reading text 四个任务是 *并发* 进行的
284 | - 所有任务都相互没有依赖,之间完全没有 callback,没有哪个任务是另一个任务的 callback。 而且他们都只依赖于 =working= 和 =texting= 这两个channel
285 | - 其中的 =go-loop= 神奇的地方是,它循环获取channel中的数据,当队列空时,它的状态会变成 parking,并没有阻塞线程,而是保存当前状态,继续去试另一个 =go= 语句。
286 | - 拿 =work= 来说, =(! texting "wife say: come home!")= 是往 channel texting 中加数据,如果 channel 已满,则也切到 parking 状态。
288 |
289 | ** 使用 generator 实现 CSP [fn:2]
290 | 在看明白了 Clojure 是如何使用 channel 来解耦我的问题后,再回过头来看 JavaScript 如何实现类似的 CSP 编程呢?
291 |
292 | 先理一下我们都要实现些什么:
293 | - go block:当然是需要这样的个block,只有在这个 block 内我们可以自如的切换状态。
294 | - channel:用来存放消息
295 | - timeout:一个特殊的 channel,在规定时间内关闭
296 | - take (!):同样的,往 channe 中发消息,也会决定下一个状态是什么。
298 |
299 | 当然,首先要实现的当然是最重要的 go block,但是在这之前,让我们看看实现 go block 的前提 ES6 的一个的新标准—— /generator/ 。
300 |
301 | *** Generator
302 | [[http://blog.dev/javascript/essential-ecmascript6.html#sec-9][ES6 终于支持了Generator]],目前Firefox与Chrome都已经实现。[fn:1] Generator 在每次被调用时返回 =yield= 后边表达式的值,并保存状态,下次调用时继续运行。
303 |
304 | 这种功能听起来刚好符合上例中神奇的 parking 的行为,于是,我们可以试试用 generator 来实现刚刚 Clojure 的 CSP 版本。
305 |
306 | *** Go Block
307 | go block 其实就是一个状态机,generator 为状态机的输入,根据不同的输入使得状态机状态转移。所以实现 go block 其实就是:
308 | - 一个函数
309 | - 可以接受一个 [[(generator)][generator]]
310 | - 如果 generator 没有下一步,则结束
311 | - 如果该步的返回值状态为 park,[[(parking)][那么就是什么也不做, 过一会继续尝试新的输入]]
312 | - 如果为 continue,[[(continue)][就接着去 generator]] 取下一输入
313 | #+BEGIN_SRC javascript -r
314 | function go_(machine, step) {
315 | while(!step.done) {
316 | var arr = step.value(),
317 | state = arr[0],
318 | value = arr[1];
319 | switch (state) {
320 | case "park":
321 | setTimeout(function() { go_(machine, step); },0); (ref:parking)
322 | return;
323 | case "continue":
324 | step = machine.next(value); (ref:continue)
325 | break;
326 | }
327 | }
328 | }
329 |
330 | function go(machine) {
331 | var gen = machine(); (ref:generator)
332 | go_(gen, gen.next());
333 | }
334 | #+END_SRC
335 |
336 | *** timeout
337 | timeout 是一个类似于 thread sleep 的功能,想让任务能等待个一段时间再执行,
338 | 只需要在 =go_= 中加入一个 timeout 的 =case= 就好了。
339 | #+BEGIN_SRC javascript
340 | ...
341 | case 'timeout':
342 | setTimeout(function(){ go_(machine, machine.next());}, value);
343 | return;
344 | ...
345 | #+END_SRC
346 |
347 | 如果状态是 timeout,那么等待 =value= 那么长的时间再继续运行 generator。
348 |
349 | 另外,当然还需要一个返回 timeout channel 的函数:
350 | #+BEGIN_SRC javascript
351 | function timeout(interval){
352 | var chan = [interval];
353 | chan.name = 'timeout';
354 | return chan;
355 | }
356 | #+END_SRC
357 |
358 | 每次使用 timeout 都会生成一个新的 channel,但是 channel 内只有一个消息,就是 timeout 的 毫秒数。
359 |
360 | *** take !
382 | 当 generator 往 channel 中 put 消息
383 | - 如果 channel 空,则将消息放入,状态变为 continue
384 | - 如果 channel 非空,则进入 parking 状态
385 |
386 | #+BEGIN_SRC javascript
387 | function put(chan, val) {
388 | return function() {
389 | if(chan.length === 0) {
390 | chan.unshift(val);
391 | return ["continue", null];
392 | } else {
393 | return ["park", null];
394 | }
395 | };
396 | }
397 | #+END_SRC
398 |
399 | *** JavaScript CSP 版本的例子
400 | 有了 go block 这个状态机以及使他状态转移表之后,终于可以原原本本的将之前的 clojure 的例子翻译成 JavaScript 了。
401 | #+BEGIN_SRC javascript
402 | function boss_yelling(){
403 | go(function*(){
404 | for(var i=0;;i++){
405 | yield take(timeout(1000));
406 | yield put(work, "boss say: work "+i);
407 | }
408 | });
409 | }
410 |
411 | function wife_texting(){
412 | go(function*(){
413 | for(;;){
414 | yield take(timeout(4000));
415 | yield put(text, "wife say: come home");
416 | }
417 | });
418 | }
419 |
420 | function working(){
421 | go(function*(){
422 | for(;;){
423 | var task = yield take(work);
424 | console.log(task, "me working");
425 | }
426 | });
427 | }
428 |
429 | function texting(){
430 | go(function*(){
431 | for(;;){
432 | var read = yield take(text);
433 | console.log(read, "me ignoring");
434 | }
435 | });
436 | }
437 | boss_yelling();
438 | wife_texting();
439 | working();
440 | texting();
441 | #+END_SRC
442 |
443 | 是不是决定跟 Clojure 的例子非常相似呢?注意每一次 yield 都是操作 go block 这个状态机,因此就这个例子来说,我们可以跟踪一下它的状态转移过程,这样可能会对这个简单的 go block 状态机有更深得理解。
444 |
445 | 1. 首先看 =boss_yelling= 这个 go 状态机,当操作为 =take(timeout(1000))= 时,状态会切换到 =timeout= 这样状态机会停一个 1000 毫秒。
446 | 2. 其他的状态机会继续运行,接下来应该就到 =wife_texting= ,同样的这个状态机也会停 4000秒
447 | 3. 现在轮到 =working= ,但是 work channel 中并没有任何的消息,所以也进入 parking 状态。
448 | 4. 同样 =texting= 状态机也进入 parking 状态。
449 |
450 | 直到 1000 毫秒后, =boss_yelling= timeout
451 |
452 | 1. =bose_yelling= 状态机继续运行,往 work channel 中放了一条消息。
453 | 2. =working= 状态机得以继续运行,打印消息。
454 |
455 | 此时没有别的状态机的状态可以变化,又过了 1000 毫秒, =working= 还会继续打印,直到第 4000 毫秒, =wife_texting= timeout,状态机继续运行,往 text channel 添加了一条消息。这时状态机 =texting= 的状态才从 parking 切到 continue,开始打印消息。
456 |
457 | 以此类推,就会得到这样的结果:
458 | #+BEGIN_EXAMPLE
459 | "boss say: work 0"
460 | "me working"
461 | "boss say: work 1"
462 | "me working"
463 | "boss say: work 2"
464 | "me working"
465 | "boss say: work 3"
466 | "me working"
467 | "boss say: work 4"
468 | "me working"
469 | "wife say: come home"
470 | "me ignoring"
471 | "boss say: work 5"
472 | "me working"
473 | ...
474 | #+END_EXAMPLE
475 |
476 |
477 | ** 在前端实践中使用 CSP
478 |
479 | 之前的实验性的代码只是为了说明 CSP 的原理和实现思路之一,更切合实际的,我们可以通过一些库来使用到 Clojure 的 core.async。这里我简单的介绍一下我从 ClojureScript 的 core.async 移植过来的 conjs[fn:5]。
480 |
481 | *** 使用移植的 core.async
482 | 由于 go block 在 Clojure 中是用 macro 生成状态机来实现的,要移植过来困难不小,因此这里我只将 core.async 的 channel 移植了过来,但是是以接受回调函数的方式。
483 | #+BEGIN_SRC js
484 | const _ = require('con.js');
485 | const {async} = _;
486 | var c1 = async.chan()
487 | var c2 = async.chan()
488 |
489 | async.doAlts(function(v) {
490 | console.log(v.get(0)); // => c1
491 | console.log(_.equals(c1, v.get(1))) // => true
492 | },[c1,c2]);
493 |
494 | async.put$(c1, 'c1');
495 | async.put$(c2, 'c2');
496 | #+END_SRC
497 |
498 | 有意思的是,我顺带实现了 Promise 版本的 core.async,会比回调要稍微更方便一些。
499 |
500 | #+BEGIN_SRC js
501 | async.alts([c1,c2])
502 | .then((v) => {
503 | console.log(v.get(0)); // => c1
504 | console.log(_.equals(c1, v.get(1))) // => true
505 | })
506 | async.put(c1, 'c1').then(_=>{console.log('put c1 into c1')})
507 | async.put(c2, 'c2').then(_=>{console.log('put c2 into c2')})
508 | #+END_SRC
509 |
510 | 虽然把 channel 能移植过来,但是缺少 macro 原生支持的 JavaScript 似乎对 go block 也无能为力,除非能有 generator 的支持。
511 |
512 | *** 使用 ES7 中的异步函数
513 | 由于在实践中我们经常会使用到 babel 来将 ES6 规范的代码编译成 ES5 的代码。所以顺便可以将 ES7 的开关打开,这样我们就可以使用 ES7 规范中的一个新特性—— async 函数。 使用 async 函数实现我们之前的例子估计代码并不会有大的变化,让我们使用 async 函数和 channel 实现一下 go 经典的乒乓球小例子。
514 |
515 | #+BEGIN_SRC js -n -r
516 | let _ = require('con.js');
517 | let {async} = _;
518 |
519 | async function player(name, table) {
520 | while (true) {
521 | var ball = await table.take(); (ref:take)
522 | ball.hits += 1;
523 | console.log(name + " " + ball.hits);
524 | await async.timeout(100).take();
525 | table.put(ball);
526 | }
527 | }
528 |
529 | (async function () {
530 | var table = async.chan();
531 |
532 | player("ping", table);
533 | player("pong", table);
534 |
535 | await table.put({hits: 0});
536 | await async.timeout(1000).take();
537 | table.close();
538 | })();
539 | #+END_SRC
540 | 当把球 ={hist:0}= 放到 =table= channel 上的时候,阻塞在第[[(take)][(take)]]行 =take= 的 player ping 会先接到球,player ping 击完球 100ms 之后,球又回到了 =table= channel。之后 player pong 之间来回击球知道 table 在 1000ms 后被关闭。
541 |
542 | 所以我们运行代码后看到的间断性的 100ms 的打印出:
543 | #+BEGIN_EXAMPLE
544 | pong 1
545 | ping 2
546 | pong 3
547 | ping 4
548 | pong 5
549 | ping 6
550 | pong 7
551 | ping 8
552 | pong 9
553 | ping 10
554 | pong 11
555 | ping 12
556 | #+END_EXAMPLE
557 |
558 | 通过 async/await,结合 conjs 的 channel, 真正让我们写出了 Clojure core.async 风格的代码。利用 CSP 异步编程的方式,我们可以用同步的思路,去编写实际运行时异步的代码。这样做不仅让我们的代码更好推理,更符合简单的命令式思维方式,也更容易 debug 和做异常处理。
559 |
560 |
561 | * Footnotes
562 |
563 | [fn:5] 源代码在 http://github.com/jcouyang/conjs,可以简单的通过 =npm install con.js= 安装。
564 |
565 | [fn:4] 这里的多线程是相对用户而言,也就是这个例子中的我,而用手机发短信(这种 I/O 操作)是如何给基站发送消息的,占用了多少线程来做我们并不关心。
566 |
567 | [fn:3] /goroutine/ 名字取自 /coroutine/ (协程),由于是 go 的实现,所以叫 goroutine 了。
568 |
569 | [fn:1] Chrome有一个 feature toggle 可以打开部分 es6 功能. 打开 =chrome://flags/#enable-javascript-harmony= 设置为 =true=
570 |
571 | [fn:2] 里面的go的实现来自 http://swannodette.github.io/2013/08/24/es6-generators-and-csp/
572 |
--------------------------------------------------------------------------------
/emacs.el:
--------------------------------------------------------------------------------
1 | (require 'color-theme)
2 | (color-theme-initialize)
3 | (color-theme-gtk-ide)
4 | (require 'clojure-mode)
5 | (clojure-font-lock-setup)
6 | (require 'org)
7 | (require 'ox-latex)
8 | (require 'htmlize)
9 | (setq make-backup-files nil)
10 | (setq book-path (expand-file-name "book"))
11 | (setq org-html-validation-link nil)
12 | (setq org-confirm-babel-evaluate nil)
13 | (add-to-list 'org-latex-classes
14 | '("tufte" "\\documentclass[11pt,twoside,openright,a5paper]{tufte-book}"
15 | ("\\chapter{%s}" . "\\chapter*{%s}")
16 | ("\\section{%s}" . "\\section*{%s}")
17 | ("\\subsection{%s}" . "\\subsection*{%s}")
18 | ))
19 | (setq tex-compile-commands '(("xelatex %r")))
20 | (setq tex-command "xelatex")
21 | (setq-default TeX-engine 'xelatex)
22 |
23 | (setq org-latex-pdf-process
24 | '("xelatex -interaction nonstopmode -output-directory %o %f"
25 | "xelatex -interaction nonstopmode -output-directory %o %f"
26 | "xelatex -interaction nonstopmode -output-directory %o %f"))
27 |
28 | (setq locate-command "mdfind")
29 | (setenv "PATH" (concat (getenv "PATH") ":/usr/local/share/npm/bin:/usr/local/bin:/usr/texbin"))
30 | (setq exec-path (append exec-path '("/usr/local/bin" "/usr/texbin")))
31 | (custom-set-variables
32 | '(org-publish-timestamp-directory
33 | (convert-standard-filename "public/.org-timestamps/")))
34 | (setq postamble (with-temp-buffer
35 | (insert-file-contents "html/postamble.html")
36 | (buffer-string)))
37 | (setq header (with-temp-buffer
38 | (insert-file-contents "html/header.html")
39 | (buffer-string)))
40 | (defun set-org-publish-project-alist ()
41 | "Set publishing projects for Orgweb and Worg."
42 | (interactive)
43 | (setq org-publish-project-alist
44 | `(("html"
45 | ;; Directory for source files in org format
46 | :base-directory ,book-path
47 | :base-extension "org"
48 | :html-doctype "html5"
49 | :html-head ,header
50 | :html-html5-fancy t
51 | :html-postamble ,postamble
52 | ;; HTML directory
53 | :publishing-directory "public"
54 | :publishing-function org-html-publish-to-html
55 | :recursive t
56 | :headline-levels 5
57 | ;; :with-sub-superscript nil
58 | :section-numbers 3
59 | :makeindex t
60 | :exclude ".*"
61 | :include ("index.org" "zh/index.org")
62 | :html-head-include-default-style nil
63 | )
64 | ("pdf"
65 | ;; Directory for source files in org format
66 | :base-directory ,book-path
67 | :base-extension "org"
68 | :publishing-directory "public/pdf"
69 | :publishing-function org-latex-publish-to-pdf
70 | :headline-levels 5
71 | ;; :with-sub-superscript nil
72 | :section-numbers 3
73 | :makeindex t
74 | :exclude ".*"
75 | :include ("index.org" "zh/index.org")
76 | )
77 |
78 | ;; where static files (images, pdfs) are stored
79 | ("blog-static"
80 | :base-directory ,book-path
81 | :base-extension "css\\|js\\|png\\|jpg\\|gif\\|mp3\\|ogg\\|swf\\|woff2\\|woff"
82 | :publishing-directory "public"
83 | :recursive t
84 | :publishing-function org-publish-attachment
85 | )
86 | ("blog" :components ("book-notes" "book-static"))
87 | )))
88 | (set-org-publish-project-alist)
89 |
--------------------------------------------------------------------------------
/html/header.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
--------------------------------------------------------------------------------
/html/postamble.html:
--------------------------------------------------------------------------------
1 | Author: %a Follow @jcouyang
2 |
3 | Modified: %C
4 | Generated by: %c
5 | <Publish> with _(:з」∠)_ by OrgPress
6 | 
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.
7 |
8 |
9 |
10 |
11 |
19 |
20 |
30 |
31 |
32 |
33 |
46 |
47 |
--------------------------------------------------------------------------------