今年的就业形式简直一片黑暗,本着明年会比今年还差的“侥幸心理”,我还是毫不犹豫地选择裸辞了,历经一个半月的努力,收到了还算不错的 offer,薪资和平台都有比较大的提升,但还是和自己的心理预期有着很大的差距 ...
今年的就业形式简直一片黑暗,本着明年会比今年还差的“侥幸心理”,我还是毫不犹豫地选择裸辞了,历经一个半月的努力,收到了还算不错的 offer,薪资和平台都有比较大的提升,但还是和自己的心理预期有着很大的差距。所以得出最大的结论就是:不要裸辞!不要裸辞!不要裸辞!因为面试期间带给人的压力,和现实与理想的落差对心理的摧残是不可估量的,在这样一个环境苟着是不错的选择。
接下来总结一般情况下前端面试中会经历的以下四个阶段和三个决定因素:
其次良好的沟通表达能力、着装和表现等场外因素能提高面试官对你的认可度。 有的人技术很牛逼,但是面试时让面试官觉得不爽,觉得你盛气逼人/形象邋遢/自以为是/表述不清,就直接不要你,那是最得不偿失的。 以下是我整个面试准备以及被问到过的问题的一个凝练,因为在面试过程中,和面试官的交流很大程度并不是简单的背书,最好是要将知识通过自己的总结和凝练表达出来,再根据面试官的提问临场发挥将之补足完善。【推荐学习:web前端、编程教学】 一、自我介绍面试官让你自我介绍,而且不限定自我介绍的范围,肯定是面试官想从你的自我介绍中了解到你,所以介绍一定要保证简短和流畅,面对不同的面试官,自我介绍的内容可以是完全一样的,所以提前准备好说辞很重要,并且一定要注意:不要磕磕巴巴,要自信!流畅的表达和沟通能力,同样是面试官会对候选人考核点之一。我也曾当过面试官,自信大方的候选人,往往更容易受到青睐。 1、个人介绍(基本情况),主要的简历都有了,这方面一定要短 2、个人擅长什么,包括技术上的和非技术上的。技术上可以了解你的转场,非技术可以了解你这个人 3、做过的项目,捡最核心的项目说,不要把所有项目像背书一样介绍 4、自己的一些想法、兴趣或者是观点,甚至包括自己的职业规划。这要是给面试官一个感觉:热衷于"折腾"或者"思考"
总的来说自我介绍尽量控制在3 - 5分钟之间,简明扼要为第一要义,其次是突出自己的能力和长处。 对于普通的技术面试官来说,自我介绍只是习惯性的面试前的开场白,一般简历上列举的基本信息已经满足他们对你的基本了解了。但是对于主管级别的面试官或者Hr,会看重你的性格、行为习惯、抗压能力等等综合能力。所以要让自己在面试过程尽可能表现的积极向上,爱好广泛、喜欢持续学习,喜欢团队合作,可以无条件加班等等。当然也不是说让你去欺骗,只是在现在这种环境中,这些“侧面能力”也是能在一定程度提升自己竞争力的法宝。 二、项目挖掘在目前这个行情,当你收到面试通知时,有很大概率是因为你的项目经验和所招聘的岗位比较符合。 所以在项目的准备上要额外上心,比如:
这些因人而异就不做赘述了,根据自己的情况好好挖掘即可。 三、个人先说个人,当你通过了技术面试,到了主管和hr这一步,不管你当前的技术多牛逼,他们会额外考察你个人的潜力、学习能力、性格与团队的磨合等软实力,这里列出一些很容易被问到的: 为什么跳槽?直接从个人发展入手表现出自己的上进心:
讲讲你和普通前端,你的亮点有哪些?1、善于规划和总结,我会对自己经手的项目进行一个全面的分析,一个是业务拆解,对个各模块的业务通过脑图进行拆解;另一个就是对代码模块的拆解,按功能去区分各个代码模块。再去进行开发。我觉得这是很多只会进行盲目业务开发的前端做不到的 2、喜欢专研技术,平常对 vue 的源码一直在学习,也有输出自己的技术文章,像之前写过一篇逐行精读 teleport 的源码,花了大约有三十个小时才写出来的,对每一行源码的功能和作用进行了解读(但是为啥阅读和点赞这么低)。 你有什么缺点?性子比较沉,更偏内向一点,所以我也会尝试让自己变得外向一点。 一个是要开各种评审会,作为前端代表需要我去准备各种材料和进行发言。 所以在团队内做比较多的技术分享,每周主持例会,也让我敢于去表达和探讨。 最近有关注什么新技术吗?
你是偏向于走各个方向探索还是一直向某个方向研究下去?我对个人的规划是这样的: 3 - 5 年在提高自己的技术深度的同时,扩宽自己的知识面,就是深度和广度上都要有提升,主要是在广度上,充分对大前端有了认知才能更好的做出选择 5 - 7 年就是当有足够的知识积累之后再选择某一个感兴趣方向深研下去,争取成为那个领域的专家 团队规模,团队规范和开发流程这个因人而异,如实准备即可,因为不同规模团队的研发模式差别是很大的。 代码 review 的目标1、最注重的是代码的可维护性(变量命名、注释、函数单一性原则等) 2、扩展性:封装能力(组件、代码逻辑是否可复用、可扩展性) 3、ES 新特性(es6+ 、ES2020, ES2021 可选链、at) 4、函数使用规范(比如遇到用 map 拿来当 forEach 用的) 5、性能提升,怎样运用算法,写出更加优雅,性能更好的代码 如何带领团队的我在上家公司是一个技术管理的角色。 0、落实开发规范,我在公司内部 wiki 上有发过,从命名、最佳实践到各种工具库的使用。新人进来前期我会优先跟进他们的代码质量 1、团队分工:每个人单独负责一个产品的开发,然后公共模块一般我会指定某几个人开发 2、代码质量保证:每周会review他们的代码,也会组织交叉 review 代码,将修改结果输出文章放到 wiki中 3、组织例会:每周组织例会同步各自进度和风险,根据各自的进度调配工作任务 4、技术分享:还会组织不定时的技术分享。一开始就是单纯的我做分享,比如微前端的体系,ice stark 的源码 5、公共需求池:比如webpack5/vite的升级;vue2.7的升级引入setup语法糖;pnpm的使用;拓扑图性能优化 6、优化专项:在第一版产品出来之后,我还发起过性能优化专项,首屏加载性能,打包体积优化;让每个人去负责对应的优化项 对加班怎么看?我觉得加班一般会有两种情况: 一是项目进度比较紧,那当然以项目进度为先,毕竟大家都靠这个吃饭 二是自身能力问题,对业务不熟啊或者引入一个全新的技术栈,那么我觉得不仅要加班跟上,还要去利用空闲时间抓紧学习,弥补自己的不足 有什么兴趣爱好?我平常喜欢阅读,就是在微信阅读里读一些心理学、时间管理、还有一些演讲技巧之类的书 然后是写文章,因为我发现单纯的记笔记很容易就忘了,因为只是记载别人的内容,而写自己的原创文章,在这个过程中能将知识非常高的比例转换成自身的东西,所以除了自个发掘金的文章,我也经常会对项目的产出有文章输出到 wiki 上 其他爱好就是和朋友约着打篮球、唱歌 四、技术技术面试一定要注意:简明扼要,详略得当,不懂的就说不懂。因为在面试过程中是一个和面试官面对面交流的过程,没有面试官会喜欢一个絮絮叨叨半天说不到重点候选人,同时在说话过程中,听者会被动的忽略自己不感兴趣的部分,所以要着重突出某个技术的核心特点,并围绕着核心适当展开。 大厂基本都会通过算法题来筛选候选人,算法没有捷径,只能一步一步地刷题再刷题,这方面薄弱的要提前规划进行个学习了。 技术面过程主要会对前端领域相关的技术进行提问,一般面试官会基于你的建立,而更多的是,面试官基于他之前准备好的面试题,或者所在项目组比较熟悉的技术点进行提问,因为都是未知数,所以方方面面都还是要求比较足的。 如果想进入一个中大型且发展前景不错的公司,并不是照着别人的面经背一背就能糊弄过去的,这里作出的总结虽然每一条都很简短,但都是我对每一个知识点进行全面学习后才提炼出来的部分核心知识点,所以不惧怕面试官的“发散一下思维”。 面试过程一般会涉及到以下八大知识类型的考量: JS/CSS/TypeScript/框架(Vue、React)/浏览器与网络/性能优化/前端工程化/架构/其他 所以面试前的技术准备绝不是一蹴而就,还需要平时的积累,比如可以利用每天十到二十分钟对其中一个小知识点进行全面的学习,长此以往,无论是几年的面试,都足够侃侃而谈。 JS篇JS的学习梭哈红包书和冴羽老师的深入JS系列博客就基本ok了 常见的JS面试题一般会有这些 什么是原型/原型链?原型的本质就是一个对象。 当我们在创建一个构造函数之后,这个函数会默认带上一个 这个原型对象是用来为通过该构造函数创建的实例对象提供共享属性,即用来实现基于原型的继承和属性的共享 所以我们通过构造函数创建的实例对象都会从这个函数的原型对象上继承上面具有的属性 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止(最顶层就是 所以通过原型一层层相互关联的链状结构就称为原型链。 什么是闭包?定义:闭包是指引用了其他函数作用域中变量的函数,通常是在嵌套函数中实现的。 从技术角度上所有 js 函数都是闭包。 从实践角度来看,满足以下俩个条件的函数算闭包
使用场景:
应用
this 的指向在绝大多数情况下,函数的调用方式决定了 1、全局的this非严格模式指向window对象,严格模式指向 undefined 2、对象的属性方法中的this 指向对象本身 3、apply、call、bind 可以变更 this 指向为第一个传参 4、箭头函数中的this指向它的父级作用域,它自身不存在 this 浏览器的事件循环?js 代码执行过程中,会创建对应的执行上下文并压入执行上下文栈中。 如果遇到异步任务就会将任务挂起,交给其他线程去处理异步任务,当异步任务处理完后,会将回调结果加入事件队列中。 当执行栈中所有任务执行完毕后,就是主线程处于闲置状态时,才会从事件队列中取出排在首位的事件回调结果,并把这个回调加入执行栈中然后执行其中的代码,如此反复,这个过程就被称为事件循环。 事件队列分为了宏任务队列和微任务队列,在当前执行栈为空时,主线程回先查看微任务队列是否有事件存在,存在则依次执行微任务队列中的事件回调,直至微任务队列为空;不存在再去宏任务队列中处理。 常见的宏任务有 常见的微任务有 宏任务和微任务的本质区别
javascript中数据在栈和堆中的存储方式1、基本数据类型大小固定且操作简单,所以放入栈中存储 2、引用数据类型大小不确定,所以将它们放入堆内存中,让它们在申请内存的时候自己确定大小 3、这样分开存储可以使内存占用最小。栈的效率高于堆 4、栈内存中变量在执行环境结束后会立即进行垃圾回收,而堆内存中需要变量的所有引用都结束才会被回收 讲讲v8垃圾回收1、根据对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同分代采用不同的回收算法 2、新生代采用空间换时间的 scavenge 算法:整个空间分为两块,变量仅存在其中一块,回收的时候将存活变量复制到另一块空间,不存活的回收掉,周而复始轮流操作 3、老生代使用标记清除和标记整理,标记清除:遍历所有对象标记标记可以访问到的对象(活着的),然后将不活的当做垃圾进行回收。回收完后避免内存的断层不连续,需要通过标记整理将活着的对象往内存一端进行移动,移动完成后再清理边界内存 函数调用的方法1、普通 2、作为对象的一个属性方法调用,如: 3、使用 4、 defer和async的区别一般情况下,当执行到 script 标签时会进行下载 + 执行两步操作,这两步会阻塞 HTML 的解析; async 和 defer 能将script的下载阶段变成异步执行(和 html解析同步进行); async下载完成后会立即执行js,此时会阻塞HTML解析; defer会等全部HTML解析完成且在DOMContentLoaded 事件之前执行。 浏览器事件机制DOM 事件流三阶段:
所有事件都要经历捕获阶段和目标阶段,但有些事件会跳过冒泡阶段,比如元素获得焦点 focus 和失去焦点 blur 不会冒泡 扩展一 e.target 和 e.currentTarget 区别?
例如: <ul> <li><span>hello 1</span></li> </ul> let ul = document.querySelectorAll('ul')[0] let aLi = document.querySelectorAll('li') ul.addEventListener('click',function(e){ let oLi1 = e.target let oLi2 = e.currentTarget console.log(oLi1) // 被点击的li console.log(oLi2) // ul console.og(oLi1===oLi2) // false }) 登录后复制 给 ul 绑定了事件,点击其中 li 的时候,target 就是被点击的 li, currentTarget 就是被绑定事件的 ul 事件冒泡阶段(上述例子), 作用:
扩展二 addEventListener 参数 语法: addEventListener(type, listener); addEventListener(type, listener, options || useCapture); 登录后复制
Vue篇笔主是主要从事 Vue相关开发的,也做过 react 相关的项目,当然 react 也只是能做项目的水平,所以在简历中属于一笔带过的那种,框架贵不在多而在精,对Vue源码系列的学习让我对Vue还是十分自信的。学习过程也是如此,如果你能够对一门框架达到精通原理的掌握程度,学习其他框架不过只是花时间的事情。 vue和react的区别1、数据可变性
2、写法
3、diff 算法
扩展:了解 react hooks吗 组件类的写法很重,层级一多很难维护。 函数组件是纯函数,不能包含状态,也不支持生命周期方法,因此无法取代类。 React Hooks 的设计目的,就是加强版函数组件,完全不使用"类",就能写出一个全功能的组件 React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 vue组件通信方式
vue 渲染列表为什么要加key?Vue 在处理更新同类型 vnode 的一组子节点(比如v-for渲染的列表节点)的过程中,为了减少 DOM 频繁创建和销毁的性能开销: 对没有 key 的子节点数组更新是通过就地更新的策略。它会通过对比新旧子节点数组的长度,先以比较短的那部分长度为基准,将新子节点的那一部分直接 patch 上去。然后再判断,如果是新子节点数组的长度更长,就直接将新子节点数组剩余部分挂载;如果是新子节点数组更短,就把旧子节点多出来的那部分给卸载掉)。所以如果子节点是组件或者有状态的 DOM 元素,原有的状态会保留,就会出现渲染不正确的问题。 有 key 的子节点更新是调用的 综上,如果是用 v-for 去遍历常量或者子节点是诸如纯文本这类没有”状态“的节点,是可以使用不加 key 的写法的。但是实际开发过程中更推荐统一加上 key,能够实现更广泛场景的同时,避免了可能发生的状态更新错误,我们一般可以使用 ESlint 配置 key 为 v-for 的必需元素。 想详细了解这个知识点的可以去看看我之前写的文章:v-for 到底为啥要加上 key? vue3 相对 vue2的响应式优化vue2使用的是 所以 vue3 采用了 其实从api的原生性能上 而 vue 做的响应式性能优化主要是在将嵌套层级比较深的对象变成响应式的这一过程。 vue2的做法是在组件初始化的时候就递归执行 而vue3是在访问到子对象属性的时候,才会去将它转换为响应式。这种延时定义子对象响应式会对性能有一定的提升 Vue 核心diff流程前提:当同类型的 vnode 的子节点都是一组节点(数组类型)的时候, 步骤:会走核心 diff 流程 Vue3是快速选择算法
Vue2是双端比较算法 在新旧字节点的头尾节点,也就是四个节点之间进行对比,找到可复用的节点,不断向中间靠拢的过程 diff目的:diff 算法的目的就是为了尽可能地复用节点,减少 DOM 频繁创建和删除带来的性能开销 vue双向绑定原理基于 MVVM 模型,viewModel(业务逻辑层)提供了数据变化后更新视图和视图变化后更新数据这样一个功能,就是传统意义上的双向绑定。 Vue2.x 实现双向绑定核心是通过三个模块:Observer监听器、Watcher订阅者和Compile编译器。 首先监听器会监听所有的响应式对象属性,编译器会将模板进行编译,找到里面动态绑定的响应式数据并初始化视图;watchr 会去收集这些依赖;当响应式数据发生变更时Observer就会通知 Watcher;watcher接收到监听器的信号就会执行更新函数去更新视图; vue3的变更是数据劫持部分使用了porxy 替代 Object.defineProperty,收集的依赖使用组件的副作用渲染函数替代watcher v-model 原理vue2 v-model 原理剖析 V-model 是用来监听用户事件然后更新数据的语法糖。 其本质还是单向数据流,内部是通过绑定元素的 value 值向下传递数据,然后通过绑定 input 事件,向上接收并处理更新数据。 单向数据流:父组件传递给子组件的值子组件不能修改,只能通过emit事件让父组件自个改。 // 比如 <input v-model="sth" /> // 等价于 <input :value="sth" @input="sth = $event.target.value" /> 登录后复制 给组件添加 // 父组件 <my-button v-model="number"></my-button> // 子组件 <script> export default { props: { value: Number, // 属性名必须是 value }, methods: { add() { this.$emit('input', this.value + 1) // 事件名必须是 input }, } } </script> 登录后复制 如果想给绑定的 value 属性和 input 事件换个名称呢?可以这样: 在 Vue 2.2 及以上版本,你可以在定义组件时通过 model 选项的方式来定制 prop/event: <script> export default { model: { prop: 'num', // 自定义属性名 event: 'addNum' // 自定义事件名 } } 登录后复制 vue3 v-model 原理 实现和 vue2 基本一致 <Son v-model="modalValue"/> 登录后复制 等同于 <Son v-model="modalValue"/> <Son :modalValue="modalValue" @update:modalValue="modalUpdate=$event.target.value"/> 登录后复制 自定义 model 参数 <Son v-model:visible="visible"/> setup(props, ctx){ ctx.emit("update:visible", false) } 登录后复制 vue 响应式原理不管vue2 还是 vue3,响应式的核心就是观察者模式 + 劫持数据的变化,在访问的时候做依赖收集和在修改数据的时候执行收集的依赖并更新数据。具体点就是: vue2 的话采用的是 Vue3 使用的是 ES6 的 proxy,proxy 不仅能够追踪属性的获取和修改,还可以追踪对象的增删,这在 vue2中需要 set/delete 才能实现。然后就是收集的依赖是用组件的副作用渲染函数替代 watcher 实例。 性能方面,从原生 api 角度,proxy 这个方法的性能是不如 Object.property,但是 vue3 强就强在一个是上面提到的可以追踪对象的增删,第二个是对嵌套对象的处理上是访问到具体属性才会把那个对象属性给转换成响应式,而 vue2 是在初始化的时候就递归调用将整个对象和他的属性都变成响应式,这部分就差了。 扩展一 vue2 通过数组下标更改数组视图为什么不会更新? 尤大:性能不好 注意:vue3 是没问题的 why 性能不好? 我们看一下响应式处理: export class Observer { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { // 这里对数组进行单独处理 if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { // 对对象遍历所有键值 this.walk(value) } } walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } } 登录后复制 对于对象是通过 理由是数组的键相较对象多很多,当数组数据大的时候性能会很拉胯。所以不开放 computed 和 watchComputed 的大体实现和普通的响应式数据是一致的,不过加了延时计算和缓存的功能: 在访问computed对象的时候,会触发 getter ,初始化的时候将 computed 属性创建的 watcher (vue3是副作用渲染函数)添加到与之相关的响应式数据的依赖收集器中(dep),然后根据里面一个叫 dirty 的属性判断是否要收集依赖,不需要的话直接返回上一次的计算结果,需要的话就执行更新重新渲染视图。 watchEffect? watchEffect会自动收集回调函数中响应式变量的依赖。并在首次自动执行 推荐在大部分时候用 $nextTick 原理?vue有个机制,更新 DOM 是异步执行的,当数据变化会产生一个异步更行队列,要等异步队列结束后才会统一进行更新视图,所以改了数据之后立即去拿 dom 还没有更新就会拿不到最新数据。所以提供了一个 nextTick 函数,它的回调函数会在DOM 更新后立即执行。 nextTick 本质上是个异步任务,由于事件循环机制,异步任务的回调总是在同步任务执行完成后才得到执行。所以源码实现就是根据环境创建异步函数比如 Promise.then(浏览器不支持promise就会用MutationObserver,浏览器不支持MutationObserver就会用setTimeout),然后调用异步函数执行回调队列。 所以项目中不使用$nextTick的话也可以直接使用Promise.then或者SetTimeout实现相同的效果 Vue 异常处理1、全局错误处理:
如果在组件渲染时出现运行错误,错误将会被传递至全局 比如前端监控领域的 sentry,就是利用这个钩子函数进行的 vue 相关异常捕捉处理 2、全局警告处理: Vue.config.warnHandler = function(msg, vm, trace) {}; 登录后复制 注意:仅在开发环境生效 像在模板中引用一个没有定义的变量,它就会有warning 3、单个vue 实例错误处理: const app = new Vue({ el: "#app", renderError(h, err) { return h("pre", { style: { color: "red" } }, err.stack); } }); 登录后复制 和组件相关,只适用于开发环境,这个用处不是很大,不如直接看控制台 4、子孙组件错误处理: Vue.component("cat", { template: `<div><slot></slot></div>`, props: { name: { type: string } }, errorCaptured(err, vm, info) { console.log(`cat EC: ${err.toString()}\ninfo: ${info}`); return false; } }); 登录后复制 注:只能在组件内部使用,用于捕获子孙组件的错误,一般可以用于组件开发过程中的错误处理 5、终极错误捕捉: window.onerror = function(message, source, line, column, error) {}; 登录后复制 它是一个全局的异常处理函数,可以抓取所有的 JavaScript 异常 Vuex 流程 & 原理Vuex 利用 vue 的mixin 机制,在beforeCreate 钩子前混入了 vuexinit 方法,这个方法实现了将 store 注入 vue 实例当中,并注册了 store 的引用属性 store,所以可以使用‘this.store.xxx`去引入vuex中定义的内容。 然后 state 是利用 vue 的 data,通过 Vue.use函数里面具体做了哪些事概念 可以通过全局方法 该方法第一个参数必须是
所以** 源码实现 export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) // 避免重复注册 if (installedPlugins.indexOf(plugin) > -1) { return this } // 获取传入的第一个参数 const args = toArray(arguments, 1) args.unshift(this) if (typeof plugin.install === 'function') { // 如果传入对象中的install属性是个函数则直接执行 plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { // 如果传入的是函数,则直接(作为install方法)执行 plugin.apply(null, args) } // 将已经注册的插件推入全局installedPlugins中 installedPlugins.push(plugin) return this } } 登录后复制 使用方式 installedPlugins import Vue from 'vue' import Element from 'element-ui' Vue.use(Element) 登录后复制 怎么编写一个vue插件要暴露一个 Myplugin.install = function(Vue, options = {}) { // 1、添加全局方法或属性 Vue.myGlobalMethod = function() {} // 2、添加全局服务 Vue.directive('my-directive', { bind(el, binding, vnode, pldVnode) {} }) // 3、注入组件选项 Vue.mixin({ created: function() {} }) // 4、添加实例方法 Vue.prototype.$myMethod = function(methodOptions) {} } 登录后复制 CSS篇Css直接面试问答的题目相对来说比较少,更多的是需要你能够当场手敲代码实现功能,一般来说备一些常见的布局,熟练掌握flex基本就没有什么问题了。 什么是 BFCBlock Formatting context,块级格式上下文 BFC 是一个独立的渲染区域,相当于一个容器,在这个容器中的样式布局不会受到外界的影响。 比如浮动元素、绝对定位、overflow 除 visble 以外的值、display 为 inline/tabel-cells/flex 都能构建 BFC。 常常用于解决
伪类和伪元素及使用场景伪类 伪类即:当元素处于特定状态时才会运用的特殊类 开头为冒号的选择器,用于选择处于特定状态的元素。比如 伪元素 伪元素用于创建一些不在文档树中的元素,并为其添加样式。比如说,我们可以通过
h1:before { content: "Hello "; } 登录后复制
src 和 href 区别
不定宽高元素的水平垂直居中
浏览器和网络篇浏览器和网络是八股中最典型的案例了,无论你是几年经验,只要是前端,总会有问到你的浏览器和网络协议。 最好的学习文章是李兵老师的《浏览器工作原理与实践》 跨页面通信的方法?这里分了同源页面和不同源页面的通信。 不同源页面可以通过 iframe 作为一个桥梁,因为 iframe 可以指定 origin 来忽略同源限制,所以可以在每个页面都嵌入同一个 iframe 然后监听 iframe 中传递的 message 就可以了。 同源页面的通信大致分为了三类:广播模式、共享存储模式和口口相传模式 第一种广播模式,就是可以通过 BroadCast Channel、Service Worker 或者 localStorage 作为广播,然后去监听广播事件中消息的变化,达到页面通信的效果。 第二种是共享存储模式,我们可以通过Shared Worker 或者 IndexedDB,创建全局共享的数据存储。然后再通过轮询去定时获取这些被存储的数据是否有变更,达到一个的通信效果。像常见cookie 也可以作为实现共享存储达到页面通信的一种方式 最后一种是口口相传模式,这个主要是在使用 window.open 的时候,会返回被打开页面的 window 的引用,而在被打开的页面可以通过 window.opener 获取打开它的页面的 window 点引用,这样,多个页面之间的 window 是能够相互获取到的,传递消息的话通过 postMessage 去传递再做一个事件监听就可以了 详细说说 HTTP 缓存在浏览器第一次发起请求服务的过程中,会根据响应报文中的缓存标识决定是否缓存结果,是否将缓存标识和请求结果存入到浏览器缓存中。 HTTP 缓存分为强制缓存和协商缓存两类。 强制缓存就是请求的时候浏览器向缓存查找这次请求的结果,这里分了三种情况,没查找到直接发起请求(和第一次请求一致);查找到了并且缓存结果还没有失效就直接使用缓存结果;查找到但是缓存结果失效了就会使用协商缓存。 强制缓存有 Expires 和 Cache-control 两个缓存标识,Expires 是http/1.0 的字段,是用来指定过期的具体的一个时间(如 Fri, 02 Sep 2022 08:03:35 GMT),当服务器时间和浏览器时间不一致的话,就会出现问题。所以在 http1.1 添加了 cache-control 这个字段,它的值规定了缓存的范围(public/private/no-cache/no-store),也可以规定缓存在xxx时间内失效(max-age=xxx)是个相对值,就能避免了 expires带来的问题。 协商缓存就是强制缓存的缓存结果失效了,浏览器携带缓存标识向服务器发起请求,有服务器通过缓存标识决定是否使用缓存的过程。 控制协商缓存的字段有 last-modified / if-modified-since 和 Etag / if-none-match,后者优先级更高。 大致过程就是通过请求报文传递 last-modified 或 Etag 的值给服务器与服务器中对应值作对比,若和响应报文中的 if-modified-since 或 if-none-match 结果一致,则协商缓存有效,使用缓存结果,返回304;否则失效,重新请求结果,返回200 输入 URL 到页面展现的全过程用户输入一段内容后,浏览器会先去判断这段内容是搜索内容还是 URL ,是搜索内容的话就会接合默认的搜索引擎生成 URL,比如 google 浏览器是goole.com/search?xxxx,如果是 URL 会拼接协议,比如 http/https。当页面没有监听 beforeupload 时间或者同意了继续执行流程,浏览器图标栏会进入加载中的状态。 接下来浏览器进程会通过 IPC 进程间通信将 URL 请求发送给网络进程,网络进程会先去缓存中查找该资源,如果有则拦截请求并直接200返回,没有的话会进入网络请求流程。 网络请求流程是网络进程请求 DNS 服务器返回域名对应的IP和端口号(如果这些之前有缓存也是直接返回缓存结果),如果没有端口号,http默认为80,https默认为443,如果是https还需要建立 TLS 安全连接创建加密的数据通道。 接着就是 TCP 三次握手建立浏览器和服务器连接,然后进行数据传输,数据传输完成四次挥手断开连接,如果设置了 网络进程将通过TCP获取的数据包进行解析,首先是根据响应头的content-type来判断数据类型,如果是字节流或者文件类型的话,会交给下载管理器进行下载,这时候导航流程就结束了。如果是 text/html 类型,就会通知到浏览器进程获取文档进行渲染。 浏览器进程获取到渲染的通知,会根据当前页面和新输入的页面判断是否是同一个站点,是的话就复用之前网页创建的渲染进程,否则的话会新创建一个单独的渲染进程。 浏览器进程将“提交文档”的消息给渲染进程,渲染进程接收到消息就会和网络进程建立传输数据的通道,数据传输完成后就返回“确认提交”的信息给浏览器进程。 浏览器接收到渲染进程的“确认提交“的消息后,就会更新浏览器的页面状态:安全状态、地址栏 URL、前进后退的历史消息,并更新 web页面,此时页面是空白页面(白屏)。 页面渲染过程(重点记忆) 最后是渲染进程对文档进行页面解析和子资源加载,渲染进程会将 HTML 转换成 DOM 树结构,将 css 转换成 styleSeets ( CSSOM)。然后复制 DOM 树过滤掉不显示的元素创建基本的渲染树,接着计算每个 DOM 节点的样式和计算每个节点的位置布局信息构建成布局树。 具有层叠上下文或者需要要裁剪的地方会独立创建图层,这就是分层,最终会形成一个分层树,渲染进程会给每个图层生成绘制列表并提交给合成线程,合成线程将图层分成图块(避免一次性绘制图层所有内容,可以根据图块优先需渲染视口部分),并在光栅化线程池中将图块转换成位图。 转换完毕后合成线程发送绘制图块命令 DrawQuard 给浏览器进程,浏览器根据 DrawQuard 消息生成页面,并显示到浏览器上。 速记: 浏览器的渲染进程将 html 解析成 dom树,将 css 解析成 cssom 树,然后会先复制一份 DOM 树过滤掉不显示的元素(比如 display: none),再和 cssom 结合进行计算每个 dom 节点的布局信息构建成一个布局树。 布局树生成完毕就会根据图层的层叠上下文或者裁剪部分进行分层,形成一个分层树。 渲染进程再将每个图层生成绘制列表并提交给合成线程,合成线程为了避免一次性渲染,就是分块渲染,会将图层分成图块,并通过光栅化线程池将图块转换成位图。 转换完毕后,合成线程将绘制图块的命令发送给浏览器进行显示 TCP 和 UDP 的区别UDP 是用户数据包协议(User Dataprogram Protocol),IP 通过 IP 地址信息把数据包传送给指定电脑后,UDP 可以通过端口号把数据包分发给正确的程序。UDP 可以校验数据是否正确,但没有重发的机制,只会丢弃错误的数据包,同时 UDP 在发送之后无法确认是否到达目的地。UDP 不能保证数据的可靠性,但是传输的速度非常快,通常运用于在线视频、互动游戏这些不那么严格保证数据完整性的领域。 TCP 是为了解决 UDP 的数据容易丢失,且无法正确组装数据包二引入的传输控制协议(Transmission Control Protocol),是一种面向连接的,可靠的,基于字节流的传输层通信协议。TCP 在处理数据包丢失的情况,提供了重传机制;并且 TCP 引入了数据包排序机制,可以将乱序的数据包组合成完整的文件。 TCP 头除了包含目标端口和本机端口号外,还提供了用于排序的序列号,以便接收端通过序号来重排数据包。 TCP 和 UDP 的区别一个 TCP 连接的生命周期会经历链接阶段,数据传输和断开连接阶段三个阶段。 连接阶段 用来建立客户端和服务器之间的链接,通过三次握手用来确认客户端、服务端相互之间的数据包收发能力。 1、客户端先发送 SYN 报文用来确认服务端能够发数据,并进入 SYN_SENT 状态等待服务端确认 2、服务端收到 SYN 报文,会向客户端发送一个 ACK 确认报文,同时服务端也会向客户端发送 SYN 报文用来确认客户端是否能够发送数据,此时服务端进入 SYN_RCVD 状态 3、客户端接收到 ACK + SYN 的报文,就会向服务端发送数据包并进入 ESTABLISHED 状态(建立连接);服务端接收到客户端发送的 ACK 包也会进入 ESTABLISHED 状态,完成三次握手 传输数据阶段 该阶段,接收端需要对每个包进行确认操作; 所以当发送端发送了一个数据包之后,在规定时间内没有接收到接收端反馈的确认消息,就会判断为包丢失,从而触发重发机制; 一个大文件在传输过程中会分为很多个小数据包,数据包到达接收端后会根据 TCP 头中的序号为其排序,保证数据的完整。 断开连接阶段 通过四次挥手,来保证双方的建立的连接能够断开 1、客户端向服务器发起 FIN 包,并进入 FIN_WAIT_1 状态 2、服务端收到 FIN 包,发出确认包 ACK,并带上自己的序号,服务端进入 CLOSE_WAIT 状态。这时候客户端已经没有数据要发给服务端了,但是服务端如果有数据要发给客户端,客户端还是需要接收。客户端收到 ACK 后进入 FIN_WAIT_2 状态 3、服务端数据发送完毕后,向客户端发送 FIN 包,此时服务器进入 LAST_ACK 状态 4、客户端收到 FIN 包发出确认包 ACK ,此时客户端进入 TIME_WAIT 状态,等待 2 MSL 后进入 CLOSED 状态;服务端接收到客户端的 ACK 后就进入 CLOSED 状态了。 对于四次挥手,因为 TCP 是全双工通信,在主动关闭方发送 FIN 包后,接收端可能还要发送数据,不能立即关闭服务器端到客户端的数据通道,所以也就不能将服务器端的 Content-length 了解吗?Content-length 是 http 消息长度,用十进制数字表示的字节的数目。 如果 content-length > 实际长度,服务端/客户端读取到消息队尾时会继续等待下一个字节,会出现无响应超时的情况 如果 content-length < 实际长度,首次请求的消息会被截取,然后会导致后续的数据解析混乱。 当不确定content-length的值应该使用Transfer-Encoding: chunked,能够将需要返回的数据分成多个数据块,直到返回长度为 0 的终止块 跨域常用方案什么是跨域? 协议 + 域名 + 端口号均相同时则为同域,任意一个不同则为跨域 解决方案 1、 传统的jsonp:利用 2、 一般使用 cors(跨域资源共享)来解决跨域问题,浏览器在请求头中发送origin字段指明请求发起的地址,服务端返回Access-control-allow-orign,如果一致的话就可以进行跨域访问 3、 Iframe 解决主域名相同,子域名不同的跨域请求 4、 浏览器关闭跨域限制的功能 5、 http-proxy-middleware 代理 预检 补充:http会在跨域的时候发起一次预检请求,“需预检的请求”要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。 withCredentials为 true不会产生预请求;content-type为application/json会产生预请求;设置了用户自定义请求头会产生预检请求;delete方法会产生预检请求; XSS 和 CSRFxss基本概念 Xss (Cross site scripting)跨站脚本攻击,为了和 css 区别开来所以叫 xss Xss 指黑客向 html 或 dom 中注入恶意脚本,从而在用户浏览页面的时候利用注入脚本对用户实施攻击的手段 恶意脚本可以做到:窃取 cookie 信息、监听用户行为(比如表单的输入)、修改DOM(比如伪造登录界面骗用户输入账号密码)、在页面生成浮窗广告等 恶意脚本注入方式:
前两种属于服务端漏洞,最后一种属于前端漏洞 防止xss攻击的策略 1、服务器对输入脚本进行过滤或者转码,比如将 2、充分利用内容安全策略 CSP(content-security-policy),可以通过 http 头信息的 content-security-policy 字段控制可以加载和执行的外部资源;或者通过html的meta 标签 3、cookie设置为 http-only, cookie 就无法通过 csrf基本概念 Csrf(cross site request forgery)跨站请求伪造,指黑客引导用户访问黑客的网站。 CSRF 是指黑客引诱用户打开黑客的网站,在黑客的网站中,利用用户的登录状态发起的跨站请求。简单来讲,CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事。 Csrf 攻击场景
防止csrf方法 1、设置 cookie 时带上SameSite: strict/Lax选项 2、验证请求的来源站点,通过 origin 和 refere 判断来源站点信息 3、csrf token,浏览器发起请求服务器生成csrf token,发起请求前会验证 csrf token是否合法。第三方网站肯定是拿不到这个token。我们的 csrf token 是前后端约定好后写死的。 websocketwebsocket是一种支持双向通信的协议,就是服务器可以主动向客户端发消息,客户端也可以主动向服务器发消息。 它是基于 HTTP 协议来建立连接的的,与http协议的兼容性很好,所以能通过 HTTP 代理服务器;没有同源限制。 WebSocket 是一种事件驱动的协议,这意味着可以将其用于真正的实时通信。与 HTTP 不同(必须不断地请求更新),而使用 websockets,更新在可用时就会立即发送 当连接终止时,WebSockets 不会自动恢复,这是应用开发中需要自己实现的机制,也是存在许多客户端开源库的原因之一。 像webpack和vite的devServer就使用了websocket实现热更新 Post 和 Get 区别
性能优化篇性能优化是中大型公司非常注重的点,因为和前端人的KPI息息相关,所以自然会作为面试题的常客。 性能优化有哪些手段
前端监控 SDK 技术要点
如何减少回流、重绘,充分利用 GPU 加速渲染?首先应该避免直接使用 DOM API 操作 DOM,像 vue react 虚拟 DOM 让对 DOM 的多次操作合并成了一次。
Css 中的 大图片优化的方案
代码优化
感兴趣的可以看看我之前的一篇性能优化技巧整理的文章极意 · 代码性能优化之道 前端工程化篇前端工程化是前端er成长路上最必不可少的技能点,一切能够提高前端开发效率的功能都可以当做前端工程化的一部分。刚入门的可以从零搭建脚手架开始万字长文详解从零搭建企业级 vue3 + vite2+ ts4 框架全过程 对于面试而言,考察的最多的还是对打包工具的掌握程度。 webpack的执行流程和生命周期webpack 是为现代 JS 应用提供静态资源打包功能的 bundle。 核心流程有三个阶段: 初始化阶段、构建阶段和生成阶段 1、初始化阶段,会从配置文件、配置对象和Shell参数中读取初始化的参数并与默认配置结合成最终的参数,之以及创建 compiler 编译器对象和初始化它的运行环境 2、构建阶段,编译器会执行它的 run()方法开始编译的过程,其中会先确认 entry 入口文件,从入口文件开始搜索和入口文件有直接或者简介关联的所有文件创建依赖对象,之后再根据依赖对象创建 module 对象,这时候会使用 loader 将模块转换标准的 js 内容,再调用 js 的解释器将内容转换成 AST 对象,再从 AST 中找到该模块依赖的模块,递归本步骤知道所有入口依赖文件都经过了本步骤的处理。最后完成模块编译,得到了每个模块被翻译的内容和他们之间的关系依赖图(dependency graph),这个依赖图就是项目所有用到的模块的映射关系。 3、生成阶段,将编译后的 module 组合成 chunk ,再把每个 chunk 转换成一个单独的文件输出到文件列表,确定好输出内容后,根据配置确定输出路径和文件名,就把文件内容写入文件系统 webpack的plugin 和loaderloader webpack只能理解 JS 和 JSON 文件,loader 本质上就是个转换器,能将其他类型的文件转换成 webpack 识别的东西 loader 会在 webpack 的构建阶段将依赖对象创建的 module 转换成标准的 js 内容的东西。比如 vue-loader 将vue文件转换成 js 模块,图片字体通过 url-loader 转换成 data URL,这些 webpack 能够识别的东西。 可以在 module.rules 中配置不同的 loader 解析不同的文件 plugin 插件本质是一个带有 apply 函数的类 比如stylelint plugin可以指定 stylelint 的需要检查文件类型和文件范围;HtmlWebpackPlugin 用来生成打包后的模板文件;MiniCssExtactPlugin会将所有的css提取成独立的chunks,stylelintplugin可以在开发阶段提供样式的检查功能。 webpack的hash策略MiniCssExtractPlugin 对于浏览器来说,一方面期望每次请求页面资源时,获得的都是最新的资源;一方面期望在资源没有发生变化时,能够复用缓存对象。这个时候,使用文件名+文件哈希值的方式,就可以实现只要通过文件名,就可以区分资源是否有更新。而webpack就内置了hash计算方法,对生成文件的可以在输出文件中添加hash字段。 Webpack 内置 hash 有三种
扩展:file-loader 的 hash 可能有同学会表示有以下疑问。 明明经常看到在处理一些图片,字体的file-loader的打包时,使用的是[name]_[hash:8].[ext] 但是如果改了其他工程文件,比如index.js,生成的图片hash并没有变化。 这里需要注意的是,file-loader的hash字段,这个loader自己定义的占位符,和webpack的内置hash字段并不一致。 这里的hash是使用md4等hash算法,对文件内容进行hash。 所以只要文件内容不变,hash还是会保持一致。 vite原理Vite 主要由两个部分组成
webpack 和 vite 对比Webpack 的热更新原理简单来说就是,一旦发生某个依赖(比如 Vite 利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用,热更新是在客户端和服务端之间建立了 websocket 连接,代码修改后服务端发送消息通知客户端去请求修改模块的代码,完成热更新,就是改了哪个文件就重新请求那个文件,这样保证了热更新速度不受项目大小影响。 所以vite目前的最大亮点在于开发体验上,服务启动快、热更新快,明显地优化了开发者体验,生产环境因为底层是 rollup ,rollup更适合小的代码库,从扩展和功能上都是不如 webpack 的,可以使用vite作为一个开发服务器dev server使用,生产打包用webpack这样的模式。 做过哪些 wewbpack 的优化0、升级 webpack 版本,3升4,实测是提升了几十秒的打包速度 1、splitChunksPlugin 抽离共用模块输出单独的chunks ,像一些第三方的依赖库,可以单独拆分出来,避免单个chunks过大。 2、DllPlugin 作用同上,这个依赖库相当于从业务代码中剥离出来,只有依赖库自身版本变化才会重新打包,提升打包速度 3、loaders的运行是同步的,同各模块会执行全部的loaders
4、exclude/include 指定匹配范围;alias指定路径别名; 5、cache-loader持久化存储; 6、ESM项目开启 useExport标记,使用 tree-shaking 7、exclude/include 指定匹配范围;alias指定路径别名;cache-loader持久化存储; 8、terserPlugin 可以提供代码压缩,去除注释、去除空格的功能;MiniCssExtractPlugin 压缩 css npm run 执行过程0、在 package.json 文件中可以定义 script 配置项,里面可以定义运行脚本的键和值 1、在 npm install 的时候,npm 会读取配置将执行脚本软链接到 2、还有一种情况,就是单纯的执行脚本命令,比如 npm run build,实际运行的是 node build.js,即使用 node 执行 build.js 这个文件 ESM和CJS 的区别ES6
CommonJS
设计模式篇代理模式代理模式:为对象提供一个代用品或占位符,以便控制对它的访问 例如实现图片懒加载的功能,先通过一张 装饰者模式装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法 通常运用在原有方法维持不变,在原有方法上再挂载其他方法来满足现有需求。 像 typescript 的装饰器就是一个典型的装饰者模式,还有 vue 当中的 mixin 单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象 比如 ice stark 的子应用,一次只保证渲染一个子应用 观察者模式和发布订阅模式1、虽然两种模式都存在订阅者和发布者(具体观察者可认为是订阅者、具体目标可认为是发布者),但是观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调的,所以观察者模式的订阅者与发布者之间是存在依赖的,而发布/订阅模式则不会。 2、两种模式都可以用于松散耦合,改进代码管理和潜在的复用。 3、在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信 4、观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列) 其他正则表达式正则表达式是什么数据类型? 是对象, 正则贪婪模式和非贪婪模式? 量词
贪婪模式 正则中,表示次数的量词默认是贪婪的,会尽可能匹配最大长度,比如 非贪婪模式 在量词后面加 贪婪& 非贪婪特点 贪婪模式和非贪婪模式,都需要发生回溯才能完成相应的功能 独占模式 独占模式和贪婪模式很像,独占模式会尽可能多地去匹配,如果匹配失败就结束,不会进行回溯,这样的话就比较节省时间。 写法:量词后面使用 优缺点:独占模式性能好,可以减少匹配的时间和 cpu 资源;但是某些情况下匹配不到,比如:
a{1,3}+ab 去匹配 aaab 字符串,a{1,3}+ 会把前面三个 a 都用掉,并且不会回溯 常见正则匹配
单元测试概念:前端自动化测试领域的, 用来验证独立的代码片段能否正常工作 1、可以直接用 Node 中自带的 assert 模块做断言:如果当前程序的某种状态符合 assert 的期望此程序才能正常执行,否则直接退出应用。 function multiple(a, b) { let result = 0; for (let i = 0; i < b; ++i) result += a; return result; } const assert = require('assert'); assert.equal(multiple(1, 2), 3)); 登录后复制 2、常见单测工具:Jest,使用示例 const sum = require('./sum'); describe('sum function test', () => { it('sum(1, 2) === 3', () => { expect(sum(1, 2)).toBe(3); }); // 这里 test 和 it 没有明显区别,it 是指: it should xxx, test 是指 test xxx test('sum(1, 2) === 3', () => { expect(sum(1, 2)).toBe(3); }); }) 登录后复制 babel原理和用途babel 用途
bebel 如何转换的? 对源码字符串 parse 生成 AST,然后对 AST 进行增删改,然后输出目标代码字符串 转换过程
好的,以上就是我对三年前端经验通过面试题做的一个总结了,祝大家早日找到心仪的工作~ 【推荐学习:web前端开发、编程基础视频教程】 |