react面试题

416 阅读26分钟

一、受控组件 非受控组件

- 受控组件指 组件里的value受到state控制,用onChange事件处理函数处理value的变化并同步到state,
- 非受控组件即value与state没有关联,需要用到的时候,用ref获取

二、setState执行流程

当调用 setState 后,新的 state 并没有马上生效渲染组件,而是,先看执行流中有没有在批量更新中,如果有,push 到存入到 dirtyeComponent 中,如果没有,则遍历 dirty 中的component,调用 updateComponent,进行 state 或 props 的更新,然后更新 UI,react 进行 diff

运算,与上一次的虚拟 DOM 相比较,进行有效的渲染更新组件

三、setState批量更新

setState接收一个新的状态,该接收到的新状态不会被立即执行么,而是存入到pendingStates(等待队列)中判断isBatchingUpdates(是否是批量更新模式): 1 >. isBatchingUpdates: true 将接收到的新状态保存到dirtyComponents(脏组件)中 2 >. isBatchingUpdates: false 遍历所有的dirtyComponents, 并且调用其 updateComponent方法更新pendingStates中的state 或者props。执行完之后,会 将isBatchingUpdates: true。

四、getDerivedStateFromProps(nextPops,prevState)

 - 在初始化和更新流程中,接受父组件的props数据,可以对props进行格式化,过滤等操作,返回值将作为新的state更新到state中,返回值与state合并完,newState可以作为shouldComponentUpdate的第二个参数
 
 - 是静态属性方法,内部访问不到this的,它更趋向纯函数
 - 取代 componentWillMout 和componentWill ReceiveProps

五、Suspense 异步组件

原理:

在React 16.6及以后的版本中,React.Suspense 组件被引入作为一种处理组件懒加载和异步渲染的新方法。 React.Suspense 使得开发者能够将应用程序中的某些部分标记为“可以暂停”,直到其依赖的异步组件或数据被加载完成。

如何使用 React.Suspense

React.Suspense 组件需要两个主要的APIs来配合工作:React.lazyPromise

  1. React.lazy: React.lazy 是一个函数,它允许你按需加载组件。当你的应用程序需要渲染一个懒加载的组件时,你可以使用 React.lazy 来动态地导入该组件。

    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    

    React.lazy 返回一个 Promise,该 Promise 在组件加载完成后解决。

  2. React.Suspense: React.Suspense 组件用来包裹可能还没有加载完成的异步组件。它接收一个 fallback 属性,这个属性是一个在异步组件加载过程中展示的备用UI。

    <React.Suspense fallback={<div>Loading...</div>}>
      <OtherComponent />
    </React.Suspense>
    

    OtherComponent 还未加载完成时,用户会看到一个 "Loading..." 的提示。一旦 OtherComponent 加载完成并被解析,React.Suspense 会暂停当前的渲染,然后使用已加载的组件来替换 fallback 内容。

异步组件的工作原理

当你使用 React.lazy 加载一个组件时,React会执行提供的函数并返回一个 Promise。这个 Promise 将在组件的模块被加载和解析后解决。在等待模块加载的过程中,React.Suspense 会渲染 fallback 属性指定的内容。

一旦组件加载完成,React会保留已加载的组件,以便将来的请求可以立即使用,而不需要再次加载。这个过程称为缓存。

处理错误

React.Suspense 也可以用来处理异步加载过程中的错误。你可以在 fallback 中返回一个错误提示,或者使用错误边界(Error Boundaries)来捕获和处理错误。

总结

React.SuspenseReact.lazy 提供了一种强大的方式来处理React应用中的异步组件加载。通过这种方式,你可以提高应用程序的性能,实现更快的页面加载,同时提供更好的用户体验。这种机制使得代码分割和懒加载变得简单,是现代Web应用开发中不可或缺的一部分。

优点: 拿到数据后再渲染,少渲染一次

六、 constructor

1.   在组建准备要挂载之前开始执行,此时组建还没挂载到网页上

七、componentWillMount

在这里调用setState方法不会触发重新render 所以一般不会用来作加载数据用处

