js牛客刷题记录1

Cookie,sessionStroage,localstorage之间的区别

Cookie前后端都可以写入,sessionStorage以及localStorage都是由前端写入

cookie的生命周期由写入时就设置好的,localstorage是写入时就一直存在,除非手动清除,sessionStroage是由页面关闭时自动删除

cookie存储空间大小约4k,sessionStroage以及localStroage空间比较大,大约5MB。

前端给后端发送请求时自动携带cookie,session以及local都不携带

cookie一般存储登录验证信息,localstroage常用于存储不宜变动的数据,减轻服务器压力

sessionStroage可以用来检测用户是否刷新进入页面,如音乐恢复进度条功能

JS基础数据类型

number

包括整数和浮点数

JS进行浮点元素运算可能得到一个不精确的值0.1+0.2=0.300000004,不能进行精确度要求比较高的运算;

NAN表示not A Number 检查时返回number

数值number类型,用来表示任何类型的数字:整数或者浮点数都可以;

实际上,JS中的数值,是一个64位的浮点数,这与Java中的double类型的浮点数是一致的;

但是它有表示的范围,在范围内,JS可以准确表示,超出范围,JS不会报错,但是数值不准:

  • 整数范围:±253-1,闭区间,即±9007,1992,5474,0992,那么超出16位的整数,必然表示不准;
  • 浮点数范围:±1.7976,9313,4862,3157308,JS有内置常量用于表示这个数值,即Number.MAX_VALUE;
  • 能表示的最小的小数:5-324,JS有内置常量用于表示这个数值,即Number.MIN_VALUE;
  • 位运算范围:JS只支持32位整数,即[-231, 231-1];

String类型

String类型(字符串)是由 Unicode 字符、数字、标点符号等组成的序列,在 JavaScript 代码中用于表示 JavaScript 文本的数据类型,字符串型数据通常由单引号或双引号包裹,由双引号定界的字符串中可以再包含有单引号,单引号定界的字符串中也可以再包含有双引号。

  • 在js中字符串需要使用引号引起来,单引号和双引号不能混用;
  • 引号不能嵌套,可以使用进行转义。

Boolean

与数字类型的值不同,Boolean类型(布尔型)变量的值只有固定的两种表示方式,一种是 true,另一种是 false,前者表示真,后者表示假,如果用数字表示,那么,true 可以使用 1 来表示,false 可以使用 0 来表示,布尔型变量的值来源于逻辑性运算符,常用于控制结构流程。

  • 布尔值只有两个true/false,主要做逻辑判断;
  • 使用typeof检查一个布尔值时返回boolean。

Null类型

在 JavaScript 代码中,Null类型(空值型)是一个比较特殊的类型,它只有一个值,就是 null,当引用一个未定义的对象时,则将返回一个这个 null 值, 从严格意义上来说,null 值本质上是一个对象类型,是一个空指针的对象类型。

  • Null(空值)类型的值只有一个,null,用来表示一个为空的对象;
  • 使用typeof检查时值时返回object,因为null表示空对象NaN、infanity、undefined。

Undefined类型

与 Null 型相同,Undefined 型(未定义)也是只有一个 undefined 值,当在编写 JavaScript 代码时,如果定义了一个变量,但没有给它赋值,那么,这个变量将返回 undefined 值,这也是变量默认的值,与 Null 型不同之处在于,Null 型是一个空值,而 Undefined 型表示无值。

  • js中输出未被赋值的被定义变量结果是Undefined;Undefined类型的值只有一个。
  • 使用typeof检查时值时返回undifine

BitInt

BigInt 类型是 JavaScript 中的一个基础的数值类型,可以表示任意精度的整数。使用 BigInt,您可以安全地存储和操作大整数,甚至可以超过数字类型的安全整数限制。

BigInt 是通过在整数末尾附加字母 n 或调用构造函数来创建的。

通过使用常量 Number.MAX_SAFE_INTEGER,您可以获得可以用数字递增的最安全的值。通过引入 BigInt,您可以操作超过 Number.MAX_SAFE_INTEGER 的数字。

String

JavaScript 的字符串类型用于表示文本数据。它是一组 16 位的无符号整数值的“元素”。在字符串中的每个元素占据了字符串的位置。第一个元素的索引为 0,下一个是索引 1,依此类推。字符串的长度是它的元素的数量。

不同于一些编程语言(例如,C 语言),JavaScript 的字符串是不可更改的。这意味着字符串一旦被创建,就不能被修改。

