js外部脚本异步加载方式

2020
20/03

如何异步加载js脚本

1.动态插入标签的方式

通过操作dom,可以在任意位置创建js脚本,这种方式优点是无论在何时启动下载,文件的下载和执行过程不会阻塞页面其他进程(包括脚本加载)。

var script=document.createElement('script'); 
script.type='text/javaScript'; 
script.src='file1.js'; 
document.getElementsByTagName('head')[0].appendChild(script); 

但缺陷是:这种方法加载的脚本会在下载完成后立即执行,那么意味着多个脚本之间的运行顺序是无法保证的。当某个脚本对另一个脚本有依赖关系时,就很可能发生错误了。我们可以增加一个回调函数,该函数会在相应脚本文件加载完成后被调用。这样便可以实现顺序加载了。

loadScript('file1.js',function(){ loadScript('file2.js',function(){}); }); 
loadScript('file3.js',function(){}); 

function loadScript(url, callback) {
	var script = document.createElement('script');
	script.type = 'text / javaScript';
	if (script.readyState) { //IE
		script.onreadystatechange = function() {
			if (script.readyState == 'loaded' || script.readyState == 'complete') {
				script.onreadystatechange = null;
				callback();
			}
		};
	} else { //其他浏览器
		script.οnlοad = function() {
			callback();
		};
	}
	script.src = url;
	document.getElementsByTagName('head')[0].appendChild(script);
}

2.使用模块化加载

上述动态插入方式虽然能够实现异步加载,但若要实现顺序执行,必须依赖js的阻塞加载来实现顺序回调。而我们真正想实现的是——脚本异步下载并按相应顺序执行,即并行加载并顺序执行。

我们可以使用requireJS插件来实现模块化加载(AMD规范)。但需要引入requireJS库。如下所示伪代码:

<script src="require.js"></script>
<script type="text/javaScript">
    require([
      "script1.js",
      "script2-a.js",
      "script2-b.js",
      "script3.js"
     ],
     function(){
      initScript1();
      initScript2();
      initScript3();
     }
    );
</script>

AMD的缺点是开始就把所有依赖写出来,这是不符合书写的逻辑顺序的。

3.使用js脚本异步加载方式async,defer

页面的生命周期主要有三个

1.DOMContentLoaded

这个是dom树构建完成之后,并没有进行加载资源(styles, imgs)。处理cssom之前的dom树完成的情况。

DOMContentLoaded and scripts

我们知道js加载会阻塞dom树的构建,因此可知道dom树构建完成的时候,js是已经解析完成了(正常情况)

非正常的情况,就是js脚本的加载方式(async, defer)

⚠️asyncdefer这两个属性仅仅适用于外部属性,即脚本指定了src属性,否则会被忽略。

在没有定义defer和async之前,异步加载的方式是动态创建script,通过window.onload方法确保页面加载完毕再将script标签插入到DOM中。

使用这两个属性,浏览器就知道会去继续解析dom,并且在后台加载执行这些js。

<script>标签的这两个属性(async, defer)还是有些不同的。

defer="defer"async="true/false"

async:HTML5新的异步、并行模式,脚本将在完成下载后等待合适的时机执行代码。

加载顺序: 谁先加载完谁就先执行。

DOMContentLoaded: 在dom未完全解析之前,在这个阶段就可以下载执行异步的脚本。如果脚本很小或缓存,并且文档足够长,就会发生这种情况

defer:告诉浏览器该脚本不会在页面加载完成之前操作DOM,脚本将会和其他资源文件并行下载。

加载顺序: defer的这个属性,始终是按照在dom中的顺序来加载执行。相当于window.onload,但它比window.onload更灵活

DOMContentLoaded: 他会延迟到在dom加载解析之后加载执行,但是在DOMContentLoaded事件之前。

html4.0中定义了defer;html5.0中定义了async。

(1)没有defer或async,浏览器会立即加载并执行指定的JS脚本,也就是说,不等待后续载入的文档元素,读到JS脚本就加载并执行。

(2)有async,加载后续文档元素的过程将和JS的加载与执行并行进行(异步)。

(3)有defer,加载后续文档元素的过程将和JS的加载并行进行(异步),但JS的执行要在所有文档元素解析完成之后,DOMContentLoaded 事件触发之前完成。

defer和async的共同点:

  1. 不会阻塞文档元素的加载。
  2. 使用这两个属性的脚本中不能调用document.write方法。
  3. 允许不定义属性值,仅仅使用属性名。
  4. 只适用于外部脚本(虽然IE4-IE7还支持对嵌入脚本的defer属性,但在IE8及之后的版本就只支持外部脚本,对不支持的会直接忽略defer属性,因此把延迟脚本放在页面底部仍然是最佳选择)

defer和async的不同点:

  1. 每一个async属性的脚本一旦加载完毕就会立刻执行,一定会在window.onload之前执行,但可能在document的DOMContentLoaded之前或之后执行。不保证按照指定它们的顺序来执行,如果JS有依赖性就要注意了。指定异步脚本的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容,因此,async适用于脚本对DOM无依赖。建议异步脚本不要在加载期间修改DOM。
  2. 每一个defer属性的脚本都是在文档元素完全载入后,一般会 的顺序执行,同时一般会在document的DOMContentLoaded之前执行,相当于window.onload,但应用上比 window.onload 更灵活!实际上,defer 更接近于DomContentLoad。常适用于脚本对DOM有依赖的情况。事实上,延迟脚本不一定会按顺序执行,也不一定会在DOMContentLoaded事件触发之前执行,因此最好只包含一个延迟脚本。

也就是说:defer是“渲染完再执行”,async是“下载完就执行”。故而,defer一般按原本顺序执行,而async的执行顺序被下载完成时间影响。

异步加载的脚本不一定按顺序执行,解决顺序问题办法:最好只有一个脚本。(解决多个脚本执行顺序问题最好就是合并js)。但简单合并js并不优雅,因为合并的js过大在加载时也会消耗性能,而且无法更好的分模块,不利于维护和不同页面的脚本区分。

现代(高版本)浏览器在html5和ES6的加持下,已经原生实现ES Module模块化加载,在script标签中使用type="module"属性可以原生实现ES Module

这里注意是ES6模块方法而不是nodejs的CommonJS方法(CommonJS模块是同步阻塞加载的,不适用于浏览器),CommonJS模块输出是值的拷贝(加载完成会有缓存),ES6模块输出是值的引用(引用时可能修改到模块的值)。CommonJS是运行时加载,ES6模块是编译时加载。

 // 方法 1 : 引入module.js,然后在script标签里面调用
  <script type="module">
    import test from './module.js';
    console.log(test())
  </script>
 
  // 方法 2 : 直接引入index.js,使用src引入
  <script type="module" async="true" src="./index.js"></script>

DOMContentLoaded and styles

我们知道,css不会直接的阻塞dom树的构建,但是css的解析执行会阻塞js的下载执行,从而间接的影响到dom的构建.

Built-in browser autofill

浏览器内置的填充功能,比如有些网站的登录的账号密码,会在DOMContentLoaded时自动填充进去,用户允许的情况。

因此脚本长时间加载执行,会导致DOMContentLoaded在等待,填充的功能也在等待。所以asyncdefer也可以防止这一点。

2.load

浏览器加载了所有资源之后。

3.beforeunload/unload

用户离开页面时候触发。

unload和beforeunload的区别是,一个是已经卸载了,一个是在卸载之前。所以如果有一些确定的东西,大多在beforeunload中处理。

document.readyState

有些时候需要知道页面执行到了哪个阶段,去执行相应的脚本。就可以通过readyState来知晓。readyState有三个阶段:

  1. loading => dom加载中
  2. interactive => dom解析完成,几乎与DOMContentLoaded同时发生,但是在DOMContentLoaded之前
  3. complete => 全部资源加载完毕,window.load.

readyState有个对应的readystatechange事件,每次readyState变化的时候,都会调用readystatechange

如何优化CSS阻塞

2020
20/03

默认情况下,CSS 被视为阻塞渲染的资源,这意味着浏览器将不会渲染任何已处理的内容,直至 CSSOM 构建完毕。所以要精简 CSS,尽快提供它,并利用媒体类型和查询来解除对渲染的阻塞。

总而言之,记住下面这几条:

  1. 默认情况下,CSS 被视为阻塞渲染的资源。
  2. 我们可以通过媒体类型和媒体查询将一些 CSS 资源标记为不阻塞渲染。
  3. 浏览器会下载所有 CSS 资源,无论阻塞还是不阻塞。

CSS 是阻塞渲染的资源。需要将它尽早、尽快地下载到客户端,以便缩短首次渲染的时间。

不过,我们有一些 CSS 样式只在特定条件下(例如显示网页或将网页投影到大型显示器上时)使用,这些资源不阻塞渲染。

我们可以通过 CSS“媒体类型”和“媒体查询”来解决这类用例:

<link href="style.css" rel="stylesheet"> 

