译-JS定时器工作原理

本篇是John Resig的JS定时器工作原理的翻译版本,由于本人水平有限,仅供个人学习与参考,如有用词不准确,请以原文为准。

译文

在基本层面上,理解JavaScript定时器的工作原理是非常重要的。由于单线程的缘故,所以他们表现的不是很直观。让我们从研究可以构建和操作定时器的三个函数开始。

  • var id = setTimeout(fn, delay); - 初始化一个单独的定时器,它将在延迟结束后出发特定函数,函数将返回唯一ID,用它可以稍后取消这个定时器。
  • var id = setInterval(fn, delay); - 和上面setTimeout类似,但是它是连续不断地调用函数(每次都是一段延迟之后),直到它被取消。
  • clearInterval(id); - 接收一个ID(从上诉两个函数之一返回的),然后结束定时器。

为了理解定时器内部工作原理,要引出一个重要概念:定时器的延迟是不保证的。由于浏览器中的所有JavaScript都在单个线程上执行,因此只有在执行过程中有开始时才会运行异步事件(例如鼠标单击和计时器)。如下图所示:

js-timers

这个图中有很多信息要消化,但完全理解它会让您更好地了解异步JavaScript执行的工作方式。这个图片是一维的:垂直表示时间(毫秒),蓝块表示JS执行块,。比如第一块执行将近18ms,鼠标点击块大约11ms,等等。

因为JS在同一时刻只能执行一段代码(因为单线程机制),这些蓝“块”都在阻塞其他异步事件。意思是:当其他异步事件发生时(比如鼠标点击,定时器出发,XMLHTTPRequest完成),事件会进入到一个队列,以供之后执行(如何排队各个浏览器都不一致,所以认为是一种简化)。

开始,在第一块JS代码里初始化两个定时器:一个10ms的setTimeout和一个10ms的setInterval。由于计时器开始的位置和时间,实际上在我们完成第一个代码块之前就会触发它。但是请注意,它不会立即执行(由于线程,它不能执行此操作)。 相反,延迟功能排队以便在下一个可用时刻执行。

此外,在第一块代码里面,我们发现有一个鼠标点击事件触发。异步事件相关的JavaScript回调(我们不知道用户何时做这个动作,因此认为是异步)不能被立刻执行,像初始化好的定时器,排在队列中等待执行。

在第一段JS代码执行结束后,浏览器立刻“问”一个问题:下一个执行的是谁?在这种情况下,点击处理程序和定时器处理程序都在等待,所以浏览器选择了一个(鼠标点击回调),然后立刻执行。定时器将会等待下一个可用时间,等待执行。

请注意,在鼠标点击处理程序执行的时候,间隔定时器第一次触发了。与计时器一样,它的处理程序也在排队等待以后执行。然而,注意间隔定时器又一次触发了(此时timer正在执行),这次处理程序向下“顺延”了。如果一大块代码执行时,队列里有很多间隔执行器,那么每个间隔执行器将连续执行,没有间隔,直到完成(注:队列里间隔为空)。相反,浏览器只是简单地在等待直到没有更多的间隔处理程序排到队列中。

事实上,我们可以看出,间隔定时器第三次触发的时候,他自己本身正在执行(注:第二次正在执行)。这告诉我们一个很重要的事实:间隔定时器并不关心正在执行的是什么,他们会盲目的排队,即使两次触发之间的delay比预定的小。

最后,在第二次间隔回调函数执行结束后,我们可以看到JS引擎没有要执行的了。这也意味着浏览器在等待新的异步事件发生。当间隔定时器再次触发时,我们得到事件是50ms。这次,没有任何东西阻塞这次执行,所以立刻触发。

我们用一个例子来更好地展示setTimeout和setInterval的不同。

setTimeout(function(){
  /* Some long block of code... */
  setTimeout(arguments.callee, 10);
}, 10);

setInterval(function(){
  /* Some long block of code... */
}, 10);

乍一看,这连段代码似乎功能上是等同的,其实不然。尤其是,setTimeout的这段代码总会有至少10ms的延迟(相对于上一次回调执行,只可能更多,不可能少) ,而setInterval会尝试每10ms执行一个回调,不管上次执行。

