0%

我们都知道,flex布局中的flex属性是以下三个属性的简写:

  • flex-grow:项目的放大比例,默认为0,即如果存在剩余空间,也不放大
  • flex-shrink:项目的缩小比例,默认为1,即如果空间不足,则会适当缩小
  • flex-basis:在分配剩余空间之前,项目的主轴空间,相当于我们设置的width

flex参数缩写后三个属性分别对应的值为:

  • flex:none // 0 0 auto —— 无宽度不可以均分,内容决定宽度
  • flex:auto // 1 1 auto —— 无宽度不可以均分,内容决定宽度
  • flex:1 // 1 1 0% —— 无宽度可以均分

对于flex:auto和flex:1来说,最主要的区别在于flex-basis这个属性上

奇奇怪怪的auto这篇博客里有提到:auto可以用于自动计算宽度,而0%很显然就是宽度为0

看下面这个例子:

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
<div class="parent">
<div class="item-1"></div>
<div class="item-2"></div>
<div class="item-3"></div>
</div>

<style type="text/css">
.parent {
display: flex;
width: 600px;
}
.parent > div {
height: 100px;
}
.item-1 {
width: 140px;
flex: 2 1 0%;
background: blue;
}
.item-2 {
width: 100px;
flex: 2 1 auto;
background: darkblue;
}
.item-3 {
flex: 1 1 200px;
background: lightblue;
}
</style>
  • 主轴上父容器总尺寸为 600px

  • 子元素的总基准值是:0% + auto + 200px = 300px,其中

    1
    2
    - 0% 即 0 宽度
    - auto 对应取主尺寸即 100px
  • 故剩余空间为 600px - 300px = 300px

  • 伸缩放大系数之和为: 2 + 2 + 1 = 5

  • 剩余空间分配如下:

    1
    2
    - item-1item-2 各分配 2/5,各得 120px
    - item-3 分配 1/5,得 60px
  • 各项目最终宽度为:

    1
    2
    3
    - item-1 = 0% + 120px = 120px
    - item-2 = auto + 120px = 220px
    - item-3 = 200px + 60px = 260px
  • 当 item-1 基准值取 0% 的时候,是把该项目视为零尺寸的,故即便声明其尺寸为 140px,也并没有什么用,形同虚设

  • 而 item-2 基准值取 auto 的时候,根据规则基准值使用值是主尺寸值即 100px,故这 100px 不会纳入剩余空间

说明:从这个案例看,auto的这部分不会被纳入剩余空间,这是最关键的

参考

flex设置成1和auto有什么区别

奇奇怪怪的auto

flex布局

基础

谈谈堆排序,大顶堆,小顶堆?

大顶堆:每个子节点都要大于等于父节点

小顶堆:每个子节点都要小于等于父节点

注意:区分大小顶堆和二叉搜索树的区别

构造小大顶堆

1. 创建小顶堆

二叉树有两种表示方法,(1)指针法;(2)索引法。这里我们使用索引法,即用数组表示堆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MinHeap {
constructor() {
this.heap = [];
}

// 后面涉及节点交换,所以提前接好交换函数
swap(nums, a, b) {
[nums[a], nums[b]] = [nums[b], nums[a]];
}

// 后面涉及比较,所以提前定义好比较函数
compare(a, b) {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
}
}

2. 设置访问节点的方法

要访问使用普通数组的二叉树节点,可以通过下列方式操作index

对于给定位置index节点:

  • 它的左侧节点的位置是2*index+1(如果存在)
  • 它的右侧节点的位置是2*index+2(如果存在)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 左侧节点
getLeftIndex(index) {
return 2*index+1;
}

// 右侧节点
getRightIndex(index) {
return 2*index+2;
}

// 父节点
getParentIndex(index) {
if (index === 0) {
return undefined;
}
return Math.floor((index-1)/2);
}

因此,可以实现堆中的三个操作:

  • insert(value):插入值,插入成功返回true,否则返回false
  • extract():移除最小值(小顶堆)或者最大值(大顶堆),并且返回这个值
  • findMinimum():返回最小值(小顶堆)或者最大值(大顶堆),并且不会移除这个值

3. 插入值

插入值的操作流程:

  1. 将值插入堆的底部节点
  2. 执行siftUp方法,表示将这个值和它的父节点进行交换,直到父节点小于这个插入的值——上移操作
