Code Monkey home page Code Monkey logo

blog's People

Contributors

jtangming avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

blog's Issues

weekly reading

1、React 16 之上:Time Slicing 与 Suspense API

本文介绍了 React 的新特性,Time-Slicing 将子组件切割为不同的块操作,并且能在不同的帧中异步执行; Suspense API 则允许 ReactJs 将界面的更新推迟到数据抓取完毕,是基于用户体验出发的编程模型。感兴趣的可以了解阅读。

2、Web 前端中的增强现实(AR)开发技术

文章力求把目前前端方向的 AR 技术都罗列一遍,细节不赘述,文章虽然是一两个月前的了,觉得能对 Web 前端工程师有一点的启发。

3、The React and React Native Event System Explained: A Harmonious Coexistence

通常情况下大部分开发者只是会用React’s event handling systemReact,对它们内部工作原理不是特别了解。本文作者阅读相关源代码后整理得到的理解,包括事件系统概览、事件接收与管理机制、EventPluginHub等。

4、http://mp.weixin.qq.com/s/MDRfdRnhJJ53611cG_Zb6g

我们常说的性能优化往往只是事后的想法,性能优化除了技术活,更多的是规划和指标。本文不是对雅虎14的的复述,而是更加详尽的阐述了文中所涉及的所有优化策略原理和来龙去脉,提供的这份快速、简洁的性能优化清单值得读者思考。

5、一大波 Android 刘海屏来袭,全网最全适配技巧!

文章介绍了Android刘海屏的相关背景,“切割”状态栏的区域所面临的几种问题,最后从技术的角度来解决问题。在今后的开发中可能会遇见类似的适配,可推荐参考。

6、High Performance React: 3 New Tools to Speed Up Your Apps

在开发中,通常缓慢的组件挂载、过深的组件树以及不必要的渲染都有可能会削弱应用用户体验。文章详细地介绍了三个辅助工具及相关技术以提升应用性能,可以阅读参考。

7、How to escape async/await hell

说得有理有据的,有点危言耸听。其实没有那么严重,但可以作为优化手段,推荐一看。

8、那些好玩却尚未被 ECMAScript 2017 采纳的提案

文章先带你了解 ECMAScript 提案流程, 接着介绍了 ECMAScript 修订中几个重要的语法修改提案。了解相关的 ES 语法趋势和步伐,可以进一步打打基础。

9、WebAssembly 对比 JavaScript 及其使用场景

文章通过对比,你将更为清晰的理解V8 的运行机制,wasm 的内存模型、内存垃圾回收等以及其它特性和使用场景。

10、探索Virtual DOM的前世今生

本文对VirtualDOM中的**与diff实现进行了详细介绍,有点炒旧饭嫌疑,不过值得我们持续关注与学习。

11、梳理前端开发使用 eslint 和 prettier 来检查和格式化代码问题

> 好代码同样的需要好工具,本文手把手教你撸一个使用 eslint 和 prettier 来检查和格式化代码问题

12、用 SOLID 原则保驾 React 组件开发

关于 React 组件开发,和通用的设计模式一样,同样的会有一些“经验法则”。推荐阅读一下,有套路傍身,开发起来更加稳妥。

13、大前端时代前端监控的最佳实践

文章是彭伟春在GMTC大会上的分享,是性能优化专题最为火爆的场,主要内容大致为:大前端时代前端监控新的变化、前端监控的最佳实践,最后是阿里云前端监控系统的实现

14、如何监控网页崩溃?

本文借 PWA 概念,使用 Service Worker 来实现网页崩溃的监控,实现了一套基于心跳检测的监控方案。

15、Event Loop 这个循环你晓得么?

本文通过图文并茂的讲解,短短10分钟则了解了Event Loop 。

16、浅谈React Scheduler任务管理

React16 的 fiber 架构下,内部会动态灵活的管理所有组件的渲染任务,本文可以帮助了解一下 React 到底是如何管理渲染任务的

17、React hooks:它不是一种魔法,只是一个数组——使用图表揭秘提案规则

近期有不少关于 React hooks 文章推荐,通过阅读本文,可以给大家建立了一个关于 Hooks 的更加清晰的思维模型,以此可以去思考新的 Hooks API 底层到底做了什么事情。

18、应对流量劫持,前端能做哪些工作?

Apollo GraphQL

  • GraphQL 的 field resolve 如果按照 naive 的方式来写,每一个 field 都对数据库直接跑一个 query,会产生大量冗余 query,虽然网络层面的请求数被优化了,但数据库查询可能会成为性能瓶颈,这里面有很大的优化空间,但并不是那么容易做。FB 本身没有这个问题,因为他们内部数据库这一层也是抽象掉的,写 GraphQL 接口的人不需要顾虑 query 优化的问题。

  • GraphQL 的利好主要是在于前端的开发效率,但落地却需要服务端的全力配合。如果是小公司或者整个公司都是全栈,那可能可以做,但在很多前后端分工比较明确的团队里,要推动 GraphQL 还是会遇到各种协作上的阻力。

二叉树

二叉树特点

  • 每个结点最多有两颗子树,所以二叉树中不存在度大于2的结点
  • 左右子树是有顺序的,次序不能任意颠倒
  • 即使树中某结点只有一棵子树,也要区分它是左子树还是右子树
  • 在二叉树的第 i 层上最多有 2^i-1 个节点(i >=1)

满二叉树

在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,则为满二叉树

完全二叉树

对于具有 n 个节点的二叉树,对于任何一个编号 i (1 <= i <= n),如果与同层级的满二叉树中编号为 i 的节点完全相同,则是完全二叉树。

注:满二叉树一定是完全二叉树,但反过来不一定成立。

二叉树的存储结构

顺序存储

二叉树的顺序存储结构就是使用一维数组存储二叉树中的结点,并且结点的存储位置,就是数组的下标索引。
如一颗完全二叉树如下:

顺序存储方式如下:

极端情况采用顺序存储的方式是十分浪费空间的,如:

二叉链表

二叉链表将结点数据结构定义为一个数据和两个指针域,如:

二叉树遍历

  • 前序遍历
  • 中序遍历
  • 后序遍历
  • 层序遍历

快速记忆法:
前序就是父节点->左子树->右子树,中序是左子树->父节点->右子树,后序是左子树 -> 右子树 ->父节点

如这样一颗二叉树:

前序遍历输出为:ABDHIEJCFG
中序遍历输出为:HDIBJEAFCG
后序遍历输出为:HIDJEBFGCA
层次遍历结果为:ABCDEFGHIJ

JavaScript 中的私有变量

参考原文:Private Variables in JavaScript
本文非直译,如有理解不对的地方,请指正。

近年来 Javascript 不断有新特性或语法加进来,不过还是始终保持一切皆对象的原则,基于运行时的概念,Javascript 并没有所谓的公共、私有属性的概念。

自 ES6 以后,虽然 Javascript 有了,但不像 C、Java 那样有专门的修饰符来控制变量的访问权限,Javascript 所有的属性都需要在函数中定义。以下文章内容将介绍如何实现私有变量。

约定命名规则的方式

最简单粗暴的方式就是在团队中约定命名规范,通常是以下划线作为属性名称的前缀(e.g. _count)。其本质上并没有阻止变量的访问权限,仅仅是开发之间默认的规则,即不能乱引用和修改我的变量。

WeakMap

WeakMap 虽然不会阻止对数据的访问,但是它能将私有变量和用户的可操作对象分开,示例代码如下:

const map = new WeakMap();
// Create an object to store private values in per instance
const internal = obj => {
  if (!map.has(obj)) {
    map.set(obj, {});
  }
  return map.get(obj);
}
class Shape {
  constructor(width, height) {
    internal(this).width = width;
    internal(this).height = height;
  }
  get area() {
    return internal(this).width * internal(this).height;
  }
}
const square = new Shape(10, 10);
console.log(square.area);      // 100
console.log(map.get(square));  // { height: 100, width: 100 }

以上代码中,将 WeakMap 的关键字设置为私有属性所属对象的实例,通过一个函数来管理返回值,如无则创建之,所有的属性将被存储在其中。在 Shape 实例中,遍历属性或者在执行 JSON.stringify 等都不会展示出实例的私有属性。

Symbol

Symbol 的实现方式与 WeakMap 类似,不过这种实现方式需要为每个私有属性创建一个 Symbol,但是在类外还是可以访问该 Symbol,即还是可以拿到这个私有属性。示例代码如下:

const widthSymbol = Symbol('width');
const heightSymbol = Symbol('height');
class Shape {
  constructor(width, height) {
    this[widthSymbol] = width;
    this[heightSymbol] = height;
  }
  get area() {
    return this[widthSymbol] * this[heightSymbol];
  }
}
const square = new Shape(10, 10);
console.log(square.area);         // 100
console.log(square.widthSymbol);  // undefined
console.log(square[widthSymbol]); // 10

闭包

前面介绍的几种方式仍然允许从类外访问私有属性,闭包是将私有变量数据封装在调用时创建的函数作用域内,从内部返回函数的结果,从而使这一作用域无法从外部访问。闭包想必不用再介绍了吧,这里可以联想一下常出现的一到面试题:JavaScript 实现一个私有变量,每次调用一个函数自动加 1。

Proxy

通俗的说 Proxy 在数据外层套了个壳,然后通过这层壳访问内部的数据,即在原对象的基础上进行了功能的衍生而又不影响原对象。理解如何使用 Proxy 实现私有变量,这里我们需要关注 set 和 get。

使用 Proxy 的方式是:let proxy = new Proxy(target, handler);,Proxy 构造函数中的两个参数具体是:

  • target 是用 Proxy 包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  • handler 是一个对象,其声明了代理 target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数

一个示例代码如下:

class Shape {
  constructor(width, height) {
    this._width = width;
    this._height = height;
  }
  get area() {
    return this._width * this._height;
  }
}

const handler = {
  get: function(target, key) {
    if (key[0] === '_') {
      throw new Error('Attempt to access private property');
    }
    return target[key];
  },
  set: function(target, key, value) {
    if (key[0] === '_') {
      throw new Error('Attempt to access private property');
    }
    target[key] = value;
  }
}

const square = new Proxy(new Shape(10, 10), handler);
console.log(square.area);             // 100
console.log(square instanceof Shape); // true
square._width = 200; // Error: Attempt to access private property

如上代码,我们实例化了一个 Proxy,在 target 参数中引入了 Shape 对象,该类里边实现了两个变量,通过 square 是没法直接访问里边的 _width 和 _height 的,这就通过代理实现了私有变量的效果。

TypeScript 中的 private

TypeScript 是 JavaScript 的超集,最终是编译为原生 JavaScript 用在生产环境。TS 支持指定私有、公共和受保护的属性,这里就不再像原文那样举例了。

需要提醒的是,使用 TypeScript 只有在编译时才能获知到这些类型,而私有、公共和受保护修饰符在编译时才起作用。所以,其实你可以使用 x.y 访问内部私有变量的,只不过 TypeScript 会在编译时给你报出一个错误,但不会停止它的编译。其实并不是真正意义上的私有变量,只是在开发编译的时候能够提醒到开发人员,间接达到实现私有变量的效果。

private fields 的方式,即 # 符号

其实在 TC39 中已有提案引入 private fields,目前还在 Stage 3 阶段,它使用 # 符号表示它是私有的,# 的使用方式与以上提的命名约定方式非常类似,但对变量的实际访问权限提供了限制。

Web 安全之攻防总结

以下基于常见的攻击方式入手,对攻防方式和策略做一些总结。

CSRF

CSRF(Cross Site Request Forgery),即跨站请求伪造,是常见的 Web 攻击之一,攻击过程示例图如下:
csrf

CSRF 利用用户已登录的身份,在当前用户毫不知情的情况下完成非法操作,通过上图总结攻击原理如下:

  • 用户正常登录过网站 A 且在本地记录了 cookie,且用户没有退出登录
  • 在用户 cookie 生效的情况下,访问危险网站 B,且在 B 站点要求访问站点 A
  • 根据访问 A 中记录的 cookie,访问之,前提是站点 A 没有做任何的 CSRF 防御

举个例子方便理解,用户登陆某银行网站,以 Get 请求的方式完成转账,攻击者可构造另一危险链接 B,并把该链接通过一定方式诱骗受害者用户点击。受害者用户若在浏览器打开此链接,会将之前登陆后的 cookie 信息一起发送给银行网站,服务器在接收到该请求后,确认 cookie 信息无误则完成该请求操作,造成攻击行为完成。

攻击者可以构造 CGI 的每一个参数,伪造请求,这也是存在 CSRF 漏洞的最本质原因。

CSRF 防御方式

1、验证码,在一些关键的节点操作加上验证码,但是该种方式一定程度上会降低用户体验
2、Referer Check,通过 Http header 的 referer 鉴定请求来源。但在某些情况下如从 https 跳转到 http,浏览器基于安全考虑,不会发送 referer,服务器就无法进行 check,所以这种方式不是 CSRF 的主要防御手段。
3、SameSite,可以对 Cookie 设置 SameSite 属性,但并不是所有浏览器都支持的。
4、Anti CSRF Token,比较完善的解决方案是加入 Anti-CSRF-Token。即发送请求时在 Http 请求中加入 token,并在服务器建立一个拦截器来验证 token。服务器读取浏览器当前域 cookie 中的 token 值,然后进行校验,即该请求中的 token 和 cookie 中的 token 值是否都存在且相等,才认为这是合法的请求,否则拒绝该次服务。

总结一下就是:

  • Get 请求严格遵循不对数据进行修改原则
  • 不让第三方网站访问到用户 Cookie
  • 阻止第三方网站来源请求
  • 请求时附带验证信息,比如验证码或者 Token

XSS 攻击

XSS(Cross Site Scripting),跨站脚本攻击。恶意攻击者往 Web 页面里注入恶意 Script 代码,当用户浏览这些网页时,就会执行其中的恶意代码,可造成对用户 cookie 信息盗取、会话劫持等各种攻击。

XSS 的攻击原理是往 Web 页面里插入可执行网页脚本代码,当用户浏览该页之时,嵌入其中的脚本代码会被执行,从而可以达到盗取用户信息或其他侵犯用户安全隐私的目的。有可能造成以下影响:

  • 利用虚假输入表单骗取用户个人信息。
  • 利用脚本窃取用户的 Cookie 值,被害者在不知情的情况下,帮助攻击者发送恶意请求。
  • 显示伪造的文章或图片。

1、非持久型 XSS

非持久型 XSS 漏洞,一般是通过给在 URL 参数中添加恶意脚本,当 URL 链接被打开后,特有的恶意代码参数被 HTML 解析、执行。

非持久型 XSS 漏洞攻击有以下几点特征:

  • 即时性,不经过服务器存储,直接通过 HTTP 的 GET 或 POST 请求就能完成一次攻击,拿到用户隐私数据。
  • 攻击者需要诱骗点击,必须要通过用户点击链接才能发起
  • 用户常常无感知,造成反馈率低,所以较难发现和响应修复
  • 盗取用户敏感保密信息

预防非持久型 XSS 攻击,可以采取如下措施:

  • 页面完全依赖后端返回来渲染页面。
  • 尽量不要从 URL、document.referrer、document.forms 等这种 DOM API 中获取数据直接渲染。
  • 尽量不要使用 eval, new Function(),document.write(),document.writeln(),innerHTML,document.createElement() 等可执行字符串的方法。
  • 如果做不到以上几点,也必须对涉及 DOM 渲染的方法传入的字符串参数做 encodeURI 或 encodeURIComponent 转义。

2、持久型 XSS

该种攻击一般存在于 Form 表单交互的提交,如文章留言,提交文本信息等。即将内容经正常功能提交进入数据库持久保存,当前端页面获得后端从数据库中读出的注入代码时,恰好将其渲染执行。

与 1 中非持久型注入不一样,其来源是之前非法提交的存储在数据库中的数据,持久型 XSS 攻击不需要诱骗点击,黑客只需要在提交表单的地方完成注入即可。

防御手段如下:

  • 输入过滤和输出编码,永远不要相信用户的输入,对用户输入的数据做一定的过滤(前后端都需要,避免如前端抓包工具绕过限制),如使用 js-xss 来实现。
  • 内容安全策略 (CSP),是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本 (XSS) 和数据注入攻击等,本质上就是建立白名单。如需了解更多 CSP 属性,请查看 Content-Security-Policy 文档
  • HttpOnly Cookie,这是预防XSS攻击窃取用户cookie最有效的防御手段。

SQL 注入攻击

SQL 注入即攻击者利用这个漏洞,可以访问或修改数据,或者利用潜在的数据库漏洞进行攻击。

攻击原理

攻击者将 SQL 命令插入到 Web 表单提交或在表单输入特殊字符,最终达到欺骗服务器执行恶意的 SQL命令。一次 SQL 攻击大致过程是:获取用户请求参数,拼接到代码当中,SQL语句按照我们构造参数的语义执行成功。SQL 注入可能是因为:

  • 针对用户提交信息如转义字符处理不全,如输入验证和单引号处理不当
  • 后台查询语句处理不当,开发者完全信赖用户的输入,未对输入的字段进行判断和过滤处理,直接调用用户输入字段访问数据库
  • SQL 语句被拼接,攻击者构造精心设计拼接过的SQL语句,来达到恶意的目的。如构造语句:select * from users where userid=123; DROP TABLE users; 直接导致 users 表被删除

