0%

  • 双向数据绑定

    • [property]
    • (eventEmitter)
    • [()]
      • 有 @Input() xxx 和 @Output xxxChange
      • 即可使用 [(xxx)] 语法糖来绑定这两个方向
  • Directive

    • 后面应该专门整理
    • 在子元素使用了某一个指令的组件中
      • 可以通过 @ViewChild(ColorfulDirective) c: ColorfulDirective;
      • 或者 @ViewChild(“color”) color: ColorfulDirective;
      • color 是在 Directive 的 Decorator 中的 exportAs 字段
        1
        <p [appColorful]="'green'" #color="colorful">via-export-as works!</p>
  • 自定义 FormControl
    • implements ControlValueAccessor 的四个方法
      • registerOnChange(fn: any): void {}
      • registerOnTouched(fn: any): void {}
      • setDisabledState(isDisabled: boolean): void {}
      • writeValue(obj: any): void {}
  • *ngTemplateOutlet
    • 类似 vue 的 slot,slot 是当年唯一一个研究过的 vue 内容..
    • [ngTemplateOutlet]=””
    • [ngTemplateOutletContext]=””
    • 可以通过模板变量或者 TemplateRef 引用来使用

Ivy 是 Angular 框架的新一代编译器。

Angular 组件模板中的 HTML 实际上都被编译成了 JavaScript 指令,用来创建和更新实际的 DOM。所以 Angular 的很大一部分就是它的编译器,把 HTML 模板编译成 JS 代码。

使用它基本上不需要手动做什么,升级(到 8)的项目有 Angular 提供的 update schematics 自动改代码,到 9 基本无感切换。

扫一眼 Compiler

这里使用 Alex Rickabaugh 在 18 年 AngularConnect 演讲的例子,这就是我们的模板:

1
2
3
4
<div>
<span>{{title}}</span>
<child-cmp *ngIf="show"></child-cmp>
</div>

Angular 2.0 Template Compiler

给每个 @Component 和 @NgModule 生成了 NgFactory,里面是一堆命令式的指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
const parentRenderNode:any = this.renderer.createViewRoot(this.parentElement);
this._text_0 = this.renderer.createText(parentRenderNode,'\n ',null);
this._el_1 = ng.createRenderElement(this.renderer,parentRenderNode,'div',ng.EMPTY_INLINE_ARRAY,null);
this._text_2 = this.renderer.createText(this._el_1,'\n ',null);
this._el_3 = ng.createRenderElement(this.renderer,this._el_1,'span',ng.EMPTY_INLINE_ARRAY,null);
this._text_4 = this.renderer.createText(this._el_3,'',null);
this._text_5 = this.renderer.createText(this._el_1,'\n ',null);
this._anchor_6 = this.renderer.createTemplateAnchor(this._el_1,null);
this._vc_6 = new ng.ViewContainer(6,1,this,this._anchor_6);
this._TemplateRef_6_5 = new ng.TemplateRef_(this,6,this._anchor_6);
this._NgIf_6_6 = new ng.Wrapper_NgIf(this._vc_6.vcRef,this._TemplateRef_6_5);
this._text_7 = this.renderer.createText(this._el_1,'\n',null);
this._text_8 = this.renderer.createText(parentRenderNode,'\n',null);

大概能看出来:
左边用属性存放了各种生成的节点,之后可以增删改查。
右边命令式的用 renderer 来生成了各种节点和引用。

这个据说是当时跑分最快的生成方式,但是组件多了之后,google team 收到反馈说这种方式生成的代码数量太大,所以导致了 View Engine 更换:

Angular 4.0 View Engine

  • 生成的代码从渲染指令替换成了一个数据结构
  • 这个数据结构在运行时被转译
  • 尺寸变小
    然后大概长这样:
1
2
3
4
5
6
7
8
9
function View_RootCmp_0(_l) {
return ng.ɵvid(0, [
(_l()(), ng.ɵeld(0, 0, null, null, 4, "div", [], null, null, null, null, null)),
(_l()(), ng.ɵeld(1, 0, null, null, 1, "span", [], null, null, null, null, null)),
(_l()(), ng.ɵted(2, null, ["", ""])),
(_l()(), ng.ɵand(16777216, null, null, 1, null, View_RootCmp_1)),
ng.ɵdid(4, 16384, null, 0, ng.NgIf, [ng.ViewContainerRef, ng.TemplateRef], {ngIf: [0, "ngIf"]}, null)
], ...);
}

这就是传说中的 ng_factory,一个定义 view 的函数,由两部分组成:

  • DOM 生成的静态定义
  • 组件状态改变时候调用的函数
    也就是视图创建和变更检查。

里面的 ɵ(读 bar,不是 θ)后面的 vid,did,eld 大概是 elementDefinition,ViewDefinition,directiveDefinition 的意思。

这些私有的函数是放在 Angular 运行时的代码,在运行的时候,这些 ng_factory 会被送给运行时,然后生成和更新相应的 DOM。Angular 使用一个 Map 数据结构提供了对他们的映射,这个 Map 也其实就是 Angular runtime 不能被摇树的原因。

Angular 8.0+ Ivy

通过把注解编译成类的静态字段,这些之前需要额外生成 ng_factory 文件的代码,现在都放在类的文件里面了。

@Component → ngComponentDef
@Directive → ngDirectiveDef
@Injectable → ngInjectableDef

1
2
3
4
5
6
7
8
9
@Component({
selector: 'root-cmp',
template: `
<div>
<span>{{title}}</span>
<child-cmp *ngIf="show"></child-cmp>
</div>`,
})
export class RootCmp { … }

装饰器变成了一个静态成员

1
2
3
4
5
6
7
8
9
10
11
12
export class RootCmp { … }
RootCmp.ngComponentDef = ng.ɵdefineComponent({
type: RootCmp,
selectors: [["test-cmp"]],
consts: 4,
vars: 2,
factory: function RootCmp_Factory(t) {
return new (t || RootCmp)();
},
directives: [ChildCmp, ng.NgIf],
template: function RootCmp_Template(rf, ctx) { … },
});

template 长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function RootCmp_Template(rf, ctx) {
if (rf & 1) {
ng.ɵelementStart(0, "div");
ng.ɵelementStart(1, "span");
ng.ɵtext(2);
ng.ɵelementEnd();
ng.ɵtemplate(3, RootCmp_child_cmp_Template_3,
1, 0, null, [1, "ngIf"]);
ng.ɵelementEnd();
} if (rf & 2) {
ng.ɵtextBinding(2, ng.ɵinterpolation1("",
ctx.title, ""));
ng.ɵelementProperty(3, "ngIf",
ng.ɵbind(ctx.show));
}
}

它由一堆指令构成,这个后面再研究。
又分为创建指令和更新指令。
统称 Ivy 指令集,且就是一堆函数。所以这些东西就可以被摇树。
template() 的具体执行方式可以看源码和 Kara Erickson 的演讲。
里面也涉及到了生命周期函数调用时机。

当然 compiler 变了 runtime 肯定也相应的改变了,但是最后的最后
你会发现还是用了 renderer2 的相关 api

作为一个复习,这部分就先到这里,要不然又花太多时间了。

2020.04.07

References

What is Angular Ivy?
The Theory of Angular Ivy | Alex Rickabaugh | AngularConnect 2018
AngularConnect 2018: Theory of Ivy (Public)
How Angular Works | Kara Erickson

angular-ivy-a-detailed-introduction

  • Angular 内建的模块化机制
  • @NgModule 装饰器内的元数据声明来实现
  • imports
    • 导入其它 module
    • 本 module 就可以使用被导入的 module 所 export 的内容
  • declarations
    • declarable 基本上跟模板显示有关的代码都算 declarable
    • 只能在一处声明
  • exports
    • 可被其他模块使用的 declarable
  • providers
    • Angular 6 之后 Service 可以通过 provideIn 来指定注入范围
  • bootstrap
    • 自动启动
  • entryComponents
    • 放动态加载的组件,防止被树摇掉
    • bootstrap 中的组件也会被自动加入到 entryComponents 中

通过 imports exports,编译器能够确定当前 module 的编译范围。

所以即使项目中有两个相同 selector 的组件,只要导入正确,就能确定使用哪个。

app.component.html:

1
2
3
<comp-root>
<content-child-1></content-child-1>
</comp-root>

comp-root.component.html:

1
2
<view-child-1></view-child-1>
<ng-content></ng-content>
component hook call times
comp-root ngOnChanges 1 time
comp-root ngOnInit 1 time
comp-root ngDoCheck 1 time
content-child-1 ngOnChanges 1 time
content-child-1 ngOnInit 1 time
content-child-1 ngDoCheck 1 time
content-child-1 ngAfterContentInit 1 time
content-child-1 ngAfterContentChecked 1 time
comp-root ngAfterContentInit 1 time
comp-root ngAfterContentChecked 1 time
view-child-1 ngOnChanges 1 time
view-child-1 ngOnInit 1 time
view-child-1 ngDoCheck 1 time
view-child-1 ngAfterContentInit 1 time
view-child-1 ngAfterContentChecked 1 time
view-child-1 ngAfterViewInit 1 time
view-child-1 ngAfterViewChecked 1 time
content-child-1 ngAfterViewInit 1 time
content-child-1 ngAfterViewChecked 1 time
comp-root ngAfterViewInit 1 time
comp-root ngAfterViewChecked 1 time
comp-root ngDoCheck 2 time
content-child-1 ngDoCheck 2 time
content-child-1 ngAfterContentChecked 2 time
comp-root ngAfterContentChecked 2 time
view-child-1 ngDoCheck 2 time
view-child-1 ngAfterContentChecked 2 time
view-child-1 ngAfterViewChecked 2 time
content-child-1 ngAfterViewChecked 2 time
comp-root ngAfterViewChecked 2 time

有意思的是:初始化过程中,额外执行了一次 Change detection

之后分析一波

  1. 减少 http 请求

    • 合并
  2. 使用 CDN

  3. 使用 Expires 头

    • Expires: GMT TIME
    • Cache-Control: max-age=3600
    • 带个 hash 使得文件更新时文件名同时更新
    • 条件 GET 请求
  4. 压缩组件/使用 Gzip

    • Accept-Encoding: gzip, deflate
    • Content-Encoding: gzip
    • 服务器的响应中添加 Vary: Accept-Encoding,使得代理缓存响应的多个版本
    • Vary: * / Cache-Control: private 禁用代理缓存
  5. 将样式表放在顶部

  6. 将脚本放在底部

  7. 避免 css 表达式

  8. 使用外部 Javascript 和 CSS

  9. 减少 DNS 查找

  10. 精简 Javascript

  11. 避免重定向

  12. 删除重复脚本

  13. 配置 ETag

  14. 使 Ajax 可缓存

相当古老的书,都可以追溯到我念书的时候了。

翻看的过程中,有一个很明显的感觉,10 年左右集结起来可以出书的黑科技,现在大部分都变成日常写码的常识,或者不怎么常考的面试题了。

目录如下:

  • 加载执行
  • 数据存取
    • 访问作用域链/原型链慢
  • DOM
    • reflow/ repaint
  • 循环/条件/递归
  • 字符串/正则表达式
    • 没看
  • UI
    • UI 线程
    • 使用定时器让出时间片段
    • web worker
  • Ajax
    • jsonp 是 json with padding 的意思
  • 编程实践
    • 位操作/原生方法

对于现在的性能优化实践,还要去总结一波才能记录。

一个小 tip

写了很久安哥拉,但是动画这部分还真没细看过。前几天 debug 到 primeng 的时候发现 tag 上面有很多 (@animation.start) 之类的事件绑定,脑门嗡的一下。但是他们用了一个我不知道是不是很通用的方式,值得记一下:

  • 通过监听动画开始/结束事件,拿到某个 DOM 元素的引用,进而去操作它

本来因为业务需求写了一点 angular 动画,想记录一下。结果写了千把行字,发现动画是 angular 里面为数不多看文档就完事儿的部分。

所以推荐看看文档即可。

看了一个演讲:Deep Dive into the Angular Compiler | Alex Rickabaugh,记录一下要点:

compiler 做了什么?

  • 把 declarative template 转换成可以执行的 imperative code
  • 前者是「What」 to do,而后者是「How」 to do in runtime

编译很难理解,为什么不自己写命令式代码?

  • 会有很多模板代码
  • Angular 背后做了很多优化,而且在不改变原来代码的前提下,可以应用到最新的优化和成果
    (相当于我们通过接口来使用某个功能,在接口之后的优化可以由 Angular 来完成)

Architecture

Ts 的编译过程
img
Angular 的编译过程
img

  • Program Creation
    • 从 tsconfig.json 开始,通过 import 引用,发现所有需要编译的 .ts 和 .d.ts 文件
    • (ng) 增加一些 shims,比如 .ngfactory 文件
  • Analysis (ng)
    • 拿到所有 ts 代码以后,编译器在这些代码中寻找 Angular 相关的内容
    • 对 component/directive/services/etc 进行「单独的」识别和处理
    • 收集整个结构的信息
  • Resolve (ng)
    • 处理结构和依赖
    • 做一些优化
  • Type Checking
    • 对模板中的表达式进行一些校验
    • 通过生成代码来进行「type check block」类型检查块
  • Emit
    • 为装饰器装饰的 class 生成 angular 代码,并且补上一些他们需要的 imperative code(应该指的是 ngc 编译出来的代码中,那些 ɵ 开头的createNgModuleFactory 之类)
    • THE most expensive phase

