前端面试React
Contents
1 虚拟DOM的原理是什么
1.1 是什么
- 虚拟DOM就是虚拟节点。React用
JS对象
来模拟DOM节点,然后将其渲染成真实的DOM节点
1.2 怎么做
-
模拟(
JSX => 虚拟DOM对象
)-
用JSX语法写出来的div其实就是一个虚拟节点
-
1 2 3
<div id={"x"}> <span className={'red'}>hi</span> </div>
-
-
通过调用
React.createElement()
,可以将JSX语法转译得到一个虚拟DOM对象-
1 2
React.createElement("div", {id: "x"}, React.createElement("span", {className: "red"}, "hi"))
-
1 2 3 4 5 6 7 8 9 10 11 12 13
{ tag: 'div', props: { id: 'x' }, children: [ { tag: 'span', props: { className: 'red' }, children: [ 'hi' ] } ] }
-
-
-
渲染(
虚拟DOM => 真实DOM
)-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
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 // 略 function setProp // 略
-
如果节点发生变化,并不会直接把新虚拟节点渲染到真实节点,而是先通过
diff
算法得到一个patch
(补丁)再更新到真实节点上
-
1.3 解决了什么问题
- DOM操作性能问题,通过虚拟DOM和diff算法减少不必要的DOM操作,保证性能下限
- DOM操作不方便问题,以前各种
DOM API
要记,现在只有setState
就可以解决DOM更新的问题
1.4 优点
- 为React带来了跨平台能力,因为虚拟节点的存在,除了可以将其渲染为真实节点,还可以渲染为其他东西
- 让DOM操作的整体性能更好,能(通过diff)减少不必要的DOM操作
1.5 缺点
- React为虚拟DOM创造了合成事件,与原生DOM事件不太一样
- 所有React事件都绑定到根元素,自动实现事件委托
- 如果混用合成事件和原生DOM事件,有可能会出bug
1.6 如何解决缺点
不用React,用Vue3。。。
2 DOM diff算法是怎样的
2.1 是什么
DOM diff 就是对比两颗虚拟DOM树的算法。当组件变化时,会render出一个新的虚拟DOM,diff算法对比新旧虚拟DOM之后,得到一个patch
,然后React用patch
来更新真实DOM
2.2 怎么做
-
先对比根节点
- 如果根节点的类型变了(div => span),那么就认为整棵树都变了,不再对比子节点,直接删除对应的真实DOM树,根据虚拟DOM树创建新的真实DOM树
- 如果根节点的类型没变,再对比属性是否发生改变
- 如果属性没变,就保留,继续进行子节点的diff
- 如果属性变了,就只更新该节点的属性,不再重新创建新节点
- 更新style时,如果多个css属性只有一个改变了,那么React只更新改变的
-
然后对子节点继续做以上操作
-
举例
- 情况1
1 2 3 4 5 6 7 8 9 10
<ul> <li>A</li> <li>B</li> </ul> <ul> <li>A</li> <li>B</li> <li>C</li> </ul>
- React依次对比
A-A
,B-B
,null-C
,发现C是新增的,最终会创建真实C节点插入页面
- 情况2
1 2 3 4 5 6 7 8 9 10
<ul> <li>B</li> <li>C</li> </ul> <ul> <li>A</li> <li>B</li> <li>C</li> </ul>
- 其实只需要创建 A 文本,保留 B 和 C 即可,但React不是这样的
- React 对比
B-A
,会删除 B 文本新建 A 文本; - 对比
C-B
,会删除 C 文本,新建 B 文本;(注意,并不是边对比边删除新建,而是把操作汇总到 patch 里再进行 DOM 操作); - 对比
null-C
,会新建 C 文本。
- React 对比
- 这也是React需要key的原因:
1 2 3 4 5 6 7 8 9 10
<ul> <li key="b">B</li> <li key="c">C</li> </ul> <ul> <li key="a">A</li> <li key="b">B</li> <li key="c">C</li> </ul>
- React 先对比 key 发现 key 只新增了一个,于是保留 b 和 c,新建 a
2.3 双端交叉diff算法(Vue)
- 将新旧DOM树同一节点的子节点们分别处理为新旧两个数组
- 头头、尾尾、新头旧尾、旧头新尾、key
- 失败就按流程继续对比,成功就重新进入循环,直到任意一数组的头指针超过尾指针
2.4 React DOM diff和Vue DOM diff的区别
- React是从左向右遍历对比,Vue是双端交叉对比
- Vue整体的diff效率比React更高,
- 假设有n个子节点,我们只是把最后的子节点移动至第一个
- 则React需要借助Map进行key搜索找到匹配项,然后复用节点
- Vue会发现移动,直接复用该节点
- 假设有n个子节点,我们只是把最后的子节点移动至第一个
3 React有哪些生命周期钩子函数
constructor
=> getDerivedStateFormProps
=> shouldComponentUpdate
=> render
=> getSnapshotBeforeUpdate
=> componentDidMount
=> componentDidUpdate
=> componentWillUnmount
- 挂载时调用 constructor,更新时不调用
- 更新时调用 shouldComponentUpdate 和 getSnapshotBeforeUpdate,挂载时不调用
- should… 在 render 前调用,getSnapshot… 在 render 后调用
- 请求放在 componentDidMount 里
4 React如何实现组件间通信
-
父子组件通信:props + 函数,
- 父组件给子组件传用props
- 子组件调用父组件传过来的函数(props.sdata(data)),通过参数传值即可
-
爷孙组件通信:两层父子通信或者使用 Context.Provider 和 Context.Consumer
-
任意组件通信:其实就变成了状态管理了
-
Redux
-
Mobx
-
5 如何理解Redux(DVA)
-
redux是一个状态管理库/容器,集中式管理react应用中多个组件共享的状态。
-
核心概念
State
页面或模块需要共享或持久化的数据,只读的Action = type + payload
普通js对象,约定type属性用来描述执行的动作Reducer
纯函数,接收旧的 state 和 action,返回新的 state。Dispatch
将action派发给storeStore
将action、state、reducer联系在一起的对象Middleware
-
React-redux
核心概念Provider
将store提供给子组件connect()()
将组件与store进行关联,把 redux 的 dispatch 和 state 映射为 react 组件的 props,接收三个参数mapStateToProps
将state合并包装到组件的props属性上mapDispatchToProps
将dispatch合并包装到组件的props属性上,如果不传,组件默认接收dispatch,传的话,写起来可以省略dispatch,更具语义性?
-
中间件
redux-thunk
- 可以在redux中进行异步操作
-
中间件
redux-promise
-
dva
- 比redux多了两个概念
Effect
、Subscription
- Effect被称为副作用,涉及到异步操作都可以写到这里,采用generator相关概念,使得异步操作更加优雅?
- Subscription订阅数据源,比如路由、键盘输入等
- 比redux多了两个概念
6 什么是高阶组件 HOC?
-
参数是函数/组件,返回值也是函数/组件的函数。
-
用法举例说明即可:
-
React.forwardRef
- 函数组件不支持ref,不能持久化保存对元素的引用
- 但使用forwardRef包裹一下,第二个参数就成了ref
- 可以将ref透传(forward)给内部的button
1 2 3 4 5 6 7 8
const Button = React.forwardRef((props, ref) => { return ( <button ref={ref}>{props.children}</button> ) }) const ref = React.createRef() <Button ref={ref}>Click</Button>
-
ReactRedux 的 connect
-
ReactRouter 的 withRouter(class组件),能够赋予组件match、location、history三个属性
-
参考阅读:「react进阶」一文吃透React高阶组件(HOC) - 掘金 (juejin.cn)
7 React Hooks如何模拟组件声明周期
- 模拟 componentDidMount => useEffect(()=>{},[]) 依赖为空数组
- 模拟 componentDidUpdate => useEffect(()=>{}) 不加依赖
- 模拟 componentWillUnmount => useEffect(()=>{ return()=>{}}, []) 依赖空数组+内部return函数
8 React-router实现原理
8.1 前端路由原理:监听浏览器URL变化,截获URL地址,然后进行URL路由匹配,从而渲染路由组件
8.2 两种路由实现方式:history和hash
8.2.1 hash
- 在html5 标准落地之前,都是通过监听
hashchange
事件,当 URL 变化时,进行页面跳转。 - hash的路由地址会有
#
- 在路由变化(点击跳转或历史跳转)时,触发
hashchange
事件,匹配到对应的路由规则,通过location.hash
改变页面路由,以DOM替换的方式更改页面内容 - 手动刷新不会向服务器发送请求,也不会触发hashchange事件
#
后面的数据不会发送到服务器,因此服务器不用担心出现404的问题- 兼容性好
- 锚点定位会失效
- 不利于SEO
8.2.2 history
- 通过 html5-mode 实现单页路由的 URL 没有
#
。- history 模式是通过调用 window.history 对象上的一系列方法来实现页面的无刷新跳转。
- 利用了 HTML5 History Interface 中新增的pushState() 和 replaceState() 方法。
- 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会向后端发送请求。
- 但当用户刷新页面之类的操作时,浏览器还是会给服务器发送请求,此时如果请求路径非根路径,nginx就会出现404的问题
- 为了避免出现这种情况,所以这个实现需要服务器(nginx)的支持,需要把所有路由都重定向到根页面。
|
|
8.3 Router组件
react-router 的工作方式,是在组件树顶层放一个 Router 组件,然后在组件树中散落着很多 Route 组件(注意比 Router 少一个“r”),顶层的 Router 组件负责分析监听 URL 的变化,在它之下的 Route 组件可以直接读取这些信息。Router 是“提供者”,Route是“消费者”。
8.4 Route
Route
只是一个具有渲染方法的普通 React 组件,路由匹配成功则渲染该组件。
9 Mixin HOC 与hooks
三者都是对代码逻辑进行复用的方案
-
Mixin:
-
Mixin类似Object.assgin方法,用赋值的方式将mixin对象中的方法都挂载到原对象上
-
React中,只有当使用createClass来创建组件时才能用
-
存在逻辑与状态被覆盖的问题
-
-
HOC
- 高阶组件,即接受一个组件,返回一个新组件
- 可以处理组件的props、state(反向继承),渲染劫持等
- 当大量使用HOC时,会出现多层嵌套的问题,影响调试
-
hooks
- 解决以上问题(多个hooks互不影响、避免嵌套)
- 使用函数组件代替class组件
10 useState为什么不能在条件或循环中使用
-
手写useState思路:
-
useState接收初始值,返回state和setState的数组;
-
state主要用于维护状态值,而setState则是改变对应的state和刷新组件的作用;
-
重新渲染组件可以使用
ReactDOM.render(<Demo/>, rootElement)
-
-
在组件中,useState维护多个state是有序的(可能是数组,链表),react对useState的标识是用index去记录的,如果在if中使用useState可能会导致顺序出错
-
确保 Hook 在每一次渲染中都按照同样的顺序被调用。
-
为什么useState是数组结构的形式返回的,能不能以Object的形式返回?
- 因为useState维护多个state是有序的
11 react工作流程
-
工作流程:
- 生成react root节点
- reconciler 协调生成需要更新的子节点
- 将节点更新commit 到视图
-
fiber:
-
Fiber是React更新时的最小单元,是一种包含指针的数据结构,从数据结构上看Fiber架构 ≈ 树 + 链表。
-
Fiber单元是从 jsx createElement之后根据ReactElement生成的,相比 ReactElement,Fiber单元具备动态工作能力。
-
12 class组件与函数组件的异同与好处
Author gsemir
LastMod 2021-05-19