<link href="print.css" rel="stylesheet" media="print"> 

<link href="other.css" rel="stylesheet" media="(min-width: 40em)">
  1. 上面的第一个样式表声明未提供任何媒体类型或查询,因此它适用于所有情况,也就是说,它始终会阻塞渲染。
  2. 第二个样式表则不然,它只在打印内容时适用,因此在网页首次加载时,该样式表不需要阻塞渲染。
  3. 最后一个样式表声明提供由浏览器执行的“媒体查询”:符合条件时,浏览器将阻塞渲染,直至样式表下载并处理完毕。

声明您的样式表资产时,请密切注意媒体类型和查询,因为它们将严重影响关键渲染路径的性能。

可以考虑下面的一个例子:

<link href="style.css" rel="stylesheet"> 

<link href="style.css" rel="stylesheet" media="all"> 

<link href="portrait.css" rel="stylesheet" media="orientation:portrait"> 

<link href="print.css" rel="stylesheet" media="print">
  1. 第一个声明阻塞渲染,适用于所有情况。
  2. 第二个声明同样阻塞渲染:“all”是默认类型,如果您不指定任何类型,则隐式设置为“all”。因此,第一个声明和第二个声明实际上是等效的。
  3. 第三个声明具有动态媒体查询,将在网页加载时计算。根据网页加载时设备的方向,portrait.css 可能阻塞渲染,也可能不阻塞渲染。
  4. 最后一个声明只在打印网页时应用,因此网页首次在浏览器中加载时,它不会阻塞渲染。

js知识总结—基础进阶篇

2020
15/03

1.eval(jsstr)和new Function(jsstr)

evalnew Function都可以动态解析和执行字符串。会将字符串转义为js代码。区别如下:

1.对解析内容的运行环境判定不同:eval中的代码执行时的作用域为当前作用域,它可以访问到函数中的局部变量,也可以访问全局变量。new Function中的代码执行时的作用域为全局作用域,不论它的在哪个地方调用的,所以它访问的都是全局变量,无法访问局部作用域的变量。

var a = 'global scope' 
function b(){ 
    var a = 'local scope' 
    eval('console.log(a)') //local scope 
    (new Function('','console.log(a)'))() //global scope
}
b()

2.eval() 安全性较低,最好不要使用。它使用与调用者相同的权限执行代码。 eval() 运行的字符串代码若被恶意方修改,将会影响计算机的安全。

3.evalnew Function更慢,因为它必须调用 JS 解释器,很吃性能。

js知识总结—基础知识篇

2020
14/03

1.js判断变量类型

  1. typeof可以判断一般类型。但无法准确识别对象。所有对象或类对象类型(null)都为”object”,比如数组typeof [] == "object"。JS数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。
  2. typeof 与其他条件组合判断:typeof arr=="object" && !!arr.push
  3. instanceof可以判断具体对象类型。用来判断对象是否为某个构造函数的实例。[] instanceof Array==true
  4. Object.prototype.toString.call()可以判断任意变量类型。Object.prototype.toString.call([])=="[object Array]"
  5. .constructor.name可以判断变量类型(nullundefined除外)。true.constructor.name=="Boolean"
  6. 判断数组也可以用Array.isArray(arr)方法

2.数组常用方法

  1. pop()尾部删除,返回尾部元素,会改变原数组
  2. push()尾部插入,会改变原数组
  3. unshift()头部插入,会改变原数组
  4. shift()头部删除 ,这两个别搞混。返回头部元素,会改变原数组
  5. join()把数组元素放入字符串,使用指定的分隔符进行分割
  6. reverse()倒序数组元素顺序,返回倒序后的新数组。会改变原数组
  7. splice()删除元素,并向数组添加新元素。会改变原数组
  8. sort()对数组对象进行排序—字符串排序按每一个英文字母先后/数字排序按字符串处理(实际上是每一位的排序),会改变原数组
  9. slice()从开始和结束位置截取数组中的元素返回新数组
  10. concat()连接(合并)两个数组
  11. arr.length=0 清空数组
  12. map()/find()/filter()/reduce()/forEach()/some()/every():ES6数组新方法。(every:一假即假,some:一真即真)

3.数组去重

  1. indexOf/includes循环去重:申明一个新新数组,判断新数组中是否包含当前项,若没有,则push
  2. ES6 Set对象去重:Array.from(new Set(arr))
  3. Object键值对去重:把数组的值存为key,判断obj[arr[i]]是否存在,若存在则重复。
  4. 利用ES6的数组reduce方法(第二个参数[]为初始值):arr.reduce((unique,item) =>unique.includes(item)?unique:[...unique,item], []);

4.去除字符串首尾空格

  1. str.trim()
  2. 正则:str.replace(/(^\s*)|(\s*$)/g,"")

5.requestAnimationFrame

requestAnimationFrame()方法用于代替使用定时器开发的动画。此方法的回调函数会在浏览器重绘之前调用。此方法的执行频率与显示器的刷新频率相关。回调函数执行次数通常是每秒60次,但在大多数浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的<iframe> 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。

大部分显示器的刷新频率为每秒60次,也就是每次刷新间隔为16.7ms,我们在渲染动画时只有每一帧间隔时间<=16.7ms才能让用户觉得不卡顿。

6.引用类型常见有哪些对象

Object、Array、RegExp、Date、Function、Math、String、Number、Boolean

7.对象深拷贝、浅拷贝

对象引用:只引用对象,没有真正的复制。只复制引用(内存地址指向/栈内存),没有复制真正的值(堆内存),对其进行修改会影响原对象。

浅拷贝:只复制一层对象的属性。如果对象还有嵌套对象,则无法复制。如Object.assign()

深拷贝:复制了对象真正的值。对其任何操作都不会影响被拷贝的对象。

实现浅拷贝:

  1. 扩展运算符
  2. Object.assign
  3. 数组通过slice()的方法
let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

//-------------------------------------------

let a = {
    age: 1
}
let c = {...a}
c.age = 2
console.log(a.age) // 1

实现深拷贝:

  1. 递归浅拷贝(可行)。
  2. 深度遍历对象,递归嵌套对象,将其键值对赋值给另外一个新创建的对象,但性能堪忧。
  3. 使用JSON反序列化和序列化,性能最快JSON.parse(JSON.stringify(oldObj))(缺点:只能深拷贝对象和数组,会忽略undefined和symbol,不能序列化函数,不能序列化循环引用的对象)
  4. 第三方库如 jQuery.extend
  5. ProxyObject.defineProperty来拦截 set 和 get 就能阻止拷贝对象修改原对象(性能高,无缺点),深拷贝点此参考

注意:数组的slice方法和concat方法无法进行深拷贝,只能浅拷贝。因为嵌套对象或数组会被引用。对象的Object.assign()方法同样如此。

8.判断两个对象是否相等

"a==b"或者Object.is(a,b)可以判断两个对象是否相等,但仅限于两个对象引用相同,是同一个对象。深浅拷贝对象无法判断。

若要判断两个不同引用的对象是否相等,得通过深度遍历及递归方法(递归时判断值是否是Object)进行值的判断(遍历及递归之前先通过Object.getOwnPropertyNames拿到两个对象的所有键名,比较键名是否相等)。

这时候不能用JSON.stringify()方法去比对对象,因为序列化之后,键是有序的,无法比较。

9.js变量类型

基本类型有六种nullundefinedbooleannumberstringsymbol

其中 JS 的数字类型是浮点类型的,没有整型。NaN 也属于 number 类型,并且 NaN 不等于自身。对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型。

typeof 对于基本类型,除了 null 是object,其他都可以显示正确的类型。(在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object

引用类型:objectfunctionarraymapset

引用类型在使用过程中会遇到浅拷贝和深拷贝的问题。typeof 对于引用类型,除了函数都会显示 object

10.隐式类型转换

使用if条件判断或使用==/&&/||等运算符时会对变量进行隐式类型转换。

隐式类型转换时,除了 undefined, null, false, NaN, '', 0, -0[],其他所有值都转为 true,包括所有对象(这里注意空数组会被转为false)。

对象在隐式类型转换时,首先会调用其原型上的 valueOf 方法或 toString方法。valueOf 优先于toString。并且这两个方法你是可以重写的。

let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
}
1 + a // => 1
'1' + a // => '10'

//-------------------

let a = {
  toString() {
    return '1';
  },
}
1 + a // => '11'
'1' + a // =>  '11'

只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'

对于加号需要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
// 因为 + 'b' -> NaN
// 你也许在一些代码中看到过 + '1' -> 1

console.log(+'1') // 1
console.log(-'1') // -1
console.log(+'a') // NaN
console.log(-'a') // NaN

console.log(+'') // 0
console.log(+[]) // 0
console.log(+true) // 1
console.log(+false) // 0
console.log(-true) // -1
console.log(-false) // -0
console.log(-false) // -0
console.log(-[]) // -0
console.log(-'') // -0
console.log(+[123]) // 123
console.log(+['123']) // 123
console.log(+[true]) // NaN
console.log(+[123,1]) // NaN

