0%

一、是什么

JavaScript中,new操作符用于创建一个给定构造函数的实例对象

例子

1
2
3
4
5
6
7
8
9
10
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name)
}
const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
person1.sayName() // 'Tom'

从上面可以看到:

  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数中的属性
  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来)

现在在构建函数中显式加上返回值,并且这个返回值是一个原始类型

1
2
3
4
5
6
function Test(name) {
this.name = name
return 1
}
const t = new Test('xxx')
console.log(t.name) // 'xxx'

可以发现,构造函数中返回一个原始值,然而这个返回值并没有作用

下面在构造函数中返回一个对象

1
2
3
4
5
6
7
8
function Test(name) {
this.name = name
console.log(this) // Test { name: 'xxx' }
return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'

从上面可以发现,构造函数如果返回值为一个对象,那么这个返回值会被正常使用

二、流程

从上面介绍中,我们可以看到new关键字主要做了以下的工作:

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的this绑定到新建的对象obj
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理

举个例子:

1
2
3
4
5
6
7
function Person(name, age){
this.name = name;
this.age = age;
}
const person1 = new Person('Tom', 20)
console.log(person1) // Person {name: "Tom", age: 20}
person1.sayName() // 'Tom'

手写new 操作符

1
2
3
4
5
6
function my new(Func,..args){
const obj={};
obj.__proto__=Func.prototype;
let result=Func.apply(obj,args);
return result instanceOf Object?result:obj
}

原型链:obj->Func.prototype->Object.prototype->null

当无返回值或者返回值不是对象,则obj作为新的对象返回,否则将返回值作为新的对象返回,当构造函数return的是简单的基本数据类型(undefined,数字,字符串,布尔值)能够正确new出想要的对象,如果构造函数return的是对象,那么我们new完得不到想要的对象

一、是什么

我们了解到,Node 采用了事件驱动机制,而EventEmitter 就是Node实现事件驱动的基础

EventEmitter的基础上,Node 几乎所有的模块都继承了这个类,这些模块拥有了自己的事件,可以绑定/触发监听器,实现了异步操作

Node.js 里面的许多对象都会分发事件,比如 fs.readStream 对象会在文件被打开的时候触发一个事件

这些产生事件的对象都是 events.EventEmitter 的实例,这些对象有一个 eventEmitter.on() 函数,用于将一个或多个函数绑定到命名事件上

二、使用方法

Node events模块只提供了一个EventEmitter类,这个类实现了Node异步事件驱动架构的基本模式——观察者模式

在这种模式中,被观察者(主体)维护着一组其他对象派来(注册)的观察者,有新的对象对主体感兴趣就注册观察者,不感兴趣就取消订阅,主体有更新的话就依次通知观察者们

基本代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
const EventEmitter = require('events')

class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter()

function callback() {
console.log('触发了event事件!')
}
myEmitter.on('event', callback)
myEmitter.emit('event')
myEmitter.removeListener('event', callback);

通过实例对象的on方法注册一个名为event的事件,通过emit方法触发该事件,而removeListener用于取消事件的监听

关于其常见的方法如下:

  • emitter.addListener/on(eventName, listener) :添加类型为 eventName 的监听事件到事件数组尾部
  • emitter.prependListener(eventName, listener):添加类型为 eventName 的监听事件到事件数组头部
  • emitter.emit(eventName[, …args]):触发类型为 eventName 的监听事件
  • emitter.removeListener/off(eventName, listener):移除类型为 eventName 的监听事件
  • emitter.once(eventName, listener):添加类型为 eventName 的监听事件,以后只能执行一次并删除
  • emitter.removeAllListeners([eventName]): 移除全部类型为 eventName 的监听事件

三、实现过程

通过上面的方法了解,EventEmitter是一个构造函数,内部存在一个包含所有事件的对象

1
2
3
4
5
class EventEmitter {
constructor() {
this.events = {};
}
}

其中events存放的监听事件的函数的结构如下:

1
2
3
4
5
{
"event1": [f1,f2,f3],
"event2": [f4,f5],
...
}

然后开始一步步实现实例方法,首先是emit,第一个参数为事件的类型,第二个参数开始为触发事件函数的参数,实现如下:

1
2
3
4
5
emit(type, ...args) {
this.events[type].forEach((item) => {
Reflect.apply(item, this, args);
});
}

当实现了emit方法之后,然后实现onaddListenerprependListener这三个实例方法,都是添加事件监听触发函数,实现也是大同小异

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
on(type, handler) {
if (!this.events[type]) {
this.events[type] = [];
}
this.events[type].push(handler);
}

addListener(type,handler){
this.on(type,handler)
}

prependListener(type, handler) {
if (!this.events[type]) {
this.events[type] = [];
}
this.events[type].unshift(handler);
}

紧接着就是实现事件监听的方法removeListener/on

1
2
3
4
5
6
7
8
9
10
removeListener(type, handler) {
if (!this.events[type]) {
return;
}
this.events[type] = this.events[type].filter(item => item !== handler);
}

off(type,handler){
this.removeListener(type,handler)
}

最后再来实现once方法, 再传入事件监听处理函数的时候进行封装,利用闭包的特性维护当前状态,通过fired属性值判断事件函数是否执行过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
once(type, handler) {
this.on(type, this._onceWrap(type, handler, this));
}

_onceWrap(type, handler, target) {
const state = { fired: false, handler, type , target};
const wrapFn = this._onceWrapper.bind(state);
state.wrapFn = wrapFn;
return wrapFn;
}

_onceWrapper(...args) {
if (!this.fired) {
this.fired = true;
Reflect.apply(this.handler, this.target, args);
this.target.off(this.type, this.wrapFn);
}
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class EventEmitter {
constructor(){
this.events={};
}
on(type,handler){
if(!this.events[type]){
this.events[type]=[]
}
this.events[type].push(handler)
}
addListener(type,handler){
this.on(type,handler)

}
prependListener(type,handler){
if(!this.events[type]){
this.events[type]=[]
}
this.events[type].unshift(handler)
}
removeListener(type,handler){
if(!this.events[type]){
return;
}
this.events[type]=this.events[type].filter(item=>item!==handler)
}
off(type,handler){
this.removeListener(type,handler)
}
emit(type,...args){
this.events[type].forEach(item=>{
Reflect.apply(item,this,args);
})
}
once(type,handler){
//把监听器注销,即利用闭包,在内部函数中销毁了外部函数的引用
let temp=(...args)=>{
handler(args);
this.off(type,temp)
}
this.on(event,temp)
}
}

是什么

本质上是优化高频率执行代码的一种手段

如:浏览器的 resizescrollkeypressmousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能

为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用throttle(节流)和debounce(防抖)的方式来减少调用频率

定义

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后再执行该事件,若在 n 秒内被重复触发,则重新计时

节流:

用时间戳实现:

1
2
3
4
5
6
7
8
9
10
11
12

function throttled(fn,delay=500){
let oldTime=new Date.now();
return function(...args){
let context=this;
let newTime=new Date.now();
if(newTime-oldTime>=delay){
fn.apply(context,args);
oldTime=new Date.now();
}
}
}

用定时器实现

1
2
3
4
5
6
7
8
9
10
11
12
function throttled(fn,delay=500){
let timer=null;
return function(..args){
let context=this;
if(!timer){//n秒内只运行一次,若在n秒内重复触发,只有一次生效
timer=setTimeout(()=>{
fn.apply(context,args);
timer=null;
},delay)
}
}
}

防抖:

1
2
3
4
5
6
7
8
9
10
function debounce(fn,delay=500){
let timer;
return function(...args){
let context=this;
clearTimeout(timer);
timer=setTimeout(()=>{
fn.apply(context,args);//n秒内只能触发一次,若在n秒内重复触发,都会被清除,重新计时
},delay)
}
}

立即执行的防抖函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function debounce(fn,delay=500,immediate){
let timer;
return function(...args){
let context=this;
if(timer)clearTimeout(timer);
if(immediate){
immediate=!immediate;
fn.apply(context,args);
}else{

timer=setTimeout(()=>{
fn.apply(context,args);
},delay)
}
}
}

区别

相同点:

  • 都可以通过使用 setTimeout 实现
  • 目的都是,降低回调执行频率。节省计算资源

不同点:

  • 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
  • 函数防抖关注一定时间连续触发的事件,因为会清除定时器,只在最后执行一次,而函数节流一段时间内只执行一次,不会清除定时器,因此会在隔一段时间执行一次。

例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次

应用场景

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

是什么

在HTML中,每个元素可以理解为一个盒子,在浏览器解析过程中,会涉及到回流和重绘:

  • 回流:布局引擎会根据各种样式计算每个盒子在页面上的大小与位置
  • 重绘:当计算好盒模型的位置,大小及其他属性后,浏览器根据每个盒子特性进行绘制

  • 解析HTML,生成DOM树,解析CSS,生成CSSOM树
  • 将DOM树和CSSOM树结合,生成渲染树Render Tree
  • Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
  • Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
  • Display:将像素发送给GPU,展示在页面上

当我们对DOM的修改引发了DOM几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来),即回流。

当对DOM的修改导致样式变化(color或background-color)却为影响几何属性,浏览器不需要重新计算元素的几何苏醒,直接为该元素绘制新的样式,这里仅仅触发重绘。

如何触发

要想减少回流和重绘的次数,首先要了解回流和重绘是如何触发的

回流触发时机

回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流,如下面情况:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距,内边框,边框大小,高度,宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 页面一开始渲染(不可避免)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小)

