React JS 的基础知识,还涉及到最先进和最具挑战性的问题
React 基础知识
1. Element 和 Component 之间的区别是什么?
Element 是一个简单的对象,描述了你希望在屏幕上显示的内容,通常以 DOM 节点或其他组件的形式。Element 可以在 props 中包含其他 Element。创建 React 元素是低成本的,一旦创建,它不会被改变。React Element 的对象表示形式如下:
const element = React.createElement(
'div',
{id: 'login-btn'},
'Login'
);
上述 React.createElement() 函数返回一个对象:
{
type: 'div',
props: {
children: 'Login',
id: 'login-btn'
}
}
最终,它通过 ReactDOM.render() 渲染到 DOM 中:
<div id='login-btn'>Login</div>
而 Component 可以以多种方式声明。它可以是带有 render()
方法的类,或者定义为一个函数。无论哪种方式,它都接受 props
作为输入,并返回一个 JSX 树作为输出:
const Button = ({ onLogin }) =>
<div id={'login-btn'} onClick={onLogin}>Login</div>;
然后 JSX 被转译为 React.createElement() 函数树:
const Button = ({ onLogin }) => React.createElement(
'div',
{ id: 'login-btn', onClick: onLogin },
'Login'
);
2. 如何在 React 中创建组件?
创建组件有两种方式:
-
函数组件:这是创建组件最简单的方式。它们是纯 JavaScript 函数,接受
props
对象作为第一个参数并返回 React 元素:function Greeting({ message }) { return <h1>{`Hello, ${message}`}</h1>; }
-
类组件:你也可以使用 ES6 类来定义组件。上面的函数组件可以写成:
class Greeting extends React.Component { render() { return <h1>{`Hello, ${this.props.message}`}</h1>; } }
3. 什么是 Pure Components?
React.PureComponent 与 React.Component 相同,但它会自动处理 shouldComponentUpdate()
方法。当 props
或 state
发生变化时,PureComponent 会对它们进行浅层比较。而普通组件不会自动比较 props
和 state
,因此在调用 shouldComponentUpdate
时,组件会默认重新渲染。
4. React 中的 state 是什么?
state 是一个对象,保存了组件生命周期中可能变化的信息。我们应该尽量保持 state 简单,并减少有 state 的组件数量。让我们创建一个带有 message 状态的用户组件:
class User extends React.Component {
constructor(props) {
super(props);
this.state = {
message: 'Welcome to React world'
};
}
render() {
return (
<div>
<h1>{this.state.message}</h1>
</div>
);
}
}
state 类似于 props
,但它是私有的,并且完全由组件控制。即,它在未通过所有者组件传递的情况下,对其他组件不可见。
5. React 中的 props 是什么?
props 是传递给组件的输入。它们可以是单个值或包含多个值的对象,类似于 HTML 标签的属性。props 的主要用途是:
- 传递自定义数据给组件。
- 触发状态变化。
- 通过
this.props.reactProp
在组件的render()
方法中使用。
例如,创建一个带有 reactProp
属性的元素:
<Element reactProp={'1'} />
该 reactProp
名称将成为附加在 React 本地 props
对象上的属性,该对象存在于所有通过 React 库创建的组件中。
6. state 和 props 之间的区别是什么?
state 和 props 都是普通的 JavaScript 对象。它们都保存会影响渲染输出的信息,但它们在组件中的功能不同。props 类似于函数参数传递给组件,而 state 则是在组件内部管理的,类似于函数中声明的变量。
7. 为什么不应该直接更新 state?
如果你尝试直接更新 state,则组件不会重新渲染。
// 错误
this.state.message = 'Hello world';
相反,应该使用 setState()
方法。它会调度对组件的 state 对象的更新。当 state 改变时,组件会重新渲染。
// 正确
this.setState({ message: 'Hello World' });
8. setState() 的回调函数有什么作用?
当
setState
完成并且组件重新渲染后,回调函数被调用。由于 setState()
是异步的,因此回调函数可用于执行后续操作。
setState({ name: 'John' }, () => console.log('The name has updated and component re-rendered'));
9. HTML 和 React 事件处理有什么区别?
-
在 HTML 中,事件名通常用小写表示:
<button onclick="activateLasers()">
在 React 中则遵循 camelCase 规则:
<button onClick={activateLasers}>
-
在 HTML 中,可以返回
false
来阻止默认行为:<a href="#" onclick="console.log('The link was clicked.'); return false;">
在 React 中需要显式调用
preventDefault()
:function handleClick(event) { event.preventDefault(); console.log('The link was clicked.'); }
10. 如何在 JSX 回调中绑定方法或事件处理程序?
有三种方式可以实现:
- **在构造函数中绑定**:
```javascript
class Foo extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('Click happened');
}
render() {
return <button onClick={this.handleClick}>Click Me</button>;
}
}
```
- **公共类字段语法**:
```javascript
handleClick = () => {
console.log('this is:', this);
}
<button onClick={this.handleClick}>Click me</button>;
```
- **回调中的箭头函数**:
```javascript
handleClick() {
console.log('Click happened');
}
render() {
return <button onClick={() => this.handleClick()}>Click Me</button>;
}
```
11. React 中的 Synthetic Events 是什么?
SyntheticEvent 是对浏览器原生事件的跨浏览器封装。它的 API 与浏览器的原生事件相同,包括 stopPropagation()
和 preventDefault()
,但这些事件在所有浏览器中工作一致。
12. "key" 属性是什么?在元素数组中使用它有什么好处?
key
是一个特殊的字符串属性,应在创建元素数组时包含。key
属性帮助 React 识别哪些项目发生了变化、添加或删除。通常使用数据的 ID 作为 key
:
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
如果没有稳定的 ID,可以在最后使用项目索引作为 key
。
13. React 中的 Lifting State Up 是什么?
当多个组件需要共享相同的动态数据时,建议将共享的状态提升到它们的最近的共同父组件中。这意味着如果两个子组件共享来自父组件的数据,应将状态移到父组件,而不是在两个子组件中维护本地状态。
14. 组件生命周期的不同阶段是什么?
组件的生命周期有三个阶段:
-
Mounting(挂载):组件准备好挂载到浏览器 DOM 中。这一阶段包含
constructor()
、getDerivedStateFromProps()
、render()
和componentDidMount()
生命周期方法。 -
Updating(更新):组件通过接收新
props
或更新状态(通过setState()
或forceUpdate()
)进行更新。这一阶段包含getDerivedStateFromProps()
、shouldComponentUpdate()
、render()
、getSnapshotBeforeUpdate()
和componentDidUpdate()
。 -
Unmounting(卸载):组件不再需要时,从浏览器 DOM 中卸载。这一阶段包括
componentWillUnmount()
。
15. React 中的 portals 是什么?
Portal 是一种推荐的方式,用于将子元素渲染到父组件 DOM 层次结构之外的 DOM 节点中。
使用方法:
ReactDOM.createPortal(child, container);
16. 什么是无状态组件?
如果组件的行为与状态无关,它可以是无状态组件。你可以使用函数或类来创建无状态组件,但如果不需要使用生命周期钩子,建议使用函数组件。函数组件编写、理解和测试更简单,运行速度稍快,并且可以避免使用 this
关键字。
17. 如果在初始状态中使用 props,会发生什么?
如果组件的 props
在没有刷新组件的情况下改变,新的 prop
值不会被显示,因为构造函数不会更新组件的当前状态。初始化状态从 props
获取值只在组件首次创建时运行。
错误示例:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
inputValue: this.props.inputValue
};
}
render() {
return <div>{this.state.inputValue}</div>;
}
}
使用 props
在 render
方法中可以更新值:
class MyComponent extends React.Component {
render() {
return <div>{this.props.inputValue}</div>;
}
}
React Router
18. history 的 push() 和 replace() 方法的作用是什么?
history 实例有两个用于导航的方法。push()
会向历史记录数组添加新位置,而 replace()
则替换数组中的当前位置。
19. 如何通过编程方式使用 React Router 进行导航?
可以通过以下三种方式实现编程式路由/导航:
-
使用
withRouter()
高阶函数:import { withRouter } from 'react-router-dom'; const Button = withRouter(({ history }) => ( <button onClick={() => { history.push('/new-location') }}> Click Me! </button> ));
-
使用组件和
render
props 模式:import { Route } from 'react-router-dom'; const Button = () => ( <Route render={({ history }) => ( <button onClick={() => { history.push('/new-location') }}> Click Me! </button> )} /> );
-
使用 context(不推荐):
const Button = (props, context) => ( <button onClick={() => { context.history.push('/new-location') }}> Click Me! </button> );
20. 如何在 React Router v4 中获取查询参数?
在 React Router v4 中,解析查询字符串的能力被移除,用户可以选择自己喜欢的实现。推荐的方法是使用 query-string
库:
const queryString = require('query-string');
const parsed = queryString.parse(props.location.search);
也可以使用原生的 URLSearchParams
:
const params = new URLSearchParams(props.location.search);
const foo = params.get('name');
React Redux
21. Redux 选择器(Selectors)是什么?为什么使用它们?
选择器是接受 Redux 状态作为参数并返回数据以传递给组件的函数。例如,要从状态中获取用户数据:
const getUserData = state => state.user.data;
选择器有两个主要优点:
- 选择器可以计算派生数据,使 Redux 只存储最小的状态。
- 选择器不会重新计算,除非其参数之一发生变化。
22. mapDispatchToProps()
有哪些不同的写法?
在 mapDispatchToProps()
中将动作创建者绑定到 dispatch()
有几种方式:
// 直接使用 dispatch
const mapDispatchToProps = (dispatch) => ({
action: () => dispatch(action())
});
// 使用 bindActionCreators
const mapDispatchToProps = (dispatch) => ({
action: bindActionCreators(action, dispatch)
});
// 简写方式
const mapDispatchToProps = { action };
23. React Redux 中的组件(component)和容器(container)有什么区别?
组件是描述应用程序展示部分的类或函数组件。容器是一个与 Redux store 连接的组件的非正式术语。容器订阅 Redux 状态更新并分发动作,通常不渲染 DOM 元素;它们将渲染委托给展示性子组件。
24. redux-saga 的思维模型是什么?
Saga 就像应用程序中的一个独立线程,专门负责副作用。redux-saga 是一个 Redux 中间件,这意味着这个线程可以通过正常的 Redux 动作从主应用程序中启动、暂停和取消,它可以访问完整的 Redux 应用程序状态,并且可以分发 Redux 动作。
25. redux-saga 中 call()
和 put()
的区别是什么?
call()
和 put()
都是 effect 创建函数。call()
函数用于创建 effect 描述,指示中间件调用 Promise。put()
函数创建一个 effect,指示中间件将一个动作分发到 store。例如,以下是如何使用这些 effect 获取特定用户数据的示例:
function* fetchUserSaga(action) {
// `call` 函数接受剩余参数,这些参数将传递给 `api.fetchUser` 函数。
// 指示中间件调用 Promise,解析后的值将赋给 `userData` 变量
const userData = yield call(api.fetchUser, action.userId);
// 指示中间件分发相应的动作。
yield put({
type: 'FETCH_USER_SUCCESS',
userData
});
}
26. Redux Thunk 是什么?
Redux Thunk 中间件允许你编写返回函数而不是动作的动作创建者。这个 thunk 可以用于延迟分发动作或仅在满足特定条件时分发。内部函数接收 dispatch()
和 getState()
方法作为参数。
27. Redux 选择器(Selectors)是什么?为什么使用它们?
这与第21条相同。选择器是接受 Redux 状态作为参数并返回数据以传递给组件的函数。选择器可以计算派生数据,使 Redux 只存储最小的状态,并且不会重新计算,除非其参数之一发生变化。
-
什么是 diffing 算法?
React 使用算法来找出如何高效地更新 UI 以匹配最新的树。diffing 算法生成将一个树转换为另一个树的最小操作数。然而,这些算法的复杂度是 O(n^3),其中 n 是树中元素的数量。例如,显示 1000 个元素将需要大约十亿次比较,这成本过高。因此,React 实现了一个基于两个假设的启发式 O(n) 算法:
- 不同类型的元素会产生不同的树。
- 开发者可以通过
key
属性提示哪些子元素在不同渲染中可能是稳定的。
29. render props 的 prop 必须命名为 render
吗?
尽管模式被称为 render props,你不必使用名为 render
的 prop。即,任何组件用来知道要渲染什么的函数 prop 都可以被视为“render prop”。例如,使用 children
prop 作为 render props:
<Mouse>
{mouse => (
<p>The mouse position is {mouse.x}, {mouse.y}</p>
)}
</Mouse>
实际上,children
prop 不需要在 JSX 元素中列出“属性”名称。你可以直接在元素中使用它,并明确声明 children
应该是一个函数:
Mouse.propTypes = {
children: PropTypes.func.isRequired
};
30. 使用纯组件(pure components)时,render props 有什么问题?
如果在 render 方法中创建函数,它会使纯组件的目的失效。因为浅比较新 props 会总是返回 false,每次渲染都会生成一个新的 render prop 值。你可以通过将 render 函数定义为实例方法来解决这个问题。
31. 如何使用 render props 创建高阶组件 (HOC)?
你可以使用带有 render prop 的常规组件来实现大多数高阶组件 (HOC)。例如,如果你更喜欢使用 with Mouse HOC 而不是组件,你可以很容易地通过常规组件和 render prop 来创建它。
function withMouse(Component) {
return class extends React.Component {
render() {
return (
<Mouse render={mouse => (
<Component {...this.props} mouse={mouse} />
)}/>
);
}
}
}
这种方式给 render props 提供了使用任意模式的灵活性。
32. 什么是窗口化技术(windowing)?
窗口化技术是一种在任何给定时间只渲染少量行的技术,可以显著减少重新渲染组件所需的时间以及创建的 DOM 节点数量。如果你的应用程序渲染了很长的数据列表,则推荐使用这种技术。react-window
和 react-virtualized
是流行的窗口化库,它们提供了多个可重用的组件,用于显示列表、网格和表格数据。
33. 门户(portals)的典型用例是什么?
React 门户在父组件有 overflow: hidden
或影响堆叠上下文(例如 z-index、position、opacity 等样式)的属性时非常有用,并且你需要在视觉上“突破”其容器。例如,弹窗、全局消息通知、悬停卡片和工具提示。
34. 如何为非受控组件设置默认值?
在 React 中,表单元素上的 value
属性将覆盖 DOM 中的值。对于非受控组件,你可能希望 React 指定初始值,但保留随后的更新为非受控状态。为此,你可以指定 defaultValue
属性而不是 value
。
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
用户名:
<input
defaultValue="John"
type="text"
ref={this.input} />
</label>
<input type="submit" value="提交" />
</form>
);
}
对于 select
和 textarea
输入,适用相同的原则。但对于复选框和单选框输入,你需要使用 defaultChecked
。