前端学习之路


  • 首页

  • 归档

  • 公益404

在浏览器地址栏键入url之后发生了什么

发表于 2017-03-16 |

本篇均以输入’www.baidu.com’为例

DNS解析

网络通讯大多数都是基于TCP/IP的,而TCP/IP又是基于IP的。我们输入的域名并不能被计算机网络所识别,因此需要将域名转为IP,而这个过程叫就做DNS解析。

基本概念

DNS(Domain Name System)

即计算机域名系统,它由域名解析器和域名服务器组成。

域名服务器:指保存有该网络中所有主机的域名和对应IP地址,并具有将域名转换为IP地址功能的服务器。
其中域名必须对应一个,且只能对应一个IP地址;而IP地址不一定有域名而且可以对应多个域名。

DNS也是应用层协议,但他是为了其他应用层协议工作的,用于将用户提供的域名解析为IP地址。

DNS主要基于UDP传输层协议,

根域

根域就是所谓的.。
其实我们的网址www.baidu.com在配置当中应该是www.baidu.com.(最后多了一个.)。一般我们在浏览器里输入的时候都会省略这个.。

域的划分

根域下来就叫顶级域或一级域。有两种划分方式,一种是互联网刚兴起时划分的com.,net.等;一种是按国家划分的如cn.,jp.等,每个域都有域名服务器。

例如 .com 就是一个顶级域名;而 www.baidu.com 不是一个域名,它是baidu.com这个域名里的一个叫做www的主机。同理,在a.www.baidu.com里,主机名是a。

解析过程

具体过程如下:

  1. 查询本地域名服务器(10.1.1.1),查找本地缓存中是否有www.baidu.com的IP地址。如果在缓存中找到,则跳到#6,没有找到则进行下一步;
  2. 在根域名服务器(.)中查询 -> 返回COM顶级域名服务器的IP
  3. 在COM顶级域名服务器(.com)中查询 -> 返回baidu.com二级域名服务器(.baidu.com)的IP
  4. 在baidu.com二级域名服务器(.baidu.com)中查询 -> 返回主机名为www的服务器的IP
  5. 将域名及对应IP存入缓存中
  6. 获得域名对应IP地址

TCP 连接

获得目标服务器的IP地址和端口号之后,客户端和服务器之间将建立一条TCP/IP连接。
TCP是面向连接的、无差错的、按序传输的。

三次握手

在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。

第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;

第二次握手:服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器 进入SYN_RECV状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED状态,完成三次握手。 完成三次握手,客户端与服务器开始传送数据.

TCP/IP三次握手图解

四次挥手

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这个原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

  1. 客户端A发送一个FIN,用来关闭客户A到服务器B的数据传送(报文段4)。
  2. 服务器B收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
  3. 服务器B关闭与客户端A的连接,发送一个FIN给客户端A(报文段6)。
  4. 客户端A发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

TCP/IP四次挥手图解

HTTP协议

TCP/IP连接建立之后,浏览器会向服务器发送一条HTTP请求报文 -> 服务器向浏览器返回一条相应报文 -> 关闭连接

报文

报文可分为两类: 请求报文和相应报文,它们都由三部分组成

  • 起始行(对报文进行描述)
  • 首部块(包含属性)
  • 主体部分(包含数据-可选)

起始行

  • 请求报文起始行
  • 相应报文起始行

请求报文起始行:
相应报文起始行:

首部块

  • 通用首部
  • 请求首部
  • 响应首部
  • 实体首部
  • 扩展首部

主体部分

是HTTP报文的负荷,也就是HTTP要传输的内容

连接方式

  • 并行连接
  • 持久连接

通常使用 少量并行+持久 的连接方式

HTTP/1.0 通过客户端设置请求首部 Connection: keep-alive

HTTP/1.1 默认持久连接,可以设置 Connection: close 关闭(默认持久连接并不代表连接会永远持续下去)