// 'a' + + 'b'  等同于 'a' + (+'b') 等同于  'a' + NaN
console.log('a' + (+'b'))  // "aNaN"
console.log( 'a' + NaN)  // "aNaN"

空数组会被转为false

[] == ![] // -> true
[]==false //true

11.++运算

++在前,返回值是新值

++在后,返回值是旧值

var a = -1
if(++a){ // 这里++a的值为新值0
  console.log(666)
}else{
  console.log(888)
}
// 结果为888

// ---------------------------------------

var a = -1
if(a++){ // 这里a++的值为旧值-1
  console.log(666)
}else{
  console.log(888)
}
// 结果为666

Vue重点知识总结—性能篇

2020
15/02

1.vue性能优化

1.编码优化(代码层面)

  1. 不要将所有数据都放到data中,因为data中数据会遍历添加gettersetter,会收集对应的watcher
  2. 若在方法或计算属性中不对组件数据进行修改,最好在刚开始时就将数据赋值给变量,之后使用直接使用变量效率较高(let {user,list} = this)。
  3. 在v-for时需要给每项元素绑定事件时用外层事件代理
  4. v-for遍历时避免同时使用v-if,使用计算属性提前把数组中要显示的进行过滤,然后v-for直接使用过滤后的数组
  5. SPA页面采用keep-alive缓存组件,可以添加include和exclude来配置需要缓存的组件。
  6. 尽量细地拆分组件,提高复用性、可维护性,减少不必要的渲染
  7. v-ifv-show 区分使⽤场景:v-iffalse时内部指令不会执行,有阻断功能,不频繁的显隐功能或简单的组件尽量用v-if代替v-show
  8. 较复杂的组件渲染比较吃力,显隐功能可以使用v-show进行缓存组件
  9. computedwatch 区分使⽤场景:computed 的值有缓存,只有它依赖的属性值发⽣改变,下⼀次获取computed 的值时才会重新计算。watch每当监听的数据变化时都会执⾏回调进⾏后续操作;当我们需要在数据变化时执⾏异步或开销较⼤的操作时,应该使⽤watch。当一个属性受多个属性影响的时候需要computed,当一个属性影响多个属性的时候用watch
  10. v-for 遍历必须为 item 添加 keykey保证唯一性(vue的dom更新采用就地复用策略)
  11. Object.freeze冻结数据,这些数据只是单纯展示,避免不必要的响应式(this.userInfo = Object.freeze(userInfo)
  12. 使用v-once指令,处理只需要渲染一次的节点或组件(低开销的静态组件)。
  13. 无状态(没有自己的数据,只是展示父组件的传值)的组件标记为函数式组件(<template functional></template>)。
  14. 合理使用路由懒加载、异步组件(缩减初始包体积,路由会按需加载)
  15. 数据持久化(做缓存)
  16. 防抖、节流
  17. 组件在销毁时注意销毁事件监听和定时器,防止内存泄漏。(vue组件在销毁时会自动销毁组件本身和组件节点上的的事件和指令)
  18. 通过对css样式的合并和js逻辑的封装:如在两不同组件中,拥有相同的样式,可通过全局css文件中设置。在js文件上,将相同的方法封装合并成一个方法,如API请求。

2.页面加载性能优化

  1. 第三方模块按需导入,比如使用elementUI时,不需要的组件无需引入,避免体积过大。(使用babel-plugin-component插件)
  2. 滚动到可视区动态加载。比如长列表使用虚拟滚动,滚动到一定位置时加载下一页。
  3. 图片懒加载,动态加载图片。比如vue-lazyload插件,通过v-lazy指令来取代img的src属性。

3.用户体验

  1. 骨架屏(app-skeleton)首屏加载Loading
  2. app壳(app-shell)默认先渲染一个导航,或者静态视图

4.SEO优化

  1. 预渲染插件(prerender-spa-plugin)
  2. 服务端渲染SSR

5.打包优化

  1. 使用CDN加载第三方资源,就可以缓解我们服务器的压⼒。什么是CDN
  2. 多线程打包(happypack)
  3. Webpack 、Vite 编译时对图⽚进⾏压缩
  4. Webpack可使用相关插件如useless-files-webpack-plugin检查及删除无用文件

6.首屏加载优化

  1. 减少入口文件体积,压缩文件
  2. 合理使用路由组件按需加载、懒加载、异步组件。缩减初始包体积,在调用某个组件时再加载对应的js文件;
  3. 静态资源本地缓存,后端返回的资源:采用http缓存,前端合理利用localStorage,CDN静态资源缓存
  4. UI框架、自定义组件按需加载
  5. 图片压缩,开启gzip压缩(webpack中配置)
  6. 骨架屏、首屏加载Loading
  7. 图标可通过font-icon或矢量图(如SVG)来代替,也可将小图片转为Base64格式进行展示。
  8. 通过精灵图来减少小图标的总请求数
  9. 图片懒加载,对未可见的图片进行延迟加载。比如通过安装vue-lazyload模块实现懒加载。图片懒加载实现
  10. 将公用的JS库通过script标签外部CDN引入,减小打包的js大小,让浏览器并行下载资源文件,提高下载速度;
  11. 加一个首屏 loading 图,提升用户体验;

7.计算首屏加载时间

首屏加载时间,指的是浏览器从相应用户输入网址,到首屏内容渲染完成的时间。可以使用window.performance提供的API进行计算。可以在window.onload事件中读取performance提供的各种数据。

计算首屏加载时间公式:times = (performance.timing.domComplete - performance.timing.navigationStart) / 1000

若首屏的DOM会持续变化的话,可以使用 MutationObserver 方法监听DOM变动,根据变动的时间计算DOM趋于稳定的时间节点。

8. 服务器访问速度优化

  1. 开启服务器 Gzip 压缩,需要前端提供压缩包,然后在服务器开启压缩,⽂件在服务器压缩后传给浏览器,浏览器解压后进⾏再进⾏解析。(通过webpack或Vite对项目体积进行压缩)
  2. 全站 CDN 加速,使用一些第三方 CDN 云加速服务。什么是CDN

2.浏览器性能检测(F12控制台)

Performance(性能)

Performance 是 Chrome 开发者工具中的一个功能,用于记录网页从初始化到运行时的所有性能指标。

使用 Performance 前,我们最好打开 Chrome 的无痕模式。因为 Chrome 上一般有着大量的插件,会或多或少的影响页面的性能,所以我们关掉这个来避免对页面性能的影响。

点击左上角的 Record(小圆点)按钮,Performance 进入 Record 阶段,从此刻开始,它会记录用户的交互以及这些交互对页面性能数据的影响。

生成的 Performance 性能报告,我们先看顶部的三个数据:FPSCPU 以及 NET

  • FPS:主要和动画性能有关,代表每秒帧数。图表中的绿色长条越高,说明FPS越高,用户体验越好。如果其中有红色长条,代表着这部分帧数有卡顿,需要优化
  • CPU:和底部的 Summary 对应,显示了页面加载过程中,各阶段对 CPU 的占用时间,占用时间越多,代表该阶段越需要优化。在 Performance 中,该部分是最需要关注的指标之一。
  • NET:每条彩色横杠表示一种资源。横杠越长,检索资源所需的时间越长。 每个横杠的浅色部分表示等待时间(从请求资源到第一个字节下载完成的时间) 深色部分表示传输时间(下载第一个和最后一个字节之间的时间)
  • Main:火焰图。它展现了主线程在 Record 过程中做的所有事情,包括:Loading、Scripting、Rendering、Painting 等等。火焰图的横轴代表着时间,纵轴代表着调用堆栈。每一个长条代表执行了一个事件或函数,长条的长度代表着耗时的长短,如果某个长条右上角是红色的则表示该函数存在性能问题,需要重点关注。
  • DOMContentLoaded :就是 dom 内容加载完毕。 那什么是 dom 内容加载完毕呢?打开一个网页当输入一个 URL,页面的展示首先是空白的,然后过一会,页面会展示出内容,但是页面的有些资源比如说图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发 DOMContentLoaded 事件。而这段时间就是 HTML 文档被加载和解析完成。
  • load: 页面上所有的资源(图片,音频,视频等)被加载以后才会触发 load 事件,简单来说,页面的 load 事件会在 DOMContentLoaded 被触发之后才触发。

Performance 提供的性能监测功能已经较为完备,但是,它有两个问题:

  • 数据缺少实时性
  • 数据面板过于复杂,不够直观

为此,Performance monitor 功能可以实时直观的数据展示页面性能。

Lighthouse面板

Lighthouse 是一个开源的自动化工具,是 Chrome 的一个扩展程序。为 Lighthouse 提供一个您要审查的网址,它将针对此页面运行一连串的测试,然后生成一个有关页面性能的报告,会对页面的加载进行分析,然后给出提高页面性能的建议。可以对以下分类做报告:

  • 性能
  • 无障碍使用
  • 用户体验
  • SEO 优化
  • 移动设备和桌面设备兼容性

3.webpack 性能检测工具

webpack-bundle-analyzer 分析

vue-cli3的项目直接 vue-cli-service build –report 就会生成一个report.html,打开这个html就能看到。非vue-cli3需要自行安装插件。

这个报告可以可以直观分析打包结果,查看各个依赖的体积分布。通过查看,我们可以将首屏未使用到的库去除,进行按需引入。较大的库我们可以换成更轻量级的库。

Vue重点知识总结—理论篇(二)

2020
11/02

作为前端开发中现行最火的框架之一,基于此,总结了一些 Vue 方面经常出现的问题,留给自己查看消化,也分享给有需要的小伙伴。

由于篇幅较长,不能将所有知识点放到一篇文章内。这是Vue重点知识梳理理论篇的第二篇。前端茫茫,学无止境。

1.模板引擎原理(指令和插槽表达式如何生效)

使用with改变作用域,渲染数据。并将其包到字符串中。`var render = with(vm){return ${data}}`,使用new Function(render)执行字符串语句。

2.描述组件渲染和更新过程

渲染组件时,会通过Vue.extend()方法构建子组件的构造函数并实例化为vueComponent。extend方法会合并一些父类如Vue的属性。最终手动调用$mount()进行挂载。更新组件时会进行patchVnode流程,也就是diff流程。

3.为什么要使用异步组件加载方式?

如果组件功能较多,打包出的文件会过大。导致页面加载过慢。这时候可以使用import("url")函数异步加载组件的方式,可以实现文件的分隔加载。

  1. 会将组件分开打包。减小体积。
  2. 会采用jsonp的方式加载,有效解决一个文件过大问题。
  3. import语法是webpack提供的。
  4. 异步组件一定是一个函数。
components:{
    mycomp:()=>import("../components/mycomp.vue")
}

4. Vue-loader 是什么

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。本质上,webpack loader 将所有类型的文件,转换为应用程序可以直接引用的模块。所以 loader 就是个搞预处理工作的。

Vue-loader 可以解析和转换 .vue ⽂件,提取出其中的逻辑代码 script 、样式代码 style 、以及模版 template ,再分别把它们交给对应的 Loader 去处理。

5.Vue 中 key 的作用

key主要作用是为了高效得更新虚拟DOM。原理是VUE在patch过程中会通过key精准判断两个节点是否是同一个。从而避免频繁更新不同元素,使得整个patch过程更加高效,减少dom操作量,提高性能。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

准确: 如果不加key,那么vue会选择复⽤节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产⽣⼀系列的bug.。

快速: key的唯⼀性可以被Map数据结构充分利⽤,相⽐于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1)

  1. 若不设置key,会在列表的渲染过程中发生一些隐蔽的bug,比如新增数据乱序。
  2. 重复的 key 会造成渲染错误。
  3. key是为Vue中的vnode标记的唯⼀id,通过这个key,我们的diff操作可以更准确、更快速
  4. diff算法的过程中,先会进⾏新旧节点的⾸尾交叉对⽐,当⽆法匹配的时候会⽤新节点的key与旧节点进⾏⽐对,然后超出差异