但是,可以基于对原始字符串的操作来创建新的字符串。例如:

  • 获取一个字符串的子串可通过选择个别字母或者使用 String.substr()
  • 两个字符串的连接使用连接运算符(+)或者 [String.concat()

符号类型

符号(Symbols)类型是唯一且不可修改的原始值,并且可以用来作为对象的key,在某些语言中也有与之相似的类型

js作用域

{}和函数外的区域为全局作用域

全局作用域中的声明的变量是全局变量,在页面的任意的部分都可以访问。
全局作用域中无法访问函数作用域的变量
全局作用域在页面打开时创建,在页面关闭时销毁。
全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,由浏览器创建,可以直接使用,全局变量是window对象的属性,函数是window对象的方法。

什么叫闭包

闭包(closure)是js语言的特色

闭包的作用:通过一系列的方法

js的变量作用域有俩种,全局变量和局部变量

var n = 999;
function f1() {
    alert(n);
}
f1(); // 999

另一方面,在函数外部无法读取函数内的局部变量。

function f1() {
    var n = 999;
}

alert(n); // error

这里有一个地方需要注意, 函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!

function f1() {
    n = 999;
}
f1();
alert(n); // 999

如果一个函数访问了次函数的父级以及父级以上的作用域变量,那么这个函数就是一个闭包

 var a = 1;
// 匿名的立即执行函数,因访问了全局变量a,所以也是一个闭包
(function test (){
    alert(a);
})()

本质上,JS中的每个函数都是一个闭包,因为每个函数都可以访问全局变量。

js的Promise

概念

Promise 对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。简而言之,就是处理异步请求的。

之所以叫promise,就是承诺做这件事情,如果成功则怎么处理失败怎么处理

// 语法
new Promise(
    // 下面定义的函数是 executor 
    function(resolve, reject) {
        ...
    }
)

executor

executor是一个带有resolve和reject俩个参数的函数

executor函数在Promise构造函数执行时立即执行,被传递resolve和reject函数(executor函数在promise构造函数返回新对象前被调用)

executor内部通常汇之星一些异步操作,一旦完成,可以调用resolve函数来promise状态改成fulfilled(即完成),或在发生错误时将它的状态改为rejected

如果在 executor 函数中抛出一个错误,那么该 Promise 状态为 rejected 。executor 函数的返回值被忽略

executor 中,reslove 或 reject 只能执行其中一个函数

  • pending: 初始状态,不是成功或失败状态
  • fulfilled: 意味着操作成功完成
  • rejected: 意味着操作失败

setInterval

# 间隔多少毫秒就执行函数一次,循环执行
    # function 延时到的时候执行的函数
    # delay 延时,缺省0,立即执行
setInterval(function[,delay]);
setInterval(() => {
    console.log('I am working!')
}, 1000)

可以理解为是一个定时循环的任务

setTimeout

# 等待多少毫秒就执行函数一次,结束
    # function 延时到的时候执行的函数
    # delay 延时,缺省0,立即执行
setTimeout(function[,delay]);
setTimeout(() => {
    console.log('I am working!')
}, 2000)

示例

new Promise(function (resolve, reject) {
    var a = 0;
    var b = 1;
    if (b == 0) reject("Divide zero");
    else resolve(a / b);
}).then(function (value) {
    console.log("a / b = " + value);
}).catch(function (err) {
    console.log(err);
}).finally(function () {
    console.log("End");
});

俩个方法

  • Promise.resolve(value) 返回 状态为 fulfilled 的 Promise 对象
  • Promise.reject(reason) 返回 状态为 rejected 的 Promise 对象

promise是异步微任务,解决了异步多层嵌套回调的问题,让代码可读性更高更容易维护。

promise是Es6提供的一个构造函数,可以使用promise构造函数new一个实例,promise构造函数接受一个函数作为参数,这个函数有俩个参数,分别是resolve和reject,resolve将promise的状态由等待变为成功,将异步操作的参数传递过去,reject则将状态由等待转变为失败,在异步操作失败时调用

function runAsync() {
    return new Promise (function(resolve, reject){
        setTimeout(function(){
            console.log('Do Sth...');
            // resolve('OK...')
            reject('NOT OK...');
        }, 3000);
    });
}
runAsync().then(res=>{
    console.log(res)
}).catch(res=>{
    console.log(res)
})

BCF是什么

B代表box,是CSS中的概念:一个页面是由很多box组成的,元素的类型和display属性,决定了这个box的类型

CF的全程是Formatting Context

是W3C规范中的一个概念,它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及其他元素的关系和相互作用。最常见的formatting context有Block fomating Context(BFC)和Inline formatin context(简称IFC)

区域:

根元素

float属性不为none

position为absolute或fixed  

display为inline-block, table-cell, table-caption, flex, inline-flex

overflow不为visible

布局:

计算BFC的高度时,浮动元素也参与计算

每个元素的margin box的左边, 与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。

内部的Box会在垂直方向,一个接一个地放置

Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠

BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。

BFC的区域不会与float box重叠

样式优先级的规则是什么

第一优先级:无条件优先的属性只需要在属性后面使用!important。它会覆盖页面内任何位置定义的元素样式。ie6不支持该属性。

第二优先级:在html中给元素标签加style,即内联样式。该方法会造成css难以管理,所以不推荐使用。

第三优先级:由一个或多个id选择器来定义。例如,#id{margin:0;}会覆盖.classname{margin:3pxl}

第四优先级:由一个或多个类选择器,属性选择器,伪类选择器定义如.classname{margin:3px}会覆盖div{margin:6px;}

第五优先级:由一个或多个类型选择器定义,div{marigin:6px;}覆盖*{margin:10px;}

第六优先级:通配选择器,如*{marigin:6px;}

举例

内联样式:写在标签属性style的样式,如 <p style="color=red">
ID选择器,如#id{…}
类选择器,如 .class{…}
属性选择器,如 input[type="email"]{…}
伪类选择器,如a:hover{…}
伪元素选择器,如 p::before{…}
标签选择器,如 input{…}
通配选择器,如 *{…}

async和await的用法

async表示函数里有异步操作

await表示紧跟在后面的表达式需要等待结果

async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行时,一旦遇到await方法就会先返回,等到出发的异步操作完成,再接着执行函数体后面的语句

特点

1)方便级联调用:即调用依次发生的场景;

2)同步代码编写方式: Promise使用then函数进行链式调用,是一种从左向右的横向写法;async/await从上到下,顺序执行,就像写同步代码一样,更符合代码编写习惯;

3)多个参数传递: Promise的then函数只能传递一个参数,虽然可以通过包装成对象来传递多个参数,但是会导致传递冗余信息,频繁的解析又重新组合参数,比较麻烦;async/await没有这个限制,可以当做普通的局部变量来处理,用let或者const定义的块级变量想怎么用就怎么用,想定义几个就定义几个,完全没有限制,也没有冗余工作;

4)同步代码和异步代码可以一起编写: 使用Promise的时候最好将同步代码和异步代码放在不同的then节点中,这样结构更加清晰;async/await整个书写习惯都是同步的,不需要纠结同步和异步的区别,当然,异步过程需要包装成一个Promise对象放在await关键字后面;

5)基于协程: Promise是根据函数式编程的范式,对异步过程进行了一层封装,async/await基于协程的机制,是真正的“保存上下文,控制权切换……控制权恢复,取回上下文”这种机制,是对异步过程更精确的一种描述;

6)async/await是对Promise的优化: async/await是基于Promise的,是进一步的一种优化,不过在写代码时,Promise本身的API出现得很少,很接近同步代码的写法;

async关键字

表明程序里面有可能有异步的过程:表明程序里面有可能有异步的过程,里面可以有await关键字;当然全部是同步代码也没关系,但这样async关键字就多余了

非阻塞: async函数里面如果有异步过程会等待,但是async函数本身会马上返回,不会阻塞当前线程,可以简单认为,async函数工作在主线程,同步执行,不会阻塞界面渲染,async函数内部由await关键字修饰的异步过程,工作在相应的协程上,会阻塞等待异步任务的完成再返回;

async函数返回类型为Promise对象: 这是和普通函数本质上不同的地方,也是使用时重点注意的地方;
(1)return newPromise();这个符合async函数本意;
(2)return data;这个是同步函数的写法,这里是要特别注意的,这个时候,其实就相当于Promise.resolve(data);还是一个Promise对象,但是在调用async函数的地方通过简单的=是拿不到这个data的,因为返回值是一个Promise对象,所以需要用.then(data => { })函数才可以拿到这个data;
(3)如果没有返回值,相当于返回了Promise.resolve(undefined);

无等待 联想到Promise的特点,在没有await的情况下执行async函数,它会立即执行,返回一个Promise对象,并且绝对不会阻塞后面的语句,这和普通返回Promise对象的函数并无二致;

await关键字

await只能在async函数内部使用:不能放在普通函数里,否则会报错

await关键字后面跟Promise对象:在Pending状态时,相应的协程会交出控制权,进入等待状态,这是协程的本质;

await 仅在异步函数和模块的顶级主体中有效

await是async wait的意思: wait的是resolve(data)的消息,并把数据data返回,比如下面代码中,当Promise对象由Pending变为Resolved的时候,变量a就等于data,然后再顺序执行下面的语句console.log(a),这真的是等待,真的是顺序执行,表现和同步代码几乎一模一样;

