Skip to content

2024-11-11

作者:guo-zi-xin
更新于:2 个月前
字数统计:4.9k 字
阅读时长:18 分钟

自我介绍

React的React.memo和useMemo的区别

React.memouseMemo都是用于优化React应用性能的工具,但是他们的应用场景和使用方式有所不同。

  • React.memo 用途:

    React.memo是一个高阶组件(HOC),主要用于优化React函数组件的性能。 当有一个组件,其输出仅依赖于props,而且在props不变时不需要重新渲染,就可以使用React.memo来包裹这个组件, 这样React会在每次渲染前比较新旧props,如果他们相等(浅比较),则跳过不必要的渲染;

使用方式:

直接将组件作为参数传递给React.memo()函数, 例如:const memoComponent = React.memo(myComponent);

  • useMemo 用途:

useMemo是一个Hook, 用于优化React函数组件内部的计算。当有一个耗时的计算或者要创建一个复杂的对象,并且这个计算结果或者对象的依赖没有变化时,就可以使用useMemo来缓存该计算结果或者对象,避免在每次渲染时都重新计算。

使用方式:

在组件内部使用,接收两个参数,一个产生计算结果的函数和一个依赖数组。只有当依赖数组中的值发生改变时,传入的函数才会重新执行。 例如:const computedValue = useMemo(()=> expensiveCalculation(a, b), [a, b])

WARNING

两者是否可以相互转换:

严格来说,React.memouseMemo是不可以相互转换的,因为它们作用的层面不同, React.memo关注的是整个组件的渲染是否需要进行,useMemo则关注的是组件内部某个值的计算是否需要重新执行。 但是,你可以根据需要达到的优化目的,在合适的地方分别使用它们。例如:如果你发现一个组件因为props没有变化却重复频繁渲染,可以考虑使用React.memo来包裹这个组件。如果组件内部有某个复杂计算逻辑频繁执行且结果可以被缓存,那么应该使用useMemo来优化这个计算过程。

React中useMemo、useCallback的区别

  • useCallback

useCallback是一个用于优化性能的React Hook,它的主要作用是避免在每次渲染的时候都重新创建函数。通过记住上一次创建的函数,只有在依赖项在发生变化时才重新创建新的函数,从而提高性能

接收两个参数:

  1. 一个函数, 这个函数就是我们需要记住的函数
  2. 一个依赖项数组,当数组中的依赖项发生变化时,就会重新创建新的函数
tsx
import React, { useState, useCallback } from 'react'

const App:React.FC = () => {
  const [count, setCount] = useState<number>(0);

  const handleClick = useCallback(() => {
    console.log('点击了按钮')
    setCount(count + 1)
  }, [count])

  return  (
    <div>
      <p>点击次数:{count}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  )
}
export default App

在这个例子中,我们使用useCallbackhandleClick函数进行了优化。 只有count变化时,handleClick函数才会被重新创建

  • useMemouseMemo是一个用于优化性能的React Hook, 他的主要作用是避免在每次渲染时都进入复杂的计算和重新创建对象。通过记住上一次的计算结果,只有在依赖项发生变化的时候才重新计算,从而提高性能

接收两个参数:

  1. 一个函数,这个函数返回需要记住的值。
  2. 一个依赖数组,当数组中的依赖项发生变化时候,才会重新计算函数的返回值
tsx
import React, { useState, useMemo } from 'react'

const App:React.FC = () => {
  const [count, setCount] = useState<number>(0)

  const expensiveCalculation = useMemo(()=> {
    console.log('计算中')
    return count * 2
  }, [count])

  return (
    <div>
      <p>结果:{expensiveCalculation}</p>
      <button onClick={()=> { setCount(count + 1) }}>增加</button>
    </div>
  )
}

export default App

在这个例子中, 我们使用useMemocount * 2 这个计算进行了优化。只有当count变化时,expensiveCalculation的值才会重新计算

React中常见的Hooks

  • useState

    用于管理功能组件中的状态

  • useEffect

    用于在功能组件中执行副作用,例如获取数据或订阅事件

  • useContext

    用于访问功能组件中React 上下文的值

  • useRef

    用于创建对跨渲染保留的元素或值的可变引用

  • useCallback

    缓存回调函数,避免在每次渲染时都创建新的回调函数实例

    当回调函数作为prop传递给子组件时,使用 useCallback可以确保子组件在依赖项未变化时不会因为接收到新的函数引起不必要的重新渲染。

    在某些情况下,可以配合 useMemo 使用, 将计算逻辑和函数绑定在一起,从而在依赖项不变时只计算一次

  • useMemo

    用于缓存计算值,类似于 useCallback,但是它缓存的是普通数值而不是回调函数

  • useReducer

    用于使用reducer函数的管理状态,类似于Redux的工作方式

  • useLayoutEffect

    类似于 useEffect 但效果在所有DOM突变后同步运行