我们学了很多知识,最后总结一下:

  • JavaScript引擎是单线程的,强制异步事件排队等待执行。
  • setTimeout和setInterval在执行异步代码上,从本质上来说是不一样的。
  • 如果定时器(timer)被正在执行块阻塞,他讲被延迟到下一个可用时间段来执行(将会比预期间隔更长)。
  • 间隔(intervals)将会连续执行,如果执行时间超过delay。

所有这些都是令人难以置信的重要知识。 了解JavaScript引擎的工作方式,特别是在发生大量异步事件的情况下,为构建高级应用程序代码打下了坚实的基础。

完!!!

关于作者

John Resig:

  • JS语言专家
  • Khan Academy前端架构师
  • 大名鼎鼎jQuery的作者
  • 畅销书《Secrets of the JavaScript Ninja》的作者。

翻译对照

  • interval: 间隔、间隔定时器
  • timer: 定时器
  • handler: 处理器
  • fire: 触发
  • queue up: 排队

个人心得

花了两个小时来翻译大牛的这篇技术博文,不仅锻炼了英语,还学习很多关于JS的内部工作原理,一个字:值!这篇对于理解JavaScript事件轮询机制也有重要帮助。

Angular随笔

最近,我在开发一个前端交互较为复杂的项目,其中好几个组件的ts文件已经超过1000行!仅仅处理其中的逻辑就已经很让我头大,更不要说去维护多人协作的老代码,再加上需求的变化,项目时间还很紧,种种因素,导致了代码的质量很低。所以,我想总结一下自己在开发过程中遇到的“坑”,分享一下自己的想法,也想探讨一下更好地解决方案。

遇到的坑

  • 场景:因为Angular的onChanges检测机制,如果父组件向子组件里传递的是对象,也就是引用传递,只改变对象的某些属性,是不会被检测到的。那么,问题来了:如果一个数组里的某个对象的某个属性变化了怎么办?子组件里是不会进行数据更新的,也就是页面不会刷新。

我现在的解决办法是:声明一个Service,父子组件共用,然后在Service里保存一个Subject,父组件里属性发生变化,触发

subject.next(item)

然后,在子组件里订阅

subject.subscribe(item => {
  doSomething()
})

由于Subject在RxJS里扮演着一个重要的角色,即既是观察者(observer),又是可观察对象(Observable)。

Angular官方推荐使用Observable代替Promise进行异步操作,就像是TS是JS的超集,Observable也可以很简单的转换成Promise,而且Angular本身也使用观察者设计模式,

Observable的具体使用方法和一些技术细节未完待续…

Angular核心概念之指令(Directive)

因为最近事情比较多,平时上班处于饱和状态,晚上回家也已经很累了,加上周末要去运动放松,实在难找出一整块时间去整理学习(ㄒoㄒ)。有过原创博客经历的人都知道,去高质量的总结、分享一些知识点其实非常的费时间,就比如去年分享的组件那篇,足足耗费了我一个下午,可是我还是觉得有很多细节没有表述清楚,加上我文笔一般,经常是想写的很多,但是打开编辑器就忘了思路…

虽然时间很紧,但我还是更倾向于高质量的总结和分享,年轻时要克服浮躁和焦虑,脚踏实地,稳步前进!

概述

Angular中的Directive分为三类:

  • 组件(Component): 带有模板的指令
  • 属性指令(Attribute Directives): 添加、删除DOM元素改变DOM结构
  • 结构指令(Structural directives): 改变元素、组件、其他指令外观和行为

组件是一种特殊的指令,详见组件。 本篇我们重点介绍另外两种指令。

常用的属性指令有内置的 NgStyleNgClass 等用来改变元素的属性、样式,还有官方实例中的HighLight指令,用来高亮元素,等等。

常用的结构指令有内置的 NgForNgIf 用来改变视图的结构。

创建指令

创建一个指令最基本的操作:

  1. 导入Directive装饰器(结构化指令还需要Input、TemplateRef和ViewContainerRef)
  2. 设置CSS选择器,Angular会在文本中定位此选择器
  3. 给指令类添加装饰器

示例代码

来源: Angular官方

  • 属性指令
<h1>My First Attribute Directive</h1>

