前端框架面试题

385 阅读23分钟

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并未限制数据流,ModelView之间可以通信
------
controller里调用 this.model.setState()
model里调用,this.render.render()
    controller ->  model -> view -> controller
    
// mvp
MVP则限制了ModelView的交互都要通过Presenter,这样对ModelView解耦,提升项目维护性和模块复用性
-----
this.presenter 里调用this.model.setState() 
this.presetnter里调用this.render(this.model.getState())
    view <-> presenter <-> model
 
// mvvmMVVM是对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可能被合并,可能异步更新

参考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.targetnull
# 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性能优化

参考:

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之后,会再次渲染组件

参考 concurent mode | fiber

题目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负责更新路由信息并将信息传递,RouteRoutes则根据context的改变,获取当前的路由信息,再根据自己的path、exact等属性判断是否需要渲染,然后再根据component、children、render属性渲染指定的组件。

# HashRouterBrowserRouter都是继承自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

image.png

image.png

redux

参考

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)

参考

react原理

fiber

react fiber算法

题目11: react-hooks

参考

react-hooks使用

react-hooks

react-hooks原理

# useState
# useEffect
    // 异步
# useLayoutEffect
    // 同步
    // useLayoutEffectClear -> useLayoutEffect -> 渲染-> useEffectClear -> useEffect
# useReducer
# useMemo
# useCallback
# useImperativeHandle 
    // 配合React.forwardRef使用

webpack

参考

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方式
    - DllPluginDllReferencePlugin
    - 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

// 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),而内建的变量(如PromiseSymbol)却无法兼容
- 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的内置(如PromiseMapSet)命名别名,这样可以避免污染全局变量
- 缺陷:
    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

前端性能优化总结

参考 前端性能优化总结

浏览器工作原理

参考 浏览器工作原理