January 23, 2020
By: Kevin
core.async在前端的应用场景
var link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
link.setAttribute('href', '/blog/img/css/main.css');
document.getElementsByTagName('head')[0].appendChild(link);
本文会分为两部分, 第一部分是理论分析和代码, 第二部分是一些例子
前端处理并发编程的方法
js在浏览器运行(的主要模式)是单线程的, 前端编程的主要模式是响应用户事件(点击, 拖动, 定时), 这两点决定了异步处理是编程的主要模式.
最早的方式只能是处理事件. 在各处事件上挂handler, 这样的问题是逻辑会很碎片化, 看不到完整的逻辑流.
挂在handler上要处理回调(callback), 回调也会派发新的事件, 这些事件也会有回调, 导致了无穷无尽的零散在各处的多级嵌套的回调(callback hell).
JS在ES6引入的Promise一定程度上解决了这些问题, 后来的await/async是promise的语法糖, 能写出更加简洁的代码.
cljs也引入了的core.async, 通过CSP(Communicating Sequential Processes)的方式来解决复杂逻辑组织的问题.
js是一门快速演进的语言, Promise.All, async/await 给编程带来了很大的便利
历史
cljs release ES6Promises Asnyc iterators for-wait-of
^ ^ ^
| | |
| | |
+------+------+------------+-------+----+------------+------+-----+
| | | | | |
| | | | | |
| 2011 | 2013 | 2015 | 2017 | 2018 |
| | | | | |
| | | | | |
+-------------+-----+------+------------+-----+------+------------+
| |
| |
v v
core.async ES2017
await/async
Promise和core.asyc的比较(场景和用法)
TODO
使用core.async做状态处理
注意: 每个例子需要在自己的区域单独执行一下
引入依赖
(ns show.core.async
(:require [cljs.core.async :as async
:refer [>! <! put! chan alts! take! pipeline go go-loop]]
[goog.events :as events]
[goog.dom.classes :as classes])
(:import [goog.events EventType]))
工具函数
(defn container []
"klipse容器"
[]
js/klipse-container)
(defn events->chan
"监听dom event 并且把event放到 channel中"
([el event-type] (events->chan el event-type (chan)))
([el event-type c]
(events/listen el event-type
(fn [e] (put! c e)))
c))
(defn mouse-loc->vec
"鼠标事件->vector: [x, y]"
[e]
[(.-clientX e) (.-clientY e)])
(defn show!
"创建一个新的<p>标签,增加到当前klipse容器,并且显示msg内容"
[msg]
(let [el (container)
p (.createElement js/document "p")]
(set! (.-innerHTML p) msg)
(.appendChild el p)))
(defn add-btn!
"在当前container添加一个新button, 并且以Name填充btn的内容显示"
[name]
(let [el (container)
btn (.createElement js/document "button")]
(set! (.-innerHTML btn) name)
(.appendChild el btn)))
(defn remove-all-children!
"删除container中的所子元素"
[]
(set! (.-innerHTML (container)) ""))
(defn create-p!
"创建并重复使用container中唯一的<p>组件"
[msg]
(let [el (container)
p (or
(.querySelector el "#my-p")
(doto (.createElement js/document "H1")
(.setAttribute "id" "my-p")
(#(.appendChild el %))))]
(set! (.-innerHTML p) msg)))
一次按钮完成
本按钮只接受一次点击.
(defn ex1 []
(let [btn (add-btn! "点我一下")
clicks (events->chan btn EventType.CLICK)]
(go
(show! "等着被点 ...")
(<! clicks)
(show! "你终于点我了!"))))
(remove-all-children!)
(ex1)
按钮点击两次完成
只接受两次点击.
(remove-all-children!)
(defn ex2 []
(let [btn (add-btn! "点我一下")
clicks (events->chan btn EventType.CLICK)]
(go
(show! "等着被点 ...")
(<! clicks)
(show! "你点了我一下!")
(show! "等着你再点我 ...")
(<! clicks)
(show! "两次了哟...!"))))
(remove-all-children!)
(ex2)
必须A和B两个按钮都点击
(defn ex3 []
(let [clicks-a (events->chan (add-btn! "按钮A") EventType.CLICK)
clicks-b (events->chan (add-btn! "按钮B") EventType.CLICK)]
(go
(show! "等待来自 A 的点击...")
(<! clicks-a)
(show! "A点了一下!")
(show! "等待来自 B 的点击...")
(<! clicks-b)
(show! "圆满了!"))))
(remove-all-children!)
(ex3)
被阻塞, 永远无法完成的点击
对于unbuffered channel, 放和取都会造成阻塞.
(defn ex4 []
(let [clicks (events->chan (add-btn! "按钮A") EventType.CLICK)
c0 (chan)]
(go
(show! "等待点击.")
(<! clicks)
(show! "阻塞于c0, 拿到值之前无法进行")
(>! c0 (js/Date.))
(show! "你永远走不到这里")
(<! c0))))
(remove-all-children!)
(ex4)
从channel中及时取出, 解除阻塞
(defn ex5 []
(let [clicks (events->chan (add-btn! "点我吧") EventType.CLICK)
c0 (chan)]
(go
(show! "等待被点")
(<! clicks)
(show! "把事件放到 c0, 被取走之前会阻塞")
(>! c0 (js/Date.))
(show! "成功从 c0 拿走了值!"))
(go
(let [v (<! c0)]
(show! (str "从 c0拿到的值: " v))))))
(remove-all-children!)
(ex5)
持续记录鼠标位置
(defn ex6 []
(let [button (add-btn! "鼠标位置")
clicks (events->chan button EventType.CLICK)
mouse (events->chan js/window EventType.MOUSEMOVE
(chan 1 (map mouse-loc->vec)))]
(go
(show! "点击按钮,记录鼠标位置!")
(<! clicks)
(set! (.-innerHTML button) "停止记录!")
(loop []
(let [[v c] (alts! [mouse clicks])]
(cond
(= c clicks) (show! "至此结束!")
:else
(do
(show! (pr-str v))
(recur))))))))
(remove-all-children!)
(ex6)
过滤性记录鼠标位置
(defn ex7 []
(let [button (add-btn! "鼠标位置")
clicks (events->chan button EventType.CLICK)
mouse (events->chan js/window EventType.MOUSEMOVE
(chan 1 (comp (map mouse-loc->vec)
(filter (fn [[_ y]] (zero? (mod y 5)))))))]
(go
(show! "点击按钮,记录鼠标位置!")
(<! clicks)
(set! (.-innerHTML button) "停止记录!")
(loop []
(let [[v c] (alts! [mouse clicks])]
(cond
(= c clicks) (show! "至此结束!")
:else
(do
(show! (pr-str v))
(recur))))))))
(remove-all-children!)
(ex7)
最多点击10次的按钮
(defn ex8 []
(remove-all-children!)
(let [clicks (events->chan (add-btn! "点击按钮") EventType.CLICK)]
(go
(show! "最多点击10次!")
(<! clicks)
(loop [i 1]
(show! (str i " 次!"))
(if (> i 9)
(show! "完成!")
(do
(<! clicks)
(recur (inc i))))))))
(remove-all-children!)
(ex8)
状态相关的两个按钮
(defn ex9 []
(let [prev-button (add-btn! "上一个动物")
next-button (add-btn! "下一个动物")
prev (events->chan prev-button EventType.CLICK)
next (events->chan next-button EventType.CLICK)
animals [:🐶 :🐛 :🐱 :🐻 :🦉 :🦅
:🦢 :🦏 :🐦 :🐟 :🐒]
max-idx (dec (count animals))]
(go
(loop [idx 0]
(if (zero? idx)
(classes/add prev-button "disabled")
(classes/remove prev-button "disabled"))
(if (== idx max-idx)
(classes/add next-button "disabled")
(classes/remove next-button "disabled"))
(create-p! (name (nth animals idx)))
(let [[v c] (alts! [prev next])]
(condp = c
prev (if (pos? idx)
(recur (dec idx))
(recur idx))
next (if (< idx max-idx)
(recur (inc idx))
(recur idx))))))))
(remove-all-children!)
(ex9)
状态相关的三个按钮
(defn style-buttons!
"按钮的样式控制, 第0个元素'上一个'按钮禁用,
最后一个元素, '下一个'按钮禁用"
[i max prev next]
(if (zero? i)
(classes/add prev "disabled")
(classes/remove prev "disabled"))
(if (== i max)
(classes/add next "disabled")
(classes/remove next "disabled")))
(defn disable-buttons!
"禁用所有button, 并且把第一个button的内容修改为'已经结束'"
[[start-stop-button :as buttons]]
(set! (.-innerHTML start-stop-button) "已经结束")
(doseq [button buttons]
(classes/add button "disabled")))
(defn keys-chan
"channel带一个transducer: 过滤左箭头(37) 和 右箭头(39) 按键,
并且映射为:previous和:next"
[]
(events->chan js/window EventType.KEYDOWN
(chan 1 (comp (map #(.-keyCode %))
(filter #{37 39})
(map {37 :previous 39 :next})))))
(defn ex10 [animals]
(let [start-stop-button (add-btn! "开始")
prev-button (add-btn! "前一个")
next-button (add-btn! "后一个")
start-stop (events->chan start-stop-button EventType.CLICK)
prev (events->chan prev-button EventType.CLICK
(chan 1 (map (constantly :previous))))
next (events->chan next-button EventType.CLICK
(chan 1 (map (constantly :next))))
max-idx (dec (count animals))]
(go
;; 开始
(<! start-stop)
;; 监听消息
(let [keys (keys-chan)
actions (async/merge [prev next keys])]
(set! (.-innerHTML start-stop-button) "停止!")
(loop [idx 0]
(style-buttons! idx max-idx prev-button next-button)
(create-p! (name (nth animals idx)))
;; 等候下一个event
(let [[action c] (alts! [actions start-stop])]
(if (= c start-stop)
(do
(events/removeAll js/window EventType.KEYDOWN)
(disable-buttons! [start-stop-button prev-button next-button])
(create-p! ""))
(condp = action
:previous (if (pos? idx)
(recur (dec idx))
(recur idx))
:next (if (< idx max-idx)
(recur (inc idx))
(recur idx))
(recur idx)))))))))
(remove-all-children!)
(ex10 [:🐶 :🐛 :🐱 :🐻 :🦉 :🦅
:🦢 :🦏 :🐦 :🐟 :🐒])
go语言的成名作
进行中, 尚未完成
https://talks.golang.org/2012/concurrency.slide#50 go vs clojure
c := make(chan Result)
go func() { c <- First(query, Web1, Web2) } ()
go func() { c <- First(query, Image1, Image2) } ()
go func() { c <- First(query, Video1, Video2) } ()
timeout := time.After(80 * time.Millisecond)
for i := 0; i < 3; i++ {
select {
case result := <-c:
results = append(results, result)
case <-timeout:
fmt.Println("timed out")
return
}
return
(defn search [query]
(let [c (chan )
t (timeout 80)]
(go (>! c (<! (fastest query web1 web2))))
(go (>! c (<! (fastest query image1 image2))))
(go (>! c (<! (fastest query videos1 videos2))))
(go (loop [i 0
ret []]
(if (= 3 i)
ret
(recur (inc i)
(conj ret (alt! [c t] ([v] v)))))))))