react和vue2框架复习,常见面试问题

151 阅读31分钟

综合

MVVM

视图模型双向绑定,是Model-View-ViewModel的缩写。Model层代表数据模型,View代表UI组件,ViewModelViewModel层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图

优点

  1. 低耦合。视图(View)可以独立于Model变化和修改,一个Model可以绑定到不同的View上。
  2. 可重用性。可以把一些试图逻辑放在一个model中,让不同的view重用这段试图逻辑。
  3. 可测试

事件委派

可以通过给父元素绑定事件委托,指定一个事件处理程序,就可以管理某一类型的所有事件。注册最少的监听器,能大大的减少与 dom 的交互次数,提高性能。

事件监听

事件监听主要用到了addEventListener这个函数,事件监听和事件绑定的最大区别就是事件监听可以给一个事件监听多个函数操作,而事件绑定只有一次

react和Vue

  • 相同:支持 Virtual DOM。核心库、路由和状态管理都是分开的。
  • 不同:渲染: React 通过 shouldComponentUpdate / setState,手动决定是否渲染来优。Vue 自动追踪组件依赖,精确渲染状态改变的组件。

观察者模式和发布/订阅模式

观察者模式  当对象之间存在一对多的依赖关系时,其中目标对象subject的状态发生改变,所有依赖它的观察者observer都会收到通知,这就是观察者模式。

发布/订阅模式 使⽤⼀个事件通道,这个通道介于订阅者和发布者之间,发布者发布事件到调度中心(就是该事件被触发),再由调度中心统一调度订阅者注册到调度中心的处理代码。采⽤事件通道可以避免发布者和订阅者之间产⽣依赖关系

  • 观察者模式比较适合在单个应用内部使用,发布订阅模式则是比较适合跨应用的场景。

SPA单页面应用

  • SPA(single-page application)仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
  • 优点
    • 1.用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
    • 2.基于上面一点,SPA 相对对服务器压力小;
    • 3.前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
  • 缺点
    • 1.初次加载耗时多
    • 2.前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
    • 3.SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。

Vue

diff算法

diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率。新旧虚拟DOM对比的时候,Diff算法比较只会在同层级进行, 不会跨层级比较。 所以Diff算法是:深度优先算法。 时间复杂度:O(n)。当数据改变时,会触发setter,并且通过Dep.notify去通知所有订阅者Watcher,订阅者们就会调用patch方法,给真实DOM打补丁,更新相应的视图

  • vue中的节点对比采用双指针,从两端向中间遍历,当指针交叉的时候,就是对比完成了
  • 开始遍历时,首先依次进行头头、尾尾、头尾、尾头对比,这也是vue中diff算法 的一个优化点

v-if和v-show的区别

v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。

所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。

Vue底层实现原理/响应原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

Observer(监听器)对数据对象进行遍历,利用Object.defineProperty()对属性加上了settergetter。给对象某个属性幅值时,就会触发setter,这时候Observer就监听到了数据的变化,就要通知Watcher(订阅者)

Watcher(订阅者)作为Observer和Compile之间通信的桥梁,主要工作就是订阅Observer中的属性值变化信息,当收到的属性值变化的消息时,触发Compile(解析器)中对应的回调函数。

Compile(解析器)主要工作就是解析Vue模板指令,将模板中的变量都替换成数据,然后初始渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

Dep(订阅器)对observer和watcher进行统一管理。

Vue生命周期

image.png

通信方式

1.props/$on+$emit 适用于父子组件通信 2. parent/parent/children 访问父/子实例 3. 全局事件总线Event-bus和 4. on+on+emit 适用于任意组件间通信 5. 插槽slot,父子组件间通信 6. 消息发布与订阅subscrib/publish,任意组件间通信 7. Vuex跨级组件间通信

watch和computed区别

computed:计算属性基于data中声明过或者父组件传递的props中的数据通过计算得到一个新值,新值会根据已知值的变化而变化。computed默认会缓存计算结果,在重复的调用中只要依赖数据不变,直接取缓存中的计算结果;依赖的数据发生改变,computed才会重新计算。所以computed更高效,优先使用。