1
2
3
4
5
6
7
8
9
insert(value) {
if (value !== null) {
this.heap.push(value); // step1:插入值
// this.heap.length - 1 即为新插入元素的index(索引)
this.siftUp(this.heap.length - 1); // step2:执行siftUp方法
return true;
}
return false;
}
上移操作siftUp

步骤:

  • 获取父节点parentIndex
  • 比较节点value与父节点value的大小交换节点
  • 递归,递归结束条件:index > 0 并且 this.heap[index] > this.heap[parentIndex]
1
2
3
4
5
6
7
8
9
siftUp(index) {
let parentIndex = this.getParentIndex(index);
while (index > 0 && this.compare(this.heap[index], this.heap[parentIndex]) > -1) {
this.swap(this.heap, index, parentIndex); // 交换
// 递归
index = parentIndex;
parentIndex = this.getParentIndex(index);
}
}

4. 从堆中找到最小值

在最小堆中,最小值永远位于堆顶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 堆长度
size() {
return this.heap.length;
}

// 堆是否为空
isEmpty() {
return this.heap.length === 0;
}

// 空即为undefined,不为空返回堆顶元素
findMinimum() {
return this.isEmpty ? undefined : this.heap[0];
}

5. 移除小顶堆中的最小值

移除值的操作流程:

  1. 两个特殊情况
    • 堆为空,直接返回undefined
    • 堆size为1,返回并移除堆顶元素,不需要其他操作
  2. 其他情况:
    • 移除堆顶元素
    • 重新调整堆结构——下移操作
1
2
3
4
5
6
7
8
9
10
11
12
extract() {
if (this.isEmpty) {
return undefined;
}

if (this.size() === 1) {
return this.heap.shift();
}

const removeValue = this.heap.shift(); // 获取要移除的节点的value
this.shiftDown(0);
}
下移操作(堆化)

整个过程可以抽象为:将堆的最后一个元素移动至根部并执行siftDown函数,执行交换函数直至堆结构正常

步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
siftDown(index) {
let element = index;

// 获取左右子节点的index
const left = this.getLeftIndex(index);
const right = this.getRightIndex(index);
const size = this.size();

// 节点比较,确定是否需要交换
if (left < size && this.compare(this.heap[element], this.heap[left]) > -1) {
element = left;
}

if (right < size && this.compare(this.heap[element], this.heap[right]) > -1) {
element = right;
}

// 判断值和传入element是否相同,和自己交换是没有意义的
if (index !== element) {
this.swap(this.heap, index, element);
this.siftDown(element); // 递归此操作
}
}

构造大顶堆

大顶堆和小顶堆的算法一模一样,不同之处在于大小比较

堆排序算法

思路/步骤

  • 数组构造成一个堆(根据实际需求创建大顶堆或者小顶堆),以小顶堆为例
  • 在创建大顶堆后,最大值会被存储在堆的第一个位置,我们要将它替换为堆的最后一个值,将堆的大小减1
  • 最后,将堆的根节点下移并且重复步骤2直到堆的大小为1

堆排序实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function heapSort(arr) {
let heapSize = arr.length;
buildMaxHeap(arr);
while (heapSize > 1) {
swap(arr, 0, --heapSize);
heapify(arr, 0, heapSize);
}
return arr;
}


// 构建最大堆
function buildMaxHeap(arr) {
for (let i = Math.floor(arr.length / 2); i >= 0; i--) {
heapify(arr, i, arr.length);
}
return arr;
}

参考

大顶堆/小顶堆的构建以及排序的应用

图解大顶堆的构建、排序过程

谈谈堆排序,大顶堆,小顶堆?

组件通讯的方式

父传子

  • Props
  • Instance Mthods

子传父

  • Callback Functions
  • Event Bubbling(冒泡)

兄弟组件

  • Parent Component

跨级组件

  • Context
  • Portals
  • Global Variables
  • Observer Pattern
  • Redux等

通讯实践

父传子&&子传父

场景说明

组件说明:Parent为父组件 Child为子组件

子组件(Child)包含一个Button,用于模拟小孩收到糖果数量(count),但是最终到手的糖果数量(givenCount)是由父组件(Parent)控制的

父组件(Parent)根据子组件收到的糖果数量(count)随机选取一个随机糖果数量(givenCount)(givenCount介于0-count之间)传递给子组件(Child)

