1 隐式转换
1.1 toString()
除对象自身外都重写了toString()
({}).toString() // "[object Object]"
([]).toString() // ""
(NaN).toString() // "NaN"
null,undefined,boolean // 'null', 'undefined', 'true','false'
1.2 valueOf
Number,String,Boolean,Date重置啦valueOf,所以
let num = new Number(1)
console.log(num.valueOf === Object.prototype.valueOf) // false
1.3 toBoolean
undefined // false
null // false
string // ""为false, 其它为true
number // 0或NaN为false,其它为true
object // true
1.4 toNumber
undefined // NaN
null // 0
boolean // 0或1
string // ""->0;"123"->123;"asdf"->NaN
object // 先转为基础类型再转数组
1.5 +操作符
+undefined // NaN
+null // 0
+true // 1
+false // 0
+'12' // 12
+'12a' // NaN
+{} // NaN
+[] // 0
1. 6 toPrimitive
toprimitive(input, preferredType)
preferredType可以指定最终转化的类型,他可以使Number类型或者String类型,这依赖于toprimitive()方法执行的结果返回的是Number类型或String类型。
1.6.1 值的转化过程如下
1.如果输入Input是基本类型,就返回这个值
2如果输入变量是Object类型,那么调用input.valueOf(),如果返回结果是基本类型,就返回这个值
3.如果都不是的话就调用input.toString(),如果结果是节本类型,就返回它
4.如果以上都不可以,就会抛出有一个类型错误TypeError,表示转化input变量到基本类型失败。
如果preferredType是Number,那转换算法就会像上述说明的顺序执行,如果是String,步骤2和步骤3会交换顺序。preferredType是一个缺省值,如果不输入的,Data类型会被当做String类型处理,其他变量会当作Number处理。默认的valueOf返回this,默认的toString()会返回类型信息。
1.6.2 实现一下
const primitiveTypes = ['Undefined', 'Null','Number', 'String', 'Boolean'];
const util = {
getType(val) {
return Object.prototype.toString.call(val).slice(8, -1)
},
isprimirtive(val) {
return primitiveTypes.includes(this.getType(val))
}
}
function toPrimitive(input, preferredType) {
// 本身就是基础数据类型,直接返回
if(utile.isprimitive(input)) return input;
// 没有传preferredType时,如果input是日期类型,则默认是String,否则默认Number
if(preferredType===undefined){
preferredType = util.getType(input) === "Date" ? "String" : "Boolean"
}
// Number时,调用顺序为valueOf->toString
if(preferredType==="Number"){
const value = input.valueOf();
if(util.isprimitive(value)) return value;
const str = input.toString();
if(util.isprimitive(value)) return str;
}
// String时,调用顺序为toString->valueOf
if(preferredType==="String"){
const str = input.toString();
if(util.isprimitive(str)) return str;
const value = input.valueOf();
if(util.isprimitive(value)) return value;
}
throw new Error("valueOf和toString方法返回的都不是基础数据类型");
}
1.7 相关面试题
1.7.1 如何让 (a=1&&a=2&&a===3) 的值为 true?
1.其中宽松相等
var a = {value: 0};
a1.valueOf = function(){
return this.value += 1;
}
a==1&&a===2&&2===3 // true
const a = {
i: 1,
toString: function () {
return a.i++;
}
}
a == 1 && a == 2 && a == 3 // true
2.让它们严格相等
var value = 0;
Object.defineProperty(window, 'a', {
get: function() {
return this,value += 1;
}
})
a===1&&a===2&&a===3 // true
js中的原始类型不满足于上面的条件(严格相等没有转化的过程),所以我们需要通过一些方法调用一个函数,并在这个函数中做我们想做的事情。严格相等时,valueOf不会被js引擎调用。
1.7.2 附defineProperty
取自 mdn.
类型 | 可选键值 | 描述 | 默认值 |
---|---|---|---|
数据描述符 | configurable | 当且仅当它为true时,该属性的描述符才能被改变,同事该属性也能从对应的对象上被删除 | false |
数据描述符 | enumerable | 当且仅当它为true时,该属性才会出现在对象的枚举属性中 | false |
数据描述符 | writable | 当且仅当它为true时,属性值(value)才能被赋值运算符改变 | false |
数据描述符 | value | 属性对应的值,可以是任何有效的JavaScript值(数值,对象,函数等) | undefined |
存取描述符 | get | 属性的getter函数,如果没有getter,则为undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该属性的返回值会被用作属性的值。 | undefined |
存取描述符 | set | 属性的setter函数,如果没有setter,则为undefined。当属性值被修改时,会调用此函数,该方法接收一个参数(也就是被赋予的新值),会传入赋值时的this对象。 | undefined |
数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
object1.property1 = 77;
// throws an error in strict mode
console.log(object1.property1);
// expected output: 42
1.7.3 实现一个toprimitive
const primitiveTypes = ['Undefined', 'Null', 'Number', 'String', 'Boolean']
const util = {
getType (val) {
returnObject.prototype.toString.call(val).slice(8, -1)
},
isprimitive (val) {
return primitiveTypes.includes(this.getType(val))
}
}
function toPrimitive (input, preferredType) {
if (util.isPrimitive(input)) return input // 本身就是基础数据类型,直接返回
// 没有传 preferredType 时,如果 input 是日期类型,则默认是 String,否则默认 Number
if (preferredType === undefined) {
preferredType = util.getType(input) === 'Date' ? 'String' || 'Number'
}
// Number 时,调用顺序为 valueOf -> toString
if (preferredType === 'Number') {
const value = input.valueOf()
if (util.isPrimitive(value)) return value
const str = input.toString()
if (util.isPrimitive(str)) return str
}
// String 时,调用顺序为 toString -> valueOf
if (preferredType === 'String') {
const str = input.toString()
if (util.isPrimitive(str)) return str
const value = input.valueOf()
if (util.isPrimitive(value)) return value
}
thrownewError('valueOf 和 toString 方法返回的都不是基础数据类型')
}
2 setState()
2.1 setState()的关键点
1.setState不会立刻改变React组件中state的值;
2.setState通过引发一次组件的更新过程来引发重新绘制;
3.多次setstate函数调用产生的效果会合并。
解释:
this.setState是一个对象,所以只是改来this.state是不能让React组件重新绘制一遍的。所以需要setState去更改状态,当setState被调用时,能驱动组件的更新过程(shouldComponentUpdate、componentWillUpdate、render、componentdidupdate)
当shouldComponentUpdate函数调用的时候,this.state没有得到更新,当componentWillUpdate函数调用的时候,this.state依然美誉得到更新,
当render函数被调用(或当shouldComponentUpdate函数返回false的时候,这时候更新过程就被中断了,虽然不会调用render,但是也会更新this.state的时候),this.state才得到更新。
一个不太优雅的写法
如果想set之后立马获得值,可以用setState的回调函数
this.setState({
xxx: xxx
},()=>{})
2.2 函数式setState用法
函数式this.setState的参数是一个函数。
找个函数会接收到两个参数(当前的state值,当前的props),如果在函数式setState中插入来一个传参是对象的this.setState,会把之前积攒的效果清空。
eg:
function incrementMultiple() {
const currentCount = this.state.count;
this.setState({count: currentCount + 1});
this.setState({count: currentCount + 1});
this.setState({count: currentCount + 1});
}
// 这个只会增加1,因为调用this.setstate时,并没有立即更改this.state,所以this.setState只是反复设置同一个值而已。
函数式:
function increment(state, props) {
return {count: state.count + 1};
}
function incrementMultiple() {
this.setState(increment);
this.setState(increment);
this.setState(increment);
}
// 这个会增加3,当incremet函数被调用时,this.state没有改变,要等到render或shouldComponentUpdate为false)时才会改变。
function incrementMultiple() {
this.setState(increment);
this.setState(increment);
this.setState({count: this.state.count + 1});
this.setState(increment);
}
// 这个会增加2,参数是对象的setState的调用会给前两次清空。
3 React Fiber
因为js单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其它输入作出相应改变,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。
把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其它任务一个执行的机会,这样唯一的线程就不会被独占,其它任务依然有运行的机会。React Fiber把更新过程碎片化,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其它禁忌任务要做,如果没有就继续更新,如果有紧急任务,那就去做紧急任务。维护每一个分片的数据结构就是Fiber。
因为一个更新过程可能被打断,所以React Fiber一个更新过程被氛围两个阶段:
第一个阶段: Reconciliation Phase
可能会调用的生命周期:
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
第一个阶段: Commit Phase
componentDidMount
componentDidUpdate
componentWillUnmount
在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!
使用React Fiber之后,一定要检查一下第一阶段相关的这些生命周期函数,看看有没有逻辑是假设在一个更新过程中只调用一次,有的话就要改了。
我们挨个看一看这些可能被重复调用的函数。
componentWillReceiveProps,即使当前组件不更新,只要父组件更新也会引发这个函数被调用,所以多调用几次没啥,通过!
shouldComponentUpdate,这函数的作用就是返回一个true或者false,不应该有任何副作用,多调用几次也无妨,通过!
render,应该是纯函数,多调用几次无妨,通过!
只剩下componentWillMount和componentWillUpdate这两个函数往往包含副作用,所以当使用React Fiber的时候一定要重点看这两个函数的实现。
4 前端性能优化CRP
CRP(Critical Rendering Path)关键渲染路径
- URL解析
地址解析和编码
HSTS
缓存检查 - DNS解析
- TCP三次握手
- 发送HTTP请求,服务器处理请求,返回相应结果
- TCP四次握手
- 浏览器渲染
- 代码运行中
4.1 浏览器渲染流程
4.1.1 构建DOM树,CSSOM树,渲染树
1.DOM树
转换,令牌,词法分析,DOM构建
Bytes->Characters->Tokens->Nodes->DOM
2.CSSOM树
3.Render-Tree渲染树
4.1.2 总结
处理HTML标记,构建DOM树
处理CSS标记,构建CSSOM树
将DOM树和CSSOM树融合成渲染树
根据生成的渲染树,计算它们在设备视口内的确切位置和大小,这个计算的阶段就是回流=>布局(layout)和重排(reflow)
根据渲染树以及回流得到的几何信息,得到节点的绝对像素=>绘制(painting)和栅格化(rasterizing)
4.1.3 优化方案
- 标签与异化和避免深层次嵌套
- css选择器渲染是从右向左
- 尽早尽快的把css下载到客户端(充分利用HTTP多请求并发机制)
- 避免阻塞的js加载减少DOM的回流和重绘
(1) 重绘:样式的改变(但宽高,大小,位置等不变)
eg: outline,visibility,color,background-color
(2) 回流:元素的大小或者位置发生了变化(当页面布局和几何信息发生变化的时候),触发了重新布局,导致渲染树重新计算布局和渲染
eg: 如添加或删除可见的DOM元素,元素的位置发生比那话,元素的尺寸发生变化;内容发
变化(比如文本变化或图片被另一个不同尺寸的图片代替);页面一开始渲染的时候(无法避免);因为回流是根据时候的大小来计算元素的位置和大小的,所以浏览器的视口尺寸变化也会引发回流
注意: 回流一定会触发重绘,而重绘不一定会回流
避免DOM的回流:
- 放弃传统操作DOM的时代,基于vue/react开始数据影响试图模式
eg:mvvm/mvc/virtual dom/dom diff - 分离读写操作
- 样式集中改变
- 缓存布局信息
- 元素批量修改
- 动画效果应用到position属性为absolute或fixed的元素上(脱离文档流)
- css硬件加速(gpu加速)
- 牺牲平滑度换取速度
- 避免table布局和使用css的js表达式
4.1.2 网络交换层面上的优化
- DNS的优化
每一次DNS解析时间预计在20~120ms
减少DNS请求次数
DNS预获取(dns-prefetch)
eg:<link rel="dns-prefetch" href="" />
- 减少HTTP请求次数和请求资源大小
资源合并压缩
字体图表
Base64
GZIP(一般的文件能压缩60%多)
图片懒加载
数据延迟分批加载
CDN资源。。。 - 应用缓存
Service Worker 浏览器独立线程进行缓存
Memory Cache 内存缓存
Disk Cache 硬盘缓存
Push Cache 推送缓存(HTTP/2中的)
打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配,如有则使用,如没有则发送网络请求。
普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话),其次才是 disk cache。
强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。
4.1.2.1 强缓存 Expires/Cache-Control
浏览器对于强缓存的处理:根据第一次请求资源时返回的相应头来确定的
- Expires:缓存过期时间,用来指定资源到期的时间(HTTP/1)
- Cache-Control:cache-control: max-age=2592000第一次拿到资源后的2592000秒内(30天),再次发送请求,读取缓存中的信息(HTTP/1.1)
两者同时存在的话,Cache-Control优先级高于Expires
4.1.2.2 协商缓存 Last-Modified / ETag
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务发起请求,由服务器根据缓存标识决定是否使用缓存的过程
协商缓存生效,返回304或not modified
协商缓存失效,返回200和请求结果
-
Last-Modified和If-Modified-Since
第一次访问资源,服务器返回资源的同时,响应头中设置 Last-Modified(服务器上的最后修改时间),浏览器接收后,缓存文件和响应头;
下一次请求这个资源,浏览器检测到有 Last-Modified,于是添加If-Modified-Since请求头,值就是Last-Modified中的值;
服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回304和空的响应体,直接从缓存读取,如果If-Modified-Since的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和200;
但是Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源; -
ETag和If-None-Match
Etag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag就会重新生成;下一次加载资源向服务器发送请求时,会将上一次返回的Etag值放到请求头If-None-Match里,服务器只需要比较客户端传来的If-None-Match跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。
数据缓存:LocalStorage本地存储
代码运行
代码编译
安全优化
5 为什么不能用index做key
所有的场景都不能吧遍历的索引作为唯一的key,每条数据的index多不是自身的属性,会随着数组的变化而变化。
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
)
)
}
可以考虑使用uuid
uuid() {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 32; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
var uuid = s.join("");
return uuid;
},
···