对React的理解
- react 起源于 facebook,是一个用于构建用户界面的 js 库。
- react 的核心设计思路:
- 声明式设计:采用范式声明,开发者只需声明显示内容,react 就会自动完成。
- 高效:react 通过虚拟 dom,最大限度的减少与 dom 的交互。
- 组件化:通过 react 构建组件,把页面功能拆分成小模块,每个小模块就是组件,可以更容易做到高内聚低耦合。
- 单项数据流:react 是单向数据流,数据通过 props 从父节点传递到子节点,如果父级的某个 props 改变了,react 会重新渲染所有的子节点。
- 缺点:没有官方系统解决方案,选型成本高。
react 想实现什么目的?
- 需要实现声明式,代码结构需要非常清晰和简洁,可读性强。
- 结构、样式和事件能够实现高内聚低耦合,方便组合与重用。
React 为什么引入 jsx?
- React 引入 JSX 是为了更方便地创建 UI 组件。
- JSX 是一种 JavaScript 语法扩展,它允许我们在 JavaScript 代码中编写类似于 HTML 的标记语言,可以更直观地描述组件的结构和样式。
- 使用 JSX 可以让代码更易读、易维护,并且可以更好地组织组件的结构。
- React 也提供了一些工具来将 JSX 转换为纯 JavaScript 代码,使得浏览器可以正确地解析和渲染组件。
什么是 HOC ?
- 高阶组件:是一个函数,接收组件作为参数,返回一个新的组件,是 Hooks 出现之前,常见的复用逻辑的手段。
- 例如:react-rudux 的 connect、antd 3 的 createForm。
- 缺点:容易形成嵌套地狱(括号套括号)。
redux 的工作原理?
- Redux 是 React 的一个状态管理库,基于函数式编程思想实现,集中式管理状态:
- 单一数据源:整个应用的 全局 state 被存储在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
- State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个描述已发生事件的普通对象。
- 使用纯函数来执行修改:为了描述 action 如何改变 state tree,需要编写纯的 reducers。
- 工作原理:
- createStore:创建 store 数据状态管理库。
- reducer:初始化、修改状态函数,定义修改规则。
- getState:获取状态值(getter)。
- dispatch:提交更新(setter)。
- subscribe:变更订阅,订阅 state 改变之后要做的事情,一般是组件更新。
React 的生命周期
- 挂载阶段:指组件从组件初始化到完成加载的过程。
constructor:构造函数,常用于初始化。getDerivedStateFormProps:使组件在 props 变化时,更新 state。触发时机分为三个:- props 被传入时、state 变化时、forceUpdate 被调用时;
render:函数返回的 jsx 结构,用于描述具体的渲染内容。componentDidmount:主要用于组件加载完成时做某些操作。
- 更新阶段:当组件的 props 或 state 发生变化时会触发更新。
getDerivedStateFromProps:在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。render:每次触发都会更新。shouldComponentUpdate:当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。getSnapshotBeforeUpdate:在最近一次渲染输出(提交到 DOM 节点)之前调用。componentDidUpdate:在更新后会被立即调用。
- 卸载阶段:当组件从 DOM 中移除时会调用。
componentWillUnmount:在组件卸载及销毁之前直接调用。
shouldComponentUpdate 是做什么的?
shouldComponentUpdate是 React 组件生命周期中的一个方法,用于控制组件是否需要重新渲染。- 当组件的 props 或 state 发生变化时,React 会自动调用
shouldComponentUpdate方法,该方法返回一个布尔值,用于指示组件是否需要重新渲染。
类组件和函数组件的区别?
- 共同点:实际用途一样,无论是高阶组件,还是异步加载,都可用作基础组件展示 UI。
- 不同点:
- 编写形式不同:
- 类组件是通过 ES6 的 Class 语法创建的,类组件需要继承 React.Component 类,并且必须实现 render 方法来返回组件 的 JSX。
- 函数组件是通过函数来创建的,只是接收 props 并返回 JSX。
- 状态管理:类组件可以使用 state 和生命周期方法来管理组件的状态和行为,而函数组件通常使用 Hooks API 来管理状态和行为。
- 性能优化:类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。
- 编写形式不同:
Class 组件中,事件中 this 为什么是 undefined?
- 在类组件中定义的方法都默认开启严格模式,所以在 Class 组件中:
- 所有指向 window 对象的 this 都指向 undefined。
- 所有内联的事件处理函数中的 this 都指向 undefined。
React 事件机制
- 在 React 中平常使用的 onClick、onChange 等写法,用的就是 React 中的事件,我们通常称为“合成事件”。
- React 自定义的合成事件机制,帮助用户解决了平台兼容性、事件委托等优化机制,用户只需关注写 React 本身就行了。
- React 内部事件委托,记录了当前事件发生的状态,统一了管理事件,提高了性能。
事件委托
- 又称事件代理机制,这种机制是指把所有子节点的事件绑定在父级,使用一个统一事件监听和处理函数。这样可以简化事件处理和回收机制,从而提升效率。
调用 setState 之后发生了什么?
- 调用
setState()方法会将状态的更新加入到待处理的更新队列中。 - React 触发内部的事务处理机制并开始批处理更新队列中的所有状态变更请求。
- 对于每个组件的更新请求,React 首先会使用新的状态值计算其新的
Virtual DOM对象,并将其存储在内存中。 - React 会比较新旧
Virtual DOM之间的差异,计算出所需要进行的最小更新操作集合。 - React 会将实际的状态变更操作同步地应用到浏览器的
DOM中,并触发componentDidUpdate生命周期函数。 - 整个过程是异步的,并且 React 的批处理机制会通过合并变更操作,减少不必要的
DOM操作,从而提高应用程序的性能。如果多次调用setState()方法对于同样的key将会被合并,最终只有最后一次调用setState()方法被执行。
setState 是同步更新?还是异步更新?
- 是同步也是异步,在合成事件和生命周期函数中是异步的,在原生事件和定时器中是同步的。
state 和 props 之间有何不同?
- props 属性:是从外部传进组件的参数,主要作用就是父组件向子组件传递数据,但是 props 对于使用它的组件来说是只读的,一旦赋值不能修改,只能通过外部组件主动传入新的 props 来重新渲染子组件。
- state 状态:一个组件的显示形态可以由数据状态和外部参数决定,外部参数是 props,数据状态就是 state。首先,在组件初始化的时候,用 this.state 给组件设定一个初始的 state,在第一次渲染的时候就会用这个数据来渲染组件,state 不同于 props 的一点是,state 可以修改,通过 this.setState() 来修改 state。
React 的 Component 与 PureComponent 区别?
- Component:有 shouldComponentUpdate() 这个生命周期。
- PureComponent:内置了 shouldComponentUpdate() ,会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。浅层比较使用的是 ===,判断的是引用地址是否发生变化。
- 如果说 props 和 state 都没有发生变化,那么就阻止更新。
- 如果发生了变化,那就更新。
- PureComponent:可以用作性能优化。
React 接口请求应该放在哪里?为什么?
- 对于异步请求,应放在 componentDidMount 中操作。
- 也可以放 componentWillMount ,但也被废弃,在新的一部渲染架构下会触发多次渲染,容易引发 Bug,不利于未来 React 升级后的代码维护。
如何设计 React 组件?
可以通过展示组件和灵巧组件来进行划分:
- 展示组件:受控于外部的 props 控制,具有极强的通用性,复用率很高。
- 代理组件:常用于封装常用属性,减少重复代码。
- 样式组件:也是一种代理组件,只是细分了处理样式领域,将当前的关注点分离到当前组件内。
- 布局组件:基本设计与样式组件完全一样,基于自身特性做了一个小小的优化。
- 灵巧组件:面向业务,功能更丰富、复杂性更高,复用性更低。
- 容器组件:几乎没有复用性,主要用于拉取数据和组合组件两个方面。
- 高阶组件:复用组件逻辑的高级技术,是基于 React 组合特性形成的设计模式。高级组件的参数是组件,返回值是新组件的函数。
- 渲染劫持:通过控制 render 函数修改输出内容,常见的场景是显示加载元素。
React 如何面向组件跨层级通信?
- 父与子:使用 props 向子组件传递数据。
- 子与父:主要依赖于回调函数,通过事件调用函数传递。
- 兄弟:依靠于共同的父组件进行中转。
- 无直接关系:使用 Context 传递数据。
React 常见的组件性能优化手段?
- 复用组件:组件复用的前提是同一层级、同一类型、同一 key 值,尽量保证这三者的稳定性。
- 减少组件的重新 render:组件重新 render 会导致组件进入协调,协调的核心就是我们常说的 vdom diff 算法,所以协调本身就是比较耗时的算法,因此,如果能够减少协调,复用旧的 fiber 节点,那么肯定会加快渲染完成的速度。组件如果没有进入协调阶段,我们称为 bailout 阶段,意思是这层组件退出更新。让组件进入 bailout 阶段的方法。
- shouldComponentUpdate:Component 类组件的一个生命周期,当用户定义的这个函数并且返回 false,则进入 bailout 阶段。
- PureComponent:更新前会自行浅比较新旧 props 与 state 是否改变,如果两者都没变,则进入 bailout 阶段。
- memo:这是一个函数,用户可以自定义,如果没有定义,默认使用浅比较,比较组件更新前后的 props 是否相同,如果相同,则进入 bailout 阶段。
- useMemo:缓存参数;useCallback:缓存函数。
vue 和 react 的相同点与不同点?
- 相同点:
- 都支持服务端渲染。
- 都有虚拟 dom,组件化开发,通过 props 参数进行父子组件数据的传递。
- 都是数据驱动视图。
- 都有状态管理,react 由 redux,vue 由 vuex。
- 不同点:
- react 严格上只针对 mvc 的 view 层,vue 是mvvm 模式。
- 虚拟 dom 不一样,vue 会跟踪每一个组件的依赖关系,不需要重新渲染整个 dom 组件数,而 react 不同,当应用的状态被改变时,全部组件都会重新渲染,所以 react 中 用 shouldComponent 这个生命周期的钩子函数来控制。
- 组件写法不一样,react 是 jsx 和 内联样式,就是把 html 和 css 全写进 js 中,vue 则是 html,css,js 在同一个文件。
- 数据绑定不一样,vue 实现了数据双向绑定,react 数据流动时单向的。
- 在 react 中,state 对象需要用 setState 方法更新状态,在 vue 中,state 对象不是必须的,数据由 data 属性在 vue 对象中管理。
在渲染多个组件的时候,key 如何取值?
- key 值标记了节点在当前层级下的唯一性,因此尽量不要取值 index,因为在同一层级下,多个循环的 index 容易重复。并且如果设计节点的增加、删除、移动,那么 key 的稳定性将会破坏,节点就会出现混乱现象。
hook 的使用规则?
- 不要在循环、条件或嵌套函数中调用 Hook。
- 确保在 React 函数的最顶层以及任何 return 之前调用它们。
Hook 为什么只能用在 React 函数的最顶层?
- React 中每个组件都有一个对应的 FiberNode,其实就是一个对象,这个对象有一个属性叫 memoizedState。当组件是函数组件的时候,fiber.memoizedState 上存储的就是 Hooks 单链表。
- 单链表的每个 hook 节点没有名字或者 key,因为除了它们的顺序,我们无法记录它们的唯一性。因此,为了确保某个 Hook 是它本身,我们不能破坏这个链表的稳定性。
- hook 里面存了很多信息,比如:state、effect、依赖项等。如果在 if 内部使用 Hook,那么组件更新的时候,hook3 可能就变成了 hook0,顺序就乱了。
- 在嵌套函数内部使用 hook ,也不能确保这个嵌套函数里面的 hook 到底什么时候调用,所以还是不能确定 fiber.memoizedState 单链表上 hook 节点的稳定性。
hook 解决了什么问题?
- 组件之间复用状态逻辑难:react 没有提供将可复用性行为 “附加” 到组件的途径,因此,只能使用 render props 或者高阶组件(HOC),但这很容易形成嵌套地狱(例如:antd 3 的 form 和 connect)。
- 复杂组件难以理解:类组件的生命周期只能写一次,复杂组件的某个生命周期内,如 componentDidMount 里,可能就会存在很多不相关,但是不得不组合在一起的代码,这个时候就容易产生 bug。
- 可以使用自定义 Hook 复用状态逻辑,也可以定义多个副作用处理函数,解决类组件臃肿的问题。
什么是自定义 Hook?
- 以 use + 大写字母开头的命名方式,用于 React 中复用状态逻辑。如:antd 4/5 的 useForm。
- 自定义 hook 可以像函数组件一样,使用所有的 hooks api。
Hook 中的 state 保存在了哪里?
- 每一个 hook 都有一个对应的 hook 对象,这个对象上会存储状态值、reducer 等值,这个 hook 对象又以单链表的数据结构存在 fiber 上,而 fiber 是 React 的虚拟 DOM,存在于内存中。
useState 和 useReducer 的区别?
- 相同点:都用于函数组件内定义状态。
- 不同点:useReducer 可以接收一个 reducer 函数,意味着可以把状态修改逻辑放在 reducer 函数中,还可以多次复用。
- useState:用于简单的状态值定义。
- useReducer:用于状态值修改逻辑复杂,想要抽离出来或者复用修改逻辑的。
- 使用 useState 的时候,如果 state 不发生变化,组件不会更新。但 useReducer 反之,state 值发生变化,组件也会更新。
为什么 useState、useReducer 返回一个数组,而不是其他结构?
- 因为函数组件中可以多次使用 useState / useReducer,可以定义多个不同 state,数组可以一次返回 state、setState,命名交给用户自定义。
- 如果是对象再结构,那么 state 的命名就需要写死,用户想定义多个 state 的时候,还需要自己改名字。
useRef 与 useState 的区别?
- 没有直接关系,是两个完全不同的值。
- useRef:用于记录一个值,这个值在函数组件卸载前,指向的都是同一个引用。
- useState:用于定义一个 state,当 state 改变,组件更新。
- 两个的值都是记录在 hook.memoizedState 上。
react的hook useEffect作用?
- 专门用来执行副作用的。即在每次 render 之后去判断依赖并执行。
- 可以实现类组件的 ComponentDidMount、componentDidUpdate 和 componentWillUnmount 三个生命周期方法。
useEffect 依赖为空数组与 componentDidMount 区别?
- useEffect:会在 commit 阶段执行完之后,异步的调用回调函数。
- componentDidMount:则会在 layout 这个子阶段同步的调用,它们的调用时机是不同的。
- hook 中 useLayoutEffect 的调用时机也会在 layout 阶段同步调用。
useEffect 与 useLayoutEffect 有什么区别?
- 共同点:
- 都接收一个包含命令式、且可能有副作用代码的函数,这些副作用包括改变 DOM、设置订阅、操作定时器等。
- 不同点:执行时机不同。
- useEffect:使用 useEffect 完成副作用操作。赋值给 useEffect 的函数会在组件渲染到屏幕之后 延迟 执行。默认情况下,effect 将在每轮渲染结束后执行,但你可以选择让它在只有某些值改变的时候才执行。
- useLayoutEffect:会在所有的 DOM 变更之后 同步 调用 effect。可以用它来读取 DOM 布局并同步触发渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将会被同步刷新。
useEffect / useLayoutEffect 中的延迟、同步是什么意思?(任务调度)
- 这里的延迟、同步,指的是 React 任务调度中的任务调度。
- 延迟:就是 useEffect 的 effect 不与组件使用同一个任务调度函数,而是再单独调用一次任务调度函数,即用的不是一个 task。因为如果 effect 和组件渲染使用同一个 task,那么 effect 势必会加长这个 task 的执行时间,阻碍组件渲染。
- 同步:useLayoutEffect 的同步,指的是 useLayoutEffect 的 effect 和组件渲染使用同一个 task,那么就会阻碍组件渲染。
- 大多数情况下,尽可能使用标准的 useEffect,以避免阻塞视觉更新。
React.memo() 和 React.useMemo() 的区别?
- React.memo() 是一个高阶组件,可以包装不想重新渲染的组件,只有当 props 发生变化时,才会重新渲染。
- useMemo() 是一个 hook,可以在组件中包装函数,来避免函数的依赖项没有改变的情况下重新渲染。
React.useCallback() 和 React.useMemo() 的区别?
- 共同点:
- 都是 React 的内置 hook。
- 通常作为优化性能被使用,可以用来缓存函数、组件、变量,以避免两次渲染间的重复计算。
- 不同点:
- useCallback() 缓存的是一个函数,useMemo 缓存的是计算结果。
React.forwardRef 是什么及其作用?
- 用来获取实例,创建一个组件,该组件能够将其接收的 ref 属性转发到内部的一个组件中。
虚拟 dom
- 虚拟 DOM 是一个描述 DOM 节点信息的 JS 对象,在 JS 中进行操作,最后再将变化的部分更新到真实DOM中。当页面需要重新渲染时,通过 diff 算法对比新旧 vdom 之间的差异,找出变化的部分,只更新变化的部分,避免不必要的DOM操作,提高页面渲染的性能和效率。
- 虚拟DOM的优点包括:
- 提高页面渲染性能和效率,避免频繁的DOM操作。
- 提高代码的可维护性和可读性,将页面渲染逻辑和业务逻辑分离。
- 提高开发效率和代码质量,可以使用组件化开发方式,提高代码的复用性和可测试性。
- 虚拟DOM的缺点包括:
- 虚拟DOM需要占用更多的内存空间和额外的计算和比较,可能会影响页面的性能和渲染速度。
diff 算法
- Diff算法是虚拟DOM的核心算法之一,它可以通过比较新旧 vdom 之间的差异,找出需要更新的部分,然后只更新这部分内容,避免不必要的DOM操作。
- Diff算法的具体实现可以分为以下几个步骤:
- 比较节点类型:如果新旧节点类型不同,则直接替换整个节点。
- 比较节点属性:如果新旧节点属性不同,则更新节点属性。
- 比较子节点:如果新旧节点都有子节点,则递归进行比较,找出需要更新的部分。
- 插入新节点:如果新节点有子节点,而旧节点没有,则直接插入新节点。
- 删除旧节点:如果旧节点有子节点,而新节点没有,则直接删除旧节点。
- 在实际应用中,Diff算法可以通过一些优化策略来进一步提高性能,例如:
- 对节点进行唯一标识:可以通过节点的 key 属性来标识节点,避免不必要的比较操作。
- 批量更新节点:可以将多个更新操作合并成一次更新,减少DOM操作次数。
- 使用异步更新:可以将更新操作延迟到下一帧或者一段时间后执行,避免阻塞主线程。
为什么虚拟 dom 会提高性能?
- 减少了 DOM 操作次数:虚拟 DOM 可以批量更新,将多次操作合并为一次,减少了 DOM 操作的次数。
- 减少了重排和重绘:虚拟 DOM 可以通过比对前后两次的虚拟 DOM 树,只对需要更新的部分进行操作,减少了浏览器的重排和重绘,提高了性能。
- 更好的跨平台兼容性:虚拟 DOM 可以在不同的平台上运行,例如浏览器、服务器端和移动端等,提高了代码的复用性和可移植性。
说说对 fiber 架构的理解?
- Fiber 是将React的渲染过程分解成一系列可中断的小任务,通过优先级调度算法来动态地调整任务的执行顺序,以提高页面渲染的性能和响应性。
- Fiber架构的主要目标是:
- 任务分解,使 Virtual DOM 可以进行增量式渲染。
- 能够把可中断的任务切片处理。
- 能够调整优先级,重置并复用任务。
- 给不同类型的任务赋予优先级。
- 在父子节点之间能够回退前进。
- 并发方面新的基础能力。
- 更好地支持错误边界。