Compilation Model (Ivy)

  • .d.ts carrying information about types from one compilation from one to the next

  • ng compiler 把 component 的信息也放进了 .d.ts 中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // lib.ts
    @Component({
    selector: 'userful-cmp',
    template: '...',
    })
    export class UserfulCmp{
    @Input() value: string;
    }
    // lib.d.ts
    export declare class UsefuleCmp{
    value: string;

    static ngComponentDef: ng.ComponentDef<
    UsefulCmp,
    'useful-cmp',
    {value: 'value'},
    ...
    >;
    }
    • 这样后面的 compiler 可以使用这些信息
    • 把组件的公共 api 声明清楚了
  • 以后的 compiler 在编译的时候遇到了这个组件时,就知道它是上面 class 的实例:

    1
    2
    3
    4
    <!-- app.html -->
    <useful-cmp [value]="something">
    I'm Useful!
    </useful-cmp>

Features of the Compiler

NgModule Scopes

Angular 能够确定某个 selector 就是某个组件(而非其他具有相同 selector 的组件)的原因当然是因为 ngModule,所有在 ngModule 的 declarations 数组中声明的组件,在它(这个 ngModule)的 template 中都是可用的。这一堆 components 叫做这个 module 的 compilation scope

当我们把某个组件抽离到它自己的 module 中时,这个组件就同时在 declarations 和 exports 数组中了。在 exports 中的组件,就构成了这个 module 的 export scope。在所有引用了这个 module 的其它 module 中,这个 export scope 中的组件也是可用的,在这些个其它的 module 中,他们的 compilation scope 就由自己的 compilation scope 和导入的 module 的 export scope 构成。

img

Template Scoping

当遇到了 <user-view></user-view> 这样的 selector 的时候,compiler 就知道它只可能是当前编译域中的那个 UserView 组件,而不是其他的同名组件。

Optimize

compiler 可以检查全体 compilation scope 来确定,哪些组件真正被使用了,哪些没有。这些信息可以给 tree-shaker 来移除没用被使用的那些代码。

Partial evaluation

部分求值的这个特性,在类似:

1
2
3
4
5
const COMPONENTS = [FooCmp, BarCmp];
@NgModule({
declarations: [...COMPONENTS],
exports: [...COMPONENTS]
})

这种写法的时候,compiler 需要去求某些表达式的值,或者执行某一行代码(比如 RouterModule.forRoot()

Dynamic Expressions

编译时无法确定的类似 document.body.scrollWidth 的值,compiler 将其转换成了 DynamicValue(document.body.scrollWidth) 的形式

Template Type-Checking

img
通过生成一段用来类型检查的代码,来实现对 template 的类型检查。
包括 *ngFor

之前工作的很好的 primeng modal dialog 突然就不好使了,只有 mask,没有弹窗。

刚开始遇到这个问题的时候,相关的 issue 基本没有。

先看 console,没问题。

看 Elements,好像是 mask 层放在了 dialog 子元素里面,而 container 容器又被 append 到了 body 上。

issue 里面有人提了,有的是不居中,但大部分是弹窗消失。

有人提供了一个绕路方式:手动设置 left/top,基本就能看见了。
但是 mask 还在原处,在某些情况下(事实上是大部分情况下,因为需要用到 appendTo 的 dialog,肯定是 nest 到某些子组件里面去的),mask 是不能覆盖到全屏的。

至于为什么,请看:4 reasons your z-index isn’t working (and how to fix it)

把 primeng 仓库的代码扒下来,看代码。发现 wrapper 这个东西,是某一个人在今年初某次叫做「重写 dialog」的 commit 中修改的。还给了 issue 引用。

去看一下 issue

他说,like we did in PrimeReact and PrimeVue。

好的,去扒下来 primeReact 代码看一眼:

code

这明显是连 mask 带里头的东西都 append 了

提个 PR 完事。

自己动手,丰衣足食。

可以说是升级了一个 catalina 引起的麻烦事,也是日常解决问题的流程:

tldr:电脑升级到了 catalina -> electron-builder 太老不支持 catalina 升级一下 -> dmg 默认不签名 -> 自动升级检测到升级包 -> 下载后通不过验证无法安装 -> 开了一个脑洞解决..


项目 electron-builder 之前的版本是 20.36.2,升级之后的版本是 22.3.2,跨了两个大版本。除了支持 catalina 以外,最一开始我以为没什么跟我有关的改动,直到自动升级时 updater 报了这么一个错:

1
2
3
4
{
"code": -1,
"domain": 'SQRLCodeSignatureErrorDomain'
}

啥玩应啊,怎么又 sql 又 qr 还签名错了。我很懵。

1、强行冷静来分析一下,api 是会返回一个 feed url 来给 electron-updater 来检查更新,下载后根据用户的选择,执行 updater 的重启更新或者下次启动更新。根据检测到更新之后的 log 和网速来看,安装包是下完了,卡在了安装这一步。

打了一点 log 也验证了这个想法。

2、那就只能面向 google debug 了。
搜索 「SQRLCodeSignatureErrorDomain」
获得 5 条结果 (用时 0.33 秒)

基本都是 github 上的结果,分别属于 Squirrel.Macatom,大体看一遍,没啥用。Squirrel 出现的位置是代码,atom 是用户抱怨更新失败。虽然 atom 症状跟我很相似,但是并没有找到什么有价值的问题定位和解决。

MayGo/tockler 这个 issue 下面提到了一个工具:RB App Checker Lite,已经好几年没更新了但还是很有用的样子。

拖下来把打的包都检查一遍,发现… dmg 内的包没有签名?!

3、然后我去了 electron-builder 的 issue 里面搜 ‘updater’,发现主要的讨论还是在 notarize 上面。甚至有人专门搞了一篇文章notarizing-your-electron-application。这个好像.. 可以放到待完善的任务里

4、然后尝试在 electron-builder.json 的顶级 dmg 字段里面强制加上 "sign": true,然后打 log 发现,代码确实执行了,但是包里面的 package 还是没有签名..

5、头大

6、取消了 target 中的 mas 和 mac,只保留 ‘dmg’ 和 ‘zip’

7、竟然输出了签名 package

8、问题暂时解决了

这个东西竟然把我整懵了。
具体来说是这样的:
一开始在 macrotask 阶段,走到执行栈 script main hello 的时候,会把下面整个一坨包到一个 then 里面放到 microtask queue 里面,这时候 nexttick queue 两个任务,microtask queue 一个任务。然后执行栈空,开始这一轮的microtask 阶段。 nexttick queue 会在 microtask queue 之前执行。所以会有 before/after hello 在 hello 之后打印。
之后 microtask queue 会一直执行,接下来执行栈 hello.then world,打印 world,nexttick enqueue 两个任务,在 microtask queue 放一个 world.then,此时 microtask queue 为:| hello.then | world.then 。然后 hello.then 完毕,出队,此时「会在下一轮 macrotask 开始之前清空 microtask 队列」,微任务队列不为空,所以继续拿出 world.then,打印 foo,入队 两个 nexttick 任务,入队一个 foo.then ….

直到 microtask 清空,然后下一个 microtask 轮次。其它一堆 brefore / after 都是在下一轮循环中的 nexttick 阶段打印的。

本文为译文,原文地址

与 Angular 1 相比,Angular变化检测机制更加透明,并且更易于推理。但是,在某些情况下(例如进行性能优化时),我们确实需要了解幕后情况。因此,让我们通过研究以下主题来更深入地研究变化检测:

  • 变化检测是怎么实现的?
  • 变化检测器长什么样,我能看到吗?
  • 默认的变化检测机制如何工作
  • 打开/关闭变化检测,和手动触发变化检测
  • 避免变化检测循环:生产与开发模式
  • 变化检测的OnPush 模式实际上干了什么?
  • 使用 Immutable.js 简化 Angular 应用的构建
  • 结论

如果您需要有关 OnPush 变化检测的更多信息,请查看Angular OnPush变化检测和组件设计-避免常见陷阱

变化检测是怎么实现的?

Angular 可以检测组件数据何时更改,然后自动重新渲染视图以反映该更改。但是,按钮单击之类的事件是非常底层的事件,它们触发后 Angular 是怎么做到自动检测的呢?

要了解它是如何工作的,我们首先需要意识到在设计上,Javascript 的整个运行时是可以被重写覆盖的。如果我们希望,我们可以重写在 StringNumber 中的方法(String.prototype.trim = function(){}之类的)。

覆盖浏览器默认行为

发生的情况是,Angular 在启动时会给一些低级浏览器 API 打补丁,比如 addEventListener,这是用于注册所有浏览器事件(包括 click handlers)的浏览器函数。Angular 会把 addEventListener 替换为一个等效的新版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
// this is the new version of addEventListener
function addEventListener(eventName, callback) {
// call the real addEventListener
callRealAddEventListener(eventName, function() {
// first call the original callback
callback(...);
// and then run Angular-specific functionality
var changed = angular2.runChangeDetection();
if (changed) {
angular2.reRenderUIPart();
}
});
}

新版本的 addEventListener 给事件处理程序(handlers)添加了功能:不仅可以调用已注册的回调,而且 Angular 还可以运行变化检测并更新 UI。

低级的 runtime patching 是怎么工作的?

浏览器 API 的低级修补是由 Angular 附带的名为 Zone.js 的库完成的。了解什么是 zone 很重要。

一个 zone 是一个执行上下文,它可以在多个 Javascript VM 执行回合后幸存下来。这是一种通用机制,我们可以用来向浏览器添加额外的功能。Angular 在内部使用 Zones 来触发变化检测,但是另一种可能的用途是进行应用程序性能分析,或跟踪跨多个 VM 轮次运行的长堆栈跟踪。

支持浏览器异步 API

以下常用的浏览器机制已被 patch,来支持变化检测:

  • 所有浏览器事件(events:click,mouseover,keyup 等)
  • setTimeout() 和 setInterval()
  • Ajax 请求

实际上,Zone.js 还修补了许多其他浏览器 API,以透明地触发 Angular 变化检测,例如 Websockets。看一下 Zone.js 的 test specifications,看看当前支持什么。

这种机制的局限性在于,如果由于某些原因,一个异步浏览器 API 不被 Zone.js 支持,那么将不会触发变化检测。例如,IndexedDB 回调就是这种情况。

这解释了变化检测如何触发,但是一旦触发,它实际上怎么工作?

The change detection tree 变化检测树

每个 Angular component 都有一个关联的变化检测器,该变化检测器在应用程序启动时创建。用下面这个 TodoItem 组件举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component({
selector: 'todo-item',
template: '<span class="todo noselect" (click)="onToggle()">{{todo.owner.firstname}} - {{todo.description}} - completed: {{todo.completed}}</span>'
})
export class TodoItem {
@Input()
todo:Todo;

@Output()
toggle = new EventEmitter<Object>();

onToggle() {
this.toggle.emit(this.todo);
}
}

这个组件接受一个 Todo object 输入,并在 todo 状态改变的时候触发一个事件。为了使示例更有趣,Todo 类包含一个嵌套对象 owner:

1
2
3
4
5
6
7
export class Todo {
constructor(public id: number,
public description: string,
public completed: boolean,
public owner: Owner) {
}
}

我们可以看到 Todo 有一个属性 owner,该属性本身就是一个具有两个属性的对象:first name 和 last name。

Todo Item 的变化检测器是什么样的?

我们实际上可以在运行时看到变化检测器的外观!要查看它,只需在Todo类中添加一些代码即可在访问某些属性时触发断点。

遇到断点时,我们可以遍历堆栈跟踪并查看实际的变化检测:

角度变化检测器是什么样的

不用担心,您将永远不必调试此代码!也不涉及任何魔术,它只是在应用程序启动时构建的普通Javascript方法。但是它是做什么的呢?

默认变化检测机制如何工作?
乍一看,此方法可能看起来很陌生,带有所有名称奇怪的变量。但是,通过更深入地研究它,我们发现它的操作非常简单:对于模板中使用的每个表达式,它会将表达式中使用的属性的当前值与该属性的先前值进行比较。

如果前后的属性值不同,则将其设置isChanged为true,就是这样!差不多了,它是通过使用一种称为的方法比较值的
looseNotIdentical(),这实际上只是===与这种NaN情况下的特殊逻辑的比较(请参见此处)。

那嵌套对象owner呢?
我们可以在变化检测器代码中看到,还
owner检查了嵌套对象的属性是否存在差异。但是只比较firstname属性,不比较lastname属性。

这是因为组件模板中未使用姓氏!同样,出于相同原因,未比较Todo的顶级id属性。

这样,我们可以放心地说:

默认情况下,“角度变化检测”通过检查模板表达式的值是否已更改来工作。这是对所有组件完成的。

我们还可以得出以下结论:

默认情况下,Angular不进行深度对象比较以检测更改,它仅考虑模板使用的属性

为什么默认情况下变化检测如此工作?
Angular的主要目标之一是更加透明和易于使用,以使框架用户不必费劲地调试框架并了解内部机制,从而能够有效地使用它。

如果您熟悉Angular 1,请考虑$digest()以及$apply()何时使用/不使用它们的所有陷阱。Angular的主要目标之一就是避免这种情况。

参照比较怎么样?
事实是Javascript对象是可变的,Angular希望为这些对象提供开箱即用的全面支持。

想象一下,如果Angular默认变化检测机制将基于组件输入的参考比较而不是默认机制,那会是什么?即使是像TODO应用程序这样简单的东西,构建起来也很棘手:开发人员必须非常小心地创建新的Todo,而不是简单地更新属性。

但是正如我们将看到的,如果确实需要,仍然可以自定义Angular变化检测。

性能如何?
注意待办事项列表组件的变化检测器如何显式引用该todos属性。

做到这一点的另一种方法是动态地遍历组件的属性,使代码通用而不是特定于组件。这样,我们就不必在一开始就为每个组件构建一个变化检测器!那么这是什么故事呢?

快速浏览虚拟机
所有这些都与Javascript虚拟机的工作方式有关。用于动态比较属性的代码,尽管VM即时编译器无法轻易地将泛型优化为本机代码。

这与变化检测器的特定代码不同,变化检测器确实显式访问每个组件输入属性。该代码非常类似于我们手工编写的代码,并且很容易被虚拟机转换为本地代码。

使用生成的但显式的检测器的最终结果是一种变化检测机制,该机制非常快(比Angular 1更快),可预测且易于推理。

但是,如果遇到性能极限情况,是否有一种方法可以优化变化检测?

在OnPush变化检测模式
如果Todo列表很大,我们可以将TodoList组件配置为仅在Todo列表更改时才进行更新。这可以通过将组件变化检测策略更新为OnPush:

@ 组件({
选择器:“ todo-list ”,
changeDetection:ChangeDetectionStrategy。OnPush,
模板:…
})
导出 类 TodoList {

}

查看由GitHub托管的原始04.ts
现在让我们向应用程序添加几个按钮:一个按钮通过直接对其进行变异来切换列表的第一项,另一个按钮将Todo添加到整个列表中。代码如下:

@ 组件({
选择器:“ app ”,
模板:<div> <todo-list [todos] =“ todos”> </ todo-list> </ div> <button(click)=“ toggleFirst()”>切换第一项</ button> <按钮(点击)= “addTodo()”>添加到待办事项列表</按钮>
})
出口 类 应用 {
待办事项:Array = initialData ;

构造函数(){
}

toggleFirst(){
    这个。待办事项 [ 0 ]。已完成 =  ! 这个。待办事项 [ 0 ]。完成 ;
}

addTodo(){
    让 newTodos =  this。待办事项。切片(0);
    newTodos。推(新的 Todo(1,“ TODO 4 ”,
        false,新 所有者(“ John ”,“ Doe ”)));
    这个。todos  =  newTodos ;
}

}

查看由GitHub托管的原始05.ts
现在让我们看看两个新按钮的行为:

第一个按钮“切换第一项”不起作用!这是因为该toggleFirst()方法直接更改列表的元素。 TodoList无法检测到此信息,因为其输入参考todos未更改
第二个按钮确实起作用!请注意,该方法addTodo()创建了待办事项列表的副本,然后向该副本添加了一个项目,最后用已复制的列表替换了todos成员变量。这会触发变化检测,因为该组件在其输入中检测到参考更改:它收到了一个新列表!
在第二个按钮中,直接更改待办事项列表将不起作用!我们确实需要一个新列表。
难道OnPush真的只是由基准进行比较的投入?
事实并非如此,如果您尝试通过单击来切换待办事项,它仍然有效!即使您也将TodoItem切换到OnPush。这是因为
OnPush不仅仅检查组件输入中的更改:组件是否发出也会触发变化检测的事件。

根据Victor Savkin在他的博客中的这段话:

使用OnPush检测器时,框架将在其任何输入属性发生更改,何时触发事件或Observable触发事件时检查OnPush组件。

尽管允许更好的性能,但OnPush如果与可变对象一起使用,则使用时会付出很高的复杂性。它可能会引入难以推理和重现的错误。但是有一种方法可以使之OnPush可行。

使用Immutable.js简化Angular应用程序的构建
如果我们仅使用不可变对象和不可变列表构建应用程序,则可以OnPush透明地在任何地方使用它,而不会陷入变化检测错误的风险。这是因为对于不可变对象,修改数据的唯一方法是创建一个新的不可变对象并替换先前的对象。对于不可变的对象,我们可以保证:

一个新的不可变对象将始终触发OnPush变化检测
我们不能通过忘记创建对象的新副本而意外地创建错误,因为修改数据的唯一方法是创建新对象
进行不可变的一个不错的选择是使用Immutable.js库。该库提供了用于构建应用程序的不可变基元,例如不可变对象(Map)和不可变列表。

该库也可以以类型安全的方式使用,请查看此先前文章以获取有关如何执行此操作的示例。

避免变化检测循环:生产与开发模式
Angular变化检测的重要属性之一是,它与Angular 1不同,它强制执行单向数据流:当更新控制器类上的数据时,变化检测将运行并更新视图。

但是视图的更新本身不会触发进一步的更改,而这些更改又会触发视图的进一步更新,从而在Angular 1中创建了称为摘要循环的内容。

如何在Angular中触发变化检测循环?
一种方法是使用生命周期回调。例如,在TodoList组件中,我们可以触发对另一个组件的回调,从而更改其中一个绑定:

ngAfterViewChecked(){
如果(这个。回调 && 此。点击){
控制台。日志(“状态更改中… ”);
这个。回调(数学。随机());
}
}

查看由GitHub托管的原始06.ts
错误消息将显示在控制台中:

EXCEPTION: Expression ‘ in App@3:20’ has changed after it was checked
仅当我们在开发模式下运行Angular时,才会引发此错误消息。如果启用生产模式会怎样?

enableProdMode();

@ NgModule({
声明:[ App ],
导入:[ BrowserModule ],
引导程序:[ App ]
})
导出 类 AppModule {}

platformBrowserDynamic()。bootstrapModule(AppModule);

查看由GitHub托管的原始07.ts
在生产模式下,不会引发该错误,并且仍然不会发现问题。

变化检测问题是否经常发生?
我们确实必须竭尽全力触发变化检测循环,但以防万一在开发阶段最好始终使用开发模式,因为这样可以避免问题。

这种保证是以Angular始终两次运行变化检测为代价的,第二次是检测此类情况。在生产模式下,变化检测仅运行一次。

打开/关闭变化检测并手动触发
在某些特殊情况下,我们确实希望关闭变化检测。想象一下一种情况,其中大量数据通过Websocket从后端到达。我们可能只想每5秒更新一次UI的特定部分。为此,我们首先将变化检测器注入组件中:

构造函数(私有 ref:ChangeDetectorRef){
参考。分离();
setInterval(()=> {
这个。参考。detectChanges();
},5000);
}

查看由GitHub托管的原始08.ts
如我们所见,我们只需拆下变化检测器,即可有效地关闭变化检测。然后,我们只需每5秒手动调用一次即可触发它detectChanges()。

结论
Angular 默认变化检测机制实际上与Angular 1类似:它比较浏览器事件之前和之后的模板表达式的值,以查看是否有所更改。它适用于所有组件。但是也有一些重要的区别:

对于一个组件,没有变化检测循环,也没有像Angular 1中命名的那样的摘要循环。这允许仅通过查看其模板及其控制器来推断每个组件。

另一个区别是,由于构建了变化检测器的方式,检测组件变化的机制要快得多。

最后,与 Angular 1 不同,变化检测机制是可自定义的。

我们真的需要对变化检测了解很多吗?
大概对于95%的应用程序和用例来说,可以肯定地说Angular Change Detection可以正常工作,我们不需要了解太多。出于以下几个原因,了解其工作原理仍然很有用:

一方面,它有助于我们理解一些可能遇到的开发错误消息,例如与变化检测循环有关的消息。
它有助于我们读取错误堆栈跟踪,所有这些zone.afterTurnDone()突然看起来都更加清晰
在性能非常高的情况下(我们确定我们不应该只是将分页添加到该庞大的数据表中吗?),了解变化检测可以帮助我们进行性能优化。
请看下面的参考资料,以进一步了解角度变化检测。

reactive 表单就不说了,validator 直接就有。问题是 legacy 项目里面的表单都是 template driven 表单,一开始文档说得很好:

To add validation to a template-driven form, you add the same validation attributes as you would with native HTML form validation.

结果完全没用,SO 上面有说估计写校验器的哥们写到 min/max 的时候烦了不想写了,另外有清楚事情原委的人解释说,一开始是加了的,但是影响到了很多已经在自己表单里面加了 min/max ,但是又不想被校验的人,所以又回滚了。

这事儿发生在 2017 年,然后这个 issue 到今天还开着:

issue 16352

在 reactive 表单中,因为可以访问到 formControl 实例,可以只写一个 validatorFn。但在 template driven 表单里面,就只能写个 customMin/customMax 指令来搞了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Directive, Input } from '@angular/core';
import { NG_VALIDATORS, Validator, FormControl } from '@angular/forms';