<h4>Pick a highlight color</h4>
<div>
  <input type="radio" name="colors" (click)="color='lightgreen'">Green
  <input type="radio" name="colors" (click)="color='yellow'">Yellow
  <input type="radio" name="colors" (click)="color='cyan'">Cyan
</div>
<p [appHighlight]="color">Highlight me!</p>

<p [appHighlight]="color" defaultColor="violet">
  Highlight me too!
</p>
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]'
})
export class HighlightDirective {

  constructor(private el: ElementRef) { }

  @Input() defaultColor: string;

  @Input('appHighlight') highlightColor: string;

  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor || this.defaultColor || 'red');
  }

  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }

  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}
  • 结构指令
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

/**
 * Add the template content to the DOM unless the condition is true.
 *
 * If the expression assigned to `appUnless` evaluates to a truthy value
 * then the templated elements are removed removed from the DOM,
 * the templated elements are (re)inserted into the DOM.
 *
 * <div *ngUnless="errorCount" class="success">
 *   Congrats! Everything is great!
 * </div>
 *
 * ### Syntax
 *
 * - `<div *appUnless="condition">...</div>`
 * - `<ng-template [appUnless]="condition"><div>...</div></ng-template>`
 *
 */
@Directive({ selector: '[appUnless]'})
export class UnlessDirective {
  private hasView = false;

  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef) { }

  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

注意事项

  1. 这里的方括号([])表示它的属性型选择器。Angular 会在模板中定位每个有一个属性叫 appHighlight 的元素,并且为这些元素加上本指令的逻辑。
  2. 一个宿主元素最多只能绑定一个结构指令,但是可以有多个属性指令
  3. 结构指令前面的星号(*)是语法糖,其实是Angular帮我们把宿主元素嵌入到‘ng-template’中,所以结构指令一般都有两种写法,官方推荐带星号的语法糖形式
  4. 对于带有输入属性的指令,在模板中是否加([])的这个问题,例如:
<p [appHighlight]="'yellow'">Highlighted in yellow</p>
<p appHighlight="orange">Highlighted in orange</p>

[]是一个绑定到 @Input 的语法,等号(=)后面的内容是变量名,这不是Directive特有的,而是整个Angular的语法,详见Angular模板语法

  • 有方括号([])时:等号右侧引号内的变量必须在ts文件中存在,否则绑定失败,或者双引号内的变量是一个string,并且用单引号引起来
  • 没有方括号([])时:自定义的Directive或者Input属性将按照HTML规定的属性绑定去解析,双引号内的变量将会是string
  • 特殊情况:Boolean类型的true和false,是否写方括号,它的值都将会被正确解析。

未完待续…

Angular依赖注入-Dependency Injection

2018年继续来搞Angular!

什么是依赖注入?

根据维基百科,依赖注入(Dependency Injection)是种解决项目依赖性的设计模式, 好处有大致以下几点:

  • 各模块间的解耦
  • 代码容易维护
  • 开发者无需关注依赖的生产过程,拿来即用

Angular中的DI

Angular的依赖注入有三个重要概念:

  • 注入器(Injector)
  • Provider
  • 依赖(Dependence)

注入器连接了调用方和提供方,使得开发人员很轻松的实现依赖注入。(注:Angular框架已经实现了注入器的生成和调用,并不需要开发者去实现)

注入服务

  1. 通过import导入被依赖对象的服务
  2. Angular读取 @Component@Injectable@Module 装饰器里providers元数据
  3. 在组件构造函数中声明

这样几步,此组件及其子组件都能共享根组件创建的实例,如果子组件或模块不想复用从根组件获取的服务,可以在自己的注入器中重新配置注入(层级注入)。

需要注意的是:

  • Angular没有模块级别作用域,只有程序级和组件级作用域
  • 对于不同的执行上下文,有着不同的注入器,并且执行上下文中的每个依赖对象都是单例的
  • 后面初始化的服务会覆盖前面初始化的服务
  • 由于组件本身是一个类,类有继承关系,但是派生类组件不能继承父类组件的注入器,二者的注入器对象并没有关联,需要使用 super() 将对应的注入服务传递到父类
  • @Host@Optional等装饰器的巧妙使用会带来意想不到的惊喜

Provider

