六种继承方式,大型单页面应用的进阶挑战

JavaScript 六种继承方式

2017/06/20 · JavaScript · 继承

原文出处: Xuthus Blog   

继承是面向对象编程中又一非常重要的概念,JavaScript支持实现继承,不支持接口继承,实现继承主要依靠原型链来实现的。

大型单页面应用的进阶挑战

2015/09/30 · HTML5, JavaScript · 单页应用

原文出处: 林子杰(@Zack__lin)   

阅读须知:这里的大型单页面应用(SPA Web App)是指页面和功能组件在一个某个量级以上,举个栗子,比如 30+个页面100+个组件,同时伴随着大量的数据交互操作和多个页面的数据同步操作。并且这里提到的页面,均属于 hash 页面,而多页面概念的页面,是一个独立的 html 文档。基于这个前提,我们再来讨论,否则我怕我们 Get 不到同一个 G 点上去。

从输入 URL 到页面加载完成的过程中都发生了什么事情?

2015/10/03 · HTML5, JavaScript · 6 评论 · HTTP, 浏览器

原文出处: 百度FEX/吴多益(@吴多益)   

背景  本文来自于之前我发的一篇微博:

图片 1

不过写这篇文章并不是为了帮大家准备面试,而是想借这道题来介绍计算机和互联网的基础知识,让读者了解它们之间是如何关联起来的。

为了便于理解,我将整个过程分为了六个问题来展开。

原型链

首先得要明白什么是原型链,在一篇文章看懂proto和prototype的关系及区别中讲得非常详细

原型链继承基本思想就是让一个原型对象指向另一个类型的实例

function SuperType() { this.property = true } SuperType.prototype.getSuperValue = function () { return this.property } function SubType() { this.subproperty = false } SubType.prototype = new SuperType() SubType.prototype.getSubValue = function () { return this.subproperty } var instance = new SubType() console.log(instance.getSuperValue()) // true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
  return this.subproperty
}
var instance = new SubType()
console.log(instance.getSuperValue()) // true

代码定义了两个类型SuperType和SubType,每个类型分别有一个属性和一个方法,SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。

实现的本质是重写原型对象,代之以一个新类型的实例,那么存在SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。

我们知道,在创建一个实例的时候,实例对象中会有一个内部指针指向创建它的原型,进行关联起来,在这里代码SubType.prototype = new SuperType(),也会在SubType.prototype创建一个内部指针,将SubType.prototype与SuperType关联起来。

所以instance指向SubType的原型,SubType的原型又指向SuperType的原型,继而在instance在调用getSuperValue()方法的时候,会顺着这条链一直往上找。

添加方法

在给SubType原型添加方法的时候,如果,父类上也有同样的名字,SubType将会覆盖这个方法,达到重新的目的。 但是这个方法依然存在于父类中。

记住不能以字面量的形式添加,因为,上面说过通过实例继承本质上就是重写,再使用字面量形式,又是一次重写了,但这次重写没有跟父类有任何关联,所以就会导致原型链截断。

function SuperType() { this.property = true } SuperType.prototype.getSuperValue = function () { return this.property } function SubType() { this.subproperty = false } SubType.prototype = new SuperType() SubType.prototype = { getSubValue:function () { return this.subproperty } } var instance = new SubType() console.log(instance.getSuperValue()) // error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType() {
  this.property = true
}
SuperType.prototype.getSuperValue = function () {
  return this.property
}
function SubType() {
  this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype = {
  getSubValue:function () {
   return this.subproperty
  }
}
var instance = new SubType()
console.log(instance.getSuperValue())  // error

问题

单纯的使用原型链继承,主要问题来自包含引用类型值的原型。

function SuperType() { this.colors = ['red', 'blue', 'green'] } function SubType() { } SubType.prototype = new SuperType() var instance1 = new SubType() var instance2 = new SubType() instance1.colors.push('black') console.log(instance1.colors) // ["red", "blue", "green", "black"] console.log(instance2.colors) // ["red", "blue", "green", "black"]

1
2
3
4
5
6
7
8
9
10
11
function SuperType() {
  this.colors = ['red', 'blue', 'green']
}
function SubType() {
}
SubType.prototype = new SuperType()
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors)  // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green", "black"]

在SuperType构造函数定义了一个colors属性,当SubType通过原型链继承后,这个属性就会出现SubType.prototype中,就跟专门创建了SubType.prototype.colors一样,所以会导致SubType的所有实例都会共享这个属性,所以instance1修改colors这个引用类型值,也会反映到instance2中。

挑战一:前端组件化

基于我们所说的前提,第一个面对的挑战是组件化。这里还是要强调的是组件化根本目的不是为了复用,很多人根本没想明白这点,总是觉得造的轮子别的业务可以用,说不定以后也可以用。

其实前端发展迭代这么快,交互变化也快,各种适配更新层出不穷。今天造的轮子,过阵子别人造了个高级轮子,大家都会选更高档的轮子,所以现在前端界有一个现象就是为了让别人用自己的轮子,自己使劲不停地造。

