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 的, 以文档为准.