ES6之includes方法详解及其与indexOf区别

2020
25/03

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016引入了该方法。

includes() 方法用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false

includes()方法在字符串中使用时,相当于indexOf(),查询成功返回true,失败返回false

'abc'.includes('ab') // true
'abc'.includes('d') // false

在数组中使用时,可以查询某个元素是否包含在数组中(只能查询Number,String类型的元素)

[1 , 2, 3].includes(0) // false
['1' , '2', '3'].includes('1') // true
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true

该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4, 但数组长度为3),则会重置为0开始。

[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true

没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

if (arr.indexOf(el) !== -1) {
    // ...
}

indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等(Strict Equality Comparison )运算符进行判断,这会导致对NaN的误判。includes使用的是不一样的判断算法(SameValueZero策略),就没有这个问题。

[NaN].indexOf(NaN) // -1
[NaN].includes(NaN) // true

下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

const contains = (() => Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    :(arr, value) => arr.some(el => el === value)
)()

另外,MapSet数据结构有一个has方法需要注意与includes区分。

  1. Map结构的has方法,是用来查找键名的,比如Map.prototype.has(key),WeakMap.prototype.has(key), Reflect.has(target, propertyKey)
  2. Set结构的has方法,是用来查找值的,比如Set.prototype.has(value),WeakSet.prototype.has(value)

DOM之事件触发、冒泡、捕获详解

2020
21/03

DOM触发事件方法

1.html attribute 标签中添加事件属性

<input value="Click me" onclick="alert('Click!')" type="button">
js逻辑在属性里创建,不是很好,写到一个方法里.
<script>
  function countRabbits() {
    for(let i=1; i<=3; i++) {
      alert("Rabbit number " + i);
    }
  }
</script>

<input type="button" onclick="countRabbits()" value="Count rabbits!">
因为html属性不区分大小写,所以你onClick可以随你写,ONCLICK, onCLICK, 最好是onclick.

2.dom property 元素属性赋值

<input id="elem" type="button" value="Click me">
<script>
  elem.onclick = function() {
    alert('Thank you');
  };
</script>
这个方式直接在dom上绑定,上面的html-attribute是浏览器读取他创建函数对象写入dom中。

处理程序始终位于dom property中:html attribute 只是初始化它的一个方法,并且也不被推荐。

如果html attributre 存在事件绑定,并且dom也存在事件绑定,相同的事件将被dom中的给替代。

直接给元素加事件属性(DOM0级模型)是注册事件最简单的方法,将处理程序赋值给相应的事件属性(事件前面带on)。这种方式适用于所有浏览器。但是缺点是元素将最多只有一个事件处理程序。

dom绑定的一些写法.

<input id="elem" type="button" value="Click me">
<script>
  //1
  elem.onclick = function() {
    alert('Thank you');
  }; 
  
  //2
  function thank() {
    alert('thank you')
  }
  elem.onclick = thank;
</script>

// 但是如果是input中也是不同的。
<input id="elem" type="button" value="Click me" onclick="thank()">

3.addEventListener

事件处理程序的注册方法为addEventListener(),它接受3个参数(事件类型,事件回调函数,布尔值),其中最后一个参数默认值为false,将会把程序注册为冒泡事件处理程序。而其若为true,则为捕获事件处理程序。两者的区别是:若为冒泡,表示在冒泡阶段调用事件处理程序,逐渐冒泡到外层。而捕获恰好相反,在捕获阶段调用处理程序,执行顺序相反。在IE中只支持事件冒泡。移除事件使用removeEventListener(),传参如上。

IE8及以下浏览器不支持此方法,使用attachEvent(on+事件类型, 事件回调函数)方法,只有两个参数(不支持事件捕获)。移除事件使用detachEvent()方法,参数相同。

<button id="elem">Click me</button>

<script>
  elem.addEventListener('click', {
    handleEvent(event) {
      alert(event.type + " at " + event.currentTarget);
    }
  });
</script>
上面的方法使用到了handleEvent, 如果发生事件,handleEvent会被调用。

也可以那么说,当addEventListener接收到处理程序对象时,事件会去调用object.handleEvent(event)

等同于:

<button id="elem">Click me</button>

<script>
  elem.addEventListener('click',function(event) {
      alert(event.type + " at " + event.currentTarget);
  });
</script>

如下,很实用的方法。

<button id="elem">Click me</button>

<script>
  class Menu {
    handleEvent(event) {
      // mousedown -> onMousedown
      let method = 'on' + event.type[0].toUpperCase() + event.type.slice(1);
      this[method](event);
    }

    onMousedown() {
      console.log(1)
    }

    onMouseup() {
      console.log(2)
    }
  }

  let menu = new Menu();
  elem.addEventListener('mousedown', menu);
  elem.addEventListener('mouseup', menu);
</script>

注意:dom添加赋值事件属性方式(普通事件)可以被第二次调用的同一个事件覆盖,而addEventListener事件绑定不会被覆盖,而是会依次执行。

事件流

事件流:事件流是描述从页面中接收事件的顺序。也可以说是事件传播的顺序。

“DOM2事件流”规定的事件流包括三个阶段:

  1. 事件捕获阶段:提供了在事件没有送达目标之前查看事件的机会。与冒泡方向相反,由父向子逐级捕获。
  2. 处于目标阶段:目标对象(元素)本身的时间处理程序调用(执行)。
  3. 事件冒泡阶段。目标元素的事件大部分都会冒泡(传播)到DOM树根。有一些特殊的事件不会冒泡,比如mouseenter和mouseleave事件不会冒泡。若要冒泡,可以使用mouseover和mouseout替代。

事件模型

js包含三种事件模型:DOM0事件模型(始事件模型),DOM2事件模型,IE事件模型

  1. DOM0级模型:事件不会流动传播。事件绑定监听函数方式:① html标签中添加属性如<div onclick="fun()"></div>;② js中获取dom元素后给元素加事件属性:document.onclick = fn,取消监听使用document.onclick = null
  2. IE事件模型:使用attachEvent("onclick", handler)detachEvent("onclick", handler)方法进行绑定事件和移除绑定。事件流只有2个阶段,没有事件捕获阶段。
  3. DOM2级模型:addEventListener()removeEventListener()进行监听和取消。事件流有3个阶段。

事件冒泡

就是一个事件(例如点击),这个点击会往上层元素进行冒泡(往上进行),他是从点击的这个元素(目标元素,即event.target)开始向上去冒泡。

这里开始,先搞清两个点event.targetevent.currentTarget

event.target是当前点击的元素(目标元素)

event.currentTarget也就是this, 是绑定函数操作的元素(addEventListener绑定的元素,有可能是目标元素的父元素)

有的时候你可能并不想他去冒泡,可以通过stopPropagation这个方法去干掉他。

  • event.stopPropagation()

这个方法有个地方需要注意一下,那就是当一个事件有多个处理程序的时候,他只会停止当前程序的冒泡,其他的程序不会收到影响,需要解决这个问题,就需要使用到event.stopImmediatePropagation方法。

尽量不要阻止事件冒泡,除非你知道你自己在干什么。

当你调用return false时会做 3 件事:

  • event.preventDefault() – 它停止浏览器的默认行为。
  • event.stopPropagation() – 它阻止事件传播(或“冒泡”)。
  • 停止回调执行并立即返回。

事件捕获

事件捕获和事件冒泡是不同的,捕获是从上而下。如果需要捕获事件,那就需要将addEventListener的第三个参数(叫做useCapture)设置成true.

默认的false是在冒泡阶段处理事件,true就是捕获阶段处理事件。看下面的代码:

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>
运行之后,发现两个顺序是相反的。而且点击的元素是位于捕获阶段的最后,冒泡阶段的开始。

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;
}