文档章节

学习 kityminder & angular (十四) event 和 scope.$apply

刘军兴
 刘军兴
发布于 2015/12/14 16:30
字数 2450
阅读 221
收藏 0

回顾 event 机制 

先回顾一下以前看的 core/event.js, 其提供了 minder 的事件机制 (event) 支持:

// 表示一个脑图中发生的事件
class MinderEvent {
  ctor(type, parms, canstop): 构造一个脑图事件, type 是一个名字字符串, 如 `contentchange'.
  getPosition(): 如果事件是从一个 kity 事件派生的,会有 `getPosition()` 获取事件发生的坐标
  getTargetNode(): 当发生的事件是鼠标事件时,获取事件位置命中的脑图节点
  stopPropagation(): 停止(向上)传播.
  preventDefault(): 取消缺省处理.
  ... 其它辅助函数略...
}

这个类从功能上看, 模仿了 DOM 的事件, 提供了基本类型信息, 以及一些辅助获取信息的函数.

// 当 minder 对象构造时, 调用指定 hook.
// 注册函数实现在 minder.js 中, 方法是在一个闭包数组 _initHooks[] 中添加该函数,
//   当 minder.ctor() 时, 调用 hooks[] 中每一个函数.
Minder.registerInitHook( _initEvents );


extend class Minder {
  _initEvents(): 初始化 event 组件(部分)所需的内部数据. 实际初始化 _eventCallbacks{} 对象.
  _resetEvents(): 估计不会用到.
  
  on(names, callback): {  // names 可以是多个事件 type, 用空格分隔.
    names.split(/s/).foreach { |type|
      This._listen(type, callback);
    }
  },
  _listen(type, callback) {
    // ... 将 callback 函数添加到 this._eventCallbacks{} 对象中名为 type 的队列中. 简写为:
    this._ec{}.type[] += callback;
  },
  off(names, callback): 与 on() 是反操作, 细节略.
 
  fire(type, params): {  // 发布事件.
    var e = new MinderEvent(...); // 构造事件实例
     this._fire(e);  // 发布实现.
    return this;
  }
  _fire(e): {  //  发布事件的实现.
    // 从前面 _listen() 已经知道, 名为 type 的事件回调函数在...
    var callbacks[] = this._eventCallbacks[type].copy(); // 复制一份.
    foreach (cb in callbacks) 
      => this.cb(e)
    return e.shouldStopPropagation();
  }
}

这是一个典型的注册/发布事件的模型. 原理上没有要说的, 主要是实现的一点点细节问题. 例如:
函数 _fire() 返回的值被 fire() 函数抛弃, 那 shouldStopPropagation() 语义如何实现呢...?

另外, 在发布的时候都会"复制"一个 event callbacks[] 数组, 我看不如不要复制, 而是添加的时候采用复制后添加
的方式也许效率更高, 更节省点内存.

======

待求解的问题

问自己一个问题: 当界面选中一个节点(或取消选中), 工具栏的变化是如何产生的?
思路可能是哪个呢? 
   1. minder 发布事件, 某个地方监听后更新UI;
   2: toolbar 设置一个 timer, 定期更新UI.
   3: toolbar 的 ng-disabled 背后做了未知侦听/计算过程, 从而改变了按钮 enabled/disabled 状态.

查看 ng-disabled 文档: https://docs.angularjs.org/api/ng/directive/ngDisabled
  该指令设置元素的 disabled 属性, 根据给出的表达式.

调整 undo-redo ng-disabled='debug_can_undo()', 然后调试加入一些 console.log() 语句, 观察:

当选中一个节点时, 会有 minder 事件 'focus', 'selectionchange', 'beforerender', 'noderender' 被发布
出来. 然后就是 9 次连续的 debug_can_undo() 方法调用, 按钮状态被设置. 那么这些是如何发生的呢?

研究一下这四个事件都是谁在监听:

1. 事件 focus: 没有侦听者;
2. 事件 selection-change: 两个侦听者:
    (1) kityminder-core 内部一个;
    (2) kityminder-editor/src/runtime/input.js:75
3. 事件 before-render: 一个侦听者: 在 kityminder-core 内部.
4. 事件 node-render: 一个侦听者: 在 kityminder-core 内部.

更进一步, 我们 hack 掉 fire() 方法, 使得其一个事件也不发布出去(或不发布 selectionchange 事件), 观察结果,
结果是 can_undo() 仍然会被调用 ( 9 次), 那么看起来不是在事件中更新工具条状态的了.

换一个思路:

记得看 angularjs 的某篇文章中提到, angularjs 会处理整个网页的 mouse,key 消息, 然后更新整个界面, 是
这样的机制吗? 让我们去看书和搜索文章 --- 似乎要调用 scope.$apply() 方法使得 AngularJS 更新界面.

搜索了一下整个 kityminder-editor 部分, 发现 service/commandBinder, service/resourceService,
  directive/kityminderEditor, directive/noteEditor, notePreviewer, resourceEditor, searchBox
这些地方有. 那些对话框我们暂时未使用到, 估计不会是它们产生 $apply() 调用.

虽然现在对 AngularJS 的 service 概念还一无所知, 但还是先看看 commandBinder.js 看看是做什么的.
里面大致是这样:

angular.module(...)
  .service(估计是服务名='commandBinder', function() {
    return {
      bind: function(...) {
        minder.on('interactchange', function() {  // 没见到发布此事件, 所以...?
          这里会调用 scope.$apply();
        });
      } 
    };
  });

为了实验, 我们注释掉 scope.$apply(), 发现有趣的一幕, can_undo() 方法被调用次数变为 4 次.
在 scope.$apply() 前面加上 console.log(), 再次实验. 结果显示有 3 次 `interactchange' 事件发生!

只能是前面我们拦截 minder.fire() 的方式不对. 再换一种方式, 根据我们前面对 kityminder 的 event 系统的知识,
我们这次拦截更底层的 _fire() 方法, 并过滤掉不关心的消息. 再次观察, 发现事件 `interactchange' 之后 "总会"
发生工具条 can_undo() 的调用, 根据点击的地方不同, 有时调用 9 次, 或 5 次不同.

 

为了理解 $apply() 等 scope 上的几个方法, 让我们去翻书吧. 打开找到《精通 AngularJS》一书第 293 页:

Scope.$apply -- 打开 AngularJS 世界的钥匙

难道我们随意想了解的一个问题就接触到了钥匙? 不管钥匙不钥匙, 问题总是要解决的. 继续看...

当 AngularJS 首次向公众发布之后, 就有许多关于它的模型变化监控算法的 "阴谋论". 其中最被津津乐道的一种
是, 怀疑 AngularJS 使用了某种轮询机制. (前面我们提及的 toolbar timer 算是轮询机制, 我也是阴谋论者么?)
书上说: 这猜测是错误的!

AngularJS 模型变化监控 背后的思路是 "善后" (observeat the end of the day), 因为引发模型变化的情况
可以被穷举出来:
   1. DOM 事件. 如 click, char 事件
   2. XHR 回调事件.
   3. 浏览器地址变化.
   4. 定时器事件. (timeout, interval)

AngularJS 只会在被明确告知的情况下才会启动它的模型监控机制. 为了让这种监控机制运转起来, 需要在
scope 对象上执行 $apply 方法. (需要模型主动调用 $apply ...) AngularJS 内置指令和服务实现调用了
$apply() 方法, 它们内部已经处理好了监控工作.

 

深入 $digest 循环

在 AngularJS 中, 检测模型变化的过程成为 $digest 循环. 注: digest 在 IT 中可理解为摘要(算法), 如 MD5, SHA.
该方法会检测注册在所有作用域上的所有监视 ($watch) 对象.

存在 $digest 循环的原因:
   1. 判定模型哪些部分发生了变化, 以及 DOM 中的哪些属性应该被更新.
   2. 减少不必要的重绘, 以提升性能, 减少 UI 闪烁.

AngularJS 在(执行完 JavaScript) 交还控制权给 DOM 渲染部分之前, 确保所有的模型值都已完成计算且已 "稳定".
这保证了 UI 一次性完成重绘. 如果每个单独的属性变化都重绘一次, 就会导致性能低下和界面闪烁.

AngularJS 使用脏检查 (dirty checking) 机制来判定某个模型值是否发生了变化. 工作机制是将之前保存的模型
值和能导致模型变化的事件发生后计算的新模型值做对比.

注册一个新的模型监视基本语法:
   scope.$watch(watchExpression, modelChangeCallback)

当作用域上添加一个新的 $watch 时, AngularJS 会计算 watch-expression, 然后将结果保存到内部.
在后续的 $digest 循环中, watch-expression 会被再次计算, 计算所得的新值和旧值进行对比. 回调函数
model-change-callback 只会在新值vs旧值不同时才会调用.

需要知道的是, 不仅我们可以自己注册 $watch, 任何指令都可以设置自己的 $watch.
(可以明显地猜测, ng-disabled 会注册 $watch).

实验: 让我们在浏览器中观察 scope 的 $$watchers[] 字段, 可以发现对于 undo-redo 按钮的 ng-disabled
注册的 $watcher 的 .last 属性记录了该属性最后的值, .exp 记录了表达式 "debug_can_undo()", 如果继续
深入, 还能发现更多惊喜的细节! 但是只能略了.

模型的稳定性

如果模型上任何一个监视器都检测不到任何变化了, 则 AngularJS 就认为该模型是稳定的. 只要一个监视器有
变化, 就会再次使整个 $digest 循环变 dirty (我猜会再循环一遍, 直到 dirty = false 才停止).

AngularJS 会持续执行 $digest 循环, 反复运算所有作用域上的所有监视, 直到没有发现任何变化为止.
(这就解释了为什么 debug_can_undo() 函数会被调用多达 9 次, 只要有一个监视器发生变化, 就会再调用一次).
实验观察: 在选中一个节点状态下, 选择另一个节点, debug_can_undo() 只调用 5 次.
原因猜测: 只有 4 个 scope 中的监视器发生变化, 加上自己调用 1 次, 然后总计调用 5 次.

实验2: 选中另一个 toolbar 的 tab, 此时 undo-redo 按钮 `不显示出来'. 此时点击节点, debug_can_undo()
  仍然会被调用(多次).

不稳定的模型如 random() 怎么办?

AngularJS 默认最多会执行 10 次循环, 之后就会声明该模型是不稳定的, 然后中断 $digest 循环.
(那我们看到的 debug_can_undo() 最多显示 9 次是这个原因吗...? )

 

小结

综上所述, 工具条状态更新流程为:
   1. kityminder 在点击等操作时发布 'interactchange' 事件;
   2. 该事件被 commandBinder 服务侦听, 并调用 angular scope.$apply() 方法;
   3. AngularJS 会进入 $digest 循环, 调用各个 $watcher (如内建指令 ng-disabled 设置的), 直到状态稳定;
   4. 更新 UI, toolbar 的按钮 disable/enable 状态变化/或不变.

所以, 小心点性能问题, 可能某个方法会被调用 N 次, 在不知道的情况下.

© 著作权归作者所有

共有 人打赏支持
刘军兴
粉丝 54
博文 184
码字总数 226359
作品 0
昌平
AngularJs学习笔记--concepts(概念)

启动(Startup) 下面描述angular是如何启动的(参考图表与下面的例子): 1. 浏览器加载HTML,将HTML标签转换为DOM对象; 2. 浏览器加载angular.js的脚本; 3. Angular等待DOMContentLoade...

武文海
2015/02/06
0
0
再谈angularJS数据绑定机制及背后原理—angularJS常见问题总结

Angular 的数据绑定采用什么机制,详述原理? 脏检查机制。阐释脏检查机制,必须先了解如下问题。 单向绑定(ng-bind) 和 双向绑定(ng-model) 的区别? ng-bind 单向数据绑定($scope ->...

634117608
04/19
0
0
Angularjs的$apply及其优化使用

今天,我们要聊得是Angularjs中的小明星$apply。当我们数据更新了,但是view层却没反应时,总能听到有人说,用apply吧,然后,懵懂无知的我们,在赋值代码后面加了$scope.$apply(),然后就惊喜...

北辰狼月
07/01
0
0
angularjs学习:$digest

angularjs扩展了javascript的事件流程机制:它会扩展这个标准的浏览器流程,创建一个Angular上下文。这个Angular上下文指的是运行在Angular事件循环内的特定代码,该Angular事件循环通常被称...

Jack_Q
2015/06/04
0
0
All About Angular 2.0

angular All About Angular 2.0Posted by Rob Eisenberg on November 6th, 2014. Have questions about the strategy for Angular 2.0? This is the place. In the following article I'll e......

Ethan_prog
2015/03/06
0
0

没有更多内容

加载失败,请刷新页面

加载更多

如何通过 J2Cache 实现分布式 session 存储

做 Java Web 开发的人多数都会需要使用到 session (会话),我们使用 session 来保存一些需要在两个不同的请求之间共享数据。一般 Java 的 Web 容器像 Tomcat、Resin、Jetty 等等,它们会在...

红薯
今天
3
0
C++ std::thread

C++11提供了std::thread类来表示一个多线程对象。 1,首先介绍一下std::this_thread命名空间: (1)std::this_thread::get_id():返回当前线程id (2)std::this_thread::yield():用户接口...

yepanl
今天
3
0
Nignx缓存文件与动态文件自动均衡的配置

下面这段nginx的配置脚本的作用是,自动判断是否存在缓存文件,如果有优先输出缓存文件,不经过php,如果没有,则回到php去处理,同时生成缓存文件。 PHP框架是ThinkPHP,最后一个rewrite有关...

swingcoder
今天
2
0
20180920 usermod命令与用户密码管理

命令 usermod usermod 命令的选项和 useradd 差不多。 一个用户可以属于多个组,但是gid只有一个;除了gid,其他的组(groups)叫做扩展组。 usermod -u 1010 username # 更改用户idusermod ...

野雪球
今天
3
0
Java网络编程基础

1. 简单了解网络通信协议TCP/IP网络模型相关名词 应用层(HTTP,FTP,DNS等) 传输层(TCP,UDP) 网络层(IP,ICMP等) 链路层(驱动程序,接口等) 链路层:用于定义物理传输通道,通常是对...

江左煤郎
今天
3
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部