如何协调渲染与端口交互 (Elm 0.17)

How to coordinate rendering with port interactions (Elm 0.17)

我想将 Elm 与 Javascript 库集成,这样 Elm 就会动态创建 "cells" (html div),而 Javascript 将是提供他们的 id-s 并使用它们来执行自定义操作。我想要的序列是

  1. Elm 创建一个单元格(并分配 id)
  2. 带有id的消息发送到端口
  3. Javascript 接收消息并执行其操作

我一开始就是这样实现的(full source):

port onCellAdded : CellID -> Cmd msg

update : Msg -> Model -> (Model, Cmd Msg)
update message ({cells} as model) =
  case message of

    Push ->
      let
        uid = List.length cells
      in
      ({ model
        | cells = [uid] ++ cells
      }, onCellAdded uid)

问题是 Javascript 在另一边

var container = document.getElementById('app');
var demoApp = Elm.RenderDemo.embed(container);

demoApp.ports.onCellAdded.subscribe(function(cellID) {
   if(document.getElementById('cell:' + cellID) === null) { window.alert("Cannot find cell " + cellID) }    
});

抱怨找不到这样的id。显然视图还没有被渲染。

所以我在Elm应用中又添加了一个状态(OnCellAdded),希望流程是这样的:

  1. Elm 创建一个单元格(在推送时)并请求 (Task.perform) 一个异步任务 OnCellAdded
  2. 此处呈现视图
  3. OnCellAdded 被调用并且带有 id 的消息被发送到端口
  4. Javascript 收到消息并执行其操作

实现看起来像这样 (diff) (full source):

update message ({cells} as model) =
  case message of

    Push ->
      let
        uid = List.length cells
      in
      ({ model
        | cells = [uid] ++ cells
      }, msgToCmd (OnCellAdded uid))

    OnCellAdded counter ->
      (model, onCellAdded counter)

msgToCmd : msg -> Cmd msg
msgToCmd msg =
      Task.perform identity identity (Task.succeed msg)

但仍然 OnCellAddedPush 之后立即处理,中间没有渲染模型。

我最后一次尝试使用 Update.andThen (diff) (full source)

Push ->
  let
    uid = List.length cells
  in
  ({ model
    | cells = [uid] ++ cells
  }, Cmd.none)
  |> Update.andThen update (OnCellAdded uid)

还是不行。我需要一些帮助。

0.17.1 开始,还没有很好的方法来实现。

我推荐的最简单的方法是使用 setTimeout to wait at least 60ms or wait until the next requestAnimationFrame

考虑这个例子:

demoApp.ports.onCellAdded.subscribe(function(cellID) {
   setTimeout(function() {
      if(document.getElementById('cell:' + cellID) === null) {
         window.alert("Cannot find cell " + cellID)
      }
   }, 60);
});

有一个添加挂钩的功能请求 #19,因此可以知道 HTML 节点何时在 DOM。

您可以看到进度 here,很可能会出现在即将发布的版本中。

我昨天想做类似的事情,将 MorrisJS 集成到 Elm 生成的 div

最终我发现 Arrive JS which uses the new MutationObserver 在大多数现代浏览器中可用以观看 DOM 的变化。

所以在我的例子中,代码看起来像这样(简化):

$(document).ready(() => {
  $(document).arrive('.morris-chart', function () {
    Morris.Bar({
      element: this,
      data: [
        { y: '2006', a: 100, b: 90 },
        { y: '2007', a: 75, b: 65 },
        { y: '2008', a: 50, b: 40 },
        { y: '2009', a: 75, b: 65 },
        { y: '2010', a: 50, b: 40 },
        { y: '2011', a: 75, b: 65 },
        { y: '2012', a: 100, b: 90 }
      ],
      xkey: 'y',
      ykeys: ['a', 'b'],
      labels: ['Series A', 'Series B']
    })
  })
})

这会监视 dom 是否有任何带有 .morris-chart class 的新元素,一旦发现它就会使用该新元素创建图表。

所以只有在 Elm 运行 view 函数然后重新生成 DOM.

之后才会调用它

也许这样的东西可以满足您的需求。

使用 requestAnimationFrame() 的实现

根据我的经验,这似乎是目前最干净的解决方案。

var container = document.getElementById('app');
var demoApp = Elm.RenderDemo.embed(container);
var requestAnimationFrame = 
       window.requestAnimationFrame ||
       window.mozRequestAnimationFrame || 
       window.webkitRequestAnimationFrame || 
       window.msRequestAnimationFrame;   //Cross browser support

var myPerfectlyTimedFunc = function(cellID) {
   requestAnimationFrame(function() { 
       if(document.getElementById('cell:' + cellID) === null) { 
          window.alert("Cannot find cell " + cellID) 
       }
   })
}

demoApp.ports.onCellAdded.subscribe(myPerfectlyTimedFunc);

See here for an SPA type setup with multiple pages and the need to re-render a JS interop'd graph。还可以更新图表中的数据值。 (控制台日志消息也可能具有指导意义。)

如果有人好奇这是如何在 Elm 端而不是 html/js 端实现的,请参阅 Elm Defer Command 库。

如您所述,问题是:

  1. Javascript 已加载并查找尚未创建的元素。
  2. Elm 在本次搜索后渲染 DOM,你需要的元素出现了。
  3. 您通过端口发送的任何 Elm 命令也会在渲染时或渲染之前发生,因此端口订阅调用的任何 javascript 都会遇到同样的问题。

Elm 使用 requestAnimationFrame (rAF) 本身作为排队 DOM 渲染的方式,这是有充分理由的。假设 Elm 在不到 1/60 秒的时间内进行了几次 DOM 操作,而不是在 dividually 中呈现每个操作 - 这将是非常低效的 - Elm 会将它们传递给浏览器的 rAF,这将作为整体 DOM 渲染的 buffer/queue。换句话说,viewupdate 之后的动画帧上被调用,因此 view 调用不会总是在每个 update.

之后发生

过去人们会使用:

setInterval(someAnimationFunc, 16.6) //16.6ms for 60fps

requestAnimationFrame 是浏览器保持队列的一种方式,它管理队列并以 60fps 的速度循环。这提供了一些改进:

  • 浏览器可以优化渲染,所以动画会更流畅
  • 非活动选项卡中的动画将停止,让 CPU 冷静下来
  • 更省电

关于 rAF 的更多信息 here, and here, and a video by Google here

我的个人故事开始于我试图将 Chartist.js 图形渲染成最初在 Elm 中创建的 div。我还希望有多个页面(SPA 样式),并且在各种页面更改时重新创建 div 元素时需要重新呈现图表。

我在索引中直接将 div 写成了 HTML,但这阻止了我想要的 SPA 功能。我还使用 JQuery ala $(window).load(tellElmToReRender) 的端口和订阅,并试了一下 Arrive.js - 但每一个都导致了各种错误和缺乏所需的功能。我把 rAF 搞砸了一点,但在错误的地方和错误的方式使用它。听了 ElmTown - Episode 4 - JS Interop 之后,我顿悟并意识到它应该如何真正使用。