const a = await new Promise((resolve, reject) => {
    // async process ...
    return resolve(data);
});
console.log(a);

await后面也可以跟同步代码: 不过系统会自动将其转化成一个Promsie对象,比如:

const a = await 'hello world'
 
// 相当于
const a = await Promise.resolve('hello world');
 
// 跟同步代码是一样的,还不如省事点,直接去掉await关键字
const a = 'hello world';

await对于失败消息的处理: await只关心异步过程成功的消息resolve(data),拿到相应的数据data,至于失败消息reject(error),不关心不处理;对于错误的处理有以下几种方法供选择:
(1)让await后面的Promise对象自己catch;
(2)也可以让外面的async函数返回的Promise对象统一catch;
(3)像同步代码一样,放在一个try...catch结构中;


async componentDidMount() { // 这是React Native的回调函数,加个async关键字,没有任何影响,但是可以用await关键字
    // 将异步和同步的代码放在一个try..catch中,异常都能抓到
    try {
        let array = null;
        let data = await asyncFunction();  // 这里用await关键字,就能拿到结果值;否则,没有await的话,只能拿到Promise对象
        if (array.length > 0) {  // 这里会抛出异常,下面的catch也能抓到
            array.push(data);
        }
    } catch (error) {
        alert(JSON.stringify(error))
    }
}

await对于结果的处理: await是个运算符,用于组成表达式,await表达式的运算结果取决于它等的东西,如果它等到的不是一个Promise对象,那么await表达式的运算结果就是它等到的东西;如果它等到的是一个Promise对象,await就忙起来了,它会阻塞其后面的代码,等着Promise对象resolve,然后得到resolve的值,作为await表达式的运算结果;虽然是阻塞,但async函数调用并不会造成阻塞,它内部所有的阻塞都被封装在一个Promise对象中异步执行,这也正是await必须用在async函数中的原因;

Flex布局

简介

传统布局:兼容性好,布局繁琐,局限性

flex布局:操作方便,布局简单,移动端广泛,IE11以上才能支持

flex是flexble box的缩写,意为弹性布局,用来为盒模型提供最大的灵活性,任何一个容器都可以指定为flex布局

当我们为父盒子设置为flex布局后,子元素的float,clear和vertical-align(这个是设置元素的垂直排列的.用来定义行内元素的基线相对于该元素所在行的基线的垂直对齐)属性将失效

flex也叫弹性布局,伸缩布局,弹性盒布局

概念

采用flex布局的元素,称为flex容器,它的所有子元素自动成为容器成员,称为flex项目(flex item)

就是通过给父盒子添加flex属性,来控制子盒子的位置和排列方式

父项常见属性

flex-direction

设置主轴的方向,在flex布局中,是分为主轴和侧轴俩个方向的,也叫行和列,x轴和y轴

默认主轴方向就是x轴,水平向右

默认侧轴方向就是y轴方向,水平向下

主轴和侧轴方向是会变化的,就看flex-direction设置谁为主轴,剩下的就是侧轴。而我们的子元素是跟着主轴来排列的

  • row:默认值,从左到右
  • row-reverse:从右到左
  • column:从上到下
  • column-reverse:从下到上

justify-content

设置主轴上子元素的排列方式

使用这个属性之前一定要确定好主轴是哪个

  • flex-start:默认值,从头部开始,如果主轴是x轴,则从右到左
  • flex-end:从尾部开始排列
  • center:在主轴居中对齐
  • space-around:平分剩余空间
  • space-between:先俩边贴边,再平分剩余空间

flex-wrap

设置子元素换行

默认情况下,项目都排在一条线上。flex-wrap属性定义,flex布局中默认是不换行的,如果装不下会缩小子元素宽度

  • nowrap:默认值,不换行
  • wrap:换行

align-items

align-items是设置侧轴上子元素排列方式,在子项为单项时使用

  • flex-start默认值,从上到下
  • flex-end:从下到上
  • center:垂直居中
  • stretch:拉伸

align-content

设置子项侧轴上的排列方式,并且只能用于子项出现换行的情况,在单行下是没有效果的

  • flex-start:默认值在侧轴的头部开始排列
  • flex-end:在侧轴的尾部开始排列
  • center:在侧轴中间显示
  • space-around:在周抽平分剩余空间
  • space-between:子项在侧轴先分布在俩头,再平分剩余空间
  • stretch:设置子项元素高度平分父元素高度

flex-flow

是flex-direction和flex-warp属性的复核属性

第一个参数flex-direction,第二个参数是flex-warp

flex-flow:column warp;

子项常见属性

flex属性

flex:1

flex属性定义子项目分配的剩余空间,用flex来表示占多少份数

align-self

控制子项在侧轴上的排列方式

align-self属性允许有单个项目有与其他项目不一样的对其方式,可覆盖align-items属性

默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch

order

数值越小,排列越靠前,默认为0

注意和z-index不一样

js的变量提升

变量提升

console.log(a);
var a=3;

按理说,按照js单线程的逻辑,这里输出的应该是报错,即
a is not defined
因为我们想的是在没有定义a之前打印输出了a变量,理应报这个我们还未定义a的错误,可事实真的如此吗,我们来看实际输出的结果:

结果竟然输出的是undefined,也就是js认为现在a已经定义过了,但是没赋值。
这是怎么回事?我们明明把定义和赋值写在了第二行。
这,就是本文介绍的,提升。

简单来说,就是js代码执行前引擎会进行预编译,预编译期间会将函数变量声明提升至变量的最前端,然后再去执行剩下的代码

函数提升

function chifan(){
  console.log('我要吃米饭')
}
chifan()
function chifan(){
  console.log('我要吃面')
}
chifan()

可以看到,定义了chifan函数,分别输出两个字符串,按照我们对js单线程的逻辑理解,执行的代码结果应该是:
‘我要吃米饭’
‘我要吃面’
可实际上呢:

这就是js所谓的函数提升,我们使用的是函数声明的方式,所以js会有类似变量提升的函数提升,与变量提升不同,函数提升不仅仅提升函数声明,而是提升函数整体

var game=function (){
  console.log('玩英雄联盟')
}
game()
var game=function (){
  console.log('玩CSGO')
}
game()

这里并没有用函数声明的方式,而用的是函数表达式的方式,所以并不存在函数整体提升,仅仅也只是声明提升,具体执行过程如下

补充

let,cost不会存在变量提升的问题,函数声明提示,都是在js编一阶段提升的,当前变量函数提升至当前作用域的最顶端,并没有开始执行代码,所以都会被赋初值undefined。

js的指向,箭头函数和普通函数

js中=>是箭头函数,是ES6标准中的新函数。箭头函数表达式的语法比函数表达式更加简介,并且没有自己的this

比如

    x => x * x

相当于

function (x) {
    return x * x;
}

箭头函数可以有一个“简写体”或常见的“块体”。

在一个简写体中,只需要一个表达式,并附加一个隐式的返回值。在块体中,必须使用明确的return语句。

