JavaScript

3/6/2022

# JavaScript的编译,上下文context

# 过程

  • 预编译期
    1. 浏览器的js引擎解析js代码
    2. 建立arguments对象(隐藏对象,不可见),函数,参数,变量
    3. 建立作用域链
    4. 确定this指向
  • 执行期
    • 按从上到下的顺序执行代码
  • 函数体的预解析发生在函数被调用之时,被调用时先进行函数体的预编译,然后按顺序进行执行

# 上下文context

  • 指的是一种运行环境
  • context指的是,函数被调用时,看this指向哪个object,这个object就是当前的上下文。

# 执行上下文

  • 概念

    • 用来区分不同的运行环境,需要引出一个概念:执行上下文(Execution Context)
    • 他是一个对象,由js的执行引擎创建
    • 有三个属性:变量对象,作用域链,this指针
  • 上下文栈

    • js执行过程中有一个上下文栈,存放的是不同的上下文对象(不同的js运行环境)。
    • 当前执行代码的context对象总是在栈顶。
  • 变量对象

    • 变量对象的创建过程如下:(变量提升被提出的原因)
      1. 创建arguments对象,其中保存多个属性,属性的key是0,1,2... value即传入参数的实际值。
      2. 找到这个作用域内的所有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指向的问题

  • 要记住几句话:
    1. this 永远指向最后调用它的那个对象
    2. 匿名函数的 this 永远指向 window
    3. 没有挂载在任何对象上的函数,非严格模式下 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;
}
1
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)
1
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)
1
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()
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

# 综合用例

  • 第一道

    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
1
2
3
4
5
6
7
8
9

# 闭包(概念 用途 手写)

# 概念

  • 闭包是包含了那个局部变量的容器,它被内部函数对象引用。
  • 特点是2个函数相互嵌套,内部函数引用了外部函数的局部变量,执行外部函数。
  • 闭包的本质还是函数

# 作用

  • 延长局部变量的生命周期
  • 使函数外部可以多次间接操作到函数内部的数据。

# 应用

  • 循环遍历监听
  • IIFE定义模块
  • jQuery内部

# 手写

# 同步与异步

# 同步

  • 同步:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。如果在函数A返回 的时候,调用者就能够得到预期的结果(即拿到了预期的返回值或者看到了预期的效果),那么这个函数 就是同步的。

# 异步

  • 不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步 任务可以执行了,该任务才会进入主线程执行。如果在函数A返回的时候,调用者还不能马上得到预期 的结果,而是需要在将来通过一定的手段得到,那么这个函数就是异步的 。
  • 发起函数和回调函数
  • 异步的执行机制:
    1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
    2. 主线程之外,还存在一个"任务队列"(task queue),只要异步任务有了运行结果,就在"任务队列"之中放置一个事件
    3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件,那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
    4. 主线程不断重复上面的第三步

# 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
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
  • 函数提升。在js里函数的优先级是最高的,如果有同名的函数和变量,函数将优先被提升至作用域顶端。
    var foo = 3
    function test(){
        console.log(foo);   //function foo(){}
        foo=5;
        console.log(foo);   // 5
        function foo(){}
    }
    test();
    console.log(foo)    // 3
1
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;
1
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
1
2
3
4
5
6
7
8
  • 让新实例的原型等于父类的实例。

  • 缺点:

    1. 新实例无法向父类构造函数传参
    2. 继承单一
    3. 所有新实例共享父类实例属性。(原型上的属性是共享的,一个属性被修改,另一个也会被修改)
  • 构造函数继承

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);
1
2
3
4
5
6
7
8
  • 特点:只继承了父类构造函数的属性,原型没有继承。可以继承多个构造函数属性(多个call)。 在子类中可向父类传参。

  • 缺点:

    1. 只能继承父类构造函数的属性
    2. 无法实现构造函数复用(每次都要重新调用)
    3. 每个新实例都会有父类构造函数的副本,冗余。
  • 组合继承(常用)(原型+构造)