页面渲染

  • HTML代码转化成DOM
  • CSS代码转化成CSSOM(CSS Object Model)
  • 结合DOM和CSSOM,生成一棵渲染树(包含每个节点的视觉信息)
  • 生成布局(layout),即将所有渲染树的所有节点进行平面合成
  • 将布局绘制(paint)在屏幕上

重排

发生条件:

  • 页面初始化
  • 插入/删除节点
  • 改变元素尺寸(width/height/margin/padding/border 等等)
  • 浏览器窗口变化
  • 元素位置改变

重绘

发生条件:

  • 重排后必有重绘
  • color
  • visibility

参考:
前端经典面试题: 从输入 URL 到页面加载发生了什么?
DNS解析过程详解
聊聊HTTPS和SSL/TLS协议
TCP三次握手 四次挥手全过程
网页性能管理详解

跨域请求方法总结

发表于 2017-03-14 |

同源策略

含义

起初的含义是指,A网页设置的Cookie,B网页不能打开,除非这两个网页”同源”。所谓”同源”是指一下几个相同:

  • 协议相同(http,https,http2等)
  • 域名相同(www.example.com)
  • 端口相同(www.example.com:3000)

目的

同源策略的出现时为了保护用户信息的安全。如果用户登录了A网站,又去浏览其他网站。如果其他网站可以获取到A网站的Cookie,重要用户信息可能就会泄露。

通过XHR实现Ajax的一个主要限制,来自于跨域安全策略。这种安全策略可以防止某些恶意行为,但合理的实现跨域请求对某些应用来说也是非常必要的。

限制范围

目前,如果非同源,共有三种行为受到限制:

  • Cookie、LocalStorage 和 IndexedDB 无法获取
  • DOM 无法获得
  • AJAX 请求不能发送

规避方法

我们首先构建出一个跨域的环境方便测试:

1
2
3
4
5
6
7
8
9
10
11
12
├── client
│ ├── app.js
│ ├── package.json
│ └── public
│ └── src
│ ├── index.html
│ └── index.js
└── server
├── app.js
├── package.json
└── public
└── src
  • 通过express分别在3000和4000端口启动两个服务
  • 默认server为3000端口、client在4000端口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// /server/app.js
var express = require('express')
var path = require('path')
var app = express()
app.use('/static', express.static(path.join(__dirname, 'public')))
app.get('/', function (req, res) {
res.send('hello world');
})
app.listen(3000, function () {
console.log('server app listening on port 3000!')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// /client/app.js
var express = require('express')
var path = require('path')
var app = express()
app.use('/static', express.static(path.join(__dirname, 'public')))
app.get('/', function (req, res) {
res.send('client')
})
app.listen(4000, function () {
console.log('client app listening on port 4000!')
})
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--/client/public/src/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>

环境搭建成功后,在4000端口上的js通过Ajax向3000端口的服务发送请求,就会触发跨域安全策略。

下面就介绍几种CORS(Cross-Origin Resource Sharing)的方法:

原生支持

Firefox 3.5+、Safari 4+、Chrome等主流浏览器都通过XMLHttpRequest对象实现了CORS的原生支持。

1
2
3
4
5
6
7
8
9
10
11
12
// /client/public/src/index.js
var xhr = new XMLHttpRequest();
xhr.onload = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr === 304) {
console.log(xhr.responseText)
} else {
console.log('fail');
}
}
}

同时需要对接口进行设置。通过设置Access-Control-Allow-Origin头可以决定来自哪些域的请求可以被实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /server/app.js
var express = require('express')
var path = require('path')
var app = express()
app.use('/static', express.static(path.join(__dirname, 'public')))
app.get('/', function (req, res) {
+ res.header('Access-Control-Allow-Origin', 'http://localhost:4000');
res.send('hello world');
})
app.listen(3000, function () {
console.log('server app listening on port 3000!')
})

设置成功后重启服务,我们可以看到这时的跨域请求已经发送成功,在client的index.html页面中已经可以看到请求得到的内容

