先回顾一下之前看的 core/event.js, 其提供了 minder 的事件机制 (event) 支持: node
// 表示一个脑图中发生的事件 class MinderEvent { ctor(type, parms, canstop): 构造一个脑图事件, type 是一个名字字符串, 如 `contentchange'. getPosition(): 若是事件是从一个 kity 事件派生的,会有 `getPosition()` 获取事件发生的坐标 getTargetNode(): 当发生的事件是鼠标事件时,获取事件位置命中的脑图节点 stopPropagation(): 中止(向上)传播. preventDefault(): 取消缺省处理. ... 其它辅助函数略... }
这个类从功能上看, 模仿了 DOM 的事件, 提供了基本类型信息, 以及一些辅助获取信息的函数. angularjs
// 当 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[] 数组, 我看不如不要复制, 而是添加的时候采用复制后添加
的方式也许效率更高, 更节省点内存. express
====== api
问本身一个问题: 当界面选中一个节点(或取消选中), 工具栏的变化是如何产生的?
思路多是哪一个呢?
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() 方法调用, 按钮状态被设置. 那么这些是如何发生的呢? app
研究一下这四个事件都是谁在监听: dom
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 页:
难道咱们随意想了解的一个问题就接触到了钥匙? 无论钥匙不钥匙, 问题老是要解决的. 继续看...
当 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() 方法, 它们内部已经处理好了监控工做.
在 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 次, 在不知道的状况下.