Angular 2 Change Detection - 1

news/2023/12/1 11:33:56
阅读 Angular 6/RxJS 最新教程,请访问 前端修仙之路

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, set, clear, 'Interval');
patchTimer(_global, set, clear, 'Immediate');
patchTimer(_global, 'request', 'cancel', 'AnimationFrame');
patchTimer(_global, 'mozRequest', 'mozCancel', 'AnimationFrame');
patchTimer(_global, '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 组件中的变化检测器。


http://www.niftyadmin.cn/n/3158063.html

相关文章

汇编语言之启航

使用MASM6.15版本&#xff0c;了解了汇编语言程序的格式。下面是我接触的第一个汇编源程序&#xff1a; 1 ;eg101.asm2 .model small3 .stack4 .data5 msg db Hello, Assembly !,13,10,$6 .code7 .startup8 mov dx,offset msg9 mov ah,9 10 …

企业邮箱登陆方式有哪些?

我是做外贸物流的&#xff0c;入职之后一直过着得过且过的工作&#xff0c;2021年即将走完&#xff0c;马上要到新年了&#xff0c;同时也到了年终考核的环节&#xff0c;为了能让自己在年终考核上有一个好的评分&#xff0c;我暗暗发誓&#xff0c;在过年之前一定要签一个大客…

LPC2478(12)DMA

1、开发环境 LPC-2478STKIARJINK 2、特性 GPDMA 使能外设到存储器、存储器到外设、外设到外设和存储器到存储器的传输。每个 DMA 流都可以为单个源和目标提供单向串行DMA 传输。例如&#xff0c;一个双向端口就需要一个发送流&#xff0c; 一个接收流。源和目标区可以是存储…

flex的tree相关操作

1、tree节点open时获取itemIndex tree.getItemIndex(event.item)

RxJava简要分析

一&#xff1a;RxJava执行流程&#xff1a; RxJava简单使用 private final String tag getClass().getSimpleName(); //数据源&#xff0c;被观察对象Observable<String> obser Observable.create(new ObservableOnSubscribe<String>() {Overridepublic void sub…

公司企业邮箱怎么群发邮件?如何在工作中脱颖而出

不是每一次的付出都是有回报的&#xff0c;但每一次的付出都会为下一次回报积蓄力量&#xff0c;从大学毕业到参加工作&#xff0c;已经整整两年了&#xff0c;在这过去的两年中自己从小白一个&#xff0c;到项目负责人&#xff0c;中间是一个非常漫长的过程&#xff0c;今天为…

马云:我肯定是12x12,你们却在纠结不能996

这是一个有争论的话题&#xff0c;但我们要表达自己真实的想法。 昨晚在阿里内部交流活动上&#xff0c;马老师与阿里的同学交流了996这个话题&#xff0c;谈到了阿里的年轻人如何成长、如何实现理想、如何通过公益为社会做出更多贡献…… 马老师的想法&#xff0c;我们公司的年…

镜像YUM安装仓库(转载唐老师的github)

地址&#xff1a;https://github.com/xfeiwang/go-to-kubernetes/blob/master/docs/centos-devops-sync-repo.mdSync CentOS packages for mirror YUM Repositories - 镜像YUM安装仓库Tables of ContentSync CentOS packagesSearch sample software packages into meida repoIn…

LPC2478(13)EMC

1、开发环境 LPC-2478STKIARJINK 2、特性 外部存储器控制器&#xff08;EMC&#xff09;是 ARM PrimeCellTM MultiPort 存储器控制器外设&#xff0c;它支持异步静态存储器设备&#xff08;例如 RAM、 ROM 和 Flash&#xff09; 以及动态存储器&#xff08;例如单一数据传输…

什么是邮箱怎么注册?学会管理企业邮箱邮件,提高工作效率

再企业中职位越高&#xff0c;需要处理的邮件数量往往也越来越多。然而&#xff0c;在你拥有自己的秘书帮你处理前&#xff0c;你不得不开始学会高效管理自己的邮件。不夸张地说&#xff0c;能把邮件管理得井井有条的人&#xff0c;在工作上也一定游刃有余。如果你是刚踏入职场…

[分布式]架构设计中的 CAP 和 BASE 理论

CAP 理论 定义&#xff1a; 在一个分布式系统中&#xff0c;当涉及读写操作时&#xff0c;只能保证一致性&#xff08;Consistence&#xff09;、可用性&#xff08;Availability&#xff09;、分区容错性&#xff08;Partition Tolerance&#xff09;3者中的2个&#xff0c;另…

MyBatis学习总结(七)——Mybatis缓存(转载)

孤傲苍狼 只为成功找方法&#xff0c;不为失败找借口&#xff01; MyBatis学习总结(七)——Mybatis缓存 一、MyBatis缓存介绍 正如大多数持久层框架一样&#xff0c;MyBatis 同样提供了一级缓存和二级缓存的支持 一级缓存: 基于PerpetualCache 的 HashMap本地缓存&#xff0c;其…

去掉字串的左右空格

String.prototype.ltrim function() { return this.replace(/(^\s*)/g, ""); } String.prototype.rtrim function() { return this.replace(/(\s*$)/g, ""); } 使用方法&#xff1a; var str " abc "; alert(str); alert(str.LTrim().Rt…

eclipse项目推送git

引用&#xff1a;http://blog.csdn.net/zongzhankui/article/details/48653731转载于:https://www.cnblogs.com/zhangqian1031/p/6590180.html

LPC2478(14)ISP/IAP

1、开发环境 LPC-2478STKIARJINK 2、特性 在系统编程&#xff1a;在系统编程(ISP)是使用 boot 装载程序软件和 UART0 串口对片内Flash 存储器进行编程和再编程的一种方法。在应用编程&#xff1a; 在应用编程(IAP)是按照最终用户的应用代码指示&#xff0c;对片内 Flash 存储…

超便捷mail163手机邮箱登录方法

突然间觉得朋友之间真的是有很特别的缘分&#xff0c;和他们相处自在&#xff0c;他们总是热气腾腾的&#xff0c;能把你从不好的情绪里拉出来带着你往前走。小事打打闹闹&#xff0c;大事永远一直在。今年本来没打算过生日&#xff0c;好多朋友都悄咪咪从各地邮寄了生日礼物&a…
最新文章