还有一些容易被忽略的操作:获取一些特定属性的值

offsetTop,offsetLetf,offsetWidth,offsetHeight,scrollTop,scrollLeft,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight

这些属性有一个共性,就是需要通过即时计算得到,因此浏览器为了获取这些值,也会进行回流,除此之外getComputedStyle方法的原理也一样

重绘触发时机:

触发回流一定会触发重绘

引起重绘·的一些其他行为:

  • 颜色修改
  • 阴影修改

浏览器优化机制

由于每次重排都会造成额外的计算消耗,因此大多数浏览器会通过队列化修改并批量执行来优化重排过程,浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列

当获取布局信息操作时,会强制队列刷新,包括前面讲到的offsetTop等方法都会返回最新的数据,因此浏览器不得不清空队列,触发回流重绘来返回正确的值

如何减少

  • 如果设定元素的样式,通过修改元素的class类型(尽可能在DOM树的最里层)
  • 避免设置多项内联样式
  • 应用元素的动画,使用position属性为fixed或absolute的元素
  • 避免使用table布局,table布局中每个元素的大小以及内容的改动,都会导致整个table的重新计算
  • 对于那些复杂动画,对其设置position:fixed/absolute,尽可能地使元素脱离文档流,从而减少对其他元素的影响
  • 使用css3硬件加速,可以让transform,opacity,filters这些动画不会引起回流重绘
  • 避免使用CSS的JavaScript表达式

