vue2
vue diff
# patch(oldVnode,vnode)
1.如果sameVnode,则patchVnode(oldVnode,vnode)
2.否则,oldVnode如果没有就创建一个空的vnode节点,绑定el, 替换,销毁重建;没有vnode,直接销毁旧节点就行
# patchVnode:对比节点文本或子节点变化
1. vnode.text
销毁oldVnode.children(如有),
删除oldVnode.text(如有)
创建新的text
2. vnode.children
如没有oldVnode.children,直接创建
如都有children,则调用updateChildren(oldVnode.children,vnode.children)
3. children和text都没有,直接删除旧的文本节点或children节点
# updateChildren(oldCh,newCh)
头尾双指针比对方式
oldCh: oldStartIdx, oldEndIdx | oldStartVnode , oldEndVnode
newCh: newStartIdx, newEndIdx | newStartVnode , newEndVnode
while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){
if(sameVnode(oldStartVnode,newStartVnode)){
// 首首比较
patchVnode(oldStartVnode,newStartVnode)
oldStartVnode=oldCh[++oldStartIdx]
newStartVnode=newCh[++newStartIdx]
}else if(尾尾比对){
}else if(新首,旧尾){
}else if(旧首,新尾){
}else{
// 遍历oldCh,看能否找到newStarVnode是sameVnode的
idxInOld = oldKeyToIdx(newStartVnode.key)
if(!idxInOld){
如果没找到,则是新节点,直接创建该新节点
}else{
如果找到
elmTomove = oldCh[idxInOld]
patchVnode(elmTomove,newStartVnode)
oldCh[idxInOld]=undefined
newStartVnode=newCh[++newStartIdx]
}
}
}
题目0:mvvm
参考 mvvm
// mvc
MVC将应用抽象为数据层(Model)、视图层(View)、逻辑层(controller),
降低了项目耦合。但MVC并未限制数据流,Model和View之间可以通信
------
controller里调用 this.model.setState()
model里调用,this.render.render()
controller -> model -> view -> controller
// mvp
MVP则限制了Model和View的交互都要通过Presenter,这样对Model和View解耦,提升项目维护性和模块复用性
-----
this.presenter 里调用this.model.setState()
this.presetnter里调用this.render(this.model.getState())
view <-> presenter <-> model
// mvvm
而MVVM是对MVP的P的改造,用VM替换P,将很多手动的数据=>视图的同步操作自动化
题目1:响应式原理
参考 响应式
// 触发更新视图
function updateView() {
console.log('视图更新')
}
const oldArrayProperty = Array.prototype
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
const dep = new Dep();
// 核心 API
Object.defineProperty(target, key, {
get() {
if (Dep.target) {
dep.depend();
}
// 收集依赖
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 修改数据时,通知页面重新渲染
dep.notify();
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
observe(options.data)
class Dep{
subs = []
depend() {
if (Dep.target) {
// 搜集依赖,最终会调用上面的 addSub 方法
this.subs.push(Dep.target)
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 调用对应的 Watcher,更新视图
subs[i].update();
}
}
}
class Watcher {
constructor(vm,expOrFn){
this.vm = vm
this.getter = expOrFn;
this.value = this.get();
}
get(){
Dep.target = this;
const value = this.getter.call(this.vm, this.vm);
return value
}
run(){
const value = this.get();
}
update(){
// 开启异步队列,批量更新 Watcher
queueWatcher(this);
}
}
const updateComponent = () => {
vm._update(vm._render());
};
new Watcher(vm, updateComponent);
// 流程 (该watcher是组件实例的一个映射,帮组件实例订阅)
# 组件渲染视图时,调用new Watcher(vm,updateComponent)
# watcher.get() // Dep.tartget = this
# watcher.getter() => updateComponent(vm)
# vm._update(vm._render()) // _render()生成vnode
# _render()过程中touch到data的get() // 收集依赖,(dep = new Dep()).subs.push(Dep.target = watcher)
# 数据set时,调用dep.notify() => watcher.update() 异步更新 => watcher.run() => watcher.get() => watcher.getter() 重新渲染视图
// computed原理
1. 在computed依赖的data属性更新后,需要对computed标记dirty
2. 在访问computed时候,会判断是否是dirty,`dirty ? 重新计算 : 返回当前的值`
computed也映射一个new Wacher()实例,赋值给Dep.target
computed是data属性的一个订阅者,它在初始化时候被data属性收集依赖,当computed依赖的data属性改变后,标记该computed为dirty,即数据更改过,当渲染使用到computed时候,再计算出computed的值从而得到最新的正确的值
// watch原理
也是实例化一个watcher
题目2: vue性能优化
# 合理使用v-show和v-if
# 合理使用computed
# v-for加key,避免同时使用v-if
# 自定义事件,dom事件及时销毁
# 合理使用异步组件
# 合理使用keep-alive
# data层级不要太深(一次性递归监听)
# 使用vue-loader在打包时编译模板
# webpack层面优化
# 前端通用性能优化
# 使用ssr
vue3
题目1:vue3响应式
// Proxy实现响应式
# 深度监听,性能更好
# 可监听 新增/删除 属性
# 可监听数组变化
// 总结
# Proxy能规避Object.defineProperty的问题
# Proxy无法兼容所有浏览器,无法polyfill
function reactive(target){
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target
}
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
// 监听收集依赖
}
const result = Reflect.get(target, key, receiver)
// 深度监听
return reactive(result)
},
set(target, key, val, receiver){
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
// 更新视图
} else {
console.log('新增的 key', key)
// 监听并更新视图
}
const result = Reflect.set(target, key, val, receiver)
return result
},
deleteProperty(){
const result = Reflect.deleteProperty(target, key)
return result
}
}
const observed = new Proxy(target, proxyConf);
return observed
}
const proxyData = reactive(data)
题目2:vue3比vue2有什么优势
# 性能更好
# 体积更小
# 更好的ts支持
# 更好的代码组织
# 更好的逻辑抽离
# 更多新功能
题目3:composition API
# 代码组织,逻辑复用,类型推导
题目4: ref,reactive,toRef,toRefs
// ref
# 生成值类型的响应式数据
# 可用于模板和reactive
# 通过.value
// toRef
// toRefs
# 将响应式对象转化为普通对象
# 对象的每个prop都是对应的ref
# 两者保持引用关系
const state = reactive({age})
const refState = toRefs(state)
const {age:ageRef} = refState
// state.age === ageRef 且保持引用关系
// 为何需要ref
# 返回值类型,会丢失响应式
# 如在setup,computed,合成函数,都有可能返回值类型
# vue如不定义ref,用户将自造ref,反而混乱
// 为何需要.value
# ref是一个对象(不丢失响应式),value存储值
# 通过.value属性的get和set实现响应式
# 用于模板,reactive时,不需要.value
// 为何需要toRef和toRefs
# 不丢失响应式的情况下,把对象数据分解/扩散
# 针对的时响应式对象,普通对象不可以
# 不创造响应式,而是为了延续响应式
题目4:vue3新功能
# createApp
# emits
# 生命周期
# 多事件
# Fragment
# 移除.sync
# 异步组件的写法
defineAsyncComponent(()=>import(xx))
# 移除filter
# Teleport
<teleport to="body">child</teleport>
# Suspense
<Suspense>
<template>
<异步组件/>
</template>
<template #fallback>
loading...
</template>
</Suspense>
# Composition API
// compositon api
# reactive
# ref
# readonly
# watch和watchEffect
# setup
# 生命周期钩子
# getCurrentInstance获取当前实例
题目5:vue3为何比vue2快
# Proxy响应式
# patchFlag
# 编译模板时,动态节点做标记
# 标记,分为不同的类型,如TEXT PROPS
# diff算法时,可以区分静态节点,以及不同类型的动态节点
# hoistStatic
# 将静态节点的定义,提升到父作用域,缓存起来
# 多个相邻的静态节点,被合并起来
# 典型的拿空间换时间的优化策略
# cacheHandler
# 缓存事件handler
# SSR优化
# 静态节点直接输出,绕过了vdom
# 动态节点,还算是需要动态渲染
# tree-shaking
# 编译时,根据不同的情况,引入不同的api
题目6:compostion api与react hooks的对比
# 前者setup只会被调用一次,后者会被调用很多次
# 前者无需useMemo,useCallback,因为setup只调用一次
# 前者无需考虑调用顺序,而后者需要保证hooks的顺序一致
# 前者reactive和ref要难理解
题目7:vue3 jsx
jsx本质是js,jsx是es规范,template只是vue自家规范
// xx.vue
export default {
setup(props,context){
// context.slots.default()
const render=()=>{
return <div>{countRef.value}</div>
}
return render
}
}
// xx.jsx
// setup函数
export default defineComponent(()=>{
const render=()=>{
return <div>{countRef.value}</div>
}
return render
})
// xx.jsx
// config
export default defineComponent({
props:[],
setup(props){
return render
}
})
题目8:vue3 script setup
vue版本 > 3.2.0
// defineEmits defineProps defineExpose
<script setup>
import {defineEmits, defineProps, defineExpose} from 'vue'
const props = defineProps({
name:string
})
const emit = defineEmits(['change'])
defineExpose({ //用于ref.current
a:1
})
function handle(){
emit('change')
}
</ script>
<template>
<div @click="handle">{{prop.a}}</div>
</template>
vite
题目1:vite是什么
# 一个前端打包工具,vue作者发起的项目
# 借助vue的影响力,发展较快
# 优势:开发环境无需打包,启动快
题目2:vite为何启动快
# 开发环境使用esmodule,无需打包
<script type="module" src="">
<script type="module">
import {add} from './xx/xx.js'
</script>
# 生产环境使用rollup,并不会快很多
react
参考: 语雀react
题目1:setState可能被合并,可能异步更新
// batchUpdate机制
# 事物机制
transtion.init()
transition.method()
transition.close()
# 判断isBatchupdates // 是否处于批量更新过程,是的话收集到dirtyComponents
# 哪些能命中batchUpdate机制 // 异步更新
- 生命周期(和它调用的函数)
- React中注册的合成事件(和它调用的函数)
- React可以管理的入口
# 哪些不能命中 // 同步更新
- setTimeout setInterval (和它调用的函数)
- 自定义DOM事件 (和它调用的函数)
- react管不到的入口
# 是否合并(只有isBatchUpdate= true,异步才会合并,同步不会合并)
- 对象形式合并 Object.assign()
- 函数形式无法合并
// setState主流程
this.setState(newState)
newState存入pending队列(_pendingStateQueue)
是否处于batch(批量) Update
是:保存(组件)于dirtyComponents(state要被修改的components)中
否:遍历所有有的dirtyComponents,调用updateComponent,更新props和state
// setState的第二个参数回调,
# 如果是异步更新,发生在所有setState更新state后,调用;
# 如果是同步更新,回调是当前state同步更新后执行
// react18,全是异步更新合并,没有同步更新
题目2:合成事件
//
由于fiber机制的特点,生成一个fiber节点时,它对应的dom节点有可能还未挂载,onClick这样的事件处理函数作为fiber节点的prop,也就不能直接被绑定到真实的DOM节点上。 为此,React提供了一种“顶层注册,事件收集,统一触发”的事件机制
// 机制
# 顶层注册,事件收集,统一触发
# 顶层注册:事件代理方式,在root元素上绑定一个统一的事件处理函数
# 事件收集:native事件触发冒泡到root元素后,处理函数执行,构建合成事件对象,按照冒泡或捕获的路径去组件中收集真正的事件处理函数
# 同一触发:收集过程之后,对所收集的事件逐一执行,并共享同一个合成事件对象
// 1.概念
React 合成事件(SyntheticEvent)是 React **模拟原生 DOM 事件所有能力的一个事件对象**,根据w3c标准模拟
// 2.目的
# 1. 进行浏览器兼容,实现更好的跨平台
# 2. 避免垃圾回收,(事件池)
# 3. 方便事件统一管理和事务机制
// 3.流程
# 事件委托,react所有事件都挂载在document元素(react 17 root)上
# 当真实 DOM 元素触发事件,会冒泡到 `document` 对象后,再处理 React 事件
# 所以会先执行原生事件,然后处理 React 事件
# 最后真正执行 `document` 上挂载的事件
// 4.事件池(react 17 不使用事件池)
# 事件池未满时创建事件对象,满时取出复用事件对象,派发给组件
# 合成事件对象的事件处理函数全部被调用之后,所有属性都会被置为 `null`,所以事件handle里setTimeout获取e.target为null
# e.persist() 保留属性
// 5. 合成事件和原生事件最好不要混用**
# 原生事件中如果执行了`stopPropagation`方法,则会导致其他`React`事件失效。因为所有元素的事件将无法冒泡到`document`上
// 6. e.nativeEvent.stopImmediatePropagation
# 事实上 nativeEvent(原生事件) 的 `stopImmediatePropagation`只能阻止绑定在 document 上的事件监听器
# 合成事件上,能阻止合成事件冒泡到document上
题目3:组件生命周期
题目4:react函数组件调用子组件的方法
const Child = React.forwadRef((props,ref)=>{
useImperativeHandle(ref=>()=>({
fn(){}
}))
})
题目5:错误边界
// api
static DerivedStateFromError
componentDidCatch
// 错误边界捕获的是其子组件树的错误,它不能捕获
- 事件处理
- 异步代码
- 服务端渲染
- 它自身抛出的错误
由于异常捕获是try catch子组件生命周期钩子中的错误,因此上述情况无法捕获
题目6:hoc(用于类组件状态逻辑复用)
import {hoistStatic} from 'hoist-non-react-statics';
function Hoc(WrapComp){
class WrappedComp {
render(){
const {forwardRef,...props}=this.props
return <WrapComp {...props} ref={forwardRef}/>
}
}
const Enhance = React.forwardRef((props,ref)=>{
return <WrappedComp {...props} forwardRef={ref}/>
})
hoistStatic(Enhance,WrapComp)
return Enhance
}
// 使用
const Comp = hoc(Comp1)
<Comp />
题目7:浅层渲染
// 概念(用于单元测试之类)
浅层渲染不依赖与DOM,它会返回React组件元素引用,并且不渲染其子元素;对组件和子组件进行隔离测试
import ShallowRenderer from 'react-test-renderer/shallow';
// 测试代码:
const renderer = new ShallowRenderer();
renderer.render(<MyComponent />);
const result = renderer.getRenderOutput();
expect(result.type).toBe('div');
expect(result.props.children).toEqual([
<span className="heading">Title</span>,
<Subcomponent foo="bar" /> // 子组件引用,但不渲染
]);
题目8:react性能优化
参考:
// 加载时性能
**让用户更早地看到界面、更早地和应用交互**
# 懒加载(lazy)使用lazy实现按需加载组件和按需加载路由
# 服务端渲染,使用服务端渲染提升首屏渲染性能
# 缓存缓存cdn cdn缓存提升React资源的加载速度
# 使用 prerender-spa-plugin 渲染首屏,基本原理是启动一个服务,用pupetter离屏渲染。这个原理和服务端渲染类似,但对代码改造更小,不过要求服务器有node环境
// 运行时性能
**降低卡顿,交互更流畅**
# 不必要的渲染,memo,pureComponent,scu
# Fragment避免不必要的节点
# 事件回调不适用匿名函数或者bind;(避免创建新函数)
应该用class fields函数或者构造函数里的bind
# 不要写内联样式
因为React在解析JSX时候需要将style对象解析成css style字符串
# 优化条件渲染
让条件分支中只包含需要改动的元素,不包含不需要改动的元素
# 缓存计算属性
# 长列表:虚拟列表优化
react-virtualized 可视区域渲染(少渲染dom节点)
# 列表使用key
# concurrent mode
// 性能工具
React官方提供了一个性能检测工具:[react-addons-perf](https://reactjs.org/docs/perf.html)
题目7:concurent mode (react 18)
// concurent mode
# 解决的就是任务拆分和任务优先级划定的问题**
# react内部通过requestAnimationFrame,模拟实现requestIdleCallback,从而实现时间切片
# commit阶段因为包含了dom操作,所有无法拆分
// 可中断渲染
解决方案:时间分片
// 时间分片
# 即利用浏览器的渲染空余时间来执行diff工作,当执行时间过长时候,停止diff工作,把执行机会让给渲染,然后等渲染间隙再继续执行diff工作,直到diff完成,再执行commit
# 时间分片时长不能大于1帧渲染的时间(16ms)
如果执行时间大于16ms,就要停止,然后让浏览器先执行渲染操作,渲染空余时候再继续执行
# 断点重启
fiber tree (类似链表结构)
# 链表结构利于暂停和重启,比如遍历到某个节点时候需要暂停,那么只要记录当前指针,等到重启时候指向下一个就可以了
// diff整体流程
# 1. **首次渲染时候构建一个和虚拟dom树一样结构的fiber树**
# 2. 组件更新时候,遍历新旧fiber树,diff区别,diff操作是分片进行,16ms内如果没完成,就先暂停等待下个渲染空闲时间再继续**
# 3. **diff完成之后进行commit,将变化提交,进行对应的dom操作,为防止界面抖动,commit是一次性完成的**
// react.lazy
# React.lazy核心逻辑就是throw一个异步加载组件的promise,加载好后return这个组件(所以如果其外部不包裹Suspense,也没有ErrorBoundary的话,页面就会崩溃)。
// react.Suspense
# ErrorBoundary catch子组件的promise异常,然后将任务添加到更新队列中,React后面会异步处理更新队列,取出promise执行,当promise resolve之后,会再次渲染组件
题目8: diff key
diff虚拟dom tree同层对比
如果是samenode,就对比属性,并递归对比子node
否则就销毁重建,
key用来判断sameNode,且不建议index
对于数组删除添加元素这种,如果不用key,就会比对混乱,
# 带来性能损耗(本来只更新一个节点,结果要更新多余节点)
# 非受控的表单的值有问题
eg:
input 输入框
old: [{id:1},{id:2}] //id 1的input框输入内容xxx
new: [{id:2}] // 没有key的化,会认为id为2的节点是旧的id为1的节点,更新时dom复用,导致id为2的节点的输入内容是xxx
题目8:react-router
参考 react-router
// api "react-router-dom";
<BrowserRouter>
<Switch>
<Route path> <Comp/> </Route>
<Route path children component render/>
</Switch>
</BrowserRouter>
withRouter()
useHistory()
// react router组件渲染原理
# React-Router使用context将路由信息传递给子组件,Router作为Provider负责更新路由信息并将信息传递,Route和Routes则根据context的改变,获取当前的路由信息,再根据自己的path、exact等属性判断是否需要渲染,然后再根据component、children、render属性渲染指定的组件。
# HashRouter和BrowserRouter都是继承自Router,它们都是Provider,向Consumer提供路由信息,只是修改URL和监听URL变化的方式不同
题目9:redux
状态机
# 状态机(状态+操作)
1.状态仓库
实质是一个存储数据的对象,但该对象对外不可见,或者说不可编辑
2.可以触发状态的改变动作
抛出一个方法,该方法可以对状态数据进行加工,并返回一个加工后的新的状态数据
3.状态改变后触发相关的订阅事件
当状态改变后,需要让订阅状态的相关实体得到反馈,以完成最终的状态同步
flux 和 redux
单项数据流
视图触发action,store接收到action后,调用reducer计算新的状态,然后更新状态并通知视图
简化版:
view -→> action -> reducer -> store
加上中间件:
view -> action -> middleware -> reducer -> store
redux
参考
// api
const store = createStore(reducer , applyMiddleware([thunkMiddleware]))
store.dispatch(action)
store.subscribe(()=>{
const state = store.getState()
})
// reducer 合并
combineReducers
// 中间件 middleware
// react-redux
# Provider
并传入store属性,store属性值为redux的store,
该组件用于给组件中增加store的context
子组件就可以通过consumer来访问store
# connect
消费context的store
返回的是一个包含容器组件的高阶组件,容器组件负责监听store变化,
并将开发者关注的数据通过props注入到UI组件中
// 实现
- getState,获取当前状态。
- subscribe,接收一个回调作为参数,订阅状态改变,它返回一个函数,用来解除订阅。
- dispatch,接收一个action对象作为参数,触发action之后会依据reducer计算新的状态,并通知订阅者
interface IStore {
subscribe: (cb: Function) => void,
getState: () => Object,
dispatch: (action: Object) => void
}
const createStore = (reducer: Function): IStore => {
let state = reducer();
const listeners = [];
const subscribe = cb => {
listeners.push(cb);
// 标志位
let isSubscribed = true
return () => {
if(!isSubscribed) return
isSubscribed = false
const index = listeners.indexOf(cb);
listeners.splice(index, 1);
};
};
const getState = () => state;
const dispatch = action => {
state = reducer(action);
listeners.forEach(listener => listener());
};
return {
subscribe, getState, dispatch
};
};
参考 redux中间件原理
// 中间件原理 (柯里化)
const store = createStore(reducer, applyMiddleware(mid1, mid2, mid3))
等价于
const enhancer = applyMiddleware(mid1, mid2, mid3)
const store = enhancer(createStore)(reducer)
function applyMiddleware(...middlewares){
return function(createStore){
return (reducer)=>{
const store=createStore(reducer)
const dispatch=()=>{}
const middlewareAPI = {
getState:store.getState,
dispatch
}
const chain = middlewates.map(middleware=>middleware(middlewareAPI))
// [next=>action=>{next(action)}之类的函数]
// 包装store.dispatch
dispatch = compose(...chain)(store.dispatch)
return {
store,
dispatch
}
}
}
}
// 中间件
function middleware1({dispatch,getState}){
return next => action =>{
return next(action) // 剥洋葱方式,最后一个next调用是store原生的dispatch
}
}
function compose(...funcs){
return funcs.reduce((a,b)=>{
return function(...args){
return a(b(...args))
}
})
}
compose(fn1,fn2,fn3) 等价于
(...args)=> fn1(fn2(fn3(...args)))
dispatch = composeFn(store.dispatch)等价于
dispatch = fn1(fn2(fn3(storeDispatch)))
dispatch = function fn1Rf(action){
return fn2Rf(action){
return fn3Rf(action){
return storeDispatch(action)
}
}
}
// redux-thunk 中间件
function actionCreator(){
// 异步返回一个函数
return function(dispatch,getState){
promise>resolve().then(()=>{
dispatch({type:'xxxx'})
})
}
}
function thunkMiddleware({dispath,getState}){
return next => action =>{
if(action === function){
return action(next,getStte)
}
next(action)
}
}
题目10: 原理
// 时间分片
正常情况下,浏览器渲染频率是16.67ms/帧;但为了让页面不太卡顿,又照顾js线程,提升至了50ms/帧,即一秒钟最多渲染20帧;
api requestIdleCallback(deadline) 在浏览器空闲时执行
dealine.timeRemaining() 剩余多少时间,渲染下一帧
const process =(deadline)=>{
// 时间够,拿出一个任务执行,否则暂停先渲染
if(dealine.timeRemaing() > limit && works.length){
works.shift()
}
if(works.length){
requestIdleCallback(process)
}
}
requestIdleCallback(process)
参考
题目11: react-hooks
参考
# useState
# useEffect
// 异步
# useLayoutEffect
// 同步
// useLayoutEffectClear -> useLayoutEffect -> 渲染-> useEffectClear -> useEffect
# useReducer
# useMemo
# useCallback
# useImperativeHandle
// 配合React.forwardRef使用
webpack
参考
tree-shaking
# mode:production会自动tree-shaking
# mode:development 配置 optimization.usedExports=true 会自动表示哪些unuse
# package.json 里的sideEffects字段
// [css,其他moule] 不会被树摇
// false 所有文件都会被树摇
# 只支持es module的模块引入
css抽取压缩
// 模块化
css-loader ,options.modules = true
// mini-css-extract-plugin 抽取
MiniCssExtractPlugin.loader 放在css-loader后处理
plugin:[new MiniCssExtractPlugin(文件名)]
// 压缩 optimize-css-assets-webpack-plugin
optimization:{
minimizer: [new OptimizeCSSAssetsPlugin({})]
}
js压缩
// uglifyjs-webpack-plugin
# webpack内置,production默认开启
分包
参考 分包
//优点
# 减少代码体积,抽取公共代码
# 缓存(第三方包)
// 考虑
# 引用次数
# 体积大小
# 入口模块和异步模块的最大并行请求数
optimization:{
splitChunks:{
cacheGroups:{} // 缓存组
}
}
模块热更替HMR
参考: 热更替原理
# webpack-dev-server 创建http服务,websocket服务,生成compiler实例
# 监听编译完成,websocket服务端向客户端发送hash和ok事件
# 通过webpackDevMiddleware库监听文件修改,库中使用了compiler.watch对文件的修改进行了监听,并且通过`memory-fs`实现了将编译的产物存放到内存中
# 向浏览器中插入客户端代码,调用了`updateCompiler()`方法,它修改了webpack配置中的entry
- main: [
// clientEntry,用于客户端websocket服务,保存服务端的hash
'xxx/node_modules/webpack-dev-server/client/index.js?http://localhost:8080',
// hotEntry,用于检查热更新逻辑,调用module.hot.check()
// module.hot来自HotModuleReplacementPlugin(为模块添加了hot属性)
'xxx/node_modules/webpack/hot/dev-server.js',
原本的entry
]
# 浏览器端获取响应,
- 监听websocket客户端,监听服务端websocket的hash和ok事件,
- 记录hash,
- 并调用module.hot.check(),进行hotCheck,ajax或jsonp请求服务端的两个补丁文件(用当前hash拼接出)
hot-update_上一次hash.js // jsonp 模块变更内容
hot-update_上一次hash.json //ajax
{h:新的hash值,c:{main:true // 哪个chunk发生了更新,chunkId}}
- 执行window.WebpackHotUpdate() , 进而调用hotApply,hotApply根据模块ID找到旧模块然后将它删除,然后执行父模块中注册的accept回调,从而实现模块内容的局部更新
// 使用
devServer:{
hot:true,
hotOnly:true,// 即便热更替不生效,也不刷新页面
}
plugins: [new webpack.HotModuleReplacementPlugin()]
# style-loader 已经支持了热更替
# JS
import number from './number';
if(module.hot) {
module.hot.accept('./number', () => {
document.body.removeChild(document.getElementById('number'));
number();
})
}
// 热更替原理
sourcemap
# 开发环境 eval-cheap-module-source-map
# 生成环境 cheap-module-source-map
- inline 将.map作为DataURI嵌入,不单独生成.map文件
- cheap 只告诉行信息,不告诉列
- eval 使用eval包裹代码模块,重新构建的速度更快
- module 让sourcemap可以映射到原始源代码
// 由于源代码到转译后的代码的映射关系由相应的loader提供,因此module模式应该会处理这部分sourcemap,以最终得到到原始源代码的映射
shiming垫片
// 提供了$这个环境变量,比如有些包依赖于jquery这个全局变量,代码中不用import $ from 'jquery'
plugins:[
new webpack.ProvidePlugin({
$:'jquery'
// 当项目中有$变量是,$ = jquery这个包到处的变量,类似加上import $ from 'jquery'
})
]
externals
有时候我们希望使用script标签方式引入外部依赖,而不是打包到bundle中,这时候就需要用到externals配置
externasl:{
jquery:'$' // 当碰到import $ from 'jqeury' 直接变成 const $ = global.$(全局变量)
}
// 打包一个Npm包时,一些依赖模块让使用者安装
library
output:{
library:'library' //window.library = library
libraryTarget:'umd' // umd支持es6 commonjs amd
// this|window|global
}
loader
// 取哪里解析loader
resolveLoader:{
modules: ['node_modules','./loader']
}
//
const loaderUtils = require('loader-utils');
function xxLoader(source){
const options = loaderUtils.getOptions(this)
this.callback(newSource)
this.callback(null,json)
this.async()
return newSource
}
plugin
// Webpack 的插件系统是基于官方自己的 Tapable(类似eventEmitter发布订阅) 库实现的
class MyPlugin {
apply(compiler) {
compiler.hooks.done.tap('My Plugin', stats => {
//
});
}
}
compiler 和compilation
# compiler是编译器,控制webpack整个编译流程,compilation则是处理模块,包括调用loader编译模块,模块优化,生成hash,生成chunk等。
# 除了模块的处理,webpack还有很多其他操作,compiler负责处理初始化,插件注册,输出文件,编译失败处理等等
原理
// 概述
webpack读取配置,根据入口开始遍历文件,解析依赖,使用loader处理各模块,然后将文件打包成bundle后输出到output指定的目录中
// 流程
1. Webpack CLI 启动打包流程,解析配置项参数;
1. 载入 Webpack 核心模块,创建 Compiler 对象;
1. 注册plugins
1. 使用 Compiler 对象开始编译整个项目;
1. 从入口文件开始,解析模块为AST,分析模块依赖,形成依赖关系树;
1. 递归依赖树,将每个模块交给对应的 Loader 处理;
1. 合并 Loader 处理完的结果,将打包结果输出到 dist 目录
性能优化
参考 webpack
# 分析工具
- stats.json | webpack --env production --json > stats.json
- speek-measure-webpack-plugin // 分析出整个构建时间和每个loader和plugin的构建时间
- webpack-bundle-analyzer插件 // 体积分析
# 提高版本和Node版本
# 多进程构建
- HappyPack
- thread-loader //官方
- parallel-webpack
# 多进程压缩
- parallel-uglify-plugin
- uglifyjf-webpack-plugin
- terser-webpack-plugin 开启parallel参数
# 进一步分包:预编译资源包
- html-webpack-externals-plugin // 类似cdn方式
- DllPlugin和 DllReferencePlugin
- externals // 改cdn引入
# 利用缓存
-. babel-loader开启缓存
-. terser-webpack-plugin开启缓存
-. 使用cache-loader或者hard-source-webpack-plugin
# 缩小构建目标,
- exclude、
- resolve减少文件搜索范围
# 提升加载速度和运行速度
- tree-shaking
- 摇树css (purgecss-webpack-plugin | uncss)
- js esmodule
- scope-hosting
- 将每个模块都提升到引入者顶部
- 减少webpack的大量包裹代码,调用栈变浅
- 图片压缩
- `image-webpack-loader`,这个loader实际是使用了imagemin进行图片压缩
- 优化polyfill方案(babel)
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp({
// webpack配置
});
//
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4
})
]
}
//
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [
{
module: 'react',
entry: '//path/to/your/cdn-domain/react.min.js',
global: 'React'
},
...
]
});
]
// dll
output: {
filename: '[name].dll.js',
path: path.resolve(__dirname, './build/library'),
library: '[name]'
},
plugins: [
new webpack.DllPlugin({
name: '[name]',
path: './build/library/[name].json'
})
]
}
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./build/library/manifest.json')
})
]
babel
参考
// babel工具包
@babel/cli
@babel/core
@babel/parser
@babel/generator
@bable/traverse
source --parser--> ast(traverse遍历) --generator--> code
// 执行顺序
• 插件在 Presets 前运行。
• 插件顺序从前往后排列。
• Preset 顺序是颠倒的(从后往前)
// @babel/core
const ast = babel.transformSync(code,options)
// @babel/cli
// @babel/preset-env
- babel的一些插件和预设可以转换新语法特性(如箭头函数,解构,可选链,class),而内建的变量(如Promise、Symbol)却无法兼容
- useBuiltIns
false: 需手动引入babel-pollyfill 或 core-js + generator
entry: 需手动引入,会根据浏览器支持程度引入不支持的api
usage: 按需加载
- 配置
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": 3
}
]
]
}
// babel-polyfill
- core-js
- regenerator // 实现 ES6/ES7 中 generators、yield、async 及 await 等相关的语法
- babel@7.4.0以后已经不推荐,直接引入下面的两个包,但这不影响@babel/preset-env useBuiltIns:usage的按需加载
- 缺陷:
1.环境变量污染
2.辅助函数(class等)冗余
//@babel/runtime
// babel-plugin-transform-runtime
- 让所有用到babel的runtime的文件,只引用一份运行时,就是@babel/runtime(辅助函数冗余问题解决)
- 自动引入core-js和regenerater,并给core-js和@babel/polyfill的内置(如Promise、Map、Set)命名别名,这样可以避免污染全局变量
- 缺陷:
Array.prototype.includes这种没法使用了
- 配置
{
"presets": [
[
"@babel/preset-env"
]
],
"plugins": [
["@babel/plugin-transform-runtime", {
"corejs": 3
}]
]
}
typescript
css
题目1:rem
// 本质是让我们写的css尺寸与屏幕宽度保持固定比例
const baseSize = 75 / 1rem 等价于ui稿上的75px
baseSize / 750 = 1rem / cw // 750的ui稿
1rem = baseSize * cw / 750
px2rem($px){
return $px / 75 *1rem // 750的ui稿
return $px / 75 * 1rem * 2 // 375的ui稿,将ui稿放大一倍
}
题目2:0.5px的细线
// 直接设置,兼容性不好
.half-px {
height: 0.5px;
}
// transform
.half-px {
height: 1px;
transform: scaleY(0.5);
transform-origin: 50% 100%;
}
// 线性渐变
.half-px {
height: 1px;
background: linear-gradient(0deg, #fff, #000);
}
// box-shadow
.half-px {
height: 1px;
background: none;
box-shadow: 0 0.5px 0 #000;
}
// svg
题目3: box-shadow原理
# box-shadow:2px 3px 4px #ccc
# 已该元素相同的尺寸和位置,画一个#ccc的矩形
# 把它向右移动2px,向下移动3px
# 使用高斯模糊算法,将它进行4px的模糊处理(边界里外对称4px)
# 模糊后的矩形和原始元素交集部分被切除掉
题目4: 层叠上下文优先级
参考
// 同层层叠上下文,比对index才有意义
// 经典7层
# 根元素的背景和边框
# z-index为负数
# block块状水平盒子
# float浮动盒子
# inline/inline-block盒子
# z-index auto或者看作z-index 0
# 正z-index
前端性能优化总结
参考 前端性能优化总结
浏览器工作原理
参考 浏览器工作原理