7.ppapi事件锁机制


7.1.事件过程剖析

  所谓 “事件” 是指 OnEvent 事件, 大部分情况下, 您不需要关注事件的内部运转机制, 但如果是涉及到事件上下文有顺序关联的逻辑编程, 您必须要对其有所了解, 并掌握 ppapi 的特殊性的解决方案。
  事件上下文有顺序关联的, 典型有这几种情形:
A) MenuBeforePopup 事件, 在该事件中, 你书写 js 加入自定义菜单, 然后插件弹出菜单;
B) 报表的 Toolbar 事件, 点击工具条的大部分按钮, 都将触发此事件, 允许您在 OnEvent 中写一些 js, 然后插件继续往下执行;
C) 报表的 BeforeRowDeleted 事件, 在弹出菜单中删除行, 会触发此事件, 然后插件继续执行删除的动作, 并最终触发 RowDeleted 事件。 其实 "Before..."开头的事件都一样, 报表中有大量类似的前后关联事件.
  在IE、360、Chrome 早期的 npapi 插件中, 事件的内部实现可以用下图来理解, 以 BeforeRowDeleted 事件为例:
  从点击菜单上的 “删除” 开始, 触发 OnEvent 事件并执行其中的js, 插件删除选中的行, 这三个动作是按顺序逐步执行的, 完全符合我们常规的期望。

  但是在 Chrome ppapi 环境下, 插件是无法直接触发 OnEvent 事件的, 插件必须通过 PostMessage 向 Chrome 内核发送消息, 间接触发 OnEvent, 这样一来, 插件的程序主线程和Chrome内核的消息处理是异步进行的, 如下图所示:
  PostMessage 的后果就是:OnEvent 执行完成的时间点, 有可能落在插件执行删除动作之前, 也可能落在插件执行删除动作之后! 如上图 1、2 圆圈的虚线.
  由于 ppapi 的这种不能同步进行的规范的限制, 让事件的顺序关联成了大问题。
  插件自 120 版本开始, 增加了针对性的解决方法。

7.2.解决步骤一

  解决方案就是: 让后续动作延迟执行, 默认延迟 500 毫秒。如下图:
  当然, 仅仅靠延缓 500 毫秒后执行还是不够的, 比如 OnEvent 执行时间完全有可能超过 500 毫秒, 在接下去的 “解决步骤二” 中, 我们会进一步完善此方案。

  由于插件抛出的事件很多, 并且大部分事件你可能都不感兴趣, 所以没有必要让所有事件都延缓 500 毫秒执行, 否则页面会显得很卡, 为此, 我们为树列表和报表增加了一个函数:
    EnableEventLock
  此函数的作用, 相当于是预先声明一下, 哪些事件需要延迟执行, 该函数可以在 OnReady 事件中执行, 例如:
function OnReady(id)
{
 AF.func("build", "../3a.xml");
 AF.func("EnableEventLock", "BeforeRowDeleted, BeforeColDeleted");
}
  该函数的详细使用方法请参见树列表、报表的函数帮助文档。

7.3.解决步骤二

  树列表和报表增加了二个函数:
    EventLock
    EventUnLock
  EventLock 函数的作用是锁住事件, 锁住后, 你可以在事件中书写 js, 哪怕 js 执行时间超过500毫秒, 也仍然是安全的, 插件会一直等待。由于插件无法感知何时完成 js 的执行, 所以你在退出 OnEvent 函数体之前必须执行 EventUnLock 解锁, 相当于告诉插件, 可以启动后续的删除动作了.
  例子语法如下:
function OnEvent(id, Event, p1, p2, p3, p4)
{
 if(Event == "BeforeRowDeleted") {
  //加锁
  var handle = AF.func("EventLock", Event);
  //你的js
  ...
  ...
  //完成, 退出前释放锁
  AF.func("EventUnLock", handle);
 }
}
  通过对这段代码的分析, 可见应该尽快执行加锁动作, 在 500 毫秒内完成加锁, 这应该是没有问题, 因为我们也要求 OnEvent 的第一层书写脚本就是判断 Event 事件名, 写 if else... if else...if else 分别执行各自的事件处理.
  至此, ppapi 的异步问题已经完美解决.

7.4.疑问解答

问题1:所有事件都必须写这样的加锁、释放锁的语句吗?
答: 仅用于你认为先后次序逻辑很重要的事件。通常以 "Before.." 开头的事件肯定是属于这一类的, 以开发文档 “事件” 的说明为准。
120版本实现了这些事件中一部分, 在后续版本中将会逐步完善.

问题2:对 IE 、360浏览器兼容吗?
答: 完全兼容。事实上, 插件内部会根据浏览器来判断, 如果不是 ppapi, 则会跳过、忽略这三个函数。

问题3:如果我 EnableEventLock 声明某些事件了, 但在 OnEvent 中不写加解锁、甚至我页面中完全不写 OnEvent 函数体, 会怎样?
答: 插件在每次抛出这些事件时, 它都会等待 500 毫秒, 给人的感觉就是界面有点卡。

问题3:如果我没有执行 EnableEventLock 声明, 但在 OnEvent 中写了加解锁, 会怎样?
答: 没有任何作用, 达不到您期望的效果。 你可以查看一下 EventLock 返回值, 锁的句柄是空的。

问题4:如果我写了加锁, 但退出 OnEvent 时忘了解锁, 会怎样?
答: 插件会一直等待下去。

问题5:如果我想按条件取消上面例子中的后续的删除动作,需要怎样做?
答: 执行 CancelEvent, 比如这样写:
function OnEvent(id, Event, p1, p2, p3, p4)
{
 if(Event == "BeforeRowDeleted") {
  var handle = AF.func("EventLock", Event);
  if(..条件判断.. (略...)) {
    AF.func("CancelEvent", handle);
    return;
  }
  ...
  AF.func("EventUnLock", handle);
 }
}
全局函数 CancelEvent 的作用是告诉插件, 请阻止执行后续的动作, 同时释放锁.
当然, 在 CancelEvent 后、退出 OnEvent 体之前再执行一遍 EventUnLock 也没有关系, 此时这个 handle 句柄的锁已经不存在了.
另外需要说明一下, 不是所有事件都可以 CancelEvent 的, 以文档为准.