@Directive({
selector: '[customMax][formControlName],[customMax][formControl],[customMax][ngModel]',
providers: [{ provide: NG_VALIDATORS, useExisting: CustomMaxDirective, multi: true }],
})
export class CustomMaxDirective implements Validator {
@Input()
customMax: number;

validate(c: FormControl): { [key: string]: any } {
return c.value > this.customMax ? { customMin: true } : null;
}
}

关于这个话题,我的理解过程是从后往前开始的,js 语法,setTimeout,异步?,callback,Promise,事件循环?一步一步越来越懵逼。

但是不管顺着哪个路线往上摸,到最后都是落到两个字:规范。

那么尝试一下从前往后总结一下自己的理解,略去细节,以后方便面试什么的回忆。

HTML Standard 和 ECMAScript® Language Specification

要说异步编程这个事情,就要说运行环境(先不说 node),再说运行环境之前,我考虑了好几个切入点,结果一个问题引出另外一个问题,结果就变成了从下面这个问题开始

啥是异步?

Asynchrony, in computer programming, refers to the occurrence of events independent of the main program flow and ways to deal with such events.

事情发生的顺序,跟你写代码的顺序是独立的,就异步了。

一个传统例子:假如现在要实现这么个东西:

用户点击一个按钮,就请求一个网络图片(挺大,要 1s 来传输)并把它展示到页面上。

同步就是:点了按钮,页面就卡死,1 秒以后显示一个图片。

异步就是:点了按钮,页面没卡,还能点别的(事件发生的顺序跟代码不一样了),1 秒左右以后,显示这个图片。

JS 是异步的吗?

不是,语言本身就异步的,基本没有,包括 JS,C,C++,Java,PHP,Python 都没这个能力。

那面经里面又异步又编程说的是啥?

是 JS 的运行环境(浏览器、node)暴露出来一些 api,让程序通过调用这些 api 来拥有「异步的行为」。

ES6 之前的 ES5,通篇没有 timer、queue 这种东西。在 ES6 才对 Promise resolve 之后的回调函数怎么执行,来规定了一个 jobQueue。

所以说其实前端是啥啊,面向浏览器编程

setTimeout()setInterval()setImmediate()process.nextTick()requestAnimationFrame()queueMicroTask()MutationObserverPromise
这些,ES 规范里面一个都没有,只有 Promise。所以从语言层面去理解,就会像我一样一脸懵逼。
其它的会提到的还有I/OUI renderingscript

换句话说,除非你在代码中调用了这些 api,你的代码都会是同步的。

浏览器怎么调度这些任务产生异步行为,才是问题。

三个概念:执行栈,任务队列,微任务队列。此处请看动画 step by step 演示 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

执行栈:一个存储函数调用的栈结构,如果当前执行的函数只有一个 a,执行栈就是 | a |,调用了另外的函数 b,那么就会变成 | a | b |,b 执行完毕之后,出栈,就会变成 | a |,a 也执行完毕,调用栈就空了。

对于要稍后执行的任务(比如 setTimeout 内的一段回调函数),浏览器会把它放到任务队列中,既然是队列就是加到最后。

对于希望稍后尽快执行的任务,(比如 Promise 的 then),就把它放到微任务队列中。

如果 a 的执行过程中,遇到了需要调度的异步 api 调用(往往是耗时的操作,压到栈里继续执行就会阻塞了),就不是压到栈里了,而是根据他们的类型,放到相应的队列里面去(以后再执行)。

「以后」是什么时候呢?

调用栈空了以后,因为这个往往意味着执行完一波操作了。就像你吃完饭了,可以去干别的了。

注意,这里要提到一点,就是「吃饭」这个行为本身,就是事件循环的一节,也就是说,是一个宏任务。一个宏任务完成后,调用栈空了,这时候就去看一看微任务队列中,有没有需要执行的微任务。比如说吃饭时候接到两个电话,一个改 bug,我们放到宏任务队列中,一个给女朋友回电话,我们放到微任务队列中。

