2024 Clojure 状态调查!中分享您的想法。

欢迎!请参阅关于页面了解如何操作的更多详细信息。

0
core.async

我希望看到 "go" 宏的一个变体,该变体在首次可挂起操作之前立即执行,而不是调度(实际上是调用 "setTimeout" 两次)。这主要是针对 CLJS,因为 CLJ 可以通过阻塞操作来完成工作,但它也可能在那里很有用。

我的使用场景

我目前使用 core.async 来编排 CSS 动画,虽然它与 "go" 一起工作完美无缺,但它确实需要小心关注操作顺序,因为 "go" 将把控制权交回浏览器,这时虽然动画尚未正常开始,但浏览器可能会决定重新布局/重绘屏幕。

示例

`
(defn show-toast [message]
(let [toast (create-toast-dom message)]

(dom/append js/document.body toast)
;; X - PROBLEM: REPAINT HAPPENS NOW
(go (<! (anim/slide-in-from-bottom animation-time toast))
    (<! (timeout 3000))
    (<! (anim/fade-out animation-time toast))
    (dom/remove toast)
    )))

`

此示例尝试创建一个 "Toast"(链接:1)DOM 节点,从底部动画进入并在 3 秒后移除。真正的实现需要处理点击等操作,但示例足够简单,可以描述问题。{{(anim/...)}} 函数设置合适的 CSS 并返回一个 {{(async/timeout time)}} 通道,使我们能够等待 "完成"。

在 X 点的问题在于 go 已被调度并交还了控制权。因此,动画的 "设置" 部分被推迟,吐司 DOM 节点按原样渲染。通常这意味着浏览器将执行重绘。这会产生 "闪烁" 效果,节点出现后再次消失并动画化(取决于默认 CSS)。

简单的解决方案是将 {{dom/append}} 调用移入 go 块中,在该示例中这很容易做到,但更复杂的情况中则较困难。另一种解决方案是在 go 之外开始动画。

`
(defn show-toast [message]
(let [toast (create-toast-dom message)]

(dom/append js/document.body toast)
(let [slide-in (anim/slide-in-from-bottom animation-time toast)]
  ;; REPAINT, but slide-in animation already started so no "flicker"
  (go (<! slide-in)
      (<! (timeout 3000))
      (<! (anim/fade-out animation-time toast))
      (dom/remove toast)
      ))))

`

在这里这也足够简单,但对于更复杂的事情来说更困难。

我更希望看到一个 "go!" 变体,它不是调度 go 块,而是直接开始执行它。

`
(defn show-toast [message]
(let [toast (create-toast-dom message)]

(dom/append js/document.body toast)
(go! (<! (anim/slide-in-from-bottom animation-time toast)) ;; REPAINT ON <!
     (<! (timeout 3000))
     (<! (anim/fade-out animation-time toast))
     (dom/remove toast)
     )))  

`

微妙的不同之处在于,在我们准备好之前,浏览器没有机会做任何事情,而且无论如何它都会等待。在我自己调整了一些动画效果之后,我总是需要将一些东西从 go 块中移动出来或移动进去,正如目前所做的那样。

我知道这是一个相当具体的用例,并且 core.async 可能根本不是完成这项工作的最佳方式,但到目前为止,我在它上面取得了巨大的成功,代码也非常容易推理。

我尚未深入研究 core.async 的内部结构,但 go! 应该是一个相当简单的添加。如果这个变更被接受,我将开始深入研究并创建补丁,只是想在这样做之前确认没有任何反对意见。

希望我的意图是清晰的。CSS 动画可能有些难以理解,这可能不会使问题实际所在的位置变得明显。

(链接:1) http://www.google.com/design/spec/components/snackbars-toasts.html#snackbars-toasts-specs

3 个答案

0
评论者:thheller

我调查了这个问题,以了解在core.async方面需要做什么来促进这个变更,结果什么也不需要。


(defmacro go!
  "与 go 类似,立即执行直到第一个 put/take"
  [& body]
  `(let [c# (cljs.core.async/chan 1)
         f# ~(ioc/state-machine body 1 &env ioc/async-custom-terminators)
         state# (-> (f#)
                    (ioc/aset-all! cljs.core.async.impl.ioc-helpers/USER-START-IDX c#))]
     (cljs.core.async.impl.ioc-helpers/run-state-machine state#)
     c#))


到目前为止运作完美,且可以很好地在库中使用,我开始认为这可能是 CLJS 的更好的“默认”选项,因为没有“线程”可以启动执行“go”。

使用正常的“go”时,执行流程是

go 之前的代码
setTimeout/nextTick
go 内部的代码直到第一次 take/put
setTimeout/nextTick

而 go! 的执行流程是

go! 之前的代码
go! 内部的代码直到第一次 take/put
setTimeout/nextTick

由于 JavaScript 的非阻塞性质,额外的 setTimeout 不会造成伤害,也没有提供很多好处。

由于无需修改 core.async,我可以在我的库中简单地保留这个宏。我的用例相当特定,如果没有人对此“通用”感兴趣,请随意关闭此问题。
0

评论者:lgs32a

作为默认值,这将违反 go 的异步执行保证,这将是破坏性的变更。从我用户的视角来看,以下是一些观察:为了可读性,我宁愿选择你提出的两个 go 使用 go-now 的例子,因为 go 传达了其整个体部的异步执行,而 go-now 中我必须在体中扫描可暂停操作以确定同步执行的哪部分。我很难说服自己真有如此复杂的问题,使得 go-now 优于重构 go。 (我认为你不可以在库代码中安全地依赖 impl 命名空间 :-))

0
...