八、componetDidMount

该处的代码是组件已经完全挂在网页上才会被调用执行,在这里调用setState会触发重新渲染,所以一般在这地方做数据加载使用

九、react16 新生命周期有哪些

react 16之后有三个生命周期被废弃(但未被删除)

    - componentWillMount

    - componentWillReceiveProps
        
    - componentWillUpdate

目前React16.8+的生命周期主要分三个阶段,分别是挂载阶段、更新阶段、卸载阶段

挂载阶段

   1、constrcutor
      - 初始化数据,给函数绑定this
   2、getDerivedStateFromProps
       - 静态方法,没有this,当props更新的是 通过新的prop修改state
   3、render
       - render函数是纯函数,只返回需要渲染的东西
   4、 componentDidMount:
       - 组件装载之后调用,此时我们可以获取dom节点,并操作

更新阶段

   1、getDerivedStateFromProps:
       每个props更新阶段都会调用
   - shouldComponentUpdate:
   2、 一共两参数 分别是newProps,newState,返回一个bollen值,true就是重新渲染,false不渲染,默认是true
   3、render: 更新阶段也会触发此生命周期
   4、getSnapshotBeforeUpdate: 
       两参数: 分别是preProps,preState,表示之前的props和state,这个函数有一个返回值,会作为componentDidUpdate的第三个参数,如果该方法不想返回值 可返回null
   5、componentDidUpdate:
       componentDidUpdate(prevProps,prevState,snapshot),表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的
 

卸载阶段

   1、componentWillunMount
   

1. 挂载阶段

  1. constructor
  2. getDerivedStateFromProps
  3. render
  4. comoponentDidMount

2. 更新过程

  1. getDerivedStateFromProps
  2. shouldComponentUpdate
  3. render
  4. getSpapshotBerforUpdate
  5. compoentDidUpdate

3. 写卸载阶段

  1. componentWillUnmount

组件通信

一、 跨级组件通信

方法一 : 使用creatContect

1. 使用crateContext
2. 调用crateContext方法 获取一个实例对象
3. 父级最外层用 使用该实例对象的Provider标签进行包裹 并将共享数据放在value属性中
4. 在组件使用该属性时,需要在组件最外层使用该实例的Consumer标签包裹  
  
const BatteryContext = createContext()
<BatteryContext.Provider value={color}>
    <Child/>
</<BatteryContext.Provider>
class Child extends Component {
   render(){
       return (
           <BatteryContext.Consumer>
               {
                   color && <h1 style={{color:color}}>我是红色的 </h1>
               }
           </BatteryContext.Consumer>
       )
   }

}

方法二、 使用recoil

1、 使用atom创建共享数据变量
2、 使用selector 获取共享atom, 并发起网络请求
3、 使用useRecoilState获取共享数据变量,该方法返回一个数组,该数据有两个字段,类似useState 第一个数据变量,第二个是方法

方法三、 使用dva

创建一个文件,该文件导出一个对象,该对象有以下属性,namespace,state,effects,reducers
namespace: 是独一无二的 名称不能重复
state: 是需要共享的数据变量
effects: 用来发起网络请求,通过yield call(方法名) 来调用reducers中的方法,触发数据改变
reducers: 通过传参,返回一个对象,更改state中变量的值

方法四: redux

路由

React-Router的实现原理

 1、客户端路由实现的思想
    1、基于hash路由:
        通过hashChange事件,感知hash的变化
        改变hash可以直接通过location.hash = xxx
    2、基于H5history路由:
        监听url的变化可以通过自定义事件触发实现
        改变url可以通过history.pushState和replaceState,会将URL压入堆栈,同时能够应用history.go等api
        
 2、 react-router实现的思想
     基于history库实现上述不同的客户端路由实现思想,并且能够保存历史记录,磨平浏览器差异
     通过维护的列表,在每次url发生变化的回收,通过配置的路由路径,匹配到对应的Component,并且render
     

如何配置React-Router实现路由切换