watch:主要用来监听某些特定数据的变化,可以支持异步操作,不支持缓存,监听的数据发生改变,直接触发相应的操作。监听函数有两个参数,一个是最新的值,一个是上一次的旧值。

使用场景:computed:当一个属性受多个属性影响时候使用,例如购物车结算功能。watch:当一条数据影响多条数据的时候使用,例如监控路由。

route路由

有hash和history两种模式,本质都是通过监听url的变化来实现路由的跳转。hash通过监测#号后的哈希值,通过hashChange事件监听,跳转。history通过history.pushState()history.replaceState()来修改url实现路由的跳转。hash虽然在URL中,但是发送网络请求时只发送#之前的URL(后面的内容实际上是浏览器内部的锚点标识,用于指定当前页面的具体位置,而不是用于向服务器传递参数)。因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误。而history模式下,前端的URL必须和实际向后端发起请求的URL一致,如果后端没有做到对路由的全覆盖,就会发送404错误。node可以配置插件:connect-history-api-fallback解决404问题。 image.png

React

为什么要有合成事件

  1. 磨平了浏览器的兼容性差异
  2. React通过顶层监听的形式,通过事件委派的方式来统一管理所有事件,可以在事件上区分事件优先级,优化用户体验。

生命周期

旧的

image.png

  • 挂载阶段: 由ReactDOM.render()触发--初次渲染

1.constructor()-通常在构造函数里初始化state对象或者给自定义方法绑定this。

2.componentWillMount()-一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息。

3.render()

4.componentDidMount()

  • 更新阶段: 由组件内部this.setState()或父组件重新render触发

1.componentWillReceiveProps()- 对 props 发生改变的监听

2.shouldComponentUpdate()- 是控制组件更新的阀门,返回布尔值,默认true

3.componentWillUpdate()- 在 Dom 发生更新之前的事情,如获得 Dom 更新前某些元素的坐标、大小等。

  •    **一直到这里 this.props 和 this.state 都还未发生更新**
    

4.render()

5.componentDidUpdate()

  • 卸载阶段: 由ReactDOM.unmountComponentAtNode()触发

componentWillUnmount()-在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount 中创建的订阅等。

新的(React16之后)

  • 挂载阶段:
  1. constructor: 构造函数,最先被执行,
  2. getDerivedStateFromProps(nextProps, prevState),这是个静态方法,当我们接收到新的属性想去修改我们state。在组件实例化、接收到新的 props 、组件状态更新时会被调用。
  3. render
  4. componentDidMount: 组件装载之后调用,此时我们可以获取到DOM节点并操作,比如对canvas,svg的操作,服务器请求,订阅都可以写在这个里面,但是记得在componentWillUnmount中取消订阅。
  • 更新阶段:
  1. getDerivedStateFromProps
  2. shouldComponentUpdate(nextProps, nextState),有两个参数表示新的属性和变化之后的state,我们通常利用此生命周期来优化React程序性能。
  3. render
  4. getSnapshotBeforeUpdate(prevProps, prevState),有两个参数表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用
  5. componentDidUpdate(prevProps, prevState, snapshot),有三个参数表示之前的props,之前的state,和snapshot。
  • 卸载阶段:
  1. componentWillUnmount

setState是同步还是异步

setState本身代码的执行是同步的,为了避免不必要的重新渲染、提升性能,react 提出分批更新的机制,因此出现了异步的情况。异步是指多个state会合到一起批量更新。合成事件和生命周期钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。

useState重复修改多次,会渲染多次吗?什么实际触发渲染?

如果useState在同步代码中重复修改多次,只会渲染一次。出于性能考虑,React会把多个setState调用合并成一个调用,这也是state的批量更新。React会将state渲染到浏览器事件结束的时候,再统一进行更新,才会触发渲染。

如果setState在异步代码中重复修改多次,渲染会在每次修改后触发一次。

使用useState无法及时更新页面,是什么原因,怎么解决?

