React 面试题整理
React
react 特性
- JSX 语法
- 单向数据绑定
- 虚拟 dom
- 声明式编程
- component 可组合,可复用,可维护
react 优点
- 高效灵活
- 声明式设计, 简单使用
- 组件式开发提高了代码复用率
- 单向数据流比双向更加安全, 速度更快
React 事件机制
React 基于浏览器事件机制实现了一套自身的事件机制, 包括了事件注册、事件合成、事件冒泡、事件派发等, React 中的这套事件称为合成事件
React 上注册的事件最终会绑定在 document 上, 而不是 react 组件对应的 dom, 以此来减少内存开销
React 自身实现了一套事件冒泡机制, 所以有时 e.stopPropagation() 无效
-
合成事件 SyntheticEvent
-
概念: React 模拟原生 DOM 事件所有能力的一个事件对象
-
优点: onClick 看似绑定在 dom 元素上, 实际上并没有绑定到真实的节点上, 而是绑定到结构的最外层(document), 使用一个统一的事件去监听, 简化了事件处理和回收机制, 减少内存开销, 提高了效率; 事件监听器维持了一个映射来保存所有组件内部的事件监听和处理函数, 当组件挂载或卸载时, 只是在监听器上插入或删除一些对象;
-
与原生事件的区别:
-
命名方式不同 原生的 onclick, React 的 onClick
-
事件处理函数书写不同 原生的 "handleClick()", React 的 {handleClick} 举例:
-
-
// 原生
<button onclick="handleClick()"></button>;
// React
const button = <button onClick={handleClick}></button>;
-
执行顺序 React 所有事件都挂载在 document 对象上, dom 元素触发事件会冒泡到 document 对象上, 再处理 react 事件 所以执行顺序是: 先执行原生事件 --> 再处理 react 事件 --> 最后真正执行 document 上挂载的事件
-
react 中阻止事件冒泡的方法:
- 阻止合成事件间的冒泡 --> e.stopPropagation()
- 阻止合成事件与最外层 document 上的事件间的冒泡 --> e.nativeEvent.stopImmediatePropagation()
- 阻止合成事件与除最外层 document 上的原生事件的冒泡 --> e.target
react-router 的理解
- 组件
- BrowserRouter(history 模式)、HashRouter(hash 模式)
- Route
- Link、NavLink --> 最终渲染为 a 标签, to 代替了 href 属性
- hooks
- useHistory --> 组件内直接访问 history , 无需通过 props
- useParams
- useLocation --> 返回当前 url 的 location 对象
- 参数传递
- 动态路由
path: "/detail/:id";
history.push("/detail/102"); --> console.log(props.match.params.id, "102");
- search 传递
history.push("/detail2?name=xm&age=18"); --> console.log(props.location.search.name, "xm");
- to 传入对象
<Link
to={{
pathname: "/detail",
query: { name: "xm", age: 18 },
state: { address: "xxx" },
search: "?id=123",
}}
>
link
</Link>
--> console.log(props.location, "obj");
- 模式 前端路由的核心, 就在于改变视图的同时不会向后端发出请求;而是加载路由对应的组件
-
hash 模式: 监听浏览器地址 hash 值变化, 执行相应的 js 切换网页; hash 指的是地址中#号以及后面的字符, 也称为散列值。hash 也称作锚点, 本身是用来做页面跳转定位的. 使用 window.location.hash 属性及窗口的 onhashchange 事件, 可以实现监听浏览器地址 hash 值变化, 执行相应的 js 切换网页.
-
history 模式: 利用 history API 实现 url 地址改变, 网页内容改变; window.history 属性指向 History 对象, 它表示当前窗口的浏览历史。当发生改变时, 只会改变页面的路径, 不会刷新页面. 移动到以前访问过的页面时, 页面通常是从浏览器缓存之中加载, 而不是重新要求服务器发送新的网页。
-
区别: 形式上最明显的就是 hash 会在浏览器地址后面增加#号, 而 history 可以自定义地址。 功能上的区别, 比如我们在开发 app 的时候有分享页面, 比如把这个页面分享到第三方的 app 里, 有的 app 里面 url 是不允许带有#号的, 所以就要使用 history 模式, 但是使用 history 模式还有一个问题就是, 在访问二级页面的时候, 做刷新操作, 会出现 404 错误, 那么就需要和后端配合, 让他配置一下 apache 或是 nginx 的 url 重定向, 重定向到你的首页路由上就 ok 了
history 致命的缺点就是当改变页面地址后, 强制刷新浏览器时, (如果后端没有做准备的话)会报错, 因为刷新是拿当前地址去请求服务器的, 如果服务器中没有相应的响应, 会出现 404 页面
hash 每次 URL 的改变不属于一次 http 请求, 所以不利于 SEO 优化
类组件和函数组件
函数组件语法更短更简单, 便于理解和开发维护 类组件有更好的状态管理, 但用了过多的 this, 降低了代码可读性和维护性
-
类组件 通过使用 ES6 类的编写形式去编写组件, 该类必须继承 React.Component 访问父组件传递过来的参数可以通过 props 在组件中必须使用 render 方法, 在 return 中 返回 React 对象
class HelloMessage extends React.Component { constructor(props) { super(props); } render() { return <div>nice to meet you! {this.props.name}</div>; } }
-
函数组件 通过函数编写的形式去实现一个 React 组件
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }
-
区别
- 编写形式不同
- 状态管理 在 hooks 出来之前, 函数组件就是无状态组件, 不能保管组件的状态, 而类组件中可以调用 setState; 函数组件如果想要管理 state, 可以使用 useState
- 生命周期 函数组件中不存在生命周期, 因为生命周期钩子都来自于继承的 React.Component, 如果需要用到生命周期就只能使用类组件 但 hooks 之后, 函数组件可以通过 useEffect 替代生命周期的作用
useEffect(() => {}, []) == componentDidMount; useEffect(() => { return () => { // return 的函数会在组件卸载时执行 }; }, []) == componentWillUnmount; useEffect(() => {}, [state1]) == componentDidUpdate;
-
调用方式 函数组件的调用就是执行这个函数 类组件需要将组件进行实例化, 然后调用实例对象的 render 方法
-
获取渲染值 类组件中的 this 是可变的, 以便于在 render 和生命周期中拿到最新值, 所以可以根据变化取到对应的 props 值 而函数组件中没有 this
生命周期
创建 --> componentDidMount 更新 --> shouldComponentUpdate, componentDidUpdate 卸载 --> componentWillUnMount
组件通信
- 父传子 --> props
- 子传父 --> 父组件向子组件传递一个函数, 子组件调用这个函数传参给父组件
- 父子组件数据共享 --> useControllableValue
- 兄弟组件通信 --> 将父组件作为中间层实现
- 父组件传递给后代组件 --> context
- React.createContext() 创建一个上下文对象 Ctx;
- 在顶层组件 A 中通过 Ctx.Provider 组件(高阶组件)提供数据
- 在底层组件 B 中通过 Ctx.Consumer 组件 或者 useContext 钩子函数获取数据
- 非关系组件 --> redux
引入 CSS 的几种方式
- 组件内直接使用
- 优点: 内联样式不会有冲突; 可以根据 state 的值展示不同样式;
- 缺点: 必须驼峰; 代码混乱;
- 组件内引入.css 文件
- 优点: 便于代码管理, 使用方便;
- 缺点: 全剧生效, 可能会有类名冲突;
- 组件内引入.module.css 文件 -->
- 实现:
- 需要 webpack 配置文件: modules:true;
- 所有的 className 需要使用 {style.className} 的形式来编写
- 优点: 将 css 文件作为一个 模块引入, 模块中所有的 css 只作用于当前组件, 不会有冲突;
- 缺点: 引用的类名不能使用"-"连接符; 不方便动态修改某些样式;
- CSS in JS
高阶组件(HOC)
接收一个或多个组件作为参数并且返回一个组件的函数
高阶组件接收一个组件, 将其加工后返回: 其功能就是封装并且分离组件的通用逻辑, 把通用的逻辑放在高阶组件中, 通过一致的处理实现代码的复用.
(高阶组件本质上就是一个函数)
-
优点 提高代码的复用性和灵活性
-
应用场景 用于处理在多个模块复用的功能
<!-- 需求: 某页面需要从存储中获取数据并进行一定的处理再渲染到页面上 --> <!-- 使用前 --> componentDidMount() { // 获取数据、处理数据得到data1 } render () { // data1 } <!-- 使用后 --> // 高阶组件: function doSomething(XxComponent) { return class extends Component { componentDidMount() { // 获取数据、处理数据得到data1 } render () { // data1 return ( <XxComponent {…this.props} data1={data1} /> ) } } } // 参数组件: class Home extends Component { render() { return <div>{this.props.data1}</div> } } export default doSomething(Home)
refs
用来获取 dom 节点, 操作 dom
setState
- 执行机制 触发事件 -> 执行 setState -> state 更新 -> 重新执行 render 函数 -> 页面视图更新
- 同步更新/异步更新
- 异步: 生命周期或 react 合成事件中, setState 是异步更新的
- 同步: setTimeout 或原生 dom 事件中, setState 是同步更新的
如何提高 React 组件渲染效率
- shouldComponentUpdate
- React.memo --> 本质是一个高阶组件, 用来缓存组件的渲染, 避免不必要的更新
- 组件拆分
事件绑定的几种方式
- render 中使用 bind --> 每次调用 render 都会生成新的方法实例, 性能欠缺
<div onClick={this.handleClick.bind(this)}>test</div>
- render 中使用箭头函数 --> 每次调用 render 都会生成新的方法实例, 性能欠缺
<div onClick={(e) => this.handleClick(e)}>test</div>
- constructor 中使用 bind
class App extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log("this > ", this);
}
render() {
return <div onClick={this.handleClick}>test</div>;
}
}
- 定义阶段使用箭头函数 --> 性能更高, 推荐使用
handleClick = () => {
console.log("this > ", this);
};
<div onClick={this.handleClick}>test</div>;
diff 算法
传统的 diff 算法通过循环递归对节点进行依次对比, 效率低下, 算法复杂度 **O(n3)**, react 的 diff 算法复杂度 **O(n)**
- tree 层级 --> 只对相同层级的节点进行比较, 只有删除创建操作没有移动操作
- component 层级 --> 如果是同一个类的组件, 则会继续往下 diff 运算, 否则直接删除这个组件下的所有子节点, 创建新的
- element 层级 --> 对于比较统一层级的节点们, 每个节点在对应的层级用唯一的 key 作为标识 提供了三种节点操作:插入、移动和删除 对比新旧节点时, 通过 key 判断是新增的节点, 还是相同的节点只需要移动
Key 的作用
作为唯一标识, 用来判断一个元素是新创建的还是被移动的, 从而减少非必要的元素渲染
- 注意: 唯一; 不要使用随机数(会在 render 时重新生成); 不能使用 index;
什么时候触发 reRender
- 组件状态更新 useState 更新状态时, 根据当前值有无改变判断是否触发 render; setState 更新状态时, 不管 state 有没有变, 都会触发 render;
- 父组件 re-render;
- Context 变更.
fiber
-
为了解决什么问题(react 15 的问题)? 在页面元素很多且频繁刷新的场景下, React 15 会出现掉帧的现象. 因为默认情况下, JS 引擎和 UI 渲染引擎都是运行在浏览器的主线程中的, 而且是互斥的关系. 如果 JS 运算持续占用主线程, 页面就没办法及时更新. 那当我们调用 setState 更新页面的时候, React 会遍历所有的节点, 计算出 VDOM 树, 然后再更新 UI(这个过程是不能被打断的). 如果页面元素很多, 就会阻塞了浏览器的 UI 渲染. (超过 16 毫秒就会出现掉帧的现象)
-
解决思路(解决主线程长时间被 JS 运算占用这一问题)? 基本思路是将运算切割为多个步骤, 分批完成. 也就是说在完成一部分任务之后, 将控制权交回给浏览器, 让浏览器有时间进行页面的渲染. 等浏览器忙完之后, 再继续之前未完成的任务. React 15 是通过递归的方式进行渲染, 使用的是 JS 引擎自身的函数调用栈, 它会一直执行到栈空为止, 所以我们说这个过程是不能被打断的. 而 Fiber 实现了自己的组件调用栈, 它以链表的形式遍历组件树, 可以灵活的中断、继续执行. 实现方式是使用了浏览器的 requestIdleCallback() api.
window.requestIdleCallback() 会在浏览器空闲时期, 依次调用函数
-
主要做了哪些操作
React 框架内部的运作可以分为 3 层:
1. Virtual DOM 层, 描述页面长什么样;
2. Reconciler 层, 负责调用组件生命周期方法, 进行 Diff 运算等;
3. Renderer 层, 根据不同的平台(ReactDom 和 RN), 渲染出相应的页面.
React 官方对 Reconciler 层进行了很大的改动, 起了个新名字叫 Fiber Reconciler, 便于区分, 15 的 Reconciler 被命名为 Stack Reconciler; 区别: Stack Reconciler 不可被打断, Fiber Reconciler 每执行一段时间, 都会将控制权交回给浏览器, 可以分段执行; Fiber 有一个调度器 (Scheduler) 可以进行任务分配, 优先级高的任务(如键盘输入)可以打断优先级低的任务(如 Diff)的执行;
-
fiber 有几个阶段?
- 阶段一, 在 VDOM 树的基础上增加额外的信息来生成 Fiber 树, 进行 Diff 计算, 得出需要更新的节点信息, 这一步是可以被打断的, 每生成一个新的节点, 都会将控制权交回给主线程, 去检查有没有优先级更高的任务需要执行。如果有, Fiber 会丢弃正在生成的树, 在空闲的时候再重新执行一遍, 如果没有, 则继续构建.
- 阶段二, 将需要更新的节点批量更新, 这个过程不能被打断
(递归改循环)
createElement()
JSX 会被 babel 转译为 React.createElement(type, config, children) createElement 参数: type: 元素类型(标签名字符串或者 React 组件); config: 元素属性; children: 子元素.
// JSX
<div id="foo">bar</div>;
// babel 转译为
React.createElement("div", { id: "foo" }, "bar");
Immutable --> 不可变数据结构
Immutable Data 就是一旦创建, 就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象
原理是 Persistent Data Structure(持久化数据结构), 也就是使用旧数据创建新数据时, 要保证旧数据同时可用且不变
Immutable 的几种数据类型: List: 有序索引集, 类似 JavaScript 中的 Array。 Map: 无序索引集, 类似 JavaScript 中的 Object。 OrderedMap: 有序的 Map, 根据数据的 set()进行排序。 Set: 没有重复值的集合。 OrderedSet: 有序的 Set, 根据数据的 add 进行排序。
扩展阅读(~~学不动了, 下面的先不学了~~)
垃圾回收机制 v8
batchUpdate 的原理
Suspense 的实现
lagacy 模式和 concurrent 模式
- lagacy --> 同步更新
- concurrent --> 异步可中断的更新, 是并发模式的, 采用时间片的方式执行异步操作