January 23, 2020
By: Kevin

core.async在前端的应用场景

  1. 前端处理并发编程的方法
    1. 历史
    2. Promise和core.asyc的比较(场景和用法)
  2. 使用core.async做状态处理
    1. 引入依赖
    2. 工具函数
    3. 一次按钮完成
    4. 按钮点击两次完成
    5. 必须A和B两个按钮都点击
    6. 被阻塞, 永远无法完成的点击
    7. 从channel中及时取出, 解除阻塞
    8. 持续记录鼠标位置
    9. 过滤性记录鼠标位置
    10. 最多点击10次的按钮
    11. 状态相关的两个按钮
    12. 状态相关的三个按钮
    13. go语言的成名作

本文会分为两部分, 第一部分是理论分析和代码, 第二部分是一些例子

前端处理并发编程的方法

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)))))))))
Tags: core.async clojurescript