图像Ping

1
2
3
4
5
6
7
8
9
// /client/public/src/index.js
var img = new Image();
img.onload = function() {
console.log('done');
}
img.onerror = function() {
console.log('fail');
}
img.src = 'http://localhost:3000';

通过创建一个img标签,并且设置对应的src,可以向目标url发送请求,但是这种方法无法收到返回的数据,只能发送GET请求。由于无法得到返回的数据,我们稍微改一下接口的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// server/app.js
var express = require('express')
var path = require('path')
var app = express()
app.use('/static', express.static(path.join(__dirname, 'public')))
app.get('/', function (req, res) {
- res.send('hello world');
+ console.log('receive get request');
})
app.listen(3000, function () {
console.log('server app listening on port 3000!')
})

这样我们通过命令行的输出就能够判断http://localhost:3000是否收到了请求。

刷新index.html页面后,我们可以看到控制台输出了fail,即图片加载失败,但是命令行会输出receive get request,即已经收到了通过图像Ping发送的GET请求。

JSONP

顾名思义 JSONP = JSON + Padding

不理解没关系,看下面的例子就知道了

1
2
3
4
5
6
7
// /client/public/src/index.js
function handleResponse (response) {
console.log(response.test);
}
var script = document.createElement('script');
script.src = 'http://localhost:3000/?callback=handleResponse';
document.body.insertBefore(script, document.body.firstChild);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /server/app.js
var express = require('express')
var path = require('path')
var app = express()
app.use('/static', express.static(path.join(__dirname, 'public')))
app.get('/', function (req, res) {
- res.send('hello world');
+ res.send(req.query.callback + '(' + '{test: 1}' + ')');
})
app.listen(3000, function () {
console.log('server app listening on port 3000!')
})

不难看出,服务端接口此时返回一个字符串,这个字符串的格式为callback({test: 1}),即用一个函数包含了一个JSON,这就是JSONP名字的由来。客户端收到这个字符串后,会对其进行调用。注意这里的callback就是作为请求的参数传给接口的值,因此这个参数要和自己定义的回调函数同名。

刷新页面重新请求时,控制台打印出1。这是因为返回的handleResponse({test: 1})字符串执行后,相当于把JSON格式的数据作为参数,这样在函数内部就能获取到了。

如果实在不理解的话,可以直接访问这个链接,它会返回一个JSONP字符串。

部分选自这篇博客

转载请注明出处

理解HTML5移动端应用开发

发表于 2017-03-13 |

开发者们可以通过HTML5简化移动应用开发的过程。HTML5移动端应用也伴随着很多问题,其中就包括性能无法与原生应用所匹敌。

许多组织开始转向HTML5的移动应用开发,以至于使应用的开发过程流水化,并且能够降低花销和复杂度。

在正确的环境下,HTML5应用能够提供给企业一个简单的替换,从而不需要进行原生应用的开发,特别是当越来越多的HTML5开发框架出现的时候。任何组织在跨平台实现移动端应用的时候,都至少应该考虑HTML5。

然而,HTML5应用并不是毫无挑战的。他们带来了性能和安全上的关注,并且在数据获取以及原生应用程序的接口上引发了问题。

什么是HTML5应用?

HTML5应用提供了”写一次代码,就能在任何地方运行”的移动端应用发展。有一系列移动设备优化的web页面,他们为视频、音频数据的推流,对图形和动画的处理、以及离线的支持提供了高级功能。他们也添加了语义化的元素,表单的控制和多媒体控件,而且伴随了大量的新API,它们提供了定位服务、拖动操作、本地应用缓存等等。

HTML5很少单独工作。更多的HTML5应用集成了CSS和JavaScript,分别定义了HTML组件如何在浏览器中渲染和对文本、对象、数组进行操作的API。许多主流的浏览器都支持这些技术,这使得实现基于web跨设备的应用的实现成为可能。

HTML5移动端应用程序开发是如何工作的