在使用JavaScript动态插入多个节点时,可以使用DocumentFragment,创建最后一次插入,就能避免多次的渲染性能

例如,多次修改一个元素布局时,

1
2
3
4
5
6
const el=document.getElementById("el")
for(let i=0;i<10;i++){
el.style.top=el.offsetTop+10+"px";
el.style.left=el.offsetleft+10+"px"
}

每次循环都需要获取多次offset属性,比较糟糕,可以使用变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求

1
2
3
4
5
6
7
8
9
const el=document.getElementById("el")
//先缓存offsetLeft和offsetTop的值
let offLeft=el.offsetLeft,offTop=el.offsetTop
for(let i=0;i<10;i++){
offLeft+=10
offTop+=10
}
el.styel.left=offLeft+"px";
el.style.top=offTop+"px"

我们还可避免修改样式,使用类名去合并样式

1
2
3
4
5
const container=document.getElementById("container")
container.style.width='100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

使用类名去合并样式

1
2
3
4
5
6
7
8
9
10
11
12
1<style>
2 .basic_style {
3 width: 100px;
4 height: 200px;
5 border: 10px solid red;
6 color: red;
7 }
8</style>
9<script>
10 const container = document.getElementById('container')
11 container.classList.add('basic_style')
12</script>

前者每次单独操作,都去触发一次渲染树更改(新浏览器不会),

都去触发一次渲染树更改,从而导致相应的回流与重绘过程

合并之后,等于我们将所有的更改一次性发出

我们还可以通过通过设置元素属性display: none,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作

1
2
3
4
5
const container = document.getElementById('container')
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'

离线操作后

1
2
3
4
5
6
7
8
let container = document.getElementById('container')
container.style.display = 'none'
container.style.width = '100px'
container.style.height = '200px'
container.style.border = '10px solid red'
container.style.color = 'red'
...(省略了许多类似的后续操作)
container.style.display = 'block'

转载自 zhangxinxu from http://www.zhangxinxu.com

一、现象描述

真正意义上的inline-block水平呈现的元素间,换行显示或空格分隔的情况下会有间距.

我们使用CSS更改非inline-block水平元素为inline-block水平,也会有该问题:

1
2
3
4
5
space a {
display: inline-block;
padding: .5em 1em;
background-color: #cad5eb;
}
1
2
3
4
5
<div class="space">
<a href="##">惆怅</a>
<a href="##">淡定</a>
<a href="##">热血</a>
</div>

这种表现是符合规范的应该有的表现。

元素被当成行内元素排版的时候,元素之间的空白符(空格、回车换行等)都会被浏览器处理,根据white-space的处理方式(默认是normal,合并多余空白),原来HTML代码中的回车换行被转成一个空白符,在字体不为0的情况下,空白符占据一定宽度,所以inline-block的元素之间就出现了空隙。这些元素之间的间距会随着字体的大小而变化,当行内元素font-size:16px时,间距为8px。

二、方法之移除空格

元素间留白间距出现的原因就是标签段之间的空格,因此,去掉HTML中的空格,自然间距就木有了。考虑到代码可读性,显然连成一行的写法是不可取的,我们可以:

1
2
3
4
5
<div class="space">
<a href="##">惆怅</a
><a href="##">淡定</a
><a href="##">热血</a>
</div>
1
2
3
4
5
<div class="space">
<a href="##">惆怅</a><!--
--><a href="##">淡定</a><!--
--><a href="##">热血</a>
</div>

使用margin负值

1
2
3
4
1.space a {
2 display: inline-block;
3 margin-right: -3px;
4}

margin负值的大小与上下文的字体和文字大小相关

使用font-size:0

类似下面的代码:

1
2
3
4
5
6
1.space {
2 font-size: 0;
3}
4.space a {
5 font-size: 12px;
6}

这个方法,基本上可以解决大部分浏览器下inline-block元素之间的间距(IE7等浏览器有时候会有1像素的间距)。

使用letter-spacing

类似下面的代码:

