React面试题
React 的事件和普通的 HTML事件有什么不同?
区别:
- 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰
- 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
- react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用 preventDefault()来阻止默认行为。
- react 事件统一挂载到 root 节点(React 17之前是 document)
合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:
- 兼容所有浏览器,更好的跨平台
- 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。方便 react 统一管理和事务机制。
- 事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行
React 组件中怎么做事件代理?它的原理是什么?
React 基于 Virtual DOM 实现了一个 SyntheticEvent 层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合W3C 标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。
在 React 底层,主要对合成事件做了两件事:
- 事件委派:React 会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
- 自动绑定:React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this 为当前组件
React 高阶组件、Render props、hooks 有什么区别,为什么要不断迭代?
这三者是目前 react 解决代码复用的主要方式:
- 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。
- render props 是指一种在 React 组件之间使用一个值为函数的prop 共享代码的简单技术,更具体的说,render prop 是一个用于告知组件需要渲染什么内容的函数 prop。
- 通常,render props 和高阶组件只渲染一个子节点。让 Hook 来服务这个使用场景更加简单。这两种模式仍有用武之地,(例如,一个虚拟滚动条组件或许会有一个 renderltem 属性,或是一个可见的容器组件或许会有它自己的 DOM 结构)。但在大部分场景下,Hook 足够了,并且能够帮助减少嵌套。
- HOC
官方解释∶高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。简言之,HOC 是一种组件的设计模式,HOC 接受一个组件和额外的参数(如果需要),返回一个新的组件。HOC 是纯函数,没有副作用。
// hoc的定义
function withSubscription(WrappedComponent,selectData){
return class extends React.component{
constructor(props){
super(props);
this.state = {
data:selectData(DataSource,props)
}
}
//一些通用的逻辑处理
render(){
//...并使用新数据渲染被包装的组件
return <WrappedComponent data={this.state.data} {...this.props}/>
}
}
}
//使用
const BlogPostWithSubscription = withSubscription(BlogPost,(DataSource,props)=>DataSource.getBlogPost(props.id));
HOC 的优缺点∶
- 优点∶ 逻辑服用、不影响被包裹组件的内部逻辑。
- 缺点∶ hoc 传递给被包裹组件的 props 容易和被包裹后的组件重名,进而被覆盖
- Render props 官方解释∶"render prop"是指一种在 React 组件之间使用一个值为函数的prop 共享代码的简单技术
具有 render prop 的组件接受一个返回 React 元素的函数,将 render的渲染逻辑注入到组件内部。在这里,"render"的命名可以是任何其他有效的标识符。
//DataProvider组件内部的渲染逻辑如下
class DataProvider extends React.Components{
state = {
name:'Tom'
}
render(){
return (<div><p>共享数据组件自己内部的渲染逻辑</p>{this.props.render(this.state)}</div>)
}
}
//调用方式
<DataProvider render={data=>(<h1>Hello {data.name}</h1>)}/>
由此可以看到,render props 的优缺点也很明显∶
- 优点:数据共享、代码复用,将组件内的 state 作为 props 传递给调用者,将渲染逻辑交给调用者。
- 缺点:无法在 return 语句外访问数据、嵌套写法不够优雅
- Hooks 官方解释∶Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。通过自定义 hook,可以复用代码逻辑。
// 自定义一个获取订阅数据的hook
function useSubscription() {
const data = DataSource.getComments();
return [data];
}
//
function CommentList(props) {
const {data} = props;
const [subData] = useSubscription();
...
}
// 使用
<CommentList data='hello' />
以上可以看出,hook 解决了 hoc 的 prop 覆盖的问题,同时使用的方式解决了 render props 的嵌套地狱的问题。hook 的优点如下∶
- 使用直观;
- 解决 hoc 的 prop 重名问题;
- 解决 render props 因共享数据 而出现嵌套地狱的问题;
- 能在 return 之外使用数据的问题。
需要注意的是:hook 只能在组件顶层使用,不可在分支语句中使用。
总结∶ Hoc、render props 和 hook 都是为了解决代码复用的问题,但是 hoc和 render props 都有特定的使用场景和明显的缺点。hook 是 react16.8 更新的新的 API,让组件逻辑复用更简洁明了,同时也解决了 hoc 和 render props 的一些缺点。
Component, Element, Instance 之间有什么区别和联系?
元素:一个元素 element 是一个普通对象(plain object),描述了对于一个 DOM 节点或者其他组件 component,你想让它在屏幕上呈现成 什么样子。元素 element 可以在它的属性 props 中包含其他元素(译注:用于形成元素树)。创建一个 React 元素 element 成本很低。元素 element 创建之后是不可变的。
组件:一个组件 component 可以通过多种方式声明。可以是带有一个render()方法的类,简单点也可以定义为一个函数。这两种情况下,它都把属性 props 作为输入,把返回的一棵元素树作为输出。
实例:一个实例 instance 是你在所写的组件类 component class 中使用关键字 this 所指向的东西(译注:组件实例)。它用来存储本地状态和响应生命周期事件很有用。
函数式组件(Functional component)根本没有实例 instance。类组件(Class component)有实例 instance,但是永远也不需要直接创建一个组件的实例,因为 React 帮我们做了这些。
React.createClass 和 extends Component 的区别有哪些?
React.createClass 和 extends Component 的区别有哪些?
- 语法区别
createClass 本质上是一个工厂函数,extends 的方式更加接近最新的 ES6 规范的 class 写法。两种方式在语法上的差别主要体现在方法的定义和静态属性的声明上。createClass 方式的方法定义使用逗号,隔开,因为 creatClass 本质上是一个函数,传递给它的是一个 Object;而 class 的方式定义方法时务必谨记不要使用逗号隔开,这是 ES6 class的语法规范。
- propType 和 getDefaultProps
React.createClass:通过 proTypes 对象和 getDefaultProps()方法来设置和获取 props.React.Component:通过设置两个属性 propTypes 和 defaultProps
- 状态的区别
React.createClass:通过 getInitialState()方法返回一个包含初始值的对象。React.Component:通过 constructor 设置初始状态
- this 区别
React.createClass:会正确绑定 this,React.Component:由于使用了 ES6,这里会有些微不同,属性并不会自动绑定到 React 类的实例上。
- Mixins
React.createClass:使用 React.createClass 的话,可以在创建组件时添加一个叫做 mixins 的属性,并将可供混合的类的集合以数组的形式赋给 mixins。如果使用 ES6 的方式来创建组件,那么 React mixins 的特性将不能被使用了
React 如何判断什么时候重新渲染组件?
组件状态的改变可以因为 props 的改变,或者直接通过 setState 方法改变。组件获得新的状态,然后 React 决定是否应该重新渲染组件。只要组件的 state 发生变化,React 就会对组件进行重新渲染。这是因为 React 中的 shouldComponentUpdate 方法默认返回 true,这就是导致每次更新都重新渲染的原因。
当React将要渲染组件时会执行shouldComponentUpdate方法来看它是否返回 true(组件应该更新,也就是重新渲染)。所以需要重写 shouldComponentUpdate 方法让它根据情况返回 true 或者 false 来告诉 React 什么时候重新渲染什么时候跳过重新渲染。
React 中可以在 render 访问 refs 吗?为什么
不可以,render 阶段 DOM 还没有生成,无法获取 DOM。DOM 的获取需要在 pre-commit 阶段和 commit 阶段:
React setState 调用之后发生了什么?是同步还是异步?
- React 中 setState 后发生了什么
在代码中调用 setState 函数之后,React 会将传入的参数对象与组件当前的状态合并,然后触发调和过程(Reconciliation)。经过调和过程,React 会以相对高效的方式根据新的状态构建 React 元素树并且着手重新渲染整个 UI 界面。
在 React 得到元素树之后,React 会自动计算出新的树与老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中,React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。
如果在短时间内频繁 setState。React 会将 state 的改变压入栈中,在合适的时机,批量更新 state 和视图,达到提高性能的效果。
- setState 是同步还是异步的
假如所有 setState 是同步的,意味着每执行一次 setState 时(有可能一个同步代码中,多次 setState),都重新 vnode diff + dom 修改,这对性能来说是极为不好的。如果是异步,则可以把一个同步代码中的多个 setState 合并成一次组件更新。所以默认是异步的,但是在一些情况下是同步的。
setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同。在源码中,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。
异步:在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
同步:在 React 无法控制的地方,比如原生事件,具体就是在addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。
一般认为,做异步设计是为了性能优化、减少渲染次数:
setState设计为异步,可以显著的提升性能。如果每次调用 setState都进行一次更新,那么意味着 render 函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新;
如果同步更新了 state,但是还没有执行 render 函数,那么 state和 props 不能保持同步。state 和 props 不能保持一致性,会在开发中产生很多的问题;
React 组件的 state 和 props 有什么区别
- props
props 是一个从外部传进组件的参数,主要作为就是从父组件向子组件传递数据,它具有可读性和不变性,只能通过外部组件主动传入新的 props 来重新渲染子组件,否则子组件的 props 以及展现形式不会改变。
- state
state 的主要作用是用于组件保存、控制以及修改自己的状态,它只能在 constructor 中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的 this.setState 来修改,修改 state属性会导致组件的重新渲染。
- 区别
state 的主要作用是用于组件保存、控制以及修改自己的状态,它只能在 constructor 中初始化,它算是组件的私有属性,不可通过外部访问和修改,只能通过组件内部的 this.setState 来修改,修改 state属性会导致组件的重新渲染。
React 中的 props为什么是只读的?
this.props 是组件之间沟通的一个接口,原则上来讲,它只能从父组件流向子组件。React 具有浓重的函数式编程的思想。
提到函数式编程就要提一个概念:纯函数。它有几个特点:
- 给定相同的输入,总是返回相同的输出。
- 过程没有副作用。
- 不依赖外部状态。
this.props 就是汲取了纯函数的思想。props 的不可以变性就保证的相同的输入,页面显示的内容是一样的,并且不会产生副作用。
React 中怎么检验 props?验证 props 的目的是什么
React 为我们提供了 PropTypes 以供验证使用。当我们向 Props 传入的数据无效(向 Props 传入的数据类型和验证的数据类型不符)就会在控制台发出警告信息。它可以避免随着应用越来越复杂从而出现的问题。并且,它还可以让程序变得更易读。
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
Greeting.propTypes = {
name: PropTypes.string
};
当然,如果项目汇中使用了 TypeScript,那么就可以不用 PropTypes来校验,而使用 TypeScript 定义接口来校验 props。
React 废弃了哪些生命周期?为什么?
被废弃的三个函数都是在 render 之前,因为 fber 的出现,很可能因为高优先级任务的出现而打断现有任务导致它们会被执行多次。另外的一个原因则是,React 想约束使用者,好的框架能够让人不得已写出容易维护和扩展的代码,这一点又是从何谈起,可以从新增加以及即将废弃的生命周期分析入手。
- componentWillMount
首先这个函数的功能完全可以使用 componentDidMount 和constructor 来代替,异步获取的数据的情况上面已经说明了,而如果抛去异步获取数据,其余的即是初始化而已,这些功能都可以在constructor 中执行,除此之外,如果在 willMount 中订阅事件,但在服务端这并不会执行 willUnMount 事件,也就是说服务端会导致内存泄漏所以 componentWilIMount 完全可以不使用,但使用者有时候难免因为各 种各样的情况在 componentWilMount中做一些操作,那么 React 为了约束开发者,干脆就抛掉了这个 API
- componentWillReceiveProps
在老版本的 React 中,如果组件自身的某个 state 跟其 props 密切相关的话,一直都没有一种很优雅的处理方式去更新 state,而是需要在 componentWilReceiveProps 中判断前后两个 props 是否相同,如果不同再将新的 props 更新到相应的 state 上去。这样做一来会破坏 state 数据的单一数据源,导致组件状态变得不可预测,另一方面也会增加组件的重绘次数。类似的业务需求也有很多,如一个可以横向滑动的列表,当前高亮的 Tab 显然隶属于列表自身的时,根据传入的某个值,直接定位到某个 Tab。为了解决这些问题,React引入了第一个新的生命周期:getDerivedStateFromProps。它有以下的优点∶
- getDSFP 是静态方法,在这里不能使用 this,也就是一个纯函数,开发者不能写出副作用的代码
- 开发者只能通过 prevState 而不是 prevProps 来做对比,保证了state 和 props 之间的简单关系以及不需要处理第一次渲染时prevProps 为空的情况
- 基于第一点,将状态变化(setState)和昂贵操作(tabChange)区分开,更加便于render 和 commit 阶段操作或者说优化
- componentWillUpdate
与 componentWillReceiveProps 类似,许多开发者也会在componentWillUpdate 中根据 props 的变化去触发一些回调 。 但不论是 componentWilReceiveProps 还 是 componentWilUpdate,都有可能在一次更新中被调用多次,也就是说写在这里的回调函数也有可能会被调用多次,这显然是不可取的。与 componentDidMount 类似, componentDidUpdate 也不存在这样的问题,一次更新中componentDidUpdate 只会被调用一次,所以将原先写在componentWillUpdate 中 的 回 调 迁 移 至 componentDidUpdate就可以解决这个问题。
另外一种情况则是需要获取 DOM 元素状态,但是由于在 fber 中,render 可打断,可能在 wilMount 中获取到的元素状态很可能与实际需要的不同,这个通常可以使用第二个新增的生命函数的解决getSnapshotBeforeUpdate(prevProps, prevState)
- getSnapshotBeforeUpdate(prevProps, prevState)
返回的值作为 componentDidUpdate 的第三个参数。与 willMount 不同的是,getSnapshotBeforeUpdate 会在最终确定的 render 执行之前执行,也就是能保证其获取到的元素状态与 didUpdate 中获取到的元素状态相同。官方参考代码:
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果我们 snapshot 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}
React 16.X 中 props 改变后在哪个生命周期中处理
在 getDerivedStateFromProps 中进行处理。
这个生命周期函数是为了替代 componentWillReceiveProps 存在的,所以在需要使用 componentWillReceiveProps 时,就可以考虑使用getDerivedStateFromProps 来进行替代。
两者的参数是不相同的,而 getDerivedStateFromProps 是一个静态函数,也就是这个函数不能通过 this 访问到 class 的属性,也并不推荐直接访问属性。而是应该通过参数提供的 nextProps 以及prevState 来进行判断,根据新传入的 props 来映射到 state。
需要注意的是,如果 props 传入的内容不需要影响到你的 state,那么就需要返回一个 null,这个返回值是必须的,所以尽量将其写到函数的末尾:
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
// 当传入的type发生变化的时候,更新state
if (type !== prevState.type) {
return {
type,
};
}
// 否则,对于state不进行任何操作
return null;
}
React 16 中新生命周期有哪
关于 React16 开始应用的新生命周期 可以看出,React16 自上而下地对生命周期做了另一种维度的解读:Render 阶段:用于计算一些必要的状态信息。这个阶段可能会被React 暂停,这一点和 React16 引入的 Fiber 架构(我们后面会重点讲解)是有关的。
Pre-commit 阶段:所谓“commit”,这里指的是“更新真正的 DOM 节点”这个动作。所谓 Pre-commit,就是说我在这个阶段其实还并没有去更新真实的 DOM,不过 DOM 信息已经是可以读取的了;
Commit 阶段:在这一步,React 会完成真实 DOM 的更新工作。Commit阶段,我们可以拿到真实 DOM(包括 refs)。
与此同时,新的生命周期在流程方面,仍然遵循“挂载”、“更新”、“卸载”这三个广义的划分方式。它们分别对应到:
- 挂载过程:
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
- 更新过程:
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
- 卸载过程:
- componentWillUnmount
React-Router 的实现原理是什么?
客户端路由实现的思想:
- 基于 hash 的路由:通过监听 hashchange 事件,感知 hash 的变化改变 hash 可以直接通过 location.hash=xxx
- 基于 H5 history 路由:改变 url 可以通过 history.pushState 和 resplaceState 等,会将 URL 压入堆栈,同时能够应用 history.go() 等 API监听 url 的变化可以通过自定义事件触发实现
react-router 实现的思想:
基于 history 库来实现上述不同的客户端路由实现思想,并且能够保存历史记录等,磨平浏览器差异,上层无感知通过维护的列表,在每次 URL 发生变化的回收,通过配置的 路由路径,匹配到对应的 Component,并且 render
react-router 里的 Link 标签和 a 标签的区别
从最终渲染的 DOM 来看,这两者都是链接,都是 标签,区别是∶<Link>
是 react-router 里实现路由跳转的链接,一般配合<Route>
使用,react-router 接管了其默认的链接跳转行为,区别于传统的页面跳转,<Link>
的“跳转”行为只会触发相匹配的<Route>
对应的页面内容更新,而不会刷新整个页面。
<Link>
做了 3 件事情:
- 有 onclick 那就执行 onclick
- click 的时候阻止 a 标签默认事件
- 根据跳转 href(即是 to),用 history (web 前端路由两种方式之一,history & hash)跳转,此时只是链接变了,并没有刷新页面而
<a>
标签就是普通的超链接了,用于从当前页面跳转到 href 指向的另一个页面(非锚点情况)。
标签就是普通的超链接了,用于从当前页面跳转到 href 指向的另一个页面(非锚点情况)。
let domArr = document.getElementsByTagName('a')
[...domArr].forEach(item=>{
item.addEventListener('click',function(){
location.href = this.href
})
})
对 Redux 的理解,主要解决什么问题
React 是视图层框架。Redux 是一个用来管理数据状态和 UI 状态的JavaScript 应用工具。随着 JavaScript 单页应用(SPA)开发日趋复杂, JavaScript 需要管理比任何时候都要多的 state(状态),Redux 就是降低管理难度的。(Redux 支持 React、Angular、jQuery甚至纯 JavaScript)。
在 React 中,UI 以组件的形式来搭建,组件之间可以嵌套组合。但React 中组件间通信的数据流是单向的,顶层组件可以通过 props属性向下层组件传递数据,而下层组件不能向上层组件传递数据,兄弟组件之间同样不能。这样简单的单向数据流支撑起了 React 中的数据可控性。
当项目越来越大的时候,管理数据的事件或回调函数将越来越多,也将越来越不好管理。管理不断变化的 state 非常困难。如果一个model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等。state 的管理在大项目中相当复杂。
Redux 提供了一个叫 store 的统一仓储库,组件通过 dispatch 将state 直接传入 store,不用通过其他的组件。并且组件通过subscribe 从 store 获取到 state 的改变。使用了 Redux,所有的组件都可以从 store 中获取到所需的 state,他们也能从 store 获取到 state 的改变。这比组件之间互相传递数据清晰明朗的多。
主要解决的问题:
单纯的 Redux 只是一个状态机,是没有 UI 呈现的,react- redux 作用是将 Redux的状态机和 React的UI呈现绑定在一起,当你dispatch action 改变 state 的时候,会自动更新页面。
Redux 状态管理器和变量挂载到 window 中有什么区别
两者都是存储数据以供后期使用。但是 Redux 状态更改可回溯——Time travel,数据多了的时候可以很清晰的知道改动在哪里发生,完整的提供了一套状态管理模式。
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。
直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。如果这还不够糟糕,考虑一些来自前端开发领域的新需求,如更新调优、服务端渲染、路由跳转前请求数据等等。前端开发者正在经受前所未有的复杂性,难道就这么放弃了吗?当然不是。这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。 可以称它们为曼妥思和可乐。如果把二者分开,能做的很好,但混到一起,就变得一团糟。一些库如 React 视图在视图层禁止异步和直接操作 DOM 来解决这个问题。美中不足的是,React 依旧把处理 state 中数据的问题留给了你。Redux 就是为了帮你解决这个问题。
Redux 和 Vuex 有什么区别,它们的共同思想
- Redux 和 Vue区别
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 参数直接对数据进行转变,使得框架更加简易;
- 共同思想
- 单—的数据源
- 变化可以预测
- 本质上∶ redux 与 vuex 都是对 mvvm 思想的服务,将数据从视图中抽离的一种方案。
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
React Hooks 解决了哪些问题?
React Hooks 主要解决了以下问题:
- 在组件之间复用状态逻辑很难
React 没有提供将可复用性行为“附加”到组件的途径(例如,把组件连接到 store)解决此类问题可以使用 render props 和 高阶组件。但是这类方案需要重新组织组件结构,这可能会很麻烦,并且会使代码难以理解。由 providers,consumers,高阶组件,render props等其他抽象层组成的组件会形成“嵌套地狱”。尽管可以在 DevTools过滤掉它们,但这说明了一个更深层次的问题:React 需要为共享状态逻辑提供更好的原生途径。
可以使用 Hook 从组件中提取状态逻辑,使得这些逻辑可以单独测试并复用。Hook 使我们在无需修改组件结构的情况下复用状态逻辑。这使得在组件间或社区内共享 Hook 变得更便捷
- 复杂组件变得难以理解
在组件中,每个生命周期常常包含一些不相关的逻辑。例如,组件常常在 componentDidMount 和 componentDidUpdate 中获取数据。但是,同一个 componentDidMount 中可能也包含很多其它的逻辑,如设置事件监听,而之后需在 componentWillUnmount 中清除。相互关联且需要对照修改的代码被进行了拆分,而完全不相关的代码却在同一个方法中组合在一起。如此很容易产生 bug,并且导致逻辑不一致。
在多数情况下,不可能将组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。同时,这也是很多人将 React 与状态管理库结合使用的原因之一。但是,这往往会引入了很多抽象概念,需要你在不同的文件之间来回切换,使得复用变得更加困难。为了解决这个问题,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
- 难以理解的 class
除了代码复用和代码管理会遇到困难外,class 是学习 React 的一大屏障。我们必须去理解 JavaScript 中 this 的工作方式,这与其他语言存在巨大差异。还不能忘记绑定事件处理器。没有稳定的语法提案,这些代码非常冗余。大家可以很好地理解 props,state 和自顶向下的数据流,但对 class 却一筹莫展。即便在有经验的 React开发者之间,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。
为了解决这些问题,Hook 使你在非 class 的情况下可以使用更多的React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。Hook 提供了问题的解决方案,无需学习复杂的函数式或响应式编程技术
React Hook 的使用限制有哪些?
React Hooks 的限制主要有两条:
- 不要在循环、条件或嵌套函数中调用 Hook;
- 在 React 的函数组件中调用 Hook。
那为什么会有这样的限制呢?Hooks 的设计初衷是为了改进 React组件的开发模式。在旧有的开发模式下遇到了三个问题。
- 组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、render props 及状态管理框架。
- 复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
- 人和机器都很容易混淆类。常见的有 this 的问题,但在 React 团队中还有类难以优化的问题,希望在编译优化层面做出一些改进。这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了Hooks 只支持函数组件。
那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表。这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。
React diff 算法的原理是什么?
实际上,diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新。
具体的流程如下:
- 真实的 DOM 首先会映射为虚拟 DOM;
- 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch是一个结构化的数据,内容包含了增加、更新、移除等;
- 根据 patch 去更新真实的 DOM,反馈到用户的界面上
一个简单的例子:
import React from 'react'
export default class ExampleComponent extends React.Component {
render() {
if(this.props.isVisible) {
return <div className="visible">visbile</div>;
}
return <div className="hidden">hidden</div>;
}
}
这里,首先假定 ExampleComponent 可见,然后再改变它的状态,让它不可见 。映射为真实的 DOM 操作是这样的,React 会创建一个div 节点
<div class="visible">visbile</div>
当把 visbile 的值变为 false 时,就会替换 class 属性为 hidden,并重写内部的 innerText 为 hidden。这样一个生成补丁、更新差异的过程统称为 diff 算法。diff 算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化:
- 策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。
- 策略二:如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)在组件比对的过程中:如果组件是同一类型则进行树比对;如果不是则直接放入补丁中。只要父组件类型不同,就会被重新渲染。这也就是为什么shouldComponentUpdate、PureComponent 及 React.memo 可以提高性能的原因。
- 策略三:同一层级的子节点,可以通过标记 key 的方式进行列表对比。(基于节点进行对比)元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key 的方式,React 可以直接移动 DOM 节点,降低内耗。
React key 是干嘛用的 为什么要加?key 主要是解决哪一类问题的
Keys 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要保证某个元素的 key 在其同 级元素中具有唯一性。
在 React Diff 算法中 React 会借助元素的 Key 值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。
注意事项:
- key 值一定要和具体的元素—一对应;
- 尽量不要用数组的 index 去作为 key;
- 不要在 render 的时候用随机数或者其他操作给元素加上不稳定的key,这样造成的性能开销比不加 key 的情况下更糟糕
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 中其他的场景几乎都可以使用防抖和节流去提高响应性能。
react 最新版本解决了什么问题,增加了哪些东西
React 16.x 的三大新特性 Time Slicing、Suspense、 hooks
Time Slicing(解决 CPU 速度问题)使得在执行任务的期间可以随时暂停,跑去干别的事情,这个特性使得 react 能在性能极其差的机器跑时,仍然保持有良好的性能
Suspense(解决网络 IO 问题)和 lazy 配合,实现异步加载组件。能暂停当前组件的渲染, 当完成某件事以后再继续渲染,解决从 react出生到现在都存在的「异步副作用」的问题,而且解决得非的优雅,使用的是 T 异步但是同步的写法,这是最好的解决异步问题的方式
提供了一个内置函数 componentDidCatch,当有错误发生时,可以友好地展示 fallback 组件; 可以捕捉到它的子元素(包括嵌套子元素)抛出的异常; 可以复用错误组件。
- 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 读取布局并同步重新渲染
- React16.9
- 重命名 Unsafe 的生命周期方法。新的 UNSAFE_前缀将有助于在代码review 和 debug 期间,使这些有问题的字样更突出废弃
- javascrip:形式的 URL。以 javascript:开头的 URL 非常容易遭受攻击,造成安全漏洞。
- 废弃"Factory"组件。 工厂组件会导致 React 变大且变慢。
- act()也支持异步函数,并且你可以在调用它时使用 await。
- 使用
<React.ProfiLer>
进行性能评估。在较大的应用中追踪性能回归可能会很方便
- React16.13.0
- 支持在渲染期间调用 setState,但仅适用于同一组件
- 可检测冲突的样式规则并记录警告
- 废弃 unstable_createPortal,使用 CreatePortal
- 将组件堆栈添加到其开发警告中,使开发人员能够隔离 bug 并调试其程序,这可以清楚地说明问题所在,并更快地定位和修复错误。
在 React 中页面重新加载时怎样保留数据?
在 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 直接可以支持。这个方法适合一些需要临时存储的场景。
为什么使用 jsx 的组件中没有看到使用 react 却需要引入react?
本质上来说 JSX 是 React.createElement(component,props, ...children)方法的语法糖。在 React 17 之前,如果使用了JSX,其实就是在使用 React, babel 会把组件转换为 CreateElement 形式。在 React 17 之后,就不再需要引入,因为 babel 已经可以帮我们自动引入 react。
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
组件通信的方式有哪些
- ⽗组件向⼦组件通讯: ⽗组件可以向⼦组件通过传 props 的⽅式,向⼦组件进⾏通讯
- ⼦组件向⽗组件通讯: props+回调的⽅式,⽗组件向⼦组件传递props 进⾏通讯,此 props 为作⽤域为⽗组件⾃身的函 数,⼦组件调⽤该函数,将⼦组件想要传递的信息,作为参数,传递到⽗组件的作⽤域中
- 兄弟组件通信: 找到这两个兄弟节点共同的⽗节点,结合上⾯两种⽅式由⽗节点转发信息进⾏通信
- 跨层级通信: Context 设计⽬的是为了共享那些对于⼀个组件树⽽⾔是“全局”的数据,例如当前认证的⽤户、主题或⾸选语⾔,对于跨越多层的全局数据通过 Context 通信再适合不过
- 发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引⼊event 模块进⾏通信
- 全局状态管理⼯具: 借助 Redux 或者 Mobx 等全局状态管理⼯具进⾏通信,这种⼯具会维护⼀个全局状态中⼼Store,并根据不同的事件产⽣新的状态