HTML5移动端应用程序的开发框架工具包包括CSS库和JavaScript文件,开发者们可以将它们包含在HTML中。框架解决了由构建HTML5应用而引发的诸多问题。

例如,一个好的框架能够巧妙处理并重复应用文档对象模型(DOM)去提升性能。框架也使实现依赖于JavaScript的行为实现更加容易,例如屏幕转换和列表滑动。总的来说,这使得新手去创建一个HTML5应用变得更加容易了,因为许多开发的细节已经被处理过。

所有的框架都不相同,IT管理者必须在决定使用哪一个之前做足功课。每个框架都有自己的局限性和问题,他们经常会在应用变得更加复杂的时候被暴露出来。

在移动设备和浏览器之间的微小差别也影响了程序在不同环境中的表现。结果,开发者有时候会基于他们共有的特性进行构建,以此来确保跨平台编程,这些程序有可能会有损用户体验。尽管存在潜在的问题,有适当训练和经验的开发者在开发安全、基于web的高性能应用时应该没有问题。

在构建HTML5应用的时候应该时刻注意的问题

内存和性能限制是在HTML5移动应用开发时要关注的重点。开发者不仅要小心控制页面的流量,也要注意DOM的实现。对象模型越复杂,导航所需的时间越长。

综上,开发者应该时刻关注用户体验。为了确保用户体验的质量,HTML5移动端应用应该是轻量级的,以至于执行不会出现延迟。另外,管理者需要在多种平台上测试对应用进行测试,解决不同种类的设备、屏幕大小、浏览器和操作系统的问题。大多数应用需要支持离线操作以解决连接中断的问题。

HTML5应用 VS 原生应用

不像HTML5应用,原生的应用是基于应用运行平台上的原生语言。然而在原生应用中存在着挑战,每个平台都需要自己的应用,有一些平台需要一个应用的不同版本去支持不同平台上的不同设备。

HTML5是平台无关的。只要用户通过支持各种语言能力的浏览器获取到应用,应用在不同平台上也应该有相同的表现。另外,管理员只有一个代码库去维护和只要一个使用HTML5部署的包。他们能够分配bug修复并且在必要的时候可以随时更新,甚至与他们的用户进行性能在线测试,而不存在原生应用中的分配难题。

尽管HTML5已经走了很长一段路了,但是它仍然不能够完全实现所有的原生功能,例如多点触控手势;以及与设备内置组件的无缝结合,比如GPS。原生应用通常也会在性能上略胜一筹,因为他们是一部分机器指令。如果设计不当,HTML5应用可能会有一段很长的加载时间。

另外,HTML5应用受制于所有Web应用的风险。开发者必须对抗网络攻击,不正确的API使用以及通过Wi-Fi、蓝牙或者文本信息获取到设备的恶意代码的攻击。

将HTML5和原生应用相结合

一个混合的移动端应用是一个HTML5应用运行在原生程序的容器中。应用使用设备的浏览器去渲染本地的HTML页面,当连接到设备的时候,功能就像一个原生应用。混合应用能够获取到像日历和联系人这样的功能,同时也有单个代码库能够嵌入到多个平台的优势。事实上,混合应用基本上能够做到原生应用能够做到的任何事情,包括支持离线操作和多点触控手势。一个混合应用甚至能够在外观和感觉上都模仿原生应用。然而,混合应用仍然不能表现得像原生应用一样,因为存在额外的抽象层。

已经出现了像PhoneGap这样的平台去实现混合移动应用。这个框架使得在开发移动端应用的时候使用HTML、CSS、JavaScript成为可能,同时也处理好了跨平台发布混合应用的细节。

原文地址

你应该知道的4种JavaScript设计模式

发表于 2017-03-12 |

每位开发者都努力写出可维护的、易读的、可复用的代码。随着应用变得越来越大,代码的结构也越来越重要。设计模式验证了解决这个挑战的重点——在特定环境中,对同类事物提供相同的组织结构。