调用栈的行为就像是吃饭过程中,需要热一下凉掉的饭菜,它是吃饭调用的另外一个函数,「热饭」就被压入栈,热完饭继续吃,就是出栈,吃饭过程中接到两个电话,就是调度任务队列,吃完饭,这个调用栈就空了。

空了我们要去看一下这一轮事件循环的下一步,微任务队列。里面有一个任务,给女友回电话。

我们把它拿出来,放到执行栈,开始打电话。

打电话过程中,女友提醒你把家里猫屎铲了。这个似乎也比较紧急,我们就把它放到微任务队列中去。

这里要注意在队列中的任务完毕以后,它才会从队列的首部移除。那么也就是说,我们在执行打电话这个微任务的时候,「铲屎」入队,这时候微任务队列中有两个微任务了。

因为「微任务」紧急嘛,所以挂了电话,我们不会去修 bug,而是继续去铲屎。在铲完屎以后,才会开始下一个事件循环,去修 bug。

分一下类

搞清楚事情的先后顺序之后,对于浏览器中的分类记一下就可以了:

微任务包括 process.nextTick ,promise ,MutationObserver,其中 process.nextTick 为 Node 独有。还有 queueMicroTask

宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。

es8 新加的 async 和 await ??

问题

1
2
3
4
5
6
7
❯ ng serve
Your global Angular CLI version (8.3.17) is greater than your local
version (6.2.9). The local Angular CLI version is used.

To disable this warning use "ng config -g cli.warnings.versionMismatch false".
Cannot destructure property `createHash` of 'undefined' or 'null'.
TypeError: Cannot destructure property `createHash` of 'undefined' or 'null'.

解决 issue

1
npm install --save-dev @angular-devkit/build-angular@latest

看到一篇文章Arrays, symbols, and realms,简单搜了一下,关于判断数组的文章基本上从 typeofinstanceofObject.prototype.toString.call(obj) === '[objectg Array]''讲到 isArray就完了。但是它实际上干了什么?

正文

在推上,一个大佬问兄弟们,大家知不知道 Array.isArray(obj) 干了什么,结果显示,并不知道。更重要的是,本大佬也弄错了。

检查 arrays 的类型

1
2
3
4
5
6
7
8
9
10
function foo(obj){
// ...
}
```
假如说我们需要针对 `obj` 是数组的情况,来专门做一些操作,比如 `JSON.stringify`,它对于数组的输出,是跟其他对象不同的。

我们可以这样:

```javascript
if (obj.constructor === Array) // ...

但对于继承于 Array 的数据来说,就不行了:

1
2
3
4
class SpecialArray extends Array {}
const specialArray = new SpecialArray();
console.log(specialArray.constructor === Array); // false
console.log(specialArray.constructor === SpecialArray); // true

针对子类我们又可以用 instanceof

1
2
console.log(specialArray instanceof Array); // true
console.log(specialArray instanceof SpecialArray); // true

但是当你遇到多个领域的时候就不好使了:

多个领域

一个领域包含了 JavaScript 的全局对象,self 指向的那个。可以这样讲:跑在 worker 里面的代码,和跑在页面里面的代码,是属于不同领域的。iframe 之间也是这样(属于不同领域)。但是,同源的 iframe 们共享一个 ECMAScript agent[啥意思?],意味着对象这个东西可以在不同领域之间传递

1
2
3
4
5
6
7
<iframe srcdoc="<script>var arr = [];</script>"></iframe>
<script>
const iframe = document.querySelector('iframe');
const arr = iframe.contentWindow.arr;
console.log(arr.constructor === Array); // false
console.log(arr.constructor instanceof Array); // false
</script>

都会返回 false,因为:

1
console.log(Array === iframe.contentWindow.Array); // false

iframe 会有各自不同的 array 构造器…

接下来说 Array.isArray

1
console.log(Array.isArray(arr)); // true

Array.isArray 对在另一个领域里面创建的数组也会返回 true。它对Array 的子类也会返回 trueJSON.stringify 内部用的就是它。

但是,就像推上那位大佬说的,这不代表 arr 本身具有数组的方法。所有的方法们可能都被设成了 undefined,甚至 arrprototype 可能都没了。

1
2
3
4
5
const noProtoArray = [];
Object.setPrototypeOf(noProtoArray, null);
console.log(noProtoArray.map); // undefined
console.log(noProtoArray instanceof Array); // false
console.log(Array.isArray(noProtoArray)); // true

下面是你遇见这种数组了怎么办..

1
2
3
4
if (Array.isArray(noProtoArray)) {
const mappedArray = Array.prototype.map.call(noProtoArray, callback);
// …
}

Symbol 和 realm (领域)

看看这段代码:

1
2
3
4
5
6
7
8
9
<iframe srcdoc="<script>var arr = [1, 2, 3];</script>"></iframe>
<script>
const iframe = document.querySelector('iframe');
const arr = iframe.contentWindow.arr;

for (const item of arr) {
console.log(item);
}
</script>

地球人都知道会 log 出来 1,2,3。但这说明了啥?
for-of 循环内部使用的是 arr[Symbol.iterator],这是跨领域之后还可以通用的。看:

1
2
3
4
const iframe = document.querySelector('iframe');
const iframeWindow = iframe.contentWindow;
console.log(Symbol === iframeWindow.Symbol); // false
console.log(Symbol.iterator === iframeWindow.Symbol.iterator); // true

每个领域有它自己的 Symbol 实例的同时,他们的 Symbol.iterator 却是一样的。

引用一句Symbol 同时是 JavaScript 里最特殊和最不特殊的东西了。

最特殊

1
2
3
4
5
6
7
const symbolOne = Symbol('foo');
const symbolTwo = Symbol('foo');
console.log(symbolOne === symbolTwo); // false
const obj = {};
obj[symbolOne] = 'hello';
console.log(obj[symbolTwo]); // undefined
console.log(obj[symbolOne]); // 'hello'

传给 Symbol 构造器的字符串只是对它的一个描述,同一个领域下面同一个描述的 symbol 也是独特的。

最不特殊

1
2
3
4
5
6
const symbolOne = Symbol.for('foo');
const symbolTwo = Symbol.for('foo');
console.log(symbolOne === symbolTwo); // true
const obj = {};
obj[symbolOne] = 'hello';
console.log(obj[symbolTwo]); // 'hello'

Symbol.for(str) 新建了一个根据你字符串唯一的 symbol 对象。重点是,跨了领域之后,它还是一样的。。

1
2
3
const iframe = document.querySelector('iframe');
const iframeWindow = iframe.contentWindow;
console.log(Symbol.for('foo') === iframeWindow.Symbol.for('foo')); // true

所以这个也就基本上是 Symbol.iterator 为啥能行的原因了。

根据传统,自己手写个 is 函数

那么 symbol 对象可以让我们写出跨领域能用 is 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const typeSymbol = Symbol.for('whatever-type-symbol');

class Whatever {
static isWhatever(obj) {
return obj && Boolean(obj[typeSymbol]);
}
constructor() {
this[typeSymbol] = true;
}
// 或者下面这种不会被遍历出来
get [typeSymbol]() {
return true;
}
}

const whatever = new Whatever();
Whatever.isWhatever(whatever); // true

这样唯一的一点不太好的地方,就是命名冲突了。别人也这么搞,而且还同名的话,就有问题了。

继承这个东西都快被嚼烂了。但是没办法,不复习,还是会忘掉。

new 的过程中发生了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(age, name){
let count = 0;
this.age = age;
this.name = name;
this.getCount = function(){
count++;
return count;
}
}

let laowang = new Person(32, 'laowang');
laowang.__proto__ === Person.prototype; // true
laowang instanceof Person; // true

let laozhang = {};
Person.call(laozhang, 33, 'laozhang');
laozhang; // {age: 33, name: 'laozhang'}
laozhang instanceof Person; // false
laozhang.__proto__ = Person.prototype;
laozhang instanceof Person; // true;

接下来再看一段:

1
2
3
laowang.__proto__ === Person.prototype; // true

Person.prototype.__proto === Object.prototype; // true

继承

(摘抄一波 MDN 文档,写的太好了)

当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

基于原型链的继承

继承属性

JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

注:原型链,是对象构成的链

看一段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let f = function(){
this.a = 1;
this.b = 2;
}

let o = new f(); // { a: 1, b: 2 }

f.prototype.b = 3;
f.prototype.c = 4;

// {a:1, b:2} ---> {b:3, c:4} ---> Object.prototype---> null

console.log(o.a); // 1
console.log(o.b); // 2
console.log(o.c); // 4

继承方法

JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。

当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var o = {
a: 2,
m: function(){
return this.a + 1;
}
};

console.log(o.m()); // 3
// 当调用 o.m 时,'this' 指向了 o.

var p = Object.create(o);
// p是一个继承自 o 的对象

p.a = 4; // 创建 p 的自身属性 'a'
console.log(p.m()); // 5
// 调用 p.m 时,'this' 指向了 p
// 又因为 p 继承了 o 的 m 函数
// 所以,此时的 'this.a' 即 p.a,就是 p 的自身属性 'a'
  • Object.create(obj) 返回一个以 obj 为原型的对象…

到现在为止,搞清楚了的是:在 JavaScript 中,一个对象如何访问到自己没有的属性/方法,通过基于原型链的继承。

总结一下

  1. 在 JavaScript 中,函数(function)是允许拥有属性的。所有的函数会有一个特别的属性 —— prototype

  2. 当试图访问一个函数 someFunction 的实例对象 someInstance 的一个属性:

    • 浏览器(或其他的执行环境)首先会查看 someInstance 中是否存在这个属性。

    • 如果不包含这个属性,那么会在 someInstance__proto__ / [[Prototype]] / constructor.prototype / someFunction.prototype(这几个东西等价)中去查找

    • 如果还没有,那么会在 someInstance__proto____proto__ 中查找。

    • (默认情况下)任何函数的原型属性都是 Object.prototype

    • 那么下面几个东西等价

    • someInstance.__proto__.__proto__

    • someFunction.prototype.__proto__

    • Object.prototype

    • 原型链的终点是 Object.prototype.__proto__,它指向 null

    • __proto__ 的整个原型链被查看之后,这里没有更多的 __proto__,浏览器断定这个属性不存在,就会给出这个属性值为 undefined 的结论


拓展一下

  • 在 ES5 中引入了一个新方法 Object.create(o),返回的对象的原型就是 o

  • 在 ES6 中引入了一套关键字:class, constructorstaticextendssuper 来实现 class,但实际上仍然是基于原型链的。(语法糖)

  • 遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。

    • hasOwnProperty 和 Object.keys() 是 JavaScript 中两个处理属性并且不会遍历原型链的方法。
      1
      2
      3
      4
      5
      for(var property in b){
      if(b.hasOwnProperty(property)){
      console.log(pro);
      }
      }

再总结一下

  • 我们需要牢记两点:1、proto和constructor属性是对象所独有的;2、 prototype属性是函数所独有的,因为函数也是一种对象,所以函数也拥有proto和constructor属性。
  • proto属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的proto属性所指向的那个对象里找,一直找,直到proto属性的终点null,再往上找就相当于在null上取值,会报错。通过proto属性将对象连接起来的这条链路即我们所谓的原型链。
  • prototype属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即f1.proto === Foo.prototype。
  • constructor属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向Function。

再拓展一下

  • 不是所有 function object 都有 prototype 属性:

    来自ECMA-262规范

    Function instances that can be used as a constructor have a prototype property. Whenever such a function instance is created another ordinary object is also created and is the initial value of the function’s prototype property. Unless otherwise specified, the value of the prototype property is used to initialize the [[Prototype]] internal slot of the object created when that function is invoked as a constructor.

    NOTE

    Function objects created using Function.prototype.bind, or by evaluating a MethodDefinition (that are not a GeneratorMethod) or an ArrowFunction grammar production do not have a prototype property.

  • 可以当成构造函数用的 function,才有 prototype,用来给实例化的对象的 __proto__ 赋值。那么没打算当构造函数的,就没有了。比如:

    • bind 创建的函数

    • 很多内置函数

      1
      2
      Array.prototype.fill.prototype                 // undefined
      Object.prototype.hasOwnProperty.prototype //undefined
    • 箭头函数

  • 反过来对象的话,没有 __proto__ 的除了 null 还有什么? [TDB]

    • 好像只有 Object.create(null) ?
    • Object.create(obj, { age : { value: 42, writable:true, configurable:true, enumerable:true}})
  • __proto__ 是一个存在于 Object.prototype 上设置了 getter 和 setter 的属性。所以说法:__proto__ 是获取 [[Prototype]] 的方式更 OK。其实 __proto__ 本身不是 [[Prototype]]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    Object.prototype
    constructor: ƒ Object()
    hasOwnProperty: ƒ hasOwnProperty()
    isPrototypeOf: ƒ isPrototypeOf()
    propertyIsEnumerable: ƒ propertyIsEnumerable()
    toLocaleString: ƒ toLocaleString()
    toString: ƒ toString()
    valueOf: ƒ valueOf()
    __defineGetter__: ƒ __defineGetter__()
    __defineSetter__: ƒ __defineSetter__()
    __lookupGetter__: ƒ __lookupGetter__()
    __lookupSetter__: ƒ __lookupSetter__()
    get __proto__: ƒ __proto__()
    set __proto__: ƒ __proto__()
  • 再去找的话,还有很多特例
    • 比如 Symbol,有构造函数,但是不能 new
    • 比如 Generator Function,有 prototype,但不是构造函数
    • Proxy [TBD] BigInt [TBD]