在前端工业化生产趋势下,如果要提高生产效率,就必须让组件规范化标准化,达到怎样的程度呢?一辆车除了底盘和车身框架需要自己设计打造之外,其他标准化零件都可以采购组装(专业学得差,有啥谬误请指正)。也就是说,除了 UI 和前端架构需要自己解决之外,其他的组件都是可以奉行拿来主义的,如果打算让车子跑得更稳更安全,可以对组件进行打磨优化完善。

说了这么说,倒不如看看徐飞的文章《2015前端组件化框架之路》 里面写的内容都是经过一定实践得出的想法,所以大部分内容我是赞成而且深有体会的。

第一个问题:从输入 URL 到浏览器接收的过程中发生了什么事情?

借用构造函数

此方法为了解决原型中包含引用类型值所带来的问题。

这种方法的思想就是在子类构造函数的内部调用父类构造函数,可以借助apply()和call()方法来改变对象的执行上下文

function SuperType() { this.colors = ['red', 'blue', 'green'] } function SubType() { // 继承SuperType SuperType.call(this) } var instance1 = new SubType() var instance2 = new SubType() instance1.colors.push('black') console.log(instance1.colors) // ["red", "blue", "green", "black"] console.log(instance2.colors) // ["red", "blue", "green"]

1
2
3
4
5
6
7
8
9
10
11
12
function SuperType() {
  this.colors = ['red', 'blue', 'green']
}
function SubType() {
  // 继承SuperType
  SuperType.call(this)
}
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors)  // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green"]

在新建SubType实例是调用了SuperType构造函数,这样以来,就会在新SubType对象上执行SuperType函数中定义的所有对象初始化代码。

结果,SubType的每个实例就会具有自己的colors属性的副本了。

传递参数

借助构造函数还有一个优势就是可以传递参数