1、 使用<Router>组件
     路由匹配是通过比较<Route>的path属性和当前地址的pathname来实现的。
     匹配成功就渲染内容,不匹配就渲染null。 
     没有路径的<Route>将始终匹配

   // when location = { pathname: '/about' }
    <Route path='/about' component={About}/> // renders <About/>
    <Route path='/contact' component={Contact}/> // renders null
    <Route component={Always}/> // renders <Always/>
 
        
 2、结合<Switch>组件 和<Route>组件
     Switch用于将Route分组
     <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/contact" component={Contact} />
    </Switch>
3、 使用<Link><NavLink> <Redirect>组件
  1、 <Link>组件在你的应用程序中创建链接,无论在何处渲染一个<Link>都会在html渲染成<a/>
  
  

React-Router4 怎么在路由改变的时候重新渲染同一个组件

  1、当路由发生变化时,组件的props也会发生变化,
  2、会调用componentWillReceilvePorps等生命周期钩子,
  3、所以我们可以当路由改变的时候,根据路由,去请求数据
   class NewsList extends Component {
          componentDidMount () {
             this.fetchData(this.props.location);
          }

          fetchData(location) {
            const type = location.pathname.replace('/', '') || 'top'
            this.props.dispatch(fetchListData(type))
          }
          componentWillReceiveProps(nextProps) {
             if (nextProps.location.pathname != this.props.location.pathname) {
                 this.fetchData(nextProps.location);
             } 
          }
          render () {
            ...
          }
        }

Vuex的使用

  #### 存在以下属性
         nameSpaced:true,  该属性如果不写的话,代表是全局的
         state: 对象
         action: 对象,每个属性值都是一个函数,在这里可以执行异步函数
         mutations: 对象,每个属性只都是一个函数,必须是同步的
         getter: 是state的对象
  #### 属性的获取
       通过导入redux的 mapState,mapAction,mapMutations,mapGetter
         
         
         
        
         

Redux 和Vuex的区别 和 共同点

区别

  vuex改进了redux中的action和reducer函数,以mutations代替了reducer,无需switch,只需要在对应mutation函数改变state值就行
  属性值获取也不一样,vuex通过mapState,mapAction,mapMutations,mapGetters,React 是通过store.getStore,其他的直接使用dispatch 直接使用
  
  Vuex由于Vue自动重新渲染的特性,无需订阅,只要sta特变化,view就会变化
  Vuex数据流的顺序: view -> dispatch(action) -> commit(mutations)-> store改变
  Redux数据流顺序: view ->  dispatch(action) -> 自动调用reducer -> store改变
  redux的必须是同步代码,如果想用异步代码,需要通过中间件:redux-thunk,redux-saga,
  

共同思想

  单一的数据源
  变化可以预测

Redux中间件是怎么拿到store和action的 然后怎么处理?

redux中间件本质就是一个函数柯里化
每个中间件都会传入两个参数,Store的getState函数和dispatch 函数,分别获的store和action,最终返回一个函数

Redux中的connect有什么作用

connect 负责连接React和Redux
1、获取state
     connect通过context获取Provider中的store,通过store.getState() 获取整个store tree上所有state
2、 包装原组件
    将connect中传入的mapStateToProps,mapDisptchToProps与组件原有的props合并后,通过属性的方式传给组件
3、监听store tree变化
    connect 缓存了store tree中state的状态,通过当前的state状态和变更前的state状态进行比较,从而确定是否调用this.setState() 方法触发Connect以其子组件的重新渲染
    

为什么useState要使用数组而不是对象

这设计到数组和对象结构
数组解构的情况下 我们可以随意对起变量名
对象解构的情况下 必须要与useState返回的变量名相同,如果变量名特殊 那就需要需要设置别名,相比数组处理起来会麻烦很多

React Hook的使用限制有哪些

### 限制条件
    不能在条件 循环 嵌套中使用Hook
    只能在React函数组件中调用Hook
    不能使用push pop splice等直接更改数组 ,会不更新,
