Angular 2 Change Detection - 1

Change Detection (变化检测) 是 Angular 2 中最重要的一个特性。当组件中的数据发生变化的时候,Angular 2 能检测到数据变化并自动刷新视图反映出相应的变化。

在介绍变化检测之前,我们要先介绍一下浏览器中渲染的概念,渲染是将模型映射到视图的过程。模型的值可以是 JavaScript 中的原始数据类型、对象、数组或其他数据对象。然而视图可以是页面中的段落、表单、按钮等其他元素,这些页面元素内部使用 DOM (Document Object Model) 来表示。

为了更好地理解,我们来看一个具体的示例:

<h4 id="greeting"></h4>
<script>
    document.getElementById("greeting").innerHTML = "Hello World!";
</script>

这个例子很简单,因为模型不会变化,所以页面只会渲染一次。如果数据模型在运行时会不断变化,那么整个过程将变得复杂。因此为了保证数据与视图的同步,页面将会进行多次渲染。接下来我们来考虑一下以下几个问题:

  • 什么时候模型会发生变化

  • 模型产生了什么变化

  • 变化后需要更新的视图区域在哪里

  • 怎么更新对应视图区域

而变化检测的基本目的就是解决上述问题。在 Angular 2 中当组件内的模型发生变化的时候,组件内的变化检测器就会检测到更新,然后通知视图刷新。因此变化检测器有两个主要的任务:

  • 检测模型的变化

  • 通知视图刷新

接下来我们来分析一下什么是变化,变化是怎么产生的。

变化和事件

变化是旧模型与新模型之间的区别,换句话说变化产生了一个新的模型。让我们来看一下下面的代码:

import { Component } from '@angular/core';

@Component({
  selector: 'exe-counter',template: `
  <p>当前值:{{ counter }}</p>
  <button (click)="countUp()"> + </button>`
})
export class CounterComponent {
  counter = 0;

  countUp() {
    this.counter++;
  }
}

页面首次渲染完后,计数器的当前值为0。当我们点击 + 按钮时,计数器的 counter 值将会自动加1,之后页面中当前值也会被更新。在这个例子中,点击事件引起了 counter 属性值的变化。

我们继续看下一个例子:

import { Component,OnInit } from '@angular/core';

@Component({
  selector: 'exe-counter',template: `
    <p>当前值:{{ counter }}</p>
  `
})
export class CounterComponent implements OnInit {
  counter = 0;

  ngOnInit() {
    setInterval(() => {
      this.counter++;
    },1000);
  }
}

该组件通过 setInterval 定时器,实现每秒钟 counter 值自动加1。在这种情况下,它是定时器事件引起了属性值的变化。最后我们再来看个例子:

import { Component,OnInit } from '@angular/core';
import { Http } from '@angular/http';

@Component({
  selector: 'exe-counter',template: `
    <p>当前值:{{ counter }}</p>
  `
})
export class CounterComponent implements OnInit {
  counter = 0;
  constructor(private http: Http) {}

  ngOnInit() {
    this.http.get('/counter-data.json')
        .map(res => res.json())
        .subscribe(data => {
          this.counter = data.value;
        });
  }
}

该组件在进行初始化的时候,会发送一个 HTTP 请求去获取初始值。当请求成功返回的时候,组件的 counter 属性的值会被更新。在这种情况下,它是由 XHR 回调引起了属性值的变化。

现在我们来总结一下,引起模型变化的三类事件源:

  • Events:click,mouseover,keyup ...

  • Timers:setInterval、setTimeout

  • XHRs:Ajax(GET、POST ...)

这些事件源有一个共同的特性,即它们都是异步操作。那我们可以这样认为,所有的异步操作都有可能会引起模型的变化。

非常好,你已经了解了引起模型变化的事件源和触发变化的时机点。但是你还不知道,是由谁来负责通知相应的变化给视图。接下来,我们将讨论一种允许 Angular 随时检测到变化的机制,它被称为 Zone

Zones

