一、原型、原型链
原型:原型就是一个普通对象,可以共享构造函数的属性和方法,所有实例中引用的原型都是同一个对象,使用prototype可以把方法挂在原型上,内存值只保存一份。
对象的__proto__是实例对象中的属性,可以理解为指针,指向了构造函数的原型prototype
function Person() {
}
Person.prototype.say = function () {
console.log('Hello')
}
const p1 = new Person();
const p2 = new Person();
console.log(p1.say === p2.say); // true
console.log(p1.__proto__ === Person.prototype); // true
原型链:对象都有_proto_ 属性,这个属性指向它的原型对象,原型对象也是对象,也有_proto_属性,指向原型对象的原型对象,这样一层一层形成的链式结构称为原型链,最顶层找不到则返回 null
根据上述原理实现new()方法
function newFun(Fun,...args){
//1.先创建一个空对象
let newobj = {}
//2.把空对象和构造函数通过原型链进行链接
newobj.__proto__ = Fun.prototype
//3.把构造函数的this绑定到新的空对象身上
const result =Fun.apply(newobj,args)
//4.根据构建函数返回的类型判断,如果是值类型,则返回对象,如果是引用类型,就要返回这个引用类型
return result instanceof objct ? result : newobj
}
二、this
- 全局对象中的this指向的是window
- 全局作用域或者普通函数中的this指向全局window
- this永远指向最后调用它的那个对象,在不是箭头函数的情况下
- new 关键词改变了this的指向
- apply,call,bind可以改变this指向
- 箭头函数中的this,它的指向在定义的时候就已经确定了箭头函数它没有this,看外层是否有函数,有就是外层函数的this,没有就是window
- 匿名函数中的this,永远指向了window,匿名函数的执行环境具有全局性,因此this指向window
三、闭包
概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
简单理解:闭包=内层函数+引用的外层函数变量
先看个简单的代码:
function outer(){
let a = 1
function inner(){
console.log(a)
}
inner()
}
outer()
闭包例子:统计一个函数调用的次数。
// 传统写法
let count = 0;
function fn(){
count++;
console.log(`函数被调用了${count}次`)
}
fn();// 函数被调用了1次
count = 10;
fn();// 函数被调用了11次
这样会有一个问题,count 是全局变量,外部可以修改count的值,影响统计结果
// 闭包写法
function fn(){
let count = 0;
return function(){
count++;
console.log(`函数被调用了${count}次`)
}
}
const result = fn();
result();// 函数被调用了1次
result();// 函数被调用了2次
这样外部只可以使用,无法修改,避免变量受污染;
注意:result 是一个全局变量,代码执行完毕不会立即销毁,result 使用fn函数,fn用到函数里面用到count, count 被引用就不会被回收,所以一直存在此时: 闭包可能会引起内存泄漏。
四、Event loop
执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去检查微任务(microTask)队列是否为空,如果为空的话,就执行Task(宏任务),否则就一次性执行完所有微任务。 每次单个宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环。
事件循环又叫做消息循环,是浏览器渲染主线程的工作方式,它开启一个不会结束的 for 循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列未尾即可,过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的浏览器环境,取而代之的是一种更加灵活多变的处理方式。 根据 W3C 官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。
五、react diff算法
传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),react 将算法进行一个优化,复杂度姜维 0(n),两者效率差距如下:
- tree层级
DoM 节点跨层级的操作不做优化,只会对相同层级的节点进行比较只有删除、创建操作,没有移动操作,如下图
react 发现新树中,R节点下没有了A,那么直接删除A,在D节点下创建A以及下属节点
上述场作中,只有删除和创建操作。
- component层级
如果是同一个类的组件,则会继续往下 diff 运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的
当component D 换成了 component G 后,即使两者的结构非常类似,也会将 D 删除再重新创建 G
- element层级 对于比较同一层级的节点们,每个节点在对应的层级用唯一的 key 作为标识提供了3种节点操作,分别为插入、移动、删除,如下场景:
通过 key 可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置。
| index | 节点 | oldIndex | maxIndex | 操作 |
|---|---|---|---|---|
| 0 | B | 1 | 0 | oldIndex(1)>maxindex(0),maxlndex=oldIndex,maxlndex变为1 |
| 1 | A | 0 | 1 | oldIndex(0)<maxindex(1),节点A移动至index(1)的位置 |
| 2 | D | 3 | 1 | oldIndex(3)>maxindex(1),maxindex=oldindex,maxIndex变为3 |
| 3 | C | 2 | 3 | oldIndex(2)<maxlndex(3),节点C移动至index(3)的位置 |
- index:新集合的遍历下标。
- oldIndex:当前节点在老集合中的下标
- maxlndex:在新集合访问过的节点中,其在老集合的最大下标
如果当前节点在新集合中的位置比老集合中的位置靠前的话,是不会影响后续节点操作的,这里这时候被动字节不用动 操作过程中只比较oldIndex和maxIndex,规则如下:
- 当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex
- 当oldIndex=maxIndex时,不操作
- 当oldIndex<maxIndex时,将当前节点移动到index的位置
diff 过程如下:
- 节点B:此时 maxIndex=0,oldndex=1;满足 maxIndex< oldindex,因此B节点不动,此时maxindex= Math.max(oldIndex, maxindex),就是1;
- 节点A:此时maxIndex=1,oldindex=0;不满足maxIndex< oldIndex,因此A节点进行移动操作,此时maxIndex= Math.max(oldIndex, maxindex),还是1
- 节点D:此时maxIndex=1,oldindex=3;满足maxIndex< oldindex,因此D节点不动,此时maxindex=Math.max(oldIndex, maxIndex),就是3
- 节点C:此时maxIndex=3,oldlndex=2;不满足maxlndex<oldndex,因此C节点进行移动操作,当前已经比较完了
六、浏览器缓存
强制缓存: Expires 和 Cache-Control
协商缓存: Last-Modified(资源最后修改时间)、 Etag(资源的唯一标识)
-
Cache-Control
1.no-cache: 表示协商缓存,即每次使用缓存前必须向服务端确认缓存资源是否更新;
2.no-store: 禁止浏览器以及所有中间缓存存储响应内容
3.public: 共有缓存,表示可以被代理服务器缓存,可以被多个用户共享
4.private: 私有缓存,不能被代理服务器缓存,不可以被多个用户共享
5.max-age: 以秒为单位的值,表示缓存的有效时间
6.must-revalidate: 当缓存过期时,需要去服务端校验缓存的有效性
强制缓存中,cache-control的max-age的优先级高于Expires -
Expires
客户端第一次请求时,服务端会在响应头部添加Expires字段.当浏览器再次发送请求时,客户端会先对比当前时间和Expires对应的时间。如果早于Expires 时间则直接使用,否则,需要再次相服务端发送请求 * 存在的问题:服务端和客户端的时间可能不同,导致缓存过期时间出现偏差; 客户端可以通过修改系统时间来继续使用缓存或提前使缓存时间失效 -
Last-Modified(资源最后修改时间)
1.服务端通过响应头部字段Last-Modified 和请求头字段If-Modified-Since比对双方资源的修改时间,来确定缓存是否需要更新。
2.浏览器第一次请求资源,服务端在返回资源的响应头中搅入Last-Modified字段,标识资源在服务端上最近一次的修改时间
3.当浏览器再次相服务端请求该资源时,请求头部带上之前服务端返回的Last-Modified, 这个请求头叫If-Modified-Since
4.当服务端再次收到请求,根据If-Modified-Since的值判断相关资源室友有变化,如果没有返回 304 Not Modified;否则返回更新后的资源,且更显Last-Modified响应头内容 -
Etag(资源的唯一标识) 为了解决精度问题和准度问题,http提供了另一种依赖于文件哈希值的精确判断缓存的方式,响应头部字段ETag和请求头部If-None-Match
1.当浏览器第一次请求资源时,服务端在返响应头中加入Etag字段,Etag字段的值为该资源的哈希值
2.当客户端再次相服务端请求资源时,在请求头上加上If-None-Match,值为之前响应头部字段Etag的值
3.服务端再次收到请求,将请求头If-None-Match字段得值和响应资源的哈希值进行对比,如果两个值相同,则说明资源没变化,返回304 Not Modified; 否则就正常返回资源内容,无论是否发生变化,都会将计算出的哈希值放入响应头部的ETag字段中