1
2
3
4
5
6
1.space {
2 letter-spacing: -3px;
3}
4.space a {
5 letter-spacing: 0;
6}

使用word-spacing

类似下面代码:

1
2
3
4
5
6
1.space {
2 word-spacing: -6px;
3}
4.space a {
5 word-spacing: 0;
6}

一个是字符间距(letter-spacing)一个是单词间距(word-spacing),大同小异。据我测试,word-spacing的负值只要大到一定程度,其兼容性上的差异就可以被忽略。因为,貌似,word-spacing即使负值很大,也不会发生重叠。

与上面demo一样的效果,这里就不截图展示了。如果您使用Chrome浏览器,可能看到的是间距依旧存在。确实是有该问题,原因我是不清楚,不过我知道,可以添加display: table;display:inline-table;让Chrome浏览器也变得乖巧。

1
2
3
4
1.space {
2 display: inline-table;
3 word-spacing: -6px;
4}

问题:

在CSS flex布局中,justify-content属性可以控制列表的水平对齐方式,例如space-between值可以实现两端对齐。

但是,如果最后一行的列表的个数不满,则就会出现最后一行没有完全垂直对齐的问题。

解决:

每一行固定列数:

方法一:模拟space-between和间隙

也就是我们不使用justify-content:space-between声明在模拟两端对齐效果。中间的gap间隙我们使用margin进行控制。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<style>
.container {
display: flex;
flex-wrap: wrap;
}
.list {
width: 24%;
height: 100px;
background-color: skyblue;
margin-top: 15px;
}
.list:not(:nth-child(4n)) {
/* 100%-24%*4=4%,4%/3分配给不是4的倍数的盒子 */
margin-right: calc(4%/3)
}
</style>
<body>
<div class="container">
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
</div>

</body>

方法二:根据个数最后一个元素动态margin

由于每一列的数目都是固定的,因此,我们可以计算出不同个数列表应当多大的margin值才能保证完全左对齐。

例如,假设每行4个元素,结果最后一行只有3个元素,则最后一个元素的margin-right大小是“列表宽度+间隙大小”的话,那最后3个元素也是可以完美左对齐的。

然后,借助树结构伪类数量匹配技术,我们可以知道最后一行有几个元素。

例如:

  • .list:last-child:nth-child(4n - 1)说明最后一行,要么3个元素,要么7个元素……
  • .list:last-child:nth-child(4n - 2)说明最后一行,要么2个元素,要么6个元素……

在本例中,一行就4个元素,因此,我们可以有如下CSS设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.container {
display: flex;
flex-wrap: wrap;
}
.list {
width: 24%;
height: 100px;
background-color: skyblue;
margin-top: 15px;
/* 每个的margin-right默认为4%/3 */
margin-right: calc(4%/4);
}
/* 最后一行3个 则最后一个的margin-right特殊设置*/
.list:last-child:nth-child(4n-1) {
margin-right: calc(24%+4%/3)
}
/* 最后一行两个 */
.list:last-child:nth-child(4n-2){
margin-right: calc(48%+8%/3) ;
}

子项宽度不固定

最后一项margin-right:auto

1
2
3
4
5
6
7
8
9
10
11
12
13
.container {
display:flex;
justify-content: space-between;
flex-wrap: wrap;
}
.list {
bacground-color: skyblue;
margin: 10px;
}
/*最后一项margin-right:auto,让margin-right占据剩余空间*/
.list:last-child {
margin-right:auto;
}

