# 概念详解
# 1 BFC, IFC, GFC 和 FFC
BFC(Block formatting contexts):块级格式上下文 页面上的一个隔离的渲染区域,那么他是如何产生的呢?可以触发BFC的元素有 float
、position
、overflow
、display:table-cell/ inline-block/table-caption
;BFC有什么作用呢?比如说实现多栏布局’
IFC(Inline formatting contexts):内联格式上下文 IFC 的 line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding
/ margin
影响)IFC 中的 line box 一般左右都贴紧整个 IFC,但是会因为 float
元素而扰乱。float
元素会位于 IFC 与 line box 之间,使得 line box 宽度缩短。 同一个 IFC 下的多个 line box 高度会不同 IFC 中时不可能有块级元素的,当插入块级元素时(如 p
中插入 div
)会产生两个匿名块与 div
分隔开,即产生两个 IFC,每个 IFC 对外表现为块级元素,与div
垂直排列。 那么 IFC 一般有什么用呢? 水平居中:当一个块要在环境中水平居中时,设置其为 inline-block
则会在外层产生 IFC,通过 text-align
则可以使其水平居中。 垂直居中:创建一个 IFC,用其中一个元素撑开父元素的高度,然后设置其 vertical-align:middle
,其他行内元素则可以在此父元素下垂直居中。
GFC(GrideLayout formatting contexts):网格布局格式化上下文 当为一个元素设置 display
值为 grid
的时候,此元素将会获得一个独立的渲染区域,我们可以通过在网格容器(grid container)上定义网格定义行(grid definition rows)和网格定义列(grid definition columns)属性各在网格项目(grid item)上定义网格行(grid row)和网格列(grid columns)为每一个网格项目(grid item)定义位置和空间。那么 GFC 有什么用呢,和 table
又有什么区别呢?首先同样是一个二维的表格,但 GridLayout 会有更加丰富的属性来控制行列,控制对齐以及更为精细的渲染语义和控制。
FFC(Flex formatting contexts):自适应格式上下文 display
值为 flex
或者 inline-flex
的元素将会生成自适应容器(flex container),可惜这个牛逼的属性只有谷歌和火狐支持。Flex Box 由伸缩容器和伸缩项目组成。通过设置元素的 display
属性为 flex
或 inline-flex
可以得到一个伸缩容器。设置为 flex
的容器被渲染为一个块级元素,而设置为 inline-flex
的容器则渲染为一个行内元素。伸缩容器中的每一个子元素都是一个伸缩项目。伸缩项目可以是任意数量的。伸缩容器外和伸缩项目内的一切元素都不受影响。简单地说,Flexbox
定义了伸缩容器内伸缩项目该如何布局。
# 2 作用域,作用域链和闭包
作用域
指的是一个变量和函数的作用范围,JS中函数内声明的所有变量在函数体内始终是可见的,在ES6前有全局作用域和局部作用域,但是没有块级作用域(catch只在其内部生效),局部变量的优先级高于全局变量。
作用域链
每个函数都有自己的执行上下文环境,当代码在这个环境中执行时,会创建变量对象的作用域链,作用域链是一个对象列表或对象链,它保证了变量对象的有序访问。
作用域链的开始是当前代码执行环境的变量对象,常被称之为“活跃对象”(AO),变量的查找会从第一个链的对象开始,如果对象中包含变量属性,那么就停止查找,如果没有就会继续向上级作用域链查找,直到找到全局对象中
闭包
红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。
MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。
其中自由变量,指在函数中使用的,但既不是函数参数 arguments
也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。
function getOuter(){
var date = '1127';
function getDate(str){
console.log(str + date); //访问外部的date
}
return getDate('今天是:'); //"今天是:1127"
}
getOuter();
其中 date
既不是参数 arguments
,也不是局部变量,所以 date
是自由变量。
总结起来就是下面两点:
- 1、是一个函数(比如,内部函数从父函数中返回)
- 2、能访问上级函数作用域中的变量(哪怕上级函数上下文已经销毁)
经典闭包题解
由于作用域链机制的影响,闭包只能取得内部函数的最后一个值,这引起的一个副作用就是如果内部函数在一个循环中,那么变量的值始终为最后一个值。
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
如果要强制返回预期的结果1,2,3怎么办?
方法1:立即执行函数
for (var i = 0; i < 3; i++) {
(function(num) {
setTimeout(function() {
console.log(num);
}, 1000);
})(i);
}
// 0
// 1
// 2
方法2:返回一个匿名函数赋值
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (num) {
return function(){
console.log(num);
}
})(i);
}
data[0](); // 0
data[1](); // 1
data[2](); // 2
无论是立即执行函数还是返回一个匿名函数赋值,原理上都是因为变量的按值传递,所以会将变量i的值复制给实参 num
,在匿名函数的内部又创建了一个用于访问 num
的匿名函数,这样每个函数都有了一个 num
的副本,互不影响了。
方法3:使用ES6中的let
var data = [];
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
# 3 原型和原型链
原型
每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype
属性上,而非对象实例本身。
原型链
每个对象拥有一个原型对象,通过 __proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null
。这种关系被称为原型链 (prototype chain),通过原型链一个对象会拥有定义在其他对象中的属性和方法。
总结
Symbol
作为构造函数来说并不完整,因为不支持语法new Symbol()
,但其原型上拥有constructor
属性,即Symbol.prototype.constructor
。- 引用类型
constructor
属性值是可以修改的,但是对于基本类型来说是只读的,当然null
和undefined
没有constructor
属性。 __proto__
是每个实例上都有的属性,prototype
是构造函数的属性,在实例上并不存在,所以这两个并不一样,但p.__proto__
和Parent.prototype
指向同一个对象。__proto__
属性在ES6
时被标准化,但因为性能问题并不推荐使用,推荐使用Object.getPrototypeOf()
。- 每个对象拥有一个原型对象,通过
__proto__
指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向null
,这就是原型链。
# 4 深浅拷贝
浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- Object.assign()
Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
let a = {
name: "timfan",
info: {
address: "江苏无锡",
age: 29
}
}
let b = Object.assign({}, a);
console.log(b);
// {
// name: "timfan",
// info: { address: "江苏无锡", age: 29 }
// }
a.name = "spacexcode";
a.info.age = 30;
console.log(a);
// {
// name: "spacexcode",
// info: { address: "江苏无锡", age: 30 }
// }
console.log(b);
// {
// name: "timfan",
// info: { address: "江苏无锡", age: 30 }
// }
上面代码改变对象 a 之后,对象 b 的基本属性保持不变。但是当改变对象 a 中的对象 info
时,对象 b 相应的位置也发生了变化。
- Array.prototype.slice()
slice()
方法返回一个新的数组对象,这一对象是一个由 begin
和 end
(不包括 end
)决定的原数组的浅拷贝。原始数组不会被改变。
let a = [0, "1", [2, 3]];
let b = a.slice(1);
console.log(b);
// ["1", [2, 3]]
a[1] = "99";
a[2][0] = 4;
console.log(a);
// [0, "99", [4, 3]]
console.log(b);
// ["1", [4, 3]]
可以看出,改变 a[1]
之后 b[0]
的值并没有发生变化,但改变 a[2][0]
之后,相应的 b[1][0]
的值也发生变化。说明 slice()
方法是浅拷贝,相应的还有 concat
等,在工作中面对复杂数组结构要额外注意。
深拷贝
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
- JSON.parse(JSON.stringify(object))
let a = {
name: "timfan",
info: {
address: "江苏无锡",
age: 29
}
}
let b = JSON.parse(JSON.stringify(a));
console.log(b);
// {
// name: "timfan",
// info: { address: "江苏无锡", age: 29 }
// }
a.name = "spacexcode";
a.info.age = 30;
console.log(a);
// {
// name: "spacexcode",
// info: { address: "江苏无锡", age: 30 }
// }
console.log(b);
// {
// name: "timfan",
// info: { address: "江苏无锡", age: 29 }
// }
但是该方法有以下几个问题
- 1、会忽略 undefined
- 2、会忽略 symbol
- 3、不能序列化函数
- 4、不能解决循环引用的对象
- 5、不能正确处理new Date()
- 6、不能处理正则
# 5 优雅降级和渐进增强
优雅降级:
Web站点在所有新式浏览器中都能正常工作,如果用户使用的是老式浏览器,则代码会检查以确认它们是否能正常工作。由于IE独特的盒模型布局问题,针对不同版本的IE的hack实践过优雅降级了,为那些无法支持功能的浏览器增加候选方案,使之在旧式浏览器上以某种形式降级体验却不至于完全失效.
渐进增强:
从被所有浏览器支持的基本功能开始,逐步地添加那些只有新式浏览器才支持的功能,向页面增加无害于基础浏览器的额外样式和功能的。当浏览器支持时,它们会自动地呈现出来并发挥作用。
# 6 浏览器进程和线程
进程
学术上说,进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体。我们这里将进程比喻为工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
线程
在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元。这里把线程比喻一个车间的工人,即一个车间可以允许由多个工人协同完成一个任务。
两者关系
- 进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
- 进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。
- 调度和切换:线程上下文切换比进程上下文切换要快得多。
浏览器是多进程的
浏览器是多进程的,浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存),简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。
- Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU进程:最多一个,用于3D绘制等
- 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为
- 页面渲染,脚本执行,事件处理等
浏览器内核(渲染进程)
请牢记,浏览器的渲染进程是多线程的
- GUI 渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
- JS 引擎线程
- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
- 事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
- 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
- 定时触发器线程
- 传说中的setInterval与setTimeout所在线程
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
- 异步 http 请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
# 7 浏览器缓存
对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。
# 缓存位置
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
- 网络请求
Service Worker
Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。
Memory Cache
Memory Cache 也就是内存中的缓存,读取内存中的数据肯定比磁盘快。**但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。**一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
先说结论,这是不可能的。首先计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。内存中其实可以存储大部分的文件,比如说 JSS、HTML、CSS、图片等等。但是浏览器会把哪些文件丢进内存这个过程就很玄学了,我查阅了很多资料都没有一个定论。
当然,我通过一些实践和猜测也得出了一些结论:
- 对于大文件来说,大概率是不存储在内存中的,反之优先。
- 当前系统内存使用率高的话,文件优先存储进硬盘。
Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
Push Cache
Push Cache 是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。并且缓存时间也很短暂,只在会话(Session)中存在,一旦会话结束就被释放。
- 所有的资源都能被推送,但是 Edge 和 Safari 浏览器兼容性不怎么好。
- 可以推送 no-cache 和 no-store 的资源。
- 一旦连接被关闭,Push Cache 就被释放。
- 多个页面可以使用相同的 HTTP/2 连接,也就是说能使用同样的缓存。
- Push Cache 中的缓存只能被使用一次。
- 浏览器可以拒绝接受已经存在的资源推送。
- 你可以给其他域名推送资源。
网络请求
如果所有缓存都没有命中的话,那么只能发起请求来获取资源了。
那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,接下来我们就来学习缓存策略这部分的内容。
# 缓存策略
通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
强缓存
强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。强缓存表示在缓存期间不需要请求,state code 为 200。
Expires
Expires: Wed, 22 Oct 2018 08:41:00 GMT
Expires 是 HTTP/1 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
Cache-control
Cache-control: max-age=30
Cache-Control 出现于 HTTP/1.1,优先级高于 Expires。该属性值表示资源会在 30 秒后过期,需要再次请求。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令
从图中我们可以看到,我们可以将多个指令配合起来一起使用,达到多个目的。比如说我们希望资源能被缓存下来,并且是客户端和代理服务器都能缓存,还能设置缓存失效时间等等。
接下来我们就来学习一些常见指令的作用
指令 | 作用 |
---|---|
public | 表示响应可以被客户端和代理服务器缓存 |
private | 表示响应只可以被客户端缓存 |
max-age=30 | 缓存 30 秒后就过期,需要重新请求 |
s-maxage=30 | 覆盖 max-age,作用一样,只在代理服务器中生效 |
no-store | 不缓存任何响应 |
no-cache | 资源被缓存,但是立即失效,下次会发起请求验证资源是否过期 |
max-stale=30 | 30秒内,即使缓存过期,也使用该缓存 |
min-fresh=30 | 希望在30秒内获取最新的响应 |
协商缓存
如果缓存过期了,就需要发起请求验证资源是否有更新。协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag。
当浏览器发起请求验证资源时,如果资源没有做改变,那么服务端就会返回 304 状态码,并且更新浏览器缓存有效期。
Last-Modified 和 If-Modified-Since
Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来,否则返回 304 状态码。
但是 Last-Modified 存在一些弊端:
- 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源。
- 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源。
因为以上这些弊端,所以在 HTTP / 1.1 出现了 ETag。
ETag 和 If-None-Match
ETag 类似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag 优先级比 Last-Modified 高。
以上就是缓存策略的所有内容了,看到这里,不知道你是否存在这样一个疑问。如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
# 应用场景
频繁变动的资源
对于频繁变动的资源,首先需要使用 Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。
代码文件
这里特指除了 HTML 外的代码文件,因为 HTML 文件一般不缓存或者缓存时间很短。
一般来说,现在都会使用工具来打包代码,那么我们就可以对文件名进行哈希处理,只有当代码修改后才会生成新的文件名。基于此,我们就可以给代码文件设置缓存有效期一年 Cache-Control: max-age=31536000,这样只有当 HTML 文件中引入的文件名发生了改变才会去下载最新的代码文件,否则就一直使用缓存。
# 8 设计模式
设计模式的五大基本原则(SOLID)。SOLID 所指的五大基本原则分别是:单一功能原则、开放封闭原则、里氏替换原则、接口隔离原则和依赖反转原则。SOLID 可以简单概括为六个字:即「高内聚、低耦合」:
- 高 层模块不依赖底层模块,即为依赖反转原则;
- 内 部修改关闭,外部扩展开放,即为开放封闭原则;
- 聚 合单一功能,即为单一功能原则;
- 低 知识要求,对外接口简单,即为迪米特法则;
- 耦 合多个借口,不如独立拆分,即为接口隔离原则;
- 合 成复用,子类继承可替换父类,即为里氏替换原则。
23 种设计模式分为「创建型」、「行为型」和「结构型」,具体类型如下图:
设计模式说白了就是「封装变化」,比如「创建型」封装了创建对象的变化过程,「结构型」将对象之间组合的变化封装,「行为型」则是抽离对象的变化行为。
下面分析几种常用的设计模式
# 工厂模式
工厂模式分为好几种,这里就不一一讲解了,以下是一个简单工厂模式的例子
class Man {
constructor(name) {
this.name = name
}
alertName() {
alert(this.name)
}
}
class Factory {
static create(name) {
return new Man(name)
}
}
Factory.create('yck').alertName()
当然工厂模式并不仅仅是用来 new
出实例。
可以想象一个场景。假设有一份很复杂的代码需要用户去调用,但是用户并不关心这些复杂的代码,只需要你提供给我一个接口去调用,用户只负责传递需要的参数,至于这些参数怎么使用,内部有什么逻辑是不关心的,只需要你最后返回我一个实例。这个构造过程就是工厂。
工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个接口,简单清晰。
在 Vue
源码中,你也可以看到工厂模式的使用,比如创建异步组件
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// 逻辑处理...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
在上述代码中,我们可以看到我们只需要调用 createComponent
传入参数就能创建一个组件实例,但是创建这个实例是很复杂的一个过程,工厂帮助我们隐藏了这个复杂的过程,只需要一句代码调用就能实现功能。
# 单例模式
单例模式很常用,比如全局缓存、全局状态管理等等这些只需要一个对象,就可以使用单例模式。
单例模式的核心就是保证全局只有一个对象可以访问。因为 JS
是门无类的语言,所以别的语言实现单例的方式并不能套入 JS
中,我们只需要用一个变量确保实例只创建一次就行,以下是如何实现单例模式的例子
class Singleton {
constructor() {}
}
Singleton.getInstance = (function() {
let instance
return function() {
if (!instance) {
instance = new Singleton()
}
return instance
}
})()
let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true
在 Vuex
源码中,你也可以看到单例模式的使用,虽然它的实现方式不大一样,通过一个外部变量来控制只安装一次 Vuex
let Vue // bind on install
export function install (_Vue) {
if (Vue && _Vue === Vue) {
// 如果发现 Vue 有值,就不重新创建实例了
return
}
Vue = _Vue
applyMixin(Vue)
}
# 适配器模式
适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。
以下是如何实现适配器模式的例子
class Plug {
getName() {
return '港版插头'
}
}
class Target {
constructor() {
this.plug = new Plug()
}
getName() {
return this.plug.getName() + ' 适配器转二脚插头'
}
}
let target = new Target()
target.getName() // 港版插头 适配器转二脚插头
在 Vue
中,我们其实经常使用到适配器模式。比如父组件传递给子组件一个时间戳属性,组件内部需要将时间戳转为正常的日期显示,一般会使用 computed
来做转换这件事情,这个过程就使用到了适配器模式。
# 装饰模式
装饰模式不需要改变已有的接口,作用是给对象添加功能。就像我们经常需要给手机戴个保护套防摔一样,不改变手机自身,给手机添加了保护套提供防摔功能。
以下是如何实现装饰模式的例子,使用了 ES7
中的装饰器语法
function readonly(target, key, descriptor) {
descriptor.writable = false
return descriptor
}
class Test {
@readonly
name = 'yck'
}
let t = new Test()
t.yck = '111' // 不可修改
在 React
中,装饰模式其实随处可见
import { connect } from 'react-redux'
class MyComponent extends React.Component {
// ...
}
export default connect(mapStateToProps)(MyComponent)
# 代理模式
代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有很多代理的场景。比如你需要买一件国外的产品,这时候你可以通过代购来购买产品。
在实际代码中其实代理的场景很多,也就不举框架中的例子了,比如事件代理就用到了代理模式。
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
因为存在太多的 li
,不可能每个都去绑定事件。这时候可以通过给父节点绑定一个事件,让父节点作为代理去拿到真实点击的节点。
# 发布订阅模式
发布-订阅模式也叫做观察者模式。通过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。在现实生活中,也有很多类似场景,比如我需要在购物网站上购买一个产品,但是发现该产品目前处于缺货状态,这时候我可以点击有货通知的按钮,让网站在产品有货的时候通过短信通知我。
在实际代码中其实发布-订阅模式也很常见,比如我们点击一个按钮触发了点击事件就是使用了该模式
<ul id="ul"></ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
在 Vue
中,如何实现响应式也是使用了该模式。对于需要实现响应式的对象来说,在 get
的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。
# 外观模式
外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。
举个例子来说,我们现在需要实现一个兼容多种浏览器的添加事件方法
function addEvent(elm, evType, fn, useCapture) {
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture)
return true
} else if (elm.attachEvent) {
var r = elm.attachEvent("on" + evType, fn)
return r
} else {
elm["on" + evType] = fn
}
}
对于不同的浏览器,添加事件的方式可能会存在兼容问题。如果每次都需要去这样写一遍的话肯定是不能接受的,所以我们将这些判断逻辑统一封装在一个接口中,外部需要添加事件只需要调用 addEvent
即可。
# 9 直出和同构
直出,常常指代后端渲染,即我们请求的页面已经是把模版和数据组合好,直接吐出来给到浏览器。
与此相对的,是前端渲染。例如单页应用,我们拿到的只是一个简单的空 <html>
,然后浏览器解析发现需要一些请求的数据和资源,发起二次请求。 当然,这样一来一去,消耗和等待的时间便会长了。
同构又是什么呢?
直出渲染的后端可以有很多,像 PHP
、JSP
、Node.js
等都是可以的。而同构的意思是,前后端使用一套代码。所以简单来说,就是 Node.js
的胜出了,同构最明显的优势,则是方便维护。
# 10 单页面与多页应用
什么是单页应用?
其实很简单,单页应用与多页最简单的区别就是,单页应用,是一个 HTML
文件。
当我们需要更改页面的展示,我们会移除掉部分元素,然后将需要新增的内容填充进去,与画画的擦除重绘相似。
单页应用的好处是:
- 页面的数据状态都能维持着。
- 部分擦除重绘,比整个页面刷新的效果体验要好很多。
当然,单页应用也会有缺点:
- 不利于 SEO。
- 请求等待时间长。
事物都是有利有弊,单页应用的最大痛处在于 SEO
。 搜索引擎优化(英语:search engine optimization,缩写为 SEO),是一种通过了解搜索引擎的运作规则来调整网站,以及提高目的网站在有关搜索引擎内排名的方式。
而关于单页应用的 SEO
,也是有各种各样的方式优化的,大家也可以去了解。
什么是多页应用?
多页应用,更常见于相同的业务不同的页面的开发,这些页面或许没有很多的联系或者公用的数据,每个页面维护各自的状态。
如今 H5
的业务多了,直出渲染也多了些。而多页应用和直出也是不错的搭配。
单页与多页应用的比较
- | 单页面应用 | 多页面应用 |
---|---|---|
组成 | 一个外壳页面和多个页面片段组成 | 多个完整页面构成 |
资源共用(css,js) | 共用,只需在外壳部分加载 | 不共用,每个页面都需要加载 |
刷新方式 | 页面局部刷新或更改 | 整页刷新 |
url 模式 | a.com/#/pageone a.com/#/pagetwo | a.com/pageone.html a.com/pagetwo.html |
用户体验 | 页面片段间的切换快,用户体验良好 | 页面切换加载缓慢,流畅度不够,用户体验比较差 |
页面跳转动画 | 容易实现 | 无法实现 |
数据传递 | 容易 | 依赖 url 传参、或者 cookie 、localStorage 等 |
搜索引擎优化(SEO) | 需要单独方案、实现较为困难、不利于 SEO 检索 可利用服务器端渲染(SSR)优化 | 实现方法简易 |