JavaScript
# JavaScript的编译,上下文context
# 过程
- 预编译期
- 浏览器的js引擎解析js代码
- 建立arguments对象(隐藏对象,不可见),函数,参数,变量
- 建立作用域链
- 确定this指向
- 执行期
- 按从上到下的顺序执行代码
- 函数体的预解析发生在函数被调用之时,被调用时先进行函数体的预编译,然后按顺序进行执行
# 上下文context
- 指的是一种运行环境
- context指的是,函数被调用时,看this指向哪个object,这个object就是当前的上下文。
# 执行上下文
概念
- 用来区分不同的运行环境,需要引出一个概念:执行上下文(Execution Context)
- 他是一个对象,由js的执行引擎创建
- 有三个属性:变量对象,作用域链,this指针
上下文栈
- js执行过程中有一个上下文栈,存放的是不同的上下文对象(不同的js运行环境)。
- 当前执行代码的context对象总是在栈顶。
变量对象
- 变量对象的创建过程如下:(变量提升被提出的原因)
- 创建arguments对象,其中保存多个属性,属性的key是0,1,2... value即传入参数的实际值。
- 找到这个作用域内的所有var和function,作为属性存储在变量对象中,如果是func,则属性名是 函数名,属性值是函数引用。 如果是var,属性名是变量名,属性值是undefined
- 变量对象的创建过程如下:(变量提升被提出的原因)
# 数据类型
# 基本数据类型
- undefined, number string null boolean + object。ES6新增Symbol
# this指向(new,隐式绑定,显式绑定)
- 创建一个空对象 obj;
- 将新创建的空对象的隐式原型指向其构造函数的显示原型。
- 使用 call或apply 改变 this 的指向
- 如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接直接返回该对象。
- new一个对象的原理
- 创建空对象 var obj = newObject()
- 让Person中的this指向obj,并执行Person这个构造函数。var result = Person.call(obj);
- 设置原型链,将obj的__proto__指向Person函数对象的prototype成员对象。obj.proto = Person.prototype;
- 判断result的返回值类型,如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。(因此调用call方法的 时候可能返回了this,也可能没有返回)
function Person(){ // let this = { // __proto__: Person.prototype // } // 创建一个新对象,赋值this,这一步是隐性的 // 给this指向的对象赋予构造属性 this.name = name this.age = age // 如果没有手动返回对象,则默认返回this指向的这个对象,也是隐性 // return this } const person = new Person()
1
2
3
4
5
6
7
8
9
10
11
12- 实现一个new方法
// 构造器函数 let Parent = function(name, age) { this.name = name this.age = age } Parent.prototype.sayName = function(){ console.log(this.name) } // 自己定义的new方法 let newFoo = function(Parent, ...rest) { // 以构造器的prototype属性为原型,创建新对象 let child = Object.create(Parent.prototype) // 将this和调用参数传给构造器执行 let result = Parent.apply(child, rest) // 如果构造器没有手动返回对象,则返回第一步的对象 return typeof result === 'object'? result:child; } // 创建实例,将构造函数Parent与形参作为参数传入 const child = newFoo(Parent, 'jack', 26) child.sayName() //'jack' // 检验,与使用new效果相同 child.instanceof Parent //t child.hasOwnProperty('name') //t child.hasOwnProperty('age') //t child.hasOwnProperty('sayName') //f
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
# 关于this指向的问题
- 要记住几句话:
- this 永远指向最后调用它的那个对象
- 匿名函数的 this 永远指向 window
- 没有挂载在任何对象上的函数,非严格模式下 this 指向 window
- 有关例子,请移步这里 (opens new window)
# 深拷贝 浅拷贝
- 浅拷贝(拷贝指针)
- 深拷贝(拷贝对象)
function deepCopy(obj){
//判断是否是简单数据类型,
if(typeof obj == "object"){
//复杂数据类型
var result = obj.constructor == Array ? [] : {};
for(let i in obj){
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
}else {
//简单数据类型 直接 == 赋值
var result = obj;
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# Function的call,apply,bind方法
- 作用:这三个方法的作用其实都是改变函数内this的指向。实质上,call和apply只是使用了Function的属性,并且调用方法而已。
- 实际用途:扩充变量的作用域
- 函数的一些基本知识:
- 函数本身都是对象
- 每个函数都有自带两个属性:length和prototype。
- 函数的arguments对象包含了要传递的参数
# call
使用:call(作用域,枚举参数)
作用:在特定的作用域里(一般是this)执行函数。
call与apply不同的是,call必须明确传入每一个参数(枚举)
调用的例子
function sum(num1, num2){ return num1 + num2 } function callSum1(num1, num2) { return sum.call(this, num1, num2) } alert(callSum1(10, 10)) // 20
1
2
3
4
5
6
7
8手写call
// 思路 替换原函数中的this为目标对象,将参数传给原函数,兼容null作目标对象,自动转换为window对象
// 将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
context = context || window //兼容传入null的情况
context.fn = this //将原函数添加到目标对象属性上
// es6写法的处理函数的参数,将具有length属性的对象 转成数组
let arg = [...arguments].slice(1)
let res = context.fn(...arg)
delete context.fn // 去除属性
return res
}
// test
const me = { name: 'Jack' }
function say() {
console.log(`My name is ${this.name || 'default'}`);
}
say.myCall(me)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# apply
使用:apply(作用域,参数)
作用:在特定的作用域里(一般是this)调用函数
调用的例子
// 思路 在context上调用方法,触发this绑定为context, 使用Symbol防止原有属性覆盖 function sum(num1, num2){ return num1 + num2 } function callSum1(num1, num2) { return sum.apply(this, arguments) } function callSum2(num1, num2) { return sum.apply(this, [num1, num2]) } alert(callSum1(10, 10)) // 20 alert(callSum2(10, 10)) // 20
1
2
3
4
5
6
7
8
9
10
11
12手写apply
// 将要改变this指向的方法挂到目标this上执行并返回
Function.prototype.myApply = function (context = globalThis) {
const key = Symbol('key')
context[key] = this
let res
if(arguments[1]) {
res = context[k](...arguments[1])
} else {
res = context[key]()
}
delete context[key]
return res
}
// test
const me = { name:'jack' }
function say() {
console.log('my name is ${this.name || 'default'}')
}
say.myApply(me)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# bind
使用:bind(绑定对象)
作用:把函数的this绑定到绑定对象上
调用的例子
window.color = 'red' var o = { color: 'blue' } function sayColor(){ alert(this.color) } var objectSayColor = sayColor().bind(o) objectSayColor(); // blue
1
2
3
4
5
6
7手写bind
Function.prototype.myBind = function (context = globalThis) {
const fn = this
const args = Array.from(arguments).slice(1)
const newFunc = function () {
const newArgs = args.concat(...arguments)
if (this instanceof newFunc) {
// 通过new调用,绑定this为实例对象
fn.apply(this, newArgs)
} else {
// 通过普通函数形式调用,绑定context
fn.apply(context, newArgs)
}
}
// 支持new调用方式
newFunc.prototype = object.create(fn.prototype)
return newFunc
}
// test
const me = { name:'jack' }
const other = { name:'jackson' }
function say(){
console.log('my name is ${this.name || 'default'}')
}
const mySay = say.bind(me)
mySay()
const otherSay = say.bind(other)
otherSay()
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
# 综合用例
第一道
var name = 'name1'; var obj = { name: 'name2', sayName: function(){ // 返回一个默认全局的函数 return function(){ console.log(this.name); }; }, changeName: function(){ // 返回一个默认全局的函数 setTimeout(function(){ this.name = 'name3'; // 然后将该函数绑定给this(当前obj对象) }.bind(this),0); } }; // obj.sayName()这个函数,让obj来调用 obj.sayName().call(obj); // 让this(也就是全局对象)来调用 obj.sayName().apply(this); obj.changeName(); setTimeout(function(){ // 输出更改之后,全局name的值 console.log(name); // 输出更改之后,obj对象中 name的值 console.log(obj.name); },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
30
31- 结果: name2 name1 name1 name3
第二道 Function.call.call.call
function f1() {
console.log(1)
}
function f2() {
console.log(2)
}
f1.call(f2) // 1
f1.call.call(f2) // 2
f1.call.call.call(f2) // window
2
3
4
5
6
7
8
9
# 闭包(概念 用途 手写)
# 概念
- 闭包是包含了那个局部变量的容器,它被内部函数对象引用。
- 特点是2个函数相互嵌套,内部函数引用了外部函数的局部变量,执行外部函数。
- 闭包的本质还是函数
# 作用
- 延长局部变量的生命周期
- 使函数外部可以多次间接操作到函数内部的数据。
# 应用
- 循环遍历监听
- IIFE定义模块
- jQuery内部
# 手写
# 同步与异步
# 同步
- 同步:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。如果在函数A返回 的时候,调用者就能够得到预期的结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数 就是同步的。
# 异步
- 不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步 任务可以执行了,该任务才会进入主线程执行。如果在函数A返回的时候,调用者还不能马上得到预期 的结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的 。
- 发起函数和回调函数
- 异步的执行机制:
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
- 主线程之外,还存在一个"任务队列"(task queue),只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
- 主线程不断重复上面的第三步
# ES5
# var 变量提升 函数提升
- var相当于全局变量,他的值可以被内部修改。用var声明的变量/函数在预编译时是有提升的。
- 变量提升 具体看这一则代码:
// 源码
function test(){
var n = 1
if(1){
var n = 2
}
console.log(n) // 输出2
}
test()
if(1){
var a = 100
}
console.log(a) // 输出100
// 预编译
var n // 此时n是undefined
var a // 此时a是undefined
function test(){
n = 1 // n被赋值1
if(1){
n = 2 // n被赋值1
}
console.log(n) //输出2
}
if(1){
a = 100 // a被赋值100
}
console.log(a) //输出100
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
- 函数提升。在js里函数的优先级是最高的,如果有同名的函数和变量,函数将优先被提升至作用域顶端。
var foo = 3
function test(){
console.log(foo); //function foo(){}
foo=5;
console.log(foo); // 5
function foo(){}
}
test();
console.log(foo) // 3
2
3
4
5
6
7
8
9
- 提升的意义
- 提升的意义是人为的,为了解决函数的相互递归问题,也就是两个函数的执行都要调用到对方。
# ES6
# 块级作用域 let const
# 继承
- 建立父类
function Person(name) {
this.name = name;
this.sum = function () {
alert(this.name);
}
}
Person.prototype.age = 10;
2
3
4
5
6
7
- 原型链继承
function Per() {
this.name = "ker";
}
Per.prototype = new Person();
var per1 = new Per();
console.log(per1.age);
// 判断元素是否在另一个元素的原型链上,per1继承了Person的属性
console.log(per1 instanceof Person); // true
2
3
4
5
6
7
8
让新实例的原型等于父类的实例。
缺点:
- 新实例无法向父类构造函数传参
- 继承单一
- 所有新实例共享父类实例属性。(原型上的属性是共享的,一个属性被修改,另一个也会被修改)
构造函数继承
function Con() {
Person.call(this, "jer") //利用call、apply将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行)
this.age = 12;
}
let con1 = new Con();
console.log(con1.name);
console.log(con1.age);
console.log(con1 instanceof Person);
2
3
4
5
6
7
8
特点:只继承了父类构造函数的属性,原型没有继承。可以继承多个构造函数属性(多个call)。 在子类中可向父类传参。
缺点:
- 只能继承父类构造函数的属性
- 无法实现构造函数复用(每次都要重新调用)
- 每个新实例都会有父类构造函数的副本,冗余。
组合继承(常用)(原型+构造)
function SubType(name) {
Person.call(this, name);
}
SubType.prototype = new Person();
let sub = new SubType("gar");
console.log(sub.name);
console.log(sub.age);
2
3
4
5
6
7
重点:结合了传参和复用
特点:1. 可以继承父类原型上的属性,可传参,可复用。 2. 每个新实例引入的构造函数属性私有。
缺点:调用了两次父类构造函数(耗内存),子类构造函数会代替原型上的父类构造函数。
寄生组合继承
// 寄生,content就是F实例的另一种表示法
function content() {
function F() {};
F.prototype = obj;
return new F();
}
let con = content(Person.prototype); // con实例(F实例)的原型继承了父类函数的原型,上述更像是原型链继承,只不过是继承了原型属性。
// 组合
function Sub() {
Person.call(this); // 这个继承了父类构造函数的属性,解决了组合式两次条用构造函数属性的缺点
}
// 重点
Sub.prototype = con; //继承con实例
con.constructor = Sub; // 修复实例
var sub1 = new Sub();
console.log(sub1.age) //10
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 重点:解决了组合继承的问题。
# Proxy(代理,守卫,拦截)
- 作用:让函数和其变量私有化
- 语法
let p = new Proxy(target, handler);
- target:需要用Proxy包装的目标对象
- handler:一个对象,其属性是当执行一个操作时代理的行为函数。
- 用法
- 比如这里有一个例子:
const obj = {
val: 10;
}
obj.val2 = 20;
// 这里用proxy 对obj对象设置get方法和set方法的拦截
const handler = {
get: function (obj, prop) {
if(prop == "id"){
return 6;
}
console.log("get");
return obj[prop];
},
set: function(obj, prop, value) {
if(typeof value !== "string") {
throw new Error("Only String values can be stored in this object!");
} else {
obj[prop] = value;
}
}
}
const initialobj = {
id: 1,
name: "Foo bar"
}
const proxiedObject = new Proxy(initialobj, handler);
console.log(proxiedObject.id);
proxiedObject.name = 8; // 这里会报错,触发拦截
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
# Promise async await
# Promise
解释:是异步编程的一种解决方案,比传统的异步解决方案更合理和强大。
作用:解决回调地狱问题
状态与关系:
- Promise中约定,当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中⼀ 种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏
创建方式
- new Promise(fn);
- Promise.resolve(fn);
promise与事件循环
- promise在初始化时,传入的函数是同步执行的,然后注册then回调。注册完之后,继续往下执行同步代码。在这之前,then中回调不会执行。 同步代码块执行完毕后,才会在时间循环中检测是否有可用的promise回调,如果有,执行,没有,下一个事件循环。
promise与es6
- es6中提供了一种generator和promise的语法糖,就是async和await。
(async () => {
let 蔬菜 = await 买菜();
let 饭菜 = await 做饭(蔬菜);
let 送饭结果 = await 送饭(饭菜);
let 通知结果 = await 通知我(送饭结果);
})
2
3
4
5
6
- 手写promise
# 宏任务 微任务 EventLoop
- 任务队列分两种类型,宏任务先执行,微任务后执行。宏任务会阻塞浏览器的渲染进程,微任务会在通任务结束后立即执行,在渲染之前。
# 宏任务(macro Task)
- IO,setTimeout; setInterval; setImmediate; requestAnimationFrame
# 微任务(micro Task)
- Promise,prototype,then catch finally; (node环境)process.nextTick; MutationObserver,
# EventLoop
为了解决JS异步编程的一种方案。
进程与线程
- 进程是CPU资源分配的最小单位。
- 线程是CPU调度的最小单位。
- 它俩的区别就是进程携带资源,线程不带。
- 它俩的关系,一个进程可以有多个线程。
浏览器中的EventLoop执行顺序
- eventLoop会不断循环的去获取任务队列中最老的一个任务(宏任务)推入栈执行,并在当次 循环里依次执行并清空微任务队列里的任务。
- 执行完微任务队列里的任务,有可能会渲染更新。(浏览器积攒变动以最高60HZ的频率更新视图)
- 图示
练习的例子:点这里 (opens new window)
# 原型与原型链
- 原型。所有引用类都有一个__proto__隐式原型对象,属性值是一个普通的对象
- 所有函数都有一个prototype原型属性,值是一个普通对象
- 所有引用类型的__proto__属性指向它构造函数的prototype
- 蓝色的线就是原型链。当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有 找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到 就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就 会形成一个链式 结构,我们称为原型链。
# 函数柯里化(Currying)
# 概念
- 柯里化是指把有多个参数的函数转成只有一个参数的函数,并返回接受余下参数且返回结果的新函数的技术。
- 直接看个例子吧:
function sum(x, y) { return x+y } // 柯里化 function curryingSum(x) { return function (y) { return x+y } } sum(1, 2) curryingSum(1)(2)
1
2
3
4
5
6
7
8
9
10
11
# 好处
- 说白了就是在函数里面再封装一层,但是这么做的意义是什么呢?(再提一嘴,bind实现的机制就是currying)
- 参数复用
- 提前确认
- 延迟执行
# currying封装
- 对于每次使用currying都要对底层函数进行修改,所以做了以下封装:
- 大概思想就是,通过闭包把初步参数保存下来,然后通过获取剩下的arguments对象进行拼接,最后
执行需要currying的函数。
function progressCurrying(fn, args) { var _this = this var len = fn.length var args = args || [] return function () { var _args = Array.prototype.slice.call(arguments) Array.prototype.push.apply(args, _args) // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数 if(_args.length < len) { return progressCurrying.call(_this, fn, _args) } // 参数收集完毕,执行fn return fn.apply(this, _args) } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 经典面试题
// 实现一个add方法,使计算结果能够满足如下预期:
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...arguments);
return _adder;
};
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
add(1)(2)(3) // 6
add(1, 2, 3)(4) // 10
add(1)(2)(3)(4)(5) // 15
add(2, 6)(1) // 9
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
# 防抖和节流
# 防抖debounce
作用:防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着n秒内函数只会被执行一次,如果n秒内 再次被处罚,则重新计算延迟时间。
应用场景:
- 搜索框输入查询
- 表单验证
- 按钮提交事件
- 浏览器窗口缩放
防抖函数的实现
- 思路:时间第一次触发,timer是null,调用later(),若immediate是t,那么立即调用 func.apply(this, params); 若immediate是f,那么过wait之后,调用func.apply(this, params)
- 事件第二次触发,如果timer重置为null,那么流程和第一次触发一样,如果timer不为null,则清空定时器, 重新计时。
- immediate为true时,表示函数在每个等待时延的开始被调用;为false,表示在时延结束被调用
// 首次运行时把定时器赋值给一个变量,第二次执行时,如果间隔没超过定时器设定的时间则会清除掉定时器,重新设定定时器,依次反复,当我们停止下 // 来时,没有执行清除定时器,超过一定时间后触发回调函数。 function debounce(func, wait, immediate = true) { let timer // 延迟执行函数 const later = (context, args) => setTimeout(() => { timer = null // 倒计时结束 if(!immediate) { func.apply(context, args) // 回调 context = args = null } }, wait); let debounced = function (...params) { let context = this let args = params if(!time) { timer = later(context, args) if(immediate) { // 立即执行 func.apply(context, args) } } else { clearTimeout(timer) // 函数在每个等待时延的结束被调用 timer = later(context, args) } } debounced.cancel = function () { clearTimeout(timer) timer = null } return debounced } // 还有一个版本 function debounce(fn, wait) { let timeout = null return function () { let context = this let args = arguments if(timeout) clearTimeout(timeout) timeout = setTimeout(() => { fn.apply(context, args) }, wait) } } // test const task = () => {console.log('run task')} const debounceTask = debounce(task, 1000) window.addEventListener('scroll', debounceTask)
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
# 函数节流(throttle)
- 作用:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
- 应用场景:
- scroll
- touchmove
- 节流实现
function throttle(fn, ms=1000) {
let canRun = true
return function (...args) {
canRun = false
setTimeout(() => {
fn.apply(this, args)
canRun = true
}, ms)
}
}
// test
const task = () => {console.log('run task')}
const throttleTask = throttle(task, 1000)
window.addEventListener('scroll', throttleTask)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 防抖与节流的区别
- 节流是不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而 防抖是只在最后一次事件之后才触发一次函数。比如在页面的无限加载场景下,我们需要用户在滚动页面时 每隔一段时间发一次ajax请求,而不是在用户停止滚动时才请求数据,那么就是节流。
# 重绘与回流/重排
# html加载
- 页面加载时,浏览器把获取到的HTML代码解析成一个DOM树,DOM树里包含了所有HTML标签,包括display:none 隐藏,还有js动态添加的元素等。
- 浏览器把所有的样式解析成样式结构体。DOMTree和样式结构体组合后构件Render Tree。
# 重绘(repaint)
- 重绘就是Render Tree中一些元素需要更新(只影响元素的外观,风格,不影响布局),就称为重绘。
- 重绘:不涉及任何DOM元素的排版问题的变动为重绘。
- 触发重绘:color修改,text-align修改,a:hover修改,:hover引起的颜色等。
# 回流(重排)(reflow)
Render Tree中的一部分因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,就叫回流。
每个页面至少需要一次回流,就是页面第一次加载的时候。
完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。
触发重排:
- width/height/margin/border/padding修改
- 动画,:hover等伪类引起的元素表现变动,display=none等造成页面回流。
- appendChild等DOM元素操作
- font类style修改。
- background修改
- scroll页面,不可避免
- resize页面(桌面版本的进行浏览器大小缩放
- 读取元素属性:offsetLeft\top\height\width, getComputedStyle()等
# 区别与优化
区别
- 回流必然引起重绘,但重绘不一定会引起回流。
- 当页面布局和几何属性改变的时候就需要回流。(比如浏览器大小缩放)
- 回流的代价比重绘大。
优化
- DOM的增删行为。如果要多加个子元素,最好是用documentfragment
- 几何属性变化(比如border,front-size)。如果要改变多个几何属性,最好将这些属性定义在一个class中,直接修改 class名,这样只引起一次回流。
- 元素位置(margin,padding)的变化。做元素位移的动画,不要更改margin之类的属性,使用定位脱离文档流后位置会更好,
- 获取元素的偏移量属性。比如scrollTop,offsetTop之类,浏览器为了保证值的正确也会回流得到最新 的值。如果要多次操作,最好取完做个缓存。
- 浏览器窗口尺寸改变。 resize也会引起回流。