创建伪元素占据剩余空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.container {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.list {
height: 100px;
background-color: skyblue;
margin: 10px;
}
.container::after {
content:'';
flex:1;
}

如果每一行列数不固定

如果每一行的列数不固定,则上面的这些方法均不适用,需要使用其他技巧来实现最后一行左对齐。

这个方法其实很简单,也很好理解,就是使用足够的空白标签进行填充占位,具体的占位数量是由最多列数的个数决定的,例如这个布局最多7列,那我们可以使用7个空白标签进行填充占位,最多10列,那我们需要使用10个空白标签。

如下HTML示意:

1
2
3
4
5
6
7
8
9
10
1<div class="container">
2 <div class="list"></div>
3 <div class="list"></div>
4 <div class="list"></div>
5 <div class="list"></div>
6 <div class="list"></div>
7 <div class="list"></div>
8 <div class="list"></div>
9 <i></i><i></i><i></i><i></i><i></i>
10</div>

相关CSS如下,实现的关键就是占位的<i>元素宽度和margin大小设置得和.list列表元素一样即可,其他样式都不需要写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.container {
2 display: flex;
3 justify-content: space-between;
4 flex-wrap: wrap;
5 margin-right: -10px;
6}
7.list {
8 width: 100px; height:100px;
9 background-color: skyblue;
10 margin: 15px 10px 0 0;
11}
12/* 和列表一样的宽度和margin值 */
13.container > i {
14 width: 100px;
15 margin-right: 10px;
16}

由于<i>元素高度为0,因此,并不会影响垂直方向上的布局呈现。

如果列数不固定HTML又不能调整

然而有时候,由于客观原因,前端重构人员没有办法去调整html结构,同时布局的列表个数又不固定,这个时候该如何实现我们最后一行左对齐效果呢?

我们不妨可以试试使用Grid布局。

Grid布局天然有gap间隙,且天然格子对齐排布,因此,实现最后一行左对齐可以认为是天生的效果。

CSS代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<style>
.container {
display: grid;
justify-content: space-between;
grid-template-columns: repeat(auto-fill,100px);
grid-gap: 10px;
}
.list {
width: 100px;
height: 100px;
background-color: skyblue;
margin-top: 5px;
}
</style>

这几种实现方法点评

首先最后一行需要左对齐的布局更适合使用CSS grid布局实现,但是,repeat()函数兼容性有些要求,IE浏览器并不支持。如果项目需要兼容IE,则此方法需要斟酌。

然后,适用范围最广的方法是使用空的元素进行占位,此方法不仅适用于列表个数不固定的场景,对于列表个数固定的场景也可以使用这个方法。但是有些人代码洁癖,看不惯这种空的占位的html标签,则可以试试一开始的两个方法,一是动态计算margin,模拟两端对齐,另外一个是根据列表的个数,动态控制最后一个列表元素的margin值实现左对齐。

我们在布局一个页面时,通常都会用到水平居中和垂直居中,处理水平居中很好处理,不外乎就是设定margin:0 auto;或是text-align:center;,就可以轻松解决掉水平居中的问题,但一直以来最麻烦对齐问题就是「垂直居中」,以下将介绍几种单纯利用CSS垂直居中的方式,只需要理解背后的原理就可以轻松应用。

下面为公共代码:

1
2
3
4
5
6
7
8
9
10
11
12
<div class="box">
<div class="small">small</div>
</div>
.box {
width: 300px;
height: 300px;
background: #ddd;
}
.small {
background: red;
}

absolute + margin实现

方法一:

1
2
3
4
5
6
7
8
9
10
11
.box {
position: relative;
}
.small {
position: absolute;
top: 50%;
left: 50%;
margin: -50px 0 0 -50px;
width: 100px;
height: 100px;
}

方法二:

margin:auto会自动去计算子元素和父元素之间的边距,并设为居中。所以就会实现上下左右都居中。

1.在普通内容流中,margin:auto的效果等同于margin:0 auto,左右会去极端剩余空间平均分配,而上下默认都为0;

2.position:absolute使绝对定位块跳出了内容流,内容流中的其余部分渲染时绝对定位部分不进行渲染。

3.为块区域设置top: 0; left: 0; bottom: 0; right: 0;将给浏览器重新分配一个边界框,此时该块将填充其父元素的所有可用空间,所以margin 垂直方向上有了可分配的空间。

4.再设置margin 垂直方向上下为auto,即可实现垂直居中。(注意高度得设置)。

1
2
3
4
5
6
7
8
9
10
11
12
13
.box {
position: relative;
}
.small {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
width: 100px;
height: 100px;
}

absolute + calc 实现

1
2
3
4
5
6
7
8
9
10
.box {
position: relative;
}
.small {
position: absolute;
top: calc(50% - 50px);
left: calc(50% - 50px);
width: 100px;
height: 100px;
}

absolute + transform 实现

1
2
3
4
5
6
7
8
9
10
11
12
.box {
position: relative;
}
.small {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%,-50%,0);
width: 100px;
height: 100px;
}

转行内元素

1
2
3
4
5
6
7
8
9
10
11
.box {
line-height:300px;
text-align: center;
}
.small {
padding: 6px 10px;
display:inline-block;
font-size:16px;
vertical-aligin: middle;
line-height: 16px;
}

vertical-align:

在W3C官方中对 vertical-align做了下面的解释:
This property affects the vertical positioning inside a line box of the boxes generated by an inline-level element.
事实上,一个Box中由很多行很多元素组成,vertical-align只作用于在同一行内的元素,它的垂直并不是相对于整个Box而言的。如果把 vertical-align:middle 放到一个单元格元素,即table的td元素中,它的垂直居中显示是没任何问题的,因为它表示相对于改行的垂直高度居中显示。而在我设定的div块中并不只存在一行,因此它无法识别默认显示在顶部。

为了解决这个问题,我找到了两种方法。一个是我们可以设置元素style中的 line-heght 值为其父元素

的height值
,这样 vertical-align:middle 就会使元素内容垂直居中。 另外还有种方法,就是将要设置垂直居中的元素的父元素style属性添加 display:table-cell 将其作为单元格显示,这样使用 vertical-align:middle 也可以实现垂直居中

转行内元素并且使用table-cell

1
2
3
4
5
6
7
8
9
10
11
12
13
.box {
display: table-cell;
height: 300px;
width: 300px;
text-align: center;
background: green;
vertical-align: middle;
}
.small {
padding: 6px 10px;
display:inline-block;
background-color: aliceblue;
}

flex

方法一:

1
2
3
4
5
.box {
display: flex;
justify-content: center;
align-items: center;
}

方法二:

1
2
3
4
5
6
7
.box {
display: flex;
justify-content: center;
}
.small {
align-self: center;
}

grid布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.box {
display: grid;
height: 300px;
width: 300px;
justify-items: center;
align-items: center;
background-color: #000;

}
.small {
width: 100px;
height: 100px;
background-color: #fff;
}

方法二:

1
2
3
4
5
6
7
.box {
display: grid;
}
.small {
justify-self: center;
align-self: center;
}

方法三:

1
2
3
4
5
6
7
8
.box {
display: grid;
justify-items: center;
}
.small {
align-self: center;
}

方法四:

1
2
3
4
5
6
7
.box {
display: grid;
align-items: center;
}
.small {
justify-self: center;
}

一、常见定位方案

在讲 BFC 之前,我们先来了解一下常见的定位方案,定位方案是控制元素的布局,有三种常见方案:

  • 普通流 (normal flow)

在普通流中,元素按照其在 HTML 中的先后位置至上而下布局,在这个过程中,行内元素水平排列,直到当行被占满然后换行,块级元素则会被渲染为完整的一个新行,除非另外指定,否则所有元素默认都是普通流定位,也可以说,普通流中元素的位置由该元素在 HTML 文档中的位置决定。

  • 浮动 (float)

在浮动布局中,元素首先按照普通流的位置出现,然后根据浮动的方向尽可能的向左边或右边偏移,其效果与印刷排版中的文本环绕相似。

  • 绝对定位 (absolute positioning)

在绝对定位布局中,元素会整体脱离普通流,因此绝对定位元素不会对其兄弟元素造成影响,而元素具体的位置由绝对定位的坐标决定。

二、BFC 概念

Formatting context(格式化上下文) 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。

那么 BFC 是什么呢?

BFC 即 Block Formatting Contexts (块级格式化上下文),它属于上述定位方案的普通流。

**具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。
**

通俗一点来讲,可以把 BFC 理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。

定位方案:

  • 内部的Box会在垂直方向上一个接一个放置。
  • Box垂直方向的距离由margin决定,属于同一个BFC的两个相邻Box的margin会发生重叠。
  • 每个元素的margin box 的左边,与包含块border box的左边相接触。
  • BFC的区域不会与float box重叠。
  • BFC是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。
  • 计算BFC的高度时,浮动元素也会参与计算。

三、触发 BFC

只要元素满足下面任一条件即可触发 BFC 特性:

  • html 根元素
  • 浮动元素:float 除 none 以外的值
  • 绝对定位元素:position (absolute、fixed)
  • display 为 inline-block、table-cells、flex
  • overflow 除了 visible 以外的值 (hidden、auto、scroll)

四、BFC 特性及应用

1. 同一个 BFC 下外边距会发生折叠

1
2
3
4
5
6
7
8
9
10
11
12
<head>
div{
width: 100px;
height: 100px;
background: lightblue;
margin: 100px;
}
</head>
<body>
<div></div>
<div></div>
</body>

1

从效果上看,因为两个 div 元素都处于同一个 BFC 容器下 (这里指 body 元素) 所以第一个 div 的下边距和第二个 div 的上边距发生了重叠,所以两个盒子之间距离只有 100px,而不是 200px。

首先这不是 CSS 的 bug,我们可以理解为一种规范,如果想要避免外边距的重叠,可以将其放在不同的 BFC 容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="container">
<p></p>
</div>
<div class="container">
<p></p>
</div>
.container {
overflow: hidden;
}
p {
width: 100px;
height: 100px;
background: lightblue;
margin: 100px;
}

这时候,两个盒子边距就变成了 200px

2. BFC 可以包含浮动的元素(清除浮动)

我们都知道,浮动的元素会脱离普通文档流,来看下下面一个例子

1
2
3
<div style="border: 1px solid #000;">
<div style="width: 100px;height: 100px;background: #eee;float: left;"></div>
</div>

3

由于容器内元素浮动,脱离了文档流,所以容器只剩下 2px 的边距高度。如果使触发容器的 BFC,那么容器将会包裹着浮动元素。

1
2
3
<div style="border: 1px solid #000;overflow: hidden">
<div style="width: 100px;height: 100px;background: #eee;float: left;"></div>
</div>

效果如图:

4

3. BFC 可以阻止元素被浮动元素覆盖

先来看一个文字环绕效果:

1
2
3
<div style="height: 100px;width: 100px;float: left;background: lightblue">我是一个左浮动的元素</div>
<div style="width: 200px; height: 200px;background: #eee">我是一个没有设置浮动,
也没有触发 BFC 元素, width: 200px; height:200px; background: #eee;</div>

5

这时候其实第二个元素有部分被浮动元素所覆盖,(但是文本信息不会被浮动元素所覆盖) 如果想避免元素被覆盖,可触第二个元素的 BFC 特性,在第二个元素中加入 overflow: hidden,就会变成:

6

这个方法可以用来实现两列自适应布局,效果不错,这时候左边的宽度固定,右边的内容自适应宽度(去掉上面右边内容的宽度)。