var func = x => x * x;                  
// 简写函数 省略return(简写体)

var func = (x, y) => { return x + y; }; 
//常规编写 明确的返回值(块体)

递归

var fact = (x) => ( x==0 ?  1 : x*fact(x-1) );
fact(5);       // 120

JS的重绘和重排(回流)

重排一定会导致重绘,重绘不一定导致重排,如DOM变化不影响几何属性,元素布局没有改变,则只发生一次重绘(不需要重排)

如何减少重绘和重拍

尽可能在DOM树的最末端改变class

避免设置多层内联样式

动画效果应用到position属性为absolute或fixed的元素上

避免使用table布局

使用css3硬件加速,可以让transform、opacity、filters等动画效果不会引起回流重绘

js操作中避免重绘

避免使用JS一个样式修改完接着改下一个样式,最好一次性更改CSS样式,或者将样式列表定义为class的名称

避免频繁操作DOM,使用文档片段创建一个子树,然后再拷贝到文档中

先隐藏元素,进行修改后再显示该元素,因为display:none上的DOM操作不会引发回流和重绘

避免循环读取offsetLeft等属性,在循环之前把它们存起来

对于复杂动画效果,使用绝对定位让其脱离文档流,否则会引起父元素及后续元素大量的回流

JS的六种继承方式

prototype原型对象

构造

js的构造器

function person(firstname,lastname,age,eyecolor)
{
    this.firstname=firstname;
    this.lastname=lastname;
    this.age=age;
    this.eyecolor=eyecolor;
}

有了构造器就可以创建新的对象实例

var myFather=new person("John","Doe",50,"blue");
var myMother=new person("Sally","Rally",48,"green");

所有的js对象都会从一个prototype原型中集成属性和方法

我们也知道在一个存在构造器的对象中是不能添加属性操作的

Person.nationality = "English";

需要添加的时候需要在构造器函数中加

function Person(first, last, age, eyecolor) {
  this.firstName = first;
  this.lastName = last;
  this.age = age;
  this.eyeColor = eyecolor;
  this.nationality = "English";
}

prototype继承

所有js对象都是一个prototype原型对象中集成属性和方法

  • Date 对象从 Date.prototype 继承。
  • Array 对象从 Array.prototype 继承。
  • Person 对象从 Person.prototype 继承。

所有js中的对象都是位于原型链顶端的Object实例

js对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻该对象的原型,依次层层向上搜索,知道匹配到一个名字匹配的或找到一个名字匹配的属性或原型链的末尾


有的时候我们想要在所有已经存在的对象添加新的属性或方法。

另外,有时候我们想要在对象的构造函数中添加属性或方法。

使用 prototype 属性就可以给对象的构造函数添加新的属性:

function Person(first, last, age, eyecolor) {
  this.firstName = first;
  this.lastName = last;
  this.age = age;
  this.eyeColor = eyecolor;
}
 
Person.prototype.nationality = "English";

当然我们也可以使用 prototype 属性就可以给对象的构造函数添加新的方法:

function Person(first, last, age, eyecolor) {
  this.firstName = first;
  this.lastName = last;
  this.age = age;
  this.eyeColor = eyecolor;
}
 
Person.prototype.name = function() {
  return this.firstName + " " + this.lastName;
};

原型链继承

function Parent() {
   this.isShow = true
   this.info = {
       name: "mjy",
       age: 18,
   };
}
 
Parent.prototype.getInfo = function() {
   console.log(this.info);
   console.log(this.isShow);
}
 
function Child() {};
Child.prototype = new Parent();
 
let Child1 = new Child();
Child1.info.gender = "男";
Child1.getInfo(); // {name: 'mjy', age: 18, gender: '男'} ture
 
let child2 = new Child();
child2.isShow = false
console.log(child2.info.gender) // 男
child2.getInfo(); // {name: 'mjy', age: 18, gender: '男'} false

优点:写法方便简洁,容易理解

缺点:对象实例沟通下所有继承的属性和方法。不能传参数,因为这个对象是一次性创建的

借用构造函数继承

call方法

call传入的参数数量不固定,第一个参数是代表函数体内的this指向,第二个参数开始往后,每个参数传入函数

apply方法

apply方法接受俩个参数,第一个参数制定了函数体内的this对象的指向,

  • 两个方法都使用了对象本身作为第一个参数。 两者的区别在于第二个参数: apply传入的是一个参数数组,也就是将多个参数组合成为一个数组传入,而call则作为call的参数传入(从第二个参数开始)。
  • call传递的参数是序列1,2,3,4
  • apply传递的参数是集合型[1,2,3,4]

例子

  var obj={
        name:"张三",
        age:20
    };
    function method(){
        console.log(this);//window
    }
    method(); 

this指向是window,那么怎么让他指向当前对象呢

var obj={
    name:"张三",
    age:20
};
function method(a,b,c){
    console.log(this,a,b,c);//{name: "张三", age: 20} 1 2 3
}
method.call(obj,1,2,3);

apply

var obj={
    name:"张三",
    age:20
};
function method(a,b,c){
    console.log(this,a,b,c);//{name: "张三", age: 20} 1 2 3
}
method.apply(obj,[1,2,3]); 

组合继承(经典继承)

将原型链和借用构造函数组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现Udine实例属性的继承。这样通过在原型上定义实现了函数复用,又保证了每个实例都有自己的属性

function Person(gender) {
  console.log('执行次数');
  this.info = {
    name: "mjy",
    age: 19,
    gender: gender
  }
}
 
Person.prototype.getInfo = function () {   // 使用原型链继承原型上的属性和方法
  console.log(this.info.name, this.info.age)
}
 
function Child(gender) {
  Person.call(this, gender) // 使用构造函数法传递参数
}
 
Child.prototype = new Person()
 
let child1 = new Child('男');
child1.info.nickname = 'xiaoma'
child1.getInfo()
console.log(child1.info);
 
let child2 = new Child('女');
console.log(child2.info);

优点: 解决了原型链继承和借用构造函数继承造成的影响。

缺点: 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

原型式继承

借用构造函数A内部创建一个临时性的构造函数,然后将传入的对象最为这个构造函数的原型,最后返回这个临时类型的一个新实例

本质上函数A是对传入的对象进行了一次复刻

function createObject(obj) {
  function Fun() {}
  Fun.prototype = obj
  return new Fun()
}
 
let person = {
  name: 'mjy',
  age: 18,
  hoby: ['唱', '跳'],
  showName() {
    console.log('my name is:', this.name)
  }
}
 
let child1 = createObject(person)
child1.name = 'xxxy'
child1.hoby.push('rap')
let child2 = createObject(person)
 
console.log(child1)
console.log(child2)
console.log(person.hoby) // ['唱', '跳', 'rap']

Object.create()

Object.create() 是把现有对象的属性,挂到新建对象的原型上,新建对象为空对象

ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的函数A方法效果相同。