20191120 补充:如何实现继承

ES5

###组合继承

就是属性在父类构造函数中定义,方法在父类构造函数的原型上定义。

然后子类构造函数中 Parent.call(this,value) 来继承父类的属性

Child.prototype = new Parent() 来继承父类的方法

缺点是 new 的时候,Child 的 prototype 上面多了无用的父类属性

组合寄生

直接用 ES5 最正确的方式来做继承
如此没有其它的东西,且有正确的 constructor 指向

1
2
3
4
5
6
7
8
Child.prototype = Object.create(Parent.prototype,{
constructor:{
value: Child,
enumerable: false,
configurable: true,
writable: true
}
})

ES6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Parent {
constructor(value){
this.val = value;
}
getValue(){
return this.val;
}
}

class Child extends Parent{
constructor(value){
super(value)
}
}

在之前家里的五年旗舰不如狗上已经搞过一次,但是没有详细记录。导致了今天的杯具。

  • 要一步一步严格按照文档来,installation
  • 比如 vs 2019 在现在(2019年11月)是怎么都不行的
  • 如果 vs 版本对了的话,只跟着文档安装 c++ workload,在编译时候会报错缺少一个 v140 工具集
  • 我们去 vs 里面装好这个依赖
  • 再次 install / install-app-deps
  • 它就会自己帮你把相应的二进制文件编译好

一些重要的参考链接:

https://electronjs.org/docs/tutorial/using-native-node-modules

https://github.com/nodejs/node-gyp#readme

https://www.jianshu.com/p/513fc8e6f243

https://newsn.net/say/electron-gyp-param.html

script
1
2
node-gyp configure --module_name=node_sqlite3 --module_path=../lib/binding/electron-v7.0-win32-x64
node-gyp rebuild --target=7.0.0 --arch=x64 --target_plathform=win32 --dist-url=https://atom.io/download/atom-shell --module_name=node_sqlite3 --module_path=../lib/binding/electron-v7.0-win-32-x64
  • --target Electron 的版本
  • --arch 系统架构 ia32 或者 x64
  • --dist-url 编译头文件的地址

升级 React Native 历来都非常痛苦,好在比起一年前,现在有了 rn-diff-purge 可以稍微减少一点 debug 的工作量。
我们的项目一直停留在 0.55,在今年年初的时候我开了一个分支,将它升级到了 0.58,但一直没有合并回开发分支。现在 google play 强行要求上传的 app 兼容 64 位,再次升级一波到 0.59.7。

后续应该会遇到很多 lib 不兼容的情况需要升级。慢慢筛查吧。

今天被问到了 fetch 的问题,一时间有点懵逼,这里整理一下。

首先好习惯,先上 MDN 文档:

使用中的主要区别:

请注意,fetch规范与jQuery.ajax()主要有两种方式的不同,牢记:

当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。

默认情况下,fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项)。自从2017年8月25日后,默认的credentials政策变更为same-originFirefox也在61.0b13中改变默认值

其实还是 axios 大法好啊。

公司 Electron 项目因为 custom 了自动升级之后的动作,所以需要获取 autoUpdater 的下载进度事件,来确定下载完成。

最近总是有个莫名其妙的问题,在测试环境下,偶尔会正常弹出应该弹出来的对话框,其他时候不管怎么样都不会触发下载事件。但手动重启应用,版本又确实升级了。。

搞了一晚上的结果:

  • electron-updater 的下载缓存位于 ~/Library/Application Support/Caches/你的应用-updater/ 下面
  • 当已从低版本向某个特定的高版本升级过,缓存中存在这个版本的升级包(macOS 默认为 zip),下一次再次向这个版本升级时候,就不会再次进行下载
  • download-progress 不会触发
  • 似乎 update-downloaded 会触发,待验证

临时的 workaround 就是删一波缓存就行了

“Side Effect” is not a react-specific term. It is a general concept about behaviours of functions. A function is said to have side effect if it trys to modify anything outside its body. For example, if it modidifies a global variable, then it is a side effect. If it makes a network call, it is a side effect as well.
source

A “side effect” is anything that affects something outside the scope of the function being executed. These can be, say, a network request, which has your code communicating with a third party (and thus making the request, causing logs to be recorded, caches to be saved or updated, all sorts of effects that are outside the function.

There are more subtle side effects, too. Changing the value of a closure-scoped variable is a side effect. Pushing a new item onto an array that was passed in as an argument is a side effect. Functions that execute without side effects are called “pure” functions: they take in arguments, and they return values. Nothing else happens upon executing the function. This makes the easy to test, simple to reason about, and functions that meet this description have all sorts of useful properties when it comes to optimization or refactoring.

Pure functions are deterministic (meaning that, given an input, they always return the same output), but that doesn’t mean that all impure functions have side effects. Generating a random value within a function makes it impure, but isn’t a side effect, for example. React is all about pure functions, and asks that you keep several lifecycle methods pure, so these are good questions to be asking.

最近给项目升级sdk版本:

compileSdkVersion:26 -> 28

targetSdkVersion:23 -> 28

buildToolsVersion:”25.0.2” -> “28.0.3”

support包:”24.2.1” -> “28.0.0”

遇到一些问题,在此记录一下解决办法:

一、编译报错:junit.framework.Assert不存在
项目中有个地方用到了Assert类(使用Assert.assertTrue()),原本的导包是:

import junit.framework.Assert;
但编译时报错:junit.framework.Assert不存在。

原因:Assert类在新版本中从junit.framework中移除,移到org.junit中。

解决办法:

(1)、找到Androidstudio目录,将.\gradle\gradle-4.4\lib\plugins目录下的junit-4.12jar,拷贝到工程app目录下的libs文件夹中,并引入,即在build.gradle中添加一行:compile files(“libs/junit-4.12.jar”),如:

dependencies {

compile files(‘libs/junit-4.12.jar’)
}
(2)、将导包 import junit.framework.Assert 替换为 org.junit.Assert;

二、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:lintVitalRelease'.
> Could not resolve all files for configuration ':app:lintClassPath'.
> Could not resolve org.jvnet.staxex:stax-ex:1.7.7.
Required by:
project :app > com.android.tools.lint:lint-gradle:26.2.1 > com.android.tools:sdk-common:26.2.1 > com.android.tools:sdklib:26.2.1 > com.android.tools:repository:26.2.1 > org.glassfish.jaxb:jaxb-runtime:2.2.11
> Could not resolve org.jvnet.staxex:stax-ex:1.7.7.
> Could not get resource 'https://jitpack.io/org/jvnet/staxex/stax-ex/1.7.7/stax-ex-1.7.7.jar'.
> Could not HEAD 'https://jitpack.io/org/jvnet/staxex/stax-ex/1.7.7/stax-ex-1.7.7.jar'. Received status code 522 from server: Origin Connection Time-out
> Could not resolve com.sun.xml.fastinfoset:FastInfoset:1.2.13.
Required by:
project :app > com.android.tools.lint:lint-gradle:26.2.1 > com.android.tools:sdk-common:26.2.1 > com.android.tools:sdklib:26.2.1 > com.android.tools:repository:26.2.1 > org.glassfish.jaxb:jaxb-runtime:2.2.11
> Could not resolve com.sun.xml.fastinfoset:FastInfoset:1.2.13.
> Could not get resource 'https://jitpack.io/com/sun/xml/fastinfoset/FastInfoset/1.2.13/FastInfoset-1.2.13.jar'.
> Could not HEAD 'https://jitpack.io/com/sun/xml/fastinfoset/FastInfoset/1.2.13/FastInfoset-1.2.13.jar'. Received status code 522 from server: Origin Connection Time-out

解决方式:issue

加上 www

1
maven { url "https://www.jitpack.io" }

项目内签名(命令行打包需要)
https://github.com/facebook/react-native/issues/16854

当使用JavaScript编写网页代码时,有很多API可以使用。

日常编写代码的过程中,我们已经见过很多诸如 DocumentWindow 这样通用的对象,本文将介绍同样司空见惯的 Console 对象的几个不常见的用法。

全部的 Web API 接口可以在 这里 查看。
Console对象的全部接口可以在 这里 查看。

简介

console 对象提供对浏览器控制台的接入。不同浏览器上它的工作方式是不一样的,但这里会介绍一些大都会提供的接口特性。
Console对象可以在任何全局对象中访问,它被浏览器定义为 Window.console,也可被简单的 console 调用。例如:

1
console.log("some coolcode message!")

方法

常用的 console.log() 这里就不赘述了。当我们需要对某些比较长的对象数组进行调试,console.log可能就不那么直观,这里就是 console.table 起作用的地方了。

console.table()

可以将数据以表格的形式显示。

这个方法接收一个强制的参数,它必须是一个数组或者是一个对象,还可以接受一个额外的参数描述表格的列数。

它把数据以table的形式打印出来, 在数组中的每一个元素(或对象中可枚举的属性)将会以行的形式显示在table中。

table的第一列是index。如果数据是一个数组,那么值就是索引。 如果数据是一个对象,那么它的值就是属性名称。 console.table 具体能展示的数据长度根据浏览器有所不同,不过一般来讲够用了。

几个例子,请自行在 console 中尝试

1
2
3
// 打印一个由字符串组成的数组

console.table(["apples", "oranges", "bananas"]);
1
2
3
4
5
6
7
8
9
10
// 打印一个属性值是字符串的对象

function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

var me = new Person("John", "Smith");

console.table(me);
1
2
3
4
// 二元数组的打印

var people = [["John", "Smith"], ["Jane", "Doe"], ["Emily", "Jones"]]
console.table(people);
1
2
3
4
5
6
7
8
9
10
11
12
// 打印一个包含对象的数组

function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

var john = new Person("John", "Smith");
var jane = new Person("Jane", "Doe");
var emily = new Person("Emily", "Jones");

console.table([john, jane, emily]);
1
2
3
4
5
6
7
8
9
// 打印属性名是对象的对象

var family = {};

family.mother = new Person("Jane", "Smith");
family.father = new Person("John", "Smith");
family.daughter = new Person("Emily", "Smith");

console.table(family);
1
2
3
4
5
6
7
8
9
10
11
12
// 一个对象数组,只打印 firstName

function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

var john = new Person("John", "Smith");
var jane = new Person("Jane", "Doe");
var emily = new Person("Emily", "Jones");

console.table([john, jane, emily], ["firstName"]);

注意:可以点击每一列的第一行(表头),来对输出的表进行排序

一句话介绍

Fractal output complex, flexible, ajax/restful data structures

Fractal 为复杂的数据输出提供了样式和转化层。

最简单的例子

正常情况下,下面的代码是分散在项目的各处的

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<?php
use League\Fractal\Manager;
use League\Fractal\Resource\Collection;

// 创建一个 Manager 实例
$fractal = new Manager();

// 从某个地方获取到需要的数据,比如说 User::all()
$books = [
[
'id' => '1',
'title' => 'Hogfather',
'yr' => '1998',
'author_name' => 'Philip K Dick',
'author_email' => 'philip@example.org',
],
[
'id' => '2',
'title' => 'Game Of Kill Everyone',
'yr' => '2014',
'author_name' => 'George R. R. Satan',
'author_email' => 'george@example.org',
]
];

// 把得到的数据,传给一个 Collection。一起传入的还有一个 Transformer,可以是一个回调函数或者一个 Transformer 对象。
$resource = new Collection($books, function(array $book) {
return [
'id' => (int) $book['id'],
'title' => $book['title'],
'year' => (int) $book['yr'],
'author' => [
'name' => $book['author_name'],
'email' => $book['author_email'],
],
'links' => [
[
'rel' => 'self',
'uri' => '/books/'.$book['id'],
]
]
];
});

// 然后用 Manager 实例来转化数据
$array = $fractal->createData($resource)->toArray();

// Turn all of that into a JSON string
echo $fractal->createData($resource)->toJson();

// 输出: {"data":[{"id":1,"title":"Hogfather","year":1998,"author":{"name":"Philip K Dick","email":"philip@example.org"}},{"id":2,"title":"Game Of Kill Everyone","year":2014,"author":{"name":"George R. R. Satan","email":"george@example.org"}}]}

几个概念

Resources 资源

Resource 是一个对象,代表了数据,并且知道数据对应的“转化器”是什么。
有两种 Resources

  1. League\Fractal\Resource\Item - 单个资源
  2. League\Fractal\Resource\Collection - 资源的集合
    它们的构造函数接受实际操作的“数据”作为第一个参数,接受“转化器”作为第二个参数。

Serializers 序列化器

一个序列化器会以某种方式组织你的转化过的数据。Fractal 提供了 DataArraySerializerArraySerializerJsonApiSerializer,也可以自己来写。
比较常用的是 DataArraySerializer,但是它会给数据增加一个 data 的命名空间,如果不喜欢,可以使用其他类型,或者自己实现一个。

Transformers 转化器

这个是最常用的一个内容。

转化器类

必须继承 League\Fractal\TransformerAbstract,并且包含一个 transform() 方法。这个方法相当于转化的格式,具体可以参考上面例子里面的回调函数。

Including Data 包含数据

比如我们的 Book 模型是有一些关系的,比如说 作者 Author,出版社 Press。
在模型里我们定义好相关的关系之后,在转化器类里面可以这样来包含相关的(转化好的)数据。

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
37
<?php namespace App\Transformer;

use Acme\Model\Book;
use League\Fractal\TransformerAbstract;

class BookTransformer extends TransformerAbstract
{
protected $defaultIncludes = [
'author'
];

protected $availableIncludes = [
'author'
];

public function transform(Book $book)
{
return [
'id' => (int) $book->id,
'title' => $book->title,
'year' => (int) $book->yr,
'links' => [
[
'rel' => 'self',
'uri' => '/books/'.$book->id,
]
],
];
}

public function includeAuthor(Book $book)
{
$author = $book->author;

return $this->item($author, new AuthorTransformer);
}
}

里面有两种包含,默认的总是会自动获取对应关系的数据,比如 user 的返回数据里面会给每一个 user 带上对应的 user_info。
可用的包含则必须要调用 parseIncludes 方法:

1
2
3
4
5
6
7
8
<?php
use League\Fractal;

$fractal = new Fractal\Manager();

if (isset($_GET['include'])) {
$fractal->parseIncludes($_GET['include']);
}

否则不会生效。

简介

在 CSS 中,最基本的布局任务,一般是通过浮动和定位来完成。在对选择器结构和层叠值和单位字体文本属性基本视觉格式化内外边距、边框颜色和背景,这一些点有所了解之后,我们会遇到对页面的结构进行调整的需求,比如列式布局,布局的一部分与另一部分重叠这样的要求。本文接下来就要介绍这些基本的工具。

定位

定位的原理很简单,利用定位,可以准确地定义元素框相对于正常位置应该出现在哪里,或者相对于父元素、另一个元素甚至浏览器窗口本身的位置。

基本概念

定位的类型

通过使用 position 属性,可以选择 4 种不同类型的定位,这会影响元素框生成的方式:

  • static :元素框正常生成。块级元素生成一个矩形框,作为文档流的一部分,行内元素则会创建一个或多个行框,置于其父元素中。
  • relative :元素框偏移某个距离。元素仍保持其未定位前的形状,它原本所占的空间仍保留。
  • absolute:元素框从文档流完全删除,并相对于其包含块定位,包含块可能是文档中的另外一个元素,或者是初始包含块。元素原来在正常文档流中所占的空间会关闭,就好像该元素原来不存在一样。元素定位后生成一个*块级框,而不论原来它在正常流中生成的是什么类型的框。
  • fixed:元素框的表现类似于将 position 设置为 absolute,不过其包含块是视窗本身(浏览器窗口)。

包含块

CSS2.1 对定位元素的包含块,定义了以下的行为:

  • “根元素”的包含块,由用户代理建立,在 HTML 中,根元素就是 html 元素。
  • 对于一个非根元素,如果其 position 值是 relative 或 static,包含块则由最近的块级框、表单元格或行内块祖先框的内容边界构成。
  • 对于一个非根元素,如果其 position 值是 absolute,包含块设置为最近的 position 值不是 static 的祖先元素,这个过程如下:
    • 如果这个祖先是块级元素,包含块则设置为该元素的内边距边界;也就是由边框界定的区域。
    • 如果这个祖先是行内元素,包含块则设置为该祖先元素的内容边界。在从左向右读的语言中,包含块的上边界和左边界是该祖先元素中*第一个框内容区的上边界和左边界,包含块的下边界和右边界是最后一个框内容区的下边界和右边界。
    • 如果没有祖先,元素的包含块定义为初始包含块。

偏移属性

也就是常见的 toprightbottomleft。(这里说一下,css 中“上下左右”的顺序,一般都是 “上右下左”,顺时针这么一个圈。)
这些属性,描述的是距离包含块最近边的偏移。例如,top 描述了定位元素上外边距边界距离其包含块的顶端有多远。top 为正值,会把定位元素的上外边距下移,若为负值,则会把定位元素的上外边距移到其包含块的顶端之上。

宽度和高度

设置宽度和高度

对于定位元素,如果使用 toprightbottomleft 来描述4个边的放置位置,那么它的高度和宽度将由这些偏移隐含确定。

See the Pen position-example-1 by twohappy (@twohappy) on CodePen.

限制宽度和高度

如果有必要,可以通过 min-widthmin-heightmax-widthmax-height 来限制元素的尺寸。

绝对定位

包含块和绝对定位元素

元素绝对定位时,其包含块是最近的 position 值不为 static 的祖先元素。通常可以人为指定一个元素作为这个包含块,将其 position 指定为 relative,且没有偏移值。

1
div.contain {position: relative;}

简单的例子:

See the Pen position-absolute-eg.2 by twohappy (@twohappy) on CodePen.

有一点:元素绝对定位时,还会为其后代元素建立一个包含块,绝对定位的元素 E,它的子元素 C 如果也是绝对定位的,C 的偏移量是相对于 E 的。

绝对定位元素的放置和大小

“放置” 和 “大小” 在绝对定位元素身上,是紧密联系着的两个概念。如前文提到的,对于定位元素,如果使用 toprightbottomleft 来描述4个边的放置位置,那么它的高度和宽度将由这些偏移隐含确定。
但是如果这样之后,再设置一个显式的高度和宽度,会怎么样呢?会有冲突,必须将某些冲突的值忽略掉。

自动边偏移

元素绝对定位时,如果除 bottom 外,某个任意偏移属性设置为 auto,会有一种特殊行为。

See the Pen absolute-position eg.2 by twohappy (@twohappy) on CodePen.

绝对定位元素没有对任何方向上的偏移值进行约束时,会出现在它的“静态位置”,就是 position 是 static 时,出现的位置。

非替换元素的放置和大小

See the Pen 过度受限下的绝对定位元素 by twohappy (@twohappy) on CodePen.

这里提出一个词叫做“过度受限”,比如一个很大的包含块的绝对定位子元素,左右偏移都限制为10px,左右外边距也都限制为10px,本身宽度也设置为10px,这时 `right`,右偏移会失效,并重置成 auto。 如果某一个横向的外边距被设置成 auto,那么只要剩下的限制条件可以满足公式,就都会生效。具体请看例子。

垂直方向的行为,基本和水平方向上一致,只不过忽略的属性从 right,变成了 bottom。

替换元素的放置和大小

替换元素有固有的高度和宽度,所以较容易确定,再这里不多赘述。

固定定位

固定定位除了包含块为视窗之外,其它行为和绝对定位很类似:

  1. 过度受限时,忽略 right 、 bottom 。
  2. 偏移量未设置时,出现在它的静态位置。

这里有一个点:针对一个特定的需求,相对于父元素的固定定位。如果有一个可以滚动的内容区域,需要相对于这个区域,显示一个固定的小块内容,不随内容滚动而滚动,可以这样写:

See the Pen 相对父元素fixed定位 by twohappy (@twohappy) on CodePen.

相对定位

这个也很好理解,因为元素不会从文档流中删除,原来的位置仍然保留,只是将内容偏移一定的距离。

1)五花肉:500克。以前看到有人说,红烧肉必须一做就做一大锅,没有两三斤肉烧不出好吃的红烧肉。其实并非如此。对于小家庭来说,500克算是一个比较合适的量,一次做得太多,虽然吃的时候过瘾,过后肯定后悔。倒不如每次都保留一个良好的印象,每过一段时间就做一顿解解馋,这更加符合健康之道。

