你在项目里是怎么封装ajax的
使用的axios库,用拦截器进行request和response的处理,在request拦截器加入token,header,根据request,url,method,body生成canToken放在全局,请求之前检查全局canelToken是否包含,如果已经存在,调用cancel,在response里removeCancelToken,处理401 auth, 404 not found, 500 server error, 其它的抛出去,在调用的时候自己处理,show toast or others。
原型链怎么使用的,js如何使用原型链实现继承,封装和多态
正常开发很少用到原型链,比如react里我们给Component的class加了setState。我们在使用的时候比如 LoginComponent extends React.Component就可以使用setState了。
对requestIdelCallback和requestAnimationFrame的理解
大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms。
requestAnimationFrame的运行机制:(在每次渲染前执行,每一帧都会执行)
var timer; btn.onclick = function(){ myDiv.style.width = '0'; cancelAnimationFrame(timer); timer = requestAnimationFrame(function fn(){ if(parseInt(myDiv.style.width) < 500){ myDiv.style.width = parseInt(myDiv.style.width) + 5 + 'px'; myDiv.innerHTML = parseInt(myDiv.style.width)/5 + '%'; timer = requestAnimationFrame(fn); }else{ cancelAnimationFrame(timer); } }); }
requesetIdleCallback每一帧有空余时间时会被触发,是一个属于宏任务的回调,就像setTimeout一样。不同的是,setTimeout的执行时机由我们传入的回调时间去控制,requesetIdleCallback是受屏幕的刷新率去控制。假如浏览器一直处于非常忙碌的状态,
requestIdleCallback
注册的任务有可能永远不会执行,此时可通过设置timeout
来保证执行。因为requestIdCallback发生在一帧的最后,此时页面布局已经完成,所以不建议在
requestIdleCallback
里再操作 DOM,这样会导致页面再次重绘。用法如下:React的Fiber树在执行过程中采用了此模式。
requestIdleCallback(myNonEssentialWork, { timeout: 2000 }); // 任务队列 const tasks = [ () => { // some operation console.log("第一个任务"); }, () => { // some operation console.log("第二个任务"); }, () => { // some operation console.log("第三个任务"); }, ]; function myNonEssentialWork (deadline) { // 如果帧内有富余的时间,或者超时 while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && tasks.length > 0) { const currentWorkUnit = tasks.shift()(); // do task } if (tasks.length > 0) requestIdleCallback(myNonEssentialWork); }
什么是闭包:
①要理解闭包,首先理解javascript特殊的变量作用域,变量的作用于无非就是两种:全局变量,局部变量。
②javascript语言的特殊处就是函数内部可以读取全局变量。
③我们有时候需要得到函数内的局部变量,但是在正常情况下,这是不能读取到的,这时候就需要用到闭包。在javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包是指有权访问另一个函数作用域中的变量的函数。其本质是函数的作用域链中保存着外部函数变量对象的引用。二.闭包的应用场景:
①函数作为参数被传递
②函数作为返回值被返回
③实际应用(隐藏数据):为什么说隐藏数据了呢,因为普通用户只能通过get set等api对数据进行查看和更改等操作,没法对data直接更改,达到所谓隐藏数据的效果;
封装功能时(需要使用私有的属性和方法),函数防抖、函数节流
单例模式三.闭包的优点:
(一)一个是前面提到的可以读取函数内部的变量
(二)另一个就是可以重复使用变量,并且不会造成变量污染
①全局变量可以重复使用,但是容易造成变量污染。不同的地方定义了相同的全局变量,这样就会产生混乱。”
②局部变量仅在局部作用域内有效,不可以重复使用,不会造成变量污染。
③闭包结合了全局变量和局部变量的优点。可以重复使用变量,并且不会造成变量污染四.闭包的缺点:
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。闭包对象第一次调用的时候,会在栈里面开辟空间,并返回引用指针,只有当这个指针不再被访问的时候才会被释放。而我们之后会使用这个指针去继续操作这个堆空间,所以这个空间是一直可被访问到的。而js采用可访问,可达算法进行的垃圾回收,所以此空间的数据是不会被GC回收。
你对EventSource和轮询及websocket的理解
EventSource
EventSource
(Server-sent events)简称SSE用于向服务端发送事件,它是基于http协议的单向通讯技术,以text/event-stream
格式接受事件,如果不关闭会一直处于连接状态,直到调用EventSource.close()
方法才能关闭连接;- 是服务器->客户端的,所以它不能处理客户端请求流
- 明确指定用于传输UTF-8数据的,所以对于传输二进制流是低效率的,即使你转为base64的话,反而增加带宽的负载,得不偿失。
轮询
- 是一种简单粗暴,同样也是一种效率低下的实现“实时”通讯方案,这种方案的原理就是定期向服务器发送请求,主动拉取最新的消息队列
- 比如轮询的间隔小于服务器信息跟新频率,会浪费很多HTTP请求,消耗宝贵的CPU时间和带宽。容易导致请求轰炸
- 长轮询的是浏览器发送一个请求到服务器,服务器只有在有可用的新数据时才会响应,长轮询和短轮询比起来,明显减少了很多不必要的http请求次数,相比之下节约了资源
websocket
- 双向通信,实时性高,相对前两者性能最佳
你对React Fiber的设计有什么看法?
Fiber结构是react16引入的,是一种双向链表结构,异步可中断设计。在任何给定时间,ReactJS维护两个Virtual DOM,一个具有更新的状态Virtual DOM,另一个具有先前的状态Virtual DOM。
workInProgress Tree 保存当先更新中的进度快照,用于下一个时间片的断点恢复, 跟 Fiber Tree 的构成几乎一样, 在一次更新的开始时跟 Fiber Tree 是一样的.
在首次渲染的过程中,React 通过 react-dom 中提供的方法创建组件和与组件相应的 Fiber (Tree) ,此后就不会再生成新树,运行时永远维护这一棵树,调度和更新的计算完成后 Fiber Tree 会根据 effect 去实现更新。这两棵树构成了双缓冲树, 以 fiber tree 为主,workInProgress tree 为辅。
一次更新的操作都是在 workInProgress Tree 上完成的,当更新完成后再用 workInProgress Tree 替换掉原有的 Fiber Tree
Fiber 总的来说可以分成两个部分,一个是调和过程(可中断),一个是提交过程(不可中断)
fiber 是解决性能问题的,而 hooks 是解决逻辑复用问题的
虚拟dom在Vue和React有什么不同?diff算法有什么不同,vue为什么叫渐进式?
Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
React是从左到右比较。
响应式原理:
Vue依赖收集,自动优化,数据可变。Vue递归监听data的所有属性,直接修改。当数据改变时,自动找到引用组件重新渲染。
React基于状态机,手动优化,数据不可变,需要setState驱动新的state替换老的state。当数据改变时,以组件为根目录,默认全部重新渲染, 所以 React 中会需要 shouldComponentUpdate 这个生命周期函数方法来进行控制
什么是热更新和热模块替换,如何设计?
HMR在配置成功以后,修改CSS/JS不会进行页面刷新。
HMR 需要用到 HotModuleReplacementPlugin 这个插件,这个插件是 webpack 自带的插件。
在 devServer 中配置 hot 为 true
css文件我们会配置css-loader,css-loader中已经增加了module.hot.accept的支持,所以即使不配置module.hot.accept,对于css也可以HMR,但是如果JS没有调用module.hot.accept,HMR执行找不到对应的内容,则会直接刷新页面,可以设置参数hotOnly: true来防止自动刷新。
原理:
在热更新开启后,当webpack打包时,会向client端注入一段HMR runtime代码,同时server端会启动了一个HMR服务器,然后通过websocket和注入的runtime进行通信。
在webpack检测到文件修改后,会重新构建,并通过ws向client端发送更新消息,浏览器通过jsonp拉取更新过的模块,回调触发模块热更新逻辑。
前端的router有什么区别,location hash和history?
hash路由的话,只是hash部分改变并不会刷新页面
history路由的话,是根据history.pushSate和history.replaceState这两个方法,并不会刷新路由。
路由实现:Hash模式,通过hashChange事件监听。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Hash 模式</title> </head> <body> <div> <ul> <li><a href="#/page1">page1</a></li> <li><a href="#/page2">page2</a></li> </ul> <!--渲染对应组件的地方--> <div id="route-view"></div> </div> <script type="text/javascript"> // 第一次加载的时候,不会执行 hashchange 监听事件,默认执行一次 // DOMContentLoaded 为浏览器 DOM 加载完成时触发 window.addEventListener('DOMContentLoaded', Load) window.addEventListener('hashchange', HashChange) // 展示页面组件的节点 var routeView = null function Load() { routeView = document.getElementById('route-view') HashChange() } function HashChange() { // 每次触发 hashchange 事件,通过 location.hash 拿到当前浏览器地址的 hash 值 // 根据不同的路径展示不同的内容 switch(location.hash) { case '#/page1': routeView.innerHTML = 'page1' return case '#/page2': routeView.innerHTML = 'page2' return default: routeView.innerHTML = 'page1' return } } </script> </body> </html>
用history实现前端路由:history.pushState(),和replaceState()都不会触发popstate事件。只有点击后退按钮或者手动调用history.back,forward才会触发。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>History 模式</title> </head> <body> <div> <ul> <li><a href="/page1">page1</a></li> <li><a href="/page2">page2</a></li> </ul> <div id="route-view"></div> </div> <script type="text/javascript"> window.addEventListener('DOMContentLoaded', Load) window.addEventListener('popstate', PopChange) var routeView = null function Load() { routeView = document.getElementById('route-view') // 默认执行一次 popstate 的回调函数,匹配一次页面组件 PopChange() // 获取所有带 href 属性的 a 标签节点 var aList = document.querySelectorAll('a[href]') // 遍历 a 标签节点数组,阻止默认事件,添加点击事件回调函数 aList.forEach(aNode => aNode.addEventListener('click', function(e) { e.preventDefault() //阻止a标签的默认事件 var href = aNode.getAttribute('href') // 手动修改浏览器的地址栏 history.pushState(null, '', href) // 通过 history.pushState 手动修改地址栏, // popstate 是监听不到地址栏的变化,所以此处需要手动执行回调函数 PopChange PopChange() })) } function PopChange() { console.log('location', location) switch(location.pathname) { case '/page1': routeView.innerHTML = 'page1' return case '/page2': routeView.innerHTML = 'page2' return default: routeView.innerHTML = 'page1' return } } </script> </body> </html>
浏览器渲染原理,重绘和重排/回流的区别,GC的原理?
- 每一轮 Event Loop 都会伴随着渲染吗?
requestAnimationFrame
在哪个阶段执行,在渲染前还是后?在microTask
的前还是后?requestIdleCallback
在哪个阶段执行?如何去执行?在渲染前还是后?在microTask
的前还是后?resize
、scroll
这些事件是何时去派发的。
- 事件循环不一定每轮都伴随着重渲染,但是如果有微任务,一定会伴随着微任务执行。
- 决定浏览器视图是否渲染的因素很多,浏览器是非常聪明的。
requestAnimationFrame
在重新渲染屏幕之前执行,非常适合用来做动画。requestIdleCallback
在渲染屏幕之后执行,并且是否有空执行要看浏览器的调度,如果你一定要它在某个时间内执行,请使用timeout
参数。resize
和scroll
事件其实自带节流,它只在 Event Loop 的渲染阶段去派发事件到EventTarget
上。网页的生成过程,大致可以分成五步。
- HTML代码转化成DOM Tree
- CSS代码转化成CSSOM Tree(CSS Object Model)
- 结合DOM和CSSOM,生成一棵渲染树Render Tree
- 生成布局(flow),将所有渲染树进行平面合成(!此步骤再次触发即回流)
- 将布局绘制(paint)在屏幕上(显卡,此步骤再次触发即重绘)
网页生成的时候,至少会渲染一次。用户访问的过程中,还会不断重新渲染。这五步里面,第一步到第三步都非常快,耗时的是第四步和第五步。"生成布局"(flow)和"绘制"(paint)这两步,合称为"渲染"(render),重排/回流是一个概念。重排比重绘要耗时。
以下三种情况,会导致网页重新渲染。
- 修改DOM
- 修改样式表
- 用户事件(比如鼠标悬停、页面滚动、输入框键入文字、改变窗口大小等等)
重新渲染,就需要重新生成布局和重新绘制。前者叫做"重排"(reflow),后者叫做"重绘"(repaint)。
"重绘"不一定需要"重排",比如改变某个网页元素的颜色,就只会触发"重绘",不会触发"重排",因为布局没有改变。
但是,"重排"必然导致"重绘",比如改变一个网页元素的位置,就会同时触发"重排"和"重绘",因为布局改变了。
- css加载不会阻塞DOM树的解析
- css加载会阻塞后面js语句的执行
为了防止css阻塞,引起页面白屏,可以提高页面加载速度
- 使用cdn
- 对css进行压缩
- 合理利用缓存
- 减少http请求,将多个css文件合并
垃圾回收是指:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间。
V8 采⽤的可访问性(reachability)算法来判断堆中的对象是否是活动对象。
webpack里loader和plugin是干啥用的,执行顺序是怎么样的?
loader是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。plugin是一个扩展器,它丰富了webpack本身,针对是loader结束后,webpack打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听webpack打包过程中的某些节点,执行广泛的任务 。
plugin通过tap注册compiler上的hook,使得webpack执行到指定时机执行回调函数。
什么是侵入式开发框架,你对装饰器的理解?
对fetch和axios的优缺点理解?
如何计算首屏加载时间?
首屏加载的时间主要是指页面的dom,脚本,样式都加载完毕了,这时候页面的脚本已经执行完毕页面的交互已经可以使用,样式也都完整,但是可能某些图片还在加载中。
时间点可以采用performance.timing,可以采用DOMContentLoaded +首屏中图片加载完时间(去除首屏不加载图片)
- DOMContentLoaded 事件,表示直接书写在HTML页面中的内容但不包括外部资源被加载完成的时间,其中外部资源指的是css、js、图片、flash等需要产生额外HTTP请求的内容。
- onload 事件,表示连同外部资源被加载完成的时间。
<script type="text/javascript"> window.logInfo = {}; //统计页面加载时间 window.logInfo.openTime = performance.timing.navigationStart; window.logInfo.whiteScreenTime = +new Date() - window.logInfo.openTime; document.addEventListener('DOMContentLoaded',function (event) { window.logInfo.readyTime = +new Date() - window.logInfo.openTime; }); window.onload = function () { window.logInfo.allloadTime = +new Date() - window.logInfo.openTime; var timname = { whiteScreenTime: '白屏时间', readyTime: '用户可操作时间', allloadTime: '总下载时间' }; var logStr = ''; for (var i in timname) { console.warn(timname[i] + ':' + window.logInfo[i] + 'ms'); logStr += '&' + i + '=' + window.logInfo[i] + 'ms'; } (new Image()).src = '/?action=speedlog' + logStr; }; </script>
对redux理解
redux包括三个部分: reducer, action, store
store: 唯一数据源,通过 store.subscribe可以订阅更新, 通过store.dispatch(action)可以发布更新,reducer是控制处理器。可以根据action的type,定制state的返回,reducer可以拆分。
import { createStore } from 'redux'; /** * 这是一个 reducer,形式为 (state, action) => state 的纯函数 */ function reducer(state = 0, action) { switch (action.type) { case 'INCREMENT': return state + 1; case 'DECREMENT': return state - 1; default: return state; } } const store = createStore(reducer); // 可以手动订阅更新,也可以事件绑定到视图层。 store.subscribe(() => console.log(store.getState()) ); // 改变内部 state 惟一方法是 dispatch 一个 action。 // action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行 store.dispatch({ type: 'INCREMENT' }); // 1 store.dispatch({ type: 'INCREMENT' }); // 2 store.dispatch({ type: 'DECREMENT' }); // 1
Redux 应用中数据的生命周期遵循下面 4 个步骤:
- 调用 store.dispatch(action)
- Redux store 调用传入的 reducer 函数。
- 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
- Redux store 保存了根 reducer 返回的完整 state 树。
ReduxToolkit是redux的最佳实现,它简化了redux的流程。
import { createSlice, configureStore } from '@reduxjs/toolkit' const counterSlice = createSlice({ name: 'counter', initialState: { value: 0 }, reducers: { incremented: state => { // Redux Toolkit allows us to write "mutating" logic in reducers. It // doesn't actually mutate the state because it uses the Immer library, // which detects changes to a "draft state" and produces a brand new // immutable state based off those changes state.value += 1 }, decremented: state => { state.value -= 1 } } }) export const { incremented, decremented } = counterSlice.actions const store = configureStore({ reducer: counterSlice.reducer }) // Can still subscribe to the store store.subscribe(() => console.log(store.getState())) // Still pass action objects to `dispatch`, but they're created for us store.dispatch(incremented()) // {value: 1} store.dispatch(incremented()) // {value: 2} store.dispatch(decremented()) // {value: 1}
前端工程化,组件化,模块化,微前端,前端工程化部署
前端工程化:比如咱们现在的前后端分离项目,可以单独作为一个项目运行,拥有自己的配置,项目结构,编译,打包等
组件化:比如我们经常用的重复的小功能,按钮,输入框,或者复杂的组合组件比如模态框, 表格等,我们把他重新封装成可复用的单元。这样当你想在不同页面使用,就可以减少很多不必要的重复代码。
模块化:现在的前端项目,可以认为一个文件就是一个模块,无论是js,css,img文件,都可以以模块的思维去解读他。JS模块化方案很多有AMD/CommonJS/UMD/ES6 Module等,CSS模块化开发大多是在less、sass、stylus等预处理器。
工程化构建:前端构建过程一般包括以下几个过程:
- 代码检查
- 运行单元测试等
- 语言编译
- 依赖分析、打包、替换等
- 代码压缩、spirit 图片压缩等
- 版本生成
微前端:微前端借鉴了微服务的架构理念,将一个庞大的前端应用拆分为多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用联合为一个完整的应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
目前可用的微前端方案:
基于
iframe
完全隔离的方案
- JS、CSS 都是独立的运行环境,页面上可以放多个
iframe
来组合业务- 无法保持路由状态,刷新后路由状态就丢失
- 完全的隔离导致与子应用的交互变得极其困难
iframe
中的弹窗无法突破其本身- 整个应用全量资源加载,加载太慢
基于
single-spa
路由劫持方案single-spa本身是没有实现样式隔离。js执行隔离的。
qiankun
孵化自蚂蚁金融科技基于微前端架构的云产品统一接入平台- 通过
import-html-entry
包解析HTML
获取资源路径,然后对资源进行解析、加载- 通过对执行环境的修改,它实现了
JS 沙箱
、样式隔离
等特性京东
micro-app
方案
- 借鉴了
WebComponent
的思想,通过CustomElement
结合自定义的ShadowDom
,将微前端封装成一个类webComponents
组件,从而实现微前端的组件化渲染。
style-loader 和 css-loader,sass-loader
css-loader 会对 @import 和 url() 进行处理,就像 js 解析 import/require() 一样,默认生成一个数组存放存放处理后的样式字符串,并将其导出
style-loader的作用是把
CSS
插入到DOM
中,就是处理css-loader
导出的模块数组,然后将样式通过style
标签或者其他形式插入到DOM
中。sass-loader加载sass/scss, 并且把sass/scss编译成css。而该loader依赖于node-sass。而node-sass依赖于node,所以要注意node-sass和node之间的版本支持
postcss-loader更像一个工厂,支持插件组装,可以通过编写postcss的插件,比如autoprefixer(生成兼容性css)这样的加上更多功能
使用postcss-loader:
npm install --save-dev postcss-loader postcss autoprefixer
module: { rules: [ { test: /\.less$/, use: [ 'style-loader', 'css-loader', 'postcss-loader', 'sass-loader' ] // use 从后往前读取 } ] }
{ loader: 'postcss-loader', options: { postcssOptions: { plugins: [ [ 'autoprefixer', { // 选项 }, ], ], }, },
Webpack和rollup配置对比:
Webpack:
entry: 打包入口
output: 输出
module =>rules => loader: 模块解析
plugins: 插手打包过程
const path = require('path') const config = { mode: 'development', //模式设置 target: 'web', entry: './src/index.ts', // 打包文件 output: { filename: 'index.js', // 输出文件 path: path.resolve(__dirname, 'lib'), // 输出路径 libraryTarget: 'commonjs', }, module: { rules: [ { test: /\.ts$/, loader: 'ts-loader', }, { test: /\.js$/, //用正则匹配文件,用require或者import引入的都会匹配到 use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } }, //加载器名,就是上一步安装的loader exclude: /node_modules/, //排除node_modules目录,我们不加载node模块中的js哦~ // include: /@gfe\/universal-logger/, include: [ path.resolve(__dirname,'./src'), path.resolve(__dirname,'./node_modules/@gfe') ], }, { test: /\.css$/, use: [ 'style-loader', 'css-loader', // 'postcss-loader' ], //依次使用以上loader加载css文件,postcss-loader可以暂时不加,后面再深入修改webpack配置的时候再说用处 // //也可以写成这样 loader:"style-loader!css-loader!postcss-loader" }, { test: /\.(png|jpe?j|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: 'img/[name].[ext]?[hash]', }, //图片文件大小小于limit的数值,就会被改写成base64直接填入url里面, //不然会输出到dist/img目录下,[name]原文件名,[ext]原后缀,[hash]在url上加上一点哈希值避免缓存。 }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: 'fonts/[name].[ext]?[hash]', }, //和上面一致 }, { test: /\.vue$/, loader: 'vue-loader', //这一个loader当然是vue项目必须的加载器啦,不加其他规则的话, //简单的这样引入就可以了,vue-loader会把vue单文件直接转成js。 }, ], }, plugins:[new HtmlWebpackPlugin()], resolve: { extensions: ['.tsx', '.ts', '.js'], }, } module.exports = config }
Rollup:
input:入口文件
output: 输出
plugins:解析模块,并且做优化
export default { input: 'src/index.ts', // //需要打包的文件 output: { file: 'lib/index.js', // 输出文件 format: 'esm', // immediately-invoked function expression — suitable for <script> tags }, plugins: [ resolve(), // tells Rollup how to find date-fns in node_modules commonjs(), // converts date-fns to ES modules ts(), babel({ babelHelpers: 'runtime', }), production && terser(), // minify, but only in production ], external: ['axios'], // 外部 }