没有key情况还会导致节点的删除和重建,影响性能。若有key,在进行更新时会直接保留原有节点,只插入新节点,无需删除重建。

  1. v-for循环的时候,key属性只能使用number/string
  2. key属性值必须是唯一标识符。
  3. key属性不要使用索引作为标识。

用引索index作为key可能会引发的问题:

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。
  2. 如果结构中还包含输入类的DOM:会产生错误DOM更新 ==> 界面有问题。
  3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的。

Vue重点知识总结—理论篇(一)

2020
04/02

作为前端开发中现行最火的框架之一,Vue 在面试中出现的频率不断增加。基于此,总结了一些 Vue 方面经常出现的面试题,留给自己查看消化,也分享给有需要的小伙伴。

就算工作时间再久,不刷题,是不能那么容易过面试的。我相信就算你是面试官,也不会百分百了解你所问的问题,也不能做到面面俱到。我一直相信,成功的面试都是双方的缘分到了。缘分到了,自然水到渠成。

1.vue解决了什么问题

  1. 不用大量地操作DOM,把重点放到处理数据和业务逻辑上(数据和视图分离,数据驱动视图),可能提高了编码效率,更快的实现业务。
  2. 解决了工程化,模块化的问题,更方便的组织和构建复杂应用。
  3. 提供统一ui库,提升开发效率。
  4. 更加可维护。

2.Vue设计原则理解

  1. 渐进式js框架
  2. 易用、灵活、高效

渐进式

与其他大型框架不同,自底向上逐层应用。核心库只关注视图层,易于上手,还便于与第三方库和自有项目整合。

渐进式在于,如果只是单页应用,完全可以直接引入Vue来开发;如果应用程序有了一定的规模,我们可以考虑引入vue-router路由;如果需要维护很多公共的状态,那么可以引入vuex;如果要做大型应用,我们可以引入Vue-cli脚手架等等。vue的全家桶就体现了渐进式的思想。

易用性

响应式和申明式的模板语法便于快速上手,不需要管底层原理,只要会html、js、css直接就可以开发应用。

灵活性

渐进式的思想最大优点就是灵活。不管是构建大型应用、还是只开发单页、还是与既有项目整合。

高效性

超快的虚拟DOM和diff算法使得我们的应用拥有较高的性能表现。Vue后续版本也得以体现,比如使用Proxy,比如优化diff算法。

3.Vue数据为什么是异步更新?

vue是组件级更新,也就是说,每一次的更新都是渲染整个组件。Vue组件中的每个data数据都会收集依赖,如果是同步的话,一旦修改了data属性,便会触发对应的 watcher,然后调用对应 watcher 下的 update方法更新视图,那么会导致视图频繁更新,性能堪忧。

如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。这也是nextTick 产生的原因。异步渲染核心思想是 nextTick 。

作用:

nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新后才执行,减少操作DOM的次数

使用场景:想要操作 基于最新数据生成的DOM 时,就将这个操作放在 nextTick 的回调中;

实现原理

将传入的回调函数包装成异步任务,异步任务又分微任务和宏任务(setTimeout、promise那些),定义了一个异步方法,多次调用nextTick会将方法存入队列,通过异步方法清空当前队列。

异步更新是Vue会在本轮数据更新后,再去异步更新视图。这也是为了提高性能的考虑。具体实现是:只要侦听到数据变化,Vue 将开启一个队列,将在同一事件循环中发生的所有数据变更进行缓存。如果同一个 watcher 被多次触发,只会被推入到队列中一次。最终执行队列中的run方法更新视图,这里涉及到nextTick。所以相同的watcher最终只会更新一次。

nextTick相当于是一个防抖操作,但类似于setTimeout(flushCallbacks, 0),它的防抖并不是说某一个时间段内的任务只执行一次。而是等到所有主线程的宏任务都执行完才去执行任务队列中的任务。而任务队列的执行顺序是优先微任务的,所以首选promise。不支持promise的话再用MutationObserver,如果都不支持,再考虑异步宏任务setImmediate(高版本IE),再不济只能用setTimeout 0

4. 解释单向数据流和双向数据绑定

单向数据流: 顾名思义,数据流是单向的。数据流动方向可以跟踪,流动单一,追查问题的时候可以更快捷。数据只能从⼀个⽅向来修改状态。

vue 组件间传递数据是单向的,即数据总是由父组件传递到子组件,子组件在其内部可以有自己维护的数据,子组件无权修改父组件传递给它的数据。这样做是为了组件间更好的解耦,在开发中可能有多个子组件依赖于父组件的某个数据,假如子组件可以修改父组件数据的话,一个子组件变化会引发所有依赖这个数据的子组件发生变化。而单向数据流会防止从子组件意外改变父级组件的状态而导致的数据混乱,让开发变得复杂。

缺点就是写起来不太方便。如在Vuex中要使UI发生变更就必须创建各种 action 来维护对应的 state。子组件要改变父组件状态时只能通过$emit方法触发父组件相应的事件来实现。

双向数据绑定:即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。双向数据绑定就是在单向绑定的基础上给输入框元素(input、textare等)添加了 change(input) 事件,来动态修改数据和视图。优点是在表单交互较多的场景下,会简化大量与业务无关的代码。

5.对 MVC、MVP、MVVM的理解

两者都是框架模式,设计目标为了解决view和model的耦合问题,解决代码分层,解决维护性问题。

MVC:Model代表数据模型,数据Model层中定义;View代表UI视图,负责数据的展示;Controller负责业务逻辑处理。

早起专注于服务端(后端),当时ajax还没有火起来,这时前后端虽然有一些分离,但依然绑定在一起。如java的springMVC、ASP.net、PHP的mvc等,前端的backBonejs。优点是分层清晰。缺点是数据流混乱、控制层臃肿,代码不易读,不易维护,灵活性依然有问题。

  1. View 传送指令到 Controller
  2. Controller 完成业务逻辑后,要求 Model 改变状态
  3. Model 将新的数据发送到 View,用户得到反馈
  4. 所有通信都是单向的,最后controller会很臃肿,不易维护。

