㈠ 详解React中传入组件的props改变时更新组件的几种实现方法
我们使用react的时候常常需要在一个组件传入的props更新时重新渲染该组件,常用的方法是在componentWillReceiveProps中将新的props更新到组件的state中(这种state被成为派生状态(Derived State)),从而实现重新渲染。React 16.3中还引入了一个新的钩子函数getDerivedStateFromProps来专门实现这一需求。但无论是用componentWillReceiveProps还是getDerivedStateFromProps都不是那么优雅,而且容易出错。所以今天来探讨一下这类实现会产生的问题和更好的实现方案。
何时使用派生状态
咱们先来看一个比较常见的需求,一个用户列表,可以新增和编辑用户,当用户点击‘新建'
按钮用户可以在输入框中输入新的用户名;当点击‘编辑'按钮的时候,输入框中显示被编辑的用户名,用户可以修改;当用户点击‘确定'按钮的时候用户列表更新。
class UserInput extends React.Component {
state = {
user: this.props.user
}
handleChange = (e) => {
this.setState({
user: {
...this.state.user,
name: e.target.value
}
});
}
render() {
const { onConfirm } = this.props;
const { user } = this.state;
return (
<div>
<input value={user.name || ''} onChange={this.handleChange} />
<button onClick={() => { onConfirm(user) }}>确定</button>
</div>
);
}
}
class App extends React.Component {
state = {
users: [
{ id: 0, name: 'bruce' },
{ id: 1, name: 'frank' },
{ id: 2, name: 'tony' }
],
targetUser: {}
}
onConfirm = (user) => {
const { users } = this.state;
const target = users.find(u => u.id === user.id);
if (target) {
this.setState({
users: [
...users.slice(0, users.indexOf(target)),
user,
...users.slice(users.indexOf(target) + 1)
]
});
} else {
const id = Math.max(...(users.map(u => u.id))) + 1;
this.setState({
users: [
...users,
{
...user,
id
}
]
});
}
}
render() {
const { users, targetUser } = this.state;
return (
<div>
<UserInput user={targetUser} onConfirm={this.onConfirm} />
<ul>
{
users.map(u => (
<li key={u.id}>
{u.name}
<button onClick={() => { this.setState({ targetUser: u }) }}>编辑</button>
</li>
))
}
</ul>
<button onClick={() => { this.setState({ targetUser: {} }) }}>新建</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'));
运行后,效果如图:
现在点击‘编辑'和‘新建'按钮,输入框中的文字并不会切换,因为点击‘编辑'和‘更新'时,虽然UserInput的props改变了但是并没有触发state的更新。所以需要实现props改变引发state更新,在UserInput中增加代码:
componentWillReceiveProps(nextProps) {
this.setState({
user: nextProps.user
});
}
或者
static getDerivedStateFromProps(props, state) {
return {
user: props.user
};
}
这样就实现了UserInput每次接收新的props的时候自动更新state。但是这种实现方式是有问题的。
派生状态导致的问题
首先来明确组件的两个概念:受控数据(controlled data lives)和不受控数据(uncontrollered data lives)。受控数据指的是组件中通过props传入的数据,受到父组件的影响;不受控数据指的是完全由组件自己管理的状态,即内部状态(internal state)。而派生状态揉合了两种数据源,当两种数据源产生冲突时,问题随之产生。
问题一
当在修改一个用户的时候,点击‘确定'按钮,输入框里的文字又变成了修改之前的文字。比如我将‘bruce'修改为‘bruce lee',确定后,输入框中又变成了‘bruce',这是我们不愿意看到的。
出现这个问题的原因是,点击确定,App会re-render,App又将之前的user作为props传递给了UserInput。我们当然可以在每次点击确定之后将targetUser重置为一个空对象,但是一旦状态多了之后,这样管理起来非常吃力。
问题二
假设页面加载完成后,会异步请求一些数据然后更新页面,如果用户在请求完成页面刷新之前已经在输入框中输入了一些文字,随着页面的刷新输入框中的文字会被清除。
我们可以在App中加入如下代码模拟一个异步请求:
componentDidMount() {
setTimeout(() => {
this.setState({
text: 'fake request'
})
}, 5000);
}
导致这个问题的原因在于,当异步请求完成,setState后App会re-render,而组件的componentWillReceiveProps会在父组件每次render的时候执行,而此时传入的user是一个空对象,所以UserInput的内容被清空了。而getDerivedStateFromProps调用的更频繁,会在组件每次render的时候调用,所以也会产生该问题。
为了解决这个问题我们可以在componentWillReceiveProps中判断新传入的user和当前的user是否一样,如果不一样才设置state:
componentWillReceiveProps(nextProps) {
if (nextProps.user.id !== this.props.user.id) {
this.setState({
user: nextProps.user
});
}
}
更好的解决方案
派生状态的数据源的不确定性会导致各种问题,那如果每份数据有且只被一个component管理应该就能避免这些问题了。这种思路有两种实现,一种是数据完全由父组件管理,一种是数据完全由组件自己管理。下面分别讨论:
完全受控组件(fully controlled component)
组件的数据完全来自于父组件,组件自己将不需要管理state。我们新建一个完全受控版的UserInput:
class FullyControlledUserInput extends React.Component {
render() {
const { user, onConfirm, onChange } = this.props;
return (
<div>
<input value={user.name || ''} onChange={onChange} />
<button onClick={() => { onConfirm(user) }}>确定</button>
</div>
)
}
}
App中调用FullyControlledUserInput的方法如下:
...
<FullyControlledUserInput
user={targetUser}
onChange={(e) => {
this.setState({
targetUser: {
id: targetUser.id,
name: e.target.value
}
});
}}
onConfirm={this.onConfirm}
/>
...
现在FullyControlledUserInput中的所有的数据都来源于父组件,由此解决数据冲突和被篡改的问题。
完全不受控组件(fully uncontrolled component)
组件的数据完全由自己管理,因此componentWillReceiveProps中的代码都可以移除,但保留传入props来设置state初始值:
class FullyUncontrolledUserInput extends React.Component {
state = {
user: this.props.user
}
onChange = (e) => {
this.setState({
user: {
...this.state.user,
name: e.target.value
}
});
}
render() {
const { user } = this.state;
const { onConfirm } = this.props;
return (
<div>
<input value={user.name || ''} onChange={this.onChange} />
<button onClick={() => { onConfirm(user) }}>确定</button>
</div>
)
}
}
当传入的props发生改变时,我们可以通过传入一个不一样的key来重新创建一个component的实例来实现页面的更新。App中调用FullyUncontrolledUserInput的方法如下::
<FullyUncontrolledUserInput
user={targetUser}
onConfirm={this.onConfirm}
key={targetUser.id}
/>
大部分情况下,这是更好的解决方案。或许有人会觉得这样性能会受影响,其实性能并不会变慢多少,而且如果组件的更新逻辑过于复杂的话,还不如重新创建一个新的组件来的快。
在父组件中调用子组件的方法设置state
如果某些情况下没有合适的属性作为key,那么可以传入一个随机数或者自增的数字作为key,或者我们可以在组件中定义一个设置state的方法并通过ref暴露给父组件使用,比如我们可以在UserInput中添加:
setNewUserState = (newUser) => {
this.setState({
user: newUser
});
}
在App中通过ref调用这个方法:
...
<UserInput user={targetUser} onConfirm={this.onConfirm} ref='userInput' />
<ul>
{
users.map(u => (
<li key={u.id}>
{u.name}
<button onClick={() => {
this.setState({ targetUser: u });
this.refs.userInput.setNewUserState(u);
}}>
编辑
</button>
</li>
))
}
</ul>
<button onClick={() => {
this.setState({ targetUser: {} });
this.refs.userInput.setNewUserState({});
}}>
新建
</button>
...
这个方法不推荐使用,除非实在没法了。
本文源码请参考:ways-to-update-component-on-props-change
㈡ 深入理解React16之:(一).Fiber架构
React16虽然出了一阵子了。刚出来的时候,粗略看了一遍更新文档。以为没什么大的改动,也听说项目从react15-16的升级过度可以很平滑,再加上项目改版上线一直比较频繁,所以一直还用的15.6的版本。
偶然在知乎看到@程墨Morgan大神的live,便抱着好奇心和学习的心态报名了,受益良多。
我理解的Fiber架构:
在我之前的一篇文章有简单介绍, 阅读react源码--记录:1.1 问题记录
下面从一个具体实例理解一下,再加上我画了图,应该很好理解啦~(图画的有点渣)
假如有A,B,C,D组件,层级结构为:
我们知道组件的生命周期为:
挂载阶段:
那么在挂载阶段,A,B,C,D的生命周期渲染顺序是如何的呢?
以render()函数为分界线。从顶层组件开始,一直往下,直至最底层子组件。然后再往上。
组件update阶段同理。
————————
前面是react16以前的组建渲染方式。这就存在一个问题,
好似一个潜水员,当它一头扎进水里,就要往最底层一直游,直到找到最底层的组件,然后他再上岸。在这期间, 岸上发生的任何事,都不能对他进行干扰,如果有更重要的事情需要他去做(如用户操作),也必须得等他上岸
看一下fiber架构 组建的渲染顺序
潜水员会每隔一段时间就上岸,看是否有更重要的事情要做。
加入fiber的react将组件更新分为两个时期
这两个时期以render为分界,
phase1的生命周期是可以被打断的,每隔一段时间它会跳出当前渲染进程,去确定是否有其他更重要的任务。此过程,React 在 workingProgressTree (并不是真实的virtualDomTree)上复用 current 上的 Fiber 数据结构来一步地(通过requestIdleCallback)来构建新的 tree,标记处需要更新的节点,放入队列中。
phase2的生命周期是不可被打断的,React 将其所有的变更一次性更新到DOM上。
这里最重要的是phase1这是时期所做的事。因此我们需要具体了解phase1的机制。
这样的话,就和react16版本之前有很大区别了,因为可能会被执行多次,那么我们最好就得保证phase1的生命周期 每一次执行的结果都是一样的 ,否则就会有问题,因此, 最好都是纯函数。
对了,程墨大神还提到一个问题,饥饿问题,即如果高优先级的任务一直存在,那么低优先级的任务则永远无法进行,组件永远无法继续渲染。这个问题facebook目前好像还没解决,但以后会解决~
所以,facebook在react16增加fiber结构,其实并不是为了减少组件的渲染时间,事实上也并不会减少,最重要的是现在可以使得一些更高优先级的任务,如用户的操作能够优先执行,提高用户的体验,至少用户不会感觉到卡顿~
㈢ 如何用reactjs构建一个完整的前端页面
用reactjs构建一个完整的前端页面的步骤:
准备:React 的安装包,建议去官网下载安装
1、使用 React 的网页源码,结构大致如下:
<!DOCTYPE html><html><head> <script src="../build/react.js"></script> <script src="../build/react-dom.js"></script> <script src="../build/browser.min.js"></script></head><body> <div id="example"></div> <script type="text/babel"> // **用户代码 ** </script></body></html>
上面代码有两个地方需要注意。
首先,最后一个<script>标签的type属性为text/babel。这是因为 React 独有的 JSX 语法,跟 JavaScript 不兼容。凡是使用 JSX 的地方,都要加上type="text/babel"。
其次,上面代码一共用了三个库:react.js、react-dom.js和Browser.js,它们必须首先加载。其中,react.js是 React 的核心库,react-dom.js是提供与 DOM 相关的功能,Browser.js的作用是将 JSX 语法转为 JavaScript 语法,这一步很消耗时间,实际上线的时候,应该将它放到服务器完成。
2、将src子目录的js文件进行语法转换,转码后的文件全部放在build子目录。
$ babel src --out-dir build
3、渲染转换成html节点,以方便操作dom:
ReactDOM.render 是 React 的最基本方法,用于将模板转为 HTML 语言,并插入指定的 DOM 节点。
这里以插入hello world为例来说明
ReactDOM.render(<h1>Hello, world!</h1>,document.getElementById('example'));
4、运行结果如下:
㈣ create-react-app build 打包隐藏源码
在使用 create-react-app 时,打包生产环境 npm run build ,浏览器打开后仍然是可以看到源码的。
在这里以新建一个默认项目为例:
项目根目录新建 .env.proction 文件,内容如下:
然后重新打包,浏览器打开后就看不到源码啦。
为了探究原理,执行 eject 后,可以看到webpack配置中有这么一段
这里的 process.env.GENERATE_SOURCEMAP 控制着是否捎带源码。所以我们可以配置环境变量 GENERATE_SOURCEMAP=false 即可。
当执行 build 时,将按顺序优先寻找 .env.proction.local , .env.proction , .env.local , .env 文件来配置环境变量,所以就有了上面的操作。
更多关于环境变量的信息可查看 Adding Custom Environment Variables 。