Provider这种设计模式由来已久,在前后台各种技术领域中被广泛使用。它藐视了注入器如何初始化Token所对应的依赖服务,最终注入到组件或者其他服务中。

Provider注册方式

  • 类Provider

对于调用者来说,业务代码和接口没有改变,从而带来极大的便利

{provider: Render, useClass: DomRender} //DOM渲染方式
//{provider: Render, useClass: CanvasRender} //Canvas渲染方式
//{provider: Render, useClass: ServerRender} //服务端渲染方式
  • 值Provider

实际项目中,以来的对象不一定是类

{provider: 'name', useValue: 'William Jing'}
  • 别名Provider

实现多个依赖,一个对象实例的所用,例如为了让新旧服务同时可用,新服务兼容老服务,可以使用此种注册方式

{provider: NewService, useClass: NewService}
{provider: OldService, useExisting: NewService}
  • 工厂Provider

有时候依赖对象是动态变化的,可能需要环境、执行权限来生成,工厂Provider可以提供解决这个问题,通过暴露一个工厂方法,返回一个最终的依赖对象

let contactServiceFactory = (_logger: LoggerService, _userService: UserService) =>{
  return new contactService(_logger, _userService.user.isAuthorized)
}
export let contactServiceProvider = {
  provider: ContactService,
  userFactory: contactServiceFactory,
  deps: [LoggerService, UserService]
};
Angular生命周期钩子

🎅🎅🎅提前祝Merry Xmas🎅🎅🎅

简介

Angular内部管理组件或指令的生命周期,给我们提供了一些接口(这些接口被称为生命周期钩子)来在允许开发者在这些事件触发时,执行相应的回调函数。

Angular一共提供了8个生命周期钩子接口,每个接口有一个唯一的前面加上‘ng’的方法,开发者可以根据实际情况实现其中的一个或者几个来对生命周期的各个阶段进行自定义处理。

图示

下图是Angular钩子方法执行顺序

hooks-in-sequence

钩子方法

  • ngOnChanges()

首次调用发生在ngOnInit之前,并且,当且仅当组件输入数据变化时被调用,输入数据指的是通过@Input装饰器显示指定的那些变量。

  • ngOnInit()

创建组件之后立刻调用,经常会使用ngOnInit获取数据。

  • ngDoCheck()

用于变化监测那些Angular忽略的更改,每次变化监测发生时被调用

  • ngAfterContentInit()

在组件中使用’ng-content’自定义内容的情况下在第一次ngDoCheck执行后调用,只执行一次

  • ngAfterContentChecked()

Angular将外部内容嵌入到组件视图后,每次变化监测都会调用ngAfterContentChecked

  • ngAfterViewInit()

Angular创建了组件的视图以及其子视图之后被调用

  • ngAfterViewChecked()

视图以及其子视图第一次初始化之后和每次变化监测时被调用

  • ngOnDestroy()

组件消失之前调用。根据官方文档描述,这里是用来释放那些不会被垃圾收集器自动回收的各类资源的地方。 取消那些对可观察对象和DOM事件的订阅。停止定时器。注销该指令曾注册到全局服务或应用级服务中的各种回调函数。 如果不这么做,就会有导致内存泄露的风险。

注意事项

  • constructor并不是生命周期钩子,而是Class级别的构造函数,constructor总是在所有钩子函数执行前执行
  • 接口是可选的,也就是说不必在Component或Directive后面加上implements…但是,官方还是强烈建开发者在指定类中添加接口以获得强类型和IDE编辑器带来的好处
  • 有的组件还提供了自己特有的生命周期钩子
  • 可能有人会问,为什么不在constructor里获取数据?是因为构造函数做的事应该尽可能简单,比如变量的初始化,不应该负责组件里的内容
  • 绝大多数情况下ngDoCheck和ngOnChanges不应该一起使用
  • ngDoCheck要慎用,因为每个检测周期内,无论数值是否发生变化,ngDoCheck都会被调用,导致调用非常频繁,所以我们的实现要必须非常轻量级
  • ngOnChanges如果输入属性是对象的话,只会检测对象的引用是否变化,而不会去监测对象属性的变化
  • AfterContent和AfterView一共4个钩子是组件专属的,不适用于指令

参考链接

中文官方文档

在线例子