浅谈 Angular 脏检查

Angular 的脏值检查机制一直是 Angular 被人诟病的地方,但瑕不掩瑜,Angular 还是一个非常优秀的框架,并且 Angular2 也已经抛弃了这个脏值检查的算法。
最近在看《AngularJS 深度剖析与最佳实践》,不得不说是一本很好的书籍,作者在第三章开始讲背后的原理,这里分析了 Angular 的 $digest 函数,即脏检查机制。所以自己也去下载了 Angular 最新的源码去瞧了下,然后做下笔记吧。

首先要注意,Angular 的 digest 的触发不是定时的,只有在指定的事件触发之后才会进入 $digest。基本上我们用的带 $ 的东西调用之后都可能会触发 digest。比如我们使用 setTimeout 就不会触发 digest,即当你使用 setTimeout 更改 viewmodel 的值后,它不会同步的反映到用户的视图中去,解决方法有两个,一个是使用 Angular 提供的 $timeout 替代 setTimeout$timeout 会在执行结束之后自动触发 digest; 另一个方法是手动调用 $apply,$apply 是 Angular 对 digest 的一层封装,我们一般不会直接调用 digest 而是通过使用 $apply 方法。比如对于 setTimeout,我们就可以这样触发 digest。

1
2
3
4
5
setTimeout(() => {
$scope.$apply(() => {
$scope.test = 123;
})
}, 500);

我们看一个例子,这也是 Angular 源码 $digest 部分的一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var scope = ...;
scope.name = 'misko';
scope.counter = 0;
expect(scope.counter).toEqual(0);
scope.$watch('name', function(newValue, oldValue){
scope.counter = scope.counter + 1;
});
expect(scope.counter).toEqual(0);
// 执行第一次 digest,第一次 digest 会遍历全部的 watcher,并触发上面的方法,从而使的 count+1
scope.$digest();
expect(scope.counter).toEqual(1);
// 第二次调用时,由于上一次调用检查 name 不脏,所以不会再去处理
scope.$digest();
expect(scope.counter).toEqual(1);
// 第三次调用时,由于 name 发生了变化,使得当前值和上一次保存的值不同,所以会触发起 $watch 方法
scope.name = 'adam';
scope.$digest();
expect(scope.counter).toEqual(2);

Angular 的脏值检查过程大致如下:
对当前作用域和子作用域上的 $$watchers 进行遍历,$$watches 保存着 scope 上的所有变量以及其 $watch 方法,调用时会取当前值和上一次值进行比较,如果不相等则会调用 $watch 方法,同时会保存当前的值以在下一次进行比较,并且记录此次检查结果为脏。然后重复进行直到数据不脏为止,因此至少要 digest 两次,超出 10 次会报错,可以调高这个次数限制。当数据不再脏即 model 稳定下来之后, Angular 才会开始一次性批量更新 UI。从而减少了浏览器的 repaint 次数,提升性能。

深入到源码来看:

阅读更多

项目采坑笔记

有段时间没写博客了,前段时间在看 Underscore 源码所以写的多了点,这段时间还是在忙自己的其他项目去了,还是有不少收获的。

Angular 视图过渡动画

之前使用 angular-promise-button 这个模块实现了按钮的自动变化,以前自己是用很多标志位来判断特别二。不仅如此,页面切换动画也是用标志位判断,这样就特别不好维护特别不优雅,上次重构的时候就把这些全部去掉了。但是问题来了,页面数据未到达时候页面就渲染肯定会造成视觉上的问题,怎么解决呢。
我们都想写一些应用很广的代码,比如指令,比如上面这个 angular-promise-button 模块等等。其实要解决上面的问题,也是几行代码就可以解决的事情了。
我所使用的是 Angular 的 ui-router。ngRoute 应该也差不多。
在 ui-router 中可以使用 resolve 达到在控制器初始化以及视图加载前确保数据到达。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$stateProvider
.state('me',{
url: '/me',
controller: 'MeCtrl',
templateUrl: 'me/me_tpl.html',
controllerAs: 'vm',
nav: true,
resolve: {
me: function(userservice) {
return userservice.getUserInfo()
.then(response => response);
}
}
})