JavaScript web开发者们在开发应用的时候经常会不知不觉的与设计模式打交道。

尽管在某些环境中使用了不同的设计模式列表,JavaScript的开发者们通常比其他开发者更常用到一些设计模式。

在这篇文章里,我想通过讨论这些常见的设计模式来提升你的编程能力,并且深入到JavaScript的结构当中去。

要讨论的设计模式包括如下几种:

  • 模块模式(Module)
  • 原型模式(Prototype)
  • 观察者模式(Observer)
  • 单例模式(Singleton)

每种模式都由几个属性构成,然而,我将会着重强调如下几个关键点:

  1. Context: 应该在什么环境下使用模式
  2. Problem: 我们要通过模式去解决什么问题
  3. Solution: 如果用模式去解决我们提出的问题
  4. Implementation: 如何实现

模块设计模式(Module Design Pattern)

JavaScript 模块是最被经常使用的设计模式,它可以使特定部分的代码与其他部分相独立。其提供的松散耦合支撑了良好的代码结构。

对于那些熟悉面向对象语言的开发者额,模块就是JavaScript中的”类”。类的好处之一就是”封装”(encapsulation)——避免自身的状态和行为被其他”类”所获取。模块模式允许公有和私有(加上更少知道的受保护的和特权)访问级别。

模块应该是立即执行函数(IIFE)从而允许对私有作用域(即被闭包保护的变量和方法)的访问。看起来是这样的:

1
2
3
4
5
6
7
8
9
(function() {
// declare private variables and/or functions
return {
// declare public variables and/or functions
}
})();

这里,在返回我们想要返回的对象之前,实例化了私有变量和方法。闭包之外的代码不能获取这些私有变量,因为他们不在同一个作用域中。来看一个更具体的实现方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var HTMLChanger = (function() {
var contents = 'contents'
var changeHTML = function() {
var element = document.getElementById('attribute-to-change');
element.innerHTML = contents;
}
return {
callChangeHTML: function() {
changeHTML();
console.log(contents);
}
};
})();
HTMLChanger.callChangeHTML(); // Outputs: 'contents'
console.log(HTMLChanger.contents); // undefined

注意到callChangeHTML绑定到了返回的对象向上,并且能够在HTMLChanger命名空间内被引用。然而,在模块之外,内容就不能被引用了。

揭示模块模式(Revealing Module Pattern)

揭示模块模式是模块模式的一个变种。其目的是在保有封装的前提下,在返回的对象字面量中暴露特定的变量和方法。像下面这样去直接实现:

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
var Exposer = (function() {
var privateVariable = 10;
var privateMethod = function() {
console.log('Inside a private method!');
privateVariable++;
}
var methodToExpose = function() {
console.log('This is a method I want to expose!');
}
var otherMethodIWantToExpose = function() {
privateMethod();
}
return {
first: methodToExpose,
second: otherMethodIWantToExpose
};
})();
Exposer.first(); // Output: This is a method I want to expose!
Exposer.second(); // Output: Inside a private method!
Exposer.methodToExpose; // undefined

尽管这看起来更加清晰了,但是仍有一个显著的缺点,就是不能够引用私有的方法。这可能会给单元测试带来挑战。同样的,公共行为也是不可覆盖的。

原型设计模式(Prototype Design Pattern)

任何JavaScript开发者都看到了关键字prototype,他们被原型继承所困扰,或者在代码中实现原型。原型设计模式依赖于JavaScript原型继承。原型模型主要被用来在性能密集的条件下创建对象。

通过传递原始对象的克隆(浅克隆)来创建对象。原型模式的一个使用案例是执行一个广泛的数据库操作去创建一个对象,被用作应用的其他部分。如果另一个进程需要使用这个对象,它可以直接去克隆之前的对象而不需要再执行一遍整个数据库操作。