let person = {
  name: 'mjy',
  age: 19,
  hoby: ['唱', '跳'],
  showName() {
    console.log('my name is: ', this.name)
  }
}
 
let child1 = Object.create(person)
child1.name = 'xxt'
child1.hoby.push('rap')
let child2 = Object.create(person)
 
console.log(child1)
console.log(child2)
console.log(person.hoby) // ['唱', '跳', 'rap']

优点是:不需要单独创建构造函数。

缺点是:属性中包含的引用值始终会在相关对象间共享,子类实例不能向父类传参

寄生式继承

寄生式继承的思路与(寄生) 原型式继承[工厂模式](https://so.csdn.net/so/search?q=工厂模式&spm=1001.2101.3001.7020) 似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}
 
function createAnother(obj) {
  let clone = objectCopy(obj);
  clone.showName = function () {
    console.log('my name is:', this.name);
  };
  return clone;
}
 
let person = {
     name: "mjy",
     age: 18,
     hoby: ['唱', '跳']
}
 
let child1 = createAnother(person);
child1.hoby.push("rap");
console.log(child1.hoby); // ['唱', '跳', 'rap']
child1.showName(); // my name is: mjy
 
let child2 = createAnother(person);
console.log(child2.hoby); // ['唱', '跳', 'rap']

优点:写法简单,不需要单独创建构造函数。

缺点:通过寄生式继承给对象添加函数会导致函数难以重用。使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.

寄生组合式继承

前面讲过,组合继承是常用的经典继承模式,不过,组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数;一次是在创建子类型的时候,一次是在子类型的构造函数内部。寄生组合继承就是为了降低父类构造函数的开销而实现的。

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}
 
function inheritPrototype(child, parent) {
  let prototype = objectCopy(parent.prototype);
  prototype.constructor = child;
  Child.prototype = prototype;
}
 
function Parent(name) {
  this.name = name;
  this.hoby = ['唱', '跳']
}
 
Parent.prototype.showName = function () {
  console.log('my name is:', this.name);
}
 
function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}
 
inheritPrototype(Child, Parent);
Child.prototype.showAge = function () {
  console.log('my age is:', this.age);
}
 
let child1 = new Child("mjy", 18);
child1.showAge(); // 18
child1.showName(); // mjy
child1.hoby.push("rap");
console.log(child1.hoby); // ['唱', '跳', 'rap']
 
let child2 = new Child("yl", 18);
child2.showAge(); // 18
child2.showName(); // yl
console.log(child2.hoby); // ['唱', '跳']

优点是:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;

缺点是:代码复杂

class继承

// 继承Person类中的sayHi方法
class Person {
    sayHi(){
        console.log("hello");
    }
}
 
class Chinese extends Person {}
 
 
// 继承Person类中的属性和方法
class Person {
    constructor(name, age, gender){
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
 
    sayHi(){
        console.log("hello");
    }
}
 
// 子类
class Chinese extends Person {
    constructor(name, age, gender, skin){
        // 在子类中,必须在constructor函数中,首先调用super()
        super(name, age, gender);    // 相当于Person.call(this,name,age,gender)
        // 调用super之后才可以去写其他代码
        this.skin = skin;
    }
}
let xm = new Chinese("xm", 20, "male", "黄");
console.log(xm);
xm.sayHi();

前端页面性能指标

首屏绘制

First Paint fp是时间线上的第一个时间点是至浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间,简而言之就是浏览器第一次发生变化的时间

首屏内容绘制

First Contentful Paint FCP 翻译为首次内容绘制,指浏览器从响应用户输入网络地址,在页面首次绘制文本,图片,非白色的canvas或者SVG才能算作FCP。

可交互时间

time to interactive TTI,首屏交互时间,表示网页第一次完全达到可交互状态的时间点。可交互状态指的是页面上的 UI 组件是可以交互的(可以响应按钮的点击或在文本框输入文字等),不仅如此,此时主线程已经达到“流畅”的程度,主线程的任务均不超过50毫秒。在一般的管理系统中,TTI 是一个很重要的指标。

首次有效绘制

First Meaning Paint FMP,表示页面的“主要内容”开始出现在屏幕上的时间点,它以前是我们测量用户加载体验的主要指标。本质上是通过一个算法来猜测某个时间点可能是 FMP,但是最好的情况也只有77%的准确率,在lighthouse6.0 的时候废弃掉了这个指标,取而代之的是 LCP 这个指标。

LCP(全称“Largest Contentful Paint”)表示可视区“内容”最大的可见元素开始出现在屏幕上的时间点。

vue的生命周期钩子函数

