前端(Vue & React & TS)超高频考点解析

1,003 阅读11分钟

Vue

1.computed 和 watch 区别

  • computed 根据已有值计算出新值,有缓存
  • watch 用于监听现有数据变化,无缓存

watch 能取代 computed 的任何的操作,反过来不行,比如 watch 中可以进行异步操作

2.生命周期

Vue2(3)生命周期:

选项式 APIHook inside setup
beforeCreate:初始化一个空的 Vue 实例,data methods 等尚未被初始化,无法调用Not needed*(setup)
created:Vue 实例初始化完成,data methods 都已初始化完成,可调用,但尚未开始渲染模板Not needed*(setup)
beforeMount:编译模板,调用 render 函数生成 vdom ,但还没有开始渲染 DOMonBeforeMount
mounted:渲染 DOM 完成,页面更新。组件创建完成,开始进入运行阶段,发送网络请求可以在此时onMounted
beforeUpdate:在数据发生改变后,DOM 被更新之前被调用,这里适合在现有 DOM 将要被更新之前访问它onBeforeUpdate
updated:在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用,尽量不要在 updated 中继续修改数据,否则可能会触发死循环onUpdated
beforeUnmount:组件进入销毁阶段,在这个阶段,实例仍然是完全正常的,移除、解绑一些全局事件、自定义事件,可以在此时操作onBeforeUnmount
unmounted:卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载onUnmounted
errorCaptured:捕获错误onErrorCaptured
activatedkeep-alive :缓存的组件激活时调用onActivated
deactivated:被 keep-alive 缓存的组件停用时调用onDeactivated

3.组件通信

  • 父子组件:
    • 使用 props 和 emits 进行通信
    • 使用 $attrs(可以通过 v-bind="$attrs" 向下级传递) 、$parent$refs
  • 爷孙组件:
    • 使用两次父子组件间通信来实现
    • 使用provide + inject来通信
  • 任意组件:
    • vue2 使用 eventBus = new Vue() 通信
    • vue3 使用如 mitt库 通信
    • vue2 ==> Vuex Vue3 ==> Pinia

4.Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理工具

  • store 是个大容器,包含以下所有内容
  • state 读取状态,带有一个 mapState 辅助函数
  • getters 读取派生状态,附有一个 mapGetters 辅助函数
  • mutations 同步提交状态变更,附有一个 mapMutations 辅助函数,建议原子操作(每次只修改一个数据)
  • actions 异步变更状态,它提交的是 mutation 间接变更状态,附有一个 mapActions 辅助函数
  • Module 给 store 划分模块,方便维护代码
  • namespaced: 模板开启命名空间,保证不冲突

Vue3 使用 useStore () 获得 store 实例,setup 使用 map 的两个映射方法 mapState 和 mapGetters 需要封装才能使用

注意:Pinia去除了 mutations 和 Module ,所有更改都是通过 action

5.VueRouter

Vue Router 是 Vue.js 的官方路由,用来构建 SPA 单页应用

说出一些概念

  • router-link router-view 、嵌套路由、Hash 模式 和 History 模式、导航守卫、懒加载、动态路由、缓存路由

Vue-router v4 升级之后,mode: 'xxx' 替换为 API 的形式,但功能是一样的

  • mode: 'hash' 替换为 createWebHashHistory()
  • mode: 'history' 替换为 createWebHistory()
  • mode: 'abstract' 替换为 createMemoryHistory()