方式1:props && Callback Functions

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
// Parent.jsx
import React, {useState} from 'react';
import Child from './child';

export default function Parent() {
const [candies, setCandies ] = useState(0);
function getCandies(count) {
setCandies(count);
}

// 随机给糖果数量
const givenCount = Math.floor(Math.random() * (candies + 1));
return (
<div>
<Child candies={givenCount} getCandiesFn={getCandies}></Child>
</div>
)
}


// Child.jsx
import React, { useState } from 'react';

export default function Child(props) {
const [count, setCount ] = useState(0);
const givenCount = props.candies;
function changeCount() {
setCount(count+1);
props.getCandiesFn(count);
}
return (
<div>
<h3>I am have {count} candies! yeah!</h3>
{/* giveCount是由父组件决定的 */}
<h3>But my mother only give me {givenCount} candies actually!</h3>
<button onClick={changeCount}>Click</button>
</div>
)
}
实现效果

方式2: refs && Event Bubbling

原理说明
  • refs原理:父组件可以通过refs来直接调用子组件实例
  • event bubbling:此方法跟react本身没有关系,利用的是原生dom元素的事件冒泡机制
1
2
3
4
// Parent.jsx


// Child.jsx

实现效果

兄弟组件

兄弟组件之间通信把父组件作为中间桥梁进行传值,实现步骤同上,不再赘述

跨级组件

参考

30分钟精通十种React组件之间通信的方法

观察者模式

当对象间存在一对多的关系时,则使用观察者模式。比如当一个对象被修改的时候,则会自动通知依赖它的对象。观察者模式属于行为型模式。

目的

建立一对多的关联关系,并能使一个对象的变化被所有关联对象感知

动机

建立一套低耦合的消息触发机制

优点

  1. 被观察者和观察者之间是抽象耦合的
  2. 耦合度较低,两者之间的关联仅仅在于消息的通知
  3. 被观察者无需关心他的观察者
  4. 支持广播通信

缺点

  1. 观察者只知道被观察对象发生了变化,但不知变化的过程和缘由
  2. 观察者同时也可能是被观察者,消息传递的链路可能会过长,完成所有通知花费时间较多
  3. 如果观察者和被观察者之间产生循环依赖,或者消息传递链路形成闭环,会导致无限循环

适用场景

  • 邮件订阅
  • 消息队列
  • 浏览器DOM事件监听

手写实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 被观察者
class Subject {
constructor(name) { // 用于存放状态
this.name = name;
this.observers = []; // 关键:观察者列表
this.state = 'I am always 18 years old.';
}

// 接收观察者方法
attach(observer) { // Hi 我是观察者~
this.observers.push(observer); // 快到观察者列表里面去!
}

// 改变状态的方法
setState(newState) { // 被观察者:我的状态变了,快通知我的观察者们~
this.state = newState;
this.observers.forEach(obj => obj.update(newState)); // 好的 正在一个个通知
}
}


// 观察者
class Observer {
constructor(name) {
this.name = name;
}

// 更新方法
update(newState) {
console.log(this.name + ' say ' + newState);
}
}



// 测试
let p1 = new Subject('Katrina'); // 被观察者
let p2 = new Observer('Kate'); // 观察者

// 接收观察者
p1.attach(p2);

// 观察者状态更新
p1.setState('I am 6 years old!'); // Kate say I am 6 years old!

简单总结

被观察者维护一个observers的队列,并通过attach方法接收观察者,通过setState方法改变状态,并且通知所有依赖的观察者进行update

发布/订阅模式

发布订阅模式是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都会得到状态改变的通知。

基本过程