只有 resolve 中的全部方法执行完后,才会开始初始化控制和加载视图。这个数据如果在控制器或者视图中要使用,可以在控制器中进行依赖注入。例如上面这个我的控制器是这样写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function(){
'use strict';
angular
.module('index')
.controller('MeCtrl', MeCtrl);
MeCtrl.$inject = ['me'];
function MeCtrl(me) {
let vm = this;
vm.user = me;
}
}());

resolve中的方法是阻塞页面进行的,这样就会带来问题了,如果数据请求比较久将导致网站停滞,我们这时候就希望可以有过渡动画出来。要达到全局过渡效果的作用,可以直接监听 $rootScope 中的三个状态即 $stateChangeStart$stateChangeSuccess 以及 $stateChangeError 事件。例如上面这个例子中,当我们触发 me 这个 state 时,也就触发了 $rootScope 上的 $stateChangeStart 事件,当处理结束后将出发 $stateChangeSuccess 并加载视图, 处理失败就会触发 $stateChangeError 事件。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
angular
.module('index', [
'ui.router',
'ui.bootstrap',
'ngAnimate',
'ngSanitize',
'ngTouch',
'infinite-scroll',
'angularPromiseButtons'
])
.config(config)
.run(($state,$rootScope) => {
$rootScope.$state = $state;
$rootScope.$on("$stateChangeStart", (event, toState, toStateParams, fromState, fromStateParams) => {
var isLoading = toState.resolve;
if(!isLoading) {
for (var prop in toState.views) {
if (toState.views.hasOwnProperty(prop)) {
if(toState.views[prop].resolve) {
isLoading = true;
break;
}
}
}
}
if (isLoading) {
$rootScope.loading = true;
}
});
$rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
$rootScope.loading = false;
});
$rootScope.$on("$stateChangeError", (event, toState, toParams, fromState, fromParams, error) => {
$rootScope.loading = false;
});
});

入口页面,省去了其他代码,这里第一行就是视图,第二行是加载动画,通过ng-show来控制显示。第三行是引入导航栏,这个在后面会说下。

1
2
3
<div ui-view class="uiview" ng-show="!$root.loading"></div>
<div class="cssload-thecube" ng-show="$root.loading"> loading... </div>
<div ng-show="$state.current.nav" ng-include="'navbar/navbar_tpl.html'"></div>

可以看到上面的代码中是监听了 $stateChangeStart 事件,然后获取目标 state 上的 resolve 方法,当 state 上的 resolve 方法全部结束后,$rootScope.loading 设置为 false,否则保持为 true。
当监听到 $stateChangeSuccess 或者 $stateChangeError 事件时,置 $rootScope.loading 为 false,退出过渡动画。在视图中可以使用 $root 得到 $rootScope
可以看到这里有很多参数,可见其功能是很强大的。

阅读更多

浅谈 JavaScript 模块定义规范

JS 模块定义常见的有三种方式,即 AMD, CMD 和 CommonJS。其实还有一个 UMD,他是 CommonJS 和 AMD 揉和在一起而已。不过这些都 out 了,拥抱 ES6 吧。话虽这么说,你让那些不用 ES6 不用 babel 的怎么活,所以还是要了解下滴。

CommonJS

CommonJS 是服务端即 Node.js 采用的模块化方案,我们应该都很熟悉了。例如:

1
2
const fs = require('fs');
fs.readFileSync();

这个过程是同步的,只有成功加载 fs 后才能执行后面的步骤。但在服务器文件都在本地,所以这个问题不大。但这个在浏览器就不合适了,如果文件加载耗时很长,将导致一直等待。

AMD

AMD 全称 Asynchronous Module Definition,意思就是异步模块定义。
用法如下:

1
2
3
require(['math'], function(math) {
math.add(1, 2);
});

math 模块的加载和 math.add() 方法的执行不是同步的,这样浏览器就不会假死。
RequireJs 和 CurlJs 实现了 AMD 规范,将他们嵌入网页,就可以在浏览器端进行模块化编程了。
关于 AMD 的详细模块定义可以参考wiki)。这里给出 Underscore 的 AMD 定义方法:

1
2
3
4
5
if (typeof define == 'function' && define.amd) {
define('underscore', [], function() {
return _;
});
}

CMD

CMD 全称 Common Module Definition,意思就是通用模块定义。。
对于依赖的模块,AMD 是提前执行,而 CMD 是延迟执行。AMD 推崇依赖前置,而 CMD 则推崇依赖就近。例如:

1
2
3
4
define(function(require, exports, module) {
const math = require('./math');
math.add(1, 2);
});

CMD 的主要实现是 SeaJS
AMD 预先加载所有依赖,使用的时候才去执行,速度快,可以并行加载多个模块。但这就需要开发的时候把全部依赖都提前定义,不便于开发和阅读,而且部分依赖(弱依赖)可能只在少数情况下使用。
CMD 只有在真正需要的时候才去加载依赖,使用的时候才去定义执行,但这个加载逻辑偏重,耗性能。

UMD

UMD 全称 Universal Module Definition。
UMD 是 AMD 和 CommonJS 的揉和,他优先使用 CommonJS 的加载方式,其次才使用 AMD 的加载方式。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function (window, factory) {
if (typeof exports === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define(factory);
} else {
window.eventUtil = factory();
}
})(this, function () {
//module ...
});

其实就是一个服务端和浏览端通用的模块解决方案。

ES6 Module

ES6 在语言规格的层面上实现了模块功能,并且实现非常简单,完全可以替代现有的模块加载方案,成为浏览器和服务端都通用的模块解决方案。

阅读更多

Underscore 源码学习(七)

Underscore 中间 flatten 相关的方法之前一直不是很理解,现在完全搞懂了,稍微说一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var flatten = function(input, shallow, strict, output) {
output = output || [];
var idx = output.length;
for (var i = 0, length = getLength(input); i < length; i++) {
var value = input[i];
// 若value为数组,把里面东西去出来赋值给output
// 否则直接赋值给output
// isArrayLike的判断可以去掉,保留的原因是因为他用来判断value是否为数组很快,可以迅速筛选掉非数组
if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
// Flatten current level of array or arguments object.
if (shallow) {
// 如果给了shallow参数,只只遍历一层
var j = 0, len = value.length;
while (j < len) output[idx++] = value[j++];
} else {
// 一直遍历下去,如果是元素则按下面赋值,如果是数组则继续遍历
flatten(value, shallow, strict, output);
idx = output.length;
}
} else if (!strict) {
output[idx++] = value;
}
}
return output;
};

这个方法不难看懂,作用是将input平铺展开,如果 shallowtrue,则只展开一层。

1
2
_.flatten([1, 2, [3], [[4, [5]]]]) // [1, 2, 3, 4, 5]
_.flatten([1, 2, [3], [[4, [5]]]], true) // [1, 2, 3, [4, [5]]]

这里的 strict 参数就是之前一直卡住的原因,就是下面这个地方:

1
2
3
4
5
6
7
8
9
10
11
_.without = restArgs(function(array, otherArrays) {
return _.difference(array, otherArrays);
});
_.difference = restArgs(function(array, rest) {
rest = flatten(rest, true, true);
// 遍历array,如果array中一个元素包含在rest中,则去掉该元素
return _.filter(array, function(value){
return !_.contains(rest, value);
});
});

这是两个方法,那时候想 without 方法调用的时候, otherArrays是一个数组了,到 difference 方法的时候,这个数组去调用 flatten 方法的时候不是会出问题吗?

1
_.flatten([1, 2, 3], true, true) // []

脑子里面就这样想…卡了好久,等我基本看了全部源码才会过来看才理解了。
difference 方法的 restArgs 很重要,他们两个是各自独立的方法,但是 without 可以共用 difference 的逻辑。
上面那样子理解是有问题的,因为在 without 方法中 otherArrays 如果是[1, 2, 3],到了 flatten 调用的时候因为 restArgs 的关系他变成了 [[1, 2, 3]],调用最后返回结果[1, 2, 3]。然后我就纳闷了,加了一层又解除这是何解…
不过抛开 without 方法去看 difference 方法就能理解了。

阅读更多