如何防御

  • 严格限制 Web 应用的数据库的操作权限,给用户仅提供能够满足其工作的最低权限,从而最大限度的减少注入攻击对数据库的危害
  • 后端代码检查输入的数据是否符合预期,严格限制变量的类型,例如使用正则表达式进行一些匹配处理
  • 对进入数据库的特殊字符(',",\,<,>,&,*,; 等)进行转义处理,或编码转换,如 lodash 的 lodash._escapehtmlchar 库
  • 所有的查询语句建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句

DoS 攻击

DoS(Denial of Service),即拒绝服务,造成远程服务器拒绝服务的行为被称为 DoS 攻击。其目的是使计算机或网络无法提供正常的服务,最常见的 DoS 攻击有计算机网络带宽攻击和连通性攻击。

为了进一步认识 DoS 攻击,需要了解 TCP 三次握手及数据段互换的过程,具体参考下图:
dos-1

在 DoS 攻击中,攻击者通过伪造 ACK 数据包,希望 Server 重传某些数据包,Server 根据 TCP 重传机制,进行数据重传。攻击者通过发送大量的半连接请求,耗费 CPU 和内存资源,实现方式如下图:
dos-2

Web 服务器在未收到客户端的确认包时,会重发请求包一直到链接超时,才将此条目从未连接队列删除。攻击者再配合IP欺骗,SYN 攻击会达到很好的效果。通常攻击者在短时间内伪造大量不存在的 IP 地址,向服务器不断地发送 SYN 包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,正常的 SYN 请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。

SYN 攻击的问题就出在 TCP 连接的三次握手中,假设一个用户向服务器发送了 SYN 报文后突然死机或掉线,那么服务器在发出 SYN+ACK 应答报文后是无法收到客户端的 ACK 报文的,从而导致第三次握手无法完成。在这种情况下服务器端一般会重试,即再次发送 SYN+ACK给 客户端,并等待一段时间后丢弃这个未完成的连接。这段时间的长度我们称为 SYN Timeout,一般来说这个时间是分钟的数量级,大约为30秒到2分钟。但如果有一个恶意的攻击者大量模拟这种情况,服务器端将为了维护一个非常大的半连接列表而消耗非常多的资源,即数以万计的半连接,将会对服务器的CPU和内存造成极大的消耗。

对于该类问题,可以做如下防范:

  • 缩短SYN Timeout时间,及时将超时请求丢弃,释放被占用CPU和内存资源
  • 限制同时打开的SYN半连接数目,关闭不必要的服务
  • 设置SYN Cookie,给每一个请求连接的IP地址分配一个Cookie。如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,以后从这个IP地址来的包会被一概丢弃

一般来说,第三种方法在防范该类问题上表现更佳。同时可以在Web服务器端采用分布式组网、负载均衡、提升系统容量等可靠性措施,增强总体服务能力。

Javascript 之数据劫持

每当收到候选人简历中有 VUE 的免不了提问“如何实现数据的双向绑定”,往往很多人都是浅尝辄止,简单来说,就是通过以下步骤实现双向绑定:

  • 监听器 Observer,用来劫持所有属性变动,如果有则通知订阅者
  • 订阅者 Watcher,收到属性的变化通知并执行相应的函数,从而更新视图

所以 VUE 双向绑定关键的一环就是数据劫持(Vue 2.x 使用的是 Object.defineProperty(),而 Vue 在 3.x 版本之后改用 Proxy 进行实现),数据劫持即在访问或修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作如修改返回结果,以下专门梳理一下 Javascript 的数据劫持。

Object.defineProperty

通过 Object.defineProperty() 来劫持对象属性的 setter 和 getter 操作,在数据变动时做你想要做的事情,示例代码如下:

Object.defineProperty(obj, key, {
    get()  {
        // 相关操作,最终 return
    },
    set(newVal) {
        // 一些 handle 操作
    }
})

Object.defineProperty 有它存在的问题,比如不能监听数组的变化,对 object 劫持必须遍历每个属性,可能存在深层次的嵌套遍历。那还有没有比 Object.defineProperty 更好的实现方式呢?

Proxy

在上一篇文章中已经有提及到 Proxy。

Proxy 的构造函数能够使用代理模式,即 let proxy = new Proxy(target, handler);,Proxy 构造函数中的两个参数具体是:

  • target 是用 Proxy 包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
  • handler 是一个对象,其声明了代理 target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数

Proxy 其内部功能十分强大的,有13种数据劫持的操作,如get、set、has、ownKey(获取目标对象的所有 key)、deleteProperty等,下面主要梳理 set、get。

get

get 即在获取到某个对象属性值的时候做预处理的方法,其有两个参数:target、key,示例代码如下:

let Obj = {};
let proxyObj = new Proxy(Obj, {
    get: function(target, key) {
        // 如通过 target 判断某个属性是否符合预期
        if (target[‘xx’] > 80) {return “该考生优秀!”}
        else if (!/^[0-9]+$/.test(key)) {return “学号格式不对!”}
        return Reflect.get(target, name, receiver); // Reflect 见文章最后的总结
    }
});

set

set 即用来拦截某个属性赋值操作的方法,可以接受四个参数:

  • target: 目标值
  • key:目标的 key
  • value:当前需要做改变的值
  • receiver:改变前的原始值

还是沿用上面的例子,比如一个考试成绩录入的校验,代码如下:

let validator = {
  set: function(target, key, value) {
    if (key === 'score') {
      if (!/^[0-9]+$/.test(value)) {
        throw new TypeError(‘分数必须为整数');
      }
      if (value > 100) {
        throw new TypeError(‘成绩满分为 100');
      }
    }
    // 对于满足条件的属性直接写入
    target[key] = value;
  }
};
let proxy = new Proxy(obj, handler);
// ...

Proxy 相对 Object.defineProperty,它支持对数组的数据对象的劫持,不用像 VUE 那样要对数据劫持的话,需要进行重载 hack。对于以上提到的嵌套问题,Proxy 可以在 get 里面递归调用 Proxy 即返回下一个”代理“来完成递归嵌套,可以说 ES6 的 Proxy 就是对 Object.defineProperty 的改进和升级。关于嵌套举例如下:

let obj = {
  0801080132:  {
      name: ‘Jason',
      score: 99
  },
  // …
};
let handler = {
  get (target, key, receiver) {
    // 如果属性值不为空或者是一个对象,则继续递归
    if (typeof target[key] === 'object' && target[key] !== null) {
      return new Proxy(target[key], handler)
    }
    return xxx // 返回业务数据
  }
}
let proxy = new Proxy(obj, handler)

Proxy 本质上就是在数据层外加上代理,通过这个代理实例访问里边的数据,这就是 Proxy 实现数据劫持的方式。总结一下:

  • 在 ES6 支持,即是 Javascript 标准层面的方式
  • 强大的功能支持,能够更方便支持业务定制,完全可以取代 Object.defineProperty
  • ES6 Reflect 为了操作对象而提供的新 API,直接静态拥有 Proxy 的 13 种方法。

深入理解现代浏览器架构(part 2)

注,下文大致意译至 inside-browser-part2

上文理解了相关概念之后,本文我们将开始研究这些进程和线程之间发生了什么,如何显示一个网站内容。

我们从一个经典的面试题说起:在浏览器中输入 URL 并点回车后,浏览器内部究竟发生了什么?(also known as a navigation)

一切从浏览器进程开始
通过前文的知识我们知道,在浏览器 Tab 以外发生的操作都是由 browser process 控制的,浏览器进程常见的一些线程为:

  • UI thread,当导航栏里面输入一个 URL 时,输入的内容将被 UI 线程处理
  • network thread,用来处理网络请求和数据的响应。【注】Chrome 72 以后,已将 network thread 单独摘成 network service process
  • storage thread,用于控制文件的读写等操作

A simple navigation

第一步:处理输入

当用户在导航栏输入信息的时候 UI 线程要进行一系列的解析,用来判定是将用户输入信息发送给搜索引擎还是直接请求你输入的站点资源。

第二步:开始导航

当按下回车后,UI 线程将初始化网络请求并返回站点内容,此时 Tab 前的图标展示为加载中状态,然后网络进程进行一系列诸如 DNS 寻址,建立 TLS 连接等操作进行资源请求。这时如果收到服务器的 HTTP 301 重定向响应,它将会告知 UI 线程进行重定向然后它会再次发起一个新的网络请求。

第三步:读取响应

一旦响应体开始响应返回,在必要的情况下它会先检查一下流的前几个字节,然后根据响应头中的 Content-Type 字段来确定响应主体的媒体类型(MIME Type),不过 Content-Type 有时候会缺失或者是错误的。

如果响应体是一个 HTML file,则将响应数据交给渲染进程来进行下一步的工作,如果是 zip 压缩文件或者其它类型的文件,这意味着是一个下载请求,则会把相关数据传输给下载管理器。

这也是浏览器会进行 SafeBrowsing 检查发生的地方,如果请求的域名和响应的内容匹配到某个已知的病毒站点,网络线程将给用户展示一个警告的页面。除此之外,网络线程还会做 Cross Origin Read Blocking(CORB)检查来确定那些敏感的跨站数据不会被发送至渲染进程。

第四步:寻找渲染进程

一旦做完各种检查以后,网络线程确信浏览器可以导航到请求的站点,网络线程将告诉 UI 线程所有数据已经准备完毕,UI 线程接下来寻找一个渲染引擎来渲染页面。

网络请求是有耗时的,浏览器需要对查找渲染进程这一步骤进行优化。在第二步开始,浏览器已经知道要导航的站点,那么 UI 线程预加载一个渲染进程,如一切符合预期则直接用渲染引擎渲染页面即可,否则,如果遇到重定向,这个准备好的渲染进程也许就用不到了,它会被摒弃,这时将会重启一个渲染进程。

第五步:提交导航

到这一步的时候,数据和渲染引擎都已经准备好了,浏览器进程通过 IPC 通知渲染进程去提交本次导航,除此之外,浏览器进程还会将刚刚接收到的响应数据流传递给对应的渲染进程让它继续接收到来的 HTML 数据,一旦浏览器进程监听到渲染引擎的导航已经被提交的消息,则导航这个过程就结束了,进入到文档的加载阶段。

到了这个时候,导航栏会被更新,安全指示符(地址前面的小锁)和站点设置 UI(site settings UI )会被更新,会话历史列表(history tab)也会被更新,这样即可以通过前进后退来切换该页面。

额外的步骤:初始化加载完成

一旦导航(navigation)完成提交,渲染引擎开始着手加载资源以及渲染页面,后续的文章将继续介绍渲染细节。一旦渲染引擎完成渲染,它会通过 IPC 告知浏览器进程(页面及内部的 iframe 页面都触发了 onload 事件),到这个点,UI 线程将停止加载转圈。

额外的补充,慎用 beforeunload 事件,定义的监听函数会在页面被重新导航的时候执行到,因此这会增加重导航的时延。

欲了解更多,请关注或阅读 an overview of page lifecycle statesthe Page Lifecycle API

In case of Service Worker

service worker 可以用来做网络代理或者缓存控制,目的在于给开发者更多的控制权限。

需要重点留意的是 service worker 只是一些跑在渲染进程里面的 JavaScript 代码。那么当有一个导航请求过来,浏览器进程是如何知道有一个 service worker 的呢?

当 service worker 在注册的时候,它的作用范围(scope)被当成一个实例被记录下来(欲了解更多,阅读 The Service Worker Lifecycle ),在导航开始的时候,网络线程会根据请求的域名在已经注册的 service worker 作用范围里面寻找有没有对应的 service worker 实例,如这个 URL 有注册过,UI 线程将寻找一个渲染引擎来执行它的代码,这样,service worker 可能使用之前缓存的数据,也可能发起新的网络请求。

网络线程在收到导航任务后查找 URL 对应的 service worker 实例

UI 线程会启动一个渲染进程来运行 URL 对应的 service worker 代码

总结

本文讨论了导航具体都发生了哪些事情以及浏览器优化导航效率采取的一些技术方案,下文继续了解浏览器如何解析 HTML/CSS/JavaScript 并渲染页面的。

浅入 React Fiber 及相关资料整理

fiber 作为一种数据结构,用于代表某些 worker,换句话说,就是一个 work 单元,通过 Fiber 的架构,提供了一种跟踪,调度,暂停和中止工作的便捷方式。

react fiber 及未来

异步渲染 Dan 提出的异步渲染的概念,异步渲染即在以异步的方式加载的同时给人以同步流程的体验,在老设备上,通过牺牲一些加载时间来获得一种流畅的体验。其实在 React@16 版本中,异步渲染默认是关闭的。
生命周期变更 在 react@16 版本中,虽然依旧支持之前的生命周期函数,但是官方已经说明在下个版本中会将废弃其中的部分,这么做的原因,主要是 reconciliation 的重写导致。在 render/reconciliation 的过程中,因为存在优先级和时间片的概念,一个任务很可能执行到一半就被其他优先级更高的任务所替代,或者因为时间原因而被终止。当再次执行这个任务时,是从头开始执行一遍,就会导致组件的某些 xxxwill 生命周期可能被多次调用而影响性能。

react@16 与其说是一个分水岭,不如说是一个过渡,react@17 才会是掀起风浪的那一个。

递归和动态规划

递归算法是一种直接或者间接调用自身函数或者方法的算法。
递归算法的实质是把问题分解成小规模的同类问题,这些同类问题作为子问题递归调用来表示问题的解。特点如下:

  • 一个问题的解可以分解为几个子问题的解
  • 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样
  • 存在递归终止条件,即存在递归出口

下面通过爬台阶问题来理解一下递归算法,问题描述:一个人爬楼梯,每次只能爬1个或2个台阶,假设有n个台阶,那么这个人有多少种不同的爬楼梯方法?

套用以上的递归特点,思考如下:【问题拆分】可以根据第一步的走法把所有走法分为两类:

  • 第一类是第一步走了 1 个台阶
  • 第二类是第一步走了 2 个台阶

所以 n 个台阶的走法就等于先走 1 阶后,n-1 个台阶的走法 ,然后加上先走 2 阶后,n-2 个台阶的走法。

用公式表示就是:

f(n) = f(n-1)+f(n-2)

有了递推公式,递归代码基本上就完成了一半。那么接下来考虑【递归终止条件】。

从以上公式得知,最终出口是f(2)、f(1)。当有一个台阶时,我们不需要再继续递归,就只有一种走法,即 f(1)=1。f(2) 表示走 2 个台阶,有两种走法,一步走完或者分两步来走,即 f(2)=2。

总结以上思路,递归条件基本如下:

f(1) = 1;
f(2) = 2;
f(n) = f(n-1)+f(n-2)

翻译成代码如下:

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    if (n == 1) return 1;
    if (n == 2) return 2;
    return climbStairs(n-1) + climbStairs(n-2);
};

深入理解现代浏览器架构(part 3)

本文将深入理解一下渲染引擎在渲染页面的时候具体都发生了什么事情。
渲染引擎会触碰到 Web 性能的方方面面,欲了解更多请关注 the Performance section of Web Fundamentals

渲染进程处理页面内容

渲染引擎负责 Tab 页面内发生的所有事情,主线程处理了大部分用户加载到的代码,如若使用了 web worker 或 service worker,与之相关的代码将会由 worker thread 处理。

渲染进程的主要任务是将 HTML,CSS,以及 JavaScript 转变为我们可以进程交互的网页内容。

Renderer process with

渲染进程中包含线程有:

  • 一个主线程(main thread)
  • 多个工作线程(worker threads)
  • 一个合成器线程(compositor thread)
  • 多个光栅化线程(raster thread)

浏览器渲染及性能优化

blog issue 中已有类似的文章,这里不再原文翻译,具体请参考 浏览器渲染及性能优化

前端工程化相关

1. 用 husky 和 lint-staged 构建 Git commit 代码检查工作流

husky 安装后,可以很方便的在 package.json 配置 git hook 脚本。

  • 待提交的代码 git add 添加到暂存区;
  • 执行 git commit;
  • husky 注册在 git pre-commit 的钩子函数被调用
    接下来可以做一些 Eslint,commit log 校验
"husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "*.js": [
      "eslint --fix",
      "git add"
    ]
  },

pre-commit 只 Lint 改动的

Feedly 的工程师 Andrey Okonetchnikov 开发的 lint-staged 就是基于这个想法,其中 staged 是 Git 里面的概念,指待提交区,使用 git commit -a,或者先 git add 然后 git commit 的时候,你的修改代码都会经过待提交区。那么就可以在这里做 lint 操作。

commitlint 规范 commit log

commit-msg 搭配 commitlint,它可以帮助我们 lint commit messages,如果我们提交的 log 不符合规范,直接拒绝提交。可以在 config 配置文件中优化相关信息。

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'scope-case': [2, 'always', ['lower-case', 'pascal-case', 'camel-case']],
    'subject-case': [2, 'never', []]
  }
};

2. conventional-changelog-cli 生成 changeLog

目前我们的工作量和他们类似:

Recommended workflow

  1. Make changes
  2. Commit those changes
  3. Make sure Travis turns green
  4. Bump version in package.json
  5. conventionalChangelog
  6. Commit package.json and CHANGELOG.md files
  7. Tag
  8. Push

如我们项目中 package.json script 中{ "bump": "SKIP_TAG=true npm run release"},去执行5、6、7、8的操作。

react hooks 基础整理与进阶

hooks 相关基础性知识总结

React Hooks 是 React 16.7.0-alpha 版本推出的新特性,文章一下都简称 hooks。

hooks 与 Redux/mobx 解决的不是同一类问题,Redux/mobx 解决的状态共享问题:

  • 组件间(可能跨层级)如何共享状态?即订阅状态,响应变化等
  • 当数据源发生变化时(如异步事件发生时),如何更新共享状态?

hooks 其根本不是解决状态共享的问题,解决的问题是如何抽离、复用与状态相关的逻辑,是继 render-propshigher-order components 之后的第三种状态共享方案,不会产生如类组件 JSX 嵌套地狱问题。

hooks 的好处是:

  • 更 FP,更新粒度更细,将 UI 渲染与状态分离
  • hooks 可以引用其它 hooks,这就是上面提到的逻辑复用

常用内置 hooks

useState

在 hooks 之前,开发组件主要是类组件和函数组件,函数组件没有 state,只能简单的将 props 映射成 view。useState 让开发者能够在函数组件里面拥有 state 并能修改 state。一个简单的例子:

import React, { useState } from 'react';
function Example() {
  // Declare a new state variable, which we'll call "count"
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

useEffect

useEffect 是用于处理各种状态变化造成的副作用,也就是说只有在特定的时刻,才会执行的逻辑。

hooks 的特点是方便 connect 一切,包括通 HTTP 获取数据流、异步事件监听与派发等都可以使用之,利用 useEffect 也可以代替一些生命周期,如事件订阅与销毁。useEffect 就是用来处理这些功能可能产生的副作用的,以下通过 Http 获取数据填充模板来说明。

function App () {
  const [data, setData] = useState({ xx: [] });
  useEffect(async () => {
    const result = await fetch(xxx);
    setData(result.data);
  }, []);
  return (
    <ul>
      {data.xx.map(item => (
        // ...
      ))}
    </ul>
  );
}

通过 useEffect 来处理副作用,传递一个空数组作为 useEffect 的第二个参数,这样就能避免在组件更新执行 useEffect 而造成的死循环。

useEffect 的第二个参数可用于定义其依赖的所有变量。如果其中一个变量发生变化,则 useEffect 会再次运行。如果包含变量的数组为空,则在更新组件时 useEffect 不会再执行,因为它不会监听任何变量的变更。

useReducer

利用 hooks 来创建一个 useReducer,代码如下:

function useReducer(reducer, initialState) {
  const [state, setState] = useState(initialState);
  function dispatch(action) {
    const nextState = reducer(state, action);
    setState(nextState);
  }
  return [state, dispatch];
}

一个使用 useReducer 的例子:

function todosReducer(state: ImmutableType<State> = initialState, action: Action) {
  switch (action.type) {
    case XXX:
      return nextState; // 伪代码
    default:
      return state;
  }
}
// action useTodos
function useTodos() {
  const [todos, dispatch] = useReducer(todosReducer, []);
  return [todos,  dispatch({ type: "add", text })];
}

不过需要注意的是,每次使用 useReducer 都是局部的数据状态管理,不会像 redux 一样可以全局持久化,如果要维持一个全局状态,可以搭配 useContext 一起使用。一个比较好的最佳实践可以参考 redux-react-hook

hooks 实现原理

开发者需要遵循的两条规则:

  • Don’t call Hooks inside loops, conditions, or nested functions
  • Only Call Hooks from React Functions

正如这篇博文 React hooks: not magic, just arrays 所描述的那样,hooks 就是通过一张类链表的关系来维持 state 和 setters 的关系,即初始化的时候,创建两个数组 state 和 setters,cursor 设置为 0,第一次调用 useState 的时候,会将 setXX 函数放入到 setters 数组中,把 useState 的初始化数据放入到 state 数组中。以此类推,需要注意的是,每一次重新渲染,cursor 都会重置为 0。

可以参考以上原文的 useState 对应数组变化的流程图:

有了以上的基础,我们更进一步,useState 本身是无状态的,那么它是如何获取前一次的 state 做 diff 的呢?

在执行函数组件的 useState 的时候,在对应的 Fiber 对象上 memoizedState 记录对应关系,返回 useState 执行的结果,而 next 指向的是下一次 useState 对应的 hook 对象,即:

hook1 => Fiber.memoizedState
state1 === hook1.memoizedState
hook1.next => hook2
state2 === hook2.memoizedState
hook2.next => hook3
state3 === hook2.memoizedState

这也就能和以上流程对应起来了,确实是一个 just Array 的关系。现在看看开头标明的,hooks 不能使用嵌套,循环和条件判断中,正是因为 state 和 setters 的一一对应关系,如上三个 hooks 的例子,如果下次执行 useState 的时候,因为如某个判断条件导致某个 useState 没执行,那么这个一一对应关系就乱套了。

那么最后,执行 useState 后如何更新 state 并执行 render 呢,和前面提到的 setState 一样的,可以参考关于 React setState,你了解多少?

图论算法

todos:

  • 图的表示:邻接矩阵和邻接表
  • 遍历算法:深度搜索和广度搜索(必学)
  • 最短路径算法:Floyd,Dijkstra(必学)
  • 最小生成树算法:Prim,Kruskal(必学)
  • 实际常用算法:关键路径、拓扑排序(原理与应用)
  • 二分图匹配:配对、匈牙利算法(原理与应用)
  • 拓展:中心性算法、社区发现算法(原理与应用)

参考:

关于编译和执行

编译和执行

历史上,计算机语言分为两组:静态语言(例如,Fortran 和C,其中变量类型是在编译时静态指定的)和动态语言(例如,Smalltalk 和JavaScript,其中变量的类型可以在运行时改变),主要区别是变量类型的确定。

静态语言通常编译成目标机器的本地机器代码(或汇编代码)程序,该程序在运行时直接由硬件执行。动态语言由解释器执行,不产生机器语言代码。

后来,虚拟机(VM)的概念开始流行,它其实只是一个高级的解释器,虚拟机使语言移植到新的硬件平台更容易。因此,VM 的输入语言常常是中间语言。例如,一种编程语言(如 Java )被编译成中间语言(字节码),然后在VM( JVM )中执行。

另外,现在有即时(JIT)编译器。JIT 编译器在程序执行期间运行,即时编译代码。而原先在程序创建期间,运行时之前执行的编译器称为 AOT 编译器。

一般来说,静态语言才适合 AOT 编译为本地机器代码,因为机器语言通常需要知道数据的类型,而动态语言中的类型事先并不确定。因此,动态语言通常被解释或 JIT 编译。

在开发过程中,AOT 编译开发周期长(从更改程序到能够执行程序以查看更改结果的时间),总是很慢。但是 AOT 编译产生的程序可以更可预测地执行,并且运行时不需要停下来分析和编译。AOT 编译的程序也更快地开始执行(因为它们已经被编译)。

与此相反的是,JIT 编译提供了更快的开发周期,但可能导致执行速度较慢或时快时慢。特别是,JIT 编译器启动较慢,因为当程序开始运行时,JIT 编译器必须在代码执行之前进行分析和编译。

以上就是背景知识。

Reference

Angular 2之变化检测

翻译的这篇文章中,我将深入探讨angular2的变化监测系统。

高级概述

一个angular2应用是一个组件树。

一个angular2应用是一个反应系统,变化监测是它的核心。

每一个组件都会有一个负责检查其模板中定义的绑定的更改检测器,例如这样的绑定:{{todo.text}}和[todo]=”t”。变化监测是由根到叶的深度优先顺序来传播绑定的。

Angular2没有一种通用的机制来实现数据的双向绑定(不过你任然可以实现数据绑定行为和ng-model,阅读这里了解更多)。这就是为什么上图的变化监测图是一颗定向的树并且不是循环的(也就是说是一种树形图),这使得系统的性能变现更好,更重要的是,我们将保障系统更易于预测和调试。

那它到底有多快呢?

默认情况下,变化检测通过树的每一个节点来检查它是否改变了,并且在每一浏览器事件都实现了它。尽管它看起来非常的低效,但在几毫秒内,Angular2变化监测系统能通过成百上千次简单的检查(次数依赖于不同的平台)。

因为在JavaScript语言中不提供给我们对象的变化通知,所以Angular必须保守的要每一次运行所有的检测。但是我们可能知道我们的某些可确定性的属性,例如使用不可改变的或可观察的对象,以前angular不能利用这些,但是现在可以。

不可变对象(IMMUTABLE OBJECTS)

如果一个组件仅仅只依赖于它的输入属性,并且它是不可变的,那么当其绑定属性变化时该组件也会发生改变,因此我们可以跳过该组件的子树变化监测,直到这样的一个属性事件发生变化。当事件发生时,我们能立马监测子树,然后禁用它,直到下一次的变化(灰色的框框代表禁用了变化监测)。

如果我们使用不可变对象,一个大的变化检测树大部分时间都是被禁用的。

实施这个监测是微不足道的,仅仅是设置了一个变化监测策略到Onpush这个方法上。

@Component({changeDetection:ChangeDetectionStrategy.OnPush})
class ImmutableTodoCmp {
  todo:Todo;
}

可观测对象(OBSERVABLE OBJECTS)

如果一个组件仅仅依赖于它的输入属性,并且它是可观测的,那么该组件会随着它的输入属性触发一个事件来改变。因此我们能跳过该组件变化监测树的子树直到这样一个事件被触发,当它发生改变,我们监测一次子树,并且禁用它直到下一次变化。

尽管表面上看它可能类似于不可变对象,但还是完全不同的。如果你有一个组件树使用不可变对象绑定,一个变化必须经历从根开始的所有组件检查,使用可观察对象则不会有这种情况。

让我举一个小例子演示这个问题

type ObservableTodo = Observable<todo>;
type ObservableTodos = Observable<array>>;

@Component({selector:'todos'})
class ObservableTodosCmp {
  todos:ObservableTodos;
  //...
}

ObservableTodosCmp模板为

<todo *ngFor="#t of todos" todo="t"></todo>

最后的ObservableTodosCmp为:

@Component({selector:'todo'})
class ObservableTodoCmp {
  todo:ObservableTodo;
  //...
}

正如你所看到的,这里Todos组件只引用到一个可观察的todo数组。所以在一个Todo中看不到变化。

当这个可监测的todo触发一个事件,该事件处理句柄将会从根的路径到那个改变的Todo组件来处理监测。

如我们仅仅使用了可观测对象,当我们启动它,Angular将会监测所有的对象。

所以第一遍监测过后的状态如下

来看看第一个可变化的todo触发一个事件,该系统将会转换到下面的状态

在监测了App_ChangeDetector, Todos_ChangeDetector和第一个Todo_ChangeDetector后,它将变回这个状态

假设变化的发生很少并且是一颗平衡的组件数,使用可观察者对象的变化检查的复杂度从O(N)到O(logN),其中的N是绑定系统的数目。

这种能力不需要依赖于任何特殊的库,任何一个可观测的库的实现就是那么几行代码。

可观测对象会导致级联更新吗?

可观察到的对象有坏名声,因为它们会导致级联更新。任何有使用过依赖于可观测模块框架来构建大型应用的人都知道我在讲什么。一个可观测对象的更新能导致一串其它可观测对象触发更新,也在做同样的事情,沿途的视图也将会被更新,这样的系统是很难去debug的。

在Angular2中使用可观测的对象显然不会有这样的问题,一个可观测对象的事件触发仅仅是作为下次从当前目录到组件跟目录的监测,然后,通过节点树的深度优先的顺序启动正常的变化检测过程,因此更新的顺序不会因为你是否使用过可观测对象而改变,这是非常重要的。使用可观测对象成为了一种简单的性能优化但不会改变你对系统的认知。

我们是否必须到处使用可观测的和不可变的对象才能体现它的好处呢?

答案是否定的,你没必要这么做。你可以使用到你应用的某一处(例如某处一张巨大的表),这块的性能将会受益,更有甚者,你可以构建不同的组件来使他们的性能最优,举个例子,一个“可观测的组件”可以包含一个“不可变属性的组件”,该“不可变属性组件”本身可以包含一个“可观测的组件”,即使在这样的情况下,变化检测系统将减少所需的传输的数量的变化。

无特例

对不可变属性和可观测对象的支持不需要(baked into)变化监测,这种类型的组件不需要用一种特殊的方式来处理,所以你可以使用一种智能方式的变化监测来写你的指令,举个例子,想象一下你的内容每N秒更新一次。

总结

  • Angular2应用是一个是反应系统
  • 变化检测系统从根到叶的传播绑定
  • 不像Angular1.x,变化监测图是一颗有向树,该系统使得性能表现更好和更可预测的
  • 默认情况下,变化监测系统将会走遍整棵树,但是如果你使用不可变属性或者可观测对象,你可以利用他们来监测树在局部是否有真正的改变
  • 这些优化组合不会扰乱提供的变化监测保障

本文翻译文献出处

http://victorsavkin.com/post/110170125256/change-detection-in-angular-2

原型、原型链以及几种继承方式

原型

关于创建对象,最普通的办法就是工厂模式,工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

构造函数模式

一个简单的构造函数如下:

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function() {alert(this.name); };
}

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,如上每个实例都有一个名为 sayName() 的方法,但那两个方法不是同一个 Function 的实例。解决办法是构造函数中的方法指向某个外部函数,这样做问题是多个方法没有封装性。

原型模式

原型模式不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面的例子所示:

function Person(){}
Person.prototype.name = "Nicholas”; 
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer”; 
Person.prototype.sayName = function(){ alert(this.name); };

创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向原型对象, 该对象是所有实例共享的属性和方法,简单说就是 prototype 是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法,与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。

可以通过下图来理解上面的表述:
oop_1

tips: 使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中;通过 isPrototypeOf() 方法来确定是否是对象实例。

更简单的原型语法是使用对象字面量来重写这个原型对象,但通过 constructor 已经无法确定对象的类型了,也可以使用如下方式操作。

function Person(){} 
Person.prototype = {
  constructor : Person, 
  // ...
};

原型模式的问题是,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,原型模式的最大问题是由其共享的本性所导致的。如在原型对象里边有一引用型属性,那么对象实例的修改,就会对其他对象实例造成影响。

解决办法是组合使用构造函数和原型模式,下面的代码解决了前面提到的问题。

function Person(name){ 
  this.name = name; 
  this.friends = ["Shelby", "Court"]; 
} 
Person.prototype = {
  constructor : Person,     
  sayName : function(){alert(this.name);}
}

在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName() 则是在原型中定义的。而修改了某一个对象实例的 friends并不会影响到其他对象实例的 friends,因为它们分别引用了不同的数组。

用以下关系图来总结上面的内容:
prototype-2

原型链与继承

ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本**是利用原型让一个引用类型继承另一个引用类型的属性和方法。实现继承是让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,如此层层递进,就构成了实例与原型的链条,这就是所谓原型链的基本概念。关系图如下:
prototype-3

原型链的问题有:

  • 如以上介绍的原型对象引用类型在各个对象实例共享的问题
  • 子类无法传参问题

那么除了直接使用原型实现继承,还有其他的方式吗?

借用构造函数继承

这种技术的基本**相当简单,即在子类型构造函数的内部调用超类型构造函数,示例代码如下:

function Person(name, age) {
    this.name = name,
    this.age = age,
    this.setName = function () {}
  }
  Person.prototype.setAge = function () {}
  function Student(name, age) {
    Person.call(this, name, age);
    // ...
  }

这种方式只是实现部分的继承,如果父类的原型还有方法和属性,子类是拿不到这些方法和属性的,若将其写在构造函数中复用就无从谈起了。

组合继承(原型链+借用构造函数)