  1. beforCreate() 实例创建前触发
  2. created() 实例创建完成
  3. beforMount()模板渲染前,可以访问数据,模板编译完成,虚拟dom已经存在
  4. mounted()模板渲染完成,可以拿到dom节点的数据
  5. beforeUpdate()更新前
  6. updated()更新完成
  7. activated()激活前
  8. deactivated()激活后
  9. beforDestory()销毁前
  10. destroyed()销毁后

组件之间如何通信

父子:props,emit,refs
兄弟:eventbus,emit
隔代:inject,provide,attr,listener
不传火了:vuex,pinia

Diff算法

简介

diff算法可以看做是一种对比算法,对比的对象是新旧虚拟DOM。顾名思义,diff算法可以找到新旧Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比用户的结果更新真实DOM

它有六个属性,sel表示当前节点的属性,children表示当前节点的其他子标签节点,elm表示当前虚拟节点对应的真实节点(这里暂时没有),key纪委当前节点的key,text表示节点下的文本

let vnode = {
    sel: 'ul', 
    data: {},
    children: [ 
        {
            sel: 'li', data: { class: 'item' }, text: 'son1'
        },
        {
            sel: 'li', data: { class: 'item' }, text: 'son2'
        },    
    ],
    elm: undefined,
    key: undefined,
    text: undefined
}

那么虚拟Dom有什么用呢。我们其实可以把虚拟dom理解成对应真实Dom的一种状态。当真实Dom发生变化后,虚拟Dom可以为我们提供这个真实Dom变化之前和变化之后的状态,我们通过对比这俩个状态,即可得出Dom真实需要更新的部分,即可实现最小量更新。在一些比较复杂的Dom变化场景中,通过对比虚拟Dom后更新真实Dom会比直接更新真实Dom效率高,这就是虚拟Dom和Diff算法真正存在的含义

H函数

在 介绍diff算法原理之前还需要简单让大家了解一下h函数,因为我们要靠它称为我们生成虚拟Dom,这个render函数里面传入的那个h函数

h函数可以接受多种类型的参数,但其实它内部只干了一件事,就是执行vnode函数。根据传入h函数的参数来决定执行vnode函数时传入的参数。那么vnode函数又是干什么的呢?vnode函数其实也只干了一件事,就是把传入h函数的参数转化为一个对象,即虚拟Dom。

// vnode.js
export default function (sel, data, children, text, elm) {
    const key = data.key 
    return {sel, data, children, text, elm, key}
}

执行h函数后,内部会通过vnode函数生成虚拟Dom,h函数在把这个虚拟return出去

算法

总结

由于diff算法对比的是虚拟Dom,而虚拟Dom是呈树状的,所以我们可以发现,diff算法中充满了递归。总结起来,其实diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。

这里再提一嘴key,我们面试中经常会被问到vue中key的作用。根据上面我们分析的,key的主要作用其实就是对比两个虚拟节点时,判断其是否为相同节点。加了key以后,我们可以更为明确的判断两个节点是否为同一个虚拟节点,是的话判断子节点是否有变更(有变更更新真实Dom),不是的话继续比。如果不加key的话,如果两个不同节点的标签名恰好相同,那么就会被判定为同一个节点(key都为undefined),结果一对比这两个节点的子节点发现不一样,这样会凭空增加很多对真实Dom的操作,从而导致页面更频繁得进行重绘和回流。

所以我认为合理利用key可以有效减少真实Dom的变动,从而减少页面重绘和回流的频率,进而提高页面更新的效率。

VNode

在刀耕火种的年代,我们需要在各个事件方法中直接操作DOM来达到修改视图的目的。但是当应用一大就会变得难以维护。

那我们是不是可以把真实DOM树抽象成一棵以JavaScript对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实DOM重绘到页面上呢?于是虚拟DOM出现了,它是真实DOM的一层抽象,用属性描述真实DOM的各个特性。当它发生变化的时候,就会去修改视图。

可以想象,最简单粗暴的方法就是将整个DOM结构用innerHTML修改到页面上,但是这样进行重绘整个视图层是相当消耗性能的,我们是不是可以每次只更新它的修改呢?所以Vue.js将DOM抽象成一个以JavaScript对象为节点的虚拟DOM树,以VNode节点模拟真实DOM,可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实DOM,只需要操作JavaScript对象后只对差异修改,相对于整块的innerHTML的粗暴式修改,大大提升了性能。修改以后经过diff算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样做减少了很多不需要的DOM操作,大大提高了性能。

Vue就使用了这样的抽象节点VNode,它是对真实DOM的一层抽象,而不依赖某个平台,它可以是浏览器平台,也可以是weex,甚至是node平台也可以对这样一棵抽象DOM树进行创建删除修改等操作,这也为前后端同构提供了可能。

这是js源码中对vnode的定义

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  functionalContext: Component | void; // only for functional component root nodes
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
 
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*当前节点的标签名*/
    this.tag = tag
    /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    this.data = data
    /*当前节点的子节点,是一个数组*/
    this.children = children
    /*当前节点的文本*/
    this.text = text
    /*当前虚拟节点对应的真实dom节点*/
    this.elm = elm
    /*当前节点的名字空间*/
    this.ns = undefined
    /*编译作用域*/
    this.context = context
    /*函数化组件作用域*/
    this.functionalContext = undefined
    /*节点的key属性,被当作节点的标志,用以优化*/
    this.key = data && data.key
    /*组件的option选项*/
    this.componentOptions = componentOptions
    /*当前节点对应的组件的实例*/
    this.componentInstance = undefined
    /*当前节点的父节点*/
    this.parent = undefined
    /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    this.raw = false
    /*静态节点标志*/
    this.isStatic = false
    /*是否作为根节点插入*/
    this.isRootInsert = true
    /*是否为注释节点*/
    this.isComment = false
    /*是否为克隆节点*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = false
  }
 
  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

这是一个最基础的VNode节点,作为其他派生VNode类的基类,里面定义了下面这些数据。

tag: 当前节点的标签名

data: 当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息

children: 当前节点的子节点,是一个数组

text: 当前节点的文本

elm: 当前虚拟节点对应的真实dom节点

ns: 当前节点的名字空间

context: 当前节点的编译作用域

functionalContext: 函数化组件作用域

key: 节点的key属性,被当作节点的标志,用以优化

componentOptions: 组件的option选项

componentInstance: 当前节点对应的组件的实例

parent: 当前节点的父节点

raw: 简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false

isStatic: 是否为静态节点

isRootInsert: 是否作为跟节点插入

isComment: 是否为注释节点

isCloned: 是否为克隆节点

isOnce: 是否有v-once指令

打个比方,比如说我现在有这么一个VNode树

{
    tag: 'div'
    data: {
        class: 'test'
    },
    children: [
        {
            tag: 'span',
            data: {
                class: 'demo'
            }
            text: 'hello,VNode'
        }
    ]
}

渲染之后就是这样的

<div class="test">
    <span class="demo">hello,VNode</span>
</div>

下面是一些创建vnode的方法

/*创建一个空VNode节点*/
export const createEmptyVNode = () => {
  const node = new VNode()
  node.text = ''
  node.isComment = true
  return node
}
/*创建一个文本节点*/
export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

vue中的keep-alive

keep-alive是vue的内置组件,会缓存不活动的组件实例而不是销毁他们。

和transition相似,keep-alive是一个抽象组件:它自身不会渲染成一个DOM元素,也不会出现在父组件中

作用:在组件切换过程中将状态保留在内存中,防止重复渲染DOM,减少加载时间即性能消耗,提高用户体验性

原理:在created函数调用时将需要缓存的Vnode节点保存在this.cache中,在render页面渲染时,如果VNode的缓存的name符合缓存条件(即可以用include以及exclude控制),则会从this.catch中取出之前缓存的Vnode实例进行渲染

参数名描述
include字符串或正则表达式只有名称匹配的组件会被缓存。
exclude字符串或正则表达式任何名称匹配的组件都不会被缓存。
max数字最多可以缓存多少组件实例。
// router.js
{
  path: '/home',
  name: 'home',
  component: () => import('../views/home.vue')
},
{ 
  path: '/test',
  name: 'test',
  component: () => import('../views/test.vue')
},
// App.vue
<keep-alive include="test">
   <router-view/>
</keep-alive>
// 1. 将缓存 name 为 test 的组件(基本)
<keep-alive include='test'>
  <router-view/>
</keep-alive>
    
// 2. 将缓存 name 为 a 或者 b 的组件,结合动态组件使用
<keep-alive include='a,b'>
  <router-view/>
</keep-alive>
    
// 3. 使用正则表达式,需使用 v-bind
<keep-alive :include='/a|b/'>
  <router-view/>
</keep-alive>    
    
// 4.动态判断
<keep-alive :include='includedComponents'>
  <router-view/>
</keep-alive>
    
// 5. 将不缓存 name 为 test 的组件
<keep-alive exclude='test'>
  <router-view/>
</keep-alive>

// 6. 和 `<transition>` 一起使用
<transition>
  <keep-alive>
    <router-view/>
  </keep-alive>
</transition>

// 7. 数组 (使用 `v-bind`)
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>
// test.vue
<template>
  <div class="wrap">
    <input type="text" v-model="inputVal">
  </div>
</template>

<script>
export default {
  name:'test',
  data(){
    return {
      inputVal:'',
    }
  }
}
</script>

此时切换路由,我们就会发现 test 文件内的 inputVal 会被缓存了,而 home 内的值没有被缓存。

此外,我们还可以通过路由中的 meta 属性来控制,是否需要缓存。
将 test 路由中的 meta 添加 keepAlive 属性为 true,表示当前路由组件要进行缓存。

// router.js
{
 path: '/home',
 name: 'home',
 component: () => import('../views/home.vue')
},
{ 
 path: '/test',
 name: 'test',
 meta:{
   keepAlive:true
 },
 component: () => import('../views/test.vue')
},

keep-alive 代码可以结合 v-if 进行包裹,如果 meta 中的 keepAlive 为 true 进行缓存,否侧不进行缓存。

<keep-alive>
  <router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />

实际开发中,我们可以结合路由守卫来实现需要缓存组件的缓存。

export default {
  beforeRouteLeave(to, from, next) {
    to.meta.keepAlive = true;
    next();
  }
}
名称描述
activated在 keep-alive 组件激活时调用, 该钩子函数在服务器端渲染期间不被调用。
deactivated在 keep-alive 组件停用时调用,该钩子在服务器端渲染期间不被调用。

使用< keep-alive > 会将数据保留在内存中,如果要在每次进入页面的时候获取最新的数据,需要在 activated 阶段获取数据,承担原来created钩子中获取数据的任务。

被包含在 < keep-alive > 中创建的组件,会多出两个生命周期的钩子: activated 与 deactivated

activated:在组件被激活时调用,在组件第一次渲染时也会被调用,之后每次keep-alive激活时被调用。

deactivated:在组件被停用时调用。

注意: 只有组件被 keep-alive 包裹时,这两个生命周期才会被调用,如果作为正常组件使用,是不会被调用,以及在 2.1.0 版本之后,使用 exclude 排除之后,就算被包裹在 keep-alive 中,这两个钩子依然不会被调用!另外在服务端渲染时此钩子也不会被调用的。

什么时候获取数据?

当引入keep-alive 的时候,页面第一次进入,钩子的触发顺序 created -> mounted -> activated,退出时触发 deactivated。
当再次进入(前进或者后退)时,只触发 activated。

我们知道 keep-alive 之后页面模板第一次初始化解析变成 HTML 片段后,再次进入就不在重新解析而是读取内存中的数据。
只有当数据变化时,才使用 VirtualDOM 进行 diff 更新。所以,页面进入的数据获取应该在 activated 中也放一份。
数据下载完毕手动操作 DOM 的部分也应该在activated中执行才会生效。

所以,应该 activated 中留一份数据获取的代码,或者不要 created 部分,直接将 created 中的代码转移到 activated 中。

应用场景

如果未使用 keep-alive 组件,则在页面回退时仍然会重新渲染页面,触发 created 钩子,使用体验不好。
在以下场景中使用 keep-alive 组件会显著提高用户体验,菜单存在多级关系(如:主页 -> 列表页 -> 详情页)的场景:

Render函数

简介

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,真的需要 JavaScript 的完全编程的能力。这时可以用渲染函数,它比模板更接近编译器。

render 函数和 template 一样都是创建 html 模板的,但是有些场景中用 template 实现起来代码冗长繁琐而且有大量重复,这时候就可以用 render 函数。

createElement 函数讲解

这个函数的作用就是生成一个VNode节点,render函数得到这个VNode节点之后,返回给Vue.js的mount函数,渲染成真实DOM节点并挂在到根节点上

createElement 函数的返回值是 VNode(即:虚拟节点)

参数