MVP

MVP的全称为Model-View-Presenter,Model提供数据,View负责显示,Controller/Presenter负责逻辑的处理。MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。

MVVM:分为Model、View、ViewModel三者

这种模式不仅解决了耦合性问题,而且通过Viewmodel层数据和视图的映射关系,减少了很多繁琐的代码,尤其是DOM操作的代码。在提高开发效率,可读性的同时,还保持了优越的性能。

  1. Model 代表数据模型,数据和业务逻辑都在Model层中定义;
  2. View 代表UI视图,负责数据的展示;
  3. ViewModel-数据响应层,负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;
  4. Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。
  5. 这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作 dom。

Vue中的MVVM模式理解

MVVM是由MVC发展而来的。两者都以数据为核心,MVC最初的用意是为了将视图和逻辑层分离。MVVM比MVC的的最大优势在于MVVM把大量的与页面视图相关的逻辑单独抽离出来(viewModel)构成了一个库。而Vue就是一个vm(viewModel)库。最简单的就是v-model指令,把数据与输入框做一个关联,不需要自己再去监听,Vue内部帮我们搞定。viewModel最多体现在Vue的各种指令上。

MVVM模式在react中的对应关系

  •  M(odel):对应组件的方法或生命周期函数中实现的业务逻辑和this.state中保存的本地数据,如果React集成了redux +react-redux,那么组件中的业务逻辑和本地数据可以完全被解耦出来单独存放当做M层,如业务逻辑放在Reducer和Action中。
  •  V(iew)-M(odel):对应组件中的JSX,它实质上是Virtual DOM的语法糖。React负责维护 Virtual DOM以及对其进行diff运算,而React-dom 会把Virtual DOM渲染成浏览器中的真实DOM
  •  View:对应框架在浏览器中基于虚拟DOM生成的真实DOM(并不需要我们自己书写)以及我们书写的CSS
  • 绑定器:对应JSX中的命令以及绑定的数据,如className={ this.props.xxx }、{this.props.xxx}等等

MVVM的双绑和单绑区别

  • 一般只有UI表单控件才存在双向数据绑定,非UI表单控件只有单向数据绑定。
  • 单向数据绑定是指:M的变化可以自动更新到ViewModel,但ViewModel的变化需要手动更新到M(通过给表单控件设置事件监听)
  • 双向数据绑定是指念:M的变化可以自动更新到ViewModel,ViewModel的变化也可以自动更新到M
  • 双向绑定 = 单向绑定 + UI事件监听。双向和单向只不过是框架封装程度上的差异,本质上两者是可以相互转换的。

优缺点:在表单交互较多的情况下,单向数据绑定的优点是数据更易于跟踪管理和维护,缺点是代码量较多比较啰嗦,双向数据绑定的优缺点和单向绑定正好相反。

MVVM的优点

MVVM比MVC精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作DOM元素。因为在MVVM中,View不知道Model的存在,Model和ViewModel也观察不到View,这种低耦合模式提高代码的可重用性。

6.Vue组件化理解

组件是独立的可复用的代码组织单元。组件化是便于复用、测试和维护。而且可以实现分而治之,大幅提高开发效率。

组件系统是Vue的核心特性之一。是一种规范,这种规范使得开发变得更可控。他使得开发者可以使用小型的可复用的组件来构建大型应用。

这种规范,使得组件之间相互影响制约,更好的进行团队协作。包括布局和样式可以独立、解耦。

Vue的更新粒度是组件级别的,组件的数据变化只会影响当前组件的更新,但是在组件更新的过程中,也会对子组件做一定的检查,判断子组件是否也要更新,并通过某种机制避免子组件重复更新。

组件分为:页面组件、业务组件(如登录框)、通用组件(如按钮、弹窗等UI组件)。

组件开发应该是高内聚(更加独立)、低耦合(组件之间不应有太多依赖关系)的。

Vue组件是基于配置的。我们通常写的组件是配置选项而非组件实例。框架后期会生成对应的构造函数。他们基于VueComponent,是Vue的扩展。

vue组件的构造函数在内部通过extend来创建,创建构造函数时会继承Vue且传入当前vue实例,Vue每一个组件都是一个Vue的实例。组件实例是继承自Vue的。

全局组件会立刻调用extend生成构造函数,在当前Vue实例化之前就已经创建了。而局部组件在运行时某个时刻才会生成。

合理使用组件能提升性能:每一个组件在实例化时(mountComponent)都会实例化自己的Watcher,每一个watchar中都有其更新函数。这样每次更新时只更新当前组件本身。所以应该尽量地细粒化组件,提高渲染的性能(这样每次更新视图时打补丁的范围就比较小了)。

组件有父子组件、根实例,组件或路由切换避免丢失状态。

单文件Vue的组件不仅仅是DOM层面的封装。也包含了组件的方法、逻辑、数据、样式。

单文件组件并不是实例的直接导出,而是经过webpack的vue-loader,将vue后缀的文件进行编译,最终导出的是js对象(组件配置对象而非构造函数)。

Vue组件常用的功能有:属性prop、插槽slot、自定义事件等,通常用于组件通信和扩展。

Vue组件遵循单向数据流原则。

7. 对比 原生或jQuery与Vue 有什么不同

原生专注视图层,通过操作 DOM 去实现页面的一些逻辑渲染;

Vue 专注于数据层,通过数据的双向绑定,最终表现在 DOM 层面,减少了 DOM 操作。更直白来说就是不操作DOM,用数据来渲染,通过虚拟的抽象数据层来直接更新页面。当数据发生变化的时候,用户界面发生相应的变化,开发者不需要手动的去操作dom。相当于数据与DOM之间做了一个映射。更简单,更方便,程序员只需要关注核心的业务逻辑和数据变化,不需要关注诸如DOM操作等其他层面。

Vue 使用了组件化思想,提高了开发效率,方便复用,便于协同开发。

Vue的核心是数据驱动,组件系统。

jquery是以程序逻辑为重,事件驱动。所谓的事件驱动简单来说就是用户通过点击,修改,删除,输入等等,来操作DOM,并触发对应的事件,然后通过后台响应处理,随之更新UI。

具体:

  1. jquery是使用选择器()选取DOM对象,对其进行赋值、取值、事件绑定等操作,和原生的HTML的区别只在于可以更方便的选取和操作DOM对象,而数据和界面是在一起的。jquery侧重样式操作,动画效果等;可以应用于一些html5的动画页面,一些需要js来操作页面样式的应用。
  2. Vue 则是通过Vue对象将数据和View完全分离开来了。对数据进行操作不再需要引用相应的 DOM 对象,他们通过 Vue 对象这个 vm 实现相互的绑定。这就是传说中的 MVVM。vue侧重数据绑定,可以应用于复杂数据操作的后台页面。

8.Vue组件化优势

每一个组件都是一个实例,每一个实例都可以作为一个组件挂载到一个实例上。每个组件是一个vueComponent的实例。方便我们进行单文件组件开发。

  1. 提高开发效率
  2. 方便重复使用
  3. 简化调试步骤,更快聚焦问题
  4. 提升项目的可维护性
  5. 便于多人协同开发

9.Vue 等单页面应用的优缺点

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染。良好的交互体验。
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;便于提高开发效率。后端不再负责模板渲染、输出页⾯⼯作,后端API通⽤化,即同⼀套后端程序代码,不⽤修改就可以⽤于Web界⾯、⼿机、平板等多种客户端。
  • 单页应⽤没有页⾯之间的切换,就不会出现“⽩屏现象”,也不会出现假死并有“闪烁”现象,用户体验及交互比较流畅。
  • 组件化开发,有利于复用和维护。
  • 只有一个Html,前端js承担了页面所有切换和渲染,相对服务器压⼒⼩,服务器只⽤出数据就可以,不⽤管展⽰逻辑和页⾯合成,吞吐能⼒会提⾼⼏倍。

缺点:

  • 首屏加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
  • 容易造成Css命名冲突。
  • 页⾯的复杂度很⾼,需要⼀定的技能⽔平,开发成本⾼

10. 单页⾯(SPA)和多页⾯区别

  • 单页应⽤ 页⾯跳转—->js渲染,整个项目只有一个Html文件,优点:页⾯切换快 缺点:⾸屏加载稍慢,seo差
  • 多页应⽤ 页⾯跳转—->返回html 优点:⾸屏时间快,seo效果好 缺点:页⾯切换慢

11.理解虚拟DOM

浏览器工作流程大致分5步:构建DOM树 –> 创建样式规则Style Rules -> 构建渲染树Render tree -> 布局Layout –> 绘制页面。

源生操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍。比如当你在一次操作时,需要更新10个DOM节点,理想状态是一次性构建完DOM树,再执行后续操作。但浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,所以最终执行10次流程。

