1、Iterator接口
实现了该接口的对象原型中有属性Symbol.iterator,其值是一个函数,返回一个可迭代对象。该对象中有next方法,每次执行返回对象的一条数据。Array,Map,Set,String等都实现了Iterator接口。
下面是自定义数组对象并实现该接口的简单实现
class SingleArray {
constructor() {
for (let index = 0; index < arguments.length; index++) {
this[index] = arguments[index];
}
this.length = arguments.length;
}
// 迭代器接口实现,供给for of消费
// 对象解构赋值,扩展运算符都是利用的此接口实现
// 自定义对象实现了该接口就可以进行 for of遍历,解构赋值,扩展运算符等
[Symbol.iterator] = () => {
let currentIndex = 0;
return {
next: () => {
return {
value: this[currentIndex++],
done: currentIndex > this.length
}
}
}
}
}
所有实现了该接口的对象都可以进行 for of遍历,解构赋值,扩展运算符。解构赋值,扩展运算符都只是该接口的语法糖而已。
问题: 普通对象没有实现该接口是如何进行解构赋值,扩展运算符的?
猜测:普通对象扩展运算符是对Object.assign的语法糖,解构赋值是根据key直接从对象中取值的包装?
2、generator生成器
一个普通函数加上*号就是一个generator函数,调用该函数返回一个可迭代对象,就是实现了上面Iterator接口的对象,该对象原型中有Symbol.iterator函数。
generator函数主要特点是可以在内部使用yield关键字定义多个状态,函数执行是返回一个可迭代对象,内部状态并不会执行,而是要等到调用可迭代对象next方法时才会一个一个的执行各个状态。
应用场景之一:快速让对象可迭代
let obj = {
name: 'zml',
age: 33,
gender: '男',
}
如上普通对象按照之前的方式,要变成一个可迭代对象可以这样写
obj[Symbol.iterator] = () => {
let keys = Object.keys(obj);
let index = 0;
return {
next: () => {
return {
value: obj[keys[index++]],
done: index > keys.length
}
}
}
}
也可以使用generator生成器方式如下
function* gen() {
let keys = Object.keys(obj);
for (let index = 0; index < keys.length; index++) {
let key = keys[index]
yield obj[key];
}
}
obj[Symbol.iterator] = gen;
async/await 是 Generator生成器 的语法糖,使得异步操作变得更加方便。async函数内置执行器,函数调用之后,会自动执行,输出最后结果,而Generator需要手动调用next。async/await 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里,这里的自动执行器就是自动循环调用next直到结束。
3.、移动端点透现象
由于click事件的延迟约300ms导致,利用touch事件而不是click事件,同时利用e.preventDefault()阻止系统默认的click事件处理。主要出现场景是modal弹框点击关闭时,由于click事件的延迟,会导致后面的区域也响应
由于手机需要判断点击是是单点还是多点等,点击时需要一个约300ms的时间判断,所以手机点击存在延时现象。为了避免此移动端一般采用touch事件处理。 点透问题出现场景,例如全屏modal点击关闭触发touch事件立刻隐藏,300ms后系统自己的click事件被触发,此时由于modal已经被隐藏,系统找到了modal后面的视图响应导致点透现象发生,可以在touch事件中利用e.preventDefault() 阻止
4、捕获和冒泡
冒泡是从触发事件的那个节点一直到document,自下而上的去触发事件。捕获是从document到触发事件的那个节点,自上而下的触发。 委托代理是采用冒泡原理,例如列表所有的cell都要绑定事件,则可以将事件绑定到列表上,减少绑定事件的数量。
5、Promise简单实现
const Pending = 'pending';
const Fulfilled = 'fulfilled';
const Rejucted = 'rejucted';
export default class MyPromise {
constructor(handle) {
if (typeof handle !== 'function') {
throw new Error('必须传函数')
}
this.status = Pending;
this.value = undefined;
this.reason = undefined;
this.resolveCallBacks = [];
this.rejectCallBacks = [];
// 立刻执行传入的函数,将resolve,reject两个函数传给外界
handle(this.__resolve, this.__reject);
}
then = (onResolved, onRejected) => {
if (typeof onResolved === 'function' && this.status === Fulfilled) {
onResolved(this.value);
} else if (typeof onRejected === 'function' && this.status === Rejucted) {
onRejected(this.reason);
}
if (this.status === Pending) {
if (typeof onResolved === 'function') {
this.resolveCallBacks.push(onResolved)
}
if (typeof onRejected === 'function') {
this.rejectCallBacks.push(onRejected)
}
}
}
static all = (list) => {
return new MyPromise((resolve, reject) => {
let results = [];
for (let index = 0; index < list.length; index++) {
const promise = list[index];
promise.then(data => {
results.push(data);
if (results.length === list.length) {
resolve(results);
}
}, err => {
reject(err)
})
}
})
}
static race = (list) => {
return new MyPromise((resolve, reject) => {
list.forEach(p => {
p.then(data => {
resolve(data);
}, err => {
reject(err)
})
})
})
}
__resolve = (data) => {
if (this.status === Pending) {// 状态一旦更改就不能再改变
this.status = Fulfilled;
this.value = data;
this.resolveCallBacks.forEach(fn => fn(data));
}
}
__reject = (err) => {
if (this.status === Pending) {// 状态一旦更改就不能再改变
this.status = Rejucted;
this.reason = err;
this.rejectCallBacks.forEach(fn => fn(err));
}
}
}
6、node中require的实现原理
1、根据相对路径,转换为绝对路径
2、查看缓存,有就返回,没有则创建Module并缓存
3、加载Module
3.1、json文件直接读取文件再将其转换为对象返回
3.2、js文件读取文件成字符串,再将字符串包裹成为一个函数字符串,使用vm模块将其转换为js函数代码,最后修改函数this指向调用函数,将require导出数据填充到当前module.exports中返回
let path = require('path');
let fs = require('fs');
let vm = require('vm');
class MyModule {
constructor(id) {
this.id = id;
this.exports = {}
}
}
MyModule._cache = {};
MyModule._wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
function myRequire(filePath) {
let absPath = path.join(__dirname, filePath);
if (MyModule._cache[absPath]) {
return MyModule._cache[absPath].exports;
}
let module = new MyModule(absPath);
MyModule._cache[absPath] = module;
loadModule(module)
return module.exports;
}
function loadModule(module) {
let ext = path.extname(module.id);
let strContent = fs.readFileSync(module.id);
if (ext === '.json') {
let obj = JSON.parse(strContent); // json字符串直接转为对象导出
module.exports = obj;
} else if (ext === '.js') {
// 将导出的js字符串包装成函数的字符串形式
let wrapContent = MyModule._wrapper[0] + strContent + MyModule._wrapper[1];
// 将函数字符串 转换为 js的函数
let jsContent = vm.runInThisContext(wrapContent);
// 修改函数this指向,让内部的exports导出内容填充到当前module.exports上
// 上面将js字符串包装成函数的目的是:为了将内部导出数据填充到当前module.exports中
jsContent.call(module.exports, module.exports)
}
}
7、node中全局this指向为当前文件的module.exports,不是global,而web中全局this就是指向window。原因是因为node中文件内部代码系统会自动包装到一个函数内部(这个函数就是上面MyModule._wrapper的形式),并自动调用call执行,修改了this的指向为module.exports,默认值为{}空对象。也正是因为node中代码都被包装到函数内部,而该函数的形式为
(function (exports, require, module, __filename, __dirname) { ',
'\n});
所以node可以直接使用require, module, __filename, __dirname等参数。
8、Web中的事件循环
任务分为同步任务和异步任务,同步任务先执行,再执行异步任务
异步任务又细分为微任务和宏任务,一般微任务执行较快耗时较短,主要有Promise,MutationObserver,process.nextTick,宏任务耗时较长,主要有setTimeout,setInterval,setImmediate等。这两种任务分别存放到微任务队列和宏任务队列中依次执行。
1、从上至下执行所有同步代码
2、执行过程中遇到宏任务就添加到宏任务队列,遇到微任务就添加到微任务队列
3、同步代码执行完毕后就依次执行微任务中所有满足需求的回调
4、依次执行宏任务中满足需求的回调
注意:微任务队列满足需求的回调需要全部执行完毕,而宏任务中的每一个回调执行完毕后需要立即检查微任务队列中是否有满足条件的微任务,若有责需要立刻切换到微任务队列优先执行
9、nodeJS事件循环和浏览器事件循环区别
1、任务队列个数不同。浏览器事件循环有2个事件队列(宏任务队列和微任务队列),nodeJS中有6个事件队列
2、微任务队列不同。浏览器有专用的微任务队列,nodeJS中没有专门存储微任务的队列
3、微任务执行时机不同。浏览器中同步任务执行完毕,和每执行完一个宏任务都会去清空微任务队列,nodeJS中只有同步任务执行完毕以及其他6个队列之间切换时才会清空微任务
4、微任务优先级不同。浏览器中微任务存储在队列中,先进先出,nodeJS中微任务按照优先级成最小堆这种结构排列,按照优先级先后执行
10、内存回收机制
引用计数算法,无法解决循环引用问题,只在早起的ie中有使用
标记-清除算法,垃圾回收器将定期从根开始查找,标记所有可访问到的对象,然后将不可访问的对象清除,现代浏览器全部用这个算法,缺点是垃圾回收时会中断程序运行
11、web安全
https://www.cnblogs.com/fundebug/p/details-about-6-web-security.html
12、webpack
https://zhuanlan.zhihu.com/p/239785494
13、src和href的区别
href是指向网络资源所在位置(的超链接),用来建立和当前元素或文档之间的连接,当浏览器识别到它他指向的文件时,就会并行下载资源,不会停止对当前文档的处理。 如link标签(加载css)等。所以推荐使用link而不是@import加载css。
src是指向外部资源的位置,当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执行完毕,所下载内容会嵌入到文档中当前标签所在的位置。如js脚本,img图片和frame等元素。所以js脚本放在底部而不是头部
14、script标签中async和defer的作用及区别
script不使用defer和async时会同步下载资源并立刻执行。
使用async或defer都是异步下载资源,async当资源下载完成立刻执行js代码,defer则当资源下载完成后会等待dom元素加载完成才执行js代码。
15、雪碧图
优点:减少加载多张图片的 HTTP 请求数(一张雪碧图只需要一个请求) 。 提前加载图片(同时也是缺点)
缺点: 维护困难,一改具改。 而且http2开启后,因为有多路复用,多个图片可以使用同一个tcp通道,所以雪碧图不建议使用。
16、盒模型
标准盒模型:width设置的是content的宽度,而盒子实际宽度为content+padding+border+margin,所以若存在padding,border等时盒子会比设置的大
怪异盒模型:当设置了box-sizing: border-box时标准盒模型转变为怪异盒模型,此时设置的width是content+padding+border的总宽度,盒子宽度跟设置的width一样
17、translate和绝对定位改变left等区别
改变transform或opacity不会触发浏览器重新布局(reflow)或重绘(repaint),只会触发复合(compositions)。而改变绝对定位left等会触发重新布局。transform使浏览器为元素创建一 个 GPU 图层,但改变绝对定位会使用到 CPU。 因此translate()更高效,可以缩短平滑动画的绘制时间。
而translate改变位置时,元素依然会占据其原始空间,而绝对定位是真实改变元素位置。
18、闭包
函数以及其内部可访问的变量被称为闭包 。
作用: 1 、使变量私有化,不被外界访问。 2、延长访问对象的生命周期。
19、CommonJS 和 ES6 Module
CommonJS是node规范,使用require同步导入,会堵塞,但是由于是node环境代码存储在硬盘速度很快。(会将路径下的字符串包裹成一个函数字符串再执行,原因是为了导入__dirname等变量,以及避免字符串内容在转换为js执行时污染全局作用域)
异同:
1、commonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用
commonJS模块一旦输出一个值,模块内部的变化就影响不到这个值。
ES6模块如果使用import从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,原始值变了,import加载的值也会跟着变。需要开发者自己保证,真正取值的时候能够取到值。
2、commonJS 模块是运行时加载,ES6 模块是编辑时输出接口
运行时加载:commonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上读取方法,这种加载称为“运行时加载”。commonJS脚本代码在require的时候,就会全部执行。一旦出现某个模板被“循环加载”,就只能输出已经执行的部分,还未执行的部分不会输出。
编译时加载:ES6 模块不是对象,而是通过export命令显式指定输出的代码,import时指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
参考:
https://juejin.cn/post/6844903790085144589
https://mp.weixin.qq.com/s?src=11×tamp=1610433564&ver=2823&signature=3xFVOWQC79iOizKX8wuMrFDdp-JlA6EhdBSY--L9QZUYjIqSnAfPVH-dSTY6XyTNR3FY4zjvdylan2UwvMS-UjGOjvl6kMr6s3L26EUn3DN8FlEX0lH-5v5PF9hWHB&new=1
20、this指向
普通函数中this的指向不是在编写时确定的,而是在执行时确定的
箭头函数中并没有属于自己的this,它的所谓的this是捕获其所在上下文的 this 值,这个所谓的this也不会被改变(比如call等改变this无效)。箭头函数被new调用时由于内部没有this无法绑定属性,并无实际意义。
21、参数传递方式
基本数据类型按值方式传递
对象按共享传递,传递对象的引用的副本,所以修改对象的属性时跟外界是同一个对象,而直接修改对象时只是修改引用副本
var obj = { a: 1};
function test(x) {
x.a = 10; // 同时修改了外界obj
x=20; // 仅仅修改了传递的副本,不影响外界
console.log(x); // 20
}
test(obj);
console.log(obj); // {a: 10}
22、HTTP的状态码
2xx: 成功。 206为断点续传
3xx: 重定向。 304为协商缓存表示服务端没有修改
4xx: 客户端错误。 404一般为url写错
5xx: 服务端错误
23、常用端口
21: FTP,文件传输协议
80: HTTP
443: HTTPS
1080:是Socks代理服务使用的端口
8080: 网页的代理服务端口
27017: mongodb默认端口
24、浏览器重绘与重排
元素几何/样式/增加/删除等会出发重排或重绘,代价很大。如下都会触发
添加、删除、更新DOM节点
通过display: none隐藏一个DOM节点-触发重排和重绘
通过visibility: hidden隐藏一个DOM节点-只触发重绘,因为没有几何变化
移动或者给⻚面中的DOM节点添加动画
添加一个样式表,调整样式属性 用户行为,例如调整窗口大小,改变字号,或者滚动。
如何避免或者说优化重排?
1、集中改变样式,如通过更改className方式批量更改样式
2、使用DocumentFragment批量增加删除修改多个元素节点,只触发一次重拍
3、如果可能使用opacity和transform ,这样只会触发复合不会导致重拍。如移动动画使用transform而不是使用left等
25、Babel
目前浏览器普遍支持es5,对es6的支持还不太友好,Babel使用相应插件将es6翻译为es5。如将箭头函数/class转换为function函数,将es6新增的方法自己实现添加到对应的类上面等等,如果编译过程出错很可能是语法错误或者的缺少支持转换的插件。
Babel也称为js的编译器,编译原理分为三个阶段
- 解析:将代码字符串解析成抽象语法树,就是遍历字符串,将各个分词逐个解析生成指定格式的语法树对象
- 变换:对抽象语法树进行变换操作,主要通过各个插件完成,如在Array上实现isArray方法,箭头函数转换等
- 再建:根据变换后的抽象语法树再生成代码字符串
26、react中setState是同步还是异步
结论:有时是同步有时是异步,具体的如setTimeOut中是同步,在生命周期方法,合成事件方法中是异步。
源码上面,react在上面这两种情况中(生命周期方法,合成事件方法中)设置了isBatchingUpdates为true,会先将state暂存在一个队列中,等待当前同步任务完成进入微任务执行阶段时再合并队列中state,批量更新state。 否则设置isBatchingUpdates为false,同步更新state。
27、redux作用
1、UI和数据分离
2、store采用不可变对象,数据变化状态可以回溯
28、Vue中computed和watch有什么区别
computed具有缓存性,只有当内部依赖的属性变化时才会重新计算,可以依赖多个属性,一对多。computed函数执行后会收集依赖并对依赖进行监听,若有变化则设置标志位dirty为true,页面刷新时判断该标志位,若为true则重新计算,否则使用缓存。
watch没有缓存性,一旦属性发生变化就回调,只能监听一个属性,一对一。
二者都是使用监听者模式观察对象的改变
29、网络劫持
DNS劫持: (输入京东被强制跳转到淘宝这就属于dns劫持)
HTTP劫持: (访问谷歌但是一直有贪玩蓝月的广告),由于http明文传输,运营商会修改你的http响应内容(即加广告)
30、Web Worker
允许开新的线程,但是线程和主线程之间不共享任何作用域和资源数据,只能通过事件机制传递数据。
用用:超复杂超大循环的计算。大量数据加密处理。大量数据预加载/存储。
31、求大数组之和
简单实现
function sum(nums) {
let n = 0;
nums.forEach(i => n += i);
return n;
}
但是如果nums数组有上亿个怎么办,即使将sum方法放到异步,但是执行的时候由于单线程问题也会导致堵塞。
这种大量计算可以采用Web Worker 将计数在子线程计算,也可以想办法让循环切割成小块分片执行
function sum(nums, callback) {
let length = nums.length;
let n = 0;
function caculate(i) {
setTimeout(() => {
n += nums[i++];
length === i ? callback(n) : caculate(i);
}, 0);
}
caculate(0)
}
32、Service workers
一个服务器与浏览器之间的中间人角色,如果网站中注册了service worker那么它可以拦截当前网站所有的请求,进行判断(需要编写相应的判断程序),如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器。从而大大提高浏览体验。
33、Fiber
旧的react中每次执行render都会生成一颗新的dom树,进行diff算法,再递归树的所有节点更新 UI。整个过程是同步的,不能被打断。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。
Fiber中将dom树修改变成可链表访问的结构,遍历时改递归为循环,将diff/更新任务切片(每个fiber树节点为切片)分批次调用。使用浏览器的requestIdleCallback方法在空闲时执行切片,每个切片完成后将控制权返回给浏览器进行可能的其他操作,防止堵塞。空闲时再执行后续的切片。
更新时fiber分为两个阶段
第一阶段Reconciliation Phase,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的;(主要是diff阶段)
第二阶段Commit Phase,那就一鼓作气把DOM更新完,绝不会被打断。
因为可以被打断重来,react的一些生命周期方法(componentWillReceiveProps, shouldComponentUpdate,render, componentWillMount, componentWillUpdate)可能会被多次执行,要注意检查看这些方法重复调用是否会有问题。
正是因为这样,所以react将一些生命周期方法标记为不安全的,而使用新的静态生命周期方法替代(why? 静态方法无法访问组件属性一般是纯函数比较安全?)
这里的中断是指一个切片执行过程中由于浏览器分配的时间分片不足被迫中断了,尚未执行完毕,这样下一次空闲时就只能再执行此切片一次。同一个切片执行完毕后会有标记,并不会再次执行。
var tasksNum = 100
window.requestIdleCallback(unImportWork)
function unImportWork(deadline) {
while (deadline.timeRemaining() && tasksNum > 0) {
// 这里模拟一个切片任务, 任务完成指向下一个切片
tasksNum--;
}
if (tasksNum > 0) {
// 注意:这个函数异步,在浏览器空闲时才会执行回调
requestIdleCallback(unImportWork)
}
}
requestAnimationFrame,浏览器下一帧刷新前回调,相比setTimeout不会导致丢帧,不会卡顿。
requestIdleCallback在浏览器每一帧渲染完成之后若还有空闲时间时回调(即这一帧渲染耗时不足16ms),如果每一帧都耗时过长那么该回调会迟迟无法执行。因为是在一帧渲染完成之后才回调,所以该回调内部不能做DOM相关操作,否则会引发再次渲染。也不能做耗时操作,否则会导致当前帧耗时太长导致丢帧。
34、javascript 中常见的内存泄露陷阱
全局对象
定时器未清除
监听器未移除
dom节点移除后还有js对象引用着该dom节点
闭包导致的生命周期延长
35、PWA应用是什么
无需下载安装即可使用的应用,用完即走。相比小程序,不需要安装如微信这种母体。主要数据使用service worker 缓存,只需要浏览器支持即可。
36、白屏时间怎么优化
输入URL,DNS解析,TCP连接下载html,从上到下解析html,遇到link加载的css异步下载。同时构建DOM树和CSSOM树,待二者全部完成后才构建渲染树。期间遇到script标签会导致堵塞。
浏览器工作流程:构建DOM -> 构建CSS规则树 -> 构建渲染树 -> 布局 -> 绘制。
CSS规则树不会堵塞DOM树的解析,但是会阻塞渲染(不堵塞解析意味着此时js能获取到dom元素,堵塞渲染意味着页面还没有显示dom元素),只有当CSS规则树构建完毕后才会进入下一个阶段构建渲染树。
通常情况下DOM和CSS规则树是并行构建的。
html/css压缩/合并,删除未使用的样式。js至于文件尾部防止堵塞。html结构精简。服务端处理优化(主要是缓存)。
https://blog.csdn.net/qq_22930381/article/details/101448112
https://www.cnblogs.com/chenjg/p/7126822.html?utm_source=tuicool&utm_medium=referral
https://blog.csdn.net/qianyu6200430/article/details/109589895
37、node中express中间件原理
express是对原生http的封装。使用app.use/app.get等时都是将回调放到一个数组中,然后等客户端请求时依次使用递归遍历数组,每次循环执行next方法,next方法就是取出数组中的一个元素根据path判断是否匹配(中间件没有path参数时全匹配),不匹配则继续递归调用下一个next, 匹配则将执行回调并将next函数传递给外界,同时递归结束了。 外界会调用自己的实现,比如日志/相应请求/添加方法等,最后调用next方法让递归继续,再次取出下一个元素循环匹配。
38、PWA应用是什么
无需下载安装即可使用的应用,用完即走。相比小程序,不需要安装如微信这种母体。主要数据使用service worker 缓存,只需要浏览器支持即可。
39、白屏时间,有什么办法可以减少吗?都是什么原理
输入URL,DNS解析,TCP连接下载html,从上到下解析html,遇到link加载的css异步下载。同时构建DOM树和CSSOM树,待二者全部完成后才构建渲染树。期间遇到script标签会导致堵塞。
浏览器工作流程:构建DOM -> 构建CSS规则树 -> 构建渲染树 -> 布局 -> 绘制。
CSS规则树不会堵塞DOM树的解析,但是会阻塞渲染(不堵塞解析意味着此时js能获取到dom元素,堵塞渲染意味着页面还没有显示dom元素),只有当CSS规则树构建完毕后才会进入下一个阶段构建渲染树。通常情况下DOM和CSS规则树是并行构建的。
白屏时间优化:
html/css压缩/合并,删除未使用的样式。js至于文件尾部防止堵塞。html结构精简。按照页面打包文件,让首屏加载的文件全部是首屏显示所必须的。长页面延迟加载非可见区域的内容等。
https://blog.csdn.net/qq_22930381/article/details/101448112
https://www.cnblogs.com/chenjg/p/7126822.html?utm_source=tuicool&utm_medium=referral
https://blog.csdn.net/qianyu6200430/article/details/109589895
40、小程序性能为什么比网页好很多
js/css等代码本地化
双线程设计,渲染视图和js逻辑层独立(避免互斥现象,但是会有线程通信问题。还有一个原因是为了避免js层访问dom元素出现开发人员任意操控页面导致开发出来的小程序无法监管,也存在安全问题,所以采用双线程,因为js线程跟视图层不在一块,js无法再访问dom元素。js层能执行的所有方法均是微信提供的,容易监管)
每一个页面是一个webView(这样可以提供更好的交互体验,更贴近原生体验,也避免了单个WebView的任务过于繁重,但是页面过多会有内存等问题)
使用混合渲染,简单组件web渲染,复杂组件使用原生提供的大量接口(如相册,视频),组件(如地图,播放器),提升性能
https://www.cnblogs.com/wonyun/p/11168698.html
41、React组件拆分优化性能
每一次渲染时经过setState-> shouldComponentUpdate->render->diff->渲染差异。
举个例子,一个页面有一个输入框每次输入一个数字时会更改state,同时有一个长ul列表。当输入数字时页面刷新,同时列表也会被render并diff,此时可以将ul长列表单独封装成一个组件,内部调用shouldComponentUpdate判断是否需要render,此时当输入框输入数字时,列表不会再执行render->diff,性能提升。
组件拆分更易于控制内部状态防止不必要的render,虽然render耗时很少,但是当项目大了后diff耗时还是不容忽略的。拆分的关键是找出变化的以及相对静止的部分进行相互拆分。
42、直接操作DOM元素为什么慢
DOM元素是渲染引擎的东西,而js是js引擎的东西,当通过js操作DOM时会有线程间的通信问题。并且js操作DOM很可能会使页面重绘导致缓慢。
43、页面插入几万个DOM元素如何避免卡顿
使用文档碎片,将元素先添加到内存中再一次性渲染到页面上
考虑到页面无法显示这么多DOM元素,使用分批插入,只插入显示范围内的元素
通过requestAnimationFrame,在页面刷新间隙插入元素
44、webpack优化
时间: 精确loader匹配查找范围,利用插件让打包过程由单线程改为多线程并发打包,利用插件多线程压缩
体积: 合适的压缩算法。 按需打包(不同路由页面打包到不同的文件中)
45、TCP和UDP
UDP: 高效,无连接,不可靠,请求头只有八个字节,支持一对一,一对多,多对多这种,应用于直播,游戏这种对实时性要求高的场景,特点是如直播或视频时网络不好丢包则显示掉帧卡顿。
TCP: 需要连接,可靠传输,请求头24个字节,包含序列号,检验和,滑动窗口大小等多个字段, 只支持一对一,丢包会重传,游戏这种不适合用TCP,因为每个用户一个tcp连接服务器压力很大。电影,网络接口这种对实时性要求不高,数据完整性要求高的场合使用TCP
46、Http1.0、1.1、 2.0区别
1.1相比1.0
更多的缓存策略,cache-control协商缓存等
请求头增加range字段,支持断点续传
增加keep-alive,默认开启,即tcp连接默认不关闭,可以被多个请求复用。同一个tcp连接允许多个请求同时发送,但是响应必须按照顺序一个一个执行,也就是前面的响应会堵塞后面的响应,当连接空闲一段时间后服务端关闭,或者客户端自己关闭。目前应用最广的是1.1版本
http2.0相比之前
多数浏览器中,要升级到http2.0必须先升级到https
1.x中header是文本,body可以是文本或者二进制,http2.0中全部是二进制
多路复用,一条tcp连接可以允许多个请求同时发送,同时响应,各个响应体有一个标记id区分是哪一个请求的响应。
Header中字段压缩,两端维护索引表,后续传输只要传输header中的键名即可
服务端可以主动push
http2.0缺陷
多路复用中若有一个请求出现丢包现象,则这个连接中所有的请求都需要重传
47、React实现原理
const title = <h1 className="title">Hello, world!</h1>;
如上jsx代码首先通过babel转译为
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
每一个html标签都会转译为React.createElement方法调用,所以js文件中只要有jsx代码就必须倒入React头文件,否则无法转译babel会报错。React.createElement的作用就是返回一个虚拟DOM对象。
接下来就是将虚拟DOM渲染成原生DOM。
React16采用Fiber技术,将diff阶段拆分成多个任务单独执行,直到所有的diff全部完成在统一提交一次渲染到页面。diff阶段可拆分,但是提交阶段必须一次完成。
function workLoop(deadline) {
// 执行子任务, 每一个任务就是对一个fiber进行diff
while (nextUnitOfWork && deadline.timeRemaining() > 1) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
// 没有子任务了,说明所有的fiber都进行完了diff操作
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
Fiber树中每一个节点都是一个fiber对象,利用函数requestIdleCallback在浏览器每一帧刷新完毕后的空闲时间执行一次diff,每次diff只比较一个fiber对象,根据当前fiber中虚拟dom的type和key同上一次页面上的虚拟dom的type和key比较, 判断是更新/删除/添加,将信息记录在当前fiber上完成此次diff,同时返回下一个需要diff的fiber对象。
function commitWorker(fiber) {
if (!fiber) {return;}
// 向上查找父节点
let parentNodeFiber = fiber.parent;
while (!parentNodeFiber.node) {
parentNodeFiber = parentNodeFiber.parent;
}
const parentNode = parentNodeFiber.node;
// 更新 删除 新增
if (fiber.effectTag === PLACEMENT && fiber.node !== null) {
parentNode.appendChild(fiber.node);
} else if (fiber.effectTag === UPDATE && fiber.node !== null) {
updateNode(fiber.node, fiber.base.props, fiber.props);
} else if (fiber.effectTag === DELETIONS && fiber.node !== null) {
commitDeletions(fiber, parentNode);
}
commitWorker(fiber.child);
commitWorker(fiber.sibling);
}
提交阶段一次性执行完成,遍历整个fiber树,对每一个fiber节点,若上面记录了增加/删除/更新的状态则进行响应的操作,否则不做处理。
增加:将原生节点添加到父节点即可
删除: 删除即可
更新:将原生节点上所有属性清空,然后再赋新的属性值。
Fiber重构的优点是将diff阶段拆分成一个个小的任务,而不是像以前那样,每次更新页面需要递归整个虚拟DOM树一次性进行diff再渲染,造成任务耗时过长容易卡顿。