五花肉的层次越多越好,我比较偏好猪肋骨外侧的“上五花”,跟猪腹部的五花肉相比,这个部位脂肪层次多,但是每层脂肪又比较薄。这些五花肉需要提前切成3-4厘米见方的小块,肉块过小,瘦肉容易变硬变干,肉块过大,烹调过程中内部油脂不容易渗出,难免有些油腻。

2)老抽酱油:20克

2)料酒:20克

3)干红辣椒:1支

4)桂皮:一段(大概1厘米长就可以了,不要使用桂皮粉,烹调过程中会变黑,影响菜的品相)

5)生姜:2-3厘米见方,切成片

6)小葱:25克,切成5-6厘米的长段

7)糖:30克

8)盐:半茶匙(约3-4克)

9)八角:4-5朵

10)花生油:10克
一步是准备配料,一旦开启炉灶,红烧肉最好一气呵成,中间任何不必要的停顿都会影响菜的口味,所以必须提前把所有材料准备停当。

第二步需要对五花肉进行预处理,传统上至少有三种处理方式:油炸、焯水和煸炒。除此之外,还可以借鉴西餐里面处理肉类的办法,直接把五花肉放在大火预热90秒的平底锅里面煎。

把五花肉的四个侧面(不要去煎顶面的猪皮和底面的瘦肉层)都煎成金黄色,经过这样处理,一方面可以除去肥肉中的一部分油脂,猪肉表面金黄色的薄层会在慢炖的过程中融化在汤汁里面,增加许多风味,如果你不信,现在就可以把鼻子凑在煎好的五花肉上闻一闻,是不是已经香气扑鼻了呢?

煎的过程中可以使用不粘锅,翻动五花肉的时候尽量使用硅胶夹子,动作要轻柔,这样可以保护肉的形态。煎好的五花肉放在盘子里面盖上铝箔保温备用。

第三步是炒糖色。红烧肉的“红”其实就是焦糖的颜色。糖加热到170摄氏度的时候就会转化成焦糖,红烧菜肴最后收汁的时候,汤汁里面的糖在高温的作用下就会变成焦糖,给菜肴覆盖一层诱人的红棕色,并增添许多风味。理论上说,炒糖色并非绝对必须,可以在最后收汁的时候完成这个过程。但是红烧肉必须用加水(或者酒),收汁的时候把这些水完全烧干需要不少时间,等待过程当中,瘦肉部分可能就会变干变硬。提前炒好糖色就可以避免这些麻烦。

炒糖色最好就用炖肉的锅子,这样等会就可以直接把肉和糖混合在一起。炖肉锅子的选择非常重要,直接关系到红烧肉的品质。如果你做的红烧肉瘦肉部分有变干变硬的倾向,十有八九就是锅子没有选对。选择锅子的时候首先要考虑的是锅盖的密封性,锅盖上绝对不能有通气孔,锅盖和锅的配合必须严丝合缝。本来我们也没有注意到这个问题,LG用锅子烧米饭的时候发现,但凡那些锅盖上有通气孔的锅子,煮出来的米饭表层都是硬的,而那些锅盖密封性能很好的锅子,煮出来的米饭表层和内层都是软的。由此联想到红烧肉的问题,LG后来通过对照试验就验证了这个猜测。其次锅底导热的均匀性也非常重要,局部过热很容易让富含胶质的红烧肉粘在锅底。人们常常觉得砂锅导热比较均匀,其实并不准确,砂锅因为导热系数低,锅底的受热其实很不均匀。个人推荐使用不锈钢复底锅,其导热均匀性比厚底铸铁锅还要好。

在锅子里面倒入10克花生油,然后倒入30克白糖。花生油在这里就是起到一个导热媒介的作用,就像电脑CPU和风扇之间的导热硅脂一样,可以加快糖融化的过程。

很快就会变成金黄色,这个时候你就需要把炉灶火力降到最低,或者干脆把锅子从炉灶上移开,因为焦糖变色的过程非常迅速,就是短短几秒钟的事情。拍摄过程中,LG就是因为等我聚焦,结果锅子在炉灶上停留的时间稍长了一点,炒好的糖色迅速变成深红色,进而变成黑色,LG只好倒掉重新再来。

然后把前面煎好的五花肉放进锅子里面,翻动一下,让糖色均匀地裹在肉的表面。注意不要把所有的肉一起倒进锅子里面,因为里面析出的汁水跟滚烫的糖混合后会飞溅出来,比较安全的办法就是用夹子把肉一块块放进锅子里面,翻动的时候动作要轻柔,一来是保护五花肉的形态,二来是避免滚烫的汁水飞溅出来。

第四步就是慢火炖肉。在锅子里面放入切好的葱段、姜片、八角、桂皮和20克老抽酱油,翻动一下,让配料和五花肉混合均匀。

然后加入20克料酒和1支干红辣椒,翻炒均匀。

最后倒入沸水,倒水的时候要贴着锅边,不要直接把水浇在肉上面,否则前面辛辛苦苦刚给五花肉裹上的糖色就被热水冲掉了。

水的高度以能够淹没肉的高度的3/4为准,然后加入半茶匙盐(3-4克),翻动一下,让盐溶解在汤汁里面。对于红烧肉来说,就像苏东坡说的那样,应该少放水,但是这样一来就有一些肉直接暴露在水面上,如果锅盖密封性能比较好,这些暴露在外的肉就能保持较多的水分,如果锅盖密封性能不好,肉里的汁水就会渐渐蒸发,进而变干变硬。