虚拟DOM就是为了解决这个浏览器性能问题而被设计出来的。例如前面的例子,假如一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性附到DOM树上,通知浏览器去执行绘制工作,这样可以避免大量的无谓的计算量。

真实的DOM节点,包含着很多属性。虚拟DOM是用js对象模拟DOM节点,好处是:页面的更新可以先全部反映在js对象上,操作js对象的速度显然要快多了。等对象操作完成再一次性渲染。

12.Vue虚拟DOM和diff算法

虚拟dom

使用JS对象来模拟DOM结构,就是所谓的虚拟dom。由于dom操作非常昂贵(吃性能),所以把DOM变化的对比,放在JS层来做,页面的更新可以先全部反映在js对象上,操作js对象的速度显然要快多了。等对象操作完成再一次性渲染,提高dom渲染效率。

由于渲染真实DOM的开销很大,有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,如果想要只更新我们修改的那一小块dom而不要更新整个dom,就用到了虚拟dom和diff算法。

diff算法

diff算法即差异查找算法。对于DOM结构即为tree(dom树)的差异查找算法。

diff算法是虚拟DOM的必然产物。通过新旧虚拟DOM的对比,将最终的变化反映到真实DOM上。

diff算法是⼀种通过同层的树节点进⾏⽐较的⾼效算法,避免了对树进⾏逐层搜索遍历,所以时间复杂度只有 O(n)。

而vue采用虚拟dom形式模拟真实dom,故其差异查找实质是对两个JavaScript对象的差异查找。

Vue为什么使用diff

因为vue中Watcher粒度的降低,一个组件对应一个watcher,只有引入diff才能精准地找到发生变化的位置。

diff算法有两个⽐较显著的特点

  1. ⽐较只会在同层级进⾏, 不会跨层级⽐较。
  2. 在diff⽐较的过程中,循环从两边向中间收拢。

Vue的diff过程(阐述1)

当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,内部会尝试将Watcher添加到异步更新队列,在每一次事件循环结束时会清空这些队列。

这时所有的watcher尝试执行其对应的更新函数,更新函数调用了组件的渲染函数和更新函数。这时会重新生成最新的虚拟DOM,然后执行更新函数比较新旧虚拟DOM,这时是真正的diff执行时刻,这个过程称为patch(打补丁),更新相应的视图。

diff调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。(patch意为补丁)。

diff算法采用:深度优先,同层比较的策略。

  1. 两个节点先判断是否都拥有子节点或文本节点然后做不同的操作
  2. 比较两组子节点的算法是diff的核心。首先假设子节点的头尾节点可能相同,做比对尝试,减少循环次数。
  3. 如果没有相同节点则按照通用方式遍历查找。借助key的方式可以高效得找到相同节点。
  4. 当找不到相同节点时,再按情况处理剩余节点。剩余的节点要么是批量删除,要么是批量新增。

比较只会在同层级进行, 不会跨层级比较。

Vue的diff过程(阐述2)

⾸先定义 oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 分别是新⽼两个 VNode 的两边的索引。

接下来是⼀个 while 循环,在这过程中,oldStartIdx、newStartIdx、oldEndIdx 以及 newEndIdx 会逐渐向中间靠拢。while 循环的退出条件是直到⽼节点或者新节点的开始位置⼤于结束位置。

while 循环中会遇到四种情况:

  1. 情形⼀:当新⽼ VNode 节点的 start 是同⼀节点时,直接 patchVnode 即可,同时新⽼ VNode 节点的开始索引都加 1。
  2. 情形⼆:当新⽼ VNode 节点的 end 是同⼀节点时,直接 patchVnode 即可,同时新⽼ VNode 节点的结束索引都减 1。
  3. 情形三:当⽼ VNode 节点的 start 和新 VNode 节点的 end 是同⼀节点时,这说明这次数据更新后 oldStartVnode 已经跑到了oldEndVnode 后⾯去了。这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后⾯,同时⽼ VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1。
  4. 情形四:当⽼ VNode 节点的 end 和新 VNode 节点的 start 是同⼀节点时,这说明这次数据更新后 oldEndVnode 跑到了oldStartVnode 的前⾯去了。这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前⾯,同时⽼ VNode节点结束索引减 1,新 VNode 节点的开始索引加 1。

while 循环的退出条件:直到⽼节点或者新节点的开始位置⼤于结束位置。

  1. 情形⼀:如果在循环中,oldStartIdx⼤于oldEndIdx了,那就表⽰oldChildren⽐newChildren先循环完毕,那么newChildren⾥⾯剩余的
    节点都是需要新增的节点,把[newStartIdx, newEndIdx]之间的所有节点都插⼊到DOM中
  2. 情形⼆:如果在循环中,newStartIdx⼤于newEndIdx了,那就表⽰newChildren⽐oldChildren先循环完毕,那么oldChildren⾥⾯剩余
    的节点都是需要删除的节点,把[oldStartIdx, oldEndIdx]之间的所有节点都删除

13.虚拟dom的作用(目的)

什么是虚拟DOM?

虚拟DOM其实就是⼀个JavaScript对象。通过这个JavaScript对象来描述真实DOM。

虽然虚拟dom通过diff算法,再利用vue数据异步更新机制, 能够尽可能地减少整个dom的重绘和频繁的渲染。除了将dom转为vdom,然后还要执行diff算法,最终的渲染还是要操作真实dom,所以效率并没有很高。而vue官方也从来没说过虚拟DOM效率有多高。

虚拟dom和diff算法只有在dom元素剧烈变化的时候才会体现他的好处:可以局部替换HTML标签(替换 vnode)。

虚拟dom的作用

  1. 由于虚拟dom和真实dom很相似。而虚拟dom是js对象,这就可以实现组件的高度抽象化
  2. 可以适配 DOM 以外的渲染目标。
  3. 不用再依赖html解析器进行模板解析。为框架跨平台提供了可能。比如React Native、weex、服务端渲染(ssr)。
  4. 提高渲染性能。

虚拟 dom 为什么会提⾼性能?

真实DOM的操作,⼀般都会对某块元素的整体重新渲染,采⽤虚拟DOM的话,当数据变化的时候,只需要局部刷新变化的位置就好了,虚拟dom相当于在js和真实dom中间加了⼀个缓存,利⽤dom diff算法避免了没有必要的dom操作,减少了整个dom的重绘和频繁的渲染,从⽽提⾼性能。

  1. 局部更新节点数据
  2. 新旧js对象进行diff,找到差异节点局部刷新。避免了没有必要的dom操作

虚拟 dom 具体实现步骤

  1. ⽤ JavaScript对象结构表⽰ DOM树的结构;然后⽤这个树构建⼀个真正的 DOM树,插到⽂档当中。
  2. 当状态变更的时候,重新构造⼀棵新的对象树。然后⽤新的树和旧的树进⾏⽐较,记录两棵树差异。
  3. 把记录的差异应⽤到先前(步骤1)所构建的真正的DOM树上,视图实现更新。

14. defineProperty数据劫持后如何通知数据和视图更新的

vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进⾏数据劫持的?说⽩了就是通过Object.defineProperty()来劫持对象属性的setter和getter操作,在数据变动时做你想要做的事情。

我们已经知道实现数据的双向绑定,⾸先要对数据进⾏劫持监听,所以我们需要设置⼀个监听器Observer,⽤来监听所有属性。如果属性发⽣变化了,就需要告诉订阅者Watcher看是否需要更新。

因为订阅者是有很多个,所以我们需要有⼀个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进⾏统⼀管理的。

接着,我们还需要有⼀个指令解析器Compile,对每个节点元素进⾏扫描和解析,将相关指令(如v-model,v-on)对应初始化成⼀个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执⾏对应的更新函数,从⽽更新视图。因此接下去我们执⾏以下3个步骤,实现数据的双向绑定:

  1. 实现⼀个监听器Observer,⽤来劫持并监听所有属性,如果有变动的,就通知订阅者。
  2. 实现⼀个订阅者Watcher,每⼀个Watcher都绑定⼀个更新函数,watcher可以收到属性的变化通知并执⾏相应的函数,从⽽更新视图。
  3. 实现⼀个解析器Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显⽰在视图上,然后初始化相应的订阅者(Watcher)。

CSS重点知识总结—flex布局

2019
05/12

1.理解flex

Flex 是 Flexible Box 的缩写,意为”弹性布局”,用来为盒状模型提供最大的灵活性。可以随着页面大小的改变自适应页面布局。

块级元素 .box{ display:flex; } 行内元素也可以设置成flex布局 .box{ display:inline-flex; }

设为Flex布局以后,子元素的float、clear和vertical-align属性将失效。

2.justify-content 和 align-items

justify-content 是弹性盒子主轴布局方式,分为靠左,靠右,居中分布,均匀分布等

align-items 是侧轴中的布局,同上