三种模式区别:

  • Hash 模式是基于锚点(#),本质改变 window.location.href 改变 URL 但是不刷新页面,而且 hash 值不会包含 HTTP 请求,监听hashchange事件,根据当前路由地址找到对应组件重新渲染
  • History 模式是基于 HTML5 中的 History API(/),有六种模式改变 URL 不刷新页面,兼容性和 hash 模式相比略差,需要服务端支持,History.pushState() 方法改变地址栏,监听 popstate 事件,根据当前路由地址找到对应组件重新渲染
    • replaceState:替换原来的路径
    • pushState:使用新的路径
    • popState:路径的回退
    • go:向前或向后改变路径
    • forward:向前改变路径
    • back:向后改变路径
  • abstract 模式不修改 url ,路由地址在内存中,但页面刷新会重新回到首页

注意:Vue3 使用 useRouter() 和 useRoute() 获得 router 和 route 实例

react-router 同有三种模式,设计上和 vue-router 一样

6.双向绑定

  • 一般使用 v-model / .sync(vue3被费力) 实现,v-model 是 v-bind:value 和 v-on:input 的语法糖
  • v-bind:value 实现了 data ⇒ UI 的单向绑定
    • 通过 Object.defineProperty API 给 data 创建 getter 和 setter,用于监听 data 的改变,data 一变就会改变 UI
  • v-on:input 实现了 UI ⇒ data 的单向绑定
    • 通过 template compiler 给 DOM 添加事件监听,DOM input 的值变了就 会去修改 data

7.Vue3为什么使用Proxy

弥补 Object.defineProperty 的不足

  • 深度监听、性能更好,惰性的数据响应式(需要用到才转换)
  • 可监听新增/删除属性(get set deleteProperty)
  • 可原生监听数组变化
  • 和 Refelct 天生搭档,更加标准化函数化

缺点:

  • Proxy无法兼容所有浏览器,无法polyfill

8.Vue3为什么使用Composition API

Composition API 比 mixins、extends、高阶组件 更好的原因:

  1. 模版中的数据来源
  2. 命名空间
  3. 性能
  4. 更适合 TypeScript

9.Vue3更新了什么

详细的可以看我这篇文章:40张图全面剖析Vue3核心的CompositionAPI(十四) - 掘金 (juejin.cn)

我这里概括下

源码升级:

  • 源码TS重写,全新的TS支持
  • 使用 Proxy 代替 Object.defineProperty 实现响应式
  • 重写虚拟DOM的实现
  • Tree-Shaking 和 SSR

新特性:

  • Composition API(组合API)
  • 新的内置组件
    • Fragment
    • Teleport
    • Suspense
  • createApp() 代替了 new Vue() 返回 app 实例
  • 移除 过滤器filter、keyCode修饰符、.sync修饰符、onon,off 和 $once
  • 组件驱动动态 CSS 值

更好的周边工具:

  • Vite、Pinia

10.vdom真的很快吗

  • 直接进行 DOM 操作永远都是最快的(但要目标明确,不能有无谓的 DOM 操作 —— 这很难)
  • 如果业务复杂,要“数据视图分离,数据驱动视图”,无法直接修改 DOM ,那 vdom 就是一个很好的选择

所以,vdom 并不比 DOM 操作更快(反而更慢,它做了 JS 运算),它只是在某个特定的场景下,无法做到精准 DOM 修改时,一个更优的选择

扩展:Svelte 不使用 vdom ,它将组件修改,编译为精准的 DOM 操作

11.如何正确的操作 DOM

mountedupdated 都不会保证所有子组件都挂载完成,如果想等待所有视图都渲染完成,需要使用 $nextTick

mounted() {
  this.$nextTick(function () {
    // 仅在整个视图都被渲染之后才会运行的代码
  })
}

React

1.虚拟DOM

  • 虚拟 DOM 就是虚拟节点,React 用 JS 对象来模拟 DOM 节 点,然后将其渲染成真实的 DOM 节点
  • 用 JSX 语法写出来的 div 其实就是一个虚拟节点(因为 JSX 语法会被转译为 createElement 函数调用,也叫 h 函 数)
function render(vdom) {
    // 如果是字符串或者数字,创建一个文本节点 
    if(typeof vdom === 'string' || typeof vdom === 'number') { 
        return document.createTextNode(vdom)
    } 
    const { tag, props, children } = vdom
    // 创建真实DOM 
    const element = document.createElement(tag) 
    // 设置属性
    setProps(element, props) 
    // 遍历子节点,并获取创建真实DOM,插入到当前节点
    children.map(render).forEach(element.appendChild.bind(element)) 
    //虚拟DOM 中缓存真实 DOM 节点
    vdom.dom = element
    // 返回 DOM 节点
    return element
} 
function setProps // 略

优点:

  • DOM 操作不方便问题。以前各种 DOM API 要记,现在只有 setState

  • 让 DOM 操作的整体性能更好,能(通过 diff)减少不必要的 DOM 操作

  • 为 React 带来了跨平台能力,因为虚拟节点除了渲染为真实节点,还可以渲 染为其他东西

缺点:

  • 性能要求极高的地方,还是得用真实 DOM 操作(太少)
  • React 为虚拟 DOM 创造了合成事件,跟原生 DOM 事件不太一样,工作中要额外注意
    • 所有 React 事件都绑定到根元素,自动实现事件委托
    • 如果混用合成事件和原生 DOM 事件,有可能会出 bug

2.React VS Vue 的 DOM diff 算法

  • DOM diff 就是对比两棵虚拟 DOM 树的算法
  • 当组件变化时,会 render 出一个新的虚拟 DOM
  • diff 算法对比新旧虚拟 DOM 之后,得到一个 patch,然后 React 用 patch 来更新真实 DOM

优化

传统的 tree diff 算法复杂度是 O(n^3) ,算法不可用

Vue React 都是用于网页开发,基于 DOM 结构,对 diff 算法都进行了优化

  • 只在同一层级比较 ,tag 不同则直接删掉重建
  • 子节点会通过 key 区分
  • 最终把时间复杂度降低到 O(n)

React diff 特点 - 仅向右移动

比较子节点时,仅向右移动,不向左移动

Vue2 diff 特点 - 双端比较

定义四个指针,分别比较

  • oldStartNode 和 newStartNode
  • oldStartNode 和 newEndNode
  • oldEndNode 和 newStartNode
  • oldEndNode 和 newEndNode

然后指针继续向中间移动,直到指针汇合

Vue3 diff 特点 - 最长递增子序列

Vue3 针对 key 做不同操作

  • 有key,那么就使用 patchKeyedChildren方法
  • 没有key,那么久使用 patchUnkeyedChildren方法

例如数组 [3,5,7,1,2,8] 的最长递增子序列就是 [3,5,7,8 ] 。这是一个专门的算法

算法步骤

  • 通过 从头开始进行遍历比较找到开始的不变节点 [A, B]
  • 通过从尾部开始进行遍历比较找到末尾的不变节点 [G]
  • 此时如果旧节点多则删除,新节点多则新建
  • 剩余的有变化的节点 [F, C, D, E, H]
    • 通过 newIndexToOldIndexMap 拿到 oldChildren 中对应的 index [5, 2, 3, 4, -1]-1 表示之前没有,要新增)
    • 计算最长递增子序列得到 [2, 3, 4] ,对应的就是 [C, D, E] ,即这些节点可以不变
    • 剩余的节点,根据 index 进行新增、删除