### 原因
    因为hooks的设计是基于数组实现的,在调用的时候按照顺序加入数组中,如果在循环条件 嵌套中使用 很可能在数组取值导致错位,执行错误的hook,

使用React遇到的坑

1、在类组件中使用push pop slice 直接更改数组,再调用this.setState, state对应的值会更新,但是如果是函数组件 就不会更新
2、 useState设置状态的时候,只有第一次生效,后期需要更新状态,必须通过useEffect 
    TableDeail是一个公共组件,在调用它的父组件里面,我们通过setState改变columns的值,以为传递给TableDeail 的 columns是最新的值,所以tabColumn每次也是最新的值,但是实际tabColumn是最开始的值,不会随着columns的更新而更新:
   
   const TableDeail = ({
        columns,
    }:TableData) => {
        const [tabColumn, setTabColumn] = useState(columns) 
    }

    // 正确的做法是通过useEffect改变这个值
    const TableDeail = ({
        columns,
    }:TableData) => {
        const [tabColumn, setTabColumn] = useState(columns) 
        useEffect(() =>{setTabColumn(columns)},[columns])
    }

useEffect 与 useLayoutEffect的区别

共同点

   运用效果: 两者都是用于处理副作用,这些副作用包括改变DOM改变,设置订阅 操作定时器等,
   使用方式: 没啥差异 可以直接替换

不同点

   1、 useEffect在React的渲染过程中是被异步调用的,而useLayoutEffect会在所有dom变更之后同步调用,因为是同步处理,所以避免在useLayoutEffect做计算量较大的耗时任务从而造成堵塞
   2、useLayoutEffect总是比useEffect先执行

image.png

函数组件模仿类组件的生命周期

getDerivedStateFromProps

function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);
  if (row !== prevRow) {
    // Row 自上次渲染以来发生过改变。更新 isScrollingDown。
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }
  return `Scrolling down: ${isScrollingDown}`;
}
React会立即退出第一渲染 并用更新后的state重新运行组件以避免耗费太多性能

shouldComponentUpdate:

   可以用React.memo包裹一个组件来对它的props进行浅比较
   
   注意点 React.memo 等同于PureComponent,只会进行浅比较
    

componentDidMount:

        useEffect(()=> {
        
          },
          []
        )
    

componentDidUpdate:

useEffect(() => { 
  // 在 componentDidMount,以及 count 更改时 componentDidUpdate 执行的内容
  document.title = `You clicked ${count} times`; 
  return () => {
    // 需要在 count 更改时 componentDidUpdate(先于 document.title = ... 执行,遵守先清理后更新)
    // 以及 componentWillUnmount 执行的内容       
  } // 当函数中 Cleanup 函数会按照在代码中定义的顺序先后执行,与函数本身的特性无关
}, [count]); // 仅在 count 更改时更新

componentWillUnmount:

useEffect(()=>{
  // 需要在 componentDidMount 执行的内容
  return function cleanup() {
    // 需要在 componentWillUnmount 执行的内容      
  }
}, [])

虚拟DOM

对虚拟DOM的理解? 虚拟 DOM 主要做了什么?虚拟 DOM 本身是什么?

    虚拟dom本身是一个javascript对象,通过js对象的方式描述dom结构
    在每次数据发生变化前,虚拟dom都会缓存一份,变化之时,现在的虚拟dom会与缓存的虚拟dom进行比较。在vue或者react内部都封装了diff算法,通过这个算法来进行比较,找到变化的地方,然后重新渲染
    

为什么要用虚拟dom

 如果直接操作真实的dom,则数据的每一次变化,会重建所有的dom元素,会导致所有元素重排重绘,消耗内存,性能变低
 而使用虚拟dom,数据发生变化的时候,会对比新旧虚拟dom,通过diff算法,找出变化的地方,然后重新编译变化的内容,渲染到页面上,并且vue或者react,在频繁的多次更新数据后,会收集多次更改,再进行虚拟dom比较
 

react diff 算法原理