我是一个左浮动的元素
我是一个没有设置浮动, 也没有触发 BFC 元素, width: 200px; height:200px; background: #eee;

先做个总结,然后再进行具体的分析:

CSS不会阻塞DOM的解析,但是会影响JAVAScript的运行,javaSscript会阻止DOM树的解析,最终css(CSSOM)会影响DOM树的渲染,也可以说最终会影响渲染树的生成。

接下来我们先看javascript对DOM树构建和渲染是如何造成影响的,分成三种类型来讲解:

JavaScript脚本在html页面中

1
2
3
4
5
6
7
8
9
10
1<html>
2 <body>
3 <div>1</div>
4 <script>
5 let div1 = document.getElementsByTagName('div')[0]
6 div1.innerText = 'time.geekbang'
7 </script>
8 <div>test</div>
9 </body>
10</html>

两段div中间插入一段JavaScript脚本,这段脚本的解析过程就有点不一样了。

当解析到script脚本标签时,HTML解析器暂停工作,javascript引擎介入,并执行script标签中的这段脚本。

因为这段javascript脚本修改了DOM中第一个div中的内容,所以执行这段脚本之后,div节点内容已经修改为time.geekbang了。脚本执行完成之后,HTML解析器回复解析过程,继续解析后续的内容,直至生成最终的DOM。

html页面中引入javaScript文件

1
2
3
4
5
6
7
8
9
10
1//foo.js
2let div1 = document.getElementsByTagName('div')[0]
3div1.innerText = 'time.geekbang'
1<html>
2 <body>
3 <div>1</div>
4 <script type="text/javascript" src='foo.js'></script>
5 <div>test</div>
6 </body>
7</html>

这段代码的功能还是和前面那段代码是一样的,只是把内嵌JavaScript脚本修改成了通过javaScript文件加载。

其整个执行流程还是一样的,执行到JAVAScript标签时,暂停整个DOM的解析,执行javascript代码,不过这里执行javascript时,需要现在在这段代码。这里需要重点关注下载环境,因为javascript文件的下载过程会阻塞DOM解析,而通常下载又是非常耗时的,会受到网络环境、javascript文件大小等因素的影响。

优化机制:

谷歌浏览器做了很多优化,其中一个主要的优化就是预解析操作。当渲染引擎收到字节流之后,会开启一个预解析线程,用来分析HTML文件中包含的JavaScript、CSS等相关文件,解析到相关文件之后,会开启一个预解析线程,用来分析HTML文件中包含的javascprit、css等相关文件、解析到相关文件之后,预解析线程会提前下载这些文件。

再回到 DOM 解析上,我们知道引入 JavaScript 线程会阻塞 DOM,不过也有一些相关的策略来规避,比如使用 CDN 来加速 JavaScript 文件的加载,压缩 JavaScript 文件的体积。

另外,如果 JavaScript 文件中没有操作 DOM 相关代码,就可以将该 JavaScript 脚本设置为异步加载,通过 async 或 defer 来标记代码,使用方式如下所示:

1
2
<script async type="text/javascript" src='foo.js'></script>
<script defer type="text/javascript" src='foo.js'></script>

async和defer区别:

  • async:脚本并行加载,加载完成之后立即执行,执行时机不确定,仍有可能阻塞HTML解析,执行时机在load事件派发之前。
  • defer:脚本并行加载,等待HTML解析完成之后,按照加载顺序执行脚本,执行时机DOMContentLoaded事件派发之前。

html页面中有css样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1//theme.css
2div {color:blue}
1<html>
2<head>
3 <style src='theme.css'></style>
4</head>
5<body>
6 <div>1</div>
7 <script>
8 let div1 = document.getElementsByTagName('div')[0]
9 div1.innerText = 'time.geekbang' // 需要 DOM
10 div1.style.color = 'red' // 需要 CSSOM
11 </script>
12 <div>test</div>
13</body>
14</html>

该示例中,JavaScript 代码出现了 div1.style.color = ‘red’ 的语句,它是用来操纵 CSSOM 的,所以在执行 JavaScript 之前,需要先解析 JavaScript 语句之上所有的CSS 样式。所以如果代码里引用了外部的 CSS 文件,那么在执行 JavaScript 之前,还需要等待外部的 CSS 文件下载完成,并解析生成 CSSOM 对象之后,才能执行 JavaScript 脚本。

而 JavaScript 引擎在解析 JavaScript 之前,是不知道 JavaScript 是否操纵了 CSSOM的,所以渲染引擎在遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行CSS 文件下载,解析操作,再执行 JavaScript 脚本。所以说 JavaScript 脚本是依赖样式表的,这又多了一个阻塞过程。

总结:通过上面三点的分析,我们知道了 JavaScript 会阻塞 DOM 生成,而样式文件又会阻塞js的执行。

一、操作方法

数组基本操作可以归纳为 增、删、改、查,需要留意的是哪些方法会对原数组产生影响,哪些方法不会

下面对数组常用的操作方法做一个归纳

下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响

  • push()
  • unshift()
  • splice()
  • concat()