其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType (name) {
  this.name = name,
  this.color = [‘red’, ‘blue'];
}
SuperType.prototype.setAge = function () {
  // ...
}
function SubType (name, age) {
  SuperType.call(this, name);
  this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.setAge = function () {
  // ...
}

组合继承的优点是可以继承实例属性,也可以继承原型方法,解决了引用属性的共享问题,可传参,缺点是调用了两次父类构造函数,生成了两份实例。

那要优化生成两份实例的问题,可以使用 SubType.prototype = new prototype; 的方式,缺点是没办法辨别实例是子类还是父类创造的。

寄生组合继承方式

直接上高级程序设计上的代码示例吧:

function object(original){ 
  function F() {}
  F.prototype = original;
  return new F();
}
function inheritPrototype(subType, superType) {  
  var prototype = object(superType.prototype);   
  prototype.constructor = subType;   
  subType.prototype = prototype; 
}
function SuperType(name) {
  this.name = name;   
  this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function() {alert(this.name); };
function SubType(name, age) {   
  SuperType.call(this, name);   
  this.age = age; 
}
inheritPrototype(SubType, SuperType); 
SubType.prototype.sayAge = function(){alert(this.age);}

这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性,且原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()。

ES6 class 继承

ES6 的继承机制与之前的方式完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this,其本质还是原型链的继承。

React 组件的生命周期

React Native 组件的生命周期

最近在开发金钥匙经理端 RN 版本的时候,经常会用到组件生命周期的相关的方法,刚开始接触有些迷糊,现在整理 React Native 组件的结构和生命周期。

生命周期函数

每一个组件都有一些生命周期方法,通过重写这些方法方便在程序运行的过程中使用。如带有 will 的方法被执行则表示某个状态的发生,RN 中的生命周期方法大致归类如下三类:

Mounting

表示调用某个被创建的组件实例

  • getInitialState & getDefaultProps

这两个回调函数分别表示组件最初被创建渲染后调用,用来获取初始化的 state 和 props,这两个方法在组件中全局只被调用一次。

  • componentWillMount

在组件第一次绘制之后,会调用 componentDidMount(),通知组件已经加载完成,需要注意的是,这个阶段不会重新渲染组件视图

  • render

该方法在组件中是必须的,一旦调用,则去检查 this.props 和 this.state 的数据并返回一个 React 元素。render() 方法不能修改组件的 state,同时需要注意的是,shouldComponentUpdate() 方法必须返回 true,否则将不会再执行 render() 方法。

  • componentDidMount

这个组件方法表示在组件第一次绘制之后,componentDidMount() 会被调用,用来通知组件已经加载完成,通常我们会在这里去从服务器拉取数据来渲染页面。

Updating

表示 props 或者 state 的改变,将会导致组件的重新渲染

  • componentWillReceiveProps

表示组件收到了新的属性(props),调用 componentWillReceiveProps() 来进行某些操作,通常用来更新 state 的状态,在这通过比较 this.props 和 nextProps 来 setState。

  • shouldComponentUpdate

当组件接收到新的 state 和 props 改变,该方法将会被触发调用。与前一个方法类似, nextProps 用来比较 state 和 props 是否有改变,通过检查来决定 UI 是否需要更新(返回 true 或 false),在一定程度上可以提高性能。

  • componentWillUpdate

表示开始准更新组件,即调用 render() 来更新界面,该方法被调用的条件是 shouldComponentUpdate() 方法最终返回 true。需要注意的是,在这个函数里面,不能使用 this.setState 来修改状态。

  • componentDidUpdate

表示调用 render() 方法完成了界面的更新,需要注意的是,该方法在初始的 render 中将不会被调用。

Unmounting

组件的销毁阶段

componentWillUnmount 表示组件即将被销毁或退出该组件,在这里常用来移除一些功能方法,如timeout事件或者abort相关的request。

生命周期的过程

生命周期表示从开始生成到最后销毁所经历的状态,网上已有很好的路程图,收藏该流程图如下:

Alt lifecycle

从图中可以清晰的划分为以下三类:

  • 组件第一次绘制阶段,如图中的上面虚线框内,在这里完成了组件的加载和初始化;
  • 组件的运行和交互阶段,如图中左下角虚线框,这个阶段组件可以处理用户交互,或者接收事件更新界面;
  • 组件的销毁阶段,如图中右下角的虚线框中,在这里常用来移除一些功能方法。

通过前面的图可以看出,在生命周期函数中只有以下三个才能调用 setState() 方法的,这些方法为:componentWillMount、componentDidMount、componentWillReceiveProps。

reference

详解 git 的四种 merge 方式

团队中不少开发同事基本使用的是 --no-ff 的方式合并,但是这样会有很多无用的 commit log 信息,且 git log graph 不够线性。下面参考别人的动图,总结一下git的四种 merge 方式。

git 中的分支其实就是一类文件记录了分支所指向的 commit id,以下都用 master 和 test 分支举例。

fast-forward

如果待合并的分支在当前分支的下游,也就是说没有分叉时,快速合并是不错的选择。这种方法相当于直接把 master 分支 HEAD 指针移动到了 test 分支最近的一次提交,如下图:

--no-ff

快速合并给人的感觉是不知道主干分支 master 代码合并自哪里,其实可以强制指定为非快速合并,只需加上 --no-ff 参数,如:git merge –no-ff test。这种合并方法会在 master 分支上新建一个提交节点,从而完成合并,如下图:

总结一下以上两种合并方式,快速 merge 和 --no-ff 两种方式都有风险存在,即如果 test 分支删除可能会丢提交的代码。

squash

squash 会在当前分支新建一个提交节点,但不会保留对合入分支的引用。如下图:

rebase

rebase 与 merge 不同,它会将合入分支上超前的节点在待合入分支上重新提交一遍,且保留了原本的 commit log 信息,如下图,B1、B2 会变为 B1’、 B2’,看起来会变成线性历史。

cherry-pick

cherry-pick 给你想要的自由度,即把想要的某一 commit 提交节点合并到你想要的分支。

通过比较,我们最终选择了 rebase 的 merge 方式,确保了提交 log 的线性和清晰度。

Service Worker

Service Worker 简介

从 Web Worker 到 Service Worker

Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。

Service Worker 是基于 Web Worker 的事件驱动的,执行的机制都是新开一个线程去处理一些额外的任务。对于 Web Worker,可以使用它来进行复杂的计算,因为它并不阻塞浏览器主线程的渲染。而 Service Worker,可以用它来进行本地缓存或请求转发,相当于一个浏览器端本地的 proxy。它可以认为是使用了Web Worker 技术来处理网络请求、响应等方面的事务。

Web Workers 的缺点

  • 不能跨域加载JS
  • worker 内代码不能访问 DOM(更新UI)
  • 浏览器兼容

Angular Universal相关整理

服务器端渲染流程

预渲染流程:

使用构建工具生成静态 HTML ;
将生成的 HTML 部署到 CDN 服务器;
CDN 提供服务器视图;
服务器视图到客户端视图转换(见下文)

服务器重绘流程:

HTTP GET 请求发送到服务器;
服务器生成一个包含渲染的 HTML 和内联 JavaScript 的以便“Preboot”的页面(可以选择添加序列化数据进行缓存);
服务器视图到客户端视图转换(见下文)

服务器视图到客户端视图转换流程:

浏览器从服务器接收初始化 payload;
用户看到服务器视图;
Preboot 创建隐藏的 div 以用于客户端初始化并开始记录事件;
浏览器对其他资源进行异步请求(如 image,css 等);
一旦加载完外部资源,Angular 客户端初始化开始;
客户端视图呈现给由 Preboot 创建的隐藏 div;
初始化完成后,Angular 客户端调用 preboot.done();
为了调整应用程序状态以反映用户在 Angular 初始化完成之前所做的更改(如 click 事件等),Preboot 事件将被重播;
Preboot 切换隐藏的客户端视图 div 为可见的服务器视图 div;
最后,Preboot 在可见的客户端视图上执行一些清理,包括设置焦点

Angular Universal相关整理

关于 React setState,你了解多少?

年初在面试相关候选人的时候总是会问到 setState,其中不少人含糊其辞,没有很好的理解,这里再梳理记录一下吧。

setState 的基本用法

当组件 state 数据有变更的时候,通过 setState(updater, callback) 这个方法来告诉组件更新数据,即能驱动组件的更新过程,触发 componentDidUpdate、render 等一系列生命周期函数的调用,如有需要则重新渲染。该方法是异步执行的过程,特点是批量执行且通过一次更新来确保性能,正因为它是异步执行的,在使用setState 改变状态之后,通常立刻通过 this.state 去拿最新的状态往往是拿不到的。举个例子:

addCount = () => {
    this.setState({ index: this.state.count + 1 });
    this.setState({ index: this.state.count + 1 });
}

在以上代码中,调用一个累加方法,同步调用两次 setState,其效果最终只是+1,如果想获取最新的 state 的话可以在componentDidUpdate 或者 setState 的回调函数里获取,也可以通过第一个参数 return function 的方式,具体代码实例如下:

addCount = () => {
    this.setState((prevState, props) => {
      return {count: prevState.count + 1};
    });
    this.setState((prevState, props) => {
      return {count: prevState.count + 1};
    });
}

深入理解 setState

setState 通过一个队列机制来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并后放入状态队列,而不会立即更新 state,通过队列机制来批量更新 state。

这里记录一下其详细过程:

1、调用 setState 方法,其内部判断第一个参数是否是 Object 或 Function,随后调用 enqueueSetState;
2、通过查看 enqueueSetState 方法的源码,其主要是做了两件事: 将新的 partialState 入队(_pendingStateQueue 数组中),执行 enqueueUpdate;

enqueueSetState: function(publicInstance, partialState, ...) {
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
  if (internalInstance) {
    (internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = [])).push(partialState), 
    enqueueUpdate(internalInstance);
  }
}

setState 是通过 enqueueUpdate 来执行 state 更新的,那 enqueueUpdate 是如何实现更新 state 的?继续往下走。
3、enqueueUpdate 如果当前正处于创建/更新组件的过程,就不会立刻去更新组件,而是先把当前的组件放在 dirtyComponent 里,这里也很好的解释了上面的例子,不是每一次的 setState 都会更新组件。否则执行 batchedUpdates 进行批量更新组件;
img

贴一下以上 3 的源码助于理解其中的过程,如下:

function enqueueUpdate(component) {
 // ...  
 if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
}

4、batchedUpdates 是将当次所有的 dirtyComponent 遍历,执行其 updateComponent 来更新组件,如调用 componentDidUpdate 生命周期方法来更新组件。

facebook/react#11527 (comment)

深入理解现代浏览器架构(part 1)

注,下文大致意译至 inside-browser-part1

CPU,GPU,内存和多进程架构

在接下来的4个 blog 系列里边,将探索 Chrome 浏览器的底层架构和渲染管道细节,旨在弄明白从代码转换成可视化界面的过程,以此来写出更优秀性能的网站或应用。

本文是该系列的第一遍,我们先了解一些关键的计算机术语以及 Chrome 浏览器的多进程架构。

CPU和GPU

想要理解浏览器的运行环境,首先要搞明白计算机的一些核心概念以及它们的作用。

CPU

CPU 即 Central Processing Unit,可以理解成计算机的大脑。CPU core 可以想象成办公室里的工人,精通天文地理琴棋书画,可以串行地一件接着一件处理交给它的任务。现在的硬件设备多以多核为主,具备更优的计算性能。

GPU

图形处理器 GPU(Graphics Processing Unit)是计算机的另一组成部分,能通过多核处理简单的任务,这样就具备了极强的计算能力。GPU 顾名思义就是用来处理图形的,在说到图形 useing GPU 或 GPU backed 时,人们就会联想到图形快速渲染或者流畅的用户体验相关的概念。最近几年来,随着 GPU 加速概念的流行,在 GPU 上单独进行的计算也变得越来越多了。

在手机或者电脑上打开某个应用程序时,背后其实是 CPU 和 GPU 支撑着这个应用程序的运行。

进程和线程上执行程序

在往下了解前,先理解清楚进程和线程的概念。进程可以看成是正在执行的应用程序,而线程则是跑在进程里面的,一个进程可以有一个或者多个线程,这些线程可以执行任何一部分应用程序的代码。
process-thread

在启动一个应用程序后,操作系统将会为这个程序创建一个进程同时还为这个进程分配一片私有的内存空间,这片空间会被用来存储所有程序相关的数据和状态。当关闭程序,这个程序对应的进程也会随之消失,进程对应的内存空间也会被操作系统回收掉。

而在应用程序中,为了满足功能的需要,进程会创建其它新的进程来处理其他任务,这些创建出来的新进程拥有全新独立的内存空间,不能与原来的进程共享内存,很多应用程序都会采取这种多进程的方式来工作,因为进程和进程之间是互相独立的它们互不影响。如果这些进程之间需要通信,可以通过IPC机制(Inter Process Communication)来进行。

浏览器架构(Browser Architecture)

那么浏览器是怎么使用进程和线程的呢?一种可能是单进程配合多个线程工作,另一种是启动多进程,每个进程里面有一些线程,不同进程通过 IPC 进行通信。

Browser Architecture

上图架构基本包含了浏览器架构的具体实现,但在现实中并没有一个标准化的浏览器实现标准,不同浏览器的实现方式可能会完全不一样,注,下文以 Chrome 浏览器为例,介绍浏览器的多进程架构。

各个进程如何分工协作的?

在 Chrome 中,主要的四个进程为:

  • Browser(浏览器进程),负责浏览器 Tab 的前进后退按钮、地址栏、书签栏的工作,处理浏览器一些不可见的底层操作,比如网络请求和文件访问
  • Renderer(渲染进程),负责一个 Tab 内页面显示相关的工作,即渲染引擎
  • Plugin(插件进程),负责控制网站使用到的插件
  • GPU 进程,负责处理整个应用程序的 GPU 任务,之所以要被独立为一个进程,是因为它要处理来自不同应用程序的渲染请求并画到面板上

process

这4个进程之间的关系是什么呢?我们从浏览器的地址栏里输入 URL 后点击回车说起。

  • Browser Process 会对这个 URL 发送请求,获取该链接的 HTML 内容
  • 将 HTML 交给 Renderer Process 来负责解析 HTML 内容,解析完成后,Renderer Process 计算得到图像帧
  • 并将这些图像帧交给 GPU Process 来将其转化为图像显示到屏幕上
  • 如若解析遇到需要请求网络的资源又会返回来交给 Browser Process 进行处理
  • 那在浏览器有装插件的情况下,同时通知 Browser Process 加载插件资源,执行插件代码。

process coordinating

Chrome 多进程架构的好处

  • 多进程可以使浏览器具有很好的容错性
    不同应用程序可以跑在浏览器不同 Tab 上,Chrome 会为每个 Tab 单独分配一个属于它们的渲染引擎,多进程架构使得每一个渲染引擎运行在各自的进程中,相互之间不受影响。这些跑在渲染引擎的代码,如若出现 BUG 导致渲染引擎崩溃,其他页面还可以正常的运行而不受影响。
  • 安全性和沙盒性(sanboxing)
    操作系统可限制进程的权限能力,渲染引擎会经常遇到安全性问题或者恶意的代码,针对这些 web 安全问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠。
  • 更好的运行效能
    在单进程的架构中,各个任务相互竞争抢夺 CPU 资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。对于多进程,它们通常包含通用基础结构的副本从而消耗更多资源,所以为了节省内存,Chrome 会限制启动的进程数,当进程数达到一定的界限后,Chrome 会将访问同一个网站的 Tab 都放在一个进程里面跑。

多进程架构优化

节省内存(Servicification)

通过将和浏览器本身(Chrome)相关的部分拆分为一个个不同的服务或聚合为一个进程。如果 Chrome 应用运行在高性能的硬件上,则相关进程服务放到不同的进程运行以提高系统的稳定性,反之,则放在同一个进程里面执行来减少内存的占用。

单帧渲染进程(Site Isolation)

Chrome 为不同的跨站 iframe 启用不同的渲染引擎。同源策略是浏览器最核心的安全模型,而进程隔离是隔离网站最有效的办法了,再加上 CPU 存在 Meltdown and Spectre 的隐患,网站隔离变得势在必行。

在 Chrome 67 版本之后支持了该特性

Diagram of site isolation

总结

理解了概念之后,后边的文章我们将开始研究这些进程和线程之间发生了什么,如何显示一个网站内容。

angular2 管道(pipes)

1.管道

我们的很多应用场景是基于这样一个简单的任务,获取数据,转换过滤后再展示给用户,angular2中我们引入了管道(Pipes)的概念,即管道是用来将数据模型中的数据经过过滤转换,然后通过模板内并展示给用户,这样做是为了更好的用户体验,例如从视图模型中直接获得的数据,不一定完全是我们想要的格式或者适合于人们查看的,举个例子,我们需要获取一个班级所有学生的平均分:

<div>this class‘s average is: {{getAvgScore()}}</div>

虽然通过视图模型中的getAvgScore方法获取到了我们需要的平均分来展示了,但是可能我们获取到的数据是一个除不断的多位小数位的数据,那么这样的结果看上去是不那么顺眼的。除了在视图模型的方法来控制小数位外,我们也可以利用管道来格式化这样的数据,也就是在模板里改变数据的显示格式。这就是angular2中管道的作用。

1.1 管道的用法

在angular2中,管道是在模板中对输入数据进行变换,并输出变换后的结果。在模板的内嵌表达式中,使用管道操作符“|”和其右边的管道方法来实现一个管道操作,使用“:”来向管道传入参数,所有的管道都是沿用这样这的一种机制。我们举个简单的例子来说明一下管道的使用:

import {Component} from 'angular2/core'
@Component({
    selector: 'hero-birthday',
    template: `<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }}</p>`
})
export class HeroBirthday {
    birthday = new Date(1988,3,15); // April 15, 1988
}

我们通过视图组件获取到我们要输出的生日日期birthday,通过插值和模板联系起来,在模板的内嵌表达式中,我们输出生日组件的值是通过管道操作符“|”和其右边的内置管道Data Pipe方法来实现的。至于管道的参数,我们在内置管道Data Pipe方法后边加冒号(:)来添加参数值,并且如果有多个参数的话,我们用多个冒号来区分开参数就好了。

最后要说一下的是,angular2管道可以链式运用。我们可以将多个管道通过“|”链式的书写到一个实用的组合体上,如我们将birthday链到DatePipe和UpperCasePipe上以便我们将生日日期显示为大写,下面的日期将会是APR 15, 1988:

<p>The chained hero's birthday is {{ birthday | date | uppercase}}</p>

1.2 管道的内置方法

为了方便使用,Angular2针对之前的经验,设置了一套常用的内置管道,如DatePipe,JsonPipe,UpperCasePipe, LowerCasePipe,CurrencyPipe,PercentPipe及SlicePipe.其目的是在任何模板中都可以便捷使用。我们将详细介绍他们的具体用法。
DatePipe是对日期\时间数据进行格式变换,在模板中直接使用date来引用DatePipe,参数用来指定所需的格式,需要说明的是,不需要再在视图组件中声明,

 <p>{{day | date: 'yyMMdd'}}</p>

JsonPipe是将Json数据对象转换成字符串格式输出,在模板中使用json来引用JsonPipe,其实现是基于JSON.stringify(),这个管道主要用来调试。

<p>{{key1: "value1", key2: "value2"}} | json</p>

UpperCasePipe&LowerCasePipe用于将输入的字符串转换成大小写,在模板中直接使用uppercase&lowercase即可。

<p>{{“this is a demo” | uppercase}} | json</p>

CurrencyPipe是将获取到的金钱数转换成特定格式的数字,在模板中直接使用currency来引用CurrencyPipe。

<p>{{price | currency: 'USD': true}}</p>

PercentPipe是将数值转换成百分比,在模板中使用percent来引用PercentPipe即可。

<p>{{1.23456 | percent: '1.2-3'}} | json</p>

例子中的“:'1.2-3'”表示调用这个这个管道时传入的参数为“'1.2-3'”,对于PercentPipe,这三个数字分别依次表示最少整数位、最少小数位和最多小数位。

SlicePipe是用来提取输入字符串中的指定切片,在模板中使用slice来引用SlicePipe。第一个参数指定切片的起始索引,第二个参数指定切片的终止索引的下一个。

<p>{{ '01234567890' | slice:1:4 }}</p>

2.自定义管道

通过上一节的内置管道可以看出,angular2内置的管道并不是特别的丰富,更进一步的是angular2允许自定义管道。自定义一个管道需要以下两个步骤:

2.1、声明元数据

和实现一个组件类似,一个自定义的管道也是具有特定元数据的类,如

import {Component,Pipe} from "angular2/core";
@Pipe({name: "anyPipeName"})
class anyPipeNamePipe {...}

Pipe注解为被装饰的类附加了管道元数据,其最重要的属性是name,也就是我们在模板中调用这个管道时使用的名称。上面定义的管道,我们可以在模板中这样使用:

<p>{{ data | anyPipeName }}</p>

2.2 实现transform方法

定义一个自定义的管道必须实现一个预定的方法transform(input,args),其中这个方法的input参数代表输入数据,args参数代表输入参数,返回值将被作为管道的输出。

import {Component,Pipe} from "angular2/core";
@Pipe({name: "anyPipeName"})
class anyPipeNamePipe {
    transform(input,args){
        return ...;
    }
}

通过以上定义一个自定义的管道,需要说明的是这样的一个pipe需要以下这样几个关键的点:

  • 管道是一个带有pipe元数据的类;
  • 这个类实现一个转换方法,通过一个输入值和一个可选的数组字符参数,最终返回转换后的值;
  • 数组参数中的某一项将每一个参数传递给pipe;
  • 我们通知Angular这是一个引入过Angular核心库的@pipe服务
  • 这个@pipe服务带有一个参数,该参数它的值是一个pipe名,我们将使用在一个模板表达式中,它可以是一个合法的Javascript定义。

最后我们通过例子来说明自定义管道的使用,但是需要特别注意的是:

  • 在组件的模板中使用自定义管道之前,需要预先声明一下,即使用Component注解的pipes属性进行声明:pipes:[EzPipe],以便Angular2注入。
  • 如果我们忽视去列出我们的自定义pipe,Angular2将会报错。我们不需要在先前的例子中列出内置指令是因为所有Angular2内置的pipes是预先注册过的,自定义的pipes必须手动注册。

自定义管道完整的例子如下:

import {Component,Pipe} from "angular2/core";
import {bootstrap} from "angular2/platform/browser";

@Pipe({name:"title"})
class TitlePipe{
    transform(input,args){
        return input.split(" ")
                .map(word => word[0].toUpperCase() + word.slice(1))
                .join(" ");
    }
}

@Component({
    selector:"Demo-app",
    template:`  
        <p>{{text | title}}</p>
    `,
    pipes : [TitlePipe]
})
class DemoApp{
    constructor(){
        this.text = "what a wonderful world!";
    }
}

bootstrap(DemoApp);

3.管道状态

3.1 无状态的管道

无状态的管道是一个纯粹的方法,流入的数据将不会记录任何东西,或者不会导致任何的副作用。大多数的管道是无状态的,例如我们之前例子的Datapipe就是一个无状态的pipe。据我们之前了解到的管道,包括angular2内置的管道,都具有这么一个特点,就是其输出仅仅是依赖于输入,这就是angular2中的无状态管道,对于无状态的管道,当视图组件的输入没有变化时,angular2框架是不会重新计算管道的输出的。

3.2 有状态的管道

有状态的管道在概念上类似于面向对象编程类,它们可以管理数据的变换,例如管道创建一个HTTP请求,存储它的返回和显示结果,就是一个有状态的pipe。需要注意的是,检索或请求数据的管道应该要谨慎使用,因为使用网路数据往往会引入错误的条件,在javascript中处理更优于在模板中处理。我们可以为了特定的后端和基本的异常捕获而创建自定义的pipe来减轻任何风险。

angular2对有状态的管道定义的关键在于使用使用Pipe注解的属性“pure”,并设置该属性的值为false即可,其作用是要求angular2框架在每个变化检查周期都执行管道的transform()方法。下面我们给一个例子,实现一个10到0的倒数计时器。

import {Component,Pipe} from "angular2/core";
import {bootstrap} from "angular2/platform/browser";

@Pipe({
    name : "countdown",
    pure : false
})
class CountdownPipe {
    transform(input){
        this.initOnce(input);
        return this.counter;
    }
    initOnce(input){
        this.counter = input;
        this.timer = setInterval(() => {
            this.counter--;
            if(this.counter === 0) 
                clearInterval(this.timer);
        }, 1000);
    }
}
@Component({
    selector:"demo-app",
    template:`<h1>this is a stateful pipe : {{ 10 | countdown }}</h1>`,
    pipes : [CountdownPipe]
})
class DemoApp{} 
bootstrap(DemoApp);

从这个例子中可以看出,自定义管道countdownPipe的输出不仅依赖输出,还依赖与其内部的变化或者运行状态。

而angular2中,AsyncPipe是有状态管道的一个标志性的例子,AsyncPipe它的输入是一个异步对象:Promise对象、Observable对象或者EventEmitter对象,并且自动的订阅(subscrib)输入对象,最终每当异步对象产生新的值,AsyncPipe会返回这个新的值,它的有状态性是因为pipe维护一个输入的订阅并且它的返回值也依赖于这个订阅器。下面给出的例子,我们将用AsyncPipe绑定一个简单的promise给一个view。

import {Component} from 'angular2/core';
// Initial view: "Message: "
// After 500ms: Message: You are my Hero!"
@Component({
    selector: 'hero-message',
    template: 'Message: {{delayedMessage | async}}',
})
export class HeroAsyncMessageComponent {
    delayedMessage: Promise<string> = new Promise((resolve, reject) => {
    setTimeout(() => resolve('You are my Hero!'), 500);
    });
}

3.3 管道无状态和有状态的区别

管道的有状态和无状态的区别,关键在于是否是需要Angular2框架在输入不变的情况下依然持续地进行变化检测,而angular2的无状态的管道是依赖输入的,即同样的输入,总是产生同样的输出。举个例子,例如我们上面的管道,当我们输入一个默认的数字后,输出值依赖其内部的运行状态变化,而无状态的管道,例如一个加减乘除的管道,在Angular2中,它被视为无状态的,因为它的一次输入不会产生多次输出。

实现 new 操作符

实例代码:

function Person(name){ 
  this.name = name;
} 
Person.prototype.getName = function() {
    return this.name;
}

var p1 = new Person('Dan');

console.log(p1); // Person {name: "Dan"}
console.log(p1.__proto__ === Person.prototype); // true

new 操作符实现了如下的功能:

  • 创建一个新对象
  • 新对象的原型指向构造函数的原型对象性,即继承构造函数的原型
  • 改变构造函数 this 的指向到新建的对象,并执行构造函数
  • 判断返回的值是不是一个对象,若是则返回这个对象,否则返回新对象

构造函数如果返回基本类型,则还是会返回原来的 this (新对象)。如果返回的是引用类型,则返回该返回值。(可以自己在上面例子加上代码验证一下)

new 操作符的模拟实现

function createNew(func, ...args) {
    let obj = {};
    // 将空对象指向构造函数的原型链
    Object.setPrototypeOf(obj, func.prototype);
    // obj 绑定到构造函数上,便可以访问构造函数中的属性
    let result = func.apply(obj, args);
    // 如果返回的 result 是一个对象则返回该对象,new 方法失效,否则返回 obj
    return result instanceof Object ? result : obj;
}

Object.setPrototypeOf()是ECMAScript 6最新草案中的方法,相对于 Object.prototype.proto ,它被认为是修改对象原型更合适的方法

写个测试用例:

function Test(name, age) {
    this.name = name;
    this.age = age;
}

let test = createNew(Test, 'Dan', 20);
console.log(test.name); // Dan
console.log(test.age); // 20

理解 RxJS

基础概念了解

在理解 RxJS 之前,先了解一下 Observable 的相关核心概念:

  • Observable 是声明式的,即定义一个订阅者(subscriber)函数,Observable 实例可以发布多个任意类型的值,适用于事件处理、异步编程和处理多个值。
  • subscriber 是一个订阅函数,创建一个 Observable 的实例并传入一个方法做法参数,当这个方法中的异步任务执行完成则执行 observe 对象的方法(也就是事件的发布)。
  • Observe 是用于接收 Observable 发布的数据流,是一个数据对象,作为数据的消费者处理数据或响应事件。它有三个回调函数,其中 next 是必须的。

来一个例子加深一下对上面概念的理解:

// declare a publishing operation
const observable = new Observable(observer => {
  setTimeout(() => {
    observe.next('test demo');
  });
});
// initiate execution
observe.subscribe({
  next: x => console.log(x),
  error: err => console.error(err),
  complete: () => console.log('done')
});

简单实现一个 Observable

根据前面的了解,我们可以大致实现一个 Observable 类。
初始化实例的时候传入 _observeFun,当调用类实例方法 subscribe 时执行传入的方法 _subscribeFun

class Observable {
  _observeFun: Function;

  constructor(observeFun) {
    this._observeFun = observeFun;
  }

  subscribe(observe: any) {
    this._observeFun(observe);
  }
}

这就是一个简易版的观察者模式的实现。

RxJS

RxJS(Reactive Extensions for JavaScript) 是基于 ReactiveX 在 JavaScript 层面上的实现,关于 ReactiveX(An API for asynchronous programming with observable streams) 请参考reactivex.io

RxJS 是一个响应式编程库,它让组合异步代码和基于回调的代码变得更简单,整个库的基础就是 Observable,注意和观察者对象 Observer 区别开。对异步数据如 Ajax、User Events、Animation、Sockets、Workers 提供了一种 Observable 类型的发布订阅实现,输出给开发者使用,参考RxJS Docs

举个例子,RxJS 提供了一个 fromEvent 用于监听相关 DOM 事件,通过 Observable 实现的这个 fromEvent 简单的代码示例如下:

function fromEvent(target, eventName) {
  return new Observable((observer) => {
    const handler = (e) => observer.next(e);
 
    // Add the event handler to the target
    target.addEventListener(eventName, handler);
 
    return () => {
      // Detach the event handler from the target
      target.removeEventListener(eventName, handler);
    };
  });
}

下面是对 fromEvent 的一个应用:

import { fromEvent } from 'rxjs';
 
const el = document.getElementById('my-element');
const mouseMoves = fromEvent(el, 'mousemove');
 
// Subscribe to start listening for mouse-move events
const subscription = mouseMoves.subscribe((evt: MouseEvent) => {
  console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);
});

即当 Observeable 的实例 subscribe() 方法被调用后,实现了一个鼠标的事件监听,每当鼠标移动,通过发布者函数的 observer.next(e) 方法将数据流发布给订阅者。类似于:

el.addEventListener('mousemove', evt => {
   console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);
}, false);