火烧开后,立刻把火力降到最低(把锅子转移到最小的灶眼上面,用最小的火力),慢火炖上90分钟左右。

第五步是收汁。把锅子里面的汁水倒出来,倒的过程中用滤篮滤去八角桂皮等香料,五花肉仍然留在锅子里面,盖紧锅盖放在一边备用。

等到汤汁收到类似糖浆性状的时候,就关掉炉灶,把五花肉从锅子里面取出来,放在平底锅里面,稍稍翻动一下,给五花肉表面包裹一层汤汁。

然后,一秒钟也不要耽搁,马上开吃,看看这红烧肉的品相,它如何美味就不必多说了吧。红烧肉做好之后也会继续散发水分,瘦肉的口感会在很短的时间内变干变硬,肉皮变冷之后会变得坚韧,完全失去软糯轻盈的口感,所以吃的时候一定要抓紧时间哦!

再重复一下完美红烧肉需要注意几个细节:

1)用平底锅把五花肉表面煎成金黄色可以增加风味。

2)锅子的选择,一定要用锅盖密封性能好的锅子,锅底导热的均匀性也要好。

3)提前炒好糖色,缩短收汁时间。

4)收汁之前撇去里面的肥油。

准备

首先配置好jwt并且使用guard,config/auth.php

1
2
3
4
'defaults' => [
'guard' => 'api',
'passwords' => 'users',
],
1
2
3
4
5
6
7
8
'guards' => [
...

'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],

走流程

Auth::guard()->attempt($credential)
这个执行到了 JWTGurad ,代码如下:

1
2
3
4
5
6
7
8
9
10
public function attempt(array $credentials = [], $login = true)
{
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

if ($this->hasValidCredentials($user, $credentials)) {
return $login ? $this->login($user) : true;
}

return false;
}

首先执行了 retrieveByCredentials
再回头看看配置里面,user provider 使用的是 eloquent。
那么我们在 EloquentUserProvider 里面找到这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials)) {
return;
}

$query = $this->createModel()->newQuery();

foreach ($credentials as $key => $value) {
if (! Str::contains($key, 'password')) {
$query->where($key, $value);
}
}

return $query->first();
}

也就是除了密码项,其它提供的凭据,比如用户名,邮箱,或者别的自定义凭据,全部where一下,找出一个用户。返回。
然后就执行到了 hasValidCredentials 这个方法,在 JWTGuard 中:

1
2
3
4
protected function hasValidCredentials($user, $credentials)
{
return $user !== null && $this->provider->validateCredentials($user, $credentials);
}

然后我们还是去 EloquentUserProvider 里面找:

1
2
3
4
5
6
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];

return $this->hasher->check($plain, $user->getAuthPassword());
}

检查了一下密码是否match,match的话,登录:$this->login($user)

1
2
3
4
5
6
public function login(JWTSubject $user)
{
$this->setUser($user);

return $this->jwt->fromUser($user);
}

返回了一个token,也就是说,登录成功的话,返回的仅仅是一个token。

重点是,登录不成功的话,返回的仅仅是一个 false。

也就是说,使用JWTGurad登录,是没办法先检查邮箱是否存在,不存在提示,存在的话再检查密码是否符合。

那就只能自己写了。

Some of the following names are regional or contextual.

  • ( ) – parentheses, brackets (UK, Canada, New Zealand, South Africa and Australia), parens, round brackets, soft brackets, first brackets or circle brackets
  • [ ] – square brackets, closed brackets, hard brackets, second brackets, crotchets,[3] or brackets (US)
  • { } – braces are “two connecting marks used in printing”; and in music “to connect staves to be performed at the same time”[4] (UK and US), French brackets, curly brackets, definite brackets, swirly brackets, curly braces, birdie brackets, Scottish brackets, squirrelly brackets, gullwings, seagulls, squiggly brackets, twirly brackets, Tuborg brackets (DK), accolades (NL), pointy brackets, third brackets, fancy brackets.
  • 〈 〉– pointy brackets, angle brackets, triangular brackets, diamond brackets, tuples, or chevrons
  • < > – inequality signs, pointy brackets, or brackets. Sometimes referred to as angle brackets, in such cases as HTML markup. Occasionally known as broken brackets or brokets.[5]
  • ⸤ ⸥; 「 」 – corner brackets
  • ⟦ ⟧ – double square brackets, white square brackets
  • Guillemets, ‹ › and « », are sometimes referred to as chevrons or [double] angle brackets.[1]

一句话结论:js中的数组是没有“字符串”索引的,形如array[‘b’] = someValue只是在array对象上添加了属性。

Difference between for…of and for…in

The for…in loop will iterate over all enumerable properties of an object.

for in 循环会遍历一个对象上面的所有enumerable属性。

The for…of syntax is specific to collections, rather than all objects. It will iterate in this manner over the elements of any collection that has a [Symbol.iterator] property.

for of 语法是针对集合的,而不是所有的对象。它会遍历定义了[Symbol.iterator]属性的集合的所有元素。

The following example shows the difference between a for…of loop and a for…in loop.

MDN的例子如下:

1
2
3
4
5
6
7
8
9
10
Object.prototype.objCustom = function() {}; 
Array.prototype.arrCustom = function() {};
let iterable = [3, 5, 7];
iterable.foo = 'hello';
for (let i in iterable) {
console.log(i); // logs 0, 1, 2, "foo", "arrCustom", "objCustom"
}
for (let i of iterable) {
console.log(i); // logs 3, 5, 7
}

这两天在看element-ui,卡在了table组件的作用域插槽.下面来理解一下:

首先keep in mind, 官方文档提到作用域:
注意: 写在父组件里的内容在父组件内编译

然后我们来理解一下slot插槽:

是写在子组件里的
写父组件时,写在子组件标签里面的内容会插到这个子组件的插槽里,替换整个slot
综合以上两点这个大概过程就是:
父组件里面如果写了,首先编译会求出值来,然后再替换到子组件中.也就是这个是是父组件里面的数据.

正常的父子组件是啥呢, child在props里面声明它希望从father接受某个property,然后father在使用child时候给它一个property,写在标签里面,当然名字要对上.

这个就是vue的父子组件通信,从上到下props,从下到上是自定义event(不细说).

可以发现,slot其实也是从父组件传个什么鬼到子组件里面去.但是我理解一般这里只能应用样式啊,数据还是father的数据,因为是渲染完了替换掉的.需要用子组件的本地data怎么搞,还得在child里面搞.

那么scoped slot 提供了什么呢, father在子组件里面写一个template, 用scope=’随便一个什么名字’ 接收子组件暴露出来的数据,然后用在这个template里面.
下面上dummy代码:

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
37
38
39
40
41
42
<!--子组件-->
<template>
<div class="child-com">
<slot :someProperty="data">
我是一个插槽,只是为了将父组件中包括在我这个子组件中的内容渲染进来
</slot>
<slot name="namedSlot">父组件中slot="namedSlot"的内容才会被替换到此处</slot>
</div>
</template>

<script>
export default {
data() {
return {
data:{message:'我是child组件data里面的message'}
}
}
}
</script>

<!--父组件-->
<template>
<div class="parent-com">
<child-component>
<template scope="whateverItIs">
<p class="parent-message">hello from parent : {{fatherMessage}}</p>
<p class="parent-message">{{whateverItIs.someProperty.message}}</p>
</template>
<p slot="namedSlot" class="parent-message">我是parent中的文字,我不插无名之辈</p>
</child-component>
</div>
</template>

<script>
import childComponent from './child.vue'
export default {
data() {
return { fatherMessage:'父组件的消息' }
}
components:{ childComponent }
}
</script>

p.s. 这是两年前的笔记。现在回头看,就是很常用也很常见的一个设计。。

Events

大部分Nodejs核心API建立在一个异步事件驱动的架构上,emitters周期性的触发事件,导致函数对象listeners调用执行。

比如,每次一个peer连接到net.Server对象时,它就触发一个事件,;当文件打开时,fs.ReadStream就触发一个事件;当数据可以被读取时,stream就触发一个事件。

所有会触发事件的对象都是EventEmitter类的实例。这些对象暴露出eventEmitter.on()方法,允许一个或者多个函数监听该对象触发的事件。通常,事件名是驼峰字符串,但是任何符合JS规范的属性名都可以用。

当EventEmitter对象触发一个事件,所有监听这个特定事件的函数会被同步调用。被调用方法返回的所有值都会被忽略并丢弃
下面是个简单的例子

1
2
3
4
5
6
7
8
9
const EventEmitter = require('events');

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');

向监听者传递参数和this

eventEmitter.emit()方法允许把任意参数传给监听函数。划重点:普通函数被EventEmitter调用时候啊,this被置为listener attached的那个触发器。

1
2
3
4
5
6
7
8
9
10
11
const myEmitter = new MyEmitter();
myEmitter.on('event', function(a, b) {
console.log(a, b, this);
// Prints:
// a b MyEmitter {
// domain: null,
// _events: { event: [Function] },
// _eventsCount: 1,
// _maxListeners: undefined }
});
myEmitter.emit('event', 'a', 'b');

然而回调函数用箭头函数时候,this就不再指向那个触发器实例了。。

1
2
3
4
5
6
const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
console.log(a, b, this);
// Prints: a b {}
});
myEmitter.emit('event', 'a', 'b');

//todo:这里指向了个啥?

同步 vs 异步

EventListener 按照注册顺序,同步调用所有的监听函数,这样可以保证正确的事件顺序,避免竞争执行和逻辑错误;需要时,监听函数可以使用setImmediate()和process.nextTick()切换成异步执行:

1
2
3
4
5
6
7
const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
setImmediate(() => {
console.log('this happens asynchronously');
});
});
myEmitter.emit('event', 'a', 'b');

只处理一次事件

当使用eventEmitter.on()注册了一个监听器后,每次它所监听的事件被触发,这个监听器就会被调用

1
2
3
4
5
6
7
8
9
const myEmitter = new MyEmitter();
let m = 0;
myEmitter.on('event', () => {
console.log(++m);
});
myEmitter.emit('event');
// Prints: 1
myEmitter.emit('event');
// Prints: 2

而使用eventEmitter.once()方法,就可以注册一个只调用一次的监听器函数。当事件触发后,这个监听器先被注销然后再执行

1
2
3
4
5
6
7
8
9
const myEmitter = new MyEmitter();
let m = 0;
myEmitter.once('event', () => {
console.log(++m);
});
myEmitter.emit('event');
// Prints: 1
myEmitter.emit('event');
// Ignored

错误事件

当一个EventEmitter实例中有错误发生,标准处理是触发一个‘error’事件。他们在node中当做特殊案例来处理。
如果一个EventEmitter连一个注册在error事件下的监听器都没有,而’error’事件触发了,这个错误会被抛出,栈追踪打印,Node.js进程退出。

1
2
3
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
// Throws and crashes Node.js

为了让Node.js进程不至于崩溃,可以在process对象的uncaughtException事件下面注册一个监听器(或者可以使用domain模块,这个模块已经不建议使用)

1
2
3
4
5
6
7
8
const myEmitter = new MyEmitter();

process.on('uncaughtException', (err) => {
console.error('whoops! there was an error');
});

myEmitter.emit('error', new Error('whoops!'));
// Prints: whoops! there was an error

坠吼还是总给’error’事件注册上监听器

1
2
3
4
5
6
const myEmitter = new MyEmitter();
myEmitter.on('error', (err) => {
console.error('whoops! there was an error');
});
myEmitter.emit('error', new Error('whoops!'));
// Prints: whoops! there was an error

EventEmitter

1
const EventEmitter = require('events');

所有的EventEmitter在新监听器添加和移除的时候都会相应的触发 newListenerremoveListener 事件

newListener 事件

  • eventName 新添加的事件名
  • listener 事件处理程序
    EventEmitter实例在一个监听器添加到其内部监听器数组之前会触发一个它自己的 newListener 事件。
    因为这个事件是在添加listener(我们假设这个回调函数叫A)之前触发的,所以有(划重点):如果在newListener回调函数中,有额外的注册在同一个事件name下的listener(我们假设这个回调函数叫B)被注册,这个listener(B)会被插入在上面提到的那个listener(B)之前。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const myEmitter = new MyEmitter();
    // Only do this once so we don't loop forever
    myEmitter.once('newListener', (event, listener) => {
    if (event === 'event') {
    // Insert a new listener in front
    myEmitter.on('event', () => {
    console.log('B');
    });
    }
    });
    myEmitter.on('event', () => {
    console.log('A');
    });
    myEmitter.emit('event');
    // Prints:
    // B
    // A

removeListener 事件

  • eventName 新添加的事件名
  • listener 事件处理程序
    这个事件在listener移除之后触发。

EventEmitter.defaultMaxListeners 属性

默认来讲,单个事件可以注册最大10个监听器。用emitter.setMaxListeners(n) 可以为个别的EventEmitter实例改变这个上限。如果要改变所有实例的上限,可以用EventEmitter.defaultMaxListerners属性