push()

push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度

1
2
3
1let colors = []; // 创建一个数组
2let count = colors.push("red", "green"); // 推入两项
3console.log(count) // 2

unshift()

unshift()在数组开头添加任意多个值,然后返回新的数组长度

1
2
3
1let colors = new Array(); // 创建一个数组
2let count = colors.unshift("red", "green"); // 从数组开头推入两项
3alert(count); // 2

splice

传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组

1
2
3
4
1let colors = ["red", "green", "blue"];
2let removed = colors.splice(1, 0, "yellow", "orange")
3console.log(colors) // red,yellow,orange,green,blue
4console.log(removed) // []

concat()

首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组

1
2
3
4
1let colors = ["red", "green", "blue"];
2let colors2 = colors.concat("yellow", ["black", "brown"]);
3console.log(colors); // ["red", "green","blue"]
4console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]

下面三种都会影响原数组,最后一项不影响原数组:

  • pop()
  • shift()
  • splice()
  • slice()

pop()

pop() 方法用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项

1
2
3
4
1let colors = ["red", "green"]
2let item = colors.pop(); // 取得最后一项
3console.log(item) // green
4console.log(colors.length) // 1

shift()

shift()方法用于删除数组的第一项,同时减少数组的 length 值,返回被删除的项

1
2
3
4
1let colors = ["red", "green"]
2let item = colors.shift(); // 取得第一项
3console.log(item) // red
4console.log(colors.length) // 1

splice()

传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组

1
2
3
4
1let colors = ["red", "green", "blue"];
2let removed = colors.splice(0,1); // 删除第一项
3console.log(colors); // green,blue
4console.log(removed); // red,只有一个元素的数组

slice()

slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组

1
2
3
4
5
6
1let colors = ["red", "green", "blue", "yellow", "purple"];
2let colors2 = colors.slice(1);
3let colors3 = colors.slice(1, 4);
4console.log(colors) // red,green,blue,yellow,purple
5concole.log(colors2); // green,blue,yellow,purple
6concole.log(colors3); // green,blue,yellow

即修改原来数组的内容,常用splice

splice()

传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响

1
2
3
4
1let colors = ["red", "green", "blue"];
2let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
3console.log(colors); // red,red,purple,blue
4console.log(removed); // green,只有一个元素的数组

即查找元素,返回元素坐标或者元素值

  • indexOf()
  • includes()
  • find()

indexOf()

返回要查找的元素在数组中的位置,如果没找到则返回-1

1
2
1let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
2numbers.indexOf(4) // 3

includes()

返回要查找的元素在数组中的位置,找到返回true,否则false

1
2
1let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
2numbers.includes(4) // true

find()

返回第一个匹配的元素

1
2
3
4
5
6
7
8
9
10
11
1const people = [
2 {
3 name: "Matt",
4 age: 27
5 },
6 {
7 name: "Nicholas",
8 age: 29
9 }
10];
11people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}

二、排序方法

数组有两个方法可以用来对元素重新排序:

  • reverse()
  • sort()

reverse()

顾名思义,将数组元素方向排列

1
2
3
1let values = [1, 2, 3, 4, 5];
2values.reverse();
3alert(values); // 5,4,3,2,1

sort()

sort()方法接受一个比较函数,用于判断哪个值应该排在前面

1
2
3
4
5
6
7
8
9
10
11
12
1function compare(value1, value2) {
2 if (value1 < value2) {
3 return -1;
4 } else if (value1 > value2) {
5 return 1;
6 } else {
7 return 0;
8 }
9}
10let values = [0, 1, 5, 10, 15];
11values.sort(compare);
12alert(values); // 0,1,5,10,15

三、转换方法

常见的转换方法有:

join()

join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串

1
2
3
1let colors = ["red", "green", "blue"];
2alert(colors.join(",")); // red,green,blue
3alert(colors.join("||")); // red||green||blue

四、迭代方法

常用来迭代数组的方法(都不改变原数组)有如下:

  • some()
  • every()
  • forEach()
  • filter()
  • map()

some()

对数组每一项都运行传入的函数,如果有一项函数返回 true ,则这个方法返回 true

1
2
3
1let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
2let someResult = numbers.every((item, index, array) => item > 2);
3console.log(someResult) // true

every()

对数组每一项都运行传入的函数,如果对每一项函数都返回 true ,则这个方法返回 true

1
2
3
1let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
2let everyResult = numbers.every((item, index, array) => item > 2);
3console.log(everyResult) // false

forEach()

对数组每一项都运行传入的函数,没有返回值

1
2
3
4
1let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
2numbers.forEach((item, index, array) => {
3 // 执行某些操作
4});

filter()

对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回

1
2
3
1let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
2let filterResult = numbers.filter((item, index, array) => item > 2);
3console.log(filterResult); // 3,4,5,4,3

map()

对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组

1
2
3
复制1let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
2let mapResult = numbers.map((item, index, array) => item * 2);
3console.log(mapResult) // 2,4,6,8,10,8,6,4,2