Zone 是下一个 ECMAScript 规范的建议之一。Angular 团队实现了 JavaScript 版本的 zone.js ,它是用于拦截和跟踪异步工作的机制。

Zone 是一个全局的对象,用来配置有关如何拦截和跟踪异步回调的规则。Zone 有以下能力:

  • 拦截异步任务调度

  • 提供了将数据附加到 zones 的方法

  • 为异常处理函数提供正确的上下文

  • 拦截阻塞的方法,如 alert、confirm 方法

我们来看一个简单的示例:

Zone.current.fork({}).run(function () {
    Zone.current.inTheZone = true;
  
    setTimeout(function () {
        console.log('in the zone: ' + !!Zone.current.inTheZone); 
    },0);
});

console.log('in the zone: ' + !!Zone.current.inTheZone);

以上代码运行后的结果是:

in the zone: false
in the zone: true

是不是感觉很神奇!在Angular 2 中,有一个 NgZone,它是专门为 Angular 2 定制的 zone。在正式介绍它之前,我们先来看一下 Angular 1.x 中的一个例子:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Angular 1.x Demo</title>
    <script src="//cdn.bootcss.com/angular.js/1.6.3/angular.min.js"></script>
</head>
<body ng-app="exeApp">
<div ng-controller="MainCtrl">
    <h4>Hello {{ name }}</h4>
</div>
<script type="text/javascript">
    angular.module('exeApp',[])
            .controller('MainCtrl',['$scope',function ($scope) {
                $scope.name = 'Angular';

                setTimeout(function () {
                    $scope.name = 'Angular 2';
                },2000);
            }]);
</script>
</body>
</html>

以上代码运行后的输出结果:

用过 Angular 1.x 的同学,应该很清楚可以通过 Angular 1.x 中的 $timeout 服务或手动调用 $scope.$digest() 方法来通知视图刷新。这对初学者来说,是很麻烦的一件事情。你们应该还记得前面计数器组件的例子,我们通过 setInterval 定时器,实现每秒钟 counter 值自动加1,页面就自动刷新了。不需要再使用 Angular 1.x 中的 $timeout 服务或手动调用 $scope.$digest() 方法来刷新视图。

为什么我们都是使用定时器,而在 Angular 2 中模型发生变化后,却能自动通知视图进行刷新呢 ?我们来分析一下,首先在浏览器中新开一个 Tab 页,在控制台输入:

window.setTimeout.toString() 
"function setTimeout() { [native code] }"

然后再打开一个 Angular 2 应用的页面,在控制台同样输入:

window.setTimeout.toString()
"function setTimeout(){return f(this,arguments)}"

我们发现在 Angular 2 中,setTimeout 方法已经被重写了,最简单的实现方式如下:

var originSetTimeout = window.setTimeout;
window.setTimeout = function(fn,delay) {
  console.log('setTimeout has been called');
  originSetTimeout(fn,delay); 
}

其实在 Angular 2 应用程序启动之前,Zone 采用猴子补丁 (Monkey-patched) 的方式,将 JavaScript 中的异步任务都进行了包装,这使得这些异步任务都能运行在 Zone 的执行上下文中,每个异步任务在 Zone 中都是一个任务,除了提供了一些供开发者使用的钩子外,默认情况下 Zone 重写了以下方法:

  • setInterval、clearInterval、setTimeout、clearTimeout

  • alert、prompt、confirm

  • requestAnimationFrame、cancelAnimationFrame

  • addEventListener、removeEventListener

Zone 内部源码片段:

var set = 'set';
var clear = 'clear';
var blockingMethods = ['alert','prompt','confirm'];
var _global = typeof window === 'object' && window || 
     typeof self === 'object' && self || global;
patchTimer(_global,set,clear,'Timeout');
patchTimer(_global,'Interval');
patchTimer(_global,'Immediate');
patchTimer(_global,'request','cancel','AnimationFrame');
patchTimer(_global,'mozRequest','mozCancel','webkitRequest','webkitCancel','AnimationFrame');

NgZone