为了克隆一个对象,构造器必须去实例化第一个对象。然后通过使用关键字prototype,变量和方法被绑定到对象的结构中。让我们看一个基本的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var TeslaModelS = function() {
this.numWheels = 4;
this.manufacturer = 'Tesla';
this.make = 'Model S';
}
TeslaModelS.prototype.go = function() {
// Rotate wheels
}
TeslaModelS.prototype.stop = function() {
// Apply brake pads
}

构造函数允许创建一个TeslaModelS对象。在创建一个TeslaModelS对象的时候,它会在构造器中保持初始状态。另外,维护go和stop方法是很容易的,因为我们是在prototype中声明的他们。一个同样用来在原型上扩展函数的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var TeslaModelS = function() {
this.numWheels = 4;
this.manufacturer = 'Tesla';
this.make = 'Model S';
}
TeslaModelS.prototype = {
go: function() {
// Rotate wheels
},
stop: function() {
// Apply brake pads
}
}

揭示原型模式(Revealing prototype pattern)

与模块模式相似,原型模式也有揭示的变体。揭示原型模式在它返回一个对象字面量的时候,提空了公有和私有成员的封装。

在我们返回一个对象的时候,我们将会在原型对象前加一个function。通过扩展我们的上一个例子,我们能够选择我们在当前原型中希望暴露的内容,以保持其访问级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var TeslaModelS = function() {
this.numWheels = 4;
this.manufacturer = 'Tesla';
this.make = 'Model S';
}
TeslaModelS.prototype = function() {
var go = function() {
// Rotate wheels
};
var stop = function() {
// Apply brake pads
};
return {
pressBrakePedal: stop,
pressGasPedal: go
}
}();

留意stop和go函数将在返回的对象中被屏蔽,由于在返回的对象作用域之外。因为JavaScript原生就支持对象的继承,因此不需要再重写底层细节了。

观察者设计模式

有很多次当我们的应用的一部分改变的时候,另一部分需要被更新。在AngularJS里面,如果$scope对象更新了,一个事件就能被触发,同时去提醒另一个部分。观察者模式就是对这种情况的合并——如果一个对象被更新,它会对依赖他的对象进行广播,告知它有变化发生了。

另一个很好的例子就是MVC架构;当数据模型更新时,视图发生改变。其中一个好处就是从数据模型中将视图解耦出来,从而减少了依赖。

Observer Design Pattern

正如UML图表所示,subject,observer和concrete对象都是必要的对象。subject包含了到concrete observer的引用,从而能够通知任何改变。observer对象是一个抽象的类,它允许实际的观察者实现通知方法。

让我们看一个AngularJS的例子,其通过事件管理实现了观察者模式

1
2
3
4
5
6
7
8
9
10
11
// Controller 1
$scope.$on('nameChanged', function(event, args) {
$scope.name = args.name;
});
...
// Controller 2
$scope.userNameChanged = function(name) {
$scope.$emit('nameChanged', {name: name});
};

在观察者模式中,区分独立的对象或者subject是很重要的。

值得一提的是,尽管观察者模式有很多优势,但是其中一个缺点就是当观察者的数量上升时,性能会有显著的下降。其中最恼人的观察者就是watchers。在AngularJS里,我们能够监视变量,函数和对象。$$digest循环运行并且在作用域对象更新时通知每一个监视器新的值。

我们能够在JavaScript中创建我们自己的Subject和Observer。让我们看一下这是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var Subject = function() {
this.observers = [];
return {
subscribeObserver: function(observer) {
this.observers.push(observer);
},
unsubscribeObserver: function(observer) {
var index = this.observers.indexOf(observer);
if(index > -1) {
this.observers.splice(index, 1);
}
},
notifyObserver: function(observer) {
var index = this.observers.indexOf(observer);
if(index > -1) {
this.observers[index].notify(index);
}
},
notifyAllObservers: function() {
for(var i = 0; i < this.observers.length; i++){
this.observers[i].notify(i);
};
}
};
};
var Observer = function() {
return {
notify: function(index) {
console.log("Observer " + index + " is notified!");
}
}
}
var subject = new Subject();
var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();
subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.subscribeObserver(observer3);
subject.subscribeObserver(observer4);
subject.notifyObserver(observer2); // Observer 2 is notified!
subject.notifyAllObservers();
// Observer 1 is notified!
// Observer 2 is notified!
// Observer 3 is notified!
// Observer 4 is notified!