diff算法探讨的是虚拟dom发生变化后,生成dom树更新补丁的方式。通过对比新旧两株虚拟dom树的变化差异,将更新的补丁作用于真实dom,以最小的成本完成dom更新
具体流程: 
    - 真实的dom首先会映射成虚拟dom
    - 当发生变化时,会生成新的虚拟dom与旧的虚拟dom对比,找出变化的地方,然后重新编译,重新渲染
  

diff 算法策略

- 策略一: 基于树进行比较,忽略节点跨级操作场景,提升对比效率
     树的对比处理方法非常暴力,即两棵树只对同一层次的节点,如果发现节点已经不存在了,则该节点及其子节点会完全删除掉,不会用于进一步的比较,这就提升了比对效率
- 策略二: 基于组件进行比较,如果组件的class一致,则认为有相似的树结构,否则则默认为不同的树结构
- 策略三: 基于节点进行对比,通过标记key的方式进行列表对比,元素对比主要发生在同层级中,通过标记节点操作生成补丁。节点操作主要包含新增,移动,删除等。通过标记key的方式,react可以直接移动dom节点,降低内耗

React key的作用

主要是用来标记列表元素,给每一个列表元素标记独一无二的key,方便对比,找到差异,减少不必要的渲染
注意事项: 
    - key的值必须唯一
    - 尽量不要用数组的index作为key,
    - 不用随机说

虚拟dom与直接操作dom 哪一个效率更高

 没有绝对性,看情况
 - 如果只是简单的修改一个按钮的文案,肯定是直接操作dom效率高,因为使用虚拟dom还需要进行比较
 - 在首次渲染中,也肯定是直接操作dom高,原因同上
 - 但是操作多dom更改操作的时候,肯定虚拟dom效率高

react最新版本解决了什么问题,增加了哪些东西

react 16.x 三大新特性:time slicing、suspense,hooks

    - time silcing: 解决cpu速度问题,使得在执行任务期间可以随时暂停,跑去干别的事情,这个使得react在性能极其差的机器跑时,仍然保持良好的性能
    - Suspence: 解决网络IO问题,和lazy配合,实现异步加载组件。能暂停当前组件的渲染,能完成某件事以后再继续渲染,解决react一直存在的异步副作用问题。
    - 提供了一个内置函数componentDidCathch: 当有错误发生时,可以友好地展示fallback组件,可以捕捉到它的子元素(包括嵌套子元素)抛出的异常,可以复用错误组件
  1. react 16.8加入hooks,让函数组件变得更加灵活,可以管理自己的保存变量状态,可以通过hooks模拟类组件的生命周期
  2. react 16.9
    • 重命名不安全的生命周期方法,給不安全的生命周期添加unsafe_前缀,有利于代码review 和 debug
    • 废弃javascript:形式的url,以javascript:开头的url非常容易遭受攻击,总造成安全泄口
  3. react 16.13.0
    • 废弃unstable_createPortal,使用createPortal

react数据持久化有什么实践么

封装持久化组件:

const useLocalStorage = (key, initValue) => {
// 刷新时会重新走这一步
const [value, setValue] = useState(() => {
    try {
        const storageValue = localStorage.getItem(key);
        return storageValue ? JSON.parse(storageValue) : initValue;
    } catch (error) {
        return initValue;
    }
});
const setNewValue = (value) => {
    try {
        setValue(value);
        localStorage.setItem(key, value);
    } catch (error) {
        console.log('----error', error);
    }
};
return [value, setNewValue];

};

举例说明之前实践过持久化的例子

之前资生堂红人项目 头部有品牌切换下拉框,有默认品牌,切换后如果刷新页面,也要保持上次选中,这种情况下,就可以用useLocalStorage,持久化

现在已经有持久化插件了

  redux-persist
  recoil-localforage
  dva-persist
    
  

React和Vue的异同

#### 相似之处:
    - 都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库
    - 都有自己的构建工具,根据自己的需要生成项目模版
    - 都使用虚拟dom 提高重绘性能
    - 都有props概念,允许组件间的数据传递
    - 支持组件化