function SubType(name) {
    Person.call(this, name);
}
SubType.prototype = new Person();
let sub = new SubType("gar");
console.log(sub.name);
console.log(sub.age);
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 重点:解决了组合继承的问题。

# Proxy(代理,守卫,拦截)

  • 作用:让函数和其变量私有化
  • 语法
let p = new Proxy(target, handler);
1
  1. target:需要用Proxy包装的目标对象
  2. 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;     // 这里会报错,触发拦截
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

# Promise async await

# Promise

  • 解释:是异步编程的一种解决方案,比传统的异步解决方案更合理和强大。

  • 作用:解决回调地狱问题

  • 状态与关系:

    • Promise中约定,当对象创建之后同⼀个Promise对象只能从pending状态变更为fulfilled或rejected中的其中⼀ 种,并且状态⼀旦变更就不会再改变,此时Promise对象的流程执⾏完成并且finally函数执⾏
  • 创建方式

    1. new Promise(fn);
    2. Promise.resolve(fn);
  • promise与事件循环

    • promise在初始化时,传入的函数是同步执行的,然后注册then回调。注册完之后,继续往下执行同步代码。在这之前,then中回调不会执行。 同步代码块执行完毕后,才会在时间循环中检测是否有可用的promise回调,如果有,执行,没有,下一个事件循环。
  • promise与es6

    • es6中提供了一种generator和promise的语法糖,就是async和await。
(async () => {
    let 蔬菜 = await 买菜();
    let 饭菜 = await 做饭(蔬菜);
    let 送饭结果 = await 送饭(饭菜);
    let 通知结果 = await 通知我(送饭结果);
})
1
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)
    1. 参数复用
    2. 提前确认
    3. 延迟执行

# 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
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

# 防抖和节流

# 防抖debounce

  • 作用:防抖函数的作用就是控制函数在一定时间内的执行次数。防抖意味着n秒内函数只会被执行一次,如果n秒内 再次被处罚,则重新计算延迟时间。

  • 应用场景:

    1. 搜索框输入查询
    2. 表单验证
    3. 按钮提交事件
    4. 浏览器窗口缩放
  • 防抖函数的实现

    • 思路:时间第一次触发,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)

  • 作用:当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
  • 应用场景:
    1. scroll
    2. 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)
1
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中的一部分因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,就叫回流。

  • 每个页面至少需要一次回流,就是页面第一次加载的时候。

  • 完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。

  • 触发重排:

    1. width/height/margin/border/padding修改
    2. 动画,:hover等伪类引起的元素表现变动,display=none等造成页面回流。
    3. appendChild等DOM元素操作
    4. font类style修改。
    5. background修改
    6. scroll页面,不可避免
    7. resize页面(桌面版本的进行浏览器大小缩放
    8. 读取元素属性:offsetLeft\top\height\width, getComputedStyle()等

# 区别与优化

  • 区别

    • 回流必然引起重绘,但重绘不一定会引起回流。
    • 当页面布局和几何属性改变的时候就需要回流。(比如浏览器大小缩放)
    • 回流的代价比重绘大。
  • 优化

    1. DOM的增删行为。如果要多加个子元素,最好是用documentfragment
    2. 几何属性变化(比如border,front-size)。如果要改变多个几何属性,最好将这些属性定义在一个class中,直接修改 class名,这样只引起一次回流。
    3. 元素位置(margin,padding)的变化。做元素位移的动画,不要更改margin之类的属性,使用定位脱离文档流后位置会更好,
    4. 获取元素的偏移量属性。比如scrollTop,offsetTop之类,浏览器为了保证值的正确也会回流得到最新 的值。如果要多次操作,最好取完做个缓存。
    5. 浏览器窗口尺寸改变。 resize也会引起回流。
Last Updated: 10/17/2021, 8:21:50 PM