function SuperType(name) { this.name = name } function SubType() { // 继承SuperType SuperType.call(this, 'Jiang') this.job = 'student' } var instance = new SubType() console.log(instance.name) // Jiang console.log(instance.job) // student

1
2
3
4
5
6
7
8
9
10
11
12
function SuperType(name) {
  this.name = name
}
function SubType() {
  // 继承SuperType
  SuperType.call(this, 'Jiang')
 
  this.job = 'student'
}
var instance = new SubType()
console.log(instance.name)  // Jiang
console.log(instance.job)   // student

问题

如果仅仅借助构造函数,方法都在构造函数中定义,因此函数无法达到复用

挑战二:路由去中心化

基于我们所说的前提,中心化的路由维护起来很坑爹(如果做一两个页面 DEMO 的就没必要出来现眼了)。MV* 架构就是存在这么个坑爹的问题,需要声明中心化 route(angular 和 react 等都需要先声明页面路由结构),针对不同的路由加载哪些组件模块。一旦页面多起来,甚至假如有人偷懒直接在某个路由写了一些业务耦合的逻辑,这个 route 的可维护性就变得有些糟糕了。而且用户访问的第一个页面,都需要加载 route,即使其他路由的代码跟当前页面无关。

我们再回过头来思考静态页面简单的加载方式。我们只要把 nginx 搭起来,把 html 页面放在对应的静态资源目录下,启动 nginx 服务后,在浏览器地址栏输入 127.0.0.1:8888/index.html 就可以访问到这个页面。再复杂一点,我们把目录整成下面的形式:

/post/201509151800.html /post/201509151905.html /post/201509152001.html /category/js_base_knowledge.html /category/css_junior_use.html /category/life_is_beautiful.html

1
2
3
4
5
6
/post/201509151800.html
/post/201509151905.html
/post/201509152001.html
/category/js_base_knowledge.html
/category/css_junior_use.html
/category/life_is_beautiful.html

这种目录结构很熟吧,对 SEO 很友好吧,当然这是后话了,跟我们今天说的不是一回事。这种目录结果,不用我们去给 Web Server 定义一堆路由规则,页面存在即返回,否则返回 404,完全不需要多余的声明逻辑。

基于这种目录结构,我们可以抽象成这样子:

/{page_type}/{page_name}.html

1
/{page_type}/{page_name}.html

其实还可以更简单:

/p/{name}.html

1
/p/{name}.html

从组件化的角度出发,还可以这样子:

/p/{name}/name.js /p/{name}/name.tpl /p/{name}/name.css

1
2
3
/p/{name}/name.js
/p/{name}/name.tpl
/p/{name}/name.css

所以,按照我们简化后的逻辑,我们只需要一个 page.js 这样一个路由加载器,按照我们约定的资源目录结构去加载相应的页面,我们就不需要去干声明路由并且中心化路由这种蠢事了。具体来看代码。咱也懒得去解析了,里面有注释。

从触屏到 CPU

首先是「输入 URL」,大部分人的第一反应会是键盘,不过为了与时俱进,这里将介绍触摸屏设备的交互。

触摸屏一种传感器,目前大多是基于电容(Capacitive)来实现的,以前都是直接覆盖在显示屏上的,不过最近出现了 3 种嵌入到显示屏中的技术,第一种是 iPhone 5 的 In-cell,它能减小了 0.5 毫米的厚度,第二种是三星使用的 On-cell 技术,第三种是国内厂商喜欢用的 OGS 全贴合技术,具体细节可以阅读这篇文章。

当手指在这个传感器上触摸时,有些电子会传递到手上,从而导致该区域的电压变化,触摸屏控制器芯片根据这个变化就能计算出所触摸的位置,然后通过总线接口将信号传到 CPU 的引脚上。

以 Nexus 5 为例,它所使用的触屏控制器是 Synaptics S3350B,总线接口为 I²C,以下是 Synaptics 触摸屏和处理器连接的示例:图片 2

左边是处理器,右边是触摸屏控制器,中间的 SDA 和 SCL 连线就是 I²C 总线接口。

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

组合继承是将原型链继承和构造函数结合起来,从而发挥二者之长的一种模式。

思路就是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType(name) { this.name = name this.colors = ['red', 'blue', 'green'] } SuperType.prototype.sayName = function () { console.log(this.name) } function SubType(name, job) { // 继承属性 SuperType.call(this, name) this.job = job } // 继承方法 SubType.prototype = new SuperType() SubType.prototype.constructor = SuperType SubType.prototype.sayJob = function() { console.log(this.job) } var instance1 = new SubType('Jiang', 'student') instance1.colors.push('black') console.log(instance1.colors) //["red", "blue", "green", "black"] instance1.sayName() // 'Jiang' instance1.sayJob() // 'student' var instance2 = new SubType('J', 'doctor') console.log(instance2.colors) // //["red", "blue", "green"] instance2.sayName() // 'J' instance2.sayJob() // 'doctor'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 继承属性
  SuperType.call(this, name)
 
  this.job = job
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SuperType
SubType.prototype.sayJob = function() {
  console.log(this.job)
}
var instance1 = new SubType('Jiang', 'student')
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "green", "black"]
instance1.sayName() // 'Jiang'
instance1.sayJob()  // 'student'
var instance2 = new SubType('J', 'doctor')
console.log(instance2.colors) // //["red", "blue", "green"]
instance2.sayName()  // 'J'
instance2.sayJob()  // 'doctor'

这种模式避免了原型链和构造函数继承的缺陷,融合了他们的优点,是最常用的一种继承模式。

挑战三:领域数据中心化

对于单向数据流循环和数据双向绑定谁优谁劣是永远也讨论没结果的问题,要看是什么业务场景什么业务逻辑,如果这个前提没统一好说啥都是白搭。当然,这个挑战的前提是非后台的单页面应用,后台的前端根本就不需要考虑前端内存缓存数据的处理,直接跟接口数据库交互就行了。明确了这个前提,我们接着讨论什么叫领域数据中心化。

前面讨论到两种数据绑定的方式,但是如果频繁跟接口交互:

  • 内存数据销毁了,重新请求数据耗时浪费流量
  • 如果两个接口字段部分不一样但是使用场景一样
  • 多个页面直接有部分的数据相同,但是先来后到导致某些计数字段不一致
  • 多个页面的数据相同,其中某些数据发生用户操作行为导致数据发生变动

因此,我们需要在业务视图逻辑层和数据接口层中间增加一个 store(领域模型),而这个 store 需要有一个统一的 内存缓存 cache,这个 cache 就是中心化的数据缓存。那这个 store 究竟是用来弄啥勒?

图片 3

Store 具有多形态,每个 store 好比某一类物品的仓储(领域,换个词容易理解),如蔬果店 fruit-store, 服装店 clothes-store,蔬果店可以放苹果香蕉黑木耳,服装店可以放背心底裤人字拖。如果品种过于繁多,我们可以把蔬果店精细化运营变成香蕉专卖店,苹果专卖店(!== appstore),甚至是黑木耳专卖店…( _ _)ノ|,蔬果种类不一样,但是也都是称重按斤卖嘛。

var bannerStore = new fruitStore();

var appleStore = new fruitStore();

有了这些仓储之后,我们可以放心的把数据丢给视图逻辑层大胆去用。想修改数据?直接让 store 去改就行了,其他页面的 DOM 文本内容也得修改吧?那是其他页面的业务逻辑做的事,我们把事件抛出去就好了,他们处不处理那是他们的事,咱别瞎操心(业务隔离)。

那么 store 具体弄啥勒?

图片 4

  • 32 个赞位置可点赞或者取消,三个页面的赞数需要同步,按钮点赞与取消的状态也要同步。
  • 条目是否已收藏,取消收藏后 Page B 需要删除数据,Page A+C 需要同步状态,如果在 Page C 又有收藏操作,Page B 需要相应增减数据,Page A 状态需要同步。
  • 发评论,Page C 需要更新评论列表和评论数,Page A+B 需要更新评论数。如果 Page B 没有被加载过,这时候 Page B 拿到的数据应该是最新的,需要同步给 A+C 页面对应的数据进行更新。

所以,store 干的活就是数据状态读写和同步,如果把数据状态的操作放到各个页面自己去处理,页面一旦多了或者复杂起来,就会产生各个页面数据和状态可能不一致,页面之前双向引用(业务耦合严重)。store 还有另一个作用就是数据的输入输出格式化,简单举个栗子:图片 5

  • 任何接口 API 返回的数据,都需要经过 input format 进行统一格式化,然后再写入 cache,因为读取的数据已按照我们约定的规范进行的处理,所以我们使用的时候也不需要理会接口是返回怎样的数据类型。
  • 某些组件需要的数据字段格式可能不同,如果把数据处理放在模板进行处理,会导致模板无法更加简洁通用(业务耦合),所以需要 output format 进行处理。

所以,store 就是扮演着这样的角色——是数据状态读写和同步,以及数据输入输出的格式化处理。

CPU 内部的处理

移动设备中的 CPU 并不是一个单独的芯片,而是和 GPU 等芯片集成在一起,被称为 SoC(片上系统)。

前面提到了触屏和 CPU 的连接,这个连接和大部分计算机内部的连接一样,都是通过电气信号来进行通信的,也就是电压高低的变化,如下面的时序图:图片 6

在时钟的控制下,这些电流会经过 MOSFET 晶体管,晶体管中包含 N 型半导体和 P 型半导体,通过电压就能控制线路开闭,然后这些 MOSFET 构成了 CMOS,接着再由 CMOS 实现「与」「或」「非」等逻辑电路门,最后由逻辑电路门上就能实现加法、位移等计算,整体如下图所示(来自《计算机体系结构》):图片 7

除了计算,在 CPU 中还需要存储单元来加载和存储数据,这个存储单元一般通过触发器(Flip-flop)来实现,称为寄存器。

以上这些概念都比较抽象,推荐阅读「How to Build an 8-Bit Computer」这篇文章,作者基于晶体管、二极管、电容等原件制作了一个 8 位的计算机,支持简单汇编指令和结果输出,虽然现代 CPU 的实现要比这个复杂得多,但基本原理还是一样的。

另外其实我也是刚开始学习 CPU 芯片的实现,所以就不在这误人子弟了,感兴趣的读者请阅读本节后面推荐的书籍。

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function object(o) { function F() {} F.prototype = o return new F() }

1
2
3
4
5
function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

在object函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。

本质上来说,object对传入其中的对象执行了一次浅复制。

var person = { name: 'Jiang', friends: ['Shelby', 'Court'] } var anotherPerson = object(person) console.log(anotherPerson.friends) // ['Shelby', 'Court']

1
2
3
4
5
6
var person = {
  name: 'Jiang',
  friends: ['Shelby', 'Court']
}
var anotherPerson = object(person)
console.log(anotherPerson.friends)  // ['Shelby', 'Court']

这种模式要去你必须有一个对象作为另一个对象的基础。

在这个例子中,person作为另一个对象的基础,把person传入object中,该函数就会返回一个新的对象。

这个新对象将person作为原型,所以它的原型中就包含一个基本类型和一个引用类型。

所以意味着如果还有另外一个对象关联了person,anotherPerson修改数组friends的时候,也会体现在这个对象中。

Object.create()方法

ES5通过Object.create()方法规范了原型式继承,可以接受两个参数,一个是用作新对象原型的对象和一个可选的为新对象定义额外属性的对象,行为相同,基本用法和上面的object一样,除了object不能接受第二个参数以外。

var person = { name: 'Jiang', friends: ['Shelby', 'Court'] } var anotherPerson = Object.create(person) console.log(anotherPerson.friends) // ['Shelby', 'Court']

1
2
3
4
5
6
var person = {
  name: 'Jiang',
  friends: ['Shelby', 'Court']
}
var anotherPerson = Object.create(person)
console.log(anotherPerson.friends)  // ['Shelby', 'Court']

挑战四:Hybrid App 化

现在 Hybrid App 架构应用很火啊 _ (:3」∠)_,不搞一下都不好意思说自己是做 H5的。这里所说的 Hybrid App 可不是那种内置打包的 html 源码那种,而是直接去服务端请求 html 文档那种,可能会使用离线缓存。有的人以为如果要使用 Hybrid 架构,就不能使用 SPA 的方式,其实 Hybrid 架构更应该使用 SPA。

遇到的几个问题,我简单列举一下:

  • 客户端通过 url 传参

    如果通过 http get 请求的 query 参数进行传参,会导致命中不到 html 文档缓存,所以通过 SPA 的 hash query 传参,可以规避这个问题。

  • 与其他 html 页面进行跳转

    这种场景下,进入新页面和返回旧页面导致 webview 会重新加载本地的 html 文档缓存,视觉体验很不爽,即使页面使用了离线缓存,而 SPA 可以规避这个问题。

  • 使用了离线缓存的页面需要支持代码多版本化

    由于采用了非覆盖性资源发布方式,所以需要仍然保留旧的代码一段时间,以防止用户使用旧的 html 文档访问某些按需加载功能或清除了本地缓存数据而拿不到旧版本代码。

  • js 和 css 资源 离线化

    由于离线缓存的资源需要先在 manifest 文件声明,你也不可能总是手动去维护需要引用的 js 和 css 资源,并且那些按需加载的功能也会因此失去按需加载的意义。所以需要将 js 和 css 缓存到 localstorage,直接省去这一步维护操作。至于用户清除 localstorage,参考第三点解决方案。

  • 图标资源离线化

    将图标文件进行 base64 编码后存入 css 文件,方便离线使用。

从 CPU 到操作系统内核

前面说到触屏控制器将电气信号发送到 CPU 对应的引脚上,接着就会触发 CPU 的中断机制,以 Linux 为例,每个外部设备都有一标识符,称为中断请求(IRQ)号,可以通过 /proc/interrupts 文件来查看系统中所有设备的中断请求号,以下是 Nexus 7 (2013) 的部分结果:

shell@flo:/ $ cat /proc/interrupts CPU0 17: 0 GIC dg_timer 294: 1973609 msmgpio elan-ktf3k 314: 679 msmgpio KEY_POWER

1
2
3
4
5
shell@flo:/ $ cat /proc/interrupts
            CPU0
  17:          0       GIC  dg_timer
294:    1973609   msmgpio  elan-ktf3k
314:        679   msmgpio  KEY_POWER

因为 Nexus 7 使用了 ELAN 的触屏控制器,所以结果中的 elan-ktf3k 就是触屏的中断请求信息,其中 294 是中断号,1973609 是触发的次数(手指单击时会产生两次中断,但滑动时会产生上百次中断)。

为了简化这里不考虑优先级问题,以 ARMv7 架构的处理器为例,当中断发生时,CPU 会停下当前运行的程序,保存当前执行状态(如 PC 值),进入 IRQ 状态),然后跳转到对应的中断处理程序执行,这个程序一般由第三方内核驱动来实现,比如前面提到的 Nexus 7 的驱动源码在这里 touchscreen/ektf3k.c。