这个属性影响所有的(包括在改变操作之前创建的)EventEmiiter实例。但是,调用emitter.setMaxListeners(n)比EventEmitter.defaultMaxListeners优先生效。

然后这个不是硬性龟腚。EventEmitter实例允许添加超出最大值的listener,只是会输出一个警告,检测到 “possible EventEmitter memory leak” 。对单个的EventEmitter,可以用emitter.getMaxListeners()和emitter.setMaxListeners() 方法来临时避免这个警告:

1
2
3
4
5
emitter.setMaxListeners(emitter.getMaxListeners() + 1);
emitter.once('event', () => {
// do stuff
emitter.setMaxListeners(Math.max(emitter.getMaxListeners() - 1, 0));
});

多个的话就多加几个吧。。

–trace-warnings 这个命令行参数可以用来显示这种warning

触发的warning可以用process.on(‘warning’)来查看,还有额外的emitter,type和count属性,各自指向事件触发器实例,事件名称和添加的事件数量。这个warning的name属性是’MaxListenersExceededWarning’。

emitter.addListener(eventName,listener)

等于emitter.on(eventName,listener)

emitter.emit(eventName[, …args])

按照注册的先后顺序,依次同步调用监听函数,给他们传入这里提供的参数。
如果emit的事件有监听函数,返回一个true,否则false

emitter.eventNames()

返回一个这个触发器上面注册监听器的事件们构成的数组,值是String或者Symbol。
注意这是个方法

1
2
3
4
5
6
7
8
9
10
const EventEmitter = require('events');
const myEE = new EventEmitter();
myEE.on('foo', () => {});
myEE.on('bar', () => {});

const sym = Symbol('symbol');
myEE.on(sym, () => {});

console.log(myEE.eventNames());
// Prints: [ 'foo', 'bar', Symbol(symbol) ]

emitter.getMaxListeners()

返回当前最大监听数:
emitter.setMaxListeners(n)设置的,或者由EventEmitter.defaultMaxListeners默认的.

emitter.listenerCount(eventName)

监听某个事件的监听器个数

emitter.listeners(eventName)

返回一个监听eventName的监听器数组的副本,(里面应该是一堆listener函数)

1
2
3
4
5
server.on('connection', (stream) => {
console.log('someone connected!');
});
console.log(util.inspect(server.listeners('connection')));
// Prints: [ [Function] ]

emitter.on(eventName, listener)

把listen函数加到监听’eventName’的监听器数组尾部,这里不会检查这个函数是否已经被添加过了,多次传入同样事件名和监听器组合的emitter.on的调用,结果将是listener被添加和调用多次。

1
2
3
server.on('connection', (stream) => {
console.log('someone connected!');
});

它返回一个指向EventEmitter的reference,这就可以链式调用.测试的时候发现理解不了调用的执行顺序,如果有Symbol的话。。。
事件监听默认是顺序执行的,按照添加的先后顺序,emitter.prependListener()方法可以把监听器插入到监听器队列的开头

1
2
3
4
5
6
7
const myEE = new EventEmitter();
myEE.on('foo', () => console.log('a'));
myEE.prependListener('foo', () => console.log('b'));
myEE.emit('foo');
// Prints:
// b
// a

emitter.once(eventName, listener)

emitter.on的一次性版本,当事件触发的时候先移除再调用callback。同样返回一个EventEmitter实例方便链式调用。
同样有一个插入到数组队列首部的方法emitter.prependOnceListener()

emitter.prependListener(eventName, listener)

emitter.prependOnceListener(eventName, listener)

emitter.removeAllListeners([eventName])

不传参数就移除所有监听器,传就移除对应事件的监听器。
移除在代码的其他地方添加的监听器是一个bad practice,尤其是当EventEmitter实例是由其他组件或者模块创建的时候。
同样返回一个EventEmitter的指向,链式调用。

emitter.removeListener(eventName, listener)

移除eventName事件下的特定监听函数listener,一次只能移除一个,如果同一个listener添加了多次,必须移除多次

1
2
3
4
5
6
7
//这里应该有个引用
const callback = (stream) => {
console.log('someone connected!');
};
server.on('connection', callback);
// ...
server.removeListener('connection', callback);

注意,一旦一个事件被触发,所有在它之上的监听器会被依次调用(在emitting之时)。这意味着任何在emitting之后,最后一个监听器执行完之前执行的removeListener()或者removeAllListeners()调用,将不会把listeners从进行中的emit()移除,在顺序执行结束后,行为就跟预期一样了(移除了)。

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
const myEmitter = new MyEmitter();

const callbackA = () => {
console.log('A');
myEmitter.removeListener('event', callbackB);
};

const callbackB = () => {
console.log('B');
};

myEmitter.on('event', callbackA);

myEmitter.on('event', callbackB);

// callbackA removes listener callbackB but it will still be called.
// Internal listener array at time of emit [callbackA, callbackB]
myEmitter.emit('event');
// Prints:
// A
// B

// callbackB is now removed.
// Internal listener array [callbackA]
myEmitter.emit('event');
// Prints:
// A

监听器们是有一个内部的数组来管理的,调用这个方法会改变被移除的listener之后的一堆listener的位置索引。这不会引起调用顺序的变化,但意味着内部监听方法队列的拷贝们,比如像emitter.listeners()方法返回的数组就都需要重建一次。(它们还是之前的拷贝)。
仍然返回一个EventEmitter的指向,方便链式调用

emitter.setMaxListeners(n)

默认的EventEmitter会在某一个事件被添加了超过10个监听函数时,打印一个warning,这个对查找内存泄漏很有帮助。但是显然不是所有的事件都应该被限制到10个,这个方法可以对具体实例设置它的上限,Infinity(或者0??)表示无上限

Stream

Stream是一个抽象接口,stream模块提供了一个基础API用来创建实现了stream接口的对象。

Nodejs提供了很多流对象,比如对一个HTTP服务器的request,和process.stdout标准输出都是流。

流可以使可写,可读,或者可读写的。所有的stream都是EventEmitter的实例。

stream模块可以这样使用

1
const stream = require('stream');

Types of Streams

  • Readable
  • Writable
  • Duplex
  • Transform

Object Mode

Nodejs API创造的streams专门对strings和Buffer对象操作。但是对其他值操作也是可能的,这种stream在Object Mode下工作。

Buffering

Writable 和 Readable 流都将数据存储在一个内部缓存里,分别可用 writable._writableState.getBuffer()readable._readableState.buffer 来检索到。可能缓存的数据数量由传入stream构造器的hightWaterMark来决定。对于普通stream,hightWaterMark规定了一个byte的总数,对于工作在Object Mode下的stream,它规定了一个总的object数。

在stream的实现调用stream.push(chunk)时,数据被缓存在Readable stream里。如果这个Stream的消费者没有调用stream.read(),这些数据就在内部队列里呆着,直到被消费为止。

一旦内部读取缓存的大小达到了highWaterMark规定的限值,stream会暂时停止从相关数据源读取数据,直到已缓存的数据被消费为止。(也就是说,stream会停止调用内部用来填充读取缓存的的readable._read()方法)

当writable.write(chunk)被重复调用时,数据被缓存在Writable stream里。当内部写缓存的总大小低于highWaterMark规定的限值时,writable.write()的调用会返回true,一旦达到或者超过限值,就会返回false。
stream API特别是stream.pipe()方法的一个重要目标,就是把数据的缓存限制在一个可接受的程度,这样速度不同的数据源和数据目的地就可以在一个有限的内存下正常工作(而不会撑满内存导致溢出)。
因 为DuplexTransform streams 都是可读写的,他们都有两个独立的内部缓存用来读和写,允许两边独立操作,同时保持一个合适有效的数据流。比如,net.Socket实例是Duplex stream,它的Readable 一侧允许消费从socket里接收到的数据,Writable一侧允许向socket内写入数据。因为读写数据的速度有可能不一样,两边分开独立(缓存和)操作就很重要。
说实话这里没看懂

给Stream Consumers的API

不管多简单,基本上所有的Node.js程序都在以某种方式使用stream。下面是一个在实现一个HTTP服务器的程序中使用stream的例子:

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
37
38
39
const http = require('http');

const server = http.createServer( (req, res) => {
// req is an http.IncomingMessage, which is a Readable Stream
// res is an http.ServerResponse, which is a Writable Stream

let body = '';
// Get the data as utf8 strings.
// If an encoding is not set, Buffer objects will be received.
req.setEncoding('utf8');

// Readable streams emit 'data' events once a listener is added
req.on('data', (chunk) => {
body += chunk;
});

// the end event indicates that the entire body has been received
req.on('end', () => {
try {
const data = JSON.parse(body);
// write back something interesting to the user:
res.write(typeof data);
res.end();
} catch (er) {
// uh oh! bad json!
res.statusCode = 400;
return res.end(`error: ${er.message}`);
}
});
});

server.listen(1337);

// $ curl localhost:1337 -d '{}'
// object
// $ curl localhost:1337 -d '"foo"'
// string
// $ curl localhost:1337 -d 'not json'
// error: Unexpected token o

可写流暴露了如write()和end()的方法,用来向stream写入数据。
可读流使用EventEmitter API来通知程序啥时候数据可以从stream中读取了。有多种方式可以读取。
可读流和可写流都各种使用EventEmitter API,来和stream的当前状态通信。

Writable Streams

Writable Streams是一个数据要写入的目的地的抽象。

  • HTTP requests, on the client
  • HTTP responses, on the server
  • fs write streams
  • zlib streams
  • crypto streams
  • TCP sockets
  • child process stdin
  • process.stdout, process.stderr

所有的Writable streams都实现了由 stream.Writable 类定义的接口。
虽然各种实例各有不同,但他们都遵循同样的基本使用模式,如下面这个例子:

1
2
3
4
const myStream = getWritableStreamSomehow();
myStream.write('some data');
myStream.write('some more data');
myStream.end('done writing data');

Class: stream.Writable

Event: ‘close’

Event: ‘drain’

Event: ‘error’

Event: ‘finish’

Event: ‘pipe’

Event: ‘unpipe’

writable.cork()

writable.end([chunk][, encoding][, callback])

writable.setDefaultEncoding(encoding)

writable.uncork()

writable.write(chunk[, encoding][, callback])

Readable Streams

Writable Streams是一个源的抽象,数据可以通过它来消费。

  • HTTP responses, on the client
  • HTTP requests, on the server
  • fs read streams
  • zlib streams
  • crypto streams
  • TCP sockets
  • child process stdout and stderr
  • process.stdin

所有的Readable streams都实现了由 stream.Readable 类定义的接口。

Two Modes 两种模式

Readable streams实际上运行在以下两种模式之一:flowing和paused

在flowing模式下,数据从相关系统中自动读取,并通过EventEmitter接口使用事件尽可能快的提供给一个程序。

在paused模式下,stream.rad()方法必须显式的调用,来从stream中读取数据块。

所有的Readable streams开始都处在paused模式,并可以通过以下几种方式之一切换成flowing模式:

  • 添加一个’data’事件的handler
  • 调用stream.resume()方法
  • 调用stream.pipe()方法,来将数据传给一个Writable

Readable可以通过以下几种方式之一切换回paused模式:

  • 如果没有pipe目的地,调用stream.pause()方法
  • 如果有pipe目的地,移除所有的’data’事件handler,并通过调用stream.unpipe()移除所有的pipe目的地。

需要记住的是,一个Readable不会生成数据,直到一个或者消费数据或者忽略数据的机制被提供。一旦消费机制消失或者失效,Readable会尝试停止生成数据

Three States 三种状态

Readable“两种操作模式”是一个对其实现中更复杂的内部状态管理的简化抽象。
明确的说,任何给定的时点,一个Readable会处在以下三种可能状态之一:

  • readable._readableState.flowing = null
  • readable._readableState.flowing = false
  • readable._readableState.flowing = true
    null:消费数据的机制没有提供,所以stream不会生成他的数据
    给’data’事件添加一个监听器,调用readable.pipe()方法,或者调用readable.resume()方法会把flowing属性置为true,导致Readable开始积极地emit事件,同时数据开始生成。
    调用readable.pause(),readable.unpipe()或者接受到‘back pressure’会让flowing属性被置为false,暂停事件流,但不会停止数据的生成。
    当flowing是false时,数据可能在stream的内部缓存里积累。

    Chosse One 挑一个

    Readable stream API 随着Node.js版本在进化,提供了一堆消费数据的方法。通常应该只选择一个来消费数据,永远不要用多种方式来消费同一个stream中的数据。
    一般推荐readable.pipe(),因为使用最简单。需要细粒度的控制数据传输和生成的可以使用EventEmitter和readable.pause()/readable.resume() API。

Class: stream.Readable

Event: ‘close’

Event: ‘data’

Event: ‘end’

Event: ‘error’

Event: ‘readable’

readable.isPaused()

readable.pause()

readable.pipe(destination[, options])

readable.read([size])

readable.resume()

readable.setEncoding(encoding)

readable.unpipe([destination])

readable.unshift(chunk)

readable.wrap(stream)