这些Hook提供了强大的工具,用于管理状态,处理副作用和重新编辑 React功能组件中的逻辑。

React中 组件我如果不想让他重复渲染 但是它内部又有许多useState变量 我应该如何操作

  • 可以使用React.memo来实现

    • React.memo 是一个高阶组件,它会对传入的组件进行浅比较props 是否相同)。
    • 如果props没有变化,React将跳过组件的渲染过程,从而达到优化的效果。
    • 适用于无状态组件内部状态与父组件无关的组件。
tsx
 import React, { useState, memo } from 'react';

// 组件通过memo来包裹
 const MyComponent = memo(() => {
   const [count, setCount] = useState(0);
   const [text, setText] = useState('');

   console.log('MyComponent Rendered');

   return (
     <div>
       <p>Count: {count}</p>
       <button onClick={() => setCount(count + 1)}>Increment</button>

       <p>Text: {text}</p>
       <input 
         value={text}
         onChange={(e) => setText(e.target.value)}
       />
     </div>
   );
 });

 export default MyComponent;
  • 可以使用useCallbackuseMemo
tsx
import React, { useState, memo, useCallback } from 'react';

const MyComponent = memo(({ handleClick }) => {
  const [count, setCount] = useState(0);

  console.log('MyComponent Rendered');

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleClick}>Parent Function</button>
    </div>
  );
});

const ParentComponent = () => {
  const [parentCount, setParentCount] = useState(0);

  const handleClick = useCallback(() => {
    console.log('Parent function called');
  }, []);

  return (
    <div>
      <button onClick={() => setParentCount(parentCount + 1)}>
        Increment Parent Count
      </button>
      <MyComponent handleClick={handleClick} />
    </div>
  );
};

export default ParentComponent;

Vue2和Vue3的区别

  • 根节点不同 vue2中必须要有根标签,vue3中可以没有根标签,会默认将多个根标签包裹在一个fragement文档碎片中,有利于减少内存

  • 组合式API和选项式API Vue2中,采用的是选项式API,将数据和函数集中起来处理,会将功能点切割,当代码逻辑复杂时,不利于代码阅读

    Vue3中 采用的是组合式API, 将同一个功能的代码集中起来处理,使得代码更加有序,有利于代码的书写和维护

  • 生命周期的变化

    • 创建前: beforeCreate -> 使用setup()
    • 创建后: created -> 使用setup()
    • 挂载前: beforeMount -> onBeforeMount
    • 挂载后: mounted -> onMounted
    • 更新前: beforeUpdate -> onBeforeUpdate
    • 更新后: updated -> onUpdated
    • 销毁前: beforeDestroy -> onBeforeMount
    • 销毁后: destroyed -> onUnMounted
    • 异常捕获:errorCaptured -> onErrorCaptured
    • 被激活: onActivated 被包含在<keep-alive>中的组件,会多出两个生命周期钩子函数,被激活时执行
    • 切换: onDeactivated 例如从A组件切换到B组件,A组件消失后执行

    我们通常会用onMounted钩子在组件挂载后发送异步请求,获取数据并更新组件状态

    这是因为onMounted钩子在组件挂载到DOM后调用,而发送异步请求通常需要确保组件已经挂载,以便正常地操作DOM或者更新组件的状态

  • v-forv-if的优先级 vue2中v-for的优先级高于v-if,可以放在一起使用,但是不建议这么做,会造成性能上的浪费

    vue3中v-if的优先级高于v-for,并且不能够放在一起使用,会报错。 可以在外部添加一个标签,将v-for移到最外层

  • diff算法不同

    • vue2的diff算法 主要是使用了 双端diff算法 遍历每一个虚拟节点,进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方。用patch记录的消息去更新dom

      缺点: 比较每一个节点,而对于一些不参与更新的元素,进行比较是有点消耗性能的。

      特点: 特别要提一下Vue的patch是即时的,并不是打包后所有修改最后一起操作DOM,也就是在vue中边记录边更新。(React则是将更新放入队列后集中处理)

      流程:

      1. 对比头头、尾尾、头尾、尾头是否可以复用,如果可以复用,就进行节点的更新或移动操作
      2. 如果经过四个端点的比较,都没有可复用的节点,则将旧的子序列保存为节点keykeyindexvaluemap
      3. 拿新的一组子节点的头部节点去map中查找,如果可以找到可复用的节点,则将相应的节点进行更新,并将其移动到头部,然后头部指针右移
      4. 然而,用新的一组子节点中的头部节点去旧的一组子节点中去寻找可复用的节点,并非总能找到,这说明这个新的头部节点是新增节点,只需要将其挂载到头部即可
      5. 经过上述处理,最后还剩下新的节点就批量新增,剩下的旧的节点就批量删除。
    • vue3的diff算法 vue3的diff算法与vue2一样 也是会先进行双端比对,只是双端比对的方式不一样。vue3的diff算法借鉴了字符串比对时的双端比对方式,即优先处理可复用的前置元素和后置元素

      流程:

      1. 处理前置节点
      2. 处理后置节点
      3. 新节点有剩余,则挂载剩余的新节点
      4. 旧节点有剩余,则写在剩余的旧节点
      5. 乱序情况(新、旧节点都有剩余), 则构建 最长递增子序列
      6. 节点在最长递增子序列中, 则该节点不需要移动
      7. 节点不在最长递增子序列中,则移动该节点
  • 响应式原理的不同 vue2 主要通过Object.defineproperty()get()set()方法来做数据劫持,结合发布订阅者模式来实现

    vue3中则通过Proxy代理的方式来实现。

