请分享您的想法,参加2024 Cloujure 状态调查!

欢迎!请查看 关于 页面以了解更多关于其工作方式的详细信息。

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" DOM 节点(链接:1),它从底部开始动画,并在 3 秒后移除。一个真正的实现将需要处理点击等操作,但该示例足够简单地描述问题。{{(anim/...)}} 函数设置了适当的 CSS,并返回一个 {{(async/timeout time)}} 通道,使我们能够等待 "完成"。

问题在于 X,go 被分发,并将控制权交回浏览器。因此,动画的 "设置" 部分被延迟,浏览器以原始状态渲染 toast 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 的问题。(我不认为你可以在库代码中安全地依赖实现命名空间 :-))

0
参考资料: https://clojure.atlassian.net/browse/ASYNC-131(由thheller报告)
...