面试题 161. 简述React-Router的路由有⼏种模式 ?
参考回答:
React-Router ⽀持使⽤ hash(对应 HashRouter)和 browser(对应 BrowserRouter) 两种
路由规则, react-router-dom 提供了 BrowserRouter 和 HashRouter 两个组件来实现应⽤的
UI 和 URL 同步:
BrowserRouter 创建的 URL 格式:xxx.com/path
HashRouter 创建的 URL 格式:xxx.com/#/path
(1)BrowserRouter
它使⽤ HTML5 提供的 history API(pushState、replaceState 和 popstate 事件)来保持
UI 和 URL 的同步。由此可以看出,BrowserRouter 是使⽤ HTML 5 的 history API 来控制路
由跳转的
basename={string}
forceRefresh={bool}
getUserConfirmation={func}
keyLength={number}
/>
其中的属性如下:
basename 所有路由的基准 URL。basename 的正确格式是前⾯有⼀个前导斜杠,但不能有尾部斜
杠;forceRefresh 如果为 true,在导航的过程中整个⻚⾯将会刷新。⼀般情况下,只有在不⽀持
HTML5 history API 的浏览器中使⽤此功能;
getUserConfirmation ⽤于确认导航的函数,默认使⽤ window.confirm。例如,当从 /a 导
航⾄ /b 时,会使⽤默认的 confirm 函数弹出⼀个提示,⽤户点击确定后才进⾏导航,否则不做
任何处理;// 这是默认的确认函数
const getConfirmation = (message, callback) => {
const allowTransition = window.confirm(message);
callback(allowTransition);
}(2)HashRouter
使⽤ URL 的 hash 部分(即 window.location.hash)来保持 UI 和 URL 的同步。由此可以看
出,HashRouter 是通过 URL 的 hash 属性来控制路由跳转的:
basename={string}
getUserConfirmation={func}
hashType={string}
/>其参数如下:
basename, getUserConfirmation 和 BrowserRouter 功能⼀样;
hashType window.location.hash 使⽤的 hash 类型,有如下⼏种:
slash - 后⾯跟⼀个斜杠,例如 #/ 和 #/sunshine/lollipops;
noslash - 后⾯没有斜杠,例如 # 和 #sunshine/lollipops;
hashbang - Google ⻛格的 ajax crawlable,例如 #!/ 和
#!/sunshine/lollipops。
面试题 162. 简述Redux 怎么实现属性传递,介绍下原理 ?
参考回答:
react-redux 数据传输∶ view-->action-->reducer-->store-->view。看下点击事件的数据是
如何通过redux传到view上:
view 上的AddClick 事件通过mapDispatchToProps 把数据传到action ---> click:
()=>dispatch(ADD)
action 的ADD 传到reducer上
reducer传到store上 const store = createStore(reducer);
store再通过 mapStateToProps 映射穿到view上text:State.text
代码示例∶
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { Provider, connect } from 'react-redux';
class App extends React.Component{
render(){
let { text, click, clickR } = this.props;
return(数据:已有⼈{text}加⼈减⼈)
}
}
const initialState = {
text:5
}
const reducer = function(state,action){
switch(action.type){
case 'ADD':
return {text:state.text+1}
case 'REMOVE':
return {text:state.text-1}
default:
return initialState;
}
}
let ADD = {
type:'ADD'
}
let Remove = {
type:'REMOVE'
}
const store = createStore(reducer);
let mapStateToProps = function (state){
return{
text:state.text
}
}let mapDispatchToProps = function(dispatch){
return{
click:()=>dispatch(ADD),
clickR:()=>dispatch(Remove)
}
}
const App1 = connect(mapStateToProps,mapDispatchToProps)(App);
ReactDOM.render(,document.getElementById('root')
)
面试题 163. Redux 中间件是什么?接受⼏个参数?柯⾥化函数两端的参数具体是什么 ?
参考回答:
Redux 的中间件提供的是位于 action 被发起之后,到达 reducer 之前的扩展点,换⽽⾔之,原本view -→> action -> reducer -> store 的数据流加上中间件后变成了 view -> action ->middleware -> reducer -> store ,在这⼀环节可以做⼀些"副作⽤"的操作,如异步请求、打印
⽇志等。
applyMiddleware源码
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
// 利⽤传⼊的createStore和reducer和创建⼀个store
const store = createStore(...args)
let dispatch = () => {
throw new Error()
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 让每个 middleware 带着 middlewareAPI 这个参数分别执⾏⼀遍
const chain = middlewares.map(middleware =>
middleware(middlewareAPI))
// 接着 compose 将 chain 中的所有匿名函数,组装成⼀个新的函数,即新的
dispatch
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}从applyMiddleware中可以看出∶
redux中间件接受⼀个对象作为参数,对象的参数上有两个字段 dispatch 和 getState,分别代
表着 Redux Store 上的两个同名函数。
柯⾥化函数两端⼀个是 middewares,⼀个是store.dispatch
面试题 164. Redux 请求中间件如何处理并发 ?
参考回答:
使⽤redux-Saga redux-saga是⼀个管理redux应⽤异步操作的中间件,⽤于代替 redux-thunk
的。它通过创建 Sagas 将所有异步操作逻辑存放在⼀个地⽅进⾏集中处理,以此将react中的同步操作
与异步操作区分开来,以便于后期的管理与维护。 redux-saga如何处理并发:
takeEvery
可以让多个 saga 任务并⾏被 fork 执⾏。
import {
fork,
take
} from "redux-saga/effects"
const takeEvery = (pattern, saga, ...args) => fork(function*() {
while (true) {
const action = yield take(pattern)
yield fork(saga, ...args.concat(action))
}
})takeLatest
takeLatest 不允许多个 saga 任务并⾏地执⾏。⼀旦接收到新的发起的 action,它就会取消前⾯
所有 fork 过的任务(如果这些任务还在执⾏的话)。 在处理 AJAX 请求的时候,如果只希望获取最
后那个请求的响应, takeLatest 就会⾮常有⽤。import {
cancel,
fork,
take
} from "redux-saga/effects"
const takeLatest = (pattern, saga, ...args) => fork(function*() {
let lastTask
while (true) {
const action = yield take(pattern)
if (lastTask) {
yield cancel(lastTask) // 如果任务已经结束,则 cancel 为空操作
}
lastTask = yield fork(saga, ...args.concat(action))
}
})
面试题 165. 简述Redux 状态管理器和变量挂载到 window 中有什么区别 ?
参考回答:
两者都是存储数据以供后期使⽤。但是Redux状态更改可回溯——Time travel,数据多了的时候可以很清晰的知道改动在哪⾥发⽣,完整的提供了⼀套状态管理模式。
随着 JavaScript 单⻚应⽤开发⽇趋复杂,JavaScript 需要管理⽐任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地⽣成尚未持久化到服务器的数据,也包括UI状态,如激活的路由,被选中的标签,是否显示加载动效或者分⻚器等等。
管理不断变化的 state ⾮常困难。如果⼀个 model 的变化会引起另⼀个 model 变化,那么当view 变化时,就可能引起对应 model 以及另⼀个model 的变化,依次地,可能会引起另⼀个 view的变化。直⾄你搞不清楚到底发⽣了什么。state 在什么时候,由于什么原因,如何变化已然不受控
制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。 如果这还不够糟糕,考虑⼀些来⾃前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前所未有的复杂性,难道就这么放弃了吗?当然不是。
这⾥的复杂性很⼤程度上来⾃于:我们总是将两个难以理清的概念混淆在⼀起:变化和异步。 可以称它们为曼妥思和可乐。如果把⼆者分开,能做的很好,但混到⼀起,就变得⼀团糟。⼀些库如 React 视图在视图层禁⽌异步和直接操作 DOM来解决这个问题。美中不⾜的是,React 依旧把处理 state 中数据的问题留给了你。Redux就是为了帮你解决这个问题。
面试题 166. 简述mobox 和 redux 有什么区别 ?
参考回答:
(1)共同点
为了解决状态管理混乱,⽆法有效同步的问题统⼀维护管理应⽤状态;
某⼀状态只有⼀个可信数据来源(通常命名为store,指状态容器);
操作更新状态⽅式统⼀,并且可控(通常以action⽅式提供更新状态的途径);
⽀持将store与React组件连接,如react-redux,mobx- react;
(2)区别 Redux更多的是遵循Flux模式的⼀种实现,是⼀个 JavaScript库,它关注点主要是以下⼏
⽅⾯∶
Action∶ ⼀个JavaScript对象,描述动作相关信息,主要包含type属性和payload属性∶
Reducer∶ 定义应⽤状态如何响应不同动作(action),如何更新状态;
Store∶ 管理action和reducer及其关系的对象,主要提供以下功能
o 维护应⽤状态并⽀持访问状态(getState());
o ⽀持监听action的分发,更新状态(dispatch(action));
o ⽀持订阅store的变更(subscribe(listener))异步流∶ 由于Redux所有对store状态的变更,都应该通过action触发,异步任务(通常都是业务或获取数据任务)也不例外,⽽为了不将业务或数据相关的任务混⼊React组件中,就需要使⽤其他框架配合管理异步任务流程,如redux-thunk,redux-saga等;
Mobx是⼀个透明函数响应式编程的状态管理库,它使得状态管理简单可伸缩∶
Action∶定义改变状态的动作函数,包括如何变更状态;
Store∶ 集中管理模块状态(State)和动作(action)
Derivation(衍⽣)∶ 从应⽤状态中派⽣⽽出,且没有任何其他影响的数据对⽐总结:
redux将数据保存在单⼀的store中,mobx将数据保存在分散的多个store中
redux使⽤plain object保存数据,需要⼿动处理变化后的操作;mobx适⽤observable保存数据,数据变化后⾃动处理响应的操作
redux使⽤不可变状态,这意味着状态是只读的,不能直接去修改它,⽽是应该返回⼀个新的状态,同时使⽤纯函数;mobx中的状态是可变的,可以直接对其进⾏修改mobx相对来说⽐较简单,在其中有很多的抽象,mobx更多的使⽤⾯向对象的编程思维;redux会⽐较
复杂,因为其中的函数式编程思想掌握起来不是那么容易,同时需要借助⼀系列的中间件来处理异步和副作⽤
mobx中有更多的抽象和封装,调试会⽐较困难,同时结果也难以预测;⽽redux提供能够进⾏时间回溯的开发⼯具,同时其纯函数以及更少的抽象,让调试变得更加的容
面试题 167. 简述Redux 和 Vuex 有什么区别,它们的共同思想 ?
参考回答:
(1)Redux 和 Vuex区别
Vuex改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,⽆需switch,只需在对应的mutation函数⾥改变state值即可
Vuex由于Vue⾃动重新渲染的特性,⽆需订阅重新渲染函数,只要⽣成新的State即可Vuex数据流的顺序是∶View调⽤store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化⾃动渲染)
通俗点理解就是,vuex 弱化 dispatch,通过commit进⾏ store状态的⼀次更变;取消了action概念,不必传⼊特定的 action形式进⾏指定变更;弱化reducer,基于commit参数直接对数据进⾏转变,使得框架更加简易(2)共同思想
单—的数据源
变化可以预测
本质上∶ redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的⼀种⽅案
面试题 168. 简述Redux 中间件是怎么拿到store 和 action? 然后怎么处理 ?
参考回答:
redux中间件本质就是⼀个函数柯⾥化。redux applyMiddleware Api 源码中每个middleware 接受2个参数, Store 的getState 函数和dispatch 函数,分别获得store和action,最终返回⼀个函数。该函数会被传⼊ next 的下⼀个 middleware 的 dispatch ⽅法,并返回⼀个接收 action
的新函数,这个函数可以直接调⽤ next(action),或者在其他需要的时刻调⽤,甚⾄根本不去调⽤它。调⽤链中最后⼀个 middleware 会接受真实的 store的 dispatch ⽅法作为 next 参数,并借此结束调⽤链。所以,middleware 的函数签名是({ getState,dispatch })=> next =>action
面试题 169. 简述为什么 useState 要使⽤数组⽽不是对象 ?
参考回答:
useState 的⽤法:
可以看到 useState 返回的是⼀个数组,那么为什么是返回数组⽽不是返回对象呢?
这⾥⽤到了解构赋值,所以先来看⼀下ES6 的解构赋值:
数组的解构赋值
const foo = [1, 2, 3];
const [one, two, three] = foo;
console.log(one); // 1
console.log(two); // 2
console.log(three); // 3
对象的解构赋值
const user = {
id: 888,
name: "xiaoxin"
};
const { id, name } = user;
console.log(id); // 888
console.log(name); // "xiaoxin"
看完这两个例⼦,答案应该就出来了:
如果 useState 返回的是数组,那么使⽤者可以对数组中的元素命名,代码看起来也⽐较⼲净
如果 useState 返回的是对象,在解构对象的时候必须要和 useState 内部实现返回的对象同
名,想要使⽤多次的话,必须得设置别名才能使⽤返回值
下⾯来看看如果 useState 返回对象的情况:
const [count, setCount] = useState(0)
const foo = [1, 2, 3];
const [one, two, three] = foo;
console.log(one); // 1
console.log(two); // 2
console.log(three); // 3
看完这两个例⼦,答案应该就出来了:
如果 useState 返回的是数组,那么使⽤者可以对数组中的元素命名,代码看起来也⽐较⼲净
如果 useState 返回的是对象,在解构对象的时候必须要和 useState 内部实现返回的对象同
名,想要使⽤多次的话,必须得设置别名才能使⽤返回值
下⾯来看看如果 useState 返回对象的情况// 第⼀次使⽤
const { state, setState } = useState(false);
// 第⼆次使⽤
const { state: counter, setState: setCounter } = useState(0)这⾥可以看到,返回对象的使⽤⽅式还是挺麻烦的,更何况实际项⽬中会使⽤的更频繁。 总结:*useState 返回的是 array ⽽不是 object 的原因就是为了* 降低使⽤的复杂度,返回数组的
话可以直接根据顺序解构,⽽返回对象的话要想使⽤多次就需要定义别名了。
面试题 170. 简述React Hooks 解决了哪些问题 ?
参考回答:
React Hooks 主要解决了以下问题:
(1)在组件之间复⽤状态逻辑很难
React 没有提供将可复⽤性⾏为“附加”到组件的途径(例如,把组件连接到 store)解决此类问题可
以使⽤ render props 和 ⾼阶组件。但是这类⽅案需要重新组织组件结构,这可能会很麻烦,并且会
使代码难以理解。由 providers,consumers,⾼阶组件,render props 等其他抽象层组成的组件
会形成“嵌套地狱”。尽管可以在 DevTools 过滤掉它们,但这说明了⼀个更深层次的问题:React 需
要为共享状态逻辑提供更好的原⽣途径。
可以使⽤ Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复⽤。Hook 使我们在⽆需修改
组件结构的情况下复⽤状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。
(2)复杂组件变得难以理解
在组件中,每个⽣命周期常常包含⼀些不相关的逻辑。例如,组件常常在 componentDidMount 和
componentDidUpdate 中获取数据。但是,同⼀个 componentDidMount 中可能也包含很多其它的
逻辑,如设置事件监听,⽽之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代
码被进⾏了拆分,⽽完全不相关的代码却在同⼀个⽅法中组合在⼀起。如此很容易产⽣ bug,并且导致
逻辑不⼀致。
在多数情况下,不可能将组件拆分为更⼩的粒度,因为状态逻辑⽆处不在。这也给测试带来了⼀定挑战。
同时,这也是很多⼈将 React 与状态管理库结合使⽤的原因之⼀。但是,这往往会引⼊了很多抽象概
念,需要你在不同的⽂件之间来回切换,使得复⽤变得更加困难。
为了解决这个问题,Hook 将组件中相互关联的部分拆分成更⼩的函数(⽐如设置订阅或请求数据),⽽
并⾮强制按照⽣命周期划分。你还可以使⽤ reducer 来管理组件的内部状态,使其更加可预测。
(3)难以理解的 class
除了代码复⽤和代码管理会遇到困难外,class 是学习 React 的⼀⼤屏障。我们必须去理解
JavaScript 中 this 的⼯作⽅式,这与其他语⾔存在巨⼤差异。还不能忘记绑定事件处理器。没有稳
定的语法提案,这些代码⾮常冗余。⼤家可以很好地理解 props,state 和⾃顶向下的数据流,但对
class 却⼀筹莫展。即便在有经验的 React 开发者之间,对于函数组件与 class 组件的差异也存在
分歧,甚⾄还要区分两种组件的使⽤场景。
为了解决这些问题,Hook 使你在⾮ class 的情况下可以使⽤更多的 React 特性。 从概念上讲,
React 组件⼀直更像是函数。⽽ Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook
提供了问题的解决⽅案,⽆需学习复杂的函数式或响应式编程技术
面试题 171. 简述 React Hook 的使⽤限制有哪些 ?
参考回答:
React Hooks 的限制主要有两条:
不要在循环、条件或嵌套函数中调⽤ Hook;
在 React 的函数组件中调⽤ Hook。
那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。
组件之间难以复⽤状态逻辑。过去常⻅的解决⽅案是⾼阶组件、render props 及状态管理框架。
复杂的组件变得难以理解。⽣命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
⼈和机器都很容易混淆类。常⻅的有 this 的问题,但在 React 团队中还有类难以优化的问题,希望在编译优化层⾯做出⼀些改进。
这三个问题在⼀定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然⽽第三个问题决定了 Hooks 只⽀持函数组件。
那为什么不要在循环、条件或嵌套函数中调⽤ Hook 呢?因为 Hooks 的设计是基于数组实现。在调⽤时按顺序加⼊数组中,如果使⽤循环、条件或嵌套函数很有可能导致数组取值错位,执⾏错误的 Hook。
当然,实质上 React 的源码⾥不是数组,是链表。
这些限制会在编码上造成⼀定程度的⼼智负担,新⼿可能会写错,为了避免这样的情况,可以引⼊ESLint 的 Hooks 检查插件进⾏预防。
面试题 172. 简述useEffect 与 useLayoutEffect 的区别 ?
参考回答:
(1)共同点
运⽤效果: useEffect 与 useLayoutEffect 两者都是⽤于处理副作⽤,这些副作⽤包括改变DOM、设置订阅、操作定时器等。在函数组件内部操作副作⽤是不被允许的,所以需要使⽤这两个函数去处理。
使⽤⽅式: useEffect 与 useLayoutEffect 两者底层的函数签名是完全⼀致的,都是调⽤的mountEffectImpl⽅法,在使⽤上也没什么差异,基本可以直接替换。
(2)不同点
使⽤场景: useEffect 在 React 的渲染过程中是被异步调⽤的,⽤于绝⼤多数场景;⽽useLayoutEffect 会在所有的 DOM 变更之后同步调⽤,主要⽤于处理 DOM 操作、调整样式、避免⻚⾯闪烁等问题。也正因为是同步处理,所以需要避免在 useLayoutEffect 做计算量较⼤的
耗时任务从⽽造成阻塞。
使⽤效果: useEffect是按照顺序执⾏代码的,改变屏幕像素之后执⾏(先渲染,后改变DOM),当改变屏幕内容时可能会产⽣闪烁;useLayoutEffect是改变屏幕像素之前就执⾏了(会推迟⻚⾯显示的事件,先改变DOM后渲染),不会产⽣闪烁。useLayoutEffect总是⽐useEffect先执⾏。
在未来的趋势上,两个 API 是会⻓期共存的,暂时没有删减合并的计划,需要开发者根据场景去⾃⾏选择。React 团队的建议⾮常实⽤,如果实在分不清,先⽤ useEffect,⼀般问题不⼤;如果⻚⾯有异常,再直接替换为 useLayoutEffect 即可。
面试题 173. 简述React diff 算法的原理是什么 ?
参考回答:
实际上,diff 算法探讨的就是虚拟 DOM 树发⽣变化后,⽣成 DOM 树更新补丁的⽅式。它通过对⽐新
旧两株虚拟 DOM 树的变更差异,将更新补丁作⽤于真实 DOM,以最⼩成本完成视图更新。
具体的流程如下:
真实的 DOM ⾸先会映射为虚拟 DOM;
当虚拟 DOM 发⽣变化后,就会根据差距计算⽣成 patch,这个 patch 是⼀个结构化的数据,内
容包含了增加、更新、移除等;
根据 patch 去更新真实的 DOM,反馈到⽤户的界⾯上。
⼀个简单的例⼦:
这⾥,⾸先假定 ExampleComponent 可⻅,然后再改变它的状态,让它不可⻅ 。映射为真实的 DOM
操作是这样的,React 会创建⼀个 div 节点。
当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写内部的 innerText
为 hidden。这样⼀个⽣成补丁、更新差异的过程统称为 diff 算法。
diff算法可以总结为三个策略,分别从树、组件及元素三个层⾯进⾏复杂度的优化:
import React from 'react'
export default class ExampleComponent extends React.Component {
render() {
if(this.props.isVisible) {
returnvisbile
;
}
return
hidden
;
}
}策略⼀:忽略节点跨层级操作场景,提升⽐对效率。(基于树进⾏对⽐)
这⼀策略需要进⾏树⽐对,即对树进⾏分层⽐较。树⽐对的处理⼿法是⾮常“暴⼒”的,即两棵树只对同⼀
层次的节点进⾏⽐较,如果发现节点已经不存在了,则该节点及其⼦节点会被完全删除掉,不会⽤于进⼀
步的⽐较,这就提升了⽐对效率。
策略⼆:如果组件的 class ⼀致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进⾏
对⽐)
在组件⽐对的过程中:
如果组件是同⼀类型则进⾏树⽐对;
如果不是则直接放⼊补丁中。
只要⽗组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate、
PureComponent 及 React.memo 可以提⾼性能的原因。
策略三:同⼀层级的⼦节点,可以通过标记 key 的⽅式进⾏列表对⽐。(基于节点进⾏对⽐)
元素⽐对主要发⽣在同层级中,通过标记节点操作⽣成补丁。节点操作包含了插⼊、移动、删除等。其中
节点重新排序同时涉及插⼊、移动、删除三个操作,所以效率消耗最⼤,此时策略三起到了⾄关重要的作
⽤。通过标记 key 的⽅式,React 可以直接移动 DOM 节点,降低内耗。
面试题 174. 简述 React key 是⼲嘛⽤的 为什么要加?key 主要是解决哪⼀类问题的?
参考回答:
Keys 是 React ⽤于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要保证某个元素的 key 在其同级元素中具有唯⼀性。
在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动⽽来的元素,从⽽减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。
注意事项:
key值⼀定要和具体的元素—⼀对应;
尽量不要⽤数组的index去作为key;
不要在render的时候⽤随机数或者其他操作给元素加上不稳定的key,这样造成的性能开销⽐不加key的情况下更糟糕。
面试题 175. 简述虚拟 DOM 的引⼊与直接操作原⽣ DOM 相⽐,哪⼀个效率更⾼,为什么 ?
参考回答:
虚拟DOM相对原⽣的DOM不⼀定是效率更⾼,如果只修改⼀个按钮的⽂案,那么虚拟 DOM 的操作⽆论如何都不可能⽐真实的 DOM 操作更快。在⾸次渲染⼤量DOM时,由于多了⼀层虚拟DOM的计算,虚拟DOM也会⽐innerHTML插⼊慢。它能保证性能下限,在真实DOM操作的时候进⾏针对性的优化时,还是更快的。所以要根据具体的场景进⾏探讨。
在整个 DOM 操作的演化过程中,其实主要⽭盾并不在于性能,⽽在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率⽽创造出来的⾼阶产物。虚拟 DOM 并不⼀定会带来更好的性能,React 官⽅也从来没有把虚拟 DOM 作为性能层⾯的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更⾼效的研发模式(也就是函数式的 UI 编程⽅式)的同时,仍然保持⼀个还不错的性能
面试题 176. 简述React 与 Vue 的 diff 算法有何不同 ?
参考回答:
diff 算法是指⽣成更新补丁的⽅式,主要应⽤于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法⼀定存在这样⼀个过程:触发更新 → ⽣成补丁 → 应⽤补丁。
React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调⽤之后。此时触发虚拟 DOM树变更遍历,采⽤了深度优先遍历算法。但传统的遍历⽅式,效率较低。为了优化效率,使⽤了分治的⽅式。将单⼀节点⽐对转化为了 3 种类型节点的⽐对,分别是树、组件及元素,以此提升效率。
树⽐对:由于⽹⻚视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同⼀层次的节点进⾏⽐较。
组件⽐对:如果组件是同⼀类型,则进⾏树⽐对,如果不是,则直接放⼊到补丁中。
元素⽐对:主要发⽣在同层级中,通过标记节点操作⽣成补丁,节点操作对应真实的 DOM 剪裁操作。
以上是经典的 React diff 算法内容。⾃ React 16 起,引⼊了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采⽤了 FiberNode 与 FiberTree 进⾏重构。fiberNode 使⽤了双链表的结构,可以直接找到兄弟节点与⼦节点。整个更新过程由 current 与 workInProgress 两
株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。
Vue 的整体 diff 策略与 React 对⻬,虽然缺乏时间切⽚能⼒,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引⼊过,后期因为收益不⾼移除掉了。除了⾼帧率动画,在 Vue 中其他的场景⼏乎都可以使⽤防抖和节流去提⾼响应性能
面试题 177. React组件命名推荐的⽅式是哪个 ?
参考回答:
通过引⽤⽽不是使⽤来命名组件displayName。
使⽤displayName命名组件
export default React.createClass({ displayName: 'TodoApp', // ...})
React推荐的⽅法:
export default class TodoApp extends React.Component { // ...}
面试题 178. 简述 react 最新版本解决了什么问题,增加了哪些东⻄ ?
参考回答:
React 16.x的三⼤新特性 Time Slicing、Suspense、 hooksTime Slicing(解决CPU速度问题)使得在执⾏任务的期间可以随时暂停,跑去⼲别的事情,这个
特性使得react能在性能极其差的机器跑时,仍然保持有良好的性能
Suspense (解决⽹络IO问题) 和lazy配合,实现异步加载组件。 能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从react出⽣到现在都存在的「异步副作⽤」的问题,⽽且解决得⾮的优雅,使⽤的是 T异步但是同步的写法,这是最好的解决异步问题的⽅式
提供了⼀个内置函数componentDidCatch,当有错误发⽣时,可以友好地展示 fallback 组件;
可以捕捉到它的⼦元素(包括嵌套⼦元素)抛出的异常; 可以复⽤错误组件。(1)React16.8 加⼊hooks,让React函数式组件更加灵活,hooks之前,React存在很多问题:
在组件间复⽤状态逻辑很难
复杂组件变得难以理解,⾼阶组件和函数组件的嵌套过深。
class组件的this指向问题
难以记忆的⽣命周期
hooks很好的解决了上述问题,hooks提供了很多⽅法
useState 返回有状态值,以及更新这个状态值的函数
useEffect 接受包含命令式,可能有副作⽤代码的函数。
useContext 接受上下⽂对象(从 React.createContext返回的值)并返回当前上下⽂值,
useReducer useState 的替代⽅案。接受类型为 (state,action)=> newState的
reducer,并返回与dispatch⽅法配对的当前状态。
useCalLback 返回⼀个回忆的memoized版本,该版本仅在其中⼀个输⼊发⽣更改时才会更改。纯
函数的输⼊输出确定性 o useMemo 纯的⼀个记忆函数 o useRef 返回⼀个可变的ref对象,其
Current 属性被初始化为传递的参数,返回的 ref 对象在组件的整个⽣命周期内保持不变。
useImperativeMethods ⾃定义使⽤ref时公开给⽗组件的实例值
useMutationEffect 更新兄弟组件之前,它在React执⾏其DOM改变的同⼀阶段同步触发
useLayoutEffect DOM改变后同步触发。使⽤它来从DOM读取布局并同步重新渲染(2)React16.9
重命名 Unsafe 的⽣命周期⽅法。新的 UNSAFE_前缀将有助于在代码 review 和 debug 期间,使这些有问题的字样更突出
废弃 javascrip:形式的 URL。以javascript:开头的URL ⾮常容易遭受攻击,造成安全漏洞。
废弃"Factory"组件。 ⼯⼚组件会导致 React 变⼤且变慢。
act()也⽀持异步函数,并且你可以在调⽤它时使⽤ await。
使⽤ 进⾏性能评估。在较⼤的应⽤中追踪性能回归可能会很⽅便(3)React16.13.0
⽀持在渲染期间调⽤setState,但仅适⽤于同⼀组件可检测冲突的样式规则并记录警告
废弃 unstable_createPortal,使⽤CreatePortal将组件堆栈添加到其开发警告中,使开发⼈员能够隔离bug并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误
面试题 179. 简述在React中⻚⾯重新加载时怎样保留数据 ?
参考回答:
这个问题就设计到了数据持久化, 主要的实现⽅式有以下⼏种:
Redux: 将⻚⾯的数据存储在redux中,在重新加载⻚⾯时,获取Redux中的数据;
data.js: 使⽤webpack构建的项⽬,可以建⼀个⽂件,data.js,将数据保存data.js中,跳转⻚⾯后获取;
sessionStorge: 在进⼊选择地址⻚⾯之前,componentWillUnMount的时候,将数据存储到
sessionStorage中,每次进⼊⻚⾯判断sessionStorage中有没有存储的那个值,有,则读取渲染数据;没有,则说明数据是初始化的状态。返回或进⼊除了选择地址以外的⻚⾯,清掉存储的
sessionStorage,保证下次进⼊是初始化的数据
history API: History API 的 pushState 函数可以给历史记录关联⼀个任意的可序列化
state,所以可以在路由 push 的时候将当前⻚⾯的⼀些信息存到 state 中,下次返回到这个
⻚⾯的时候就能从 state ⾥⾯取出离开前的数据重新渲染。react-router 直接可以⽀持。这个⽅法适合⼀些需要临时存储的场景
面试题 180. 简述在React中怎么使⽤async/await ?
参考回答:
async/await是ES7标准中的新特性。如果是使⽤React官⽅的脚⼿架创建的项⽬,就可以直接使⽤。如果是在⾃⼰搭建的webpack配置的项⽬中使⽤,可能会遇到 regeneratorRuntime is notdefined 的异常错误。那么我们就需要引⼊babel,并在babel中配置使⽤async/await。可以利⽤
babel的 transform-async-to-module-method 插件来转换其成为浏览器⽀持的语法,虽然没有性能的提升,但对于代码编写体验要更好
面试题 181. 简述React.Children.map和js的map有什么区别 ?
参考回答:
JavaScript中的map不会对为null或者undefined的数据进⾏处理,⽽React.Children.map中的map可以处理React.Children为null或者undefined的情况
面试题 182. 简述为什么 React 要⽤ JSX ?
参考回答:
JSX 是⼀个 JavaScript 的语法扩展,或者说是⼀个类似于 XML 的 ECMAScript 语法扩展。它本身没有太多的语法定义,也不期望引⼊更多的标准。
其实 React 本身并不强制使⽤ JSX。在没有 JSX 的时候,React 实现⼀个组件依赖于使⽤
React.createElement 函数。代码如下:
class Hello extends React.Component {
render() {
return React.createElement(
'div',
null,
`Hello ${this.props.toWhat}`
);
}
}
ReactDOM.render(
React.createElement(Hello, {toWhat: 'World'}, null),
document.getElementById('root')
);
⽽ JSX 更像是⼀种语法糖,通过类似 XML 的描述⽅式,描写函数对象。在采⽤ JSX 之后,这段代码会这样写:class Hello extends React.Component {
render() {
returnHello {this.props.toWhat}
;
}
}
ReactDOM.render(
,
document.getElementById('root')
);通过对⽐,可以清晰地发现,代码变得更为简洁,⽽且代码结构层次更为清晰。
因为 React 需要将组件转化为虚拟 DOM 树,所以在编写代码时,实际上是在⼿写⼀棵结构树。⽽XML在树结构的描述上天⽣具有可读性强的优势。
但这样可读性强的代码仅仅是给写程序的同学看的,实际上在运⾏的时候,会使⽤ Babel 插件将 JSX语法的代码还原为 React.createElement 的代码。
总结: JSX 是⼀个 JavaScript 的语法扩展,结构类似 XML。JSX 主要⽤于声明 React 元素,但React 中并不强制使⽤ JSX。即使使⽤了 JSX,也会在构建过程中,通过 Babel 插件编译为React.createElement。所以 JSX 更像是 React.createElement 的⼀种语法糖。
React 团队并不想引⼊ JavaScript 本身以外的开发体系。⽽是希望通过合理的关注点分离保持组件开发的纯粹性。
面试题 183. 简述HOC相⽐ mixins 有什么优点?
参考回答:
HOC 和 Vue 中的 mixins 作⽤是⼀致的,并且在早期 React 也是使⽤ mixins 的⽅式。但是在使⽤ class 的⽅式创建组件以后,mixins 的⽅式就不能使⽤了,并且其实 mixins 也是存在⼀些问题的,⽐如:
隐含了⼀些依赖,⽐如我在组件中写了某个 state 并且在 mixin 中使⽤了,就这存在了⼀个依赖关系。万⼀下次别⼈要移除它,就得去 mixin 中查找依赖多个 mixin 中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我⼀直觉得命名真的是⼀件麻烦事。。
雪球效应,虽然我⼀个组件还是使⽤着同⼀个 mixin,但是⼀个 mixin 会被多个组件使⽤,可能会存在需求使得 mixin 修改原本的函数或者新增更多的函数,这样可能就会产⽣⼀个维护成本HOC 解决了这些问题,并且它们达成的效果也是⼀致的,同时也更加的政治正确(毕竟更加函数式
了)
面试题 184. 简述React 中的⾼阶组件运⽤了什么设计模式 ?
参考回答:
使⽤了装饰模式,⾼阶组件的运⽤
function withWindowWidth(BaseComponent) {
class DerivedClass extends React.Component {
state = {
windowWidth: window.innerWidth,
}
onResize = () => {
this.setState({
windowWidth: window.innerWidth,
})
}
componentDidMount() {
window.addEventListener('resize', this.onResize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
}
render() {
return
}
}
return DerivedClass;
}
const MyComponent = (props) => {
returnWindow width is: {props.windowWidth}};
export default withWindowWidth(MyComponent);
装饰模式的特点是不需要改变 被装饰对象 本身,⽽只是在外⾯套⼀个外壳接⼝。JavaScript ⽬前已
经有了原⽣装饰器的提案,其⽤法如下@testable
class MyTestableClass {
}