总结

  • 更快的渲染性能
  • 更小的体积
  • 更好的Typescript支持
  • 更灵活的组合式API
  • 更好的响应式系统

Vue响应式如何实现的

  • Vue2

    • Vue2中的数据响应式会根据数据类型做不同的处理。如果是对象,则通过Object.defineProperty(obj, key, descriptor)拦截对象属性访问,当数据被访问或者被改变时,感知并作出反应; 如果是数组,则通过覆盖数组原型方法,扩展它的7个变更方法(pushpopshiftunshiftsplicesortreverse),使这些方法可以额外的做更新通知,从而做出响应

    • 缺点

      • 初始化时的递归或者遍历会造成性能损失;
      • 通知更新过程需要维护大量dep实例和watcher实例,额外占用的内存比较多;
      • 新增或删除对象属性无法拦截,需要通过Vue.set()以及delete这样的API才会生效
      • 对于ES6中新产生的MapSet等数据结构不支持
  • Vue3

    • Vue3响应式使用的是ES6的proxyReflect相互配合实现数据响应式,解决了Vue2中视图不能自动更新的问题;
    • Proxy是深度监听, 所以可以监听对象和数组内的任意元素,从而可以实现视图实时更新;
    • 响应式大致分为三个阶段:
      • 初始化阶段 初始化阶段通过组件初始化方法形成对应的proxy对象,然后形成一个负责渲染的effect
      • get依赖收集阶段 通过解析template,替换真实data属性,来触发get,然后通过satck方法,通过proxy对象和key形成对应的deps,将负责渲染的effect存入deps。(这个过程还有其他的effect,比如watchEffect存入deps中)
      • set派发更新阶段 当我们this[key] = value改变属性的时候,首先通过trigger方法,通过Proxy对象和key找到对应的deps, 然后给deps分类分成computedRunnerseffect,之后依次执行,如果需要调度的,直接放入调度

    WARNING

    Proxy只会代理对象的第一层,那么Vue3是如何处理这个问题的呢?

    • 判断当前Reflect.get()的返回值是否为Object, 如果是则在通过reactive方法做袋米,这样就实现了深度观测

    WARNING

    监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

    • 我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

