对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,这是可以优化的。
首先我们优化 Loader 的文件搜索范围
module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/
}
]
}
}
对于 Babel 来说,希望只作用在 JS 代码上的,然后 node_modules
中使用的代码都是编译过的,所以完全没有必要再去处理一遍。
当然这样做还不够,还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间
loader: 'babel-loader?cacheDirectory=true'
受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]
DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。DllPlugin的使用方法如下:
// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}
然后需要执行这个配置文件生成依赖文件,接下来需要使用 DllReferencePlugin
将依赖文件引入项目中
// webpack.conf.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
在 Webpack3 中,一般使用 UglifyJS
来压缩代码,但是这个是单线程运行的,为了加快效率,可以使用 webpack-parallel-uglify-plugin
来并行运行 UglifyJS
,从而提高效率。
在 Webpack4 中,不需要以上这些操作了,只需要将 mode
设置为 production
就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log
这类代码的功能。
可以通过一些小的优化点来加快打包速度
resolve.extensions
:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json']
,如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面resolve.alias
:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径module.noParse
:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助DNS 服务器中以资源记录的形式存储信息,每一个 DNS 响应报文一般包含多条资源记录。一条资源记录的具体的格式为
(Name,Value,Type,TTL)
其中 TTL 是资源记录的生存时间,它定义了资源记录能够被其他的 DNS 服务器缓存多长时间。
常用的一共有四种 Type 的值,分别是 A、NS、CNAME 和 MX ,不同 Type 的值,对应资源记录代表的意义不同:
Dispatcher
、 Store
、View
、Action
。Store
存储了视图层所有的数据,当 Store
变化后会引起 View 层的更新。如果在视图层触发一个 Action
,就会使当前的页面数据值发生变化。Action 会被 Dispatcher 进行统一的收发处理,传递给 Store 层,Store 层已经注册过相关 Action 的处理逻辑,处理对应的内部状态变化后,触发 View 层更新。Flux 的优点是单向数据流,解决了 MVC 中数据流向不清的问题
,使开发者可以快速了解应用行为。从项目结构上简化了视图层设计,明确了分工,数据与业务逻辑也统一存放管理,使在大型架构的项目中更容易管理、维护代码。其次是 Redux
,Redux 本身是一个 JavaScript 状态容器,提供可预测化状态的管理。社区通常认为 Redux 是 Flux 的一个简化设计版本,它提供的状态管理,简化了一些高级特性的实现成本,比如撤销、重做、实时编辑、时间旅行、服务端同构等。单一数据源、纯函数 Reducer、State 是只读的
。Dispatch
的时候会有一个 middleware 中间件层
,拦截分发的 Action 并添加额外的复杂行为
,还可以添加副作用。第一类方案的流行框架有 Redux-thunk、Redux-Promise、Redux-Observable、Redux-Saga
等。Reducer
层中直接处理副作用,采取该方案的有 React Loop
,React Loop
在实现中采用了 Elm 中分形的思想,使代码具备更强的组合能力。rematch 或 dva
,提供了更详细的模块架构能力,提供了拓展插件以支持更多功能。Action
触发的方式,可以在调试器中使用时间回溯,定位问题更简单快捷;最后是 Mobx
,Mobx 通过监听数据的属性变化,可以直接在数据上更改触发UI 的渲染。在使用上更接近 Vue,比起 Flux 与 Redux
的手动挡的体验,更像开自动挡的汽车。Mobx 的响应式实现原理与 Vue 相同
,以 Mobx 5
为分界点,5 以前采用 Object.defineProperty
的方案,5 及以后使用 Proxy
的方案。它的优点是样板代码少、简单粗暴、用户学习快、响应式自动更新数据
让开发者的心智负担更低。v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
这个算法分为三步:
新生代对象晋升到老生代有两个条件:
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行
1. 浅拷贝的原理和实现
自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象
方法一:object.assign
object.assign
是 ES6 中object
的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝
。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。
object.assign 的语法为:Object.assign(target, ...sources)
object.assign 的示例代码如下:
let target = {};
let source = { a: { b: 1 } };
Object.assign(target, source);
console.log(target); // { a: { b: 1 } };
但是使用 object.assign 方法有几点需要注意
Symbol
类型的属性。let obj1 = { a:{ b:1 }, sym:Symbol(1)};
Object.defineProperty(obj1, 'innumerable' ,{
value:'不可枚举属性',
enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1);
console.log('obj2',obj2);
从上面的样例代码中可以看到,利用
object.assign
也可以拷贝Symbol
类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题
,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能
方法二:扩展运算符方式
let cloneObj = { ...obj };
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj) //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj) //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果
扩展运算符 和
object.assign
有同样的缺陷,也就是实现的浅拷贝的功能差不多
,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便
方法三:concat 拷贝数组
数组的
concat
方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过concat
只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。
let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr); // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]
方法四:slice 拷贝数组
slice
方法也比较有局限性,因为它仅仅针对数组类型
。slice方法会返回一个新的数组对象
,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。
slice 的语法为:arr.slice(begin, end);
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr); //[ 1, 2, { val: 1000 } ]
从上面的代码中可以看出,这就是
浅拷贝的限制所在了——它只能拷贝一层对象
。如果存在对象的嵌套,那么浅拷贝将无能为力
。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝
手工实现一个浅拷贝
根据以上对浅拷贝的理解,如果让你自己实现一个浅拷贝,大致的思路分为两点:
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? []: {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了
2. 深拷贝的原理和实现
浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝
。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。
这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下
:
将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。
方法一:乞丐版(JSON.stringify)
JSON.stringify()
是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为JSON
的字符串,并将对象里面的内容转换成字符串,最后再用JSON.parse()
的方法将JSON
字符串生成一个新的对象
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
但是该方法也是有局限性的 :
undefined
symbol
RegExp
引用类型会变成空对象Date
引用类型会变成字符串NaN
、Infinity
以及 -Infinity
,JSON
序列化的结果会变成 null
obj[key] = obj
)。function Obj() {
this.func = function () { alert(1) };
this.obj = {a:1};
this.arr = [1,2,3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable:false,
value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);
使用
JSON.stringify
方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify
暂时还是无法满足的,那么就需要下面的几种方法了
方法二:基础版(手写递归实现)
下面是一个实现 deepClone 函数封装的例子,通过
for in
遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制
let obj1 = {
a:{
b:1
}
}
function deepClone(obj) {
let cloneObj = {}
for(let key in obj) { //遍历
if(typeof obj[key] ==='object') {
cloneObj[key] = deepClone(obj[key]) //是对象就再次调用该函数递归
} else {
cloneObj[key] = obj[key] //基本类型的话直接复制值
}
}
return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2); // {a:{b:1}}
虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify
一样,还是有一些问题没有完全解决,例如:
Symbol
类型;只是针对普通的引用类型的值做递归复制
,而对于 Array、Date、RegExp、Error、Function
这样的引用类型并不能正确地拷贝;循环引用没有解决
。这种基础版本的写法也比较简单,可以应对大部分的应用情况。但是你在面试的过程中,如果只能写出这样的一个有缺陷的深拷贝方法,有可能不会通过。
所以为了“拯救”这些缺陷,下面我带你一起看看改进的版本,以便于你可以在面试种呈现出更好的深拷贝方法,赢得面试官的青睐。
方法三:改进版(改进后递归实现)
针对上面几个待解决问题,我先通过四点相关的理论告诉你分别应该怎么做。
Symbol
类型,我们可以使用 Reflect.ownKeys
方法;Date、RegExp
类型,则直接生成一个新的实例返回;Object
的 getOwnPropertyDescriptors
方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object.create
方法创建一个新对象,并继承传入原对象的原型链;WeakMap
类型作为 Hash
表,因为 WeakMap
是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map
和 weakMap
的关键区别,这里要用 weakMap
),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap
存储的值如果你在考虑到循环引用的问题之后,还能用 WeakMap
来很好地解决,并且向面试官解释这样做的目的,那么你所展示的代码,以及你对问题思考的全面性,在面试官眼中应该算是合格的了
实现深拷贝
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)
const deepClone = function (obj, hash = new WeakMap()) {
if (obj.constructor === Date) {
return new Date(obj) // 日期对象直接返回一个新的日期对象
}
if (obj.constructor === RegExp){
return new RegExp(obj) //正则对象直接返回一个新的正则对象
}
//如果循环引用了就用 weakMap 来解决
if (hash.has(obj)) {
return hash.get(obj)
}
let allDesc = Object.getOwnPropertyDescriptors(obj)
//遍历传入参数所有键的特性
let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
// 把cloneObj原型复制到obj上
hash.set(obj, cloneObj)
for (let key of Reflect.ownKeys(obj)) {
cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
}
return cloneObj
}
// 下面是验证代码
let obj = {
num: 0,
str: '',
boolean: true,
unf: undefined,
nul: null,
obj: { name: '我是一个对象', id: 1 },
arr: [0, 1, 2],
func: function () { console.log('我是一个函数') },
date: new Date(0),
reg: new RegExp('/我是一个正则/ig'),
[Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)
我们看一下结果,cloneObj
在 obj
的基础上进行了一次深拷贝,cloneObj
里的 arr
数组进行了修改,并未影响到 obj.arr
的变化,如下图所示
compile
函数,生成 render 函数字符串 ,编译过程如下:
模板 -> AST (最消耗性能)
优化runtime的性能
new Watcher
函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象patch
方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素
nextTick
可以让我们在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
nextTick
主要使用了宏任务和微任务。根据执行环境分别尝试采用
Promise
MutationObserver
setImmediate
setTimeout
定义了一个异步方法,多次调用
nextTick
会将方法存入队列中,通过这个异步方法清空当前队列
什么是队头阻塞?
对于每一个HTTP请求而言,这些任务是会被放入一个任务队列中串行执行的,一旦队首任务请求太慢时,就会阻塞后面的请求处理,这就是HTTP队头阻塞
问题。
有什么解决办法吗
并发连接
我们知道对于一个域名而言,是允许分配多个长连接的,那么可以理解成增加了任务队列,也就是说不会导致一个任务阻塞了该任务队列的其他任务,在
RFC规范
中规定客户端最多并发2个连接,不过实际情况就是要比这个还要多,举个例子,Chrome中是6个。
域名分片
TianTian.com
,可以分出很多二级域名,比如Day1.TianTian.com
,Day2.TianTian.com
,Day3.TianTian.com
,这样子就可以有效解决队头阻塞问题。假设要实现一个类似 PPT 自动播放的效果,你很可能会想到使用 JavaScript 定时器控制页面跳转来实现。但其实有更加简洁的实现方法,比如通过 meta 标签来实现:
<meta http-equiv="Refresh" content="5; URL=page2.html">
上面的代码会在 5s 之后自动跳转到同域下的 page2.html 页面。我们要实现 PPT 自动播放的功能,只需要在每个页面的 meta 标签内设置好下一个页面的地址即可。
另一种场景,比如每隔一分钟就需要刷新页面的大屏幕监控,也可以通过 meta 标签来实现,只需去掉后面的 URL 即可:
<meta http-equiv="Refresh" content="60">
meta viewport相关
DOCTYPE html>
<head lang=”en”>
<meta charset=’utf-8′>
<meta name=”description” content=”不超过150个字符”/>
<meta name=”keywords” content=””/>
<meta name=”author” content=”name, [email protected]”/>
<meta name=”robots” content=”index,follow”/>
<meta name=”apple-mobile-web-app-title” content=”标题”>
<meta name=”apple-mobile-web-app-capable” content=”yes”/>
<meta name=”apple-mobile-web-app-status-bar-style” content=”black”/>
<meta name=”renderer” content=”webkit”>
<meta http-equiv=”Cache-Control” content=”no-siteapp” />
<meta name=”HandheldFriendly” content=”true”>
<meta name=”MobileOptimized” content=”320″>
<meta name=”screen-orientation” content=”portrait”>
<meta name=”x5-orientation” content=”portrait”>
<meta name=”full-screen” content=”yes”>
<meta name=”x5-fullscreen” content=”true”>
<meta name=”browsermode” content=”application”>
<meta name=”x5-page-mode” content=”app”>
<meta name=”msapplication-tap-highlight” content=”no”>
<meta http-equiv=”pragma” content=”no-cache”>
<meta http-equiv=”cache-control” content=”no-cache”>
<meta http-equiv=”expires” content=”0″>
参考:前端进阶面试题详细解答
每一种节点类型有自己的属性,也就是prop,每次进行diff的时候,react会先比较该节点类型,假如节点类型不一样,那么react会直接删除该节点,然后直接创建新的节点插入到其中,假如节点类型一样,那么会比较prop是否有更新,假如有prop不一样,那么react会判定该节点有更新,那么重渲染该节点,然后在对其子节点进行比较,一层一层往下,直到没有子节点
key
属性,方便比较。React
只会匹配相同 class
的 component
(这里面的class
指的是组件的名字)component
的 setState
方法的时候, React
将其标记为 - dirty
.到每一个事件循环结束, React
检查所有标记 dirty
的 component
重新绘制.shouldComponentUpdate
提高diff
的性能优化⬇️
为了降低算法复杂度,
React
的diff
会预设三个限制:
Diff
。如果一个DOM节点
在前后两次更新中跨越了层级,那么React
不会尝试复用他。div
变为p
,React会销毁div
及其子孙节点,并新建p
及其子孙节点。key prop
来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:Diff的思路
该如何设计算法呢?如果让我设计一个Diff算法
,我首先想到的方案是:
新增
,执行新增逻辑删除
,执行删除逻辑更新
,执行更新逻辑React团队
发现,在日常开发中,相较于新增
和删除
,更新
组件发生的频率更高。所以Diff
会优先判断当前节点是否属于更新
。基于以上原因,Diff算法
的整体逻辑会经历两轮遍历:
更新
的节点。更新
的节点。diff算法的作用
计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面。
传统diff算法
通过循环递归对节点进行依次对比,算法复杂度达到
O(n^3)
,n是树的节点数,这个有多可怕呢?——如果要展示1000个节点,得执行上亿次比较。。即便是CPU快能执行30亿条命令,也很难在一秒内计算出差异。
React的diff算法
将Virtual DOM树转换成actual DOM树的最少操作的过程 称为 调和 。
diff
算法是调和的具体实现。
diff策略
React用 三大策略 将O(n^3)复杂度 转化为 O(n)复杂度
策略一(tree diff):
策略二(component diff):
策略三(element diff):
对于同一层级的一组子节点,通过唯一id区分。
tree diff
那么问题来了,如果DOM节点出现了跨层级操作,diff会咋办呢?
答:diff只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。
如上图所示,以A为根节点的整棵树会被重新创建,而不是移动,因此 官方建议不要进行DOM节点跨层级操作,可以通过CSS隐藏、显示节点,而不是真正地移除、添加DOM节点
component diff
React对不同的组件间的比较,有三种策略
shouldComponentUpdate()
来判断是否需要 判断计算。dirty component
(脏组件),从而替换 整个组件的所有节点。注意:如果组件D和组件G的结构相似,但是 React判断是 不同类型的组件,则不会比较其结构,而是删除 组件D及其子节点,创建组件G及其子节点。
element diff
当节点处于同一层级时,diff提供三种节点操作:删除、插入、移动。
总结
tree diff
:只对比同一层的 dom 节点,忽略 dom 节点的跨层级移动如下图,react 只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。
这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
这就意味着,如果 dom 节点发生了跨层级移动,react 会删除旧的节点,生成新的节点,而不会复用。
component diff
:如果不是同一类型的组件,会删除旧的组件,创建新的组件element diff
:对于同一层级的一组子节点,需要通过唯一 id 进行来区分diff的不足与待优化的地方
尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响React的渲染性能
与其他框架相比,React 的 diff 算法有何不同?
diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新
具体的流程是这样的:
在回答有何不同之前,首先需要说明下什么是 diff 算法。
diff 算法是指生成更新补丁的方式
,主要应用于虚拟 DOM 树变化后,更新真实 DOM
。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁
将单一节点比对转化为了 3 种类型节点的比对
,分别是树、组件及元素
,以此提升效率。
树比对
:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。组件比对
:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。元素比对
:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。同一层级的子节点,可以通过标记 key 的方式进行列表对比。自 React 16 起,引入了 Fiber 架构
。为了使整个更新过程可随时暂停恢复
,节点与树分别采用了 FiberNode 与 FiberTree 进行重构
。fiberNode 使用了双链表的结构
,可以直接找到兄弟节点与子节点Preact
的 Diff
算法相较于 React
,整体设计思路相似,但最底层的元素采用了真实 DOM
对比操作,也没有采用 Fiber
设计。Vue 的 Diff
算法整体也与 React
相似,同样未实现 Fiber
设计React 拥有完整的 Diff 算法策略,且拥有随时中断更新的时间切片能力
,在大批量节点更新的极端情况下,拥有更友好的交互体验。diff 策略与 React 对齐
,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。**学习原理的目的就是应用。那如何根据 React diff 算法原理优化代码呢?**这个问题其实按优化方式逆向回答即可。
diff
算法的设计原则,应尽量避免跨层级节点移动。key
进行优化,尽量减少组件层级深度。因为过深的层级会加深遍历深度,带来性能问题。shouldComponentUpdate
或者 React.pureComponet
减少 diff
次数。当执行
JS
代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。
b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b')
}
想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,
JS
解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为undefined
,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用
b() // call b second
function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
var
会产生很多错误,所以在 ES6中引入了let
。let
不能在声明前使用,但是这并不是常说的let
不会提升,let
提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用
React.createElement(): 根据指定的第一个参数创建一个React元素
React.createElement(
type,
[props],
[...children]
)
//写法一:
var child1 = React.createElement('li', null, 'one');
var child2 = React.createElement('li', null, 'two');
var content = React.createElement('ul', { className: 'teststyle' }, child1, child2); // 第三个参数可以分开也可以写成一个数组
ReactDOM.render(
content,
document.getElementById('example')
);
//写法二:
var child1 = React.createElement('li', null, 'one');
var child2 = React.createElement('li', null, 'two');
var content = React.createElement('ul', { className: 'teststyle' }, [child1, child2]);
ReactDOM.render(
content,
document.getElementById('example')
);
现在的方法也不一定是安全的,因为没有办法确定得到的公钥就一定是安全的公钥。可能存在一个中间人,截取了对方发给我们的公钥,然后将他自己的公钥发送给我们,当我们使用他的公钥加密后发送的信息,就可以被他用自己的私钥解密。然后他伪装成我们以同样的方法向对方发送信息,这样我们的信息就被窃取了,然而自己还不知道。为了解决这样的问题,可以使用数字证书。
首先使用一种 Hash 算法来对公钥和其他信息进行加密,生成一个信息摘要,然后让有公信力的认证中心(简称 CA )用它的私钥对消息摘要加密,形成签名。最后将原始的信息和签名合在一起,称为数字证书。当接收方收到数字证书的时候,先根据原始信息使用同样的 Hash 算法生成一个摘要,然后使用公证处的公钥来对数字证书中的摘要进行解密,最后将解密的摘要和生成的摘要进行对比,就能发现得到的信息是否被更改了。
这个方法最要的是认证中心的可靠性,一般浏览器里会内置一些顶层的认证中心的证书,相当于我们自动信任了他们,只有这样才能保证数据的安全。
核心概念
entry
:入口。webpack是基于模块的,使用webpack首先需要指定模块解析入口(entry),webpack从入口开始根据模块间依赖关系递归解析和处理所有资源文件。output
:输出。源代码经过webpack处理之后的最终产物。loader
:模块转换器。本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。plugin
:扩展插件。基于事件流框架 Tapable
,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。module
:模块。除了js范畴内的es module、commonJs、AMD
等,css @import、url(...)
、图片、字体等在webpack中都被视为模块。解释几个 webpack 中的术语
module
:指在模块化编程中我们把应用程序分割成的独立功能的代码模块chunk
:指模块间按照引用关系组合成的代码块,一个 chunk
中可以包含多个 module
chunk group
:指通过配置入口点(entry point
)区分的块组,一个 chunk group
中可包含一到多个 chunkbundling
:webpack 打包的过程asset/bundle
:打包产物webpack 的打包思想可以简化为 3 点:
Loader
转换为 JS 模块 (module
),模块之间可以互相引用。entry point
)递归处理各模块引用关系,最后输出为一个或多个产物包 js(bundle)
文件。chunk group
),在不考虑分包的情况下,一个 chunk group
中只有一个 chunk
,该 chunk 包含递归分析后的所有模块。每一个 chunk
都有对应的一个打包后的输出文件(asset/bundle
)打包流程
Compiler
对象,加载所有配置的插件,执行对象的 run
方法开始执行编译。entry
找出所有的入口文件。loader
对模块进行翻译,再找出该模块依赖的模块,这个步骤是递归执行的,直至所有入口依赖的模块文件都经过本步骤的处理。chunk
,再把每个 chunk
转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会。简版
Compiler
对象;Compiler
对象开始编译整个项目;在以上过程中,
Webpack 会在特定的时间点广播出特定的事件
,插件在监听到相关事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
构建流程核心概念:
Tapable
:一个基于发布订阅的事件流工具类,Compiler
和 Compilation
对象都继承于 Tapable
Compiler
:compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。在编译初始化阶段被创建的全局单例,包含完整配置信息、loaders
、plugins以及各种工具方法Compilation
:代表一次 webpack 构建和生成编译资源的的过程,在watch
模式下每一次文件变更触发的重新编译都会生成新的 Compilation
对象,包含了当前编译的模块 module
, 编译生成的资源,变化的文件, 依赖的状态等AST
语法树。每个模块文件在通过Loader解析完成之后,会通过acorn
库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。最终Webpack
打包出来的bundle
文件是一个IIFE
的执行函数。
// webpack 5 打包的bundle文件内容
(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})
webpack详细工作流程
context如何运用
在跨层级通信中,主要分为一层或多层的情况
父组件向子组件通信
,子组件向父组件通信
以及平级的兄弟组件间互相通信
。Props
即可。那么场景体现在容器组件与展示组件之间,通过 Props
传递 state
,让展示组件受控。通过 React 的 ref API 获取子组件的实例
,然后是通过实例调用子组件的实例函数
。这种方式在过去常见于 Modal 框的显示与隐藏Context API
,最常见的用途是做语言包国际化用CSS实现扇形的思路和三角形基本一致,就是多了一个圆角的样式,实现一个90°的扇形:
div{
border: 100px solid transparent;
width: 0;
heigt: 0;
border-radius: 100px;
border-top-color: red;
}
一般来说,流量控制就是为了让发送方发送数据的速度不要太快,要让接收方来得及接收。TCP采用大小可变的滑动窗口进行流量控制,窗口大小的单位是字节。这里说的窗口大小其实就是每次传输的数据大小。
Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。
传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于event-driven模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。
Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的HTTP请求,而每个 Apache process 只能处理一个。
1px 问题指的是:在一些 Retina屏幕
的机型上,移动端页面的 1px 会变得很粗,呈现出不止 1px 的效果。原因很简单——CSS 中的 1px 并不能和移动设备上的 1px 划等号。它们之间的比例关系有一个专门的属性来描述:
window.devicePixelRatio = 设备的物理像素 / CSS像素。
打开 Chrome 浏览器,启动移动端调试模式,在控制台去输出这个 devicePixelRatio
的值。这里选中 iPhone6/7/8 这系列的机型,输出的结果就是2: 这就意味着设置的 1px CSS 像素,在这个设备上实际会用 2 个物理像素单元来进行渲染,所以实际看到的一定会比 1px 粗一些。 解决1px 问题的三种思路:
如果之前 1px 的样式这样写:
border:1px solid #333
可以先在 JS 中拿到 window.devicePixelRatio 的值,然后把这个值通过 JSX 或者模板语法给到 CSS 的 data 里,达到这样的效果(这里用 JSX 语法做示范):
<div id="container" data-device={{window.devicePixelRatio}}></div>
然后就可以在 CSS 中用属性选择器来命中 devicePixelRatio 为某一值的情况,比如说这里尝试命中 devicePixelRatio 为2的情况:
#container[data-device="2"] {
border:0.5px solid #333
}
直接把 1px 改成 1/devicePixelRatio 后的值,这是目前为止最简单的一种方法。这种方法的缺陷在于兼容性不行,IOS 系统需要8及以上的版本,安卓系统则直接不兼容。
这个方法的可行性会更高,兼容性也更好。唯一的缺点是代码会变多。
思路是先放大、后缩小:在目标元素的后面追加一个 ::after 伪元素,让这个元素布局为 absolute 之后、整个伸展开铺在目标元素上,然后把它的宽和高都设置为目标元素的两倍,border值设为 1px。接着借助 CSS 动画特效中的放缩能力,把整个伪元素缩小为原来的 50%。此时,伪元素的宽高刚好可以和原有的目标元素对齐,而 border 也缩小为了 1px 的二分之一,间接地实现了 0.5px 的效果。
代码如下:
#container[data-device="2"] {
position: relative;
}
#container[data-device="2"]::after{
position:absolute;
top: 0;
left: 0;
width: 200%;
height: 200%;
content:"";
transform: scale(0.5);
transform-origin: left top;
box-sizing: border-box;
border: 1px solid #333;
}
}
这个思路就是对 meta 标签里几个关键属性下手:
<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
这里针对像素比为2的页面,把整个页面缩放为了原来的1/2大小。这样,本来占用2个物理像素的 1px 样式,现在占用的就是标准的一个物理像素。根据像素比的不同,这个缩放比例可以被计算为不同的值,用 js 代码实现如下:
const scale = 1 / window.devicePixelRatio;
// 这里 metaEl 指的是 meta 标签对应的 Dom
metaEl.setAttribute('content', `width=device-width,user-scalable=no,initial-scale=${scale},maximum-scale=${scale},minimum-scale=${scale}`);
这样解决了,但这样做的副作用也很大,整个页面被缩放了。这时 1px 已经被处理成物理像素大小,这样的大小在手机上显示边框很合适。但是,一些原本不需要被缩小的内容,比如文字、图片等,也被无差别缩小掉了。