#### 不同之处
    - vue 默认支持数据数据双向绑定,而react一直提倡单向数据流
    - 虚拟dom
        vue2.x开始引进虚拟dom,vue宣称可以更快计算出虚拟dom的差异,这是因为在它的渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树
        对于react而言,每当管理的数据状态发生变化,含子组件就会渲染,我们可以用pureComponent,React.memo,shouComponentUpdate来进行优化,但是Vue将此视为默认的优化
     - 组件化模版编写不同:vue是普通html的编写,但是react是jsx的编写
     - 监听数据变化的实现原理不同
         vue通过getter和setter以及一些函数的劫持,能精确知道数据变化,不需要特别的的优化就能得到很好的性能
         react默认是通过比较引用的方式进行的,如果不进行优化,就会导致大量不必要的渲染
         

React中props.children和React.Children的区别

props.children 是React组件的一个特殊属性,它是用来获取传递给该组件的所有子元素
React.Children 是React提供的一个工具,它提供了一些有用的方法来处理React元素的子元素。这些方法包括React.children.map,React.childer.forEach,React.Child.only,React.Children.toArray等,这些方法通常返回新的React元素数组或者子元素的函数
      

React的状态提升是什么

将多个组件需要共享的状态都提升到他们最近的父组件中,在父组件上改变这个状态,然后通过props分发到子组件

React中constructor和getInitalState的区别

作用: 功能是一样的,都是用来初始化数据
区别: 前者是es6语法,后者是es5

react的严格模式如何使用,有何作用

StictMode是用来突出显示应用程序中潜在的问题工具
- 识别不安全的生命周期
- 检测过时的用法

react必须使用jsx吗

- react并不强制使用jsx
- 每个jsx只是调用React.createElement(component,prop,...children)的语法糖
- 如果不想使用jsx 可以使用该语法糖完成
        
        

为什么使用jsx的组件没有使用react却需要引入react

- 本质上 jsx 是React.createElement的语法糖,babel会将其转化成这种是形式,所以一定要引入react
- 在React 17 之前如果使用jsx,一定要引入react,但是在React17后,babel会自动引入react,所以不需要手动引入了

React.child.map和js的map有什么区别么

js的map不会对null或者undefined的数据进行处理,但是React.Children.map可以处理

React 中的高阶组件运用了什么设计模式?

#### 装饰着模式
    它允许你通过将对象包装在装饰着对象中动态添加新行为,在React中,HOC通过包装一个组件来添加新的行为或功能,而不会改变原组件的代码

事件处理函数的this指向问题

 1、使用箭头函数
 2、在构造函数中 用bind,将函数的上下文指向this
 3、直接在点击事件中 bind 指向this
 

为什么列表循环渲染的key最好不要用index

举例说明

 变化前的数组是[1,2,3,4],key就是对应的下标:0,1,2,3;变化后的数组值是[4,3,2,1],key就是对应的下标:0,1,2,3;
 - diff算法在对比的时候,找到key=0的值是1,但是在变化后数组里找到key=0的值为4
 - 因为子元素不一样就重新删除并更新
 - 但是如果加了唯一的key,如下:
    变化前数组的值是[1,2,3,4],key对应的下标:id0,id1,id2,id3,
    变化后数组的值是[4,3,3,1],key对应的下标:id3,id2,id1,id0,
   那么在diff算法时,找到key值是id0值为1 ,在变化后的id0 值也为1,因为子元素相同,只需要做移动操作,提升性能
   
    

forwardRef

forwardRef是一种高阶组件技术,用在函数中组件中转发ref到其子组件。通常情况下,函数无法接收ref,因为函数组件没有实例,但是forwardRef可以将ref传递给组件函数的子组件

用途:

 主要是为了在函数组件中转发ref到子组件
 

API:

 forwardRef函数的签名是 React.forwardRef((props,ref)=>{}),它接收一个函数作为参数,函数的参数包括props和ref,并且返回一个react组件,如果父组件想要获取子组件的属性和方法,可以使用useImperativeHandle