Typescript中 type和interface的区别

  • typetype是类型别名,顾名思义,类型别名只是给类型起一个新名字。它并不是一个类型,只是一个别名而已; 有了type,书写Typescript类型会更方便

  • interfaceinterface(接口)是TS设计出来定义对象类型的,可以对对象的形状进行描述

    • 相同点

      1. 两者都可以定义一个对象或函数
      2. 都允许继承(extends)
      typescript
        /** interface 继承 interface  使用 extends 关键字 */
        interface Person {
          name:string;
        }
      
        interface Student extends Person {
          grade: number;
        }
      
        const person:Student = {
          name: 'xxx',
          grade: 100
        }
      
        /** type 继承 type  使用交叉类型 */
        type Person = {
          name:string;
        }
      
        type Student = Person & { grade: number }
      
        /** interface 继承 type */
        type Person = {
          name: string
        }
      
        interface Student extends Person {
          grade:number;
        }
      
      /** type 继承 interface */
      interface Person {
        name: string;
      }
      
      type Student = Person & { grade: number }
    • 差异 不同点

      • type 可以声明基本类型、联合类型、交叉类型、元组, interface只能声明对象

        typescript
        type Name = string // 基本类型
        
        type ArrItem = number | string  // 联合类型
        
        const arr:ArrItem[] = [1, '2', 3]
        
        type Person = { name: string }
        
        type Student = Person & { grade: number } // 交叉类型
        
        type Teacher = Person & { major: string }
        
        type StudentAndTeacherList = [Student, Teacher]  // 元组类型
        
        const list: StudentAndTeacherList = [
          { name: 'xxx', grade: 100 },
          { name: 'xxxx', major: 'Chinese' }
        ]
      • type 不能够合并重复声明, interface是可以的

        typescript
        interface Person {
          name: string
        }
        
        interface Person {  // 重复声明的 interface 会自动合并
          age: number
        }
        
        const person: Person = {
          name: 'xxx',
          age: 18
        }
        
        type Person = {
          name: string;
        }
        
        type Person = {   // Duplicate identifier 'Person'
          age: number
        }
        
        const person: Person = {
          name: 'lin',
          age: 18
        }

Css中我想实现一个上下固定容器 中间自适应的布局我应该如何实现

  • 可以使用flex布局来实现

通过给父级容器设置flex属性,并且让他排列方式为垂直排列:flex-direction:column, 中间容器设置flex:1可以实现此布局

html
  <style>
    .container {
      display: flex;
      flex-direction: column;
      height: 100vh;
    }
    .header {
      background-color: #f8b400;
      padding: 20px;
      text-align: center;
    }
    .content {
      flex: 1;
      background-color: #d1e8e2;
      padding: 20px;
      overflow-y: auto;
    }
    .footer {
      background-color: #6a0572;
      padding: 20px;
      text-align: center;
      color: #fff;
    }
  </style>

  <div class="container">
    <div class="header">Header (固定高度)</div>
    <div class="content">Content (自适应高度)</div>
    <div class="footer">Footer (固定高度)</div>
  </div>
  • 通过Grid布局来实现

    通过给父级元素设置display:grid, 并且设置网格容器纵向高度为grid-template-rows: auto 1fr auto;, 表示上下两个容器的高度随容器调整,并且中间的容器按比例分配剩余所有高度

html
  <style>
    .container {
      display: grid;
      grid-template-rows: auto 1fr auto;
      height: 100vh;
    }
    .header {
      background-color: #f8b400;
      padding: 20px;
      text-align: center;
    }
    .content {
      background-color: #d1e8e2;
      padding: 20px;
      overflow-y: auto;
    }
    .footer {
      background-color: #6a0572;
      padding: 20px;
      text-align: center;
      color: #fff;
    }
  </style>

  <div class="container">
    <div class="header">Header (固定高度)</div>
    <div class="content">Content (自适应高度)</div>
    <div class="footer">Footer (固定高度)</div>
  </div>

Vue3中 watch和watchEffect的区别

  • watchwatchEffect都是监听器,watchEffect是一个副作用函数,他们之间的区别有:
    1. watch:既要指明监听数据的源,也要指明监听的回调;
    2. watchEffect: 可以自动监听数据源作为依赖,不用指明监听哪个数据,监听的回调中用到哪个数据,那就监听哪个数据;
    3. watch可以访问改变前后的值, watchEffect只能获取改变后的值;
    4. watch在运行的时候,不会立即执行,值改变后才会执行; watchEffect运行后可以立即执行, 这一点可以通过watchEffect的配置项immeriate来改变
    5. watchEffect有点像computed:
    • computed注重的是计算出来的值(回调函数的返回值),所以必须写返回值;
    • watchEffect注重的是过程(回调函数的函数体),所以不用写返回值
    • watchEffect所指定的回调中用到的数据只要发生变化,则重新执行回调

手写题

  • 编写一个函数来查找字符串数组中的最长公共前缀,如果不存在公共前缀,返回空字符串 ""

    • 示例1: 输入:str = ["flower","flow","flight"]; 输出:"fl"
    • 示例2: 输入:str = ["dog","racecar","car"]; 输出:""

