React基础
JSX
const element = <h1>Hello, world!</h1>;
JSX,既不是字符串也不是HTML,本质上是一个 JavaScript 的语法扩展,且更接近于JavaScript,是通过React.createElement()
创建的一个对象,称为React 元素
。
React 不强制使用JSX,但将标记与逻辑放在一起形成组件
,实现关注点分离。同时,JSX 能够防止XSS注入攻击。
元素渲染
-
React 元素是不可变对象。一旦被创建,你就无法更改它的子元素或者属性。
更新 UI 唯一的方式是创建一个全新的元素,并将其传入ReactDOM.render()
。 -
React 只更新它需要更新的部分。
React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。
组件&Props
- 函数组件:接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。
- class组件:形如
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
- Props 的只读性: 所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
- state 允许 React 组件随用户操作、网络响应或者其他变化而动态更改输出内容。
组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。
这样的函数被称为“纯函数”,因为该函数不会尝试更改入参,且多次调用下相同的入参始终返回相同的结果。
React实战视频讲解:进入学习
State&生命周期
setState(updater,[callback])
在React中,如果是由React引发的事件处理
(比如通过onClick引发的事件处理),调用setState不会同步更新this.state
,
为什么要异步?如果setState是同步更新state,而state的更新又会触发组件的重新渲染,那么每次setState都会渲染组件,这对性能是很大的消耗。
- 正常React绑定的事件:异步更新
- 通过addEventListener绑定的事件:同步更新
- 通过setTimeoutt处理点击事件:同步更新
使用 compoentDidUpdate
或 setState
的回调函数,来保证在更新应用后触发。
批量更新,是基于一个队列和一个变量锁isBatchingUpdates
实现。
正确地使用 State的姿势:
-
不要直接修改 State
-
调用setState不会立即更新
-
所有组件使用的是同一套更新机制,当所有组件didmount后,父组件didmount,然后执行更新
-
更新时会把每个组件的更新合并,每个组件只会触发一次更新的生命周期。
-
钩子函数和合成事件中:
在react的生命周期和合成事件中,react仍然处于他的更新机制中,这时isBranchUpdate为true
。
按照上述过程,这时无论调用多少次setState,都会不会执行更新,而是将要更新的state存入_pendingStateQueue
,将要更新的组件存入dirtyComponent
。
当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件didmount
后会将isBranchUpdate
设置为false
。这时将执行之前累积的setState
。
- 异步函数和原生事件中
由执行机制看,setState本身并不是异步的
,而是如果在调用setState时,如果react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象
。
在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕,
isBranchUpdate
被设置为false
,根据上面的流程,这时再调用setState即可立即执行更新,拿到更新结果。
- componentDidMount调用setstate
它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。
componentDidMount本身处于一次更新中
,我们又调用了一次setState,就会在未来再进行一次render,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。
-
componentWillUpdate
、componentDidUpdate
不能调用setState, 会造成死循环,导致程序崩溃。 -
推荐:在调用setState时使用函数传递state值,在回调函数中获取最新更新后的state。
生命周期:
- 挂载
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
注意:
下述生命周期方法即将过时,在新代码中应该避免使用它们: UNSAFE_componentWillMount()
- 更新
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
注意:
下述方法即将过时,在新代码中应该避免使用它们:
UNSAFE_componentWillUpdate() UNSAFE_componentWillReceiveProps()
- 卸载
当组件从 DOM 中移除时会调用如下方法:
componentWillUnmount()
事件处理
- 在 React 中你不能通过返回false 来阻止默认行为。必须明确调用
preventDefault
。
React自己实现了一套事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,并且抹平了各个浏览器的兼容性问题。
- React事件与原生事件的执行顺序
-
react的所有事件都
挂载在document中
-
当真实dom触发后冒泡到document后才会对react事件进行处理
-
所以原生的事件会先执行
-
然后执行react合成事件
-
最后执行真正在document上挂载的事件
- react事件和原生事件可以混用吗?
react事件和原生事件最好不要混用。
原生事件中如果执行了stopPropagation方法,则会导致其他react事件失效。因为所有元素的事件将无法冒泡到document上
。
this绑定:你必须谨慎对待 JSX 回调函数中的 this,在 JavaScript 中,class 的方法默认不会绑定 this。
方法有三:
- 在构造比函数中绑定一下:
this.handleClick = this.handleClick.bind(this);
- 在类以方法定义事件处理函数时,使用箭头函数:
handleClick = () => {console.log('this is:', this);}
- 直接在回调函数中使用箭头函数:
<button onClick={() => this.handleClick()}>Click me</button>
注意:
[性能优化点]每次渲染 Button 时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染
。
我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。
组合vs继承
React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。 Props 和 组合
为你提供了清晰而安全地定制组件外观和行为的灵活方式。
注意:组件可以接受任意 props,包括基本数据类型,React 元素以及函数。
- 使用一个特殊的
{props.children}
来将他们的子组件传递到渲染结果中 - 少数情况下,你可能需要在一个组件中预留出几个“洞”。这种情况下,我们可以不使用 children,而是自行约定:将所需内容传入 props,并使用相应的 prop,类似于槽
slot
的概念。
Context
Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递
props。
Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言。
[代码优化点]
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。
如果你只是想避免层层传递一些属性,组件组合(component composition)有时候是一个比 context 更好的解决方案。
一种无需 context 的解决方案是将子组件自身传递下去,因而中间组件无需知道该子组件用到的props
。
错误边界
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界。
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。
错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
[代码优化点]
错误边界无法捕获以下场景中产生的错误:
- 事件处理(了解更多)
- 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
Refs转发
Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。这个技巧对高阶组件(也被称为 HOC)特别有用
。
Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
Fragments
React 中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点。
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
);
}
或者使用短语法:<> </>
高阶组件
定义:高阶组件是参数为组件,返回值为新组件的函数。
HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用
。
高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式
。
const EnhancedComponent = higherOrderComponent(WrappedComponent);
组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。例如:Redux 的 connect
注意:
- 不要在 render 方法中使用 HOC。 每次调用 render 函数都会创建一个新的 EnhancedComponent,导致子树每次渲染都会进行卸载,和重新挂载的操作!
- 务必复制静态方法。你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法
- Refs 不会被传递。
与第三方库协同
我们会添加一个 ref 到这个根 DOM 元素
。 在 componentDidMount 中,我们能够获取它的引用这样我们就可以把它传递给 jQuery 插件了。
为了防止 React 在挂载之后去触碰这个 DOM,我们会从 render() 函数返回一个空的 <div />
。
这个
元素既没有属性也没有子元素,所以 React 没有理由去更新它,使得 jQuery 插件可以自由的管理这部分的 DOM
class SomePlugin extends React.Component {
componentDidMount() {
this.$el = $(this.el);
this.$el.somePlugin();
}
componentWillUnmount() {
this.$el.somePlugin('destroy');
}
render() {
return <div ref={el => this.el = el} />;
}
}
性能优化
- 部署时使用生产版本,去除一些警告信息
- 虚拟化长列表。"虚拟滚动”技术。这项技术会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量。
react-window
和react-virtualized
是热门的虚拟滚动库。 - 避免调停。你可以通过
覆盖生命周期方法 shouldComponentUpdate 来进行提速
。该方法会在重新渲染前被触发。其默认实现总是返回 true. 如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。
继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较
覆写了 shouldComponentUpdate() 的实现.
shouldComponentUpdate(nextProps, nextState) {
return true;
}
- 不可变数据的力量。不改变原来的对象,使用 ...扩展运算符 或 Object.assign 返回新对象。
Diff算法
- 当对比两颗树时,React 首先比较两棵树的根节点。
-
当根节点为不同类型的元素时,React 会拆卸原有的树并且建立起新的树。
componentWillUnmount()
->componentWillMount()
->componentDidMount()
-
当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,
仅比对及更新有改变的属性
。然后子节点递归。
- 子节点递归
在子元素列表末尾新增元素时,更新开销比较小;
如果只是简单的将新增元素插入到表头,那么更新开销会比较大,不会意识到应该保留后面的,而是会重建每一个子元素 。这种情况会带来性能问题。
通过添加key来解决。
尽量用相同的节点类型和稳定可预测的Key。
Render Prop
render prop 是一个用于告知组件需要渲染什么内容的函数 prop。使用 Props 而非 render。
重要的是要记住,render prop 是因为模式才被称为 render prop ,你不一定要用名为 render 的 prop 来使用这种模式。
将 Render Props 与 React.PureComponent 一起使用时要小心。
如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势
。
因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。
Key的使用方式
react根据key来决定是销毁重新创建组件还是更新组件,原则是:
- key相同,组件有所变化,react会只更新组件对应变化的属性。
- key不同,组件会销毁之前的组件,将整个组件重新渲染。
使用index做key存在的问题:
当元素数据源的顺序发生改变时,会重新渲染。而如果使用唯一ID作为key,子组件的值和key均未发生变化,只是顺序发生改变,因此react只是将他们做了移动,并未重新渲染。
虚拟DOM
关于能否提升性能
很多文章说VitrualDom可以提升性能,这一说法实际上是很片面的。
直接操作DOM是非常耗费性能的,这一点毋庸置疑。但是React使用VitrualDom也是无法避免操作DOM的。
如果是首次渲染,VitrualDom不具有任何优势
,甚至它要进行更多的计算,消耗更多的内存。
VitrualDom的优势在于React的Diff算法和批处理策略
,React在页面更新之前,提前计算好了如何进行更新和渲染DOM。
实际上,这个计算过程我们在直接操作DOM时,也是可以自己判断和实现的,但是一定会耗费非常多的精力和时间,而且往往我们自己做的是不如React好的。
所以,在这个过程中React帮助我们"提升了性能"。
所以,我更倾向于说,VitrualDom帮助我们提高了开发效率,在重复渲染时它帮助我们计算如何更高效的更新,而不是它比DOM操作更快。
跨浏览器兼容
React基于VitrualDom自己实现了一套自己的事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,抹平了各个浏览器的事件兼容性问题。
实现原理
- React组件的渲染流程
-
使用React.createElement或JSX编写React组件,实际上所有的JSX代码最后都会转换成
React.createElement(...)
,Babel帮助我们完成了这个转换的过程。 -
createElement函数对key和ref等特殊的
props进行处理
,并获取defaultProps对默认props进行赋值
,并且对传入的孩子节点进行处理,最终构造成一个ReactElement对象
(所谓的虚拟DOM)。 -
ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中
采用了批处理
、事务等机制
并且对特定浏览器进行了性能优化,最终转换为真实DOM
- 虚拟DOM组成
- 防止XSS: 借助Symbol.for('react.element')
- 批处理和事务机制:setState
- 针对性的性能优化:IE/Edge Fragment
- 事件机制:自己实现了一套事件机制,其将所有绑定在虚拟DOM上的事件映射到真正的DOM事件,并
将所有的事件都代理到document上
,自己模拟了事件冒泡和捕获的过程
,并且进行统一的事件分发。