订阅者(Subscriber)把自己想订阅的事件注册到(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布到该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

特点

订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。

实现思路

  • 创建一个EventEmitter类
  • 在该类上创建一个事件中心(Map)存储
  • on方法用来把函数fn都加到事件中心里面(订阅者注册事件)
  • emit方法取到arguments里第一个当作event,根据event值去执行对应事件中心中的函数(发布者发布事件,调度中心处理)
  • off方法用于取消订阅
  • once方法表示只监听一次,调用完毕后删除缓存函数(订阅一次)
  • 注册一个newListener用于监听新的事件订阅

优点

一方面实现了发布者与订阅者之间的解耦,中间者可在两者操作之间进行更细粒度的控制。如:条件过滤发布,权限控制等等

缺点

整一个中间调度会越来越庞大,需要手动清除里面的发布回调

适用场景

  • 模块间通信
  • 网站登录

手写实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class EventEmitter {
constructor() {
// 用来存放注册的事件与回调
this._events = {};
}

// 订阅:将事件回调函数存储到对应的事件上
on(eventName, callback) {
// 由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
const callbacks = this._events[eventName] || []; // 有则读取,没有则创建
callbacks.push(callback); // 添加
this._events[eventName] = callbacks; // 更新
}

// 发布:获取到事件对应的回调函数依次执行
emit(eventName, ...args) { // args用于收集发布事件时传递的参数
const callbacks = this._events[eventName] || [];
callbacks.forEach(cb => cb(...args)); // 执行回调,得到结果数组
}

// 一次监听:1.先注册 2.事件执行后取消订阅
once(eventName, callback) {
// 由于需要在回调函数执行后,取消订阅当前事件,所以需要对传入的回调函数做一层包装,然后绑定包装后的函数
const one = (...args) => {
// 执行回调
callback(...args);
// 取消监听
this.off(eventName, one);
}

// 由于:我们订阅事件的时候,修改了原回调函数的引用,所以,用户触发 off 的时候不能找到对应的回调函数
// 所以,我们需要在当前函数与用户传入的回调函数做一个绑定,我们通过自定义属性来实现
one.initialCallback = callback;
this.on(eventName, one);
}

// 取消监听:找到事件对应的回调函数,删除对应的回调函数
off(eventName, callback) {
const callbacks = this._events[eventName] || []; // 获取回调函数事件列表
// 删除对应的回调函数
const newCallbacks = callbacks.filter(fn => fn !== callback && fn.initialCallback !== callback /* 用于once的取消订阅 */);
this._events[eventName] = newCallbacks; // 更新
}
}


// test
let events = new EventEmitter();

events.on('sayHi', function() {
console.log('Katrina sya hello');
})


let cbFun = function() {
console.log('I am here.');
}

events.on('hello', cbFun);
// events.off('hello', cbFun);


function once() {
console.log('once');
}

events.once('hello', once);

events.emit('hello');


// I am here
// once

参考

观察者模式和订阅发布模式是一样的吗?

实现需求

在页面上动态显示当前时间

实现思路

  1. getCurrentTime函数——Date.now()获取时间 + 显示页面
  2. formatDate函数——修改时间格式
  3. setInterval——每秒调用getCurrentTime函数

代码实现

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
<div>
<h1 class="title">currentTime:</h1>
<h1 id="container"></h1>
</div>
<script>
// 定时器实现每秒请求获取函数,并且写入h1中
setInterval(getCurrentTime, 1000);
// 获取当前时间
function getCurrentTime() {
const container = document.getElementById('container');
const currentTime = formateDate(Date.now());
container.innerHTML = currentTime;
}

// 修改时间格式
function formateDate(date) {
let dateChange = new Date(date);
let year = dateChange.getFullYear();
let month = dateChange.getMonth() + 1;
month = month < 10 ? '0' + month : month;
let day = dateChange.getDate();
day = day < 10 ? '0' + day : day;
let hour = dateChange.getHours();
hour = hour < 10 ? '0' + hour : hour;
let minute = dateChange.getMinutes();
minute < 10 ? '0' + minute : minute;
let second = dateChange.getSeconds();
second < 10 ? '0' + second : second;
let final = `${year}/${month}/${day} ${hour}:${minute}:${second}`;
return final;
}
</script>

知识补充

setInterval() 第一个参数的用法

手写场景 | setTimeout实现setInterval

由面试题输入URL后到渲染发生了什么?引发的思考和知识梳理

注意:本思维导图仅提供思路,不保证答案的完整性,思维导图完善好之后会更新在github或者百度网盘,到时候会放链接哒~

废话不多说!直接看题!

题目描述

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
var n = 123;

function fn() { // ----------------(1)
console.log(this.n);
}


let obj = {
n: 456,
fn1: function() { // ----------------(2)
console.log(this.n);
},
fn2: function() { // ----------------(3)
fn();
},

fn3: fn, // ----------------(4)
fn4: () => { // ----------------(5)
console.log(this.n);
},
}

var bar = obj.fn1; // ----------------(6)

obj.fn1(); // 456
obj.fn2(); // 123
obj.fn3(); // 456
obj.fn4(); // 123
bar(); // 123

考察知识点

this指向问题【常考!必考!头疼!】

这里仅对非严格模式下this指向问题进行讨论,严格模式不做讨论

  • 一般来说,谁调用this就指向谁
  • 在非严格模式下,没有明确的调用者this指向window
  • 箭头函数,this指向定义箭头函数的上下文

解析

依次分析结果:

(1)处直接在全局变量window里定义了一个函数,我们知道,对于所有JavaScript全局对象、函数以及用var声明的变量均自动成为window对象的成员,如果直接调用fn(),this指向window(可以理解为是调用了挂载在window上的全局方法fn)

1
2
3
4
5
var n = 123;
function fn() { // ----------------(1)
console.log(this.n);
console.log(this); // window
}

(2)处在obj对象上定义了fn1方法,如果通过obj.fn1()调用,this指向调用这个方法的上下文对象,即obj(可以理解为此时fn1是挂载在obj上的)

1
2
3
4
5
6
7
8
let obj = {
n: 456,
fn1: function() { // ----------------(2)
console.log(this.n);
console.log(this); // obj
},
...
}

(3)处特殊的点在于:在obj对象上定义了fn2方法,但是这个方法里面嵌套的是调用在(1)处定义的fn函数,根据(2)的分析结果,(3-1)处的this指向为obj,但是!这里调用的是fn(),fn()内部的this指向并没有变化【回顾一下,改变this指向的方法是call apply apply】,因此(3-2)处fn的this指向仍为window

1
2
3
4
5
6
7
let obj = {
...
fn2: function() { // ----------------(3)
console.log(this); //(3-1) obj
fn(); //(3-2) window
},
}

(4)处特殊的点在于:在obj对象上定义了fn3方法,但是这个方法是通过fn赋值得到的,如果你深刻理解函数名是指向函数的指针,就非常容易理解了,fn3处的代码可作改写,更进一步理解!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fn() {
console.log('this指向', this);
}
let obj = {
...
fn3: fn, // ----------------(4)
/*
可以改写为
fu3: function() {
console.log('this指向', this);
}
*/
}

obj.fn3(); // this指向 obj {...}

(5)处特殊的点在于:fn4是一个箭头函数,箭头函数中的this指向的是定义箭头函数的上下文,箭头函数的作用域在函数fn4内部,fn4所在作用域为最外层的js环境,因为其没有其他函数包裹,最外层的js环境指向的对象是window对象,所以this指向window对象

1
2
3
4
5
let obj = {
fn4: () => { // ----------------(5)
console.log(this.n);
},
}

(6)处是在全局定义了两个变量bar,基本思路和(4)处有异曲同工之妙,不多说了

1
var bar = obj.fn1;                             // ----------------(6) 

拓展

下面打印的结果为:

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
let n = 123;

function fn() {
console.log(this.n);
}


let obj = {
n: 456,
fn1: function() {
console.log(this.n);
},
fn2: function() {
fn();
},

fn3: fn,
fn4: () => {
console.log(this.n);
},
}

obj.fn1(); // 456
obj.fn2(); // undefined
obj.fn3(); // 456
obj.fn4(); // undefined

因为,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但 let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性

知识点

函数 | 深刻理解函数名是指向函数的指针

变量 | var let const区别及应用

函数 | this指向

在《HTML5与CSS3基础教程》(第8版)P215有这样一句话:

如果不显式设置宽度和高度,浏览器就会使用auto,对于大多数默认显示为块级元素的元素,width的auto值是由包含块的宽度减去元素的内边距、边框和外边距计算出来的,简单地说,包含块的宽度指的是父元素给元素留出的宽度

熟悉盒模型的话这句话特别容易理解,这里不多说了。

问题1: heigth:auto失效问题

问题描述:

举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
.outer {
height: 200px;
width: 200px;
background-color:pink;
text-align: center;
vertical-align: center;
}

.inner {
background-color: blue;
width:auto;
height: 100;
border: 1px solid red;
padding: 5px;
margin: 10px;
}
</style>
<body>
<div class="outer">
<div class="inner"></div>
</div>
</body>

根据上述定义auto的计算方式,不难计算,内部div的width为168px

但是如果我同步将height设置为auto,原本想的是,height可以按照width:auto一样的计算方式得到结果,但是发现,内部div的height为0!

原因解释:

这里需要注意的是:

在块级元素中,width:auto会自适应撑满父元素宽度,这里的撑满是指撑满剩余部分,是需要经过计算的

在内联元素中,width:auto则呈现包裹性,即元素宽度由子元素的宽度决定

而对于height:auto来说,不论是内联元素还是块级元素,height:auto都是呈包裹性的,即元素的高度由子元素高度决定

总结来说,造成上述问题的原因是要理解,height:auto根据块内内容自动调节高度。

问题2:margin:auto垂直方向失效问题

在《HTML5与CSS3基础教程》(第8版)P21继续提到:

如果手动设置width,但将某个外边距设置为auto,那么这个外边距将进行伸缩以弥补不足的部分

如果手动设置width,并将左右边距设为auto,那么两个外边距就会将设为相等的最大值,实现元素居中

问题描述:

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
.outer {
height: 200px;
width: 200px;
background-color:pink;
}

.inner {
background-color: blue;
width:100px;
height: 100px;
border: 1px solid red;
padding: 5px;
margin: auto;
}
</style>
<body>
<div class="outer">
<div class="inner">

</div>
</div>
</body>

从图中可以发现,将margin设置为auto后,浏览器自动计算了内部div的margin值以弥补空间

问题是,按照道理来说,margin:auto是将上下左右四个方向都设置为了auto,为什么计算的时候只对左右两个方向有效?上下两个方向无效?

原因解释:

这里我们首先需要理解下面两点:

  1. 理解auto的概念:auto是自动填充剩余空间的意思
  2. 理解块级元素的概念:块级元素在默认情况下会占据整行空间,即使元素宽度不是百分之百占据浏览器,浏览器也会给它留出整行的空间

因此,对于内部div来说,如果设置左右外边距为auto的话,计算方式如下:

1
外部div的width = 左外边距 + 左边框宽度 + 左内边距 + 内部div的width + 右内边距 + 右边框宽度 + 右外边距

但是块级元素的高度是不会自动扩充的,所以它的外部尺寸是不会自动充满父元素的,也没有剩余空间,因此margin上下设置auto不能实现垂直居中

解决办法:通过定位+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>
.outer {
height: 200px;
width: 200px;
background-color:pink;
position: relative;
}

.inner {
background-color: blue;
width:100px;
height: 100px;
border: 1px solid red;
padding: 5px;
margin: auto;
position:absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
<body>
<div class="outer">
<div class="inner">
</div>
</div>
</body>

这里的关键在于:上下左右四个定位全部设置为0,这样就有了多余空间,auto就能平分剩余的空间去实现水平垂直居中

拓展问题:CSS百分比的参照问题

问题描述:

之前在牛客做过这样一道选择题:

当 margin-top、padding-top 的值是百分比时,分别是如何计算的?

A. 相对父级元素的 height,相对自身的 height

B. 相对最近父级块级元素的 height,相对自身的 height

C. 相对父级元素的 width,相对自身的 width

D. 相对最近父级块级元素的 width,相对最近父级块级元素的 width

知识点:

  • 参照父元素宽度的元素:padding margin width text-indent
  • 参照父元素高度的元素:height
  • 参照父元素属性:font-size line-height
  • 特殊情况:相对定位的时候,top bottom left right参照的是父元素的内容区的高度与宽度,而绝对定位参照的是最近定位元素包含padding的高度与宽度

实践出真知:margin padding 参照问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<style>
.outer {
height: 300px;
width: 200px;
background-color:pink;
position: relative;
}

.inner {
background-color: blue;
width:100px;
height: 100px;
position:absolute;
padding-top: 50%;
margin-left: 50%;
}
</style>
<body>
<div class="outer">
<div class="inner">
</div>
</div>
</body>
原因分析:

假如padding-top是按照height来说,父元素的高度会包含子元素的高度,如果子元素的padding-top继续增加,那么父元素的高度也会增加,因为父元素要包含子元素嘛,这样的话,父元素的高度就成为了一个死循环. 还有一种说法,我们都知道padding-left/right和margin-left/right都是按照宽度来说的,当我们不设置具体的上下左右只设置margin或padding时四个值应该保持为一种类型