NgZone 是基于 Zone 实现的,它是Zone派生出来的一个子Zone,在 Angular 环境内注册的异步事件都运行在这个子 Zone 内 (因为NgZone拥有整个运行环境的执行上下文),它扩展了自有的一些 API 并添加了一些功能性的方法到它的执行上下文中。

在 Angular 源码中,有一个 ApplicationRef_ 类,其作用是用来监听 NgZone 中的 onMicrotaskEmpty 事件,无论何时只要触发这个事件,那么将会执行一个 tick 方法用来告诉 Angular 去执行变化检测,简化版的代码如下:

class ApplicationRef { 
  private _views: InternalViewRef[] = [];

  constructor(private zone: NgZone) {
        this.zone.onMicrotaskEmpty.subscribe(() => {
            this.zone.run(() => { 
              this.tick();
            }); 
        });
  }
  
  tick() { 
    if (this._runningTick) {
      throw new Error('ApplicationRef.tick is called recursively');
    }
    this._views.forEach((view) => view.detectChanges());
   }
}

现在我们先来总结一下前面所讲的内容:

  • 异步操作被安排为任务

  • Zones 跟踪任务的执行

  • Angular 处理由执行异步操作引起的事件

  • Angular 对所有组件执行变化检测,若发生变化则更新视图

要完全理解 Zone 的工作原理是比较困难的,对我们大部分的人来说,只要知道 Angular 内部是通过它来跟踪异步任务,然后执行变化检测任务就可以了。

我有话说

1.在 Angular 2 项目中怎么访问 Zone 打补丁前的方法,如 setTimeout、clearTimeout 等

因为 Zone 内部通过内建的 __symbol__ 函数来模拟 Symbol

function __symbol__(name) {
  return '__zone_symbol__' + name;
}

因此我们可以在浏览器的控制台中运行:

Object.keys(window).forEach((key) => {
 if(key.indexOf('zone_symbol') > 0) {
    console.log(key);
 }  
});

运行后控制台的输出结果如下:

2.前面介绍 Zone 使用的示例,为什么控制台会输出那样的结果 ?

// 加载Zone.js给浏览器中的一些异步操作打上补丁
// 创建Root Zone
// 调用Zone.current对象上的fork方法创建新的zone,我们称之为childZone
Zone.current.fork({}).run(function () { 
      // 运行run方法,Zone.current被设置为函数被执行时所属的Zone,即childZone
    Zone.current.inTheZone = true;
  
      // 这里注册了一个定时器。由于被打过了猴子补丁,这里调用的并不是
    // 浏览器"默认"的setTimeout方法。因此,这里实际上是在配置代理。这里
    // 要重点指出的是这个代理会保留一个指向创建时所属Zone的引用即childZone,
    // 稍后会用到这个引用。
    setTimeout(function () {
        // 定时时间到,此时的Zone.current的值会被重置为childZone
        console.log('in the zone: ' + !!Zone.current.inTheZone); 
    },0);
  
      // 代码执行完 Zone.current属性被重置为Root Zone
    // Zone的生命周期里的钩子函数会被触发
});

console.log('in the zone: ' + !!Zone.current.inTheZone);

如果还是不好理解的话,我们可以想象一下同步的过程:

const rootZone = Zone.current;
// 创建一个新的Zone
const childZone = Zone.current.fork({});
// 设置当前的zone
Zone.current = zone;
// 为当前的zone添加inTheZone属性
Zone.current.inTheZone = true;
console.log('in the zone: ' + !!Zone.current.inTheZone);
// 退出当前的zone
Zone.current = rootZone;
console.log('in the zone: ' + !!Zone.current.inTheZone);

总结

这篇文章我们先介绍了浏览器中渲染的概念,然后通过三个示例引出了引起模型变化的事件源并总结了它们之间的共性,此外我们还介绍了 Angular 1.x 项目中初学者容易遇到的问题,并基于该问题引入了 Zone 和 NgZone 的概念,最后我们简单介绍了 Zone.js 的内部工作原理。下一篇文章我们将详细介绍 Angular 2 组件中的变化检测器。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