观察者模式在 Web 中最常见的应该是 DOM 事件的监听和触发。对于上面的例子,发布订阅相关信息如下:

  • 订阅,通过 addEventListener 订阅 my-element 视图块的 mousemove 事件
  • 发布,当鼠标在 my-element 移动时,便会向订阅者发布这个消息

fromEvent 和监听 DOM 事件的类比图效果如下:
rxjs-1

RxJS 还有一个重要的概念:Operators,是对可操作的数据流进行一些中间处理的 API,在 subscribe 中的 observer 最终接收到的数据往往是经过 Operator 处理完后的数据。这些 API 是一些工具型函数,把现有的异步代码转换成可观察对象,对 observer stream 进行迭代处理,其中包括对不同类型的值进行例如 Mapping、过滤、组合等,把当前的转成(transformTo)另一个(更多时候配合 pipe 使用。

import { map } from 'rxjs/operators';
 
const nums = of(1, 3, 5, 7);
 
const squareValues = map((val: number) => val * 10);
const squaredNums = squareValues(nums);
 
squaredNums.subscribe(x => console.log(x));

可以将上面代码形象表示成

--1---3--5-----7------
   map(i => i * 10)
--10--30-50----70-----

以上是对 RxJS 的基础了解,RxJS 的核心大致理解成如下图:
RXJS-2

下面我们更进一步。

RxJS 相对于 Promise

RxJS 的核心概念是 Observable(可观察对象),通过以上基础概念理解,我们对比一下 Promise:

  • 可观察对象是声明式的,在被订阅之前,它不会开始执行,Promise 是在创建时就立即执行的
  • 可观察对象能通过 next 提供多个值,Promise 只提供一个
  • 对于正常返回流程,可观察对象提供 Operaters 和 subscribe,而 Promise 只有 .then() 语句

除了可以通过 Observer 的 error 回调来处理外,RxJS 还提供了 catchError 操作符,它允许你在 pipe 中处理已知错误。更重要的是 catchError 提供了 retry 操作符让你可以尝试失败的请求。对于错误处理,由于可观察对象会异步生成值,所以用 try/catch 是无法捕获错误的。

举个例子:

import { ajax } from 'rxjs/ajax';
import { map, retry, catchError } from 'rxjs/operators';
 
const apiData = ajax('/api/data').pipe(
  retry(3), // Retry up to 3 times before failing
  map(res => {
    if (!res.response) {
      throw new Error('Value expected!');
    }
    return res.response;
  }),
  catchError(err => of([]))
);
 
apiData.subscribe({
  next(x) { console.log('data: ', x); },
  error(err) { console.log('errors already caught... will not run'); }
});

Promise 的错误处理方式可以如下:

somethingAync.then(function() {
    return somethingElseAsync();
}, function(err) {
    handleMyError(err);
});

这种方式的缺点就是,没法捕获 somethingElseAsync() 里边的错误,可以使用 Promise 的原型方法 catch。

用法实战

实现一个输入框搜索的功能,大致需要做这么些事情:

  • 监听输入数据
  • 过滤输入数据
  • 防抖
  • 确实输入值有变化才发起请求(比如按某个字符,然后快速按退格
  • 输出结果与输入请求相对应,或者可取消前一异步请求

RxJS 一个简单的实现代码:

import { fromEvent } from 'rxjs';
import { ajax } from 'rxjs/ajax';
import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
 
const searchBox = document.getElementById('search-box');
 
const typeahead = fromEvent(searchBox, 'input').pipe(
  map((e: KeyboardEvent) => e.target.value),
  filter(text => text.length > 2),
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(() => ajax('/api/endpoint'))
);
 
typeahead.subscribe(data => {
 // Handle the data from the API
});

Angular 原生集成了 RxJS,对它的应用可以说比较丰富了,如:

  • EventEmitter 类派生自 Observable
  • HTTP 模块使用可观察对象来处理 AJAX 请求和响应
  • 路由器和表单模块使用可观察对象来监听对用户输入事件的响应

异步复杂度要到什么程度才需要用到Rxjs?

对于群聊,试想一下如果每收到一个 notification 都立刻渲染的话,会有什么问题。通常的做法是批量渲染,即收集一段时间的消息,然后把它们一起渲染出来,例如每一秒批量渲染一次。

用原生 JS 写的话,需要维护一个消息队列池、一个定时器,收到消息,先放进队列池,然后定时器负责把消息渲染出来。

使用 RxJS 的话,很简单:

Rx.Observable
    .fromEvent(ws, 'message')
    .bufferTime(1000)
    .subscribe(messages => render(messages))

其中最为关键的是 bufferTime 这个操作符,可以用下图来概括以上逻辑:
bufferTime

RxJS 具备的 100 多个 Operators 提供了强大的业务支撑能力,使用恰当可是事半功倍。到底什么样复杂的异步操作才不算杀鸡焉用牛刀呢?

RxJS 的设计核心在于响应式(reactive)编程,如果你把一个场景下所有 component/service 的 input 和 ouput 都理解为 stream 之间的 publish 和 subscribe,那自然而然就会选择 RxJS。RxJS 的响应式编程另外一大好处在于扩展性好,复杂场景下添加一个具有更新数据能力的组件带来的代码改动可以做到最小,因为你只需要关注 stream 之间的 dependency。

最后不得不再提的是 Operators,Operator 仅仅是提供一种业务抽象能力,但我觉得模式和思路收益更大,抛去上层扩展,Rx 的内核也就 Observable 和 Subject。

还有两个没提的概念,即 subject 和 scheduler。可以参考Rx中的subject和scheduler怎么理解概念?

A Recap of Frontend Development in 2019

本文翻译的文章链接:https://levelup.gitconnected.com/a-recap-of-frontend-development-in-2019-1e7d07966d6c

在即将过去的 2019 年,前端世界持续突飞猛进,本文主要回顾了web前端开发的主要事件,新闻以及未来趋势。

2019 最受欢迎的框架或库下载统计

React 依旧保持遥遥领先的姿态并且还在持续增长,jQuery 占据在第二的位置(居然有这种事)。紧随其后的是 Angular 和 Vue,同样拥有庞大的用户群体。Svelte 在过去的一年中吸引了不少的注意力,但是还挣扎着能否生存下去。

download in past year

WebAssembly 成为连接 HTML,CSS 和 JavaScript 的 Web 第四种语言

WebAssembly 在今年持续保持低调,但在 12 月初却又重大消息 — W3C 联盟正式将其推荐为一种 web 语言

自从在 2017 年发布 WebAssembly 后,其吸引了大量的关注并快速被采用,在过去几年来,我们看到了 1.0 规范的创建和在所有主流浏览器上的集成。

WebAssembly 在 2019 年的另一条新闻是字节码联盟的成立,该联盟似乎是“通过合作来实施并推动标准,提出新标准来打造 WebAssembly 在浏览器的未来”。

我们仍在等待 WebAssembly 真正站稳脚跟,并获得大量采用,并且随着每次更新,我们都更加接近这个目标。毫无疑问,W3C 的声明是公司使用其合法化迈出的重要一步,接下来需要继续降低使用 WebAssembly 的入门门槛,以使其更易于构建产品。

TypeScript 广受好评

2019 年对于 TypeScript 来说是其爆发的一年,TypeScript 不仅仅是在 JS 代码中添加数据类型,而且许多开发人员经常选择在个人项目和工作中直接使用它。

在 2019 的 StackOverflow 最受欢迎语言调查报告中,TS 和 Python 一起并列第二,紧随 Rust 之后,在可预见的未来,如 2020 年,TS 还会继续飙升。

nost loved language

TS 被广泛运用到前后端当中,甚至使用 TS 是一种时髦的行为,这也导致了其迅速被广泛使用。

TS 几乎整合进了所有主流编辑器,提供了优质的开发体验,对于 JavaScript 开发者来说,TS 作为一种开发工具,用于类型校验和定义接口便于自查或者当做文档记录数据结构类型,以此来减少 bug。

值得注意的是,TS 在 2019 年的 npm 下载量超过了 react,远超了竞争对手 flow 和 Reason。

npm download

2018 年底发布了 TypeScript v3.0,在 2019 年已经更新到 release 3.7,包含了 ECMAScript 最新的特性以便提升类型检测的功能。

Learn TypeScript - Tutorials, Courses, and Books

React

Vue 和 Angular 拥有不少客户,甚至 Vue 在 github 上的 star 都超过了 React,但是在个人适用性和专业度来看,React 还是继续保持强劲的领先。

2018 React 团队首先介绍了 hooks,在2019年,hooks 席卷了 React 世界,绝大多数开发人员将其作为管理状态和组件生命周期的首选方式。整一年,关于 hooks 的文章铺天盖地的,模式开始固化,涌现了很多自定义的 hooks 功能性的包。

Hooks 为函数组件提供了一种简单简洁的语法来管理其状态和生命周期,另外,无需创建 HOC 和 render props,React 提供了构建自定义 hooks 的能力,可以用来复用代码和共享业务逻辑。

React 核心团队更加关注开发体验和工具来提升效率

在 React v16.8 版本中引入 hooks 之后,此后的大部分更改都相对较小,同时在 2019 年发布了版本 16.14。

hooks 发版后,React 核心团队更加关注开发体验和工具来提升效率,在 Conf 2019 上,开发经验(developer experience)作为主题被提及。React Conf主题演讲者和React团队经理Tom Occhino表示,开发人员的经验植根于这三件事:低门槛,高效能和可扩展。让我们来看看 React 团队发布了什么或者计划中发布的功能:

  • 全新版本的 React DevTools
  • 全新的 React 性能分析器工具
  • Create React App v3
  • Testing utility updates
  • Suspense
  • 并发模式(Concurrent mode (upcoming))
  • 在 Facebook 使用 CSS-in-JS(upcoming)
  • Progressive/selective page hydration (upcoming)
  • Accessibility a11 improvements in React core (upcoming)

其信念就是良好的开发体验带来良好的用户体验,以此来俘获各位。大会相关链接请戳 React Conf 2019 Day1

Vue 即将发布 V3 版本,其使用量持续增长

Vue 可能尚未获得最多的使用率,但它确实拥有最热情的用户群体。Vue 吸收了 React 和 Angular 的最佳部分,同时也变得更加简单。它的另一个卖点就是更加开放,不被某一个大公司控制如 React (Facebook) 或者 Angular (Google)。

Vue 最大的的新闻就是即将发布 3.0 版本,它的 alpha 版本有望在 2019 第四季度末发布。Vue 2.x 在今年年初仅仅获得少量的更新,大部分精力都投入到了 v3 版本中。

今年发布的东西不多,并不意味着并没有发生什么,当尤雨溪发布了 RFC for v3 后,在社区引起了广泛的讨论。

不过另开发者不爽的是,Vue API 进行了大修改,然而,在反对声之后,它 API 更加被期待为叠加的和对 Vue 2 向后兼容的。随着 release 版本的持续进行,许多开发者声称 Vue 应该吸收一些 Svelte 的东西,否则真的和 React 太像了。尽管社区中仍有许多人对此表示关注,但在他们等待发布时,噪音似乎已平息。

伴随着各种讨论声, Vue 3 持续在更新一些大的变化:

  • 合成 API(composition API)
  • 全局 mounting / 配置 API 的变更
  • Fragments
  • 时间切片(实验性的)
  • 多 v-models
  • Portals
  • 新自定义指令 API
  • 提升响应性
  • 重写虚拟 DOM
  • 提升 static porps
  • hooks API(实验性的)
  • 插槽操作优化(父子组件单独渲染)
  • TS 支持支持更加友好

Learn Vue.js - Tutorials, Courses, and Books

Angular 发布V8、9版本,特别是新的Ivy编译/渲染管道

Angular 的面面俱到让其俘获了大量的用户群体,由于Angular是一个强有力的框架,它要求开发者遵循它的方式,并且为其提供了所需的工具。

这消除了关于应将哪些库和依赖项带入项目的许多争论,这是在构建 React 应用程序的团队中可能会发现的潜在问题。它还要求开发人员使用 TypeScript 编写其应用程序。由于大多数技术选型已有确定方案,因此公司将其视为一个不错的选择,因为它使开发人员可以专注于开发产品,而不是对某些 packages 做调研花掉很多时间。

在 2019 年,Angular 发布了 V8 版本,并且还发布了一个新的渲染器/编译管道,称之为 Ivy。 Ivy 最大的好处就是降低了 budle 包体积大小,除此之外,它还有很多额外的提升。目前为止,直到 Angular 9 为止,Ivy 是一项可选功能。 这篇文章 详细介绍了 V8 版本的新特性,主要的更新如下:

  • 现代 JavaScript 的差异加载
  • 可选的 Ivy
  • Angular 路由器向后兼容性
  • 提升 Web Worker 打包
  • 可选择使用共享(Opt-In Usage Sharing)
  • 依赖关系更新

Angular 团队即将在 2019 年末或者 2020 年初发布版本 9,最大的变化就是 Ivy 将成为新的渲染器标准。

可访问性(a11y)和 i18n 变得越来越重要

web 应该对所有人开放并可用,并且前端世界一直在优先考虑。从 2015 年开始,JavaScript 和 web 迅猛发展,一些开发模式和框架基本固化,事情变得更加稳定,那么开发者可以集中更多的精力去落地 App 并使其可访问性更好,但是还有很长的路要走。

国际化同样重要,要使得应用在不同地区、不同文化或语言能被很好的兼容到。

ES2019 特性

ECMAScript(JavaScript所基于的规范)继续其年度更新周期,为 ES2019 版本添加了新功能

  • Object.fromEntries()
  • String.trimStart() 和 String.trimEnd()
  • 更好地处理 JSON.stringify 中的 unicode
  • Array.flat()
  • Array.flatMap()
  • try/catch binding
  • Symbol.description

尽管 ES2019 进行了一些重大更新,但即将面世的 ES2020 似乎才是自 ES6 / ES2015 以来最受期待的功能:

  • 私有 class 域
  • 可选的链式操作,如 obj.field?.maybe?.exists
  • 空值合并,如 item ?? 'use this only if item is null/undefined'
  • BigInts

Flutter 的扩张并挑战 React Native,是构建跨平台移动应用程序的另一个绝佳选择

Flutter 在 React Native 发行 2 年后发布,但也发展迅猛。 Flutter 在 github 上 star 数(80.5k)几乎接近 RN 的 83k。 的速度在GitHub星中赶上了React Native,发展迅猛未来可期,Flutter 正在使自己成为最佳的跨平台移动框架。

Node.js 基金会和 JS 基金会合并组成 OpenJS 基金会

为了支撑 JavaScript 生态系统和加速其发展,Node.js 基金会和 JS 基金会合并组成 OpenJS 基金会。基金会传达的信息是根据托管的 31 个开源项目,包括 Node,jQuery和 Webpack,来进行合作和发展。这一举动被视为对整个JS社区都是积极的,并得到了Google,IBM和Microsoft等大型科技公司的支持。

当前发布的Node version 12将被长期支持直到 2023 年,node 12 提供了许多新功能,安全更新和性能改进。一些值得注意的更新包括对 import/export,类私有字段的原生支持,V8 Engine 7.4 版的兼容性,以及对 TLS 1.3 的支持,增加了其他诊断工具。

Svelte 发布的 V3 版本获得了一定的关注但使用率低

Svelte 找到了将自己置身于本已拥挤的前端框架世界中的办法,然后,正如我们文章前面提到的,这还没有转化为大量实际应用,对 Svelte 最好的总结是“简单但强大”。在 Svelte 网站 提及了三点:

  • 更少的节点书写
  • 没有 虚拟 DOM
  • 真正的响应式

静态站点继续被采用,开发人员采用 JAMStack(JavaScript, APIs, Markup)

静态站点将旧网站与新工具,库和更新结合在一起,以提供优质的用户体验。我们能够使用像 React 这样的库来构建我们的站点,然后在构建时将它们编译成静态 HTML 页面。由于所有的页面都是预编译的,这就意味着无需等待请求数据并填充页面,这样页面将更加迅速的呈现出来。

PWAs 获取更多的增长和采用

上面提到的静态站点无法满足各种场景,另一个可选方案是 PWA(progressive web apps)。PWA 允许在浏览器中缓存资源,使得页面得到立即响应并提供离线支持,另外,也支持消息推送。

一个争议点是 PWA 能否取代原生应用,无论结果如何,毫无疑问,长期以来,PWA将成为公司构建产品的重要组成部分。

前端工具正变得越来越好

几年来,JavaScript fatigue 一直是前端开发人员抱怨的,但是我们已经慢慢看到,开源项目维护人员的不懈努力是这种情况得到缓解。

几年前,我们开发项目不得不自己搭建一套前端框架,包括如何构建和发布。现在一切都变的更加简单,我们有各种 CLI,且有比较成熟的一套前端工程化解决方案。

GraphQL继续受到开发人员的喜爱,并在科技公司中得到进一步的采用

GraphQL 有望解决传统基于 REST 的应用程序呈现的许多问题。总而言之,目前 GraphQL 得到了部分大公司的青睐。

GraphQL 是数据驱动的模式,允许客户端开发人员自定义数据结构并以此来返回正确格式的数据。GraphQL API提供了一个架构,用于记录所有数据及其类型,从而使开发人员可以全面了解API。这一年的下载量如下:
graphQl-download

CSS-in-JS 势头强劲

Web 开发的进展感觉就像是在统一 JavaScript 下的所有东西一样,可以通过使用 import/export 语法来共享样式和依赖项,这在 CSS-in-JS 的采用中得到了体现。

VS Code 主导了文本编辑器市场

VS Code 有多牛逼,直接看下面的这张图吧:
Text Editor

Webpack 5 进入测试版并即将发布

Webpack 已成为几乎所有现代 JavaScript 工具链的核心组件,并且是最常用的构建工具。Webpack 一直在提高其性能和可用性,使其对开发人员更友好, V5 版本致力于以下几点:

  • 通过缓存持久化提高构建性能
  • 使用更好的算法和默认值来改善长期缓存
  • 通过内部改造从而不造成大量的 breaking changes

Jest 从 Flow 移到 TypeScript

Chrome 在 2019 年发布了几个稳定版本(72–78)

Microsoft Edge 浏览器移至 Chromium,创建新 logo

Facebook 发布了 Hermes,这是 Android 的 JavaScript 解析器,用于改进 React Native

2020 的预测

  • 性能将是持续被关注,特别是随着代码拆分和 PWA 的进一步利用
  • WebAssembly 将变得更加常见,将被真正的采用且用于生产
  • GraphQL 在新创公司和新项目上超过了 REST,而老牌公司则向其迁移
  • TypeScript 将成为新项目的首选
  • 区块链的使用,是的网络更加开放
  • CSS-in-JS 模式可能成为默认模式从而取代 plain CSS
  • “Codeless” apps 将更加的流行,随着 AI 的发展,开发应用将变得更加容易
  • 在跨应用领域,Flutter 将可能超过 React Native
  • Svelte 将进一步增长并更多的被使用
  • Deno(TypeScript 运行时) 将看到一些 practical usage
  • AR/VR 技术在 Web 的提升?
  • 容器化将在前端更加流行(Docker, Kubernetes)

Top Articles and Videos of 2019

搜索与回溯算法

TODOS:

  • 贪心算法(必学)
  • 启发式搜索算法:A*寻路算法(了解)
  • 地图着色算法、N 皇后问题、最优加工顺序
  • 旅行商问题

angular2 表单

表单在我们的web应用中是至关重要的,表面上看,表单最直截了当的就是用户输入数据,点击提交这么简单。但事实上,表单所呈现的形式是非常复杂多变的,比如我们angular表单可以实现用户控制,监视变化,验证输入、错误信息处理和数据绑定等功能。

本节将详细介绍angular2表单,主要是从技巧方面来认识表单在angular2中带给用户的良好体验,具体如下:

  • 通过组件和模板来创建一个表单;
  • 通过ngModel双向数据绑定的例子来了解如何在input中读写值;
  • angular2相关表单指令的运用,如NgForm、NgForm、NgForm、NgForm和NgCongrolGroup等;
  • 表单局部变量的运用;
  • 表单错误信息和处理或者Validators验证处理等。

1.表单组件类和表单模板

angular2表单是基于HTML的模板及其控制该模板数据以及用户交互的组件组成,我们需要引入一个Componet,这意味着可以在该组件中定义选择器和模板或者引入一个外链的html模板。接下来定义一个组件类,其作用是控制表单相关属性的表现,定义表单方法等。

如下面的例子:【暂时用官网的例子。TODO...】

import {Compontent} from ‘angular2/core’;
import {NgForm} from ‘angular2/common’;
import {Hero} from ./hero’;
@Component ({
    selector: ‘hero-form’, 
    templateUrl: ‘app/hero-form.compontent.html
})
export class HeroFormCompent {
    powers = [‘ReallySmart’,’Super Flexible’,’Super Hot’,’Weather Changer’];
    model=new Hero(18,’DrIQ’,this.powers[0],’Chuck Overstreet’);
    submitted = false;
    onSubmitt(){this.submitted= true;}

在这个例子中,在引入的Component中,需要定义一个selector选择器,这表示我们能够在父模板中插入该表单。同样的,模板可以是外链URL模板也可以直接包裹在template键对应的值中。如:

template: `<form #f="ngForm" (submit)="search(f.value)">
            <select>
                <option value="web">网页</option>
                <option value="news">新闻</option>
                <option value="image">图片</option>
            </select>
            <input type="text" ngControl="kw">
            <button type="submit">搜索</button>
        </form>`

而控制表单的属性或者用户行为等,我们将定义一个类来处理,如HeroFormCompent类,我们定义了相关的属性,提交数据方法等。

2.表单指令

2.1 NgForm

NgForm指令为表单建立一个控件组对象,它包含当前选择器所在的form标签,关于NgForm请看下面的例子:

import {Component} from "angular2/core";
import {bootstrap} from "angular2/platform/browser";
import {CORE_DIRECTIVES,FORM_DIRECTIVES} from "angular2/common";

//组件
@Component({
    selector:"xx-app",
    directives:[FORM_DIRECTIVES,CORE_DIRECTIVES],
    template:`
        <form #f="ngForm"  
            <input type="text" ngControl="title">
            (ngSubmit)="onSubmit(f.value)"
        </form>
    `         
})
2.1.1 指令依赖声明

NgForm指令包含在预定义的数组变量FORM_DIRECTIVES中,所以我们要在组件注解的directives属性中优先声明FORM_DIRECTIVES,这样就可以直接使用NgForm指令了。

2.1.2 局部变量

通过使用“#”符号,我们可以创建一个引用控件组对象的局部变量,如上例中的变量f,这个变量它的value属性是一个JSON对象,该对象的键对应的是表单中input元素的ng-control属性,值对应的是input元素的值。接下来我们介绍NgControl。

2.2.NgControl

2.2.1 NgControl的运用

我们可以在input标签中添加ngControl属性,NgControl将创建一个新的Control并动态的将它添加到父ControlGroup中,同时绑定一个DOM元素到这个新的Control,这就将这个input标签和Control联系起来了,我们访问该标签将直接通过这个ngControl的属性值来访问。

需要注意的是,ngControl必须是作为一个NgForm或者NgFormModel的后代来使用,否则将会报错,因为这个指令需要将创建的控件对象添加到祖先(NgForm或者NgFormModel)所创建的控件中。

在这里,值得一提的是NgControlNmae,它的选择符是[ngControl],这就意味着,你必须和ngControl来搭配使用,这个指令才会发挥它的作用。NgControlName指令为宿主DOM对象创建一个控件对象,并将这个对象以ngControl属性指定的名称绑定到DOM对象上,举个例子,如我们最常用的用户名和密码表单:

<form #f="ngForm">
    <input class="user-name" type="text" ngControl="user">
    <input class="password" type="password" ngControl="password">
</form>

我们创建了两个Control对象,NgControlName指令为宿主DOM对象创建两个控件对象,然后将ngControl属性指定的名称user、password绑定到了其对应的input标签对应的DOM数上。这样的好处是我们可以很方便的通过控件组获取对应的值,也能实现ngModel模型与表单的双向绑定,下一节我们将介绍NgModel。

2.2.2 ngControl监视状态

表单不仅仅是数据的绑定,同样的,我们也希望能够监测到表单的状态,NgControl指令能够保持对状态的监视,除此之外,它会在这下面的三个状态值中影响着当前表单的控制器。

状态TrueFalse
control是否被访问 ng-touched ng-untouched
control是否发生了变化 ng-dirty ng-pristine
control是否合法有效 ng-valid ng-invalid

通过监测NgControl状态的改变,我们能设置我们想要的特殊css类来更新控制器,比如能够通过监视状态合法性的属性ng-valid和ng-invalid来改变控制器是否需要弹出或者显示输入非法的状态提醒,如显示、隐藏错误信息等。我们能瞬间探测到这些状态的改变,同时我们可以马上为我们的表单组件添加对应的处理。以下是对这些状态使用的一些例子:

// todo ...

2.3 NgModel

2.3.1 NgModel的运用

在表单中,我们常常需要用到这样的场景,在model数据结构有变化的时候,我们希望能够及时的反应在表单中,同样的,我们在操作表单的时候,也是需要表单的变化需要实时的在model中有响应的。也就是说,我们需要同一时间去显示、监听和摘录数据。

angular2采用的是ngModel来实现数据的双向绑定的,NgModel指令可以令表单和模型(model)的数据绑定超级简单,它的语法是:[(ngModel)]=“...”,例子如下。

<input type=”text” class=”form-controt” required [(ngModel)]=”model.name”>
TODO:监视这个表单的值:{{model.name}}
2.3.2 NgModel双向绑定原理

在模板语法里,我们已经了解过了属性绑定和事件绑定,在属性绑定里,值产生于模型赋值给目标属性,通过中括号-[]来包裹这个属性,那么我们的模型也将通过这个中括号内的属性值来辨识这个目标属性。这是一种由模型向视图的单项数据绑定。而事件绑定则相反,在事件绑定中,目标属性对应表单的变化的值将赋予给模型,通过小括号-()来包裹这个属性,这个包裹的属性将会标识模型中对应该目标属性名的变化。这是一种反向的视图向模型的单向数据绑定。这就是[()]实现数据双向绑定的方式,很好的预示将要发生什么。

如上面的例子,我们可以改写成这样:

<input type=”text” class=”form-control” required [ngModel]=”model.name” (ngModelChange)=”model.name=$event”>
TODO:监视这个表单的值: {{model.name}}

该表单中,模板表达式:model.name=$event是被用来发现来自于DOM事件的$event事件,ngModelChange不是一个input元素事件,本身不会产生一个DOM事件,实际上它是NgModel指令的一个事件属性,它是能够返回输入框的一种angular的EventEmitter属性,这种属性能够精确的捕获我们分配给模型“anyName”属性的值。在angular2表单中,我们看到[(anyName)]时,它预示着这个anyName指令将拥有一个该属性的输入值和一个对应着anyName-change的输出值。

2.4 NgSumit

用户填完表单之后,我们需要获取表单的完整数据以便提交。通常我们会在表单的底部添加一个提交按钮并设置其type的值等于submit,按钮本身不做任何事情但是却能监听表单的提交这个动作,但是此刻的提交没什么作用,为此,angular2提供了一个NgSubmit指令于form标签,这样我们就可以绑定事件到模型的submit方法上用来出来表单的提交了。例子如下:

import { Component } from 'angular2/core';  
import { FORM_DIRECTIVES } from 'angular2/common';

@Component({  
  selector: 'demo-form',  
  directives: [FORM_DIRECTIVES],  
  template: `   
    <form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
       <input type="text" ngControl="sku">  
       <button type="submit" class="button">Submit</button>  
    </form> 
  `
})  
export class DemoForm {  
  onSubmit(value: string): void {  
    console.log('you submitted value: ', value);  
  }
}

该例子中,我们定义了一个模板局部变量#f,并用NgForm指令来初始化它的值,这样我们就能通过submit来获取该表单需要提交的数据结构了。

深入理解 Zones

Zones 是一个持续异步任务的执行上下文,允许 Zone 的创建者观察并控制其区域内代码的执行,Zones 的职责是对宿主环境的任务调度和处理,例如被用来 Debug 或测试等,对于某些框架如 Angular,Zones 主要被用作通知变化监测。

创建子 Zone

在深入理解 Zones 之前,我们需要了解 Zones 是如何创建的。通过引入 zone.js 库,可以在应用中全局的使用 Zone,该 Zone 我们通常称之为跟 Zone。而创建子 Zone 只需要在父 Zone 中 fork 另一份实例即可,示例代码如下:

	let rootZone = Zone.current;
	// 从父 Zone 中 fork 一个新的实例 ZoneA
	let zoneA = rootZone.fork({name: 'zoneA'}); 

通常情况下,我们需要将不同的框架或者业务代码在不同 Zone 的执行上下文中运行,Zone.current 表示当前执行环境下的宿主 Zone,唯一改变 Zone.current 的方式是使用 Zone.prototype.run() 方法,可以使用 run() 方法来控制 Zones 的进入或退出。通过下面的例子来加深对这个概念的理解,示例代码如下:

	zoneA.run(fnOuter() => {
    // 通过 `run()` 方法,`Zone.current` 被更新,当前 Zone 为 zoneA
    console.log(Zone.current === zoneA);

    // Zones 可以相互嵌套
    rootZone.run(fnInner => () {
      // 通过 `run()` 方法,`Zone.current` 被更新,当前 Zone 为 rootZone
      // 同时让当前执行环境逃离 zoneA
      console.log(Zone.current === rootZone);
    });
  });

跟踪异步操作

通过对上文的理解,我们知道了如何将某框架或者业务逻辑代码的执行环境加入或逃离某个 Zone,这对异步操作的跟踪是有意义的。通过下面的例子来说明,示例代码如下:

	let rootZone = Zone.current;
	let zoneA = rootZone.fork({name: 'A'});

	setTimeout(timeoutCb1() => {
	  console.log('该 callback 将在 rootZone 中执行', Zone.current === rootZone);
	}, 0);

	zoneA.run(run1() => {
	  console.log('该 callback 将在 zoneA 中执行', Zone.current === zoneA);
	  setTimeout(timeoutCb2() => {
	    console.log('该 callback 将在 zoneA 中执行', Zone.current === zoneA);
	  }, 0);
	});

一旦异步工作被有序的调度执行的时候,回调函数将在与调用异步 API 时存在的 Zone 中执行,通过包装到 Zone 中的回调,所有的这些操作导致的调度任务将会被指向当前的 Zone 中。跟踪异步操作重要的作用是允许通过在 wrapCallback 之前或之后使用不同的请求方式来拦截它们。在某些情形下,包裹后的 wrapCallback 在有进程调度时,将在当前的 wrapCallback 完成调度,以确保每个异步任务的相互不影响。

上文提到了一个重要概念是回调包装,Zone 一个重要的方面就是支持跨异步操作,为了实现跨异步操作,当有一个任务需要通过异步 API 获取数据时,是需要捕获并恢复当前 Zone。举个例子,在某个 Zone 的上下文中执行一个异步操作如 setTimeout,这个 setTimeout() 方法需要完成的步骤是:

  • 通过 Zone.current 捕获当前 Zone;
  • 在代码中包装 callback,一旦被包装的 callback() 方法被执行则将会恢复当前 Zone

也就是说,管理当前代码的规则将保留在将要执行的异步任务中,它将区别于其他的 Zone,不同的 Zone 关联着不同的异步任务并且有自己的规则,随着这些异步任务被处理,每一个异步的 wrapCallback 将准确的恢复其当前 Zone,同时保存以备下次异步任务的时候再次被调度。

为了说明这两个步骤的必要性,我们举例说明如下,通过调用 fetch() 方法来返回一个 promise,在 fetch() 方法内部可以使用它当前的 Zone 来做错误处理,而在应用中通过 then() 来返回请求结果,这使得跟踪异步操作井然有序。

Zones 的继承与可组合性

父子 Zone 之间存在着继承关系,同样的也遵循了 JavaScript 的原型继承,Zone 的每个函数在被执行时都会创建自己的执行环境,当代码在某一执行环境中运行时,会创建由变量对象构成的一个作用域链,这确保了执行环境对所有变量或者函数的有序访问。通过下面的例子来理解一下 Zone 的继承,示例代码如下:

	let rootZone = Zone.current;
	let zoneA = rootZone.fork({name: 'zoneA', properties: {a: 1, b:1}});
	let zoneB = zoneA.fork({name: 'zoneA', properties: {a: 2}});
	
	console.log('zoneA 属性 a 的值是:', zoneA.get('a')); // 1
	console.log('zoneA 属性 b 的值是:', zoneA.get('b')); // 1
	console.log('zoneB 属性 a 的值是:', zoneB.get('a')); // 2

	// 在当前 Zone 获取不到某属性是,将向父 Zone 查询改属性
	console.log('zoneB 属性 b 的值是:', zoneB.get('a')); // 1

Zones 可以通过 Zone.fork() 来组合在一起,并且所有运行在 Zone 中的业务代码都有一个根 Zone,确保所有的代码都在其中,并且其将有很多的子 Zone。子 Zone 有其独立的运行规则,其功能大致如下:

  • 在当前 Zone 处理请求而不委派
  • 将拦截委托给父 Zone,并且可选的在 wrapCallback 之前或者之后添加钩子

父子 Zone 之间通过 ZoneDelegate 来实现交互的,一个子 Zone 不能简单的调用父 Zone 中的方法,要实现继承父类方法的功能,需要在子 Zone 创建一个回调函数并且绑定到父 Zone 中。我们要做的就是在有异步操作触发该回调的时候拦截它,以便来确定是否需要通过 ZoneDelegate 再进一步触发父类中的方法。

并不是每个子 Zone 都重写了父 Zone 中的方法,不过 ZoneDelegate 存储了当前 Zone 最近的父 Zone 的一份实例,以便向上调用相关的方法。下面通过表格进一步说明他们之间的关系:

Zone ZoneDelegate 描述
run invoke 当运行在 run() 方法中的代码块被执行,ZoneDelegate 的钩子 invoke() 被调用,则表示在不改变当前 Zone 的情况下,允许向父级 Zone 派发钩子,即执行父 Zone 对应的钩子。 

这就是父子组件间的可组合型。可组合性确保了 Zone 之间的职责分明,例如顶层的父 Zone 可以处理一些全局的错误处理,而子 Zone 则可以选择做用户行为跟踪。

理解 Zones

通过上文对 Zones 的介绍,我们对其有了一个大致的认识。Zones 实际上是 Dart 的一种语言特性,是一种用于拦截和跟踪异步工作的机制,可以简单地将其理解为一个异步事件拦截器,也就是说 Zones 能够 hook 到异步任务的执行上下文,并在一些关键节点上重写相应的钩子方法,以此来完成某些操作。Zone 是一个全局对象,其配置了相应的规则用于拦截和跟踪异步回调,主要功能如下:

  • 拦截异步任务调度
  • 为跨异步操作和错误处理提供当前 Zone 的包裹回调
  • 提供一种方式将数据附加到 Zone
  • 在提供的上下文中执行错误处理
  • 截取阻塞方法

Zone 本身不做任何事情,它仅仅在执行异步任务或事件的时候去执行相应的钩子。zone.js 库重写了(monkey patches)所有的浏览器异步 API 并且在执行的时候将这些异步任务和事件重定向到新的 API 上,即 Zone 能够获取到异步任务或事件的执行上下文,并在一些关键节点上重写相应的钩子方法,以此来完成某些操作。下面以猴子补丁 setTimeout 来说明其被重写的过程,示例代码如下:

	// 原生的 setTimeout
	let originalSetTimeout = window.setTimeout;
	// 重写 setTimeout
	window.setTimeout = function(callback, delay) {
	  return originalSetTimeout(
	    // 在 Zone 中通过包装回调函数重写 setTimeout
	    Zone.current.wrap(callback), 
	    delay
	  );
	}
	
	// zone.js 源码缩略
	Zone.prototype.wrap = function (callback, source) {
       // ...
       var _callback = this._zoneDelegate.intercept(this, callback, source);
       var zone = this; // 捕获当前 Zone
       return function () {
           // 在当前的 Zone 中执行最初的 callback
           return zone.runGuarded(_callback, this, arguments, source);
       };
   };

上面的代码示例中,Zone.prototype.wrap() 方法用于重新包装 callback,该 callback 通过 Zone.prototype.runGuarded() 来执行,执行的过程中将会被 ZoneSpec.onInvoke() 拦截并在该函数中处理,Zone.prototype.runGuarded() 方法类似于 Zone.prototype.run(),不同的是,除了将包裹函数执行在当前 Zone 之外,它还处理异步操作的异常捕获,任何的异常都会被捕获,并在 Zone.HandleError() 方法中处理。

通过前面的介绍可以了解到,Zones 以同样的接口、不同的方式实现并重写了一系列与事件相关的标准方法。因此,当开发者使用标准接口时,实际上会先调用 Zones 的重写方法,再由这些方法调用底层的标准方法。这种对上层应用透明的设计,使得在引入 Zones 的时候,原有代码不需要做太大的改动。

reference

深入理解Angular2变化监测和ngZone

深入理解Angular2变化监测和ngZone

废弃【基于zone.js 0.6.x以下版本】

Angular应用程序通过组件实例和模板之间进行数据交互,也就是将组件的数据和页面DOM元素关连起来,当数据有变化后,NG2能够监测到这些变化并更新视图,反之亦然,它的数据流向是单项的,通过属性绑定和事件绑定来实现数据的流入和流出,数据从属性绑定流入组件,从事件流出组件,数据的双向绑定就是通过这样来实现的。那么它是如何实现变化检测的呢?

需要进行变化监测的情形

试想一下,在什么样的场景下,angular才需要去更新视图:

  • event,在view中绑定事件来监听用户的操作,如果数据有变更则更新视图;
  • xmlHTTPRequest/webSocket,例如从远端服务拉取对应的数据,这是一个异步的过程;
  • timeout,例如:setTimeout, setInterval, requestAnimationFrame都是在某个延时后触发。

以上的共同特征是什么?很明显共同点是它们都是异步的处理,即需要使用异步回调函数,这带给我们的结论就是,不管任何时候的一个异步操作,我们应用程序状态可能已经被改变,这就需要告诉Angular去更新视图。

我们创建一个组件来呈现一个Todo例子,我们可以在模板中这样使用这个组件:

<todo-cmp [model]="myTodo" (complete)="onCompletingTodo(todo)"></todo-cmp>

这将告诉Angular不管任何时候myTodo发生改变,Angular必须通过调用视图模型设置的myTodo数据(model setter)来自动的更新todo模板组件。

同样的,数据的流出是通过事件绑定来实现的,如果一个complete事件被触发,它将调用这个onCompletingTodo方法,该方法可能是一个获取后台最新数据的操作,这将需要用后台返回的异步数据与之前的数据参考进行对比来确定是否需要更新视图。

正如上面的例子,Angular2的属性和事件绑定的核心语法是很简单的,我们通过属性绑定实现了数据从父传递给了子,而事件绑定则实现了数据由子到父的传递,这也就是Angular2用来实现数据双向绑定的方法。它实现的是单向流的数据传递,也就是说,你的数据流只能向下流入组件,如果你需要进行数据变化,你可以发射导致变化的事件到顶部,待数据变化处理完成,然后再往下流入组件。那么问题来了,Angular2如何知道数据是否已经处理处理完成,这份新的数据是否有变化,如果数据有变化,那是怎么来通知数据往下流入组件通知组件来改变视图呢?这里我们先理解zone。

关于Zone

Zone实际上是Dart的一种语言特性,其是对Javascript某些设计缺陷的一些补充,简单的可以概述成Zone是一个异步事件拦截器,也就是说Zone能够hook到异步任务的执行上下文,以此来处理一些操作,比如说,在我们每次启动或者完成一个异步的操作、进行堆栈的跟踪处理、某段功能代码进入或者离开zone,我们可以在这些关键的节点重写我们所需处理的方法。

Zone中提供了各类hooks,允许在每一个回调函数的开始和结束时,去执行统一的自定义逻辑,其本身是不做任何事的,相反它是依赖其它的代码,获取到这些代码片段的执行上下文,通过hooks来完成相关的功能。Zone的另一个值得一提的是它必须依赖异步操作,当一个异步操作在执行时,它是有必要去捕获的这个异步操作并在该异步功能开始或者完成时建立对应的callback,然后存储到当前的zone,举个例子,如果一个代码片段在fork的zone中执行,并且这段代码中包含一个setTimeout的异步任务,那么执行到和完成这个setTimeout方法需要包裹一个异步的回调函数,存储到当前zone。

这样是确保每个异步操作之间的相互不受影响,也就是受保护的状态,例如一个页面由业务代码和一些第三方广告代码组成,这两份代码之间是相互独立的,我们需要的是业务代码的异常捕获数据提交到我们自己的后台服务器上,第三方广告代码的异常捕获提交到他们自己的服务器上。当fork了多个zone之后,异步操作将会精准的执行其所在的子zone上面方法。

Zone的一个重要意义在于,我们的功能或者业务代码运行在了fork的一个zone中,我们zone有了对该代码块执行上下文的控制权。其中也提供了一些钩子(hook)来处理我们基本的业务情景需求,大致有:

  • Zone.onZoneCreated:在zone被fork时运行
  • Zone.beforeTask:在执行zone.run包裹的函数之前调用
  • Zone.afterTask:在执行zone.run包裹的函数之后调用
  • Zone.onError:zone.run方法中的Task任务抛出异常时的钩子函数

下面我们通过这样的一个例子来帮助你理解Zone,简单的代码如下:

zone.fork({
    beforeTask: () => {
        console.log('hi, beforeTask in.');
    },
    afterTask: () => {
        console.log('hi, afterTask in.');
    }
}).run(function () {
  zone.inTheZone = true;

  setTimeout(function () {
    console.log('in the zone: ' + !!zone.inTheZone);
  }, 0);
});

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

这段代码按照执行上下文顺序的执行,我们在zone的run函数执行的开始和结束会有对应的hooks,例如要统计这段代码执行所消耗的时间,然而通常情况下,这里的异步处理,比如说是服务端异步返回给我们所需要的数据,或者是一些异步事件更改视图模型的数据等。这样通过beforeTaskafterTask统计到整个代码的耗时。这种情形在zone得到了很好的解决,Zone能够hook到异步任务的执行上下文,在异步事件发生或者结束的时候,允许我们在这样的异步任务节点执行一些分析代码。zone使用也很简单,一旦我们引入zone.js,那我们在全局作用域中可以获取到zone对象。

但是这远远不够的,很多时候我们的应用场景要比这个复杂的多,现在是时候体现zone的暴力美了,zone.js采用猴子补丁(Monkey-patched)的方式将Js中的异步任务都进行了包裹,同样的这使得这些异步任务都将运行在zone的执行上下文中,每一个异步的任务在zone.js都是一个task,除了提供了一些供开发者使用的勾子(hook)函数外,默认情况下zone.js重写了并提供了如下的方法:

  • Zone.setInterval() / Zone.setTimeout()
  • Zone.alert()
  • Zone.prompt()
  • Zone.requestAnimationFrame()
  • Zone.addEventListener()
  • Zone.removeEventListener()

综上所述,我们应该能理解zone.js的应用场景了,即实现了异步task的跟踪分析和错误记录以便更好的进行开发debug等。接下来将回到主题来探讨一下Angular2的数据绑定和zone的关系。

Angular2数据绑定和Zone

在Angular1.x中,默认的选择是双向的数据绑定,你的控制器数据发生变化,或者表单直接操作数据变动等,最终体现在视图中显示数据。

Angular1.x双向数据绑定的问题是,随着你的项目增长,它往往会导致整个应用的级联效应,并很难跟踪你的数据流。除非你使用Angular1.x框架的内置服务和指令,否则我们在model上做数据修改或者数据输出,Angular是无法预知的,当然就不会去更新视图模板中的数据来展示给UI。

好在Angular2框架把zone.js作为依赖,因为zone.js是一个独立的库,可以不依赖于其他库或者框架而单独被使用,因此在Angular2开发的应用中,zone拥有angular应用运行环境的执行上下文,事实证明,zone是能够解决在我们在angular应用中变化监测的问题的。

下面我们来介绍ngZone。实际上,ngZone是基于Zone.js来实现的,Angular2 fork了zone.js,它是zone派生出来的一个子zone,在Angular环境内注册的异步事件都运行在这个子zone上(因为ngZone拥有整个Angular运行环境的执行上下文),并且onTurnStart和onTurnDone事件也会在该子zone的run方法中触发。

在Angular2源码中,有一个ApplicationRef类,其作用是用来监听ngZone中的onTurnDone事件,不论何时只要触发这个事件,那么将会执行一个tick()方法用来告诉Angular去执行变化监测。

// very simplified version of actual source
class ApplicationRef {
  changeDetectorRefs:ChangeDetectorRef[] = [];

  constructor(private zone: NgZone) {
    this.zone.onTurnDone
      .subscribe(() => this.zone.run(() => this.tick());
  }

  tick() {
    this.changeDetectorRefs
      .forEach((ref) => ref.detectChanges());
  }
}

Angular2的变化监测

现在我们已经知道了Angular2的变化监测在何时被触发,那它是怎么去做变化监测的呢?实际上在Angular2中,任何的一个Angular2应用都是由大大小小的组件组成的,可以把它看成是一颗线性的组件树,重要的是,每一个组件都有自己的变化检测器。这样的一个图可以帮助你理解这些概念,具体也可以参考我翻译的关于Angular2变化监测的文章。

正是因为每个组件都拥有它的变化检测器,组成了Angular2应用的一颗组件树,同样的我们也有变化监测树,它也是线性的,数据的流向也是从上到下,因为变化监测在每个组件中的执行也是从根组件开始,从上往下的执行。单向的数据流相对angular1.x的环形数据流来说要更好预测的多,其实我们清楚视图中数据的来源,也就是说这些数据的变化是来自于哪个组件数据变化的结果。我们来举个例子吧:

@Component({
  template: '<v-card [vData]="vData"></v-card>'
})
class VCardApp {
  constructor() {
    this.vData = {
      name: '***',
      email: '****@**.com'
    }
  }

  changeData() {
    this.vData.name = '*****';
  }
}

Angular2在整个运行期间都会为每一个组件创建监测类,用来监测每个组件在每个运行周期是否有异步操作发生。当变化监测被执行时会发生什么呢?假象一下changeData()方法在一个异步的操作之后被执行,那么vData.name被改变,然后被传递到<v-card [vData]="vData"></v-card>的变化检测器来和之前的数据对比是否有改变,如果和参照数据对比有变动的话,Angular将更新视图。

因为在JavaScript语言中不提供给我们对象的变化通知,所以Angular必须保守的要对每一个组件的每一次运行结果执行变化检测,但其实很多组件的输入属性是没有变化的,没必要对这样的组件来一次变化监测,如何减少不必要的监测,我们有两种方式去实现。

Immutable Objects

不可变对象(Immutable Objects)给我们提供的保障是对象不会改变,即当其内部的属性发生变化时,相对旧有的对象,我们将会保存另一份新的参照。它仅仅依赖输入的属性,也就是当输入属性没有变动(没有变动即没有产生一份新的参照),Angular将跳过对该组件的全部变化监测,直到有属性变化为止。如果需要在Angular2中使用不可变对象,我们需要做的就是设置changeDetection: ChangeDetectionStrategy.OnPush,如下的例子:

@Component({
  template: `
    <h2>{{vData.name}}</h2>
    <span>{{vData.email}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
class VCardCmp {
  @Input() vData;
}

例子中,VCardCmp仅仅依赖它的输入属性,同时我们也设定了变化监测策略为OnPush来告诉Angular如果属性属性没有任何变化的话,则跳过该组件的变化监测。

Observables

和不可变对象类似,但却又和不可变对象不同,它们有相关变化的时候不会提供一份新的参照,可观测对象在输入属性发生变化的时候来触发一个事件来更新组件视图,同样的,我们也是添加OnPush来跳过子组件树的监测器,我们给这样的一个例子来帮你加深理解:

@Component({
  template: '{{counter}}',
  changeDetection: ChangeDetectionStrategy.OnPush
})
class CartBadgeCmp {

  @Input() addItemStream:Observable<any>;
  counter = 0;

  ngOnInit() {
    this.addItemStream.subscribe(() => {
      this.counter++; // application state changed
    })
  }
}

该组件是模拟的当用户触发一个事件后增加counter这样一个场景,确切的讲,CartBadgeCmp设置了一个插值counter和一个输入属性addItemStream,当有异步操作需要更新counter的时候,将会触发一个事件流,但是输入属性addItemStream作为参考对象将不会更改,意味着该组件树的变化监测将不会发生。那怎么办?我们将怎么来通知Angular某区块有改变呢?Angular2的变化监测总是从组件树的头到尾来执行,我们其实需要的就是在整个组件树的某个发生改变的地方来做出相应即可,Angular是不知道那一块目录有改变的,但是我们知道,我们可以通过依赖注入给组件来引入一个ChangeDetectorRef,这个方法正是我们所需要的,它能标记整颗组件树的目录直到下一次变化监测的执行,代码示例如下:

class CartBadgeCmp {
    constructor(private cd: ChangeDetectorRef) {}

    @Input() addItemStream:Observable<any>;
    counter = 0;

    ngOnInit() {
        this.addItemStream.subscribe(() => {
            this.counter++; // application state changed
            this.cd.markForCheck(); // marks path
        })
    }
}

当这个可监测的addItemStream触发一个事件,该事件处理句柄将会从根路径到这个已经改变的addItemStream组件来处理监测,一旦变化监测跑遍整个监测路径,它将会存储OnPush状态到整个组件树。这样做的好处是,变化监测系统将会走遍整棵树,你可以利用他们来监测树在局部是否有真正的改变,以此来做出相应的改变。

好了,当目前为止,我们已经清楚了Angular2的变化监测的实现,个人理解可能有不到位的地方,欢迎指正。

参考

浏览器渲染及性能优化

浏览器的渲染过程

webkit 内核渲染过程图:

webkit

gecko 内核渲染过程图:

gecko

不同的浏览器内核有着不一样的渲染过程,细节不一致但大致的浏览器渲染的流程如下:

  • HTML 解析文件,生成 DOM Tree,解析 CSS 文件生成 CSSOM Tree
  • 将 DOM Tree和 CSSOM Tree 结合,生成 Render Tree(渲染树)
  • 根据 Render Tree 渲染绘制,将屏幕上渲染像素点

css 加载会造成哪些阻塞

通过上面的流程则能很好的回答问题了。

  1. DOM 解析和 CSS 解析是两个并行的进程,所以 CSS 加载不会阻塞 DOM 的解析
  2. Render Tree 是依赖于 DOM Tree 和 CSSOM Tree 的,所以他必须等待到 CSSOM Tree 构建完成,所以,CSS 加载是会阻塞 DOM 的渲染的

而 JS 可能有调用相关 API 更新 DOM 节点和 CSS,在操作前必须等待这些元素渲染完毕,浏览器会维持 HTML 中 css 和 JS 的顺序,因此,样式表会在后面的 JS 执行前先加载执行完毕,所以,这也是阻碍的。

结论

  • css 加载不会阻塞 DOM 树的解析
  • css 加载会阻塞 DOM 树的渲染
  • css 加载会阻塞后面 js 语句的执行

渲染性能

60fps 与设备刷新率

目前大多数设备的屏幕刷新率为 60 次/秒。因此,如果在页面中有一个动画或渐变效果,或者用户正在滚动页面,那么浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致。

其中每个帧的预算时间仅比 16 毫秒多一点 (1 秒/ 60 = 16.66 毫秒)。但实际上,浏览器有整理工作要做,因此您的所有工作需要在 10 毫秒内完成。如果无法符合此预算,帧率将下降,并且内容会在屏幕上抖动。 此现象通常称为卡顿,会对用户体验产生负面影响。

像素管道

像素至屏幕管道中的关键点:
像素管道

JavaScript
一般来说,使用 JavaScript 来实现一些视觉变化的效果。比如用 jQuery 的 animate 函数做一个动画、对一个数据集进行排序或者往页面里添加一些 DOM 元素等。当然,除了 JavaScript,还有其他一些常用方法也可以实现视觉变化效果,比如:CSS Animations、Transitions 和 Web Animation API。

Style
此过程是根据匹配选择器(例如 .headline 或 .nav > .nav__item)计算出哪些元素应用哪些 CSS 规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。

Layout
在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。

网页的布局模式意味着一个元素可能影响其他元素,例如 元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。

Paint
绘制是填充像素的过程。

它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。

绘制一般是在多个表面(通常称为层)上完成的。

Composite
由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。

对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。

管道的每个部分都有机会产生卡顿,因此务必准确了解您的代码触发管道的哪些部分。
每一帧并不一定总是需要经过像素管道每个部分的处理。

Reference

深入理解 async/await

理解 async/await

可以直接参考:理解 async/await

ES7 提出的 async 函数,终于让 JavaScript 对于异步操作有了终极解决方案(No more callback hell

Async 函数的改进在于下面四点:

  • 内置执行器 Generator 函数的执行必须依靠执行器,而 Aysnc 函数自带执行器,更直观的同步写法
  • 更好的语义 async 和 await 相较于 * 和 yield 更加语义化
  • 更广的适用性 async 函数的 await 命令后面则可以是 Promise 或者基本类型的值(Number,string,boolean...)
  • 返回值是 Promise async 函数返回值是 Promise 对象,可以直接使用 then() 方法进行调用

数据响应机制(angularJs、vue)

angular的数据响应机制

主要了解 $watch、$digest、$apply

  • $watch 绑定要检查的值,即当一个作用域创建的时候,angularJs 会根据如插值,内置指令和 $watch 建立绑定关系。当 $watch 绑定了要检查的属性之后,当绑定的属性发生变化,就会执行回调函数。回调函数的作用是如果新值和旧值不同时(或相同时)要干什么事,通常给开发来实现这个 callback。
  • $digest 遍历递归,主要是解决数据绑定发生变化后,如何触发更新的问题。脏检查的核心,就是 $digest 循环。$watch 将相关绑定关系的属性推到 $$watchers 队列中,$digest 触发后就去检测哪些 $scope 属性发生变化,通过 dirty 进行标记,以此完成页面的渲染。
  • $apply 触发 $digest(非必须,即业务中手动触发)。angularJs 内部怎么触发 $digest?根据双向绑定关系,将插值、内置指令封装了 $apply,当插值或者内置指令被执行的时候,内部手动执行了 $apply。即如 ng-click 它其实包含了document.addEventListener('click')和$scope.$apply()。

参考下图:
angular-data-bind

从零扒一扒 promise

从零扒一扒 promise

在开发过程中,很多时候都在熟练的使用 Promise/A+ 规范,然而有时在使用的时候发现并不是很了解它的底层实现,下面扒一扒它的实现。

本文基于 AngularJs $q

Promises/A+规范

Promise 是 JS 异步编程中的重要概念,异步抽象处理对象,是目前比较流行 Javascript 异步编程解决方案之一。

术语

  • 解决 (fulfill): 指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用 fulfill 来表示解决,但在后世的 promise 实现多以 resolve 来指代之。
  • 拒绝(reject): 指一个 promise 失败时进行的一系列操作。
  • 拒因 (reason): 也就是拒绝原因,指在 promise 被拒绝时传递给拒绝回调的值。
  • 终值(eventual value): 所谓终值,指的是 promise 被解决时传递给解决回调的值,由于 promise 有一次性的特征,因此当这个值被传递时,标志着 promise 等待态的结束,故称之终值,有时也直接简称为值(value)。
  • Promise: promise 是一个拥有 then 方法的对象或函数,其行为符合本规范。
  • thenable: 是一个定义了 then 方法的对象或函数,文中译作“拥有 then 方法”。
  • 异常(exception): 是使用 throw 语句抛出的一个值。

异步回调

Promise 解决的就是异步任务处理问题,简单举例如下(假设有一个异步任务 asyncJob1,它执行完成后要执行 asyncJob2):

// asyncJob2 作为参数传给 asyncJob1,在它完成某些操作后调用
asyncJob1(asyncJob2)

// asyncJob1 的伪代码
function asyncJob1(callback) {
    // some task to get the 'result'
    callback(result);
}

这样做的问题是 callback 的控制权在 asyncJob1 里面了,并且若有多个异步任务将会有回调地狱的问题,如:

asyncJob1(param, function(job1Result) {
    asyncJob2(job1Result, function (job2Result) {
        asyncJob3(job2Result, function (job3Result) {
            alert('we are done');
            // ...
        }
    });
})

控制反转

稍稍把代码修改一下:

function asyncJob1() {
    // some task to get the 'result'
    // ...
    return function (callback) {
        callback(result);
    }
}

那么调用的方式就是:

asyncJob1()(function (job1Result) {
    // ...
});

写代码的时候,「同步」的写法语义更容易理解,即”干完某件事后,再处理另外一件事”,通过 then 方法来实现链式调用:

function asyncJob1() {
    // some task to get the 'result'
    return {
        then: function (callback) {
            callback(result);
        }
    }
}

可按照如下例子来调用,这样看起来就更有序了。

asyncJob1(param).then(function (job1Result) {
    return asyncJob2(job1Result);
}).then(function (job2Result) {
    return asyncJob3(job3Result);
}).then(function (job3Result) {
    // finally done
});

带着上面的思路,接下来实现一个简版的 Promise。

Promise simple demo

上面的例子,都是函数执行完成后同步执行回调,看下面的例子:

var Promise = function () {
    var _callback, _result;

    return {
        resolve: function (result) {
            _result = result;
            if (_callback) {
                _callback(result);
            }
            _callback = null;
        },
        then: function (callback) {
            _callback = callback;
        }
    }
};

于是上面的回调实现就可以变成:

function asyncJob1(param) {
    var promise = Promise();
    setTimeout(function monkey() {
        var result = param + ' with job1';
        promise.resolve(result);
    }, 100);
    return promise;
}

asyncJob1('monkey').then(function (result) {
    console.log('we are done', result);
});

Promise 支持多个回调

有时在某件事完成之后,可以同时做其他的多件事情,为此修改 Promise,增加回调队列:

var Promise = function () {
    var _pending = [], _result;

    return {
        resolve: function (result) {
            _result = result;
            if (_pending) {
                for (var i = 0, l = _pending.length; i < l; i++) {
                    _pending[i](_result);
                }
            }
            _pending = null;
        },
        then: function (callback) {
            _pending.push(callback);
        }
    }
};

于是在 job1 后可以添加多个回调

var job1 = asyncJob1('monkey');
job1.then(function (result) {
    console.log('we are done1:', result);
});

job1.then(function (result) {
    console.log('we are done2:', result);
});

这样之后可能还不够,因为如果另外一个回调是异步处理的话,可能就没法得到结果了,比如:

var job1 = asyncJob1('monkey');
job1.then(function (result) {
    console.log('we are done1:', result);
});

setTimeout(function () {
    job1.then(function (result) { // then 调用时已经报错了
        console.log('we are done2:', result); // 此处没有打印
    });
}, 1000);

可以在 then 中增加一个判断,如果已经 resolve 过了,则直接执行回调:这样处理后上面的'done2'就可以输出了

var Promise = function () {
    ...
    return {
        ...
        then: function (callback) {
            if (_pending) {
                _pending.push(callback);
            } else {
                callback(_result);
            }
        }
    }
};

Promise 作用域安全性

以上的 Promise 返回后,外部可以直接访问 then、resolve 这两个方法,然而外部应该只关心 then,resolve 方法不应该暴露出去,防止外部调用 resolve 修改了 Promise 的状态。代码修整如下:

var Deferred = function () {
    ...
    return {
        ...
        promise: {
            then: function (callback) {
                ...
            }
        }
    }
};

以上只列出了修改的代码,可以看出这个改动很小,其实就是给 then 封装多了一层,调用的方式就变成如下:

function asyncJob1(param) {
    var defer = Deferred();
    setTimeout(function () {
        var result = param + ' with job1';
        defer.resolve(result);
    }, 100);
    return defer.promise;
}

asyncJob1('monkey').then(function (result) {
    console.log('we are done', result);
});

Promise 链式调用

截到目前为止,promise 原型还不能实现链式调用,比如这样调用的话,第二个 then 就会报错

asyncJob1('monkey').then(function (job1Result) {
    return asyncJob2(job1Result);
}).then(function (job2Result) {  // <-- 此处的then会报错
    console.log('we are done', job2Result);
});

链式调用是promise很重要的特性,为了实现链式调用,我们要实现:

  • then方法也返回一个promise
  • 返回的promise必须要用传给then方法函数的返回值,来设置(resolve)自己的result。
  • 传递给then方法的函数,必须返回promise或值
    • 如果返回的是promise,则必须等待这个promise处理后,才设置result
    • 如果返回的是值,则直接设置result

先来看看代码实现:

var Deferred = function () {
    var _pending = [], _result;

    return {
        resolve: function (result) {
            _result = result;
            if (_pending) {
                for (var item, r, i = 0, l = _pending.length; i < l; i++) {
                    item = _pending[i];
                    r = item[1](_result);
                    // 如果回调的结果返回的是 promise(有then方法), 则调用 then 方法并将 resolve 方法传入
                    if (r && typeof r.then === 'function') {
                        r.then.call(r, item[0].resolve);
                    } else {
                        item[0].resolve(_result);
                    }
                }
            }
            _pending = null;
        },
        promise: {
            then: function (callback) {
                // 创建一个新的 defer 并返回, 并且将 defer 和 callback 同时添加到当前的 pending 中
                var defer = Deferred();
                if (_pending) {
                    _pending.push([defer, callback]);
                } else {
                    callback(_result);
                }
                return defer.promise;
            }
        }
    }
};

执行以下代码,我们能得到:we are all done! monkey with job1 with job2 的输出

asyncJob1('monkey').then(function cbForJob1(job1Result) {
    return asyncJob2(job1Result);
}).then(function cbForJob2(job2Result) {
    console.log('we are all done!', job2Result);
});

Promise 错误分支

以上的 Promise 都是只有成功的 resolve 调用,在使用的 Promise 都能接受 2 个回调:resolve、reject。

为了实现可以 reject,需要引入一个 promise 的状态,记录它是被 resolve 还是 reject 过。

var Deferred = function () {
    var _pending = [], _result, _reason;
    var _this = {
        resolve: function (result) {
            if (_this.promise.status !== 'pending') {
                return;
            }
            _this.promise.status = 'resolved';
            _result = result;
            for (var item, r, i = 0, l = _pending.length; i < l; i++) {
                item = _pending[i];
                r = item[1](_result);
                // 如果回调的结果返回的是 promise(有then方法), 则调用 then 方法并将 resolve 方法传入
                if (r && typeof r.then === 'function') {
                    r.then.call(r, item[0].resolve, item[0].reject);
                } else {
                    item[0].resolve(_result);
                }
            }
        },
        reject: function (reason) {
            if (_this.promise.status !== 'pending') {
                return;
            }
            _this.promise.status = 'rejected';
            _reason = reason;
            for (var item, r, i = 0, l = _pending.length; i < l; i++) {
                item = _pending[i];
                r = item[2](_reason);
                if (r && typeof r.then === 'function') {
                    r.then.call(r, item[0].resolve, item[0].reject);
                } else {
                    item[0].reject(_reason);
                }
            }
        },
        promise: {
            then: function (onResolved, onRejected) {
                // 创建一个新的 defer 并返回, 并且将 defer 和 callback 同时添加到当前的 pending 中
                var defer = Deferred();
                var status = _this.promise.status;
                if (status === 'pending') {
                    _pending.push([defer, onResolved, onRejected]);
                } else if (status === 'resolved') {
                    onResolved(_result);
                } else if (status === 'rejected') {
                    onRejected(_reason);
                }
                return defer.promise;
            },
            status: 'pending'
        }
    };

    return _this;
};

为了简单起见,reject的代码和resolve差不多,可以抽取一下减少多余的代码。

Promise 融入异步

在上面的所有调用中,resolve 或 reject 里的回调调用都是同步的,这取决于回调的实现。如果回调本身是同步的,就可能会出问题。

比如按上面的 promise 的代码,把 job 的调用中的 setTimeout 去掉,就会得不到结果。

function asyncJob1(param, isOk) {
    var defer = Deferred();
    //setTimeout(function () {
        var result = param + ' with job1';
        if (isOk) {
            defer.resolve(result);
        } else {
            defer.reject('job1 fail');
        }
    //}, 100);
    return defer.promise;
}

function asyncJob2(param, isOk) {
    var defer = Deferred();
    //setTimeout(function () {
        var result = param + ' with job2';
        if (isOk) {
            defer.resolve(result);
        } else {
            defer.reject('job2 fail');
        }
    //}, 100);
    return defer.promise;
}

asyncJob1('monkey', true).then(function (job1Result) {
    return asyncJob2(job1Result, true);
}).then(function (job2Result) {
    console.log('we are all done!', job2Result); // 无输出
});

调用时机

resolve 和 reject 只有在执行环境堆栈仅包含平台代码时才可被调用 注1

注1 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 resolve 和 reject 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

这个事件队列可以采用“宏任务(macro - task)”机制或者“微任务(micro - task)”机制来实现。

由于 promise 的实施代码本身就是平台代码(译者注:即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。

所以我们要确保这些调用都是异步的,这里只是简单地用 setTimeout 来示意处理,这样之后像上面的调用也有结果输出了。

var Deferred = function () {
    var _pending = [], _result, _reason;
    var _this = {
        resolve: function (result) {
            if (_this.promise.status !== 'pending') {
                return;
            }
            _result = result;

            setTimeout(function () {
                processQueue(_pending, _this.promise.status = 'resolved', _result);
                _pending = null;
            }, 0);
        },
        reject: function (reason) {
            if (_this.promise.status !== 'pending') {
                return;
            }
            _reason = reason;

            setTimeout(function () {
                processQueue(_pending, _this.promise.status = 'rejected', null, _reason);
                _pending = null;
            }, 0);
        },
        promise: {
            then: function (onResolved, onRejected) {
                var defer = Deferred();
                var status = _this.promise.status;
                if (status === 'pending') {
                    _pending.push([defer, onResolved, onRejected]);
                } else if (status === 'resolved') {
                    onResolved(_result);
                } else if (status === 'rejected') {
                    onRejected(_reason);
                }
                return defer.promise;
            },
            status: 'pending'
        }
    };

    function processQueue(pending, status, result, reason) {
        var item, r, i, l, callbackIndex, method, param;
        if (status === 'resolved') {
            callbackIndex = 1;
            method = 'resolve';
            param = result;
        } else {
            callbackIndex = 2;
            method = 'reject';
            param = reason;
        }
        for (i = 0, l = pending.length; i < l; i++) {
            item = pending[i];
            r = item[callbackIndex](param);
            // 如果回调的结果返回的是promise(有then方法), 则调用then方法并将resolve方法传入
            if (r && typeof r.then === 'function') {
                r.then.call(r, item[0].resolve, item[0].reject);
            } else {
                item[0][method](param);
            }
        }
    }

    return _this;
};

以上仅仅是简版的 Promise,离我们平常用的promise还差很远,仅仅给自己带来的一些思考。

理解 react-redux 之 connect

redux 看上去足够简单,但却有非常强的规范约束,作为 react 全家桶中极为重要的组成部分,是 JavaScript 应用程序的可预测状态容器,是一种状态管理的解决方案。

来看其官网上的一个 gist demo:
import { createStore } from 'redux'

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

let store = createStore(counter)

store.subscribe(() =>
  console.log(store.getState())
)

store.dispatch({ type: 'INCREMENT' }) // 1
store.dispatch({ type: 'INCREMENT' }) // 2
store.dispatch({ type: 'DECREMENT' }) // 1

redux 的使用规范是确保整个应用程序的数据状态存储在单个 store 的对象树中,在 reducer 中更改状态树 state ,唯一方法是 dispatch action,这个 dispatch action 包含 type 及相关变化数据的一个对象。

一个应用往往有复杂的组件嵌套,单纯使用 redux的话,可以在最外层容器组件中初始化 store,通用做法是将 state 上的属性作为 props 层层传递下去,这种实践想想都是令人厌烦的。那接下来我们就引入 react-redux 的 connect。

react-redux 两个主要的 API 是 Provider、connect,我们先从一个简单的例子开始:

// root.js
class Root extends React.Component {
  _store: Store<any>;
  render() {
    return (
      <Provider store={this._store}>
        <App />
      </Provider>
    );
  }
}

// app.js
class App extends React.Component {}

const mapStateToProps = state => {
  return {nav: state.router};
};

export default connect(mapStateToProps)(App);

例子通过 connect 的方式将 store 数据连接到组件中,在 state 变化的时候,组件的 mapStateToProps 会被调用并重新计算出一个新的 stateProps 来更新当前组件数据。connect 做了两件事情,一是把组件需要的 store 对象树属性 map 到当前组件中,二是把组件需要的数据结构在这个 map 函数中以 plain Object 返回,这做可以方便的定制当前组件需要的数据从而避免了层层 store 数据的嵌套。

要理解 connect 是如何工作的,需要先理解以下两点:

  • React 的 context
  • Provider

理解 react 的 context

旧版 context 的一个简单使用例子如下:

// 顶层组件
class Root extends Component {
    getChildContext() {
        return {
            text: 'xxx'
        }
    }
    render() {
        return <Parent />
    }
}

// 中间层组件
class IntermediateC extends Component {
    render() {
        return <Child />
    }
}

// 需要数据的组件
class Child extends Component {
    render() {
        return this.context.text
    }
}

旧版 context 不用在每个中间层组件中都显示地将 props 设置到子组件的属性中,通过顶层组件中的 getChildContext() 方法设置需要返回的数据即可。

新版 context 用 Provider、Consumer 对来实现,Provider 用来包裹顶层组件,Consumer 用来包裹底层获取数据的组件。但是获取数据的组件有多个数据来源,那么以上的嵌套地域又将重现。关于新旧版的 context API 在使用上有很大区别,但本质上都是解决同样的问题,即解决跨组件数据传递的问题,只是新版 context API 更符合 react 风格。

具体可以参见 https://reactjs.org/docs/context.html

Provider 的工作原理

Provider 查看源码其提供了3个方法:getChildContext、constructor、render,其中构造函数获取 props.store 供组件内部使用,render 方法返回一个 react 子元素,源码为 return Children.only(this.props.children),其中的 Children.only 是 react 提供的方法,this.props.children 表示的是一个 Provider 的子组件。

Provider 是一个容器组件, 从源码看做的事情很简单,即把嵌套的内容原封不动地作为子组件给渲染出来,最重要的是把 props.store 放到 context,从而子组件 connect 的时候都可以获取到。

connect 底层工作原理

要从 connect 源码看的确复杂了,这里不再逐行解析源码。我们来继续看看 connect 的使用:
connect(...args)(Component)

通过在根组件 Provider 组件设置 store 作为整个顶层组件的 context,其下所有子孙节点组件都可以获取到这个 store 对象树的数据。那接下来 connect 做的事情其实就很明了了,即 connect 首先执行的是一个 HOC,在这个高阶组件中,connect 接下来把 mapStateToProps 和 mapDispatchToProps 里的返回的属性,连同通过 context 获取到的 store 一起,过滤包装 store 数据最终传递给了被包裹的组件,connect 不会修改传递给它的组件类,相反它返回一个新的、被连接的组件类供开发者使用。

最后 connect 通过 redux store 的 subscribe API 来监听数据的变化,通过 shallowEqual 对比之前组件缓存的 props 和新计算出的属性,来决定是否需要更新组件,即重新将 args 里边的 props 传递给第二个被传入的 Component,达到更新组件的目的。理解了 connect 的工作原理,那么接下来就知道在开发过程中需要注意什么了。

reference

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.