2021年面试复习题(上 - 基础知识篇)

1,116 阅读16分钟

零。简介

自我介绍

  • 负责过什么项目,以及在每个项目中的职责?
  • 做过的所有项目,遇到过什么有难度的问题?如何解决??

壹。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 finallyprocess.nextTickMutationObserver
  • 宏任务:setTimeoutsetIntervalsetImmediateI/OrequestAnimationFrame

事件的【捕获、冒泡、委托】

  • 捕获: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 事件回调。这里没有$el
  • beforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用。
  • mounted:el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
  • beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
  • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
  • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

Vue.js组件的生命周期

Vue组件渲染和更新的过程

  • 渲染组件时,会通过Vue.extend方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()进行挂载。
  • 更新组件时,会进行patchVnode流程。核心就是diff算法

Vue.js组件渲染和更新过程

Diff更新的过程

  • 响应式数据发生更新
  • setter 拦截更新操作
  • dep 通知 watcher 执行 update 方法
  • 执行 updateComponent 方法更新组件
  • 执行 render 函数生成新的 vnode
  • vnode 传递给 vm._update 方法
  • 调用 patch 方法
  • 执行 patchVnode 进行 Domdiff 更新操作
    • 新老节点都存在、新节点不是文本节点、新老节点都有孩子节点,才执行 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被调用时vm.el 替换,若根实例挂载到了文档上的元素上,当mounted被调用时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利用isSameNodekey值判断是否是同一节点;但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配置,避免重复编译
  • 使用UglifyJsTerser压缩代码
  • 前端性能优化——webpack篇
  • 使用 Happypackloader 由单进程转为多进程
  • 使用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。主要包括expirescache-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:如果缓存的内容失效,请求必须发送到服务器/代理以进行重新验证
  • 协商缓存:去服务器比对,若没改变才直接读取本地缓存,返回的状态码是304。主要包括last-modifiedEtag
    • (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最大54pxamfe-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的实例
  • 坑3:通信问题(可以通过回调函数通信、也可以通过vuex通信,但子应用的computed不是响应式的,因为不是一个vuex实例,需要子应用在接受store的时候手动监听)

玖。前端工程化

开发流程与规范

  • 需求评审会,产品出原型
    • 此时研发可以搭架子了,但要确定好需求和原型定稿
    • 后端也开始写接口,但没出来前自己mock一份
  • UI出设计稿,对比设计稿进行初版调试(主要还是保证功能的前提下再调交互)
  • 进行接口联调,完善业务逻辑及页面展示
  • 自测的过程中,对比UI稿进行像素级还原
  • 产品验收通过,进行提测并配合测试解决问题
  • 运维配置cicd与docker,进行自动部署
  • (后期出现问题再重新进行需求评审)

设计模式

  • 单一职责原则
  • 接口隔离原则
  • 依赖倒置原则
  • 里氏替换原则
  • 开闭原则
  • 迪米特法则

喜欢的前端书籍有哪些?

  • 《你不知道的JavaScript》上中下
  • 《编写可维护的JavaScript》
  • 《CSS权威指南》
  • 《深入浅出webpack》
  • 《啊哈算法》
  • 《图解HTTP》
  • 《算法(第4版)》
  • 《重构:改善既有代码的设计》