如何协调渲染与端口交互 (Elm 0.17)
How to coordinate rendering with port interactions (Elm 0.17)
我想将 Elm 与 Javascript 库集成,这样 Elm 就会动态创建 "cells" (html div),而 Javascript 将是提供他们的 id-s 并使用它们来执行自定义操作。我想要的序列是
- Elm 创建一个单元格(并分配 id)
- 带有id的消息发送到端口
- 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
),希望流程是这样的:
- Elm 创建一个单元格(在推送时)并请求 (
Task.perform
) 一个异步任务 OnCellAdded
- 此处呈现视图
OnCellAdded
被调用并且带有 id 的消息被发送到端口
- 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)
但仍然 OnCellAdded
在 Push
之后立即处理,中间没有渲染模型。
我最后一次尝试使用 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 库。
如您所述,问题是:
- Javascript 已加载并查找尚未创建的元素。
- Elm 在本次搜索后渲染 DOM,你需要的元素出现了。
- 您通过端口发送的任何 Elm 命令也会在渲染时或渲染之前发生,因此端口订阅调用的任何 javascript 都会遇到同样的问题。
Elm 使用 requestAnimationFrame (rAF) 本身作为排队 DOM 渲染的方式,这是有充分理由的。假设 Elm 在不到 1/60 秒的时间内进行了几次 DOM 操作,而不是在 dividually 中呈现每个操作 - 这将是非常低效的 - Elm 会将它们传递给浏览器的 rAF,这将作为整体 DOM 渲染的 buffer/queue。换句话说,view
在 update
之后的动画帧上被调用,因此 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 之后,我顿悟并意识到它应该如何真正使用。
我想将 Elm 与 Javascript 库集成,这样 Elm 就会动态创建 "cells" (html div),而 Javascript 将是提供他们的 id-s 并使用它们来执行自定义操作。我想要的序列是
- Elm 创建一个单元格(并分配 id)
- 带有id的消息发送到端口
- 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
),希望流程是这样的:
- Elm 创建一个单元格(在推送时)并请求 (
Task.perform
) 一个异步任务OnCellAdded
- 此处呈现视图
OnCellAdded
被调用并且带有 id 的消息被发送到端口- 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)
但仍然 OnCellAdded
在 Push
之后立即处理,中间没有渲染模型。
我最后一次尝试使用 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 库。
如您所述,问题是:
- Javascript 已加载并查找尚未创建的元素。
- Elm 在本次搜索后渲染 DOM,你需要的元素出现了。
- 您通过端口发送的任何 Elm 命令也会在渲染时或渲染之前发生,因此端口订阅调用的任何 javascript 都会遇到同样的问题。
Elm 使用 requestAnimationFrame (rAF) 本身作为排队 DOM 渲染的方式,这是有充分理由的。假设 Elm 在不到 1/60 秒的时间内进行了几次 DOM 操作,而不是在 dividually 中呈现每个操作 - 这将是非常低效的 - Elm 会将它们传递给浏览器的 rAF,这将作为整体 DOM 渲染的 buffer/queue。换句话说,view
在 update
之后的动画帧上被调用,因此 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 之后,我顿悟并意识到它应该如何真正使用。