该方法旨在尽量减少 DOM 的移动,达到最少的 DOM 操作

3.setState

setState 默认异步更新

componentDidMount() {
  this.setState({val: this.state.val + 1}, () => {
    // 回调函数可以拿到最新值
    console.log('callback', this.state.val)
  })
  console.log(this.state.val) // 拿不到最新值
}

setState 有时同步更新

  • 根据 setState 的触发时机是否受 React 控制
  • 如果触发时机在 React 所控制的范围之内,则异步更新
    • 生命周期内触发
    • React JSX 事件内触发
  • 如果触发时机不在 React 所控制的范围之内,则同步更新
    • setTimeout setInterval
    • 自定义的 DOM 事件
    • Promise then
    • ajax 网络请求回调

setState 默认会合并

  • 多次执行,最后 render 结果还是 1
componentDidMount() {
  this.setState({val: this.state.val + 1})
  this.setState({val: this.state.val + 1})
  this.setState({val: this.state.val + 1})
}

setState 有时不会合并

  1. 同步更新,不会合并
  2. 传入函数,不会合并 (因为对象可以 Object.assign,函数无法合并)
this.setState((prevState, props) => {
  return { val: prevState.val + 1 }
})

setState 是 React 最重要的 API,使用要注意:

  • 使用不可变数据
  • 合并 vs 不合并
  • 异步更新 vs 同步更新

setState是同步还是异步

  • setState 是同步执行,state 都是同步更新,只不过让 React 做成了异步的样子
  • 因为要考虑性能,多次 state 修改,只进行一次 DOM 渲染
  • 所以不是微任务也不是宏任务