3.flex-grow、flex-shrink、flex-basis及其简写

  1. flex-basis  ,basis英文意思是<主要成分>,所以他和width放在一起时,肯定把width干掉,basis遇到width时就会说我才是最主要的成分,你是次要成分,所以见到我的时候你要靠边站。
  2. flex-grow,grow英文意思是<扩大,扩展,增加>,这就代表当父元素的宽度大于子元素宽度之和时,并且父元素有剩余,这时,flex-grow就会说我要成长,我要长大,怎么样才能成长呢,当然是分享父元素的空间了。
  3. flex-shrink, shrink英文意思是<收缩>,这就代表当父元素的宽度小于子元素宽度之和时,并且超出了父元素的宽度,这时,flex-shrink就会说外面的世界太苦了,我还是回到父亲的怀抱中去吧!因此,flex-shrink就会按照一定的比例进行收缩。

这三个属性可以使用flex:[grow] [shrink] [basis]进行缩写。如下:

  1. flex:none 等同于 flex:0 0 auto 等同于flex-grow: 0;flex-shrink: 0;flex-basis: auto;
  2. flex:auto 等同于flex:1 1 auto
  3. flex: 1 等同于 flex:1 1 0%
  4. flex: 2 等同于 flex:2 1 0%
  5. flex: 10%  等同于 flex:1 1 10%
  6. flex: 2 3 等同于 flex:2 3 0%,等同于 flex-grow: 2; flex-shrink: 3; flex-basis: 0%;
  7. flex: 2 100px 等同于flex:2 1 100px

4.左右定宽,中间自适应

思路:中间 flex = 1, 宽用百分比,左右固定宽,父元素 display:flex。

<div class="container">
    <div class="left"></div>
    <div class="mid"></div>
    <div class="right"></div>
</div>
.container {
	display:flex;
}
.mid{
	flex:1;
	width:100%;
	height: 100px;
	background: royalblue;
}
.left{
	width:100px;
	height: 100px;
	background: rosybrown;
}
.right{
	width:100px;
	height: 100px;
	background: cadetblue;
}

5.等分布局(不知道几等分情况)

思路:父元素 display:flex,子元素 flex:1

<div class="container">
    <div class="left"></div>
    <div class="mid"></div>
    <div class="right"></div>
</div>
.container {
	display: flex;
}
.mid{
	flex:1;
	height: 100px;
	background: royalblue;
}
.left{
	flex:1;
	height: 100px;
	background: rosybrown;
}
.right{
	flex:1;
	height: 100px;
	background: cadetblue;
}

6.三个小盒子在一个大盒子垂直居中排列

思路:在父 dom 里使用 display: flex 和 align-items: center

<div class="box">
	<span class="left"></span>
	<span class="mid"></span>
	<span class="right"></span>
</div>
.box {
	height: 300px;
	width: 300px;
	display: flex;
	background: red;
	align-items: center;
}
.left{
	background: yellow;
	height: 100px;
	width: 50px;
}
.mid{
	background: rebeccapurple;
	height: 200px;
	width: 50px;
}
.right{
	background: green;
	height: 70px;
	width: 50px;
}

7.三个小盒子在一个大盒子垂直排列,水平居中

思路:与上题大致相同,需要父元素加一个 flex-direction: column

.box {
	height: 300px;
	width: 300px;
	display: flex;
	background: red;
	align-items: center;
	flex-direction: column;
}
.left{
	background: yellow;
	height: 80px;
	width: 150px;
}
.mid{
	background: rebeccapurple;
	height: 20px;
	width: 100px;
}
.right{
	background: green;
	height: 70px;
	width: 150px;
}

8.一个小盒子在一个大盒子里居中

思路:父元素flex布局,水平居中align-items:center,垂直居中:justify-content:center

<div class="box">
	<span class="mid"></span>
</div>
.box {
	height: 300px;
	width: 300px;
	display: flex;
	background: red;
	align-items: center;
	justify-content: center;
}

9.左右布局,一侧定宽,一侧自适应撑满

思路:父元素display:flex,子元素一侧固定宽度,一侧百分百。

<div class="main">
	<div class="left">固定宽度300px</div>
	<div class="right">自适应宽度</div>
</div>
.main {
	display: flex;
	height: 100%;
}
.left,
.right {
	height: 100%;
	border: 1px solid red;
	box-sizing: border-box;
}
.left {
	width: 300px;
}
.right {
	width: 100%;
}

10.水平布局,多个盒子(不定),盒子比例16:9,间隔10px,左右贴边,响应式

思路:padding-bottom 的值是百分比形式时,百分比的基数是其所在元素的父元素的宽度而不是高度(同 padding-leftpadding-right 一样)。子元素先均分,然后子元素再套一层子元素,高度为0,padding-bottom值为宽度的9/16。

难点:这道题难点在于盒子数量是不固定的。若盒子固定,则可以利用vw或vh单位来处理。而padding-bottom的内边距百分比特性又是比较偏的一个用法,容易被人忽略。

<div class="box">
	<div class="item">
		<div class="cont"></div>
	</div>
	<div class="item">
		<div class="cont"></div>
	</div>
	<div class="item">
		<div class="cont"></div>
	</div>
	<div class="item">
		<div class="cont"></div>
	</div>
	<div class="item">
		<div class="cont"></div>
	</div>
	<div class="item">
		<div class="cont"></div>
	</div>
</div>
.box {
        width: 100%;
        display: flex;
}
.box .item {
	flex: 1;
	margin-right: 10px;
}
.box .item>.cont{
	width: 100%;
	height: 0;
	padding-bottom: calc(100% / 16 * 9);
	background-color: #03A9F4;
}
.box .item:last-child {
	margin-right: 0;
}

浏览器HTTP的缓存机制详解

2019
15/09

当我们的页面发起资源请求时,浏览器会通过缓存等手段来尽快响应,避免不必要的http消耗,所以我们经常见到:Memory Cache、Disk Cache、Push Cache,现在又多了一种ServiceWorker。我们来简单对比如下:

ServiceWorker

Service worker是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。Service Worker 可以使你的应用先访问本地缓存资源,包括js、css、png、json等多种静态资源。

当我们启用ServiceWorker时,浏览器就会显示我们的资源来自ServiceWorker。Service Worker的缓存有别与其他缓存机制,我们可以自由控制缓存、文件匹配、读取缓存,并且是持续性的。当ServiceWorker没有命中时才会去重新请求数据。

因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 传输协议来保障安全 它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能的三个步骤:

    • 首先需要注册 Service Worker
    • 然后监听 install 事件后就可以缓存需要的文件
    • 最后在下次用户访问的时候,通过拦截请求的方式查询是否存在缓存,如果存在就直接读取缓存文件否则就去请求数据

特点:

  1. 独立于主JavaScript线程(这就意味着它的运行丝毫不会影响我们主进程的加载性能)
  2. 设计完全异步,大量使用Promise(因为通常Service Worker通常会等待响应后继续,Promise再合适不过了)
  3. 不能访问DOM,不能使用XHR和localStorage
  4. Service Worker只能由HTTPS承载(出于安全考虑)

详见:前端缓存之:Service Worker | 码农备忘录 (zuifengyun.com)

当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

Memory Cache

Memory Cache为内存中的缓存,读取内存中的资源是非常快的。 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束。内存读取虽然快且高效,但它是短暂的,当浏览器或者tab页面关闭,内存就会被释放了。而且内存占用过多会导致浏览器占用计算机过大内存。

  • 内存中的缓存:读取高效、持续时效短,会随着进程的释放而释放
  • 在缓存资源时并不关心返回资源的请求头 Cache-Control 的值是什么,同时资源的匹配也并非仅仅是对 URL 做判断,还可能会对 Content-Type、Cors 等其他特征做校验
  • 普通刷新 F5 操作,优先使用 Memory Cache

Disk Cache

Disk Cache 将资源存储在硬盘中,读取速度次于Memory Cache。

优点:可长期存储,可定义时效时间、容量大;缺点:读取速度慢。

根据http header 请求头触发的缓存策略来做缓存,包含强缓存和协商缓存。

  • 硬盘中的缓存:对比 Memory Cache 读取速度慢,但是容量大、缓存时效长
  • 它根据 HTTP Header 中的字段判断哪些资源需要被缓存、哪些资源可以不请求直接使用、哪些资源已经过期需要重新请求。即使在跨站点的情况下相同地址的资源一旦被硬盘缓存下来就不会再次去请求数据
  • 对于大文件来说大概率在硬盘中缓存,反之内存缓存优先;当前系统内存使用率高的情况下文件优先存储进硬盘

Push Cache

当以上三种缓存都没有命中时才会使用Push Cache。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂。这种缓存一般用不到。

我们通过控制台network,最直观的查看资源用的缓存方式。如图所示:

缓存过程

meta标签设置http-equiv

http-equiv属性可以被称作http响应头,可对整个页面进行缓存设置。

注意:设置meta标签只缓存页面内容,不会缓存页面引用的资源文件。