水平扫描法

typescript
  const fun = (arr: string[]): string => {
    if (arr.length === 0) return ''

    // 取第一个字符串作为初始前缀
    let prefix = arr[0]
    // 从第二个字符串开始遍历
    for (let k = 1; k < arr.length; k++) {
      // 不断缩短前缀,直到当前字符串包含这个前缀
      while (arr[k].indexOf(prefix) !== 0) {
        // 去掉前缀后的最后一个字符
        prefix = prefix.slice(0, -1);
        if (prefix === '') return ''
      }
    }
    return prefix
  }

二分法

typescript
  // 检查所有字符串时候都带有指定前缀
  const checkHasPrefix = (arr: string[], prefix: string): boolean => {
    return arr.every(item => item.startsWith(prefix))
  }

  // 二分法查找
  const fun2 = (arr: string[]): string => {
    // 寻找最短字符串的长度
    const minLen = Math.min(...arr.map(item => item.length))
    let low = 0
    let high = minLen

    // 二分法查找最长公共前缀
    while (low <= high) {
      const mid = Math.floor((low + high) / 2)
      const prefix = arr[0].substring(0, mid)
      if (checkHasPrefix(arr, prefix)) {
        // 尝试更长的前缀
        low = mid + 1
      } else {
        // 尝试缩短前缀
        high = mid - 1
      }
    }
    // 最长公共前缀的长度为high
    return arr[0].substring(0, high)
  }
  • 寻找字符串中出现最多的字符串

    • 示例:var str = "afjghdfraaaasdenas"; 输出 'a'
typescript
  const func = (str: string): string => {
    const map: Record<string, number> = {}
    let maxChar = ''
    let maxCount = 0

    //统计每个字符出现的次数
    for (const key of str) {
      map[key] = (map[key] || 0) + 1

      //如果当前字符的计数超过了maxCount, 更新maxChar和maxCount
      if (map[key] > maxCount) {
        maxChar = key
        maxCount = map[key]
      }
    }
    return maxChar
  }
  • 树形结构
typescript
  const list = [
    {id: 1, name: '部门1', pid: 0},
    {id: 2, name: '部门2', pid: 1},
    {id: 3, name: '部门3', pid: 1},
    {id: 4, name: '部门4', pid: 3},
    {id: 5, name: '部门5', pid: 4},
  ]

  interface TreeNode {
    id: number;
    pid: number;
    name: string;
    children?: TreeNode[]
  }

  const buildTree = (data: TreeNode[]): TreeNode[] => {
    const map: Record<number, TreeNode> = {};
    const result: TreeNode[] = []

    //先遍历数组,将每个元素的id作为键,将元素本身放在map中
    data.forEach(item => {
      map[item.id] = { ...item, children: [] }
    })

    //再次遍历数组,构建树形结构
    data.forEach(item => {
      const { pid } = item;
      if (pid === 0) {
        //pid为0的时候是根节点
        result.push(map[item.id])
      } else {
        // 否则, 将当前元素添加到其父元素的children数组当中
        if (map[pid]) {
          map[pid].children!.push(map[item.id])
        }
      }
    })
  • reduce实现树形结构
typescript
  const list = [
    {id: 1, name: '部门1', pid: 0},
    {id: 2, name: '部门2', pid: 1},
    {id: 3, name: '部门3', pid: 1},
    {id: 4, name: '部门4', pid: 3},
    {id: 5, name: '部门5', pid: 4},
  ]

  interface TreeNode {
    id: number;
    pid: number;
    name: string;
    children?: TreeNode[]
  }

  const reduceTree = (arr: TreeNode[]): TreeNode => {
    const map: Record<number, TreeNode> = {}
    return arr.reduce<TreeNode[]>((acc, cur) => {
      // 为每个节点初始化 children 数组
      map[cur.id] = { ...cur, children: map[cur.id]?.children || [] };
      const treeCur = map[cur.id];
      if (cur.pid === 0) {
        //根节点直接推入acc 结果数组
        acc.push(treeCur)
      } else {
        //如果不是根节点,将其添加到父节点的 children当中
        if (!map[cur.pid]) {
          map[cur.pid] = { id: item.pid, pid: 0, name: '', children: [] }
        }
        map[cur.pid].children!.push(treeCur)
      }
      return acc
    }, [])
  }

人生没有捷径,就像到二仙桥必须要走成华大道。