ANGULAR.JS:NG-SELECTANDNG-OPTIONSPS:其实看英文文档比看中文文档更容易理解,前提是你的英语基础还可以。英文文档对于知识点讲述简明扼要,通俗易懂,而有些中文文档读起来特别费力,基础差、底子薄的有可能一会就会被绕晕了,最起码英文文档中的代码与中文文档中的代码是一致的,但知识点讲述实在是差距太大。Angular.jshasapowerfuldire
AngularJS中使用Chart.js制折线图与饼图实例  Chart.js 是一个令人印象深刻的 JavaScript 图表库,建立在 HTML5 Canvas 基础上。目前,它支持6种图表类型(折线图,条形图,雷达图,饼图,柱状图和极地区域区)。而且,这是一个独立的包,不依赖第三方 JavaScript 库,小于 5KB。   其中用到的软件:   Chart.js框架,版本1.0.2,一
IE浏览器兼容性后续前言 继续尝试解决IE浏览器兼容性问题,结局方案为更换jquery、angularjs、IE的版本。 1.首先尝试更换jquery版本为1.7.2 jquery-1.9.1.js-->jquery-1.7.2.js--> jquery2.1.4.js 无效 2.尝试更换IE版本IE8 IE11-
Angular实现下拉菜单多选写这篇文章时,引用文章地址如下:http://ngmodules.org/modules/angularjs-dropdown-multiselecthttp://dotansimha.github.io/angularjs-dropdown-multiselect/#/AngularJSDropdownMultiselectThisdire
在AngularJS应用中集成科大讯飞语音输入功能前言 根据项目需求,需要在首页搜索框中添加语音输入功能,考虑到科大讯飞语音业务的强大能力,遂决定使用科大讯飞语音输入第三方服务。软件首页截图如下所示: 涉及的源代码如下所示: //语音识别$rootScope.startRecognize = function() {var speech;
Angular数据更新不及时问题探讨前言 在修复控制角标正确变化过程中,发觉前端代码组织层次出现了严重问题。传递和共享数据时自己使用的是rootScope,为此造成了全局变量空间的污染。根据《AngularJs深度剖析与最佳实践》,如果两个控制器的协作存在大量的数据共享和交互可以利用Factory等服务的“单例”特性为它们注入一个共享对象来传递数据。而自己在使用rootScope
HTML:让表单、文本框只读,不可编辑的方法有时候,我们希望表单中的文本框是只读的,让用户不能修改其中的信息,如使中国">的内容,"中国"两个字不可以修改。实现的方式归纳一下,有如下几种。方法1:onfocus=this.blur()中国"onfocus=this.blur()>方法2:readonly中国"readonly>中国"readonly="tru
在AngularJS应用中实现微信认证授权遇到的坑前言 项目开发过程中,移动端新近增加了一个功能“微信授权登录”,由于自己不是负责移动端开发的,但最后他人负责的部分未达到预期效果。不能准确实现微信授权登录。最后还得靠自己做进一步的优化工作,谁让自己是负责人呢?原来负责人就是负责最后把所有的BUG解决掉。 首先,熟悉一下微信授权部分的源代码,如下所示:
AngularJS实现二维码信息的集成思路需求 实现生成的二维码包含订单详情信息。思路获取的内容数据如下: 现在可以获取到第一级数据,第二级数据data获取不到。利用第一级数据的获取方法获取不到第二级数据。for(i in data){alert(i); //获得属性 if(typeof(data[i]) == "o
Cookie'data'possiblynotsetoroverflowedbecauseitwastoolarge(5287>4096bytes)!故事起源 项目开发过程中遇到以上问题,刚开始以为只是个警告,没太在意。后来发现直接影响到了程序的执行效果。果断寻找解决方法。问题分析 根据Chrome浏览器信息定位,显示以下代码存在错误:
AngularJS控制器controller之间如何通信angular控制器通信的方式有三种:1,利用作用域继承的方式。即子控制器继承父控制器中的内容2,基于事件的方式。即$on,$emit,$boardcast这三种方式3,服务方式。写一个服务的单例然后通过注入来使用利用作用域的继承方式由于作用域的继承是基于js的原型继承方式,所以这里分为两种情况,当作用域上面的值
AngularJS路由问题解决遇到了一个棘手的问题:点击优惠详情时总是跳转到药店详情页面中去。再加一层地址解决了,但是后来发现问题还是来了:Couldnotresolve'yhDtlMaintain/yhdetail'fromstate'yhMaintain'药店详情http://192.168.1.118:8088/lmapp/index.html#/0优惠券详情
书海拾贝之特殊的ng-src和ng-href在说明这两个指令的特殊之前,需要先了解一下ng的启动及执行过程,如下:1)浏览器加载静态HTML文件并解析为DOM;2)浏览器加载angular.js文件;3)angular监听DOMContentLoaded事件,监听到时开始启动;4)angular寻找ng-app指令,确定作用范围;
angularjs实现页面跳转并进行参数传递Angular页面传参有多种办法,我在此列举4种最常见的:1.基于ui-router的页面跳转传参(1)在AngularJS的app.js中用ui-router定义路由,比如现在有两个页面,一个页面(producers.html)放置了多个producers,点击其中一个目标,页面跳转到对应的producer页,同时将producerId
AngularJS实现表格数据的编辑,更新和删除效果实现首先,我们先建立一些数据,当然你可以从你任何地方读出你的数据var app = angular.module('plunker', ['ui.bootstrap']);app.controller('MainCtrl', function($scope) { $scope.name = 'World'; $sc
ANGULAR三宗罪之版本陷阱      坑!碰到个大坑,前面由于绑定日期时将angular版本换为angular-1.3.0-beta.1时,后来午睡后,登录系统,发现无论如何都登陆不进去了,经过调试,发现数据视图已经无法实现双向绑定了。自己还以为又碰到了“僵尸程序”了呢,对比药店端的程序发现并没有什么不同之处。后来自己经过一番思索才隐约感觉到是不是angular的版本造成的,将版本换为之前
JS实现分页操作前言 项目开发过程中,进行查询操作时有可能会检索出大量的满足条件的查询结果。在一页中显示全部查询结果会降低用户的体验感,故需要实现分页显示效果。受前面“JS实现时间选择插件”的启发,自己首先需要查看一下HTML5能否实现此效果。 整了半天,不管是用纯CSS3也好,还是用tmpagination.js还是bootstrap组件也好,到最后自己静下心来理
浏览器兼容性解决之道前言 浏览器兼容性一直是前端开发中不得不面对的一个问题。而最突出的就是IE。对绝大多数公司来说,兼容IE6的性价比已经很低,而IE7则几乎已经绝迹。所以,常见的兼容性下限是IE8。这也正是Angular1.2x的兼容性目标,Angular团队声明:Angular的持续集成服务器会在IE8下运行所有的测试。但这些测试不会运行在IE7及以下版本,它们也不会保证An
JS利用正则表达式校验手机号绪 由于项目需求,需要在前端实现手机号码的校验。当然了,对于基本的格式校验应该放在客户端进行,而不需要再将待校验的手机号发送至服务端,在服务端完成校验,然后将校验结果返回给客户端,客户端根据返回的结果再进行进一步的处理。如此反而复杂化了处理过程。 其实,处于安全考虑,应该在服务端进行二次校验。以下为在客户端的JS中校验手机号码格式
基于项目实例解析ng启动加载过程前言 在AngularJS项目开发过程中,自己将遇到的问题进行了整理。回过头来总结一下angular的启动过程。 下面以实际项目为例进行简要讲解。1.载入ng库2.等待,直到DOM树构造完毕。3.发现ng-app,自动进入启动引导阶段。4.根据ng-app名称找到相应的路由。5.加载默认地址。6.Js顺序执行,加载相应模版页sys_tpls/