<!--设定网页缓存时效-->
<meta http-equiv="Cache-Control" content="max-age=7200" />
<!--设定网页的到期时间-->
<meta http-equiv="Expires" content="Mon, 20 Jul 2009 23:00:00 GMT" />

用法:一般用于静态HTML页面缓存。

缺点:浏览器支持情况不同。expires支持较好。Cache-Control不一定管用。

强缓存和协商缓存

浏览器的缓存分为强缓存协商缓存,当客户端请求某个资源的时候,获取缓存的流程如下:

  1. 浏览器第一次打开一个网页获取资源后,根据返回的header(响应头)信息来告诉如何缓存资源。响应头携带(Last-Modified)内容最后修改时间。
  2. 下次请求,先根据这个资源的http header(响应头)判断它是否命中强缓存cache-controlexpires信息),如果命中,则直接从本地缓存中获取资源(包括缓存header信息),未命中则向服务器请求资源。
  3. 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过第一次请求返回的请求头字段信息If-Modified-SinceEtag/If-None-Match)验证这个资源是否命中协商缓存,这个过程成为http再验证,如果命中,服务器直接返回请求(返回新的响应头信息更新缓存中的对应header信息)而不返回资源,告诉客户端从缓存中获取,客户端收到返回后就直接从客户端缓存获取资源。协商缓存返回的状态码一般为304。
  4. 强缓存和协商缓存的共同之处在于:如果命中缓存,服务器不会返回资源;区别是:强缓存不发送请求到服务器(不与服务器通信),但是协商缓存会发送请求到服务器
  5. 当协商缓存没有命中时,服务器会返回资源给客户端。
  6. 当ctrl+F5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存
  7. 当F5刷新页面时,跳过强缓存但会检查协商缓存。

浏览器第一次请求

浏览器后续请求

强制刷新ctrl + f5 ,请求头header带有Cache-control:no-cache(为了兼容,还带了 Pragma: no-cache),同时不带有if-not-match / If-Modified-Since,所以请求服务器协商缓存会被当做失效,返回200和最新内容。

强缓存相关header字段

强缓存可以通过设置两种响应头实现:Expires 和 Cache-Control。

  1. Expires策略:数据缓存到期时间,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。Expires设置失效时间,精确到时分秒。不过Expires 是HTTP 1.0的东西,现在浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
  2. Cache-control策略(重点):Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。同时设置的话,其优先级高于Expires。可以指定一个 max-age 字段,表示缓存的内容将在一定时间后失效。比如:Cache-Control:max-age=300 时,则代表在这个请求正确返回的5分钟内再次加载资源,就会命中强缓存。

http协议头Cache-Control

值可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age,各个消息中的指令含义如下:

  1. Public指示响应可被任何缓存区缓存。
  2. Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当前用户的部分响应消息,此响应消息对于其他用户的请求无效。
  3. no-cache指示请求或响应消息不能缓存
  4. no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
  5. max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
  6. min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
  7. max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

协商缓存相关的header字段

协商缓存可以通过设置两种响应头实现:Last-Modified 和 ETag 。

Last-Modifued/If-Modified-Since和Etag/If-None-Match这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modifued或者Etag),则后续请求会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modifued或者Etag字段,则请求头也不会有对应字段。

Last-Modified/If-Modified-Since要配合Cache-Control使用。

  1. Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
  2. If-Modified-Since:当资源过期时(浏览器判断Cache-Control标识的max-age过期),发现响应头具有Last-Modified声明,则再次像服务器请求时带上头if-modified-since,表示请求时间。服务器收到请求后发现有if-modified-since则与被请求资源的最后修改时间进行对比(Last-Modified),若最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;若最后修改时间较旧(小),说明资源无新修改,响应HTTP 304 走缓存。

Etag/If-None-Match也要配合Cache-Control使用

  1. Etag:服务器响应时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
  2. If-None-Match:当资源过期时,浏览器发现响应头里有Etag,则再次像服务器请求时带上请求头if-none-match(值是Etag的值)。服务器收到请求进行比对,决定返回200或304
    为什么既有Last-Modified还有Etag(两者为什么并存,有什么好处)

使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
  3. 某些服务器不能精确的得到文件的最后修改时间。

这时,利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

协商缓存生效,返回304和Not Modified:

协商缓存失效,返回200和请求结果:

用户的行为对缓存的影响

强缓存

  • 不会向服务器发送请求,直接从缓存中读取资源,可以设置两种 HTTP Header 来实现:Expires、Cache-Control

Expires

  • 缓存过期时间用来指定资源的过期时间,是服务器端的具体的时间点,结合 last-modified 来使用
  • 是 HTTP/1 的产物,受限于本地时间,如果本地时间修改可能会造成缓存失效

Cache-Control

  • 可以在请求头或响应头中来设置,多个指令配合使用达到多个目的
  • 是 HTTP/1.1 的产物,如果和 Expires同时存在,那么它的优先级要高,所以 Expires 的存在成为了一种兼容性的写法

协商缓存

  • 强缓存的依据来自于缓存是否过期,而不关心服务端文件是否已经更新,这可能会导致加载的文件不是服务端最新的内容,此时我们就需要用到协商缓存
  • 协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发送请求,由服务器根据缓存标识决定是否使用缓存的过程,主要分两种情况
    • 协商缓存生效返回 304 和 Not Modified
    • 协商缓存失效返回 200 和请求的结果
  • 协商缓存可以通过设置两种 HTTP Header 来实现:Last-Modified 和 ETag

Last-Modified

  • 浏览器在第一次返回资源时,在响应头中添加 Last-Modified 的 header,值是这个资源在服务器上的最后的修改时间浏览器接收后缓存文件和 header
  • 浏览器下一次请求这个资源,浏览器检测到有 Last-Modified 这个 header,于是添加 If-Modified-Since 这个 header,值就是 Last-Modified 中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回 304 和空的响应体,直接从缓存读取,如果 If-Modified-Since 的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和 200
  • Last-Modified 的弊端:
    • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
    • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETag 和If-None-Match

ETag

  • ETag 是服务器响应请求时,返回当前资源文件的一个唯一标识,只要资源有变化 ETag 就会重新生成
  • 浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET 200 回包形式将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回 304 知会客户端直接使用本地缓存即可。
  • Last-Modified 和 ETag 的区别:
    • 精度上 ETag 优于 Last-Modified
    • 性能上 ETag 逊于 Last-Modified
    • 优先级上服务器优先考虑 ETag

缓存机制

  • 强制缓存优先于协商缓存进行,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存,协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。
  • 如果什么缓存策略都没设置,那么浏览器会怎么处理?对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
  • 浏览器缓存分为强缓存和协商缓存,两者有两个比较明显的区别:
    • 如果浏览器命中强缓存,则不需要给服务器发请求;而协商缓存最终由服务器来决定是否使用缓存,即客户端与服务器之间存在一次通信。
      在 chrome 中强缓存(虽然没有发出真实的 http 请求)的请求状态码返回是 200 (from cache);
    • 而协商缓存如果命中走缓存的话,请求的状态码是 304 (not modified)。 不同浏览器的策略不同,在 Fire Fox中,from cache 状态码是 304.

实际应用场景

  • 频繁变动的资源:Cache-Control: no-cache
  • 不常变化的资源:Cache-Control: max-age=31536000,一年的有效期,比如 jQuery 类库基本不换化

用户行为对浏览器缓存的影响

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

前端算法:JavaScript实现字符串全排列

2019
25/05

我个人认为前端工程师笔试题中,算法题一般不难,也就这个难度,甚至比这还要简单。这是我在笔试过程中遇到的一个题~下面分享一下解题思路。

大体结构:定义一个方法,传入str变量,返回一个数组,包含所有排列:

function fun(str){ 
   var result = []; 
   return result;
}

主要逻辑:肯定是需要递归的~先将第一个字母取出,然后将剩下的字符串全排列。将这个字母,依次插入到每个排列所有缝隙。 如:abc进行全排列,取出a,得到全排列bc和cb,先向bc插,可以得到abc,bac,bca;再向cb插,得到acb,cab,cba;

if(str.length == 1 || str.length == 0 ){
   result.push(str);
   return result;
}else{
   var one = str.substr(0,1);
   var left = str.substr(1);
   var leftResult = fun(left);
   for(i=0;i<leftResult.length;i++){
      for(j=0;j<leftResult[i].length+1;j++){//加1的目的是让字符one也可以插入到最后一个位置
         result.push(leftResult[i].slice(0,j) + one + leftResult[i].slice(j));
      }
   }
}

这样就能实现字符串的全排列啦~

思路有了,可以用数组的迭代方法来实现:

const anagrams = str => {
   if (str.length <= 2) return str.length === 2 ? [str, str[1] + str[0]] : [str];
   return str.split('').reduce((acc, letter, i) =>acc.concat(anagrams(str.slice(0, i) + str.slice(i + 1)).map(val => letter + val)), [])
};