发布/订阅

然而,发布/订阅模式使用了一种主题/事件的渠道,这个渠道架设在希望接收到通知的对象(订阅者)和触发事件的对象(发布者)之间。这个事件系统允许代码定义特定的应用事件,并且能够传递包含着订阅者所需的值的定制化参数。其中的想法就是避免订阅者和发布者之间的依赖。

这与观察者模式有所区别,因为任何一个订阅者都实现了一个适当的事件处理机制去注册和接收来自发布者的主题通知广播。

许多开发者选择将发布/订阅模式与观察者模式相结合,尽管两者存在差异。订阅器在发布/订阅模式中通过消息媒介被通知,但是观察者通过实现一个与subject类似的处理程序而获得通知。

在AngularJS里,一个订阅者订阅一个事件需要使用$on('event', callback),发布者发布一个事件需要使用$emit('event',args)或者$broadcast('event', args)。

单例模式(Singleton)

单例模式只允许单次实例化,但是同一个对象可以有许多实例。单例模式阻止了客户端创建多个实例,在第一个对象被创建之后,他将会返回它自己的实例。

对于大多数之前还没有使用过单例模式的人来说,找到单例模式的用例是不容易的。一个例子是使用一个办公室的打印机。如果有十个人在办公室里,并且他们都要用一台打印机,十台电脑共享了打印机(实例)。通过共享一个打印机,他们分享了相同的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var printer = (function () {
var printerInstance;
function create () {
function print() {
// underlying printer mechanics
}
function turnOn() {
// warm up
// check for paper
}
return {
// public + private states and behaviors
print: print,
turnOn: turnOn
};
}
return {
getInstance: function() {
if(!printerInstance) {
printerInstance = create();
}
return printerInstance;
}
};
function Singleton () {
if(!printerInstance) {
printerInstance = intialize();
}
};
})();

create方法是私有的,因为我们不想让客户端去访问他,然而,getInstance方法是公开的。每一个办公室的员工都能通过与getInstance方法进行交互从而生成一个打印机的实例。向下面这样:

1
var officePrinter = printer.getInstance();

在AngularJS里面,单例是很普遍的,最值得注意的是services,factories和providers。因为他们都保留了状态并且提供了资源的获取,创建两个实例破坏了service/factory/provider共享的关键。

当超过一个线程想要获取相同资源的时候,竞争这种情况就会发生在多线程程序里。单例模式很容易受到竞争的影响,因此如果没有实例在一开始被初始化,两个线程可能就会创建两个对象而不是返回实例。这违背了单例模式的设计原则。因此,当开发者在多线程应用中实现单例时,必须要保证同步。

总结

设计模式通常在更大的应用中被使用,然而要去理解哪一个比另一个更有优势,需要从实践中得到。

在构建任何应用之前,你应该全盘思考每一个行为以及他们如何与另一个进行交互。在回顾Module,Protoytpe,Observer和Singleton设计模式之后,你应该能够认识这些模式并且广泛应用他们。

原文地址

克服编程中的直观

发表于 2017-03-08 |

在一系列的实验中,研究者们开始发现在「困难」(或「不流利」)和「认知」之间的关系。他们为两组人员提供了相同的测试,一组以易读的(直观的)格式,而另一组以困难的(不流利)格式。在所有他们实施的实验中,格式不流利的组大体上得分更高。这背后的理论就是人类会默认依赖自动的、不费力的、简单的系统去推理。但是如果事情是背道而驰的,或者很难去理解的,我们就会切换到一种更深层的、仔细的、有分析性的模式去思考。

