webpack熟悉吗?webpack打包流程是什么?
webpack打包流程
- 解析配置文件: 读取并解析配置文件,并根据配置生成一个Compiler对象。
- 读取入口文件:根据配置中的入口文件,读取这些文件及其依赖的模块,并将它们组成一个依赖关系图。
- 解析模块依赖:根据模块之间的关系,递归解析它们的依赖,直到所有依赖都被解析完
- 加载模块:根据路径,使用对应的loader加载对应的源代码,并将其转换成Webpack可处理的形式
- 转换代码:根据配置中的插件,对加载的模块金雄一系列的转换操作,比如压缩、合并、优化等
- 生成代码,将所有模块转换后的代码合成一个或者多个文件,并输出到相应的输出目录中。
Webpack声明周期
- 进入编译前:此时会初始化Compiler对象
- 开始编译前:此时会读取入口文件和依赖,并创建依赖关系图
- 开始编译:此时会开始编译入口文件和依赖的模块,并生成输出文件
- 生成文件前:此时可以在插件中处理生成的输出文件
- 完成打包:此时可以在插件中进行一些清理工作
你在公司制定前端规范的时候,都有哪些内容?
- 命名风格
- CSS命名规范
- 选择器命名一律用小写字母,避免使用拼音命名
- 选择器统一采用横线很分隔,禁止使用下换线,分隔符尽量不要超过4个
- JS命名规范
- 代码中命名不能使用 、 , 不能以 、_,不能以 、,不能以、_以及数字结尾
- 代码中命名禁止使用拼音、拼音+英文
- 尽量使用驼峰命名法
- 常量统一使用大写,单词之间用下换线 _ 隔开
- TS接口名统一用大写字母 I 作为前缀
- TS枚举类名称以及其属性名统一使用大写,单词之间用下换线 _ 隔开
- 函数名、方法名、属性名、变量名规范
- 获取单个对象、接口数据,使用get作为前缀
- 获取多个对象使用list作为前缀
- 获取数组使用List作为后缀
- 保存、添加使用save、add作为前缀
- 编辑、更新、删除,移除使用edit、update、delete、remove作为前缀
- 代码格式规范
- CSS代码格式
- 一个样式独占一行,使用tab或者4个空格
- 选择器单独一行,与大括号之间空格,样式属性值与冒号之间空格
- 嵌套层级不能过多,最多不要超过4层
- JS代码格式
- 尽量使用const和let声明变量
- 大括号使用规范,注意前后空格和换行
- 引号使用
- HTML代码中class、id或其他属性都采用双引号
- JS中字符串使用单引号
- 注意使用 === 、== !=、!== 的使用
- 采用4个空格缩进
- 三元表达式在一条表达式中最多出现两次
- CSS代码格式
- 对象处理
- 不要修改标准JS对象的原型
- 不要通过原型向对象添加新的属性或方法,通过封装导出的方式引用
- 使用对象字面量创建
- 使用展开运算符…复制或者合并数组或对象
- 使用解构获取对象的属性值
- 使用数组对象的方法前,如果不确定,则需要先进行类型判断
- 使用JSON.parse前,需要对变量做类型及JSON格式检查
- 使用定时器后,一定要在销毁组件是,进行销毁。
- 注释规约
- 如果用只说明用途或功能,单行注释,如果要对参数进行解释,多行注释
- 修改代码的同时,注释也要进行相应的修改,尤其是参数或返回值
小程序跟H5的区别是什么?
- 运行环境的不同
- 小程序: 小程序的运行环境是基于浏览器内核完全重构的一个内置解析器,针对性做了优化,配合自己定义的开发语言标准,提升了小程序的性能。 脚本内无法使用浏览器中常用的window对象和document对象,从源头上避免了DOM的内存泄漏。
- H5: 无法控制开发人员对DOM的操作,容易出现内存泄漏,在SPA单页应用还存在页面加载慢的问题。
- 开发成本不同
- 小程序: 小程序规范了开发标准,则简单得多。前端常见的HTML、CSS变成了微信自定义的WXML、WXSS,这样避免了框架五花八门,加大了项目接手人员上手维护难度。支付宝小程序可能是AXML、ACSS;头条小程序可能是TTML、TTSS等。
- H5: 涉及开发工具(vscode、Atom等)、前端框架(Angular、react等)、模块管理工具(Webpack 、Browserify 等)、任务管理工具(Grunt、Gulp等),还有UI库选择、接口调用工具(ajax、Fetch Api等)、浏览器兼容性
- 获取系统权限的不同
- 小程序: 更多的系统权限,比如网络通信状态、数据缓存能力等,这些系统级权限都可以和小程序无缝衔接。
- H5: 获取系统权限是大多H5被诟病的地方,这也是H5的大多应用场景被定位在业务逻辑简单、功能单一的原因。
- 运行流畅度的不同
- 小程序: 小程序,它的代码直接在APP上运行,通过浏览器双线程UI渲染和业务逻辑分离等技术,因此,在微信中使用小程序,才会比H5流畅很多,首次打开需要几秒的加载时间外,小程序各个页面的切换、跳转等体验已经媲美原生App,有着同样的柔丝般顺滑的效果。
- H5: 实际上是打开一个网页,而网页需要在浏览器中渲染。所以加载这一过程,会给人明显的「卡顿」感觉,面对复杂的业务逻辑或者丰富的页面交互时尤为明显。
- 运维方式不同
- 小程序: 小程序支持灰度发布、AB测试,在出现异常情况下可以实时在管理界面上操作回退。
- H5: H5如果出现异常问题,需要运维人员在生产环境重新部署回滚系统,要动到生产环境的系统部署,有较大的风险。
- 用户体验不同
- 小程序: 由于微信的关系,小程序近几年大火,用户的接受度和认可度都非常高,而且小程序的体验确实要比h5好很多,小程序下载到本地可以缓存,因此用户体验也更平滑,更关键的是,用户对小程序的收藏等操作会更自然,体验更好,不用担心收藏后的东西不好找等情况。
- H5: H5传统上给人感觉加载始终比较卡,而且H5一旦点击退出去后经常会比较难找到对应入口,特别是链接深度比较深,用户在里面一直点击后,用户一旦退出也很难找到自己关注的东西。
- CSS命名规范
vue3中做了哪些优化?
-
源码优化
- 更好的代码管理方式,代码库中各功能区分得更加细致,责职分工更加明确
- package 是可以独立于 Vue.js 使用的,例如 reactivity 响应式库能力,可以单独依赖这个响应式库而不用依赖整个 Vue.js 减少了引用包的体积大小,在 Vue.js 2.x 是做不到这一点的
- 使用了类型语言 TypeScript 重构项目
-
性能优化
- 移除了一些冷门的内置方法,引入 tree-shaking 的技术(在打包中只会引入使用了的代码,从而减少了打包后代码的体积)
-
数据劫持优化
- 数据响应式处理放弃使用 Object.defineProperty,改用 new Proxy 监听整一个对象。原因是 Object.defineProperty 不管是否遇到有嵌套层级的对象,都需要无脑递归对象,逐层监听对象,对性能损耗过大。而采用 Proxy 后,只有在真正遇到有嵌套层级的对象的时候,才会在 getter 的时候逐层递归,对每一个对象进行监听处理,在很大程度上提升了性能。
-
编译优化
-
petch 函数优化;通过编译阶段对静态模版的分析,编译生成了 Block tree
- Block tree 是一个将模板基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,每个区块只需要以一个 Array 来追踪自身包含的动态节点
- 借助 Block tree,Vue.js 将 vnode 更新性能由与模板整体大小相关提升为与动态内容的数量相关
-
在编译阶段还包含了对 Slot 的编译优化、事件侦听函数的缓存优化,并且在运行时重写了 diff 算法
-
-
语法APi优化:composition api
- Oprion API:当组件小的时候,这种分类方式一目了然;但是在大型项目中,一个组件可能有多个逻辑关注点,每一个关注点都有自己的 Options,当需要修改某一个逻辑关注点时,就需要在单个文件中不断上下切换和寻找
- Composition API:将某个逻辑关注点相关的代码全部放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中翻来覆去
vue2和vue3的响应式有什么区别?
Vue3和 Vue2 对比,Vue2 的响应式存在很多的问题
- 初始化时需要遍历对象所有 key,如果对象层次较深,性能不好
- 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
- 无法监听到数组元素的变化,只能通过劫持重写了几个数组方法
- 动态新增,删除对象属性无法拦截,只能用特定 set/delete API 代替
- 不支持 Map、Set 等数据结构
Vue2 的响应式原理是怎么样的?
- Vue2 的对象数据是通过 Object.defineProperty 对每个属性进行监听,当对属性进行读取的时候,就会触发 getter,对属性进行设置的时候,就会触发 setter
```javascript/*** 这里的函数 defineReactive 用来对 Object.defineProperty 进行封装。**/function defineReactive(data, key, val) {// 依赖存储的地方const dep = new Dep()Object.defineProperty(data, key, {enumerable: true,configurable: true,get: function () {// 读取数据的时触发,在 getter 中收集依赖dep.depend()// 返回结果return val},set: function(newVal) {// 修改数据时触发,更新数据,并通知dep,更新储存,触发watcher更新val = newVal// 在 setter 中触发依赖dep.notify()}}) }
```
1. 在Watcher里面进行属性读取,读取的时候会把自己设置到一个全局变量中
2. 在Watcher中读取属性会触发getter,在getter里面进行依赖收集,依赖储存的地方叫Dep,在Dep里面就可以把全局变量中的依赖进行收集,,收集完毕就会把全局依赖变量置空,将来数据发生变化的时候,就去Dep中把相关的Watcher拿出来执行一遍
// Watcher就是所谓的依赖class Watcher {constructor(vm, exp, cb) {this.vm = vmthis.getter = expthis.cb = cbthis.value = this.get()}get() {// 读取时,依赖收集,并储存在Dep中Dep.target = thislet value = this.getter.call(this.vm, this.vm)// 收集完毕,置空全局依赖变量Dep.target = undefined// 将最新的结果返回return value}update() {// 数据变化,重走一遍watcher,触发getconst oldValue = this.valuethis.value = this.get()this.cb.call(this.vm, this.value, oldValue)}}
-
- 总结:
- Object.defineProperty监听对象的每一个属性,当读取数据时会触发getter,修改数据时会触发setter,
- 在改变数据setter时,调用dep中的notify方法,notify会遍历依赖后调用watcher依赖器中update方法更新,update中会调用get方法重新收集依赖,将结果在dep中储存,并将结果返回。
- Dep是依赖的储存器,负责添加、删除相关的依赖和通知相关依赖进行相关的操作
- Watcher是依赖器,只有watcher中的get触发才会收集,然后调用Dep进行添加、删除依赖操作
- 由于Object.defineProperty无法监听对象的变化,所以Vue2中设置了一个Observer类来管理对象的响应式依赖,同时会递归检测对象中子数据的变化
为什么 Vue2 新增响应式属性要通过额外的 API,$set?
-
这是因为Object.defineProperty只会对属性进行监测,而不能对对象镜像监测,为了可以监测对象Vue2创建了一个Observer类。
-
Observer类的作用就是把一个对象全部转换成响应式对象,包括子属性,当对象新增或者删除属性的是否负责同时对应的Watcher进行更新操作
// $set操作 function set(target, key, val) {const ob = target.__ob__// 响应式赋值defineReactive(ob.value, key, val)// 通知dep,等同于修改数据,触发setter操作ob.dep.notify()return val }
Object.defineProperty 真的不能监听数组的变化吗?
-
Object.defineProperty是可以监听数组的变化,但是它是通过下标,实际开发基本很少这样处理数据,更多的是通过内置方法,push、pop、shift、unshift等,这些方法是监听不到的,所以放弃了Object.defineProperty对数组进行监听,采用通过对数组原型上的方法进行重写监听,本质还是使用原生Array原型上的方法去操作数组,只不过是做了一些逻辑处理数据。
// 拦截器其实就是一个和 Array.prototype 一样的对象。 const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) ;['push','pop','shift','unshift','splice','sort','reverse' ].forEach(function (method) {// 缓存原始方法const original = arrayProto[method]Object.defineProperty(arrayMethods, method, {value: function mutator(...args) {// 最终还是使用原生的 Array 原型方法去操作数组return original.apply(this, args)},eumerable: false,writable: false,configurable: true}) })
- 由于Vue2放弃了Object.defineProperty对数组进行监听的方法,所以通过下标操作数组是无法实现响应式操作,如:this.list.length = 0
Vue3 的响应式原理是怎么样的?
-
Vue3是通过Proxy对数据事项getter/setter代理,从而实现响应式数据.
-
在副作用函数中读取响应式数据的时候,就会触发Proxy的getter
-
在getter里面把当前副作用函数保存起来,将来对应响应式数据发生变化时,把之前保存起来的副作用函数取出来执行。
// 使用一个全局变量存储被注册的副作用函数 let activeEffect // 注册副作用函数 function effect(fn) {activeEffect = fnfn() } const obj = new Proxy(data, {// getter 拦截读取操作get(target, key) {// 将副作用函数 activeEffect 添加到存储副作用函数的全局变量 targetMap 中track(target, key)// 返回读取的属性值return Reflect.get(target, key)},// setter 拦截设置操作set(target, key, val) {// 设置属性值const result = Reflect.set(target, key, val)// 把之前存储的副作用函数取出来并执行trigger(target, key)return result} }) // 存储副作用函数的全局变量 const targetMap = new WeakMap() // 在 getter 拦截器内追踪依赖的变化 function track(target, key) {// 没有 activeEffect,直接返回if(!activeEffect) return// 根据 target 从全局变量 targetMap 中获取 depsMaplet depsMap = targetMap.get(target)if(!depsMap) {// 如果 depsMap 不存,那么需要新建一个 Map 并且与 target 关联depsMap = new Map()targetMap.set(target, depsMap)}// 再根据 key 从 depsMap 中取得 deps, deps 里面存储的是所有与当前 key 相关联的副作用函数let deps = depsMap.get(key)if(!deps) {// 如果 deps 不存在,那么需要新建一个 Set 并且与 key 关联deps = new Set()depsMap.set(key, deps)}// 将当前的活动的副作用函数保存起来deps.add(activeEffect) } // 在 setter 拦截器中触发相关依赖 function trgger(target, key) {// 根据 target 从全局变量 targetMap 中取出 depsMapconst depsMap = targetMap.get(target)if(!depsMap) return// 根据 key 取出相关联的所有副作用函数const effects = depsMap.get(key)// 执行所有的副作用函数effects && effects.forEach(fn => fn()) }
- Vue3 中依赖收集的规则,首先把响应式对象作为 key,一个 Map 的实例做为值方式存储在一个 WeakMap 的实例中,其中这个 Map 的实例又是以响应式对象的 key 作为 key, 值为一个 Set 的实例为值。而且这个 Set 的实例中存储的则是跟那个响应式对象 key 相关的副作用函数。
- 副作用函数使用Set类型,是因为Set类型能自动去除重复内容
- 对简单类型数据,Vue3通过对其做了一层包裹的方式来实现对原始值变成响应式数据的,因为简单类型数据是按值传递,如函数中形参是简单类型数据,但两者没有引用关系,改变形参无法影响到实参,无法实现响应式。
- 因此 ref 本质是一个实例化的包裹对象,来处理对简单类型数据的代理,使用一层对象为包裹,将简单类型数据放对象里面作为value,监听对象,即可间接响应式对简单类型数据的响应式方案。
Vue3 中是怎么监测数组的变化?
- vue2是不可以通过下标对响应式数组进行设置和读取,但Vue3是可以的
- vue3中也需要对数组原型上方法进行重写,数组响应式对象使用includes、indexOf、lastIndexOf方法重写,因为它们内部的 this 指向的是代理对象,并且在获取数组元素时得到的值要也是代理对象,所以当使用原始值去数组响应式对象中查找的时候,如果不进行特别的处理,是查找不到的,所以我们需要对上述的数组方法进行重写才能解决这个问题。
Vue2和Vue3的总结
- Vue2
- 是通过 Object.defineProperty 将对象的属性转换成 getter/setter 的形式来进行监听它们的变化,当读取属性值的时候会触发 getter 进行依赖收集,当设置对象属性值的时候会触发 setter 进行向相关依赖发送通知,从而进行相关操作
- 由于 Object.defineProperty 只对属性 key 进行监听,无法对引用对象进行监听,所以在 Vue2 中创建一个了 Observer 类对整个对象的依赖进行管理,当对响应式对象进行新增或者删除则由响应式对象中的 dep 通知相关依赖进行更新操作。
- Object.defineProperty 也可以实现对数组的监听的,但因为性能的原因 Vue2 放弃了这种方案,改由重写数组原型对象上的 7 个能操作数组内容的变更的方法,从而实现对数组的响应式监听。
- Vue3
- Vue3 则是通过 Proxy 对数据实现 getter/setter 代理,从而实现响应式数据,然后在副作用函数中读取响应式数据的时候,就会触发 Proxy 的 getter,在 getter 里面把对当前的副作用函数保存起来,将来对应响应式数据发生更改的话,则把之前保存起来的副作用函数取出来执行。
- Vue3 对数组实现代理时,用于代理普通对象的大部分代码可以继续使用,但由于对数组的操作与对普通对象的操作存在很多的不同,那么也需要对这些不同的操作实现正确的响应式联系或触发响应。这就需要对数组原型上的一些方法进行重写。
- 比如通过索引为数组设置新的元素,可能会隐式地修改数组的 length 属性的值。同时如果修改数组的 length 属性的值,也可能会间接影响数组中的已有元素。另外用户通过 includes、indexOf 以及 lastIndexOf 等对数组元素进行查找时,可能是使用代理对象进行查找,也有可能使用原始值进行查找,所以我们就需要重写这些数组的查找方法,从而实现用户的需求。原理很简单,当用户使用这些方法查找元素时,先去响应式对象中查找,如果没找到,则再去原始值中查找。
- 另外如果使用 push、pop、shift、unshift、splice 这些方法操作响应式数组对象时会间接读取和设置数组的 length 属性,所以我们也需要对这些数组的原型方法进行重新,让当使用这些方法间接读取 length 属性时禁止进行依赖追踪,这样就可以断开 length 属性与副作用函数之间的响应式联系了。
vue3中的watchEffect和watch
在Vue的Composition API中,watch和watchEffect都是用于监听响应式数据变化的函数
- watch 用于监听一个或者多个响应式数据或计算属性,并在它们改变时执行一个函数
- immediate 是否立即执行
- deep 是否深度监听,针对对象和数组
- flush 控制回调何时执行(pre、post、 sync)
- watchEffect 用于立即执行函数,并监听该函数内部所有引用的响应数据或计算属性
主要区别
- 自动监听依赖 与 显示声明
- watchEffect是自动监听函数内所有引用的响应式,而watch需要指明监听的引用数据
- 立即执行
- watchEffect是立即执行一次,而watch需要设置immediate选项
- 旧值与新值
- watch回调提供旧值和新值,而watchEffect不提供回调参数
- 多目标监听
- watch 可以观察多个目标,但watchEffect 监听函数内的所有响应式引用
使用场景
- watch
- 需要使用旧值和新值
- 需要基于条件监听处理逻辑
- 需要可控配置
- watchEffect
- 需要多个依赖响应式来触发影响同一个目标的逻辑
- 只关心最新的值
interface和type的区别是什么?
类型别名 type
type不仅可以用来表示基本类型,还可以用来表示对象类型、联合类型、元组和交集
type userName = string;
// 联合类型
type userId = string | number;
// 元组
type Data = [number, string];
type Person = {name: string;age: number
}
接口 interface
interface 仅限描述对象类型
interface Person {name: string;age: number;}
- 都可以描述对象和函数,但语法不同
// typetype Ipoint = {x: number;y: number;}type SetPoint = (x: number, y: number) => void;// interfaceinterface IPoint {x: number;y: number;}interface SetPoint {(x: number, y: number): void;}
- 两者都可以被继承,混合继承
interface Person {name: string;}interface Student extends Person { id: number; };type Animal = {name: string;}type Cart = Animal & { behavior: leg; };// 混合继承type Student1 = Person & { id: number; };interface Cart1 extends Animal {say: string;}
- 声明合并
- interface 同名声明合并叠加
- type 不可能同名,会报错
vite、webpack、roolup的区别是什么
Webpack介绍
- 热更新方面:Webpack支持HMR,当Webpack需要全部冲洗编译并更新,效率较低。
- tree-shaking:webpack2开始支持,webpack5支持的更好
- 分包方面:Webpack支持代码切割
- ESM打包:现在webpack支持es6module输出
Vite介绍
- 两部分组成
- 一个开发服务器,它基于原生ES模块提供了丰富的内建功能
- 一套构建指令,它是用Rollup打包,支持预配置,可以输出用于生产环境的优化过的静态资源
- 主要特点
- 快速的冷启动:直接开启开发服务器,不需要进行打包操作,不需要分类模块依赖和编译,因此启动速度非常快
- 即时的模块热更新
- 真正的按需编译:利用现代浏览器支持ES Module的特性,当浏览器请求某个模块的时候,根据需要对模块的内容进行编译,加载请求,这种方式大大缩短了编译时间。
- 优点
- vite热更新,实现按需加载,按模块加载,速度快
- vite利用HTTP头来加速整个页面的重新加载,源代码模块的请求会根据304进行协商缓存,依赖模块请求这是通过Cache-Control进行强缓存,因此一旦被缓存它们将不需要再次请求
- 热更新原理:在热模块HMR方面,当修改一个模块的时候,仅让浏览器重新请求该模块即可,无须像webpack那样需要把该模块的相关依赖模块全部编译一次,效率更高。
- vite在生产环境通过Rollup进行打包(特点:打包体积小),生成esm模块包(特点:快)
- vite在开发环境时,基于浏览器支持esm,让浏览器解析模块,然后服务器按需编译返回。同时基于esbuild(go)进行预构建打包不常变动的第三包,并用进行缓存。(缓存+快)
- Vite 使用 esbuild 预构建依赖。Esbuild 使用 Go 编写,所以比以 Node.js 编写的打包器预构建依赖快 10-100 倍。
- vite热更新,实现按需加载,按模块加载,速度快
- 缺点
- 生态:生态不如webpack,wepback在于loader和plugin非常丰富
- prod环境的构建:目前用的Rollup,原因在于esbuild对于css和代码分割不是很友好
- 还没有被大规模使用,很多问题或者诉求没有真正暴露出来
Rollup介绍
-
优点:
- Rollup 是一款 ES Modules 打包器,从作用上来看,Rollup 与 Webpack 非常类似。不过相比于 Webpack,Rollup要小巧的多,打包生成的文件更小。(识别commonJs需要插件)
- 热更新:Rollup不支持HMR,在对js以外的模块的支持上不如webpack,但是如果是打包纯js库例如react,前期的vue的话,使用rollup是很合适的,打包的产物比较干净,没有webpack那么多工具函数
- Rollup 的插件机制设计得相对更干净简洁,单个模块的 resolve / load / transform 跟打包环节完全解耦,所以 Vite 才能在开发时模拟 Rollup 的插件机制,并且兼容大部分 Rollup 插件
- rollup原生支持tree-shaking
-
缺点:
- 加载其他类型的资源文件或者支持导入 CommonJS 模块,又或是编译 ES 新特性,这些额外的需求 Rollup需要使用插件去完成
- rollup并不适合开发应用使用,因为需要使用第三方模块,而目前第三方模块大多数使用CommonJs方式导出成员,并且rollup不支持HMR,使开发效率降低
结论:
Rollup更适合打包库,webpack更适合打包项目应用,vite基于rollup实现了热更新也适合打包项目。