这个驱动程序将读取 I²C 总线中传来的位置数据,然后通过内核的 input_report_abs 等方法记录触屏按下坐标等信息,最后由内核中的input 子模块将这些信息都写进 /dev/input/event0 这个设备文件中,比如下面展示了一次触摸事件所产生的信息:

130|shell@flo:/ $ getevent -lt /dev/input/event0 [ 414624.658986] EV_ABS ABS_MT_TRACKING_ID 0000835c [ 414624.659017] EV_ABS ABS_MT_TOUCH_MAJOR 0000000b [ 414624.659047] EV_ABS ABS_MT_PRESSURE 0000001d [ 414624.659047] EV_ABS ABS_MT_POSITION_X 000003f0 [ 414624.659078] EV_ABS ABS_MT_POSITION_Y 00000588 [ 414624.659078] EV_SYN SYN_REPORT 00000000 [ 414624.699239] EV_ABS ABS_MT_TRACKING_ID ffffffff [ 414624.699270] EV_SYN SYN_REPORT 00000000

1
2
3
4
5
6
7
8
9
130|shell@flo:/ $ getevent -lt /dev/input/event0
[  414624.658986] EV_ABS       ABS_MT_TRACKING_ID   0000835c
[  414624.659017] EV_ABS       ABS_MT_TOUCH_MAJOR   0000000b
[  414624.659047] EV_ABS       ABS_MT_PRESSURE      0000001d
[  414624.659047] EV_ABS       ABS_MT_POSITION_X    000003f0
[  414624.659078] EV_ABS       ABS_MT_POSITION_Y    00000588
[  414624.659078] EV_SYN       SYN_REPORT           00000000
[  414624.699239] EV_ABS       ABS_MT_TRACKING_ID   ffffffff
[  414624.699270] EV_SYN       SYN_REPORT           00000000

寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数。

function createAnother(o) { var clone = Object.create(o) // 创建一个新对象 clone.sayHi = function() { // 添加方法 console.log('hi') } return clone // 返回这个对象 } var person = { name: 'Jiang' } var anotherPeson = createAnother(person) anotherPeson.sayHi()

1
2
3
4
5
6
7
8
9
10
11
12
function createAnother(o) {
  var clone = Object.create(o) // 创建一个新对象
  clone.sayHi = function() { // 添加方法
    console.log('hi')
  }
  return clone  // 返回这个对象
}
var person = {
  name: 'Jiang'
}
var anotherPeson = createAnother(person)
anotherPeson.sayHi()

基于person返回了一个新对象anotherPeson,新对象不仅拥有了person的属性和方法,还有自己的sayHi方法。

在主要考虑对象而不是自定义类型和构造函数的情况下,这是一个有用的模式。

挑战五:性能优化

@前端农民工 在 别处 已经说得很清楚了,直接传送门过去看吧,这里不罗嗦了。

 

1 赞 2 收藏 评论

图片 8

从操作系统 GUI 到浏览器

前面提到 Linux 内核已经完成了对硬件的抽象,其它程序只需要通过监听 /dev/input/event0 文件的变化就能知道用户进行了哪些触摸操作,不过如果每个程序都这么做实在太麻烦了,所以在图像操作系统中都会包含 GUI 框架来方便应用程序开发,比如 Linux 下著名的 X。

但 Android 并没有使用 X,而是自己实现了一套 GUI 框架,其中有个 EventHub 的服务会通过 epoll 方式监听 /dev/input/ 目录下的文件,然后将这些信息传递到 Android 的窗口管理服务(WindowManagerService)中,它会根据位置信息来查找相应的 app,然后调用其中的监听函数(如 onTouch 等)。

就这样,我们解答了第一个问题,不过由于时间有限,这里省略了很多细节,想进一步学习的读者推荐阅读以下书籍。

寄生组合式继承

在前面说的组合模式(原型链+构造函数)中,继承的时候需要调用两次父类构造函数。

父类

function SuperType(name) { this.name = name this.colors = ['red', 'blue', 'green'] }

1
2
3
4
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

第一次在子类构造函数中

function SubType(name, job) { // 继承属性 SuperType.call(this, name) this.job = job }

1
2
3
4
5
6
function SubType(name, job) {
  // 继承属性
  SuperType.call(this, name)
 
  this.job = job
}

第二次将子类的原型指向父类的实例

// 继承方法 SubType.prototype = new SuperType()

1
2
// 继承方法
SubType.prototype = new SuperType()

当使用var instance = new SubType()的时候,会产生两组name和color属性,一组在SubType实例上,一组在SubType原型上,只不过实例上的屏蔽了原型上的。

使用寄生式组合模式,可以规避这个问题。

这种模式通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

基本思路:不必为了指定子类型的原型而调用父类的构造函数,我们需要的无非就是父类原型的一个副本。

本质上就是使用寄生式继承来继承父类的原型,在将结果指定给子类型的原型。

function inheritPrototype(subType, superType) { var prototype = Object.create(superType.prototype) prototype.constructor = subType subType.prototype = prototype }

1
2
3
4
5
function inheritPrototype(subType, superType) {
  var prototype = Object.create(superType.prototype)
  prototype.constructor = subType
  subType.prototype = prototype
}

该函数实现了寄生组合继承的最简单形式。

这个函数接受两个参数,一个子类,一个父类。

第一步创建父类原型的副本,第二步将创建的副本添加constructor属性,第三部将子类的原型指向这个副本。

function SuperType(name) { this.name = name this.colors = ['red', 'blue', 'green'] } SuperType.prototype.sayName = function () { console.log(this.name) } function SubType(name, job) { // 继承属性 SuperType.call(this, name) this.job = job } // 继承 inheritPrototype(SubType, SuperType) var instance = new SubType('Jiang', 'student') instance.sayName()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 继承属性
  SuperType.call(this, name)
 
  this.job = job
}
// 继承
inheritPrototype(SubType, SuperType)
var instance = new SubType('Jiang', 'student')
instance.sayName()

补充:直接使用Object.create来实现,其实就是将上面封装的函数拆开,这样演示可以更容易理解。

function SuperType(name) { this.name = name this.colors = ['red', 'blue', 'green'] } SuperType.prototype.sayName = function () { console.log(this.name) } function SubType(name, job) { // 继承属性 SuperType.call(this, name) this.job = job } // 继承 SubType.prototype = Object.create(SuperType.prototype) // 修复constructor SubType.prototype.constructor = SubType var instance = new SubType('Jiang', 'student') instance.sayName()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, job) {
  // 继承属性
  SuperType.call(this, name)
 
  this.job = job
}
// 继承
SubType.prototype = Object.create(SuperType.prototype)
// 修复constructor
SubType.prototype.constructor = SubType
var instance = new SubType('Jiang', 'student')
instance.sayName()

ES6新增了一个方法,Object.setPrototypeOf,可以直接创建关联,而且不用手动添加constructor属性。

// 继承 Object.setPrototypeOf(SubType.prototype, SuperType.prototype) console.log(SubType.prototype.constructor === SubType) // true

1
2
3
// 继承
Object.setPrototypeOf(SubType.prototype, SuperType.prototype)
console.log(SubType.prototype.constructor === SubType) // true

1 赞 2 收藏 评论

图片 9

扩展学习

  • 《计算机体系结构》
  • 《计算机体系结构:量化研究方法》
  • 《计算机组成与设计:硬件/软件接口》
  • 《编码》
  • 《CPU自制入门》
  • 《操作系统概念》
  • 《ARMv7-AR 体系结构参考手册》
  • 《Linux内核设计与实现》
  • 《精通Linux设备驱动程序开发》

第二个问题:浏览器如何向网卡发送数据?

从浏览器到浏览器内核

前面提到操作系统 GUI 将输入事件传递到了浏览器中,在这过程中,浏览器可能会做一些预处理,比如 Chrome 会根据历史统计来预估所输入字符对应的网站,比如输入了「ba」,根据之前的历史发现 90% 的概率会访问「www.baidu.com 」,因此就会在输入回车前就马上开始建立 TCP 链接甚至渲染了,这里面还有很多其它策略,感兴趣的读者推荐阅读 High Performance Networking in Chrome。

接着是输入 URL 后的「回车」,这时浏览器会对 URL 进行检查,首先判断协议,如果是 http 就按照 Web 来处理,另外还会对这个 URL 进行安全检查,然后直接调用浏览器内核中的对应方法,比如 WebView 中的 loadUrl 方法。

在浏览器内核中会先查看缓存,然后设置 UA 等 HTTP 信息,接着调用不同平台下网络请求的方法。

需要注意浏览器和浏览器内核是不同的概念,浏览器指的是 Chrome、Firefox,而浏览器内核则是 Blink、Gecko,浏览器内核只负责渲染,GUI 及网络连接等跨平台工作则是浏览器实现的

HTTP 请求的发送

因为网络的底层实现是和内核相关的,所以这一部分需要针对不同平台进行处理,从应用层角度看主要做两件事情:通过 DNS 查询 IP、通过 Socket 发送数据,接下来就分别介绍这两方面的内容。

DNS 查询

应用程序可以直接调用 Libc 提供的 getaddrinfo() 方法来实现 DNS 查询。

DNS 查询其实是基于 UDP 来实现的,这里我们通过一个具体例子来了解它的查找过程,以下是使用 dig +trace fex.baidu.com 命令得到的结果(省略了一些):

; <<>> DiG 9.8.3-P1 <<>> +trace fex.baidu.com ;; global options: +cmd . 11157 IN NS g.root-servers.net. . 11157 IN NS i.root-servers.net. . 11157 IN NS j.root-servers.net. . 11157 IN NS a.root-servers.net. . 11157 IN NS l.root-servers.net. ;; Received 228 bytes from 8.8.8.8#53(8.8.8.8) in 220 ms com. 172800 IN NS a.gtld-servers.net. com. 172800 IN NS c.gtld-servers.net. com. 172800 IN NS m.gtld-servers.net. com. 172800 IN NS h.gtld-servers.net. com. 172800 IN NS e.gtld-servers.net. ;; Received 503 bytes from 192.36.148.17#53(192.36.148.17) in 185 ms baidu.com. 172800 IN NS dns.baidu.com. baidu.com. 172800 IN NS ns2.baidu.com. baidu.com. 172800 IN NS ns3.baidu.com. baidu.com. 172800 IN NS ns4.baidu.com. baidu.com. 172800 IN NS ns7.baidu.com. ;; Received 201 bytes from 192.48.79.30#53(192.48.79.30) in 1237 ms fex.baidu.com. 7200 IN CNAME fexteam.duapp.com. fexteam.duapp.com. 300 IN CNAME duapp.n.shifen.com. n.shifen.com. 86400 IN NS ns1.n.shifen.com. n.shifen.com. 86400 IN NS ns4.n.shifen.com. n.shifen.com. 86400 IN NS ns2.n.shifen.com. n.shifen.com. 86400 IN NS ns5.n.shifen.com. n.shifen.com. 86400 IN NS ns3.n.shifen.com. ;; Received 258 bytes from 61.135.165.235#53(61.135.165.235) in 2 ms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
; <<>> DiG 9.8.3-P1 <<>> +trace fex.baidu.com
;; global options: +cmd
.           11157   IN  NS  g.root-servers.net.
.           11157   IN  NS  i.root-servers.net.
.           11157   IN  NS  j.root-servers.net.
.           11157   IN  NS  a.root-servers.net.
.           11157   IN  NS  l.root-servers.net.
;; Received 228 bytes from 8.8.8.8#53(8.8.8.8) in 220 ms
 
com.            172800  IN  NS  a.gtld-servers.net.
com.            172800  IN  NS  c.gtld-servers.net.
com.            172800  IN  NS  m.gtld-servers.net.
com.            172800  IN  NS  h.gtld-servers.net.
com.            172800  IN  NS  e.gtld-servers.net.
;; Received 503 bytes from 192.36.148.17#53(192.36.148.17) in 185 ms
 
baidu.com.      172800  IN  NS  dns.baidu.com.
baidu.com.      172800  IN  NS  ns2.baidu.com.
baidu.com.      172800  IN  NS  ns3.baidu.com.
baidu.com.      172800  IN  NS  ns4.baidu.com.
baidu.com.      172800  IN  NS  ns7.baidu.com.
;; Received 201 bytes from 192.48.79.30#53(192.48.79.30) in 1237 ms
 
fex.baidu.com.      7200    IN  CNAME   fexteam.duapp.com.
fexteam.duapp.com.  300 IN  CNAME   duapp.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns1.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns4.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns2.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns5.n.shifen.com.
n.shifen.com.       86400   IN  NS  ns3.n.shifen.com.
;; Received 258 bytes from 61.135.165.235#53(61.135.165.235) in 2 ms

可以看到这是一个逐步缩小范围的查找过程,首先由本机所设置的 DNS 服务器(8.8.8.8)向 DNS 根节点查询负责 .com 区域的域务器,然后通过其中一个负责 .com 的服务器查询负责 baidu.com 的服务器,最后由其中一个 baidu.com 的域名服务器查询 fex.baidu.com 域名的地址。

可能你在查询某些域名的时会发现和上面不一样,最底将看到有个奇怪的服务器抢先返回结果。。。

这里为了方便描述,忽略了很多不同的情况,比如 127.0.0.1 其实走的是 loopback,和网卡设备没关系;比如 Chrome 会在浏览器启动的时预先查询 10 个你有可能访问的域名;还有 Hosts 文件、缓存时间 TTL(Time to live)的影响等。

通过 Socket 发送数据

有了 IP 地址,就可以通过 Socket API 来发送数据了,这时可以选择 TCP 或 UDP 协议,具体使用方法这里就不介绍了,推荐阅读 Beej’s Guide to Network Programming。

HTTP 常用的是 TCP 协议,由于 TCP 协议的具体细节到处都能看到,所以本文就不介绍了,这里谈一下 TCP 的 Head-of-line blocking 问题:假设客户端的发送了 3 个 TCP 片段(segments),编号分别是 1、2、3,如果编号为 1 的包传输时丢了,即便编号 2 和 3 已经到达也只能等待,因为 TCP 协议需要保证顺序,这个问题在 HTTP pipelining 下更严重,因为 HTTP pipelining 可以让多个 HTTP 请求通过一个 TCP 发送,比如发送两张图片,可能第二张图片的数据已经全收到了,但还得等第一张图片的数据传到。

为了解决 TCP 协议的性能问题,Chrome 团队去年提出了 QUIC 协议,它是基于 UDP 实现的可靠传输,比起 TCP,它能减少很多来回(round trip)时间,还有前向纠错码(Forward Error Correction)等功能。目前 Google Plus、 Gmail、Google Search、blogspot、Youtube 等几乎大部分 Google 产品都在使用 QUIC,可以通过 chrome://net-internals/#spdy 页面来发现。

虽然目前除了 Google 还没人用 QUIC,但我觉得挺有前景的,因为优化 TCP 需要升级系统内核(比如 Fast Open)。

浏览器对同一个域名有连接数限制,大部分是 6,我以前认为将这个连接数改大后会提升性能,但实际上并不是这样的,Chrome 团队有做过实验,发现从 6 改成 10 后性能反而下降了,造成这个现象的因素有很多,如建立连接的开销、拥塞控制等问题,而像 SPDY、HTTP 2.0 协议尽管只使用一个 TCP 连接来传输数据,但性能反而更好,而且还能实现请求优先级。

另外,因为 HTTP 请求是纯文本格式的,所以在 TCP 的数据段中可以直接分析 HTTP 的文本,如果发现。。。

Socket 在内核中的实现

前面说到浏览器的跨平台库通过调用 Socket API 来发送数据,那么 Socket API 是如何实现的呢?

以 Linux 为例,它的实现在这里 socket.c,目前我还不太了解,推荐读者看看 Linux kernel map,它标注出了关键路径的函数,方便学习从协议栈到网卡驱动的实现。

底层网络协议的具体例子

接下来如果继续介绍 IP 协议和 MAC 协议可能很多读者会晕,所以本节将使用 Wireshark 来通过具体例子讲解,以下是我请求百度首页时抓取到的网络数据:图片 10

最底下是实际的二进制数据,中间是解析出来的各个字段值,可以看到其中最底部为 HTTP 协议(Hypertext Transfer Protocol),在 HTTP 之前有 54 字节(0x36),这就是底层网络协议所带来的开销,我们接下来对这些协议进行分析。

在 HTTP 之上是 TCP 协议(Transmission Control Protocol),它的具体内容如下图所示:图片 11

通过底部的二进制数据,可以看到 TCP 协议是加在 HTTP 文本前面的,它有 20 个字节,其中定义了本地端口(Source port)和目标端口(Destination port)、顺序序号(Sequence Number)、窗口长度等信息,以下是 TCP 协议各个部分数据的完整介绍:

0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Data | |U|A|E|R|S|F| | | Offset| Reserved |R|C|O|S|Y|I| Window | | | |G|K|L|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Options | Padding | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | data | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |           |U|A|E|R|S|F|                               |
| Offset| Reserved  |R|C|O|S|Y|I|            Window             |
|       |           |G|K|L|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options                    |    Padding    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                             data                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

具体各个字段的作用这里就不介绍了,感兴趣的读者可以阅读 RFC 793,并结合抓包分析来理解。

需要注意的是,在 TCP 协议中并没有 IP 地址信息,因为这是在上一层的 IP 协议中定义的,如下图所示:图片 12

IP 协议同样是在 TCP 前面的,它也有 20 字节,在这里指明了版本号(Version)为 4,源(Source) IP 为 192.168.1.106,目标(Destination) IP 为 119.75.217.56,因此 IP 协议最重要的作用就是确定 IP 地址。

因为 IP 协议中可以查看到目标 IP 地址,所以如果发现某些特定的 IP 地址,某些路由器就会。。。

但是,光靠 IP 地址是无法进行通信的,因为 IP 地址并不和某台设备绑定,比如你的笔记本的 IP 在家中是 192.168.1.1,但到公司就变成172.22.22.22 了,所以在底层通信时需要使用一个固定的地址,这就是 MAC(media access control) 地址,每个网卡出厂时的 MAC 地址都是固定且唯一的。

因此再往上就是 MAC 协议,它有 14 字节,如下所示:图片 13

当一台电脑加入网络时,需要通过 ARP 协议告诉其它网络设备它的 IP 及对应的 MAC 地址是什么,这样其它设备就能通过 IP 地址来查找对应的设备了。

最顶上的 Frame 是代表 Wireshark 的抓包序号,并不是网络协议

就这样,我们解答了第二个问题,不过其实这里面还有很多很多细节没介绍,建议大家通过下面的书籍进一步学习。

扩展学习

  • 《计算机网络:自顶向下方法与Internet特色》
  • 《计算机网络》
  • 《Web性能权威指南》

第三个问题:数据如何从本机网卡发送到服务器?

本文由澳门新葡亰平台官网发布于web前端,转载请注明出处:六种继承方式,大型单页面应用的进阶挑战

TAG标签:
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。