零。简介
自我介绍
- 负责过什么项目,以及在每个项目中的职责?
- 做过的所有项目,遇到过什么有难度的问题?如何解决??
壹。JavaScript
基础知识
深入理解JavaScript【原型和原型链】
prototype
JS设计之初为了实现简单继承,引入了prototype属性,也叫原型对象(显式原型)。
__proto__属性
所有的对象obj(null和undefined除外)都具有__proto__属性(隐式原型),__proto__属性在本质上为一个指针,指向函数对象的prototype属性。
构造器constructor
顾名思义,构造器constructor就是用来构造函数对象的,constructor 属性返回对创建此对象的函数对象的引用。通俗了讲就是指向当前对象的爸爸
原型链
如何避免【内存泄漏】?
- 别写死循环
- postmessage???
- 尽可能少的创建全局变量
- 记得手动清除定时器
- 记得少用闭包
- 记得清除DOM引用
- 多使用弱引用,比如WeakMap和WeakSet(
弱引用是指垃圾回收的过程中不会将键名对该对象的引用考虑进去,只要所引用的对象没有其他的引用了,垃圾回收机制就会释放该对象所占用的内存。)
【改变原数组】的方法
- push、pop、unshift、shift、splice、sort、reverse(fillcopyWithin)
【EventLoop事件循环】是什么,有哪些微任务
- 执行宏任务、宏任务执行结束、有微任务就执行微任务、执行完
所有微任务、浏览器渲染;此时执行下一个宏任务 - 微任务:
Promise.then catch finally、process.nextTick、MutationObserver - 宏任务:
setTimeout、setInterval、setImmediate、I/O、requestAnimationFrame
事件的【捕获、冒泡、委托】
- 捕获:
addEventListener的第3个参数设置为true即可 - 冒泡:通过
e.stopPropagation();阻止监听事件的冒泡,利用e.preventDefault();阻止默认事件的冒泡 - 委托:监听
document的点击事件根据e.target进行事件分发
document.getElementById('cont5').addEventListener('click', function (e) {
// 阻止额外监听事件的冒泡,但标签的默认事件依旧会执行,比如 a 标签的 href 属性
e.stopPropagation();
// 阻止默认事件的冒泡,标签的默认事件不会执行,但额外监听的事件依旧会执行
e.preventDefault();
// 可以同时阻止默认事件和额外监听的事件的冒泡,比较暴力「貌似不好使」
return false;
// e.path 为点击时触发的路径,如下:
// 0: span#cont5
// 1: a#cont4
// 2: div#cont2
// 3: div#cont1
// 4: body
// 5: html
// 6: document
// 7: Window {}
}, false);
crypto-js中【AES加密】的原理
u8 data[16]={ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
u8 IV[16]={ 0x30, 0x31, 0x32, 0x33,
0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x61, 0x62,
0xd63, 0x64, 0x65, 0x66};
// 其实只要把
for(i=0;i<16;i++) {
data[i]=data[i]^IV[i];
}
// 然后再把data作为新的数据加密就OK了。
// 当然你也可以不用他。
// 那么如果数据不是16的倍数改怎么办呢。这里我们就用到的数据扩张
- AES密钥:16个字节长度的字符串(1个数据是8位,这样就是128位),也就是AES-128加密
- 初始向量:使用除ECB以外的其他加密模式均需要传入一个初始向量,其大小与块大小相等,AES块大小是128bit,所以Iv的长度是16字节,初始向量可以加强算法强度
- 加密模式:CBC、ECB、CTR、OFB、CFB
- 填充方式:决定了最后的一个块需要填充的内容,填充方式有PKCS5Padding、PKCS7Padding、NOPADDING(PKCS7就是数据少几个就填充几个)
/**
* 解密
* @param word 待解密的密文
* @param keyStr 16位字符串的key
* @returns 解密后的json字符串,需要自己parse
*/
export function CryptoJSDecrypt(word: string, keyStr: string = '') {
keyStr = keyStr ? keyStr : 'JR9mmlvYXR8M9Nsi';
const key = CryptoJS.enc.Utf8.parse(keyStr);
// encrypt
const decrypt = CryptoJS.AES.decrypt(word, key, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return CryptoJS.enc.Utf8.stringify(decrypt).toString();
}
js中【异步】的发展历程
CallBack写法:单纯的嵌套代码,如若再加上业务代码,代码可读性极差Promise:为了解决“回调地狱”问题,提出了Promise对象,并且后来加入了ES6标准,Promise对象简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。Generator函数:Genrator 函数要用* 来比标识,yield关键字表示暂停。将函数分割出好多个部分,调用一次next就会继续向下执行。返回结果是一个迭代器,迭代器有一个next方法。async-await函数:为了解决promise.then的链式调用,ES2017 标准引入了 async 函数,它就是 Generator 函数的语法糖,使得异步操作变得更加方便。async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。- 异步编程经历了从
callback回调到Promise(ES6)到async/await(ES7)的伟大探索。异步编程的本质就是用尽可能接近同步的语法去处理异步机制。
function* myGenerator() {
yield '1'
yield '2'
return '3'
}
const gen = myGenerator(); // 获取迭代器
gen.next() // { value: "1", done: false }
gen.next() // { value: "2", done: false }
gen.next() // { value: "3", done: true }
gen.next() // { value: undefined, done: true }
代码输出
name触发了window.name,会转换为字符串。status同理
var name = [1,2,3];
console.log(name); // '1,2,3'
var name = { a:1, b:2, c:3 }
console.log(name); // [object Object]
贰。Vue 2.x
VUE源码的收获
- Vue源码阅读总结大会-序
- 封装了很多常用的函数!懂得很多项目细节的处理方法。比如isObject、isUndef、isDef、判断类型等
- 真的用了很多设计模式和大量的闭包
- 使用很多标志位
- Vue源码阅读总结大会-终
- 更加熟悉这个框架,可以解决遇到的奇怪问题而不会干着急(比如之前碰到的 computed 缓存)
- 学到很多新知识点(MessageChannel,shadow dom)
- 可以让你重新认识旧知识(宏微任务)
- 怎么写出规范又好维护的代码
- 可以看懂别人打包后的代码(笑哭)
- 项目有些问题的处理思路,比如在 Diff 中有个函数,学会了怎么在两个数组中,匹配是否有相同项
lodash源码
element-ui源码
2.1 Vue
Vue组件的生命周期
beforeCreate:在实例初始化之后,数据观测(data observer) 之前被调用。created:实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$elbeforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用。mounted:el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
Vue组件渲染和更新的过程
- 渲染组件时,会通过Vue.extend方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()进行挂载。
- 更新组件时,会进行patchVnode流程。核心就是diff算法
Diff更新的过程
- 响应式数据发生更新
setter拦截更新操作dep通知watcher执行update方法- 执行
updateComponent方法更新组件 - 执行
render函数生成新的vnode - 将
vnode传递给vm._update方法 - 调用
patch方法 - 执行
patchVnode进行Dom的diff更新操作- 新老节点都存在、新节点不是文本节点、新老节点都有孩子节点,才执行
updateChildren方法 - 双端比较,新老节点的开始和结束节点
- 几种假设都没命中,只能暴力遍历找到相同元素
- 新老节点都存在、新节点不是文本节点、新老节点都有孩子节点,才执行
双向绑定的原理
- Vue 采用
数据劫持 结合 发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty(点我查看该属性)来劫持数据的setter,getter,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理。 - Observer:数据的观察者,让数据对象的读写操作都处于自己的监管之下。当初始化实例的时候,会递归遍历data,用Object.defineProperty来拦截数据(包含数组里的每个数据)。
- Dep:数据更新的发布者,get数据的时候,收集订阅者,触发Watcher的依赖收集;set数据时发布更新,通知Watcher 。
- Watcher:数据更新的订阅者,订阅的数据改变时执行相应的回调函数(更新视图或表达式的值)。
- 图中红色的箭头表示的是收集依赖时获取数据的流程。Watcher会收集依赖的时候(这个时机可能是实例创建时, 解析模板、初始化watch、初始化computed,也可能是数据改变后,Watcher执行回调函数前),会获取数据的值,此时Observer会拦截数据 (即调用get函数),然后通知Dep可以收集订阅者啦。Dep将订阅数据的Watcher保存下来,便于后面通知更新。
- 图中绿色的箭头表示的是数据改变时,发布更新的流程。当数据改变时,即设置数据时,此时Observer会拦截数据(即调用set函数),然后通知Dep,数据改变了,此时Dep通知Watcher,可以更新视图啦。
你了解哪些Vue性能优化方法?
- 路由懒加载
- keep-alive缓存页面
- 使用v-show复用DOM
- v-for 遍历避免同时使用 v-if
- 长列表性能优化,静态列表:
list = Object.freeze([]) - 虚拟滚动:vxe-table
- 事件的销毁,Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。
beforeDestroy() { clearInterval(this.timer) } - 图片懒加载,对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域 内的图片先不做加载,等到滚动到可视区域后再去加载。参考项目:
vue-lazyload,代码:<img v-lazy="/static/img/1.png"> - 第三方插件按需引入,
import { Button, Select } from 'element-ui'; - 无状态的组件标记为函数式组件,
<template functional> ... </template> - 子组件分割,独立可复用功能可抽象出来
- 变量本地化,如果有for循环等频繁访问
this.xxx的情况,提前赋值给本地变量 - SSR
简单说一说vue生命周期的理解?
- beforeCreate:在实例初始化之后,数据观测(data observe)和event/watcher事件配置之前被调用,这时无法访问data及props等数据;
- created:在实例创建完成后被立即调用,此时实例已完成数据观测(data observer),属性和方法 的运算,watch/event事件回调,挂载阶段还没开始, $el 尚不可用。
- beforemount:在挂载开始之前被调用,相关的render函数首次被调用。
- mounted:实例被挂载后调用,这时el被新创建的vm. el也在文档内。注意mounted不会保证所有子组件一起挂载。
- beforeupdata:数据更新时调用,发生在虚拟dom打补丁前,这时适合在更新前访问现有dom,如手动移除已添加的事件监听器。
- updated:在数据变更导致的虚拟dom重新渲染和打补丁后,调用该钩子。当这个钩子被调用时,组件dom已更新,可执行依赖于dom的操作。多数情况下应在此期间更改状态。 如需改变,最好使用watcher或计算属性取代。注意updated不会保证所有的子组件都能一起被重绘。
- beforedestory:在实例销毁之前调用。在这时,实例仍可用。
- destroyed:实例销毁后调用,这时vue实例的所有指令都被解绑,所有事件监听器被移除,所有子实 例也被销毁。
- activated:(新增钩子)keep-alive 组件激活时调用。 类似 created 没有真正创建,只是激活
- deactivated:(新增钩子)keep-alive 组件停用时调用。类似 destroyed 没有真正移除,只是禁用
vue中组件之间的通信方式?
props ★★- 父组件 A 通过 props 向子组件 B 传递值, B 组件传递 A 组件通过 $emit A 组件通过 v-on/@ 触发
- 子组件通过 events 给父组件发送消息,实际上就是子组件把自己的数据发送到父组件。
$emit/$on 事件总线 ★★vuex ★★★- 结合
localStorage保存登录信息及权限列表等
- 结合
$parent/$children$attrs/$listeners- 多级组件嵌套需要传递数据时,通常使用的方法是通过vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此Vue2.4 版本提供了另一种方法。
$attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个 组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 interitAttrs 选项一起使用。$listeners: 包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v- on="$listeners" 传入内部组件
provide/inject ★★★- 优点:使用简单 缺点:不是响应式
- 父级:
provide: { name: '王者峡谷' //这种绑定是不可响应的 }(name: this会有响应式,把当前组件实例传递下去,但子组件会绑定一些多余的属性,比如props、methonds等) - 子级:
inject: ['name'] }
有几种定义全局方法的形式
- Vue.$set
- bus
- vuex
- window
- Vue.use?
Vue.use插件机制的原理
- Vue.use是干什么的?原理是什么?
- 检查插件是否安装,如果安装了就不再安装
- 如果没有没有安装,那么调用插件的install方法,并传入Vue实例(vue会对plugin中的两种情况处理,要么plugin中有install函数,要么plugin本身就是一个函数。)
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 获取已经安装的插件
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// 看看插件是否已经安装,如果安装了直接返回
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// toArray(arguments, 1)实现的功能就是,获取Vue.use(plugin,xx,xx)中的其他参数。
// 比如 Vue.use(plugin,{size:'mini', theme:'black'}),就会回去到plugin意外的参数
const args = toArray(arguments, 1)
// 在参数中第一位插入Vue,从而保证第一个参数是Vue实例
args.unshift(this)
// 插件要么是一个函数,要么是一个对象(对象包含install方法)
if (typeof plugin.install === 'function') {
// 调用插件的install方法,并传入Vue实例
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
// 在已经安装的插件数组中,放进去,缓存插件
installedPlugins.push(plugin)
return this
}
}
Style 标签上 Scope 的实现原理
2.2 VueX
Mutations和Actions的区别及原理
- 其运行流程是: 用户根据View内容进行操作,通过Dispatch触发Action,Action提交Mutation更新state,State更新后触发View更新,这一流程遵循单向数据流的原则。
- 几个概念:
- state:存储应用状态数据的对象,与vue组件中data类似,state的值可以是对象,也可以是返回对象的函数。通过store.state访问状态数据
- getters:从state中派生的状态数据,接收state作为第一个参数,第二个为可选参数,类似组件中的 computed,派生数据,在数据出门后进行的加工
- mutations:提交mutation来修改store中的状态(同步操作),每个mutation都有一个字符串事件类型(type)与一个回调函数(handler),在回调函数中修改状态
- 注意:不能直接去调用mutation的回调函数,需要当mutation类型为increment时,才能调用此函数;mutation必须是同步的;在store中初始化时设置好所有的属性
- actions:与mutations类似,提交修改state的行为,处理异步任务
- 注意:提交的是mutation,不是直接修改状态可以包含任意异步操作
- modules:将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
2.3 Vue-Router
hash和history的区别及原理
① hash模式
vue-router默认的就是hash模式:使用URL的hash来模拟一个完整的URL,当#后面的hash发生变化,不会导致浏览器向服务器发出请求,浏览器不发出请求就不会刷新页面,每次 hash 值的变化,会触发hashchange 这个事件,通过这个事件我们就可以知道 hash 值发生了哪些变化。然后我们便可以监听hashchange来实现更新页面部分内容的操作。
hash模式背后的原理是onhashchange事件,可以在window对象上监听这个事件:
window.onhashchange = function(event) {
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
document.body.style.color = hash;
}
对于hash模式会创建hashHistory对象,在访问不同的路由的时候,会发生两件事: HashHistory.push()将新的路由添加到浏览器访问的历史的栈顶,和HasHistory.replace()替换到当前栈顶的路由
② history模式
主要使用HTML5的pushState()和replaceState()这两个api来实现的,pushState()可以改变url地址且不会发送请求,replaceState()可以读取历史记录栈,还可以对浏览器记录进行修改
window.history.pushState(stateObject, title, URL)
window.history.replaceState(stateObject, title, URL)
切换历史状态
包括back,forward,go三个方法,对应浏览器的前进forward,后退back,跳转go操作:
history.go(-2); //后退两次
history.go(2); //前进两次
history.back(); //后退
hsitory.forward(); //前进
③ 两者区别
- hash 模式较丑,history 模式较优雅
- pushState 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,故只可设置与当前同文档的 URL
- pushState 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发记录添加到栈中
- pushState 通过 stateObject 可以添加任意类型的数据到记录中;而 hash 只可添加短字符串
- pushState 可额外设置 title 属性供后续使用
- hash 兼容IE8以上,history 兼容 IE10 以上
- history模式会将URL修改得就和正常请求后端的URL一样,如后端没有配置对应/user/id的路由处理,则会返回404错误。当用户刷新页面之类的操作时,浏览器会给服务器发送请求,所以这个实现需要服务器的支持,需要把所有路由都重定向到根页面。
- history 模式需要后端配合将所有访问都指向 index.html,否则用户刷新页面,会导致 404 错误
- --分割线--
- 随着history api的到来,前端路由开始进化了,前面的hashchange,你只能改变#后面的url片段,而history api则给了前端完全的自由
- 通过history api,我们丢掉了丑陋的#,但是它也有个问题:不怕前进,不怕后退,就怕刷新,f5,(如果后端没有准备的话),因为刷新是实实在在地去请求服务器的,不玩虚的。 在hash模式下,前端路由修改的是#中的信息,而浏览器请求时是不带它玩的,所以没有问题.但是在history下,你可以自由的修改path,当刷新时,如果服务器中没有相应的响应或者资源,会分分钟刷出一个404来。
弎。Vue 3.x
Vue3有哪些改进?
使用monorepo管理源码- 使用 TypeScript 开发源码
源码体积优化- Vue3编译中的优化
- 一个template里可以写多个根元素
- 响应式数据原理改成proxy数据劫持
- diff算法优化,做了静态提升。编译模版时进行静态分析,标记动态节点,diff对比差异时仅对比动态节点(性能提升明显);
- 语法上:支持 Composition API
Vue3相比Vue2的diff算法有何优化
- Vue2利用
isSameNode及key值判断是否是同一节点;但Vue3利用「位运算」来判断VNode的类型进行对比,大大提升了diff效率; - Vue2虽然已有「同层Diff」和「双端比较」但其diff是「全量的对比」,dom层次深的话会不断的递归调用
patchVnode函数;Vue3是「利用PatchFlag的概念做静态提升」,不需要更新的静态节点只会被创建一次; - Vue3拥有「cacheHandlers(事件侦听器缓存)」
肆。Webpack
webpack有哪些核心模块?
- Concepts概念 - webpack官网
- Entry
string | [string] | { <entryChunkName> string }- 指示哪个模块的WebPack应该使用开始建立了其内部的依赖关系图。webpack 将确定入口点依赖于哪些其他模块和库(直接或间接)。
- Ex1:
['./src/file_1.js', './src/file_2.js'] - Ex2:
{ a2: 'dependingfile.js', b2: { dependOn: 'a2', import: './src/app.js' } }- dependOn:当前入口点依赖的入口点。必须在加载此入口点之前加载它们。
- filename: 指定磁盘上每个输出文件的名称。
- import: 启动时加载的模块。
- library:指定库选项以捆绑当前条目中的库。
- runtime:运行时块的名称。设置后,将创建一个新的运行时块。false从 webpack 5.43.0开始,它可以设置为避免新的运行时块。
- publicPath:在浏览器中引用此条目的输出文件时,为其指定公共 URL 地址。另见output.publicPath。
- Output
- 告诉webpack在哪里发布它创建的包以及如何命名这些文件。
- Ex1:
{ filename: '[name].[contenthash].bundle.js', path: path.resolve(__dirname, 'dist'), library: { name: 'MyLibrary', type: 'var' } } output.library.type为配置库的公开方式,默认包含的类型有:'var', 'module', 'assign', 'assign-properties', 'this', 'window', 'self', 'global', 'commonjs', 'commonjs2', 'commonjs-module', 'amd', 'amd-require', 'umd', 'umd2', 'jsonp'and 'system',但其他类型可能由插件添加。
- Loaders
- 开箱即用,webpack 只理解 JavaScript 和 JSON 文件。loaders 允许的 WebPack 处理其他类型的文件,并将其转换为有效的模块,可以通过您的应用程序消耗并添加到依赖关系图。
- Plugins
- 虽然加载器用于转换某些类型的模块,但可以利用插件来执行更广泛的任务,如包优化、资产管理和环境变量注入。
- Mode
- development、production或none
- Optimization(优化)
- minimize
- minimizer
- splitChunks
- runtimeChunk
- --其他概念--
- HMR
- source-map
- 预加载
- pwa
webpack做过哪些性能优化?
- Webpack之性能优化篇
- 配置entry和output,把首屏加载不需要的模块打包通过
externals+CDN的形式引入 - 通过
webpack.DllReferencePlugin读取vendor-manifest.json中的映射,再通过内置的__webpack_require__函数引用,并通过add-asset-html-webpack-plugin插件将dll.js注入到生成的html模板中 - 将各个
module.rule()添加oneOf配置,避免重复编译 - 使用
UglifyJs和Terser压缩代码 - 前端性能优化——webpack篇
- 使用
Happypack将loader由单进程转为多进程 - 使用
npm install --save-dev compression-webpack-plugin@1.1.12插件优化gzip压缩 - 使用
@fullhuman/postcss-purgecss插件对css进行tree-shaking(但对css module的样式无能为力) - 设置css配置中的
extract:true即可开启使用css分离插件ExtractTextPlugin 利用缓存:webpack.cache、babel-loader.cacheDirectory、HappyPack.cache都可以利用缓存提高rebuild效率
webpack的工作原理和流程
- Webpack CLI 启动打包流程;
- 载入 Webpack 核心模块,创建 Compiler 对象;
- 使用 Compiler 对象开始编译整个项目;
- 从入口文件开始,解析模块依赖,形成依赖关系树;
- 递归依赖树,将每个模块交给对应的 Loader 处理;
- 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。
webpack的构建流程
初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数。开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始编译整个项目。确定入口:根据配置中的entry找出所有的入口文件。编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系树。输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统即dist目录。
webpack如何开启tree-shaking
Webpack 4默认启用了Tree Shaking,同时消除了副作用。- 启动时,添加
--optimize-minimize参数 - 或
- 在
vue.config.js中添加新插件new webpack.optimize.UglifyJsPlugin() - 并且
- 配置
package.json中的sideEffects属性false:所有模块都没有副作用,但会影响css文件的导入;["!src/common.js"]:表示只要不是这个文件都有副作用
- 注意:
- 只能对使用
ES6方式导出的文件起作用(比如用lodash-es代替lodash)。对于commonjs无效,对于umd亦无效 - 需要用支持
tree-shaking的方式写import- 比如,
import _ from 'lodash';不支持 import { debounce } from 'lodash';支持
- 比如,
tree-shaking的原理是什么
- 删除文件中声明却没用到的代码、删除没有用到的import引入
- 使用 ES2015 模块语法(即import和export)。
- 确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 模块(这是流行的 Babel 预设 @babel/preset-env 的默认行为 -有关更多详细信息,请参阅文档)。
- 将"sideEffects"属性添加到您的项目package.json文件。
- 使用配置选项启用各种优化,包括缩小和摇树。production mode
vite的优势?为什么这个时刻会出现vite?解决了什么大问题?跟webpack有什么区别?webpack不能实现嘛?有什么限制?
- Vite 旨在利用生态系统中的新进展解决上述问题:浏览器开始原生支持 ES 模块,且越来越多 JavaScript 工具使用编译型语言编写。
- 快速冷启动:只启动一台静态页面的服务器,对文件代码不打包。Vite 通过在一开始将应用中的模块区分为 依赖(转换为基于 esbuild 预构建依赖) 和 源码(以 原生ESM 方式加载,只需要在浏览器请求源码时进行转换并按需提供源码) 两类,改进了开发服务器启动时间。
- 按需编译:服务器会根据客户端的请求加载不同的模块处理,实现真正的按需加载
- 模块热更新:采用立即编译当前修改文件的办法。同时 vite 还会使用缓存机制( http 缓存 => vite 内置缓存 ),加载更新后的文件内容
compiler编译器的大体编译过程是什么?都有哪些步骤?
- 遍历 HTML 模版字符串,通过正则表达式匹配 "<"
- 跳过某些不需要处理的标签,比如:注释标签、条件注释标签、Doctype。
- 备注:整个解析过程的核心是处理开始标签和结束标签
- 解析开始标签
- 得到一个对象,包括 标签名(tagName)、所有的属性(attrs)、标签在 html 模版字符串中的索引位置
- 进一步处理上一步得到的 attrs 属性,将其变成 [{ name: attrName, value: attrVal, start: xx, end: xx }, ...] 的形式
- 通过标签名、属性对象和当前元素的父元素生成 AST 对象,其实就是一个 普通的 JS 对象,通过 key、value 的形式记录了该元素的一些信息
- 接下来进一步处理开始标签上的一些指令,比如 v-pre、v-for、v-if、v-once,并将处理结果放到 AST 对象上
- 处理结束将 ast 对象存放到 stack 数组
- 处理完成后会截断 html 字符串,将已经处理掉的字符串截掉
- 解析闭合标签
- 如果匹配到结束标签,就从 stack 数组中拿出最后一个元素,它和当前匹配到的结束标签是一对。
- 再次处理开始标签上的属性,这些属性和前面处理的不一样,比如:key、ref、scopedSlot、样式等,并将处理结果放到元素的 AST 对象上
- 备注:视频中说这块儿有误,回头看了下,没有问题,不需要改,确实是这样
- 然后将当前元素和父元素产生联系,给当前元素的 ast 对象设置 parent 属性,然后将自己放到父元素的 ast 对象的 children 数组中
- 最后遍历完整个 html 模版字符串以后,返回 ast 对象
- --分割线--
- runtime-compiler
- template → ast → render函数 → virtual DOM → 真实DOM
- (template → ast → render函数)由vue-template-compiler实现。
- runtime-only (性能更高,打包后的代码量更少)
- render函数 → virtual DOM → 真实DOM
伍。CSS
会触发回流(重排、重布局)的属性
- 盒子模型相关属性会触发重布局:width、height、padding、margin、display、border-width、border、min-height
- 定位属性及浮动也会触发重布局:top、bottom、left、right、position、float、clear
- 改变节点内部文字结构也会触发重布局:text-align、overflow-y、font-weight、overflow、font-family、line-height、vertival-align、white-space、font-size
会触发重绘的属性
- color、border-style、border-radius
- background、background-image、background-position、background-repeat、background-size
- outline-color、outline、outline-style、outline-width
- box-shadow、visibility、text-decoration
对于回流和重绘的优化
- 不要把DOM结点的属性值放在一个循环里面当成循环里面的变量 offsetHeight offsetWidth
- 不要使用table布局,可能很小的一个小改动都会造成整个table的重新布局
- 用translate替代top改变(top会触发回流)
- 用opacity替代visibility(需要配合独立图层 transform:translateZ(0))
- 不要一条一条的修改DOM样式,预先定义好class,然后修改DOM的className
- 把DOM离线后修改,比如:先把DOM给display:none(有一次reflow),然后你修改100次,然后再把他显示出来
- 动画实现的速度的选择
- 对应动画新建图层
- 启用GOU硬件加速(transform:tranlateZ(0)或者transform:tranlate3d(0,0,0))
陆。HTTP
HTTP的强缓存和协商缓
- 强缓存:直接从本地副本比对读取,不去请求服务器,返回的状态码是200。主要包括
expires和cache-control- (HTTP1.0)Expires是通过
绝对时间来判断的,当浏览器判断时间在Expires设置的时间之前,就满足缓存条件,直接从本地获取数据。(但这也存在弊端,当用户修改客户端时间时缓存可能就会失效。) - (HTTP1.1)Cache-Control具有以下属性:
- max-age=xxx:缓存的内容将在 xxx 秒后失效,这个时间是个时间间隔相对时间。
- public:所有内容都将被缓存(客户端和代理服务器都可缓存)
- private:内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
- no-cache:必须先与服务器确认返回的响应是否被更改,然后才能使用该响应来满足后续对同一个网址的请求。因此,如果存在合适的验证令牌(ETag),no-cache 会发起往返通信来验证缓存的响应,如果资源未被更改,可以避免下载
- no-store:所有内容都不会被缓存或 Internet 临时文件中
- must-revalidation/proxy-revalidation:如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证
- (HTTP1.0)Expires是通过
- 协商缓存:去服务器比对,若没改变才直接读取本地缓存,返回的状态码是304。主要包括
last-modified和Etag- (HTTP1.0)Last-Modified为最后一次更新的时间,时间精度为秒。
- (HTTP1.0)Last-Modified-Since就是浏览器缓存数据时的Last-Modified。
- (HTTP1.1)Etag为一段字符串,每个文件都有属于自己的字符串,(分布式系统中会导致每个文件都有不同的Etag让缓存失效)。
- (HTTP1.1)If-None-Match为缓存时的Etag字符串。
既生Last-Modified何生Etag?
- Last-Modified标注的最后修改只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
- 如果某些文件会被定期生成,当有时内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存
- 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
http如何升级为websocket
- Connection: Upgrade
- Upgrade: websocket
- Sec-WebSocket-Version: 13
- Sec-WebSocket-Key: AoKNwAT7JdI+Kb2OxBMbUw==
- Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
webworker如何传递大数据
柒。浏览器
浏览器都包含哪些进程?
Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载等
第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建GPU进程:最多一个,用于3D绘制等浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的)- 默认每个Tab页面一个进程,互不影响
- 主要作用为:页面渲染,脚本执行,事件处理等
浏览器的渲染进程包括哪些线程?
GUI渲染线程- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
JS引擎线程- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
事件触发线程- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处
- 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
定时触发器线程- 传说中的setInterval与setTimeout所在线
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行
- 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
异步http请求线程- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
浏览器渲染流程
- 解析html建立dom树
- 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
- 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
- 绘制render树(paint),绘制页面像素信息
- 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。
css加载是否会阻塞dom树渲染?
- css是由单独的下载线程异步下载的
- 所以,css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
- 但是,会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)
捌。H5和小程序
多端适配方案是什么?rem的大小是基于什么计算的?
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">- 「rem」是指根元素(root element,html)的字体大小,从遥远的 IE6 到版本到 Chrome 他们都约好了,根元素默认的 font-size 都是 16px。
- rem是通过根元素进行适配的,网页中的根元素指的是
html。我们通过设置html的字体大小就可以控制rem的大小; lib-flexible/flexible最大54px;amfe-flexible会无限大- 放弃
"px2rem-loader": "^0.1.9",使用"postcss-px2rem": "^0.3.0"
如何实现移动端1px的线?
@media (-webkit-min-device-pixel-ratio:3),(min-device-pixel-ratio:3) { .border1px::after{ transform: scaleY(.33); } }
ios有没有什么特殊处理?设备系数比如两倍屏三倍屏怎么处理?
- ios不支持css3的
transparent属性,需要改为rgba(255,255,255,0) - 手机端上元素点击后:hover样式不会自动消失
小程序与Vue的区别
- ⽣命周期不⼀样,微信⼩程序⽣命周期⽐较简单
- 数据绑定也不同,微信⼩程序数据绑定需要使⽤ {{}} , vue 直接 : 就可以
- 显示与隐藏元素, vue 中,使⽤ v-if 和 v-show
- 控制元素的显示和隐藏,⼩程序中,使⽤ wx-if 和 hidden 控制元素的显示和隐藏
- 事件处理不同,⼩程序中,全⽤ bindtap(bind+event) ,或者catchtap(catch+event) 绑定事件, vue: 使⽤ v-on:event 绑定事件,或者使⽤@event 绑定事件
- 数据双向绑定也不也不⼀样在 vue 中,只需要再表单元素上加上 v-model ,然后再绑定data 中对应的⼀个值,当表单元素内容发⽣变化时, data 中对应的值也会相应改变,即双向绑定。微信⼩程序必须获取到表单元素,改变的值,然后再把值赋给⼀个 data 中声明的变量。
玖。微前端
对微前端做了哪些应用?遇到过什么坑?
- 应用1:作为基座,嵌入公司的零代码平台,实现客户、工单、权限管理模块
- 应用2:作为子应用,在线客服模块被嵌入公司的其他营销平台
- 坑1:路由切换的分发问题(需要将基座的当前路由传下来、基座的页面路由改为
xxx/*的形式、路由避免使用/:applicationId/:worksheetId/editFlow/:flowId的匹配形式) - 坑2:主微应用的隔离问题
- 解决css样式隔离:使用
postcss-plugin-namespace添加命名空间、使用REM命名规范 - js隔离:需要在卸载子应用时清空vue和vue-router和$el的实例
- 解决css样式隔离:使用
- 坑3:通信问题(可以通过回调函数通信、也可以通过vuex通信,但子应用的computed不是响应式的,因为不是一个vuex实例,需要子应用在接受store的时候手动监听)
玖。前端工程化
开发流程与规范
- 需求评审会,产品出原型
- 此时研发可以搭架子了,但要确定好需求和原型定稿
- 后端也开始写接口,但没出来前自己mock一份
- UI出设计稿,对比设计稿进行初版调试(主要还是保证功能的前提下再调交互)
- 进行接口联调,完善业务逻辑及页面展示
- 自测的过程中,对比UI稿进行像素级还原
- 产品验收通过,进行提测并配合测试解决问题
- 运维配置cicd与docker,进行自动部署
- (后期出现问题再重新进行需求评审)
设计模式
- 单一职责原则
- 接口隔离原则
- 依赖倒置原则
- 里氏替换原则
- 开闭原则
- 迪米特法则
喜欢的前端书籍有哪些?
- 《你不知道的JavaScript》上中下
- 《编写可维护的JavaScript》
- 《CSS权威指南》
- 《深入浅出webpack》
- 《啊哈算法》
- 《图解HTTP》
- 《算法(第4版)》
- 《重构:改善既有代码的设计》