我一直在想这是如何影响编程的。编程是一个智力上的挑战任务,但幸运的是,我们发明了工具去使它变得可管理了。我发现在一定程度上,一个直观的的、简单属性的语言、框架、或者类库可能会开始产生一些负面效果。从个人经历和指导新人的过程中,我发现当我们使用一些允许我们去顺着思维的方向推理的工具时,任何我们遇到困难的时候,我们都会感觉到自己做错了。尽管我们已经有了解决困难的必需技能,我们通常会开始质疑、重查我们的工作。我们会提出问题,去寻找相关框架的最佳实践,而不是用我们自己的方法去解决问题。有关这点的最经典的问题就是Stack overflow上的问题:『如何用jQuery去做X』,或者回答『用jQuery的某个插件去做X』,X可能是从基础算法到websockets的任何事情。

框架的未知领域

当我们使用一个框架的时候,特定的一类问题将会很好解决。如果我们待在框架创造出来的空间里,程序设计会变得很直观。我们可以这称作框架的直观空间。另一方面,我们把框架涉及不到的空间成为框架的未知领域(nagative space)。未知领域并不一定是框架的缺点,它只是不在框架所考虑要解决的范围之内。然而,它已经将程序员置于直觉空间中很久了。如果程序员发现自己在未知领域,就会感觉到无从下手。

当初学程序员发现他们自己在未知领域的时候,他们经常会向类库的作者求助,并让他们再次回到直观空间里。这就是一些流行框架会产生一个插件和扩展的生态系统来扩展他的直观空间的原因。如果这使得程序员更加有生产力,那么这看起来并不是本质上的错误。然而,它可能会无意间产生消极的影响:

  1. 增加了程序员对生态系统的类库作者的依赖性
  2. 受到技术冲击的时候将架构决策完全归结于类库
  3. 传播了错误的思想:程序应该永远是直观的

开发者和类库作者相互依赖

我应该首先说明,这是一个技术上错误的分歧。所有程序员在任何程序设计的讨论中都扮演着两种角色。你可能会开发产品的业务逻辑然后转而去构建一个通用的抽象库来帮助你在你的代码库里进行复用。然而,我注意到在开源项目里,人们的行为倾向于使分歧看起来真实

我发现在开源项目里能够成功的最简单的方式就是去将框架的未知领域变为直观空间。换句话说,就是写插件和扩展。当框架变得更加流行,大量的开发者(通常是初学者)将会开始抱怨在这个框架里实现X有多么困难(正如我们看到的,X可能完全不相关)。现在,在这样一个商业化的世界里,开源是极具竞争性的。一旦一个开源项目结局了许多人的直观上的问题,许多人就会转变立场。这成为了错误理念的传播器:一个程序员应该花所有的时间在直观空间编程

结论

我认为改正这个问题的根本要从教育中落实。当某人开始学习编程的初期,我们的文化就趋向于强调对工具的依赖。我收到了许多有志气的程序员的提问,关于什么是最好的学习工具或语言。这基本上总是一个不成熟的问题。我过去常常给出这样的回答:”取决于你要构建什么”或者”选择一个对初学者友好的社区”或者”投资一个成长中的语言”。我认为所有这些都是好的答案,但是这在程序员的早期学习过程中并不重要。当你学习编程的本质的时候,都是一样的。此外,这些答案使得依赖工具的文化成为可能。

代码复用,类库、分享和开源对于软件工程师来说都非常重要,但是我们应该谨慎,不能让人们相信”编程就应该像把东西粘起来一样简单”的思想。事实上,这几天当我感觉到事情有点太简单的时候,我都会对此非常怀疑。如果编程这样简单的话,它早就已经被自动化了。

原文地址

转载请注明出处

123
Tianhao Cui

Tianhao Cui

私、気になります!

15 日志
© 2017 Tianhao Cui
由 Hexo 强力驱动
主题 - NexT.Pisces