4.React 如何实现组件间通信

  1. 父子组件通信:props + 函数
  2. 爷孙组件通信:两层父子通信或者使用 Context.Provider 和 Context.Consumer
  3. 任意组件通信:消息订阅-发布、集中式管理(Redux 、Mobx)

5.React生命周期

react生命周期(新)
  1. 挂载时调用 constructor,更新时不调用
  2. 更新时调用 shouldComponentUpdate 和 getSnapshotBeforeUpdate,挂载时不调用
  3. should... 在 render 前调用,getSnapshot... 在 render 后调用
  4. 请求最好放在 componentDidMount 里

6.高阶组件 HOC?

  • 参数是组件,返回值也是组件的函数

例如

  1. React.forwardRef
  2. ReactRedux 的 connect
  3. ReactRouter 的 withRouter

参考阅读:「react进阶」一文吃透React高阶组件(HOC) - 掘金 (juejin.cn)

6.Redux

Redux 是一个状态管理库/状态容器

工作流程

  1. 组件通过 dispatch 方法触发 Action( type + payload 荷载)
  2. Store 接受 Action 并将 Action 分发给 Reducer
  3. Reducer 根据 Action 类型对状态进行更改并将更改后的状态返回给 Store
  4. 组件订阅了 Store 中的状态, Store 中的状态更新会同步到组件

React-redux核心概念:

  • connect(mapStateToProps,mapDispatchToProps)(Component)

connect方法接受两个参数,返回一个高阶组件

connect方法的第一个参数是mapStateToProps方法,将store中的state传递到组件的props

onnect方法的第二个参数是mapDispatchToProps方法,将store中的dispatch传递到组件的props

connect方法可以帮我们订阅store,当store中状态发生变更的时候,帮助我们重新渲染组件

Typescript

1.TS和JS区别

  1. 语法层面:TypeScript = JavaScript + Type(TS 是 JS 的超集)

  2. 执行环境层面:浏览器、Node.js 可以直接执行 JS,但不能执行 TS(Deno 可以执行 TS)

  3. 编译层面:TS 有编译阶段,JS 没有编译阶段(只有转译阶段和 lint 阶段)

  4. 编写层面:TS 更难写一点,但是类型更安全

  5. 文档层面:TS 的代码写出来就是文档,IDE 可以完美提示。JS 的提示主要靠 TS

2.any、unknown、never 的区别

any VS unknown

二者都是顶级类型(top type),任何类型的值都可以赋值给顶级类型变量:

let foo: any = 123; // 不报错
let bar: unknown = 123; // 不报错

但是 unknown 比 any 的类型检查更严格,any 什么检查都不做,unknown 要求先收窄类型:

const value: unknown = "Hello World";
const someString: string = value; 
// 报错:Type 'unknown' is not assignable to type 'string'.(2322)
const value: unknown = "Hello World";
const someString: string = value as string; // 不报错

如果改成 any,基本在哪都不报错。所以能用 unknown 就优先用 unknown,类型更安全一点

never

never 是底类型,表示不应该出现的类型,这里有一个尤雨溪给出的例子

interface A {
  type: 'a'
}

interface B {
  type: 'b'
}

type All = A | B

function handleValue(val: All) {
  switch (val.type) {
    case 'a':
      // 这里 val 被收窄为 A
      break
    case 'b':
      // val 在这里是 B
      break
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val
      break
  }
}

3.type 和 interface 的区别

官方给出的文档说明

  1. 组合方式:interface 使用 extends 来实现继承,type 使用 & 来实现交叉类型
  2. 扩展方式:interface 可以重复声明扩展(合并),type 一个类型只能声明一次
  3. 范围不同:type 适用于基本类型,interface 一般不行
  4. 命名方式:interface 会创建新的类型名,type 只是创建类型别名,并没有新创建类型

4.TS 工具类型

  1. 将英文翻译为中文。

    • Partial 部分类型

    • Required 必填类型

    • Readonly 只读类型

    • Exclude 排除类型

    • Extract 提取类型

    • Pick / Omit(主要针对对象)排除 key 类型

    • ReturnType 返回值类型

  2. 举例说明每个工具类型的用法