原因:1.批更新策略2.浅比较:React 在更新组件时默认进行浅比较,如果新旧 state 状态相同,则认为组件不需要重新渲染,这可能导致视图没有及时更新。

解决:1.使用useEffect,并精确配置依赖项。2.设置key属性,避免react认为是相同元素。3.使用useRef

react中class组件与函数式组件

官方推荐使用函数式组件,而不是类。因为hooks更简洁,代码量少,用起来比较"轻",而类比较"重"。类组件的根基是面向对象编程,所以它有继承、有属性、有内部状态的管理。函数式组件是函数式编程,更符合react函数式的本质。Hooks 的本质:一套能够使函数组件更强大、更灵活的“钩子”。

组件间通信方式

  • 父组件给子组件:父组件通过 props 向子组件传递需要的信息。
  • 子组件给父组件:props+回调的方式,子组件调用绑定在父组件上的函数,将想要传递的信息作为参数,传递给父组件。
  • 跨级组件通信:使用props,利用中间组件层层传递/使用context。
  • 兄弟组件以及不在同一个父级中的非兄弟组件:消息订阅与发布使用pubsub-js、event等。 可以通过redux等进行全局状态管理。

react为什么不能在循环、条件、嵌套语句中写hooks

  • react用链表来严格保证hooks的顺序。hook 相关的所有信息收敛在一个 hook 对象里,而 hook 对象之间以单向链表的形式相互串联。
  • 以useState为例,在它的实现过程之有mountupdate两个阶段
    • mountState(首次渲染)阶段会按useState声明的顺序构建出一个链表并渲染;
    • updateState 阶段会按顺序去遍历之前构建好的链表,取出对应的数据信息进行渲染。hooks 的渲染是通过“依次遍历”来定位每个 hooks 内容的。如果前后两次读到的链表在顺序上出现差异,那么渲染的结果自然是不可控的。
  • 必须按照顺序调用从根本上来说是因为 useState 这个钩子,你每生成一个新的状态,React 并不知道这个状态名字叫啥,所以需要通过顺序来索引到对应的状态值

useEffect依赖为空数组和componentDidMount和useLayoutEffect有什么区别

对于 React 的函数组件来说,其更新过程大致分为以下步骤:

  1. 因为某个事件 state 发生变化。
  2. React 内部更新 state 变量。
  3. React 处理更新组件中 return 出来的 DOM 节点(进行一系列 dom diff 、调度等流程)。
  4. 将更新过后的 DOM 数据绘制到浏览器中。
  5. 用户看到新的页面。

useEffect 在第 4 步之后执行,且是异步的,保证了不会阻塞浏览器进程。 useLayoutEffect 在第 3 步至第 4 步之间执行,且是同步代码,所以会阻塞后面代码的执行。

render 执行之后,componentDidMount 会执行,如果在这个生命周期中再一次 setState ,会导致再次 render ,返回了新的值,浏览器只会渲染第二次 render 返回的值,这样可以避免闪屏(官网:可以在 componentDidMount() 里直接调用 setState() 。它将触发额外渲染,但此渲染会发生在浏览器更新屏幕之前。如此保证了即使在 render() 两次调用的情况下,用户也不会看到中间状态。)。但是 useEffect 是在真实的 DOM 渲染之后才会去执行,这会造成两次 render ,有可能会闪屏。实际上 useLayoutEffect 会更接近 componentDidMount 的表现,它们都同步执行且会阻碍真实的 DOM 渲染的。

react-fiber

  • 在 React 15 版本的时候,我们如果有组件需要更新的话,那么就会递归向下遍历整个虚拟 DOM 树来判断需要更新的地方。这种递归的方式 弊端 在于无法中断,必须更新完所有组件才会停止。这样的弊端会造成如果我们需要更新一些庞大的组件,那么在更新的过程中可能就会长时间阻塞主线程,从而造成用户的交互、动画的更新等等都不能及时响应。
  • React 的组件更新过程简而言之就是在持续调用函数的一个过程,这样的一个过程会形成一个虚拟的调用栈。假如我们控制这个调用栈的执行,把整个更新任务拆解开来,尽可能地将更新任务放到浏览器空闲的时候去执行,那么就能解决以上的问题。
  • Fiber 重新实现了 React 的核心算法,带来了杀手锏增量更新功能。它有能力将整个更新任务拆分为一个个小的任务,并且能控制这些任务的执行。