  1. 一个HTML标签字符串,组件选项对象,或者解析上述任何一种的async异步函数
  2. 一个包含模板属性的数据对象,可以在template中使用这些特性
  3. 子虚拟节点,由createElemet构建组成,也就可以使用字符串来生成文本虚拟节点
// main.js文件
new Vue({
  el: '#app',
  render:function (createElement) {
    //1.普通用法
    // createElement(标签,{属性},[内容])
    return createElement("h2",{class:"box"},['hello',createElement("button",["按钮"])])
  }
})

我们也可以自定义一个组件,传递给render函数

// ...
let Cpn = {
  template:'<h2>{{message}}</h2>',
  data(){
    return {
      message:"我是组件"
    }
  }

}

new Vue({
  el: '#app',
  render:function (createElement) {
    //2.使用组件
    return createElement(Cpn)
  }
})

效果如图

nextTick原理

js运行机制

首先我们要了解JavaScript是一个单线程的脚本语言,因为如果同时对同一个Dom节点进行添加和删除操作,那么就会出现问题,我该听谁的,具体怎么操作。
了解了js是一门单线程的脚本语言,我们来认识几个概念

  • 主线程: 可以理解成js的首要任务,也可以方便理解成同步任务的先进先出。调用了,使用了同步任务就执行。
    执行栈: 因为执行栈是一个先入后出的一个数据结构,为了便于理解,我们姑且先浅显的理解成有一个同步任务来了进入栈底马上执行它,然后释放。

任务队列: 任务队列的存在就是为了应对一些异步任务的存在,我们在执行一个任务如果迟迟等不到总不可能一直等待着,程序不接着往下进行。
宏任务和微任务 在这么我们都将其看作是异步任务,因为通俗理解主线程是在执行同步任务完成之后才会来执行宏任务和微任务。在这里我们通俗理解微任务的优先级比宏任务要高,先于宏任务执行。

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

宏任务包含:执行script标签内部代码、setTimeout/setInterval、ajax请、postMessageMessageChannel、setImmediate,I/O(Node.js) 微任务包含:Promise、MutonObserver、Object.observe、process.nextTick(Node.js)

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

异步: 指的是不进入主线程,某个异步任务可以执行了,该任务才会进入主线程执行。执行完函数或方法后,不必阻塞性地等待返回值或消息,只需要向系统委托一个异步过程,那么当系统接收到返回值或消息时,系统会自动触发委托的异步过程,从而完成一个完整的流程。

异步更新队列

vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更

如同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM是非常重要的

然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout

vue的nextTick实现原理

  1. vue异步队列的方式来控制Dom更新和nextTick回调后先执行
  2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  3. 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

$nextTick的原理:$nextTick本质是返回一个Promise 加分回答 应用场景:在钩子函数created()里面想要获取操作Dom,把操作DOM的方法放在$nextTick中

vue组件data为什么必须是函数

因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

CSS3硬件加速

GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率

左边元素的动画通过 left/top 操作位置实现,右边元素的动画通过 transform: translate 实现,你可以打开 chrome 的 “Paint flashing” 查看,绿色部分是正在 repaint 的内容。

查看地址

从 demo 中可以看到左边的图形在运动时外层有一圈绿色的边框,表示元素不停地 repaint,并且可以看到其运动过程中有丢帧现象,具体表现为运动不连贯,有轻微闪动。

那么transform是如何让动画不会导致重绘的呢?最直接的答案就是transform会直接使用硬件加速,在GPU中运行,绕开了软件渲染。

之前学习 flash 的时候,就知道动画是由一帧一帧的图片组成,在浏览器中也是如此。我们首先看一下,浏览器每一帧都做了什么。