实现原理:

 基于React元素的属性转发机制,当使用forwardRef时,实际上是创建了一个新的React元素,这个元素将所有的props(除了ref)转发给了内容组件。ref是一个特殊的prop,它不会转发给内部组件,而是被附加到内部组件实例上
下面是forwardRef的用一个简化实现
function forwardRef render(props, ref) {
  // 从 props 中解构出 ref,其余的 props 将被转发给内部组件
  const { forwardedRef = ref, ...remainingProps } = props;

  // 创建一个新的 React 元素,其中包含转发的 props 和 ref
  return React.createElement(
    props.component, // 内部组件
    remainingProps, // 转发的 props
    forwardedRef // ref 将被附加到内部组件的实例上
  );
}

useMemo ,useCallback,React.memo

useMemo

     - useMemo 是用来缓存计算结果的,只有当依赖发生变化的时候,才会重新计算
     - 语法: 
         const memorizedValue = useMemo(()=> computeExpensiveValue(a,b),[a,b]),第一个参数是一个函数,用来计算需要缓存的值,当依赖发生变化的时候才会重新计算

useCallback

     - useCallback用来返回一个缓存函数,只有当依赖发生变化 才会重新生成
     - 语法:-   `const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);useCallback 的第一个参数是一个回调函数,第二个参数是一个依赖数组,只有当依赖项发生变化时,才会重新创建回调函数。

React.memo 与React.PureComponent对比

- 两者都是优化性能的
- pureComponent 是用在类组件中,是对props和state进行浅比较
- memo,是对props进行浅比较
- memo要比pureComponent更加灵活

注意: 因为memo有缓存,大量使用,会造成性能上的开销

useEffect和useMemo区别

   - useEffect在dom更新后触发,useMemo在dom更新前触发
   - useEffect可以帮助我们在dom更新完成后执行某些副作用,比如数据的获取,设置订阅以及手动更改dom
   - useMemo只是用处执行计算后的结果 不执行任何副左右操作

useEffect,useLayoutEffect,useInsertionEffect分别用来干什么

- useEffect: 用于副作用捕获,在dom元素渲染之后调用,常用于数据操作处理
- useLayoutEffect: useEffect的一个版本,在dom更新之后同步执行,但是在浏览器绘制之前执行,可能会堵塞页面渲染
- useInsertionEffect:  useEffect的一个版本,在dom更新之前执行,用于css-in-js插入动态样式

根据useEffect执行机制,写出下面题的答案

import React, { useState } from "react";
import ReactDOM from "react-dom";

组件App
function App() {
  const [n, setN] = useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  React.useEffect(() => {
    console.log("App");
    return () => {
      console.log("App挂了");
    };
  });
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>+1</button>
      {/* {n % 2 === 0 ? <B /> : ""} */}
      <B />
    </div>
  );
}

组件B
function B() {
  const [m, setM] = useState(0);
  const onClick = () => {
    setM(m + 11
  };
  React.useEffect(() => {
    console.log("B");

    return () => {
      console.log("B挂了");
    };
  });
  useEffect(()=>{
    let tag = false
    api('').then(res=>{
      if(tag){
        setData(res.data)
      }
    })
    return ()=>{
      tag = true
    }
  },[search])
  return (
    <div>
      B组件
      <h1>m: {m}</h1>
      <button onClick={onClick}>+1</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

因为useEffect是在dom更新结束后,也就是render结束之后,才执行useEffect

  • 组件app首次渲染之后,先执行console.log('b'),再执行console.log('App')
  • 当执行n+1之后,先执行console.log('B挂了'),在执行console.log('b'),再执行console.log('App挂了'),最后执行console.log('App')
  • 当执行m+1之后,先执行console.log('B挂了'),在执行console.log('b'),

useRef

返回的是一个可变的ref对象,其.current属性被初始化传入参数,返回在ref对象在组件的生命周期保持不变
ref对象只有一个current属性,地址一直都不会变,useRef变化不会使页面重新渲染

useReducer

   用来替代useState的选择,对于一些复杂的状态逻辑或者需要多个状态之前协同的情况下非常有用
   useReducer接收一个reducer函数和一个初始化状态,并返回一个包含当前状态值和dispatch 函数的数组

案例:

import React, { useReducer } from 'react';

// 定义 reducer 函数
const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
};

// 初始状态
const initialState = { count: 0 };

// App 组件
function App() {
  // 使用 useReducer 来管理状态
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      Count: {state.count}
      <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
    </div>
  );
}

什么是React Fibber?

React Fiber 是 React 框架的一种底层架构,为了改进 React 的渲染引擎,使其更加高效、灵活和可扩展。

传统上,React 使用一种称为堆栈调和递归算法来处理虚拟 DOM 的更新,这种方法在大型应用或者频繁更新的情况下可能会产生性能问题。React Fiber 则是基于一种增量渲染的思想,它将更新任务分解成小的、可中断的单元,使得 React 在更新时可以更灵活地控制和切换任务,提高应用的响应性。

React Fiber 的核心特点包括

  1. 增量渲染: React Fiber 将更新任务拆分成多个小任务单元(称为 “fiber”),并使用优先级调度器来处理这些任务,以提高响应性和用户体验。

  2. 优先级调度: Fiber 引入了优先级概念,使 React 能够根据任务的优先级来决定任务的执行顺序,确保高优先级任务得到及时处理。

  3. 中断与恢复: React Fiber 允许在渲染过程中中断任务,然后在适当的时机恢复执行,从而避免了阻塞的情况。

  4. 任务取消: Fiber 具备任务取消的能力,可以取消不必要的更新,提高性能。

hook的原理

实现的关键在于react的更新策略,hook是基于函数编程思想实现的,目的是为了让函数组件拥有状态和react的其他特性,主要有以下几个部分

  1. Fiber架构 Fiber架构将组件树转换成一颗可中断的,有优先级的调度树,实现增量渲染和优化渲染性能。利用Fiber的调度机制实现hook的状态更新和副作用的操作
  2. 内部的hookApi useState用于组件状态管理, useEffect用于执行副作用等
  3. hook队列 React内部维护了

React Fiber介绍

参考文章zhuanlan.zhihu.com/p/540415887

Fiber 架构

react 16.18.0 版本引入 fiber 架构,实现异步可中断更新。先把 vdom 树转成 fiber 链表,然后再渲染 fiber。主要是解决之前由于直接递归遍历 vdom,不可中断,导致当 vdom 比较大的,频繁调用耗时 dom api 容易产生性能问题。

Fiber 数据结构

主要分下面几块:

  • 节点基础信息的描述
  • 描述与其它 fiber 节点连接的属性
  • 状态更新相关的信息
  • 优先级调度相关

这边和 hook 关联比较大的主要是 memoizedState 和 updateQueue 属性。函数组件会将内部用到的所有的 hook 通过单向链表的形式,保存在组件对应 fiber 节点的 memoizedState 属性上。updateQueue 是 useEffect 产生的 effect 连接成的环状单向链表。

为什么不能在循环、条件或嵌套函数中调用 Hooks?

通过fiber节点中的memorizedState中的属性存储hook,又通过其中的next属性指向下一个hook,因为hook的单向链表是在mount阶段确定的,所以在执行过程不能在条件,循环,嵌套中使用,不然会出现取错hook的情况

React的错误边界

参考文章:blog.csdn.net/xun__xing/a…

ErrorBoundary

React 的错误边界是⼀种⽤于处理组件渲染期间错误的机制。它是⼀个 React 组件,可以捕获其 ⼦组件抛出的 JavaScript 错误,并展示⼀个备⽤ UI。

import { useErrorBoundary } from 'use-error-boundary';

function ErrorBoundary({ children }) {

    const { ErrorBoundary, didCatch, error } = useErrorBoundary();

    if (didCatch) {

    return <h1>出错了!</h1>;

    }

    return <ErrorBoundary>{children}</ErrorBoundary>;

}

getDerivedStateFromError和componentDidCatch实现ErrorBoundary

image.png

image.png

image.png

image.png