原型链
原型: 每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。 每一个函数/class类,都会有一个 prototype属性 通过new 构造函数/class 类,那么生成的对象其 proto 指向其构造函数/class类的prototype
这是每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。
原型还可以指向构造函数 constructor,每个原型都有一个 constructor 属性指向关联的构造函数。
function Person() {
}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
原型也是一个对象,既然是对象,那它也有自己的原型,即原型的原型,构成原型链
原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 proto 指向构造函数的 prototype ,所以我们再更新下关系图: Object.prototype 的原型为null
继承及各种继承的优缺点
继承的目标就是共享方法,但是各自的属性互不干扰,避免引用类型的属性被篡改,同时希望能够在继承中传入参数,自定义属性
- 原型链继承 直接将构造函数的原型指向某个对象或者 new 构造函数/类 new 构造函数/class类 通过__proto__ 来继承 构造函数/class类上的prototype, 再通过原型链一直指向Object.prototype --> null
将一个构造函数/类的原型复制给某个对象 从而实现继承
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
this.xxx = xxx;
// xxx
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]
缺点:将Child.prototype指向了一个对象, 该对象上的引用类型的属性会被所有实例共享 在创建 Child 的实例时,不能向Parent传参
2.借助构造函数 (经典继承)
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
// 内部执行要继承的 属性和方法,将属性和方法绑定在实例自身,而非prototype,可以避免原型链继承导致所有实例访问的是同一个单例引用类型
Parent.call(this);
}
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
好处:通过内部call,带入this,可以避免了引用类型的属性被所有的实例共用 也可以在Child中向Parent中传参 缺点: 每次创建实例都会重新创建方法,不能使方法达到共享
3.组合继承 原型链继承和经典(构造函数)继承 组合的方式,将属性使用call放在实例上,把方法放在继承的原型链上,避免两种缺点
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child1 = new Child('kevin', '18');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child('daisy', '20');
console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
- 原型式继承 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}
var person1 = createObj(person);
var person2 = createObj(person);
person1.name = 'person1';
console.log(person2.name); // kevin
person1.friends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]
// 实现一个Object.create
// 功能就是将传入的对象作为创建的对象的原型
function createObj(o, definedProptypeObj){
function A() {};
// __proto__ 直接等于也行
A.prototype = o;
const a = = new A();
// 对definedProptypeObj 进行遍历,并将属性描述器写入
Object.defineProperty('',xxx)
}
缺点:和原型链继承一样,引用类型的属性被共享了,容易被篡改
5.寄生式继承 创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。 类似aop先继承一个对象,再添加属性
function createObj (o) {
var clone = Object.create(o);
clone.sayName = function () {
console.log('hi');
}
return clone;
}
缺点: 和经典继承,借助构造函数一样,方法会重复创建
6.寄生组合式继承
组合继承最大的缺点是会调用两次父构造函数。
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('kevin', '18');
console.log(child1);
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);
核心思想就是不要再new Parent一次,而是把parent的prototype 用中间函数的方式来直接通过原型链来继承,避免再new 一次
作用域链
每一个执行上下文都有一个outer,指向定义的时候所在的执行上下文,outer将不同的执行上下文串联起来,形成作用域链
词法作用域
用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。 词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
会打印函数,而不是 undefined 这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。
执行上下文栈
JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以程序结束之前, ECStack 最底部永远有个 globalContext:
instanceof 和 typeof
判断一个函数类型 ,instanceof 和 typeof, Object.prototype.toString.call() 来判断最为准确,和Symbol.xxx 的一个属性相关,几乎不会去改动
typeof
typeof 一般被用于判断一个变量的类型,我们可以利用 typeof 来判断number, string, object, boolean, function, undefined, symbol 这七种类型
null会被typeof判断为object array,date,regexp都被被判断为object类,不能详细判断具体类型
typeof 是使用机器码来判断的
- 000:对象
- 010:浮点数
- 100:字符串
- 110:布尔
- 1:整数
but, 对于
undefined和null来说,这两个值的信息存储是有点特殊的。
null:所有机器码均为0
undefined:用 −2^30 整数来表示
所以 typeof判断 null出问题了,是一个遗留bug问题
因此在用 typeof 来判断变量类型的时候,我们需要注意,最好是用 typeof 来判断基本数据类型(包括symbol),避免对 null 的判断。
实现instanceof instanceof 使用:
实例 instanceof 类
instanceof 主要的作用就是判断一个实例是否属于某种类型
// 可以通过原型链来实现
function my_instanceof(left, right){
// 递归找left的原型链上的构造函数 是否为right ,null会报错,这里暂不考虑
let current_proto = left.__proto__;
while(current_proto) {
if(current_proto.constructor === right) return true;
current_proto = current_proto.__proto__;
}
return false;
}
function Foo() { }
Object instanceof Object // true
Function instanceof Function // true
Function instanceof Object // true
Foo instanceof Foo // false
Foo instanceof Object // true
Foo instanceof Function // true
如果想要判断一个对象的具体类型可以考虑用 instanceof,但是 instanceof 也可能判断不准确,比如一个数组,他可以被 instanceof 判断为 Object。
bind call apply 实现
bind 的实现原理就是返回一个新函数,用call/apply去重新直接改写this,但是需要注意 这个this是不是此函数的实例,如果是,那么不能把this指向第一个参数了,并将return 的新函数 prototype重新指向上面的prototype (或许创建一个中间函数, 此时的return函数的this其实就是实例this,不需要替换了
call和apply的实现就是 将使用的函数放在要指向的this的对象的方法上,然后调用删除
curry 柯里化
var fn = curry(function(a, b, c) {
return [a, b, c];
});
fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]
// count 表示fn已经被执行的个数
function mid_curry(fn) {
const args = Array.prototype.slice.call((argument, 1))
// 返回一个可执行函数,但是fn的参数一部分被确定
return function () {
// fn.apply(this, [...xxxx]);
fn(...args, ...arguments)
}
}
function curry(fn, length) {
// 获取fn的参数个数
const length = length || fn.length || 0;
return function() {
// 如果arguments 小于length 那么继续返回一个函数提供调用,如果不是,则返回fn执行结果
if(arguments.length < length) {
// 说明还有参数需要继续待执行,此时需要返回一个函数可以执行剩余参数
// curry函数需要再插入一个中间函数
// 使用apply
return curry(mid_curry(fn, ...arguments), length - arguments.length);
}
// 使用apply
return fn(...argument);
}
}
DOM事件
DOM 0级事件处理,DOM 2级事件处理和DOM 3级事件处理
DOM 0级事件
- on-event (HTML 属性):
<input onclick="alert('xxx')"/>
需要注意的是,基于代码的使用性与维护性考量,现在已经不建议用此方式来绑定事件。
on-event (非HTML 属性):
像是window或document此类没有实体元素的情况:
window.onload = function(){
document.write("Hello world!");
};
若是实体元素:
// HTML
<button id="btn">Click</button>
// JavaScript
var btn = document.getElementById('btn');
btn.onclick = function(){
alert('xxx');
}
若想解除事件的话,则重新指定on-event为null即可:
btn.onclick = null
-
同一个元素的同一种事件只能绑定一个函数,否则后面的函数会覆盖之前的函数
-
不存在兼容性问题
DOM 2级事件
- Dom 2级事件是通过 addEventListener 绑定的事件
2.同一个元素的同种事件可以绑定多个函数,按照绑定顺序执行
3.解绑Dom 2级事件时,使用 removeEventListener
// addEventListener 可以添加多个回调函数,而0级事件可以是一个,再此添加的话就会被取代掉
btn.removeEventListener( "click" ,a)
Dom 2级事件有三个参数:第一个参数是事件名(如click);第二个参数是事件处理程序函数;第三个参数如果是true的话表示在捕获阶段调用,为false的话表示在冒泡阶段调用。捕获阶段和冒泡阶段在下一节具体介绍。
还有注意removeEventListener():不能移除匿名添加的函数。
DOM 3级事件
DOM3级事件在DOM2级事件的基础上添加了更多的事件类型,增加的类型如下:
- UI事件,当用户与页面上的元素交互时触发,如:load、scroll
- 焦点事件,当元素获得或失去焦点时触发,如:blur、focus
- 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dblclick、mouseup
- 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
- 文本事件,当在文档中输入文本时触发,如:textInput
- 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
- 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
- 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified
- 同时DOM3级事件也允许使用者自定义一些事件。
DOM事件流
事件流(Event Flow)指的就是「网页元素接收事件的顺序」。事件流可以分成两种机制:
- 事件捕获(Event Capturing) true
- 事件冒泡(Event Bubbling) false (默认)
当一个事件发生后,会在子元素和父元素之间传播(propagation)。这种传播分成三个阶段:
- 捕获阶段:事件从window对象自上而下向目标节点传播的阶段;
- 目标阶段:真正的目标节点正在处理事件的阶段;
- 冒泡阶段:事件从目标节点自下而上向window对象传播的阶段。
假设我们点击(click)了<div>点我</div>元素,那么在「事件冒泡」的机制下,触发事件的顺序会是:
<div>点我</div><body><html>document
像这样click事件逐层向上依序被触发,就是「事件冒泡」机制。
v8 垃圾回收机制
内存结构图
垃圾回收主要发生在新生代和老生代
新生代和老生代
新生代
在V8引擎的内存结构中,新生代主要用于存放存活时间较短的对象。新生代内存是由两个semispace(半空间)构成的,内存最大值在64位系统和32位系统上分别为32MB和16MB,在新生代的垃圾回收过程中主要采用了Scavenge算法。
Scavenge算法是一种典型的牺牲空间换取时间的算法,对于老生代内存来说,可能会存储大量对象,如果在老生代中使用这种算法,势必会造成内存资源的浪费,但是在新生代内存中,大部分对象的生命周期较短,在时间效率上表现可观,所以还是比较适合这种算法。
在
Scavenge算法的具体实现中,主要采用了Cheney算法,它将新生代内存一分为二,每一个部分的空间称为semispace,也就是我们在上图中看见的new_space中划分的两个区域,其中处于激活状态的区域我们称为From空间,未激活(inactive new space)的区域我们称为To空间。这两个空间中,始终只有一个处于使用状态,另一个处于闲置状态。我们的程序中声明的对象首先会被分配到From空间,当进行垃圾回收时,如果From空间中尚有存活对象,则会被复制到To空间进行保存,非存活的对象会被自动回收。当复制完成后,From空间和To空间完成一次角色互换,To空间会变为新的From空间,原来的From空间则变为To空间。
对象晋升
- 对象是否经历过一次
Scavenge算法 To空间的内存占比是否已经超过25%满足任何一个条件那么新生代的对象就会晋升到老生代
上述例子在经过一系列操作后最终对象会被垃圾回收,但是一旦我们碰到循环引用的场景,就会出现问题,我们看下面的例子:
function foo() {
let a = {};
let b = {};
a.a1 = b;
b.b1 = a;
}
foo();
复制代码
这个例子中我们将对象a的a1属性指向对象b,将对象b的b1属性指向对象a,形成两个对象相互引用,在foo函数执行完毕后,函数的作用域已经被销毁,作用域中包含的变量a和b本应该可以被回收,但是因为采用了引用计数的算法,两个变量均存在指向自身的引用,因此依旧无法被回收,导致内存泄漏。
因此为了避免循环引用导致的内存泄漏问题,截至2012年所有的现代浏览器均放弃了这种算法,转而采用新的Mark-Sweep(标记清除)和Mark-Compact(标记整理)算法。在上面循环引用的例子中,因为变量a和变量b无法从window全局对象访问到,因此无法对其进行标记,所以最终会被回收。
Mark-Sweep(标记清除)分为标记和清除两个阶段,在标记阶段会遍历堆中的所有对象,然后标记活着的对象,在清除阶段中,会将死亡的对象进行清除。Mark-Sweep算法主要是通过判断某个对象是否可以被访问到,从而知道该对象是否应该被回收,具体步骤如下:
- 垃圾回收器会在内部构建一个
根列表,用于从根节点出发去寻找那些可以被访问到的变量。比如在JavaScript中,window全局对象可以看成一个根节点。 - 然后,垃圾回收器从所有根节点出发,遍历其可以访问到的子节点,并将其标记为活动的,根节点不能到达的地方即为非活动的,将会被视为垃圾。
- 最后,垃圾回收器将会释放所有非活动的内存块,并将其归还给操作系统。
以下几种情况都可以作为根节点:
- 全局对象
- 本地函数的局部变量和参数
- 当前嵌套调用链上的其他函数的变量和参数
但是Mark-Sweep算法存在一个问题,就是在经历过一次标记清除后,内存空间可能会出现不连续的状态,因为我们所清理的对象的内存地址可能不是连续的,所以就会出现内存碎片的问题,导致后面如果需要分配一个大对象而空闲内存不足以分配,就会提前触发垃圾回收,而这次垃圾回收其实是没必要的,因为我们确实有很多空闲内存,只不过是不连续的。
为了解决这种内存碎片的问题,Mark-Compact(标记整理)算法被提了出来,该算法主要就是用来解决内存的碎片化问题的,回收过程中将死亡对象清除后,在整理的过程中,会将活动的对象往堆内存的一端进行移动,移动完成后再清理掉边界外的全部内存,我们可以用如下流程图来表示:
由于JS的单线程机制,垃圾回收的过程会阻碍主线程同步任务的执行,待执行完垃圾回收后才会再次恢复执行主任务的逻辑,这种行为被称为全停顿(stop-the-world)。在标记阶段同样会阻碍主线程的执行,一般来说,老生代会保存大量存活的对象,如果在标记阶段将整个堆内存遍历一遍,那么势必会造成严重的卡顿。
因此,为了减少垃圾回收带来的停顿时间,V8引擎又引入了Incremental Marking(增量标记)的概念,即将原本需要一次性遍历堆内存的操作改为增量标记的方式,先标记堆内存中的一部分对象,然后暂停,将执行权重新交给JS主线程,待主线程任务执行完毕后再从原来暂停标记的地方继续标记,直到标记完整个堆内存。这个理念其实有点像React框架中的Fiber架构,只有在浏览器的空闲时间才会去遍历Fiber Tree执行对应的任务,否则延迟执行,尽可能少地影响主线程的任务,避免应用卡顿,提升应用性能。
得益于增量标记的好处,V8引擎后续继续引入了延迟清理(lazy sweeping)和增量式整理(incremental compaction),让清理和整理的过程也变成增量式的。同时为了充分利用多核CPU的性能,也将引入并行标记和并行清理,进一步地减少垃圾回收对主线程的影响,为应用提升更多的性能。
如何避免内存泄漏
-
尽可能少的创建全局变量 比如避免在全局作用域上创建变量,或者避免在函数作用域上不以任何声明的方式创建一个变量 当进行垃圾回收时,在标记阶段因为
window对象可以作为根节点,在window上挂载的属性均可以被访问到,并将其标记为活动的从而常驻内存,因此也就不会被垃圾回收,只有在整个进程退出时全局作用域才会被销毁。如果你遇到需要必须使用全局变量的场景,那么请保证一定要在全局变量使用完毕后将其设置为null从而触发回收机制。 -
手动清除定时器 在我们的应用中经常会有使用
setTimeout或者setInterval等定时器的场景,定时器本身是一个非常有用的功能,但是如果我们稍不注意,忘记在适当的时间手动清除定时器,那么很有可能就会导致内存泄漏,示例如下:
const numbers = [];
const foo = function() {
for(let i = 0;i < 100000;i++) {
numbers.push(i);
}
};
window.setInterval(foo, 1000);
复制代码
在这个示例中,由于我们没有手动清除定时器,导致回调任务会不断地执行下去,回调中所引用的numbers变量也不会被垃圾回收,最终导致numbers数组长度无限递增,从而引发内存泄漏。
- 少用闭包
闭包是JS中的一个高级特性,巧妙地利用闭包可以帮助我们实现很多高级功能。一般来说,我们在查找变量时,在本地作用域中查找不到就会沿着作用域链从内向外单向查找,但是闭包的特性可以让我们在外部作用域访问内部作用域中的变量,示例如下:
function foo() {
let local = 123;
return function() {
return local;
}
}
const bar = foo();
console.log(bar()); // -> 123
复制代码
在这个示例中,foo函数执行完毕后会返回一个匿名函数,该函数内部引用了foo函数中的局部变量local,并且通过变量bar来引用这个匿名的函数定义,通过这种闭包的方式我们就可以在foo函数的外部作用域中访问到它的局部变量local。一般情况下,当foo函数执行完毕后,它的作用域会被销毁,但是由于存在变量引用其返回的匿名函数,导致作用域无法得到释放,也就导致local变量无法回收,只有当我们取消掉对匿名函数的引用才会进入垃圾回收阶段。
- 清除DOM引用
以往我们在操作DOM元素时,为了避免多次获取DOM元素,我们会将DOM元素存储在一个数据字典中,示例如下:
const elements = {
button: document.getElementById('button'),
};
function removeButton() {
document.body.removeChild(document.getElementById('button'));
}
复制代码
在这个示例中,我们想调用removeButton方法来清除button元素,但是由于在elements字典中存在对button元素的引用,所以即使我们通过removeChild方法移除了button元素,它其实还是依旧存储在内存中无法得到释放,只有我们手动清除对button元素的引用才会被垃圾回收。
- 弱引用
如果我们一旦疏忽,就会容易地引发内存泄漏的问题,为此,在ES6中为我们新增了两个有效的数据结构
WeakMap和WeakSet,就是为了解决内存泄漏的问题而诞生的。其表示弱引用,它的键名所引用的对象均是弱引用,弱引用是指垃圾回收的过程中不会将键名对该对象的引用考虑进去,只要所引用的对象没有其他的引用了,垃圾回收机制就会释放该对象所占用的内存。这也就意味着我们不需要关心WeakMap中键名对其他对象的引用,也不需要手动地进行引用清除, 我们没有手动清除WeakMap中的键名对数组的引用,但是内存依旧已经回到原始的大小,说明该数组已经被回收,那么这个也就是弱引用的具体含义了 ,key和value都是弱引用
首先打开node命令行,输入以下命令:
node --expose-gc // --expose-gc 表示允许手动执行垃圾回收机制
复制代码
然后我们执行下面的代码。
// 手动执行一次垃圾回收保证内存数据准确
> global.gc();
undefined
// 查看当前占用的内存,主要关心heapUsed字段,大小约为4.4MB
> process.memoryUsage();
{ rss: 21626880,
heapTotal: 7585792,
heapUsed: 4708440,
external: 8710 }
// 创建一个WeakMap
> let wm = new WeakMap();
undefined
// 创建一个数组并赋值给变量key
> let key = new Array(1000000);
undefined
// 将WeakMap的键名指向该数组
// 此时该数组存在两个引用,一个是key,一个是WeakMap的键名
// 注意WeakMap是弱引用
> wm.set(key, 1);
WeakMap { [items unknown] }
// 手动执行一次垃圾回收
> global.gc();
undefined
// 再次查看内存占用大小,heapUsed已经增加到约12MB
> process.memoryUsage();
{ rss: 30232576,
heapTotal: 17694720,
heapUsed: 13068464,
external: 8688 }
// 手动清除变量key对数组的引用
// 注意这里并没有清除WeakMap中键名对数组的引用
> key = null;
null
// 再次执行垃圾回收
> global.gc()
undefined
// 查看内存占用大小,发现heapUsed已经回到了之前的大小(这里约为4.8M,原来为4.4M,稍微有些浮动)
> process.memoryUsage();
{ rss: 22110208,
heapTotal: 9158656,
heapUsed: 5089752,
external: 8698 }
复制代码
js浮点数精度
转化为二进制会导致精度丢失,64位 JavaScript 里的数字是采用 IEEE 754 标准的 64 位双精度浮点数。该规范定义了浮点数的格式,对于64位的浮点数在内存中的表示,最高的1位是符号位,接着的11位是指数,剩下的52位为有效数字
// 最后还可以使用第三方库,如`Math.js`、`BigDecimal.js`
\
/**
* 精确加法
*/
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
parseFloat(1.4000000000000001.toPrecision(12)) === 1.4 // True
// toFixed
numObj.toFixed(digits)
/**
** 减法函数,用来得到精确的减法结果
** 说明:javascript的减法结果会有误差,在两个浮点数相减的时候会比较明显。这个函数返回较为精确的减法结果。
** 调用:accSub(arg1,arg2)
** 返回值:arg1加上arg2的精确结果
**/
function accSub(arg1, arg2) {
var r1, r2, m, n;
try {
r1 = arg1.toString().split(".")[1].length;
}
catch (e) {
r1 = 0;
}
try {
r2 = arg2.toString().split(".")[1].length;
}
catch (e) {
r2 = 0;
}
m = Math.pow(10, Math.max(r1, r2)); //last modify by deeka //动态控制精度长度
n = (r1 >= r2) ? r1 : r2;
return ((arg1 * m - arg2 * m) / m).toFixed(n);
}
// 给Number类型增加一个mul方法,调用起来更加方便。
Number.prototype.sub = function (arg) {
return accMul(arg, this);
};
/**
** 乘法函数,用来得到精确的乘法结果
** 就是先✖️10x次方都变成正数再除以10x次方 除法类似
** 说明:javascript的乘法结果会有误差,在两个浮点数相乘的时候会比较明显。这个函数返回较为精确的乘法结果。
** 调用:accMul(arg1,arg2)
** 返回值:arg1乘以 arg2的精确结果
**/
function accMul(arg1, arg2) {
var m = 0, s1 = arg1.toString(), s2 = arg2.toString();
try {
m += s1.split(".")[1].length;
}
catch (e) {
}
try {
m += s2.split(".")[1].length;
}
catch (e) {
}
return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
}
// 给Number类型增加一个mul方法,调用起来更加方便。
Number.prototype.mul = function (arg) {
return accMul(arg, this);
};
new 的模拟实现
一句话介绍 new:
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一
function Otaku () {
……
}
// 使用 new
var person = new Otaku(……);
// 使用 objectFactory
var person = objectFactory(Otaku, ……)
function objectFactory(fn) {
// 1.生成一个新的对象
const obj = Object.create(null);
// 2.fn的this指向obj
const args = Array.prototype.slice.call(arguments, 1);
const ret = fn.call(obj, ...args);
// 3. 将obj的原型指向fn.prototype
obj.__proto__ = fn.prototype;
// 判断一下fn返回结果
//return obj;
return typeof ret === 'object' ? ret : obj;
}
// 如果构造函数返回了一个对象,在实例 person 中只能访问返回的对象中的属性。,基础类型则返回实例
js事件循环机制
浏览器环境下
- 执行栈和事件队列 当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。 后进先出
一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,除非发生了栈溢出,即超过了所能使用内存的最大值。
么当一个异步代码(如发送ajax请求数据)执行后会如何呢?前文提过,js的另一大特点是非阻塞,实现这一点的关键在于下面要说的这项机制——事件队列(Task Queue)。
js引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列。被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码...,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环。
node环境下的事件循环
node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。 因此实际上node中的事件循环存在于libuv引擎中。
// nodejs => js层 + c++ 层
// c/c++层 分为三个部分
// 1.v8引擎,用于 JS 解析和执行,它还是自定义c++拓展,比如全局变量等;
// 2. Libuv (c语言)是一个跨平台的异步 IO 库。
// 它主要的功能是它封装了各个操作系统的一些 API, 提供网络还有文件进程的这些功能。 (包含了net/fs等第三方库)
// 3. 不依赖libuv的第三方库 比如 DNS 解析库,还有 HTTP 解析器等等。
事件循环模型
下面是一个libuv引擎中的事件循环的模型:
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...
- timers: 这个阶段执行定时器队列中的回调如
setTimeout()和setInterval()。 - I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和
setImmediate()的回调。 - idle, prepare: 这个阶段仅在内部使用,可以不必理会。
- poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- check:
setImmediate()的回调会在这个阶段执行。 - close callbacks: 例如
socket.on('close', ...)这种close事件的回调。
poll阶段
循环的开始阶段 轮询阶段
当个v8引擎将js代码解析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,收到代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback。
值得注意的是,poll阶段在执行poll queue中的回调时实际上不会无限的执行下去。有两种情况poll阶段会终止执行poll queue中的下一个回调:1.所有回调执行完毕。2.执行数超过了node的限制。
// nodejs => js层 + c++ 层
// c/c++层 分为三个部分
// 1.v8引擎,用于 JS 解析和执行,它还是自定义c++拓展,比如全局变量等;
// 2. Libuv (c语言)是一个跨平台的异步 IO 库。
// 它主要的功能是它封装了各个操作系统的一些 API, 提供网络还有文件进程的这些功能。 (包含了net/fs等第三方库)
// 3. 不依赖libuv的第三方库 比如 DNS 解析库,还有 HTTP 解析器等等。
// libuv
// 1.libuv的模型和限制
// libuv 生产者和消费者模型
// 和硬件性能有关系
setImmediate(() => {
console.log('immediate');
});
setTimeout(() => {
console.log('timeout');
}, 0); // 0 受到硬件性能影响 循环的时候 setTimeout 是否被放到了队列里面了 是放在setImmediate 前面还是后面
// const fs = require('fs');
// fs.readFile(__filename, () => {
// setTimeout(() => {
// console.log('timeout');
// }, 0);
// setImmediate(() => {
// console.log('immediate');
// });
// });
setImmediate 意思是轮询阶段完成就执行
setTimeout 是在最小阈值过后执行
check阶段
check阶段专门用来执行setImmediate()方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段。
close阶段
当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去。
timer阶段
这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数。
I/O callback阶段
如上文所言,这个阶段主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。
process.nextTick() 优先级最高,比微任务还要高
尽管没有提及,但是实际上node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。与执行poll queue中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用process.nextTick()方法会导致node进入一个死循环。。直到内存泄漏。
Promise 的实现
// 通过发布订阅实现一个promise resolve可以保存留在下次调用
function My_promise(fn) {
let status = 'pending';
// 只需要一个就可以了
let value = undefined;
const callbacks = this.callbacks = [];
// 需要一个 reslove函数和一个reject函数 保留一个队列,将保留的值进行收藏,并且只能有一个状态,一旦被赋值就不再执行
// fn执行是同步的
const resolve = function(res) {
// 查看状态
// 当reslove是后执行的,但是也有可能先执行,要看状态,如果是后执行,那是此时去指向then返回的回调函数
// resole的执行本身需要异步
// 状态必须是pending,否则不执行
setTimeout(() => {
if(status !== 'pending') return;
// res 如果是promise,那么需要把这个promise 的resolve的结果拿到
// if(newValue && (typeof newValue === 'object' || typeof newValue === 'function')){
// const {then} = newValue
// if(typeof then === 'function') {
// // newValue 为新产生的 Promise,此时resolve为上个 promise 的resolve
// //相当于调用了新产生 Promise 的then方法,注入了上个 promise 的resolve 为其回调
// // 此时 reslove应该支持 递归回调
// then.call(newValue,resolve)
// return
// }
// }
if(res instanceof My_promise) {
//
// res.then((r) => {
// value = r;
// status = 'fulfilled';
// // 执行cb
// resCb();
// })
res.then.call(res, resolve, reject);
} else {
value = res;
status = 'fulfilled';
// 执行cb
resCb();
}
});
};
// reject 就是类似 resolve
const reject = function(res) {
setTimeout(() => {
if(status !== 'pending') return;
if(res instanceof My_promise) {
// 需要把this指向自身的promise
res.then.call(res, resolve, reject);
} else {
value = res;
status = 'rejected';
// 执行cb
resCb();
console.log(status, 'status');
}
});
};
const resCb = () => {
while(callbacks.length) {
const fulfiledFn = callbacks.shift();
handle(fulfiledFn);
};
// 来递归调用next this.next 来寻找链条 拿到callbacks进行调用
};
const handle = function(_cb) {
// 1. 通过状态来判断
// status 都改成this
if(status === 'fulfilled') {
// fulfilled 状态 说明已经完成resolve
// setTimeout(() => {
// resolve_(cb(value));
// });
// 那么直接执行
_cb.resolve_(_cb.cb(value));
} else if(status === 'rejected') {
if(_cb.rejectCb) {
console.log(111, _cb.rejectCb, '_cb.rejectCb_cb.rejectCb')
_cb.reject_(_cb.rejectCb(value));
}
} else {
// 说明状态不对 那么存储 对应的cb和resolve
callbacks.push(_cb);
}
}
this.then = function(cb, rejectCb) {
// then函数很可能会先执行,此时resolve的值是有可能没有拿到的,如果没有拿到resolve的值,那么此时是需要储存 cb,等待resolve有了,来进行发布操作
// cb的执行本身是异步的
// 返回一个新的promise
return this.next = new My_promise((resolve_, reject_) => {
// 1. cb执行 2. 将cb的值作为reslove的传参传入
handle({
cb,
rejectCb, // 可能为 undefined
resolve_,
reject_,
});
});
}
// catch 其实也是返回一个新的 promise 语法糖
this.catch = function(cb) {
return this.then(null, cb);
}
fn(resolve, reject);
}
// My_promise.prototype.then = function(cb) {
// // cb的执行本身是异步的
// // 返回一个新的promise
// return new My_promise((resolve, reject) => {
// setTimeout(() => {
// resolve(cb(this.resolveResult));
// });
// });
// }
// 链式调用,上级需要保留下级的cb,可以进行跳过调用处理
new My_promise((resolve, reject) => {
setTimeout(() => {
resolve({ test: 1 });
}, 1000)
}).then((data) => {
console.log('result1', data);
//dosomething
return test();
}).then((data) => {
console.log('result2', data);
}).catch((data) => {
console.log('reject1', data);
});
function test(id) {
return new My_promise(((resolve, reject) => {
setTimeout(() => {
reject({ test: 2 })
}, 5000)
}))};
generator函数
function* example() {
yield 1;
yield 2;
yield 3;
};
var iter=example();
iter.next();
// =>
var marked0$0 = [example].map(regeneratorRuntime.mark);
function example() {
return regeneratorRuntime.wrap(function example$(context$1$0) {
while (1) switch (context$1$0.prev = context$1$0.next) {
case 0:
context$1$0.next = 2;
return 1;
case 2:
context$1$0.next = 4;
return 2;
case 4:
context$1$0.next = 6;
return 3;
case 6:
case "end":
return context$1$0.stop();
}
}, marked0$0[0], this);
}
var iter = example();
iter.next();
通过对 Regenerator 转换后的生成器代码及工具源码分析,我们探究了生成器的运行原理。Regenerator 通过工具函数将生成器函数包装,为其添加如 next/return 等方法。同时也对返回的生成器对象进行包装,使得对 next 等方法的调用,最终进入由 switch case 组成的状态机模型中。除此之外,利用闭包技巧,保存生成器函数上下文信息。
上述过程与 C#中 yield 关键字的实现原理基本一致,都采用了编译转换思路,运用状态机模型,同时保存函数上下文信息,最终实现了新的 yield 关键字带来的新的语言特性。
async await
co库就是一个async await的模拟实现
v8 排序源码
// 稍等
**插入排序
css盒模型
标准模型+IE模型(怪异盒模型)。 包括margin,border,padding,content
ss3新增的属性 box-sizing: content-box | border-box分别设置盒模型为标准模型(content-box)和IE模型(border-box)。
.border-box设置为IE模型,它的元素宽度width=content + 2 padding + 2 border = 70px + 2 10px + 2 5px = 100px。
.content-box设置为标准模型,它的元素宽度width=100px。
dom.style.width/height只能取到行内样式的宽和高,style标签中和link外链的样式取不到。dom.currentStyle.width/height取到的是最终渲染后的宽和高,只有IE支持此属性。window.getComputedStyle(dom).width/height同(2)但是多浏览器支持,IE9以上支持。dom.getBoundingClientRect().width/height也是得到渲染后的宽和高,大多浏览器支持。IE9以上支持,除此外还可以取到相对于视窗的上下左右的距离
当两个垂直外边距相遇时,他们将形成一个外边距,合并后的外边距高度等于两个发生合并的外边距的高度中的较大者。注意:只有普通文档流中块框的垂直外边距才会发生外边距合并,行内框、浮动框或绝对定位之间的外边距不会合并
html文档流
常见定位方案
- 普通流 (normal flow)
在普通流中,元素按照其在 HTML 中的先后位置至上而下布局,在这个过程中,行内元素水平排列,直到当行被占满然后换行,块级元素则会被渲染为完整的一个新行,除非另外指定,否则所有元素默认都是普通流定位,也可以说,普通流中元素的位置由该元素在 HTML 文档中的位置决定。
- 浮动 (float)
在浮动布局中,元素首先按照普通流的位置出现,然后根据浮动的方向尽可能的向左边或右边偏移,其效果与印刷排版中的文本环绕相似。
- 绝对定位 (absolute positioning)
在绝对定位布局中,元素会整体脱离普通流,因此绝对定位元素不会对其兄弟元素造成影响,而元素具体的位置由绝对定位的坐标决定。
BFC 概念
Formatting context(格式化上下文) 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。
那么 BFC 是什么呢?
BFC 即 Block Formatting Contexts (块级格式化上下文),它属于上述定位方案的普通流。
**具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。 **
通俗一点来讲,可以把 BFC 理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。
BFC的原理(渲染规则)
- BFC元素垂直方向的边距会发生重叠。属于不同BFC外边距不会发生重叠
- BFC的区域不会与浮动元素的布局重叠。
- BFC元素是一个独立的容器,外面的元素不会影响里面的元素。里面的元素也不会影响外面的元素。
- 计算BFC高度的时候,浮动元素也会参与计算(清除浮动)
如何创建BFC 只要元素满足下面任一条件即可触发 BFC 特性:
- body 根元素
- 浮动元素:float 除 none 以外的值
- 绝对定位元素:position (absolute、fixed)
- display 为 inline-block、table-cells、flex
- overflow 除了 visible 以外的值 (hidden、auto、scroll)
BFC创建了一个独立的环境,这个环境中的元素不会影响到其他环境中的布局,所以BFC内的外边距不与外部的外边距发生重叠。
BFC 可以包含浮动的元素(清除浮动)
我们都知道,浮动的元素会脱离普通文档流,来看下下面一个例子\
<div style="border: 1px solid #000;">
<div style="width: 100px;height: 100px;background: #eee;float: left;"></div>
</div>
由于容器内元素浮动,脱离了文档流,所以容器只剩下 2px 的边距高度。如果使触发容器的 BFC,那么容器将会包裹着浮动元素。 ***父元素使用了overflow: hidden 进行了浮动清除 使得父元素的bfc也可以包括浮动 ***
<div style="border: 1px solid #000;overflow: hidden">
<div style="width: 100px;height: 100px;background: #eee;float: left;"></div>
</div>
效果如图:
BFC 可以阻止元素被浮动元素覆盖
先来看一个文字环绕效果:\
<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>
这时候其实第二个元素有部分被浮动元素所覆盖,(但是文本信息不会被浮动元素所覆盖) 如果想避免元素被覆盖,可触第二个元素的 BFC 特性,在第二个元素中加入 overflow: hidden,就会变成:
这个方法可以用来实现两列自适应布局,效果不错,这时候左边的宽度固定,右边的内容自适应宽度(去掉上面右边内容的宽度)。
IFC 行内元素布局
先了解
- line-height的数字值是和font-size大小相关的;
- vertical-align的百分比值是和line-height值相关的;
图片下面有一明显的红色条条,😡什么鬼,很诡异不是么,我要的是img充满整个div啊!!!好吧,我加了一个额外的inline元素填写内容Xx,发现原来多出来的那一块正好是文字的下半空白部分。吆喝,这么巧?实际上,如果将这里的Xx内容去掉,只剩下img,那个条条依然存在,表现行为好像父元素div里面除了img元素还有一个空白的元素一样,😕姑且叫它空白节点吧(肉眼中不存在却在影响着布局),这个是比较诡异的一个表现,查标准没找到有相关的说明。但请将这个空白节点先记住,我们的重点是研究条条是咋出来的。这条条看上去貌似是文本和图片垂直方向上对齐生成的,那么这就引粗来一个问题,inline元素默认的垂直方向的对齐方式是什么样的?也就是vertical-align的默认值是啥?
OK,🤣I know you know。vertical-align默认值是baseline,OK,那就先来挖一挖vertical-align具体是个什么鬼。
baseline字面意思就是基线,何为基线?首先请记住下面这几个概念:
- 基线:小写字母'x'的下边缘所在的那条线;
- x-height: 小写字母'x'的高度;
- ex: 1ex就是一个小写字母'x'的高度,类似em单位,注意,ex和em都是相对单位;
当元素的 CSS 属性
display的计算值为inline,inline-block或inline-table时,称它为行内级元素。行内级元素生成行内级盒(inline-level boxes) ,参与行内格式化上下文(inline formatting context)。同时参与生成行内格式化上下文的行内级盒称为行内盒(Inline boxes)。
规范里IFC文字很多,提炼下我们需要的:
如果一个矩形区域,包含着一些排成一条线的盒子,称为line box。
一个line box的宽度,由他的包含块(containg block)和floats的存在情况决定。line box的高度,由你给出的代码决定。(line-height),即所有inline box的最大高度差。
当盒子的高度小于父级盒子高度时,垂直方向的对齐'vertical-align'属性决定。
内联元素是有两个高度的content-area高度(background-color实际渲染的那个高度)和 virtual-area 高度(实际区域占空间的高度也就是line-height)
-
所有的内联元素都有两个高度
- 基于字体度量的 content-area
- virtual-area(也就是 line-height )
-
内联元素都有一个
空白节点存在着来确定基线等概念; -
基线的确定和字体有关,和内部的内联元素无关;
-
IFC很难懂;
-
line-box(行盒) 的高度的受其子元素的 line-height 和 vertical-align 的影响;
-
我们貌似没法用CSS来更改字体度量。
各种线
line-height作用范围以及默认作用vertical-align: baseline
inline:如文字
默认font-size下x的基线为基准,以各自的基线进行对齐,但注意不同font-size和font-family的文字的基线是不一样的
inline-block:
默认font-size下x的基线为基准,如果里面没有inline元素(类似img,或者空span设置了display:inline-block),或者overflow不是visible,该元素的基线就是其margin的底;如果有inline元素,其基线就是最后一行inline元素的基线
现在可以解决我们的疑问了.文字和图片是按照基线来对齐的,因此下面出现的空隙就是半行间距
终于松了一口气,可是当我们把img标签前面的文字去掉, 却惊人地发现,效果还是和原来一样,空隙依然存在!!!
<div class="box">
<img src="3.png" alt="">
</div>
123
123
要理解这一点,就要引入另外两个概念:行内框盒子模型与幽灵空白节点
幽灵空白节点
浏览器在渲染的时候,会每一个行框盒子(linebox)前面渲染一个宽度为零高度为行高的空白节点,就跟文字的表现一样
我们现在来解释一个图片的间隙:
首先:浏览器有默认的font-size,如在chrome中为16px,
其次:line-height不作用于外部的div,作用于内部的元素,line-heiht默认的属性值为normal, 为font-size的1.0~1.2倍,具体由浏览器决定,导致了行间距和半行间距大于0
再次:文字和图片按基线对齐,因此底部会出现宽度为半行距的空隙
最后:由于浏览器的渲染机制,导致空白节点的表现和文字的变现一样.
搞清楚原理之后,我们可以着手来消除这个间隙
方法一:设置vertical-align:top/middle/bottom
.box > img {
vertical-align: middle;
}
方法二:将img设置为block元素
.box > img {
display: block;
}
方法三:去除行间距,line-height或者font-size为0
.box {
line-height: 0;
/*font-size: 0;*/
}
flex 和 grid 布局
讲到布局,我们就会想到 flex 布局,甚至有人认为竟然有 flex 布局了,似乎没有必要去了解 Grid 布局。但 flex 布局和 Grid 布局有实质的区别,那就是 flex 布局是一维布局,Grid 布局是二维布局。flex 布局一次只能处理一个维度上的元素布局,一行或者一列。Grid 布局是将容器划分成了“行”和“列”,产生了一个个的网格,我们可以将网格元素放在与这些行和列相关的位置上,从而达到我们布局的目的。
Grid 布局远比 flex 布局强大!
flex布局示例:
Grid 布局示例:
display: grid;
// **auto-fill 关键字**:表示自动填充,让一行(或者一列)中尽可能的容纳更多的单元格
grid-template-columns: repeat(auto-fill, 200px);
grid-gap: 5px;
grid-auto-rows: 50px;
css权重
权重记忆口诀:从0开始,一个行内样式+1000,一个id选择器+100,一个属性选择器、class或者伪类+10,一个元素选择器,或者伪元素+1,通配符+0。
双飞翼/圣杯布局
圣呗布局和双飞翼布局从字面意思来看是这样的:
一个像圣杯或者像展翅的禽类这样的布局
通俗的来说就是左右两栏固定宽度,中间部分自适应的三栏布局。
两者本质
// 圣杯布局
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>圣杯布局</title> <style> * { margin: 0; padding: 0; } .header, .footer { height: 100px; line-height: 100px; background-color: green; text-align: center; font-size: 30px; font-weight: bolder; } .footer { background-color: goldenrod; } .container { padding: 0 220px 0 200px; overflow: hidden; } .left, .middle, .right { position: relative; float: left; min-height: 130px; word-break: break-all; } .left { margin-left: -100%; left: -200px; width: 200px; background-color: red; } .right { margin-left: -220px; right: -220px; width: 220px; background-color: green; } .middle { width: 100%; background-color: blue; } </style> </head> <body> <div class="header">header</div> <div class="container"> <div class="middle"> <h4>middle</h4> <p> middlemiddlemiddlemiddlemiddlemiddlemiddlemiddle middlemiddlemiddlemiddlemiddlemiddlemiddlemiddle middlemiddlemiddlemiddlemiddlemiddlemiddlemiddle middlemiddlemiddlemiddlemiddlemiddlemiddlemiddle middlemiddlemiddlemiddlemiddlemiddlemiddlemiddle middlemiddlemiddlemiddlemiddle </p> </div> <div class="left"> <h4>left</h4> <p> leftleftleftleftleftleftleftleftleftleftleftleft leftleftleftleftleftleftleftleftleftleftleftleft leftleftleftleftleftleftleftleftleftleftleftleft </p> </div> <div class="right"> <h4>right</h4> <p> rightrightrightrightrightrightrightrightrightright rightrightrightrightrightrightrightrightrightright rightrightrightrightrightrightright </p> </div> </div> <div class="footer">footer</div> </body> </html>
margin负值的原理
为了方便理解负值margin,我们引入参考线的定义,参考线就是就是margin移动的基准点,而margin的值就是移动的数值。
margin的参考线有两类,一类是top、left,它们以外元素作为参考线,另一类是right、bottom,它们以自身作为参考线。
简单点说就是:
- top负值就是以包含块(Containing block) 内容区域的上边或者上方相连元素 margin 的下边为参考线;
- left负值就是以包含块(Containing block) 内容区域的左边或者左方相连元素 margin 的右边为参考线;
- right负值就是以元素本身border的右边为参考线;
- bottom负值就是以元素本身border的下边为参考线;
圣杯布局
- 首先把left、middle、right都放出来
- 给它们三个设置上float: left, 脱离文档流;
- 一定记得给container设置上overflow: hidden; 可以形成BFC撑开文档
- left、right设置上各自的宽度
- middle设置width: 100%;
接下来比较重要了:
- 给left、middle、right设置position: relative;
- left设置 left: -leftWidth, right设置 right: -rightWidth;
- container设置padding: 0, rightWidth, 0, leftWidth;
双飞翼布局
双飞翼布局和圣杯布局很类似,不过是在middle的div里又插入一个div,通过调整内部div的margin值,实现中间栏自适应,内容写到内部div中。
这样可以先做好主体部分,然后再将附属部分放到合适的位置!
- 首先把left、middle、right都放出来, middle中增加inner
- 给它们三个设置上float: left, 脱离文档流;
- 一定记得给container设置上overflow: hidden; 可以形成BFC撑开文档
- left、right设置上各自的宽度
- middle设置width: 100%;
接下来与圣杯布局不一样的地方:
- left设置 margin-left: -100%, right设置 right: -rightWidth;
- middle中放置一个div设置padding: 0, rightWidth, 0, leftWidth;
css3 新特性
过渡
transition: CSS属性,花费时间,效果曲线(默认ease),延迟时间(默认0)
/*宽度从原始值到制定值的一个过渡,运动曲线ease,运动时间0.5秒,0.2秒后执行过渡*/
transition:width,.5s,ease,.2s
transition-property: width;
transition-duration: 1s;
transition-timing-function: linear;
transition-delay: 2s;
动画
animation:动画名称,一个周期花费时间,运动曲线(默认ease),动画延迟(默认0),播放次数(默认1),是否反向播放动画(默认normal),是否暂停动画(默认running)
/*执行一次logo2-line动画,运动时间2秒,运动曲线为 linear*/
animation: logo2-line 2s linear;
/*无限执行logo2-line动画,每次运动时间2秒,运动曲线为 linear,并且执行反向动画*/
animation: logo2-line 2s linear alternate infinite;
animation-fill-mode : none | forwards | backwards | both;
/*none:不改变默认行为。
forwards :当动画完成后,保持最后一个属性值(在最后一个关键帧中定义)。
backwards:在 animation-delay 所指定的一段时间内,在动画显示之前,应用开始属性值(在第一个关键帧中定义)。
both:向前和向后填充模式都被应用。 */
形状转换
transform:适用于2D或3D转换的元素
transform-origin:转换元素的位置(围绕那个点进行转换)。默认(x,y,z):(50%,50%,0)
rotate 旋转
transform:rotate(30deg);
translate 移动
transform:translate(30px,30px);
scale 缩放
transform:scale(.8);
skew 倾斜转换
transform: skew(10deg,10deg);\
transform:rotateX(180deg);
transform:rotateY(180deg);
transform:rotate3d(10,10,10,90deg);
css3选择器
阴影 box-shadow
box-shadow: 水平阴影的位置 垂直阴影的位置 模糊距离 阴影的大小 阴影的颜色 阴影开始方向(默认是从里往外,设置inset就是从外往里);
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<style>
div
{
width:300px;
height:100px;
background:#09f;
box-shadow: 10px 10px 5px #888888;
}
</style>
</head>
<body>
<div></div>
</body>
</html>
边框
图片 border-image: 图片url 图像边界向内偏移 图像边界的宽度(默认为边框的宽度) 用于指定在边框外部绘制偏移的量(默认0) 铺满方式--重复(repeat)、拉伸(stretch)或铺满(round)(默认:拉伸(stretch));
border-radius: n1,n2,n3,n4;
border-radius: n1,n2,n3,n4/n1,n2,n3,n4;
/*n1-n4四个值的顺序是:左上角,右上角,右下角,左下角。*/
背景
background-clip
background-origin
background-size
反射
-webkit-box-reflect:方向[ above-上 | below-下 | right-右 | left-左 ],偏移量,遮罩图片
html
<p>下倒影</p>
<p class="reflect-bottom-p"><img src="test.jpg" class="reflect-bottom"></p>
css
.reflect-bottom-p {
padding-bottom: 300px;
}
.reflect-bottom {
-webkit-box-reflect: below;
}
文字
word-break: normal|break-all|keep-all; // 解决英文不自动换行
颜色
rgba hsla
渐变
Filter(滤镜)
<p>原图</p>
<img src="test.jpg" />
<p>黑白色filter: grayscale(100%)</p>
<img src="test.jpg" style="filter: grayscale(100%);"/>
<p>褐色filter:sepia(1)</p>
<img src="test.jpg" style="filter:sepia(1);"/>
<p>饱和度saturate(2)</p>
<img src="test.jpg" style="filter:saturate(2);"/>
<p>色相旋转hue-rotate(90deg)</p>
<img src="test.jpg" style="filter:hue-rotate(90deg);"/>
<p>反色filter:invert(1)</p>
<img src="test.jpg" style="filter:invert(1);"/>
<p>透明度opacity(.5)</p>
<img src="test.jpg" style="filter:opacity(.5);"/>
<p>亮度brightness(.5)</p>
<img src="test.jpg" style="filter:brightness(.5);"/>
<p>对比度contrast(2)</p>
<img src="test.jpg" style="filter:contrast(2);"/>
<p>模糊blur(3px)</p>
<img src="test.jpg" style="filter:blur(3px);"/>
<p>阴影drop-shadow(5px 5px 5px #000)</p>
<img src="test.jpg" style="filter:drop-shadow(5px 5px 5px #000);"/>
布局
flex grid
多列布局
html
<div class="newspaper">
当我年轻的时候,我梦想改变这个世界;当我成熟以后,我发现我不能够改变这个世界,我将目光缩短了些,决定只改变我的国家;当我进入暮年以后,我发现我不能够改变我们的国家,我的最后愿望仅仅是改变一下我的家庭,但是,这也不可能。当我现在躺在床上,行将就木时,我突然意识到:如果一开始我仅仅去改变我自己,然后,我可能改变我的家庭;在家人的帮助和鼓励下,我可能为国家做一些事情;然后,谁知道呢?我甚至可能改变这个世界。
</div>
css
.newspaper
{
column-count: 3;
-webkit-column-count: 3;
-moz-column-count: 3;
column-rule:2px solid #000;
-webkit-column-rule:2px solid #000;
-mox-column-rule:2px solid #000;
}
盒模型
box-sizing
媒体查询
混合模式
css3的混合模式,两个(background-blend-mode和mix-blend-mode)。这两个写法和显示效果都非常像!区别就在于background-blend-mode是用于同一个元素的背景图片和背景颜色的。mix-blend-mode用于一个元素的背景图片或者颜色和子元素的。看以下代码,区别就出来了!
background-blend-mode
代码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<style>
div{
width: 480px;
height: 300px;
background:url('test.jpg')no-repeat,#09f;
}
</style>
<body>
<!---->
<p>原图</p>
<div></div>
<p>multiply正片叠底</p>
<div style="background-blend-mode: multiply;"></div>
<p>screen滤色</p>
<div style="background-blend-mode: screen;"></div>
<p>overlay叠加</p>
<div style="background-blend-mode: overlay;"></div>
<p>darken变暗</p>
<div style="background-blend-mode: darken;"></div>
<p>lighten变亮</p>
<div style="background-blend-mode: lighten;"></div>
<p>color-dodge颜色减淡模式</p>
<div style="background-blend-mode: color-dodge;"></div>
<p>color-burn颜色加深</p>
<div style="background-blend-mode: color-burn;"></div>
<p>hard-light强光</p>
<div style="background-blend-mode: hard-light;"></div>
<p>soft-light柔光</p>
<div style="background-blend-mode: soft-light;"></div>
<p>difference差值</p>
<div style="background-blend-mode: difference;"></div>
<p>exclusion排除</p>
<div style="background-blend-mode: exclusion;"></div>
<p>hue色相</p>
<div style="background-blend-mode: hue;"></div>
<p>saturation饱和度</p>
<div style="background-blend-mode: saturation;"></div>
<p>color颜色</p>
<div style="background-blend-mode: color;"></div>
<p>luminosity亮度</p>
<div style="background-blend-mode: luminosity;"></div>
</body>
</html>
css样式隔离(模块化)
- 手写源生 CSS
- 使用预处理器 Sass/Less
- 使用后处理器 PostCSS
- 使用 css modules
- 使用 css in js
使用预处理器 Sass/Less
随着时间的不断发展,我们逐渐发现,编写源生的 css 其实并不友好,例如:源生的 css 不支持变量,不支持嵌套,不支持父选择器等等,这些种种问题,催生出了像 sass/less 这样的预处理器。
预处理器主要是强化了 css 的语法,弥补了上文说了这些问题,但本质上,打包出来的结果和源生的 css 都是一样的,只是对开发者友好,写起来更顺滑。
后处理器 PostCSS
随着前端工程化的不断发展,越来越多的工具被前端大佬们开发出来,愿景是把所有的重复性的工作都交给机器去做,在 css 领域就产生了 postcss。
postcss 可以称作为 css 界的 babel,它的实现原理是通过 ast 去分析我们的 css 代码,然后将分析的结果进行处理,从而衍生出了许多种处理 css 的使用场景。
常用的 postcss 使用场景有:
- 配合 stylelint 校验 css 语法
- 自动增加浏览器前缀 autoprefixer
- 编译 css next 的语法
CSS 模块化的实现方式
BEM 命名规范
BEM 的意思就是块(block)、元素(element)、修饰符(modifier)。是由 Yandex 团队提出的一种前端命名方法论。这种巧妙的命名方法让你的 css 类对其他开发者来说更加透明而且更有意义。
BEM 的命名规范如下:
/* 块即是通常所说的 Web 应用开发中的组件或模块。每个块在逻辑上和功能上都是相互独立的。 */
.block {
}
/* 元素是块中的组成部分。元素不能离开块来使用。BEM 不推荐在元素中嵌套其他元素。 */
.block__element {
}
/* 修饰符用来定义块或元素的外观和行为。同样的块在应用不同的修饰符之后,会有不同的外观 */
.block--modifier {
}
复制代码
通过 bem 的命名方式,可以让我们的 css 代码层次结构清晰,通过严格的命名也可以解决命名冲突的问题,但也不能完全避免,毕竟只是一个命名约束,不按规范写照样能运行。
CSS 模块化的实现方式
BEM 命名规范
BEM 的意思就是块(block)、元素(element)、修饰符(modifier)。是由 Yandex 团队提出的一种前端命名方法论。这种巧妙的命名方法让你的 css 类对其他开发者来说更加透明而且更有意义。
BEM 的命名规范如下:
/* 块即是通常所说的 Web 应用开发中的组件或模块。每个块在逻辑上和功能上都是相互独立的。 */
.block {
}
/* 元素是块中的组成部分。元素不能离开块来使用。BEM 不推荐在元素中嵌套其他元素。 */
.block__element {
}
/* 修饰符用来定义块或元素的外观和行为。同样的块在应用不同的修饰符之后,会有不同的外观 */
.block--modifier {
}
复制代码
通过 bem 的命名方式,可以让我们的 css 代码层次结构清晰,通过严格的命名也可以解决命名冲突的问题,但也不能完全避免,毕竟只是一个命名约束,不按规范写照样能运行。
CSS Modules
CSS Modules 指的是我们像 import js 一样去引入我们的 css 代码,代码中的每一个类名都是引入对象的一个属性,通过这种方式,即可在使用时明确指定所引用的 css 样式。
并且 CSS Modules 在打包的时候会自动将类名转换成 hash 值,完全杜绝 css 类名冲突的问题。
使用方式如下:
1、定义 css 文件。
.className {
color: green;
}
/* 编写全局样式 */
:global(.className) {
color: red;
}
/* 样式复用 */
.otherClassName {
composes: className;
color: yellow;
}
.otherClassName {
composes: className from "./style.css";
}
复制代码
2、在 js 模块中导入 css 文件。
import styles from "./style.css";
element.innerHTML = '<div class="' + styles.className + '">';
复制代码
3、配置 css-loader 打包。
CSS Modules 不能直接使用,而是需要进行打包,一般通过配置 css-loader 中的 modules 属性即可完成 css modules 的配置。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.css$/,
use:{
loader: 'css-loader',
options: {
modules: {
// 自定义 hash 名称
localIdentName: '[path][name]__[local]--[hash:base64:5]',
}
}
}
]
}
};
复制代码
4、最终打包出来的 css 类名就是由一长串 hash 值生成。
._2DHwuiHWMnKTOYG45T0x34 {
color: red;
}
._10B-buq6_BEOTOl9urIjf8 {
background-color: blue;
}
CSS In JS
CSS in JS,意思就是使用 js 语言写 css,完全不需要些单独的 css 文件,所有的 css 代码全部放在组件内部,以实现 css 的模块化。
CSS in JS 其实是一种编写思想,目前已经有超过 40 多种方案的实现,最出名的是 styled-components。
使用方式如下:
import React from "react";
import styled from "styled-components";
// 创建一个带样式的 h1 标签
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
// 创建一个带样式的 section 标签
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
// 通过属性动态定义样式
const Button = styled.button`
background: ${props => (props.primary ? "palevioletred" : "white")};
color: ${props => (props.primary ? "white" : "palevioletred")};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
// 样式复用
const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;
<Wrapper>
<Title>Hello World, this is my first styled component!</Title>
<Button primary>Primary</Button>
</Wrapper>;
复制代码
可以看到,我们直接在 js 中编写 css,案例中在定义源生 html 时就创建好了样式,在使用的时候就可以渲染出带样式的组件了。
除此之外,还有其他比较出名的库:
- emotion
- radium
- glamorous
css性能优化
> 1. 避免使用@import,外部的css文件中使用@import会使得页面在加载时增加额外的延迟。
首先,使用@import引入css会影响浏览器的并行下载。使用@import引用的css文件只有在引用它的那个css文件被下载,解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析,构建render tree等一系列操作,这就导致浏览器无法并行下载
所需的样式文件。
其次,多个@import会导致下载顺序紊乱,在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件优先于@import下载,并且打乱甚至破坏@import自身的并行下载。
所以不要使用这一方法,使用link标签就行了。
> 2.避免过分重排
- 浏览器为了重新渲染部分或整个页面,重新计算页面元素位置和几何结构的进程叫做reflow
- 浏览器根据定义好的样式来计算,并将元素放到该出现的位置上,这个过程叫做reflow
- 页面上任何一个节点触发来reflow,会导致他的子节点和祖先节点重新渲染
- 导致reflow发生的情况
- 改变窗口的大小
- 改变文字的大小
- 添加 删除样式表
- 内容的改变 输入框输入内容也会
- 伪类的激活
- 操作class属性
- 脚本操作dom js改变css类
- 计算offsetWidth和offsetHeight
- 设置style属性 10.改变元素的内外边距
- 常见重排元素
1. 大小有关的 width,height,padding,margin,border-width,border,min-height
2. 布局有关的 display,top,position,float,left,right,bottom
3. 字体有关的 font-size,text-align,font-weight,font-family,line-height,white-space,vertical-align
4. 隐藏有关的 overflow,overflow-x,overflow-y
- 减少reflow对性能的影响的建议
1. 不要一条条的修改dom的样式,预先定义好class,然后修改dom的classname
2. 不要修改影响范围较大的dom
3. 为动画元素使用绝对定位
4. 不要table布局,因为一个很小的改动会造成整个table重新布局
5. 避免设置大量的style属性,通过设置style属性改变节点样式的话,每一次设置都会触发一次reflow,所以最好使用class属性
6. 如果css里面有计算表达式,每次都会重新计算一遍,触发一次reflow
> 用repaint来取代reflow
- 当一个元素的外观被改变,但是布局没有改变的情况
- 当元素改变的时候,不影响元素在页面中的位置,浏览器仅仅会用新的样式重绘此元素
- 常见的重绘元素
- 颜色 color,background
- 边框样式 border-style,outline-color,outline,outline-style,border-radius,box-shadow,outline-width
- 背景有关 background,backgound-image,background-position,background-repeat,background-size
> 使用CSS动画
- css动画启用GPU加速,应用GPU的图形性能对浏览器中的一些图形操作交给GPU完成。canvas2D,布局合成,css3转换,css3d变换,webGL,视频
- 2d加速
- 3d加速
css文件压缩
性能优化时最容易想到的,也是最常见的方法,就是文件压缩,这一方案往往效果显著
文件的大小会直接影响浏览器的加载速度,这一点在网络较差时表现尤为明显,构建工具webpack,gulp/grunt,rollup,压缩之后能够明显减少,可以大大降低浏览器的加载时间。
去除无用CSS
虽然文件压缩能够降低文件大小,但css文件压缩通常只会去除无用的空格,这样就限制来css文件的压缩比例。如果压缩后的文件仍然超过来预期的大小,可以试着找到并删除代码中无用的css。
一般情况下,会存在这两种无用的CSS代码:
- 不同元素或者其他情况下的重复代码,
- 整个页面内没有生效的CSS代码
有选择地使用选择器
css选择器的匹配是从右向左进行的,这一策略导致来不同种类的选择器之间的性能也存在差异。相比于 #markdown-content-h3,显然使用 #markdown.content h3时,浏览器生成渲染树所要花费的时间更多。因为后者需要先找到DOM中的所有h3元素,再过滤掉祖先元素不是.content的,最后过滤掉.content不是#markdown的。试想,页面中的元素更多,那么匹配所要花费的时间代价自然更高。
显得浏览器在这一方面做了很多优化,不同选择器的性能差别并不明显,甚至可以说差别甚微,此外不同选择器在不同浏览器中的性能表现也不统一,在编写css的时候无法兼顾每种浏览器,鉴于这两点,在使用选择器时,尽量记住以下几点:
1. 保持简单,不要使用嵌套过多过于复杂的选择器
2. 通配符和属性选择器效率最低,需要匹配的元素最多,尽量避免使用。
3. 不要使用类选择器和ID选择器修饰元素标签,如:h3#markdown-content,这一多此一举,还会降低效率
4. 不要为了追求速度而放弃可读性和可维护性
减少使用昂贵的属性
在浏览器绘制屏幕时,所有需要浏览器进行操作或计算的属性相对而言都需要花费更大的代价,而页面发生重绘时,它们会降低浏览器的渲染性能。所以在编写css时,应该尽量减少使用昂贵属性,如:
box-shadow, border-radius, filter, 透明度, :nth-child等
当然并不是不要使用这些属性,这些都是经常使用的属性,只是这里可以作为一个了解。当有其他方案可以选择的时候,可以优先选择没有昂贵属性或昂贵属性更少的方案,这一网站的性能会在不知不觉中得到一定的提升。
硬件加速的好坏
- 仅仅依靠GPU还是不行的,许多动画还是需要CPU的介入,连接cpu和GPU的总带宽不是无限的,所以需要注意数据在cpu和GPU之间的传输,尽量避免造成通道的拥挤,要一直注意像素的传输。
- 一个重点是了解创建的合成层的数量,每一个层都对应来一个GPU纹理,太多的层会消耗很多内存。
**chrome://flags/#composited-layer-borders**观察的地址。- 每一个dom元素的合成层都会被标记一个额外的边框,这一就可以验证是否有了很多层
- 另一个重点是保持GPU和CPU之间传输量达到最小值,也就是说,层的更新数量最好是一个理想的常量,每次层更新的时候,一堆新的像素就可能需要传输给GPU。
- 因为为了高性能,动画开始之后避免层的更新也是非常重要的,避免动画进行中其他层一直更新导致拥堵。
- 也就是使用这些css属性来实现动画:transformation, opacity, filter
- 使用性能工具检测优化的合理性,timeline检测优化是否合理,还需要实现自动操作来做性能回归测试。
- 检测层数和层更新次数是非常有用的。
层叠上下文
层叠上下文,英文称作”stacking context”. 是HTML中的一个三维的概念。如果一个元素含有层叠上下文,我们可以理解为这个元素在z轴上就“高人一等”。 普通元素的层叠水平优先由层叠上下文决定,因此,层叠水平的比较只有在当前层叠上下文元素中才有意义
div居中
flex 布局实现 (元素已知宽度)
CSS 代码:
<style>
.box{
width: 300px;
height: 300px;
background-color: #ccc;
display: flex;
display: -webkit-flex;
justify-content: center;
align-items: center;
}
.box .a{
width: 100px;
height: 100px;
background-color: blue;
}
</style>
HTML 代码:
<div class="box">
<div class="a"></div>
</div>
position (元素已知宽度)
父元素设置为:position: relative;
子元素设置为:position: absolute;
距上50%,据左50%,然后减去元素自身宽度的一半距离就可以实现
position transform (元素未知宽度)
如果元素未知宽度,只需将上面例子中的 margin: -50px 0 0 -50px;替换为:transform: translate(-50%,-50%);
position(元素已知宽度)(left,right,top,bottom为0,maigin:auto )
table-cell 布局实现
CSS:
<style>
.box{
width: 300px;
height: 300px;
background-color: red;
display: table-cell;
vertical-align: middle;
}
.box .a{
margin-left: 100px;
width: 100px;
height: 100px;
background-color: blue;
}
</style>
<div class="box">
<div class="a">love</div>
</div>
css浮动
什么是浮动元素: 浮动元素同时处于常规流内和流外的元素。其中块级元素认为浮动元素不存在,而浮动元素会影响行内元素的布局,浮动元素会通过影响行内元素间接影响了包含块的布局。
常规流: 页面上从左往右,从上往下排列的元素流,就是常规流
脱离常规流: 绝对定位,fixed定位的元素有自己固定的位置,脱离了常规流
包含块: 一个元素离它最近的块级元素是它的包含块
闭合浮动的方法:
- BFC: 1) 包含块设置overflow:hidden 或者 2)包含块设置display:table-cell/table/flex...
- 伪元素:
.clearfix::after {
content: '';
display: block;
clear: both;
}
- 包含块自己也浮动
这个方法也是w3c使用的方法。不过,下一个元素会受到这个浮动元素的影响。为了解决这个问题,有些人选择对布局中的所有东西进行浮动,然后使用适当的有意义的元素(常常是站点的页脚)对这些浮动进行清理。这有助于减少或消除不必要的标记。