  1. JavaScript:JavaScript 实现动画效果,DOM 元素操作等。
  2. Style(计算样式):确定每个 DOM 元素应该应用什么 CSS 规则。
  3. Layout(布局):计算每个 DOM 元素在最终屏幕上显示的大小和位置。由于 web 页面的元素布局是相对的,所以其中任意一个元素的位置发生变化,都会联动的引起其他元素发生变化,这个过程叫 reflow。
  4. Paint(绘制):在多个层上绘制 DOM 元素的的文字、颜色、图像、边框和阴影等。
  5. Composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。

浏览器在获取 render tree(详细知识可以查看深入了解浏览器重排和重绘)后,渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU 形成渲染纹理。这里的秘诀就在于通过transform的层会使用GPU渲染,因此不需要重绘,这一点非常类似 3D 绘图功能,最终这些使用 transform 的图层都会由独立的合成器进程进行处理。

过程如下:

render tree -> 渲染元素 -> 图层 -> GPU 渲染 -> 浏览器复合图层 -> 生成最终的屏幕图像。

注意:

chrome devtools 中可以开启 Rendering 中的 Layer borders 查看图层纹理。

其中黄色边框表示该元素有 3d 变换,表示放到一个新的复合层(composited layer)中渲染,蓝色栅格表示正常的 render layer。

在文章开始给出的例子中,CSStransformGPU直接创建一个新的层。我们也可以开启 Layer borders,(这个选项可以帮助我们查看哪些是单独的层,开启这个选项以后单独的层会具有一个橙色的边框。)可以观察到,使用 transform: translate 动画的元素,外围有一个黄色的边框,可知其为复合层。

在 GPU 渲染的过程中,一些元素会因为符合了某些规则,而被提升为独立的层(黄色边框部分),一旦独立出来,就不会影响其它 DOM 的布局,所以我们可以利用这些规则,将经常变换的 DOM 主动提升到独立的层,那么在浏览器的一帧运行中,就可以减少 Layout 和 Paint 的时间了。

源代码


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>css3 animation</title>
<link rel="icon" href="favicon.ico" type="image/vnd.microsoft.icon" />
<style>
        *, *:hover {
            margin: 0;
            padding: 0
        }
        body {
            display: flex;
            justify-content: center;
            overflow: hidden;
        }
        .wrapper {
            width: 50%;
            height: 100vh;
            border-left: 1px solid #666;
            padding: 10px;
        }
        .element {
            width: 60px;
            height: 40px;
            border-radius: 5px;
            background: #92B901;
        }
        @keyframes move_animation {
            0% {transform: translate(0, 0);}
            25% {transform: translate(200px, 0);}
            50% {transform: translate(200px, 200px);}
            75% {transform: translate(0, 200px);}
            100% {transform: translate(0, 0);}
        }
        @keyframes position_animation {
            0% { top: 10px; left: 10px; }
            25% { top: 10px; left: 210px; }
            50% { top: 210px; left: 210px; }
            75% { top: 210px; left: 10px; }
            100% { top: 10px; left: 10px; }
        }
        .css-div {
            animation: move_animation 4s infinite;
        }
        .ps-div {
            position: absolute;
            animation: position_animation 4s infinite;
        }
    </style>
</head>
<body>
<section class="wrapper">
<div class="ps-div element"></div>
</section>
<section class="wrapper">
<div class="css-div element"></div>
</section>
</body>
</html>

创建独立图层

哪些规则能让浏览器主动帮我们创建独立的层呢?

  1. 3D 或者透视变换(perspective,transform) 的 CSS 属性。
  2. 使用加速视频解码的 video 元素。
  3. 拥有 3D(WebGL) 上下文或者加速 2D 上下文的 canvas 元素。
  4. 混合插件(Flash)。
  5. 对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素。
  6. 拥有加速 CSS 过滤器的元素。
  7. 元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)。
  8. 元素有一个兄弟元素在复合图层渲染,并且该兄弟元素的 z-index 较小,那这个元素也会被应用到复合图层。

开启 GPU 加速

CSS的animation、tranform、transition并不会自动开启GPU加速,而是通过浏览器的缓慢的软件渲染引擎来实现执行,那么我们怎么才能实现GPU加速呢,很多浏览器提供了某些触发该模式的规则。

比如使用 translate3d() rotate3d() scale3d() 这几个方法,我们就可以使用GPU加速了。

如下几个css属性可以触发硬件加速:

  • transform( translate3d、translateZ(0)等)
  • opacity
  • filter(滤镜:drop-shadow()、opacity(),函数与已有的box-shadow、opacity属性很相似;不同之处在于,通过滤镜,一些浏览器为了更好的性能会提供硬件加速)
  • will-change:哪一个属性即将发生变化,进而进行优化。

因此为了页面更加流畅,高性能的动画,我们可以使用GPU来处理。

如果有一些元素不需要用到上述属性,但是需要触发硬件加速效果,例如:某些情况下,我们并不想要对元素应用3D变换的效果,却还想要实现GPU加速,可以使用一些小技巧来诱导浏览器开启硬件加速。

transform: translateZ(0)

这个声明就是可以触发桌面端和移动端的GPU加速,这是一个非常有效的方式(包含所有的浏览器前缀):

.element {
    -webkit-transform: translateZ(0);
    -moz-transform: translateZ(0);
    -ms-transform: translateZ(0);
    -o-transform: translateZ(0);
    transform: translateZ(0); 
    /**或者**/
    transform: rotateZ(360deg);
    transform: translate3d(0, 0, 0);
} 

使用硬件加速需要注意的地方

Memory

大部分重要的问题都是关于内存。GPU处理过多的内容会导致内存问题。这在移动端和移动端浏览器会导致崩溃。因此,通常不会对所有的元素使用硬件加速。(过多地开启硬件加速可能会耗费较多的内存,因此什么时候开启硬件加速,给多少元素开启硬件加速,需要用测试结果说话。)

Font rendering

GPU渲染字体会导致抗锯齿无效。这是因为GPUCPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

The Near Future

有必要使用transform hack的地方是提高性能。浏览器自身也提供了优化的功能,这也就是will-change属性。这个功能允许你告诉浏览器这个属性会发生变化,因此浏览器会在开始之前对其进行优化。这里有一个例子:

.example {
  will-change: transform;
}

遗憾的是,并不是所有浏览器都支持这个功能。

GPU优点

  • GPU渲染可以提高动画性能
  • GPU渲染会提高动画的渲染帧数
Last modification:January 18, 2023
如果觉得我的文章对你有用,请随意赞赏