更新过程可控实现原理

主要由三个大部分完成--任务拆分,任务挂起、恢复、终止,任务具备优先级。

任务拆分:

在 React Fiber 机制中,它采用"化整为零"的思想,将调和阶段(Reconciler)递归遍历 VDOM 这个大任务分成若干小任务,每个任务只负责一个节点的处理。

任务挂起、恢复、终止:

  • workInProgress tree代表当前正在执行更新的 Fiber 树。在render或者setState后,会构建一颗 Fiber 树,也就是 workInProgress tree,这棵树在构建每一个节点的时候会收集当前节点的副作用,整棵树构建完成后,会形成一条完整的副作用链。
  • currentFiber tree表示上次渲染构建的 Filber 树。在每一次更新完成后 workInProgress 会赋值给 currentFiber。
  • 在新 workInProgress tree 的创建过程中,会同 currentFiber 的对应节点进行 Diff 比较,减少新创建对象带来的开销。也就是说无论是创建还是更新、挂起、恢复以及终止操作都是发生在 workInProgress tree 创建过程中的。workInProgress tree 构建过程其实就是循环的执行任务和创建下一个任务。
挂起

当第一个小任务完成后,先判断这一帧是否还有空闲时间(使用requestIdleCallback来检查是否还有剩余的帧时间),没有就挂起下一个任务的执行,记住当前挂起的节点,让出控制权给浏览器执行更高优先级的任务。

恢复

在浏览器渲染完一帧后,判断当前帧是否有剩余时间,如果有就恢复执行之前挂起的任务。如果没有任务需要处理,代表调和阶段完成,可以开始进入渲染阶段。

如何判断一帧是否有空闲时间的呢?-->使用前面提到的 RIC (RequestIdleCallback) 浏览器原生 API,React 源码中为了兼容低版本的浏览器,对该方法进行了 Polyfill。

终止

当在调和过程中触发了新的更新,在执行下一个任务的时候,判断是否有优先级更高的执行任务,如果有就终止原来将要执行的任务,开始新的 workInProgressFiber 树构建过程,开始新的更新流程。这样可以避免重复更新操作。这也是在 React 16 以后生命周期函数 componentWillMount 有可能会执行多次的原因。

具备优先级

在创建或者更新 FiberNode 的时候,通过算法给每个任务分配了任务的优先级。

组件什么时候会重新渲染(re-render)

  1. 当本身的 props 或 state 改变时。
  2. Context value 改变时,使用该值的组件会 re-render。
  3. 当父组件重新渲染时,它所有的子组件都会 re-render,形成一条 re-render 链。

常用hook有哪些

useState/useEffect/useContext/useCallback/useRef/useLayoutEffect

useCallBack()性能优化

  1. 当需要将一个函数传递给子组件时,最好使用useCallback进行优化,将优化后的函数传递给子组件,子组件避免了不必要的更新(依赖的props不变,子组件就不会更新)
  2. 进一步优化--使用useRef()解决闭包陷阱问题。useRef返回一个ref对象,返回的ref对象在组建的整个生命周期保持不变。 image.png

React.memo()和useMemo()

  • React.memo()是一个高阶组件,可以用它来包装我们不想重新渲染的组件,除非其中的props发生变化。 -useMemo()是一个Hook,我们可以用它在组件中包装函数。用它来确保该函数中的值仅在其依赖项之一发生变化时才重新计算。

useMemo()和useCallBack()

useCallBack()是看依赖有没有改变,判断需不需要把一个新的fn()返回出去(对里面包裹的函数进行优化)。useMemo()是对它的返回结果进行优化。 image.png

useMemo()使用的案例

  1. 进行大量的计算操作,是否有必要每次渲染时都重新计算
  2. 对子组件传递相同内容的对象时,使用useMemo进行性能优化

