文档章节

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

刘军兴
 刘军兴
发布于 2015/12/14 16:30
字数 2450
阅读 263
收藏 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 次, 在不知道的情况下.

© 著作权归作者所有

共有 人打赏支持
刘军兴
粉丝 57
博文 192
码字总数 233937
作品 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
2018/04/19
0
0
angular 调试时出现$http.get(...).success is not a function错误

我是angular 初学者,在调试时出现$http.get(...).success is not a function错误,我在网上找了一下,没有得到解决,求一下大神,帮我看一下出现了什么问题谢谢! 具体错误:TypeError: $h...

宸紫懿
2016/12/29
1K
0
$apply already in progress at Error

Search: Sort by: name age {{phone.name}} {{phone.snippet}} {{phone.age}} XMLHttpRequest cannot load file:///D:/ProgramFilesZhtt/tomcat1081/webapps/AngularJS/js/05.json. Cross or......

梦幻女侠
2013/10/11
1K
1
angularjs双向绑定后,发生了什么事情?是什么可以让view层和controller层进行绑定的?

大家好,我是IT修真院北京总院第24期的学员,一枚正直纯洁善良的web程序员 今天给大家分享一下,修真院官网js任务7,深度思考中的知识点——angularjs双向绑定后,发生了什么事情?是什么可以...

我是一只北极熊啊
2017/10/16
0
0

没有更多内容

加载失败,请刷新页面

加载更多

漏洞防御与修复工作

漏洞管理工作是企业安全建设必不可少的一环,在风险管理工作中,漏洞管理能够防患于未然,企业对漏洞管理有着广泛的基础建设和实践经验。但随着攻防技术的发展,传统漏洞管理的安全技术和管理...

linuxprobe16
50分钟前
1
0
MicroPython技术及应用前景

1 Micropython技术是什么? MicroPython极精简高效的实现了Python3语言。它包含Python标准库的一小部分,能在单片机和受限环境中运行。 1.1 MicroPython发展 由剑桥大学的理论物理学家乔治....

bodasisiter
56分钟前
4
0
跟我学Spring Cloud(Finchley版)-13-通用方式使用Hystrix

本节详细讲解使用Hystrix的通用方式。 简介 Hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。Hystrix主要...

周立_ITMuch
今天
2
0
🛠️Hanjst/汉吉斯特更新加JavaScript运行时优化等

这是 Hanjst/汉吉斯特 发布以来的首个主要升级更新版本。这次的主要升级更新的内容包括移除HTML Comments注释行, 优化在 Hanjst include模板文件时的JavaScript运行时环境。 Hanjst 在设计和...

wadelau
今天
3
0
OSChina 周六乱弹 —— 舔狗是没有好下场的

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @我没有抓狂 :#今天听什么# #今天听这个# 分享 Nirvana 的歌曲《Smells Like Teen Spi...》 《Smells Like Teen Spi...》- Nirvana 手机党少...

小小编辑
今天
590
14

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部