React 快速入门
React 是一个用于构建用户界面的 JavaScript 库。数据改变时 React 能有效地更新并正确地渲染组件。
我们写 React 就是创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI 界面。
第一个组件:
import React from 'react';
class HelloReact extends React.Component {
render() {
return (
<div>
Hello {this.props.name}
</div>
);
}
}
export default HelloReact;
组件拥有动态渲染 name 的功能,name 字段是使用该组件的地方传入的。
<HelloReact name={"第一个组件"}/>
因此我们就可以正确渲染出一个DOM结构
就是通过这种不断去组合各类有状态的组件,构建一个复杂的工业项目。
JSX
接触 React 想必 JSX 应该会是你接触的第一个新概念,因为你可能是第一次在 js 文件中直接编写“HTML”。
return (
<div>
Hello {this.props.name}
</div>
);
JSX 是一个 JavaScript 的语法扩展,它可以生成 React “元素”。
它的写法完全等价于:
import React from 'react';
class HelloJSX extends React.Component {
render() {
return React.createElement("div", null, "Hello ", this.props.name);
}
}
export default HelloJSX;
其实 React 最终需要的就是 React.createElement("div", null, "Hello ", this.props.name)
通过它,React 可以创建一系列数据结构来表示 React 中的元素,最终再把它转换成真实的 DOM 插入到页面中。
因此 JSX 的结构与真实的 DOM 结构只能说是相似,它们之间还需要 React 去做一系列转换。
JSX的本质是什么?
JSX 本身在 React 项目中只是语法糖,不能直接被浏览器识别的。需要经过编译,那么这个编译后的就是它的本质了。
JSX 在 React 项目中被编译成了 React 元素,就是一个树状的数据结构。也就是我们常说的虚拟 DOM。
组件传值
既然 Reac t是通过组件的组合来实现复杂工程的构建。那么组件之间的第一个问题就是它们之间如何通信。
props
通过 props 向子组件传递数据,并且所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
import React from 'react';
class Child extends React.Component {
render() {
const { list } = this.props
return <ul>{list.map((item, index) => {
return <li key={item.id}>
<span>{item.title}</span>
</li>
})}</ul>
}
}
class Parent extends React.Component{
constructor(props) {
super(props)
this.state = {
list: [
{
id: 'id-1',
title: '标题1'
},
{
id: 'id-2',
title: '标题2'
},
{
id: 'id-3',
title: '标题3'
}
]
}
}
render() {
return <Child list={this.state.list} />
}
}
export default Parent;
代码解释:
- Parent 父组件把自身的 list 状态传递给子组件
<Child list={this.state.list} />
- Child 子组件通过 props 获取到父组件传递的数据并成功渲染
const { list } = this.props
这个就是父组件向子组件传递数据。
思考一个问题,当我们的组件层级嵌套很深,例如 Parent 组件需要向 Child 的 Child 也就是孙子组件,甚至乎更加深的层级去传递一些数据该如何做呢?可以通过context去实现。
Context
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
我们常常用它来传递应用级别的配置数据,例如APP的主题、用户喜好之类的。
我们来实现一个切换主题的简单场景:
import React from 'react'
// 创建 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext('light') // {1}
// 函数式组件可以使用 Consumer
function ThemeLink (props) {
return <ThemeContext.Consumer> // {3}
{ value => <p>link's theme is {value}</p> }
</ThemeContext.Consumer>
}
class ThemedButton extends React.Component {
render() {
const theme = this.context // React 会往上找到最近的 theme Provider,然后使用它的值。
return <div>
<p>button's theme is {theme}</p>
</div>
}
}
ThemedButton.contextType = ThemeContext // 指定 contextType 读取当前的 theme context。
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
<ThemeLink />
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: 'light'
}
}
render() {
return <ThemeContext.Provider value={this.state.theme}> // {2}
<Toolbar />
<hr/>
<button onClick={this.changeTheme}>change theme</button>
</ThemeContext.Provider>
}
changeTheme = () => {
this.setState({
theme: this.state.theme === 'light' ? 'dark' : 'light'
})
}
}
export default App
代码解释:
- {1}
const ThemeContext = React.createContext('light')
创建一个context,返回一个ThemeContext 对象 - {2}
<ThemeContext.Provider value={this.state.theme}></ThemeContext.Provider>
ThemeContext 的提供者 - {3}
<ThemeContext.Consumer></ThemeContext.Consumer>
ThemeContext 的消费者
React.createContext 源码
export function createContext(
defaultValue,
calculateChangedBits
){
const context = {
?typeof: REACT_CONTEXT_TYPE,
_calculateChangedBits: calculateChangedBits,
_currentValue: defaultValue,
_currentValue2: defaultValue,
Provider:null,
Consumer:null ,
};
context.Provider = {
?typeof: REACT_PROVIDER_TYPE,
_context: context,
};
return context;
}
这是经过删减的源码,调用 createContext
函数返回的就是一个context对象,我们使用的 ThemeContext.Provider
也就是该对象返回的其中一个属性。
state
通过上面我们已经知道组件之间是如何进行通信的,那么组件如何维护自身的状态呢?就是 state
了,因为它的一些特性,也让它成为了面试必考。
import React from 'react'
class StateDemo extends React.Component{
constructor(props) {
super(props);
this.state = {
count: 0
}
}
add = ()=>{
this.setState({
count: this.state.count + 1
})
}
render() {
return <div>
<p>{this.state.count}</p>
<button onClick={this.add}>增加</button>
</div>
}
}
export default StateDemo
代码解释:
- 在 constructor 中初始化组件的 state 对象
- 当点击按钮时通过 setState 改变 state 的状态
这就是组件的 state,以及如何改变 state,关于它的常见面试题有:
- setState 是同步还是异步的?
- 为什么有时连续两次
setState
只有一次生效? - 为什么 state 必须为不可变值
setState 异步情况
1.1 React 生命周期中 setState
componentDidMount() {
console.log('SetState调用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index); // 0
console.log('SetState调用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index); // 0
}
两次打印都是0,没有立即更新
1.2 React 合成事件中 setState
add = ()=>{
console.log('合成事件中调用setState');
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count); // 0
console.log('合成事件中调用setState');
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count); // 0
}
- 两次输出都为0没有立即更新
- 这里进行了两次设置,但是页面只增加了1,说明被合并了
setState 同步情况
1.1 异步函数中执行 setState
componentDidMount() {
setTimeout(()=>{
console.log('SetState调用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index); // 1
console.log('SetState调用setState');
this.setState({
index: this.state.index + 1
})
console.log('state', this.state.index); // 2
},0);
}
我们使用 setTimeout 异步函数包装下,发现,setState 同步执行了。
1.2 原生事件中执行 setState
componentDidMount(){
document.body.addEventListener('click', this.bodyClickHandler); // 在生命周期中绑定原生事件
}
bodyClickHandler = ()=>{
console.log('原生事件中调用setState');
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count); // 1
console.log('原生事件中调用setState');
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count); // 2
}
在原生事件中执行 setState 也同步执行了。
异步情况:
- React 生命周期中 setState
- React 合成事件中 setState
原因:
在React
的生命周期和合成事件中,React
仍然处于他的更新机制中,这时isBatchingUpdates
为 true
。
这时无论调用多少次setState
,都会不会执行更新,而是将要更新的state
存入_pendingStateQueue
,将要更新的组件存入dirtyComponent
。
当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount
后会将 isBatchingUpdates
设置为 false
。这时将执行之前累积的setState
同步情况:
- 异步函数中执行 setState
- 原生事件中执行 setState
原因:
由执行机制看,setState
本身并不是异步的,而是当调用setState
时,如果React
正处于更新过程,当前更新会被暂存,等上一次更新执行后再执行,这个过程给人一种异步的假象。
在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后再执行,这时上一次更新过程已经执行完毕,isBatchingUpdates
被设置为 false
,根据上面的流程,这时再调用setState
即可立即执行更新,拿到更新结果。
简单理解:当isBatchingUpdates
为 true
时,加入异步队列,下一轮执行;当isBatchingUpdates
为 false
时,不加入异步队列,立即执行。可以理解成类似 JavaScript EventLoop
机制。
这是 React-16.6.0 的源码
if (!isBatchingUpdates && !isRendering) {
performSyncWork();
}
这句话的大概意思是,isBatchingUpdates
变量为 false
时立即执行performSyncWork方法。从该方法名的意思是“执行同步任务”。
Batch模式下React不会立刻修改state,而是把这个对象放到一个更新队列中,稍后才会从队列中把新的状态提取出来合并到state中,然后再触发组件更新,这样的设计主要目的是为了提高 UI 更新的性能。
setState 合并
从 React 源码中也可以看出 setState 第一个参数其实可以接受两种类型的,一种是对象,一种是函数。对象型是会进行合并处理 Object.assagin
,而函数型是不会的。
Component.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.',
);
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
我们再来看看源码中 enqueueSetState
是如何进行处理的:
for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
let partial = oldQueue[i];
let partialState =
typeof partial === 'function'
? partial.call(inst, nextState, element.props, publicContext)
: partial;
if (partialState != null) {
if (dontMutate) {
dontMutate = false;
nextState = Object.assign({}, nextState, partialState);
} else {
Object.assign(nextState, partialState);
}
}
}
这是其中处理 state queue 的源码片段,我们关注重点即可
if(partial === 'function'){
partial.call(inst, nextState, element.props, publicContext)
}
if (partialState != null) {
Object.assign(nextState, partialState);
}
如果 queue 中的 state 是函数则直接执行,如果是非函数,且不是null,则先进行对象合并操作。
我们来测试下传入函数是否真的不会合并
componentDidMount() {
console.log('SetState传入函数');
this.setState((state, props) => ({
index: state.index + 1
}));
console.log('state', this.state.index); // 0
console.log('SetState传入函数');
this.setState((state, props) => ({
index: state.index + 1
}));
console.log('state', this.state.index); // 0
setTimeout(()=>{
console.log('state', this.state.index); // 2
},0);
}
从结果可以看出来,我们同时更新了两次index,说明传入函数并不会被合并。
state必须为不可变值
我们那上面的 add 方法做例子,改造下:
add = ()=>{
this.state.count++;
this.setState({
count: this.state.count
})
}
- 我们先直接改变 state 对象中的 count 值
- 然后再去 setState
我们发现效果是一样的,是不是表示我们这样做也可以呢?答案是否定的。
必须得这样写:
add = ()=>{
this.setState({
count: this.state.count + 1
})
}
不可变值在对象和数组中的应用:
import React from 'react'
class StateDemo1 extends React.Component{
constructor(props) {
super(props);
this.state = {
list:[
{
id:1,
name:"a"
},
{
id:2,
name:"b"
},
{
id:3,
name:"C"
},
]
}
}
render() {
return (
<div>
<ul>
{
this.state.list.map((item)=>{
return <li key={item.id}>
<span>{item.name}</span>
</li>
})
}
</ul>
<button onClick={this.deletePop}>删除最后一条</button>
</div>
)
}
}
export default StateDemo1
当我们需要对这个列表进行删除时, this.state.list
就必须遵循不可变值。
deletePop = ()=>{
const newList = [...this.state.list];
newList.pop();
this.setState({
list: newList
})
}
- 首先对
this.state.list
进行浅复制 - 再操作新的 list 数组
- 在 setState 中进行赋值操作
在这里我们没有去改变原 state.list
值,而是浅复制了一个副本出来再去操作的。这就是在 React
编程中要非常注意的“不可变值”。
至于为什么必须要不可变值呢,这个其实跟React性能优化有非常大的关系,在下一篇文章中会详细讲述其中原因。
React 事件
合成事件
Virtual DOM 在内存中是以对象的形式存在的,如果想要在这些对象上添加事件,就会非常简单。React 基于 Virtual DOM 实现了一个 SyntheticEvent (合成事件)层,我们所定义的事件处理器会接收到一个 SyntheticEvent 对象的实例,它完全符合 W3C 标准,不会存在任何 IE 标准的兼容性问题。并且与原生的浏览器事件一样拥有同样的接口,同样支持事件的冒泡机制,我们可以使用 stopPropagation() 和 preventDefault() 来中断它。
合成事件的实现机制
在 React 底层,主要对合成事件做了两件事:事件委派和自动绑定。
1. 事件委派
在使用 React 事件前,一定要熟悉它的事件代理机制。它并不会把事件处理函数直接绑定到真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升。
2. 自动绑定
在 React 组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this
为当前组件。而且 React 还会对这种引用进行缓存,以达到 CPU 和内存的最优化。
实际上,React 的合成事件系统只是原生 DOM 事件系统的一个子集。它仅仅实现了 DOM Level 3 的事件接口,并且统一了浏览器间的兼容问题。有些事件 React 并没有实现,或者受某些限制没办法去实现,比如 window
的 resize
事件。
对于无法使用 React 合成事件的场景,我们还需要使用原生事件来完成。
为什么要手动绑定this
ES6类的方法内部如果含有this
,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。而React中执行事件回调方法放入一个队列中,当事件被触发时执行相应的回调,因此该事件回调方法并未与React组件实例绑定在一起,所以我们需要进行手动绑定上下文。
bind this 绑定
import React from 'react'
class EventDemo extends React.Component{
constructor(props) {
super(props);
this.state = {
count: 0
}
}
add(){
this.setState({
count: this.state.count + 1
})
}
render() {
return <div>
<p>{this.state.count}</p>
<button onClick={this.add.bind(this)}>增加</button>
</div>
}
}
或者在 constructor 中 bind this
constructor(props) {
super(props);
this.state = {
count: 0
}
this.add = this.add.bind(this);
}
箭头函数
使用箭头函数就不需要使用 bind this 进行绑定了
add = ()=>{
this.setState({
count: this.state.count + 1
})
}
合成事件与原生事件的区别
- React 事件使用驼峰命名,而不是全部小写;
- 阻止原生事件传播需要使用 e.preventDefault(),不过对于不支持该方法的浏览器(IE9 以下),只能使用 e.cancelBubble = true 来阻止。而在 React 合成事件中,只需要使用 e.preventDefault() 即可;
- React自己实现了一套事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,并且抹平了各个浏览器的兼容性问题。
React
事件和原生事件的执行顺序
import React from 'react'
class EventDemo extends React.Component {
constructor(props) {
super(props);
this.parent = React.createRef();
this.child = React.createRef();
}
componentDidMount() {
this.parent.current.addEventListener('click', (e) => {
console.log('dom parent');
})
this.child.current.addEventListener('click', (e) => {
console.log('dom child');
})
document.addEventListener('click', (e) => {
console.log('document');
})
}
childClick = (e) => {
console.log('react child');
}
parentClick = (e) => {
console.log('react parent');
}
render() {
return (
<div onClick={this.parentClick} ref={this.parent}>
<div onClick={this.childClick} ref={this.child}>
test Event
</div>
</div>)
}
}
export default EventDemo
执行结果:
dom child
dom parent
react child
react parent
document
由上面的流程我们可以理解:
React
的所有事件都挂载在document
中- 当真实dom触发后冒泡到
document
后才会对React
事件进行处理 - 所以原生的事件会先执行
- 然后执行
React
合成事件 - 最后执行真正在
document
上挂载的事件
React事件和原生事件可以混用吗?
React
事件和原生事件最好不要混用。
原生事件中如果执行了stopPropagation
方法,则会导致其他React
事件失效。因为所有元素的事件将无法冒泡到document
上。
受控组件与非受控组件
受控组件与非受控组件主要是针对表单元素
受控组件
我们先来看一段React处理表单的代码:
import React from 'react'
class FormDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
name: 'frank'
}
}
render() {
return <div>
<p>{this.state.name}</p>
<input id="inputName" value={this.state.name} onChange={this.onInputChange}/>
</div>
}
onInputChange = (e) => {
this.setState({
name: e.target.value
})
}
}
export default FormDemo
你心中一定会有疑问,为何 <input>
要绑定一个 change 事件呢?
在 HTML 中,表单元素(如<input>
、 <textarea>
和 <select>
)之类的表单元素通常自己维护 state,并根据用户输入进行更新。而在 React 中,可变状态通常保存在组件的 state 属性中,并且只能通过使用 setState()
来更新。被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”。
总结下 React 受控组件更新 state 的流程:
(1) 可以通过在初始 state 中设置表单的默认值。
(2) 每当表单的值发生变化时,调用 onChange
事件处理器。
(3) 事件处理器通过合成事件对象 e
拿到改变后的状态,并更新应用的 state。
(4) setState
触发视图的重新渲染,完成表单组件值的更新。
非受控组件
先来看一段代码:
class FormDemo1 extends React.Component {
constructor(props) {
super(props)
this.content = React.createRef();
}
handleSubmit=(e)=>{
e.preventDefault();
const { value } = this.content.current;
console.log(value);
}
render() {
return <form onSubmit={this.handleSubmit}>
<input ref={this.content} type="text" defaultValue="frank" />
<button type="submit">Submit</button>
</form>
}
}
在 React 中,非受控组件是一种反模式,它的值不受组件自身的 state 或 props 控制。通常,需要通过为其添加 ref 来访问渲染后的底层 DOM 元素。
受控组件和非受控组件的最大区别是:非受控组件的状态并不会受应用状态的控制,而受控组件的值来自于组件的 state。
小结
由于React在面试中占有非常的比重,因此关于React技术栈的面试文章将分解为多篇进行讲解