闭包陷阱

像useEffect(),useMemo()之类的有个参数是需要写它依赖的数组的hooks,如果没有准确的填写他依赖的数组,那么里面的使用的memoizedState属性就是渲染时的属性,而不是最新的,这就是闭包陷阱。

解决方法:

  • 在依赖数组中写上依赖的参数
  • 使用useRef(),每次渲染时,useRef都是返回的同一个对象(内存空间不变)。

解决过期闭包

在setState函数中,当入参是一个函数的时候,React会保证传入一个入参,入参是state上一次的取值,setState会返回下一个最新的取值。

获取DOM元素的方法

  1. 设置className类名,在useEffect里document.querySelector(".类"),都是挂在完毕后执行(不会阻塞DOM更新),所以能取到DOM元素。(不推荐)
  2. 在组件身上绑定ref={xxx},使用const xxx = uesRef(),xxx.current获取DOM元素。(xxx是一个对象,要使用他的current属性)

redux

中间件

Redux整个工作流程是:当action发出之后,reducer立即算出state,整个过程是一个同步的操作。那么如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件。Redux中,中间件就是放在就是在dispatch过程,在分发action进行拦截处理。redux-thunk:用于异步操作。redux-logger:用于日志记录。(dispatch<-再次派发action,getState<-获取依赖的状态)

工作流程

  1. 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。
  2. 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
  3. State一旦有变化,Store就会调用监听函数,来更新View。

redux中的优化

使用useSelector时,第二个参数写上shallowEqual,进行浅对比(使用Object.is()对基本类型做一个精确比较;过滤掉基本类型,两个对象进行比较,先拿出key值,对key的长度进行对比,不相等直接返回FALSE,相等则使用hasOwnProperty方法进行一层的比较)

在React中可以做哪些优化

避免不必要的渲染:对于函数组件可以使用React.memo包装。useCallBack/usememo进行优化。React.lazy和React.Suspense延迟加载不是立即需要的组件。尽量使用CSS而不是强制加载和卸载组件。使用React.Fragment避免添加额外的DOM。

React Jsx转换成真实DOM过程?

通过React.creatElement转换成虚拟DOM,再通过ReactDOM.render转换成真实DOM,更新时通过diff算法进行高效更新。

react diff算法

React 不能通过双端对比进行 Diff 算法优化是因为目前 Fiber 上没有设置反向链表

tree diff

diff 算法只会对相同层级的 DOM 节点进行比较。如果发现节点不存在,那么会将该节点以及其子节点完全删除,不会再继续比较。

component diff

如果是同一类型的组件,那么会继续对比 VM 数。如果不是同一类型的组件,那么会将其和其子节点完全替换,不会再进行比对同一类型的组件,有可能 VM 没有任何的变化,如果可以确定的知道这点,那么就可以节省大量的 diff 时间,所以用户可以设置 shouldComponentUpdate() 来判断是否需要进行 diff 算法。

element diff

当节点处于同一层级的时候时,有三种操作:插入、移动、删除 这里 React 有一个优化策略,对于同一层级的同组子节点,添加唯一的 key 进行区分。这样的话,就可以判断出来是否是移动节点。通过 key 发现新旧集合中的节点都是相同的节点,就只需要进行移动操作就可以。

组件

受控组件和非受控组件

受控组件是在 React 中处理输入表单的一种技术。在受控组件表单中,数据由React组件处理。非受控组件的方法可以通过使用Ref来处理表单数据。在非受控组件中,Ref用于直接从DOM访问表单值,而不是事件处理程序,控制能力较弱。

高阶组件

高阶组件是将组件作为参数并生成另一个组件的组件。 Redux connect是高阶组件的示例。

函数

纯函数

纯函数是始终接受一个或多个参数并计算参数并返回数据或函数的函数。 它没有副作用,例如设置全局状态,更改应用程序状态,它总是将参数视为不可变数据。

高阶函数

Array.map,Array.filter和Array.reduce是高阶函数,因为他们将函数作为参数。

react路由懒加载的实现原理

React.lazy()接受一个函数作为参数,这个函数需要调用import()来异步加载组件,它返回一个PromiseReact.lazy()需要配合Suspense组件一起使用,在Suspense组件中渲染React.lazy异步加载的组件。用Suspense的loading代替了多个异步加载的子组件的loading,避免了页面上出现多个loading的问题。当Suspense的状态为 Pending 时显示的是 Suspense 中 fallback 的内容,只有状态变为 resolve 后才显示组件。

Webpack

打包流程/原理

webpack的运行流程是一个串行过程,依次执行:

  1. 读取webpack的配置参数
  2. 启动webpack,创建Complier对象开始解析项目
  3. 从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树
  4. 对不同类型的依赖模块文件使用对应的Loader进行编译,最终转为JavaScript文件
  5. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这是可以修改输出内容的最后机会
  6. 整个过程中Webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

Loader和Plugin

什么是Loader和Plugin

Loader本质是一个函数,是告诉webpack如何转化处理某一类型的文件。

Plugin本质是一个插件,是用来自定义webpack打包过程的方式,含有apply方法,通过这个方法可以参与到整个webpack打包的各个流程

Loader是Webpack中用于将非JavaScript模块转换为Webpack可处理的模块的程序。例如,当我们使用Webpack构建应用程序时,我们可能会使用CSS、Sass、Less等样式文件,或者使用Babel将ES6语法转换为ES5语法。在这种情况下,我们需要使用不同的Loader来处理这些不同的文件类型。Loader通常被定义为一个函数,它将原始资源作为参数,然后返回转换后的代码。

Plugin是Webpack中的另一个重要概念,用于扩展Webpack的功能。Plugin通常被用于执行一些特定的任务,例如清理构建目录、压缩代码、生成HTML文件、提取CSS等等。Plugin是通过Webpack的插件机制来实现的,通常需要实现一个JavaScript类,并在Webpack的配置中进行引用和配置。

在实现上,Loader是通过将它们添加到Webpack配置文件中的module.rules数组中来实现的。每个Loader都有一个test属性,它指定了需要转换的文件类型。当Webpack运行时,它会遍历所有的Loader,找到匹配的Loader,并对文件进行转换。

Plugin是通过将它们添加到Webpack配置文件中的plugins数组中来实现的。Plugin通常实现Webpack的钩子函数,在不同的阶段执行不同的任务。例如,在编译阶段,Webpack会调用compiler.hooks.compile.tap方法,Plugin可以通过这个方法来执行一些编译相关的任务。

Loader和Plugin编写思路

  • Loader要遵循单一原则,每个loader只做一种转义工作。每个loader拿到的是源文件内容,可以通过返回值的方式将处理后的内容输出。
  • Plugin:webpack在运行的生命周期中会广播出许多事件,Plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

常见的loader

  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试
  • image-loader:加载并且压缩图片文件
  • babel-loader:把 ES6 转换成 ES5
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • eslint-loader:通过 ESLint 检查 JavaScript 代码

常见的plugin

  • define-plugin:定义环境变量
  • html-webpack-plugin:简化html文件创建
  • uglifyjs-webpack-plugin:通过UglifyES压缩ES6代码
  • webpack-parallel-uglify-plugin: 多核压缩,提高压缩速度
  • webpack-bundle-analyzer: 可视化webpack输出文件的体积
  • mini-css-extract-plugin: CSS提取到单独的文件中,支持按需加载

模块热更新

模块热更新是webpack的一个功能,他可以使得代码修改后不用刷新浏览器就可以更新。

  • 通过webpack-dev-server创建两个服务器:提供静态资源的服务express和Socket服务
  • express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
  • socket server是一个websocket的长链接,双方可以通信
  • socket server监听到对应的模块发生变化时,会生成两个文件(.json,.js文件)
  • 通过长链接,socket server可以直接将这两个文件主动发送给服务器
  • 浏览器拿到这两个新文件后,通过HMR runtime机制,加载这个两个文件,并且针对修改的模块进行更新

文件监听原理

轮询判断文件最后编辑事件是否发生变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等aggregateTimeout后在执行

如何利用webpack来优化前端性能?

  • 压缩代码。可以利用webpack-paralle-uglify-plugin来多线程并行压缩JS文件,利用cssnano来压缩css,配置image-webpack-loder压缩图片。
  • Tree shaking。将代码中没有引用过的模块进行标记删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
  • 代码分割技术:将代码分割成多份进行懒加载或异步加载,避免打包成一份后导致体积过大影响页面的首屏加载

babel

  1. 解析:将代码转成AST
  2. 转化:访问AST的节点进行变换操作生产新的AST
  3. 生成:以新的AST为基础生成代码

什么是长缓存?在webpack中如何做到长缓存优化?

浏览器在用户访问页面的时候,为了加快加载速度,会对用户访问的静态资源进行存储,但是每一次代码升级或是更新,都需要浏览器去下载新的代码,最方便和简单的更新方式就是引入新的文件名称。在webpack中可以在output中输出的文件指定chunkhash,并且分离经常更新的代码和框架代码。通过NameModulesPlugin或是HashedModuleIdsPlugin使再次打包文件名不变。

weabpack4和5的区别

  1. 压缩代码:4上需要下载terser-webpack-plugin,5自带js压缩功能,内置terser-webpack-plugin插件
  2. loader的优化,4加载不同类型资源需要用不同的loader(url-loder导出url,raw-loader导出源代码,file-loader发送单独文件),5都可以用asset

webpack treeShaking机制的原理是什么?

在ES6以前,我们可以使用CommonJS引入模块:require(),这种引入是动态的,也意味着我们可以基于条件来导入需要的代码。但是CommonJS规范无法确定在实际运行前需要或者不需要某些模块,所以CommonJS不适合tree-shaking机制。在 ES6 中,引入了完全静态的导入语法:import。

  • 静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码

webpack和vite的区别

Webpack 会遍历你的应用程序中的所有文件,并启动一个开发服务器,然后将整个代码渲染到开发环境中。Vite 的工作方式不同,它不会遍历整个应用程序,Vite 只是转换当时正在使用的文件/模块。它使用基于路由的代码拆分来了解代码的哪些部分实际需要加载,因此,它不必重新打包所有内容。开始开发构建时,Vite首先将JavaScript 模块分为两类依赖模块和源码模块依赖项模块将使用esbuild进行处理和捆绑,esbuild是一个用Go编写的js打包工具,执行速度比webpack快10-100倍。

模块化

CommonJS和ES6中模块引入的区别

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJs 是单个值导出,ES6 模块可以导出多个
  4. CommonJs 是动态语法可以写在判断里,ES6 模块 不能
  5. CommonJs 的 this 是当前模块,ES6 模块的 this 是 undefined
  6. CommonJs 是同步加载,ES6 模块支持异步加载

好处

  • 避免命名冲突(减少命名空间污染)
  • 更好的分离, 按需加载
  • 更高复用性
  • 高可维护性

模块化打包方法

  1. commonjs 通过requireexports进行导入导出。采用同步的方式加载模块。在服务端,模块文件都是存在本地磁盘,读取快。但在浏览器端,限制于网络问题,更合理的方案是使用异步加载。
  2. AMD 采用异步加载模块的方式,模块的加载不影响它后面语句的运行。
  3. CMD 与AMD类似,AMD推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。

Git

git发生冲突的场景

比如多个分支修改了同一个文件(任何地方)就会发生冲突,使用git status查看冲突文件。解决冲突内容:对每个文件使用git add命令来标记冲突已解决。一旦暂存这些原本有冲突的文件,git就会将他们标记为冲突已解决然后再提交。使用git log可以查看到合并的信息。

常用

git add 将代码提交到暂存区。git commit 将代码提交到本地仓库。git push 将代码提交到远端分支。git remote add: 添加一个远程版本库关联。git remote rm: 删除某个远程版本库关联。git branch:查看本地所有分支信息。