React简介
React是一个声明式、高效且灵活的用于构建用户界面的JavaScript库。使用React可以将一些简短的、独立的代码片段组合成复杂的UI界面,这些代码片段称为组件。
UI = render(data) => 单向数据流
MVC、MVVM
MVC:
在Model中定义数据及其改变数据的方法,使用观察者模式定义注册所有View的方法;在Controller中初始化View和Model,调用Model的方法让View向Model注册,这样Model更新就会去通知View,从而更新视图;在View中根据对应DOM绑定方法,当用户操作DOM时,操作Controller中的更新Model的方法。
MVVM:
在Model中只定义数据,在ViewModel中定义操作数据的方法,当用户操作View时,View调用ViewModel中操作数据的方法,从而改变了Model,ViewModel中的数据发生变化也通知View更新视图,即实现了View和ViewModel的双向数据绑定。
其实Vue和React严格来说都不是MVVM模式。因为Vue的ref属性直接操作了DOM,跳过了ViewModel;而React只是单向数据流,即只有视图是根据数据变化的。
JSX模板语法
JSX称为JS的语法扩展,将UI与逻辑层耦合在组件⾥,⽤{}标识。因为 JSX 语法上更接近 JS ⽽不是 HTML,所以使⽤ camelCase(小驼峰命名)来定义属性的名称; JSX ⾥的 class 变成了 className,而 tabindex 则变为 tabIndex。
- JSX支持JS表达式、变量、方法名:
//变量
const name = 'casey';
const element = <h1>hello, {name}</h1>
//方法
const user = {
firstName: 'wa',
lastName: 'Rui'
};
function formarName(user) {
return user.firstName + ' ' + user.lastName;
};
const element = (
<h1>hello, {formatName(user)}</h1>
);
- JSX指定属性。(支持防注入,可预防XSS攻击。)React如何预防XSS(反射型XSS、存储型XSS):在渲染到浏览器前进行转义,对含有特殊含义的字符都转义了,恶意代码在渲染到HTML前都被转成了字符串。
- JSX表示对象:
const element = (
<h1 className='a'>haha</h1>
);
//等同于
const element = React.createElement('h1', { className:'a' }, 'haha');
const element = {
type: 'h1',
props: {
className: 'a',
children: 'haha'
}
}
- 将JSX渲染为DOM:
ReactDOM.render(element, document.getElementById('root');
//render只能代表当前时刻的状态;更新元素只能再次ReactDOM.render
- JSX转JS:babel。
props及state
组件
从概念上类似于JavaScript函数,接收任意的入参,即props,并返回用于描述页面展示内容的React元素。分为函数式组件和Class类组件。
//函数式组件
function Welcome(props) {
return <h1>hello, {props.name}</h1>;
}
//类组件
class Welcome extends React.Component {
render() {
return <h1>hello, {this.props.name}</h1>
}
}
//自定义组件使用大写字母开头
//渲染组件
ReactDOM.render(element, document.getElementById('root'));
组件的组合与拆分:
//页面内多次引用
<div>
<Welcome name='casey'/>
<Welcome name='jom'/>
</div>
//组合
function Comment(props) {
return (
<div className='Comment'>
<div className='UserInfo'>
<img className='Avatar' src={props.author.avatarUrl} alt={props.author.name}/>
<div className='UserInfo-name'>
{props.author.name}
</div>
</div>
<div className='Comment-text'>
{props.text}
</div>
<div className='Comment-date'>
{formatDate(props.date)}
</div>
</div>
)
}
//拆分
function Comment(props) {
return (
<div className='Comment'>
<UserInfo user={props.author}/>
<div className='Comment-text'>
{props.text}
</div>
<div className='Comment-date'>
{formatDate(props.date)}
</div>
</div>
)
}
props
所有React组件都必须像纯函数一样保护它们的props不被更改。
//使用props形式
function Clock(props) {
return (
<div>
<h1>hello, world</h1>
<h2>it is {props.date.toLocaleTimeString()}</h2>
</div>
);
}
function tick() {
ReactDOM.render(
<Clock date={new Date()}/>,
document.getElementById('root')
);
}
setInterval(tick, 1000);
state
如何避免多次ReactDom.render?
//引用生命周期,根组件保留一个
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {
date: new Date()
}
}
componentDidMount() {
this.timeID = setInterval(() => this.tick(), 1000);
}
componentWillUnmount() {
clearInterval(this.timeID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>hello, world</h1>
<h2>it is {this.state.date.toLocaleTimeString()}</h2>
</div>
)
}
}
ReactDOM.render(<Clock/>, document.getElementById('root'));
注意事项:
- 构造函数是唯一可以给state赋值的地方。
- state更新可能是异步的,可能是同步的。
//异步更新
this.setState({
counter: this.state.counter + this.props.increment
});
//同步更新
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
- state更新会合并。
- 单向数据流:state只在当前组件里生效,属于组件内的属性。重复实例化相同的组件,内部的内存地址也是不一样的。
setState
异步目的:批量处理,性能优化。
setState异步情况:
//1.合成事件
class App extends Component {
state = { val: 0 };
increment = () => {
this.setState({ val: this.state.val + 1 });
console.log(this.state.val);//0
}
render() {
return (
<div onClick={this.increment}>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
//2.生命周期
class App extends Component {
state = { val: 0 };
componentDidMount() {
this.setState({ val: this.state.val + 1 });
console.log(this.state.val);//0
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
同步更新情况:
//1.原生事件
class App extends Component {
state = { val: 0 };
changeValue = () => {
this.setState({ val: this.state.val + 1 });
console.log(this.state.val);//1
}
componentDidMount() {
document.body.addEventListener('click', this.changeValue, false);
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
//2.setTimeout
class App extends Component {
state = { val: 0 };
componentDidMount() {
setTimeout(_ => {
this.setState({ val: this.state.val + 1 });
console.log(this.state.val);//1
}, 0);
}
render() {
return (
<div>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
//批处理(batch处理)
class App extends Component {
state = { val: 0 };
batchUpdates = () => {
this.setState({ val: this.state.val + 1 });
this.setState({ val: this.state.val + 1 });
this.setState({ val: this.state.val + 1 });
}
render() {
return (
<div onClick={this.batchUpdates}>
{`Counter is: ${this.state.val}`}//1
</div>
)
}
}
setState情况总结:
- setState只在合成事件和生命周期中是“异步”的,在原生事件和setTimeout中都是“同步”的。
- setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和生命周期钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,也可以通过设置setState(partialState, callback)中的第二个参数callback拿到更新后的结果。
- setState的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout中不会批量更新。在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行。如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。
生命周期
如下图,看图说明生命周期时分为挂载时、更新时、卸载时来分别说明。
render
- 是class组件必需的方法。
- 获取最新的props和state。
- 在不修改组件state的情况下,每次调用时都返回相同的结果。
constructor
如果不初始化state或不进行方法绑定,则不需要为React组件实现构造函数。
- 通过给this.state赋值对象来初始化内部state。
- 为事件处理函数绑定实例。
constructor(props) {
super(props);
this.state = {counter: 0};
this.handleClick = this.handleClick.bind(this);
}
//1.不要在此调用setState();2.避免将props的值赋值给state。
componentDidMount
- 会在组件挂载后(插入DOM树中)立即调用;
- 依赖于DOM节点的初始化应该放在这里,如需通过网络请求获取的数据;
- 可以在此生命周期里加setState,但发生在浏览器更新屏幕之前,会导致性能问题;
- 在render阶段的constructor中初始化state,但有更新时可在此生命周期中setState。
componentDidUpdate
componentDidUpdate(prevProps, prevState, snapshot);
会在更新后被立即调用,首次渲染不会执行此方法。
componentDidUpdate(prevProps) {
//典型⽤法(不要忘记⽐较 props):加条件判断,不然死循环
if(this.props.userID !== prevProps.userID) {
this.fetchData(this.props.userID);
}
}
如果组件实现了 getSnapshotBeforeUpdate() ⽣命周期, 则它的返回值将作为 componentDidUpdate() 的第三个参数 “snapshot” 参数传递。否则此参数将为 undefined。
如果 shouldComponentUpdate() 返回值为 false,则不会调⽤ componentDidUpdate()。
componentWillUnmount
componentWillUnmount() 会在组件卸载及销毁之前直接调用。例如,清除 timer,取消网络请求; componentWillUnmount() 中不应调用 setState(),因为该组件将永远不会重新渲染。
shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState)
不常用。根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发⽣变化组件都会重新渲染。 作为性能优化使⽤,返回false可以跳过re-render。
shouldComponentUpdate() 返回 false,不会调用 UNSAFE_componentWillUpdate()、render() 和 componentDidUpdate()。
getDerivedStateFromProps
不常用。是为了取代componentWillReceiveProps和componentWillUpdate设置的。根据props的变化改变state,它应该返回一个对象来更新state,如果返回null则不更新任何内容。
- 在使用此生命周期时,要注意把传入的prop值和之前传入的prop进行比较;
- 因为这个生命周期是静态方法,同时要保持它是纯函数,不要产生副作用。
static getDerivedStateFromProps(nextProps, prevState) {
const {type} = nextProps;
//当传入的type发生变化时,更新state
if(type !== prevState.type) {
return { type };
}
//否则对于state不进行任何操作
return null;
}
class ColorPicker extends React.Component {
state = {
color: '#000'
};
static getDerivedStateFromProps(props, state) {
if(props.color !== state.color) {
return { color: props.color }
}
return null;
}
//选择颜色方法
render() {
//显示颜色和选择颜色操作
setState({ color: XXX })
}
}
class ColorPicker extends React.Component {
state = {
color: '#000',
prevPropColor: ''//setState和forceUpdate也会触发此生命周期,会覆盖
}
static getDerivedStateFromProps(props, state) {
if(props.color !== state.prevPropColor) {
if(props.color !== state.prevPropColor) {
return {
color: props.color,
prevPropColor: props.color
}
}
return null;
}
//选择颜色方法
render() {
//显示颜色和选择颜色操作
}
}
}
getSnapshotBeforeUpdate
不常用。
getSnapshotBeforeUpdate(prevProps, prevState)
getSnapshotBeforeUpdate()在最近一次渲染输出(提交到DOM节点)之前调用;此生命周期方法的任何返回值将作为参数传递给componentDidUpdate()。
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>
)
}
}
static getDeriveStateFromError
不常用。配合Error boundAries使用,此生命周期会在后代组件抛出错误后被调用。它将抛出的错误作为参数,并返回一个值以更新state。
componentDidCatch
不常用。会在提交阶段被调用,因此允许执行副作用。它应该用于记录错误之类的情况。
语法:
componentDidCatch(error, info);
案例:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false;
}
}
static getDerivedStateFromError(error) {
//更新state
return {
hasError: true
}
}
componentDidCatch(error, info) {
logComponentStackToMyService(info.componentStack);
}
render() {
if(this.state.hasError) {
//可以渲染任何的自定义降级UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
UNSAFE_componentWillMount
不建议使用。UNSAFE_componentWillMount() 在挂载之前被调用;它在 render() 之前调⽤,因此在此方法中同步调⽤ setState() 不会⽣效;需要的话用componentDidMount替代。
UNSAFE_componentWillReceiveProps
不建议使用。UNSAFE_componentWillReceiveProps() 会在已挂载的组件接收新的 props 之前被调⽤; 如果你需要更新状态以响应 prop 更改(例如,重置它),你可以比较 this.props 和 nextProps 并在此 方法中使用 this.setState() 执行 state 转换。
UNSAFE_componentWillUpdate
不建议使用。
- 当组件收到新的 props 或 state 时,会在渲染之前调用 UNSAFE_componentWillUpdate();
- 使用此作为在更新发⽣之前执⾏准备更新的机会;
- 初始渲染不会调⽤此方法; 如果 shouldComponentUpdate() 返回 false,则不会调用 UNSAFE_componentWillUpdate()。
事件处理
语法格式
- 在JSX元素上添加事件,通过on*EventType这种内联方式添加,命名采用小驼峰式(camelCase)的形式而不是纯小写。(原生HTML中对DOM元素绑定事件,事件类型是小写的)
- 无需调用addEventListener进行事件监听,也无需考虑兼容性,React已经封装好了一些的事件类型属性。
- 使用JSX语法时需要传入一个函数作为事件处理函数,而不是一个字符串。
- 不能通过返回false的方式阻止默认行为。必须显式地使用preventDefault。
<!--DOM-->
<button onclick='activateLasers()'>
Activate Lasers
</button>
<!--React-->
<button onClick={activateLasers}>
Activate Lasers
</button>
//js
<form onsubmit="console.log('you clicked sumbit.'); return false">
<button type='submit'>submit</button>
</form>
//react:一般不需要使用addEventListener为已创建的DOM元素添加监听器
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('you clicked sumbit.');
}
return (
<form onSubmit={handleSubmit}>
<button type='submit'>submit</button>
</form>
)
}
绑定this
class Toggle extends React.Component {
constructor(prop) {
super(props);
this.state = {
isToggleOn: true;
}
//1.在构造函数中绑定
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
//2.直接使用箭头函数的写法
handleClick = () => {
this.setState(prevState => ({
isToggleOn: !prevState.isToggleOn
}));
}
render() {
return (
//this的值为undefined
<button onClick={this.handleClick}>
{this.state.isToggleOn ? 'on':'off'}
</button>
//3.直接在调用事件时使用bind绑定this
<button onClick={this.handleClick().bind(this)}>
{this.state.isToggleOn ? 'on':'off'}
</button>
//4.使用箭头函数在指定事件时绑定:但每次render都会创建不同的回调函数,如果该回调函数作为props传入子组件,每次子组件都要重新render
<button onClick={() => this.handleClick()}>
{this.state.isToggleOn ? 'on':'off'}
</button>
)
}
}
ReactDOM.render(
<Toggle/>,
document.getElementById('root')
);
接收参数
- 通过对象e会被作为第二个参数传递;
- 通过箭头函数的方式,事件对象必须显式进行传递;
- 通过Function.prototype.bind的方式,事件对象以及更多的参数将会被隐式的进行传递。
<button onClick={e => this.deleteRow(id, e)}>delete row</button>
<button onClick={this.deleteRow.bind(this, id)}>delete row</button>
条件渲染
if else 渲染
class LoginControl extends React.Component {
constructor(props) {
super(props);
this.handleLoginClick = this.handleLoginClick.bind(this);
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = {
isLoggedIn: false
}
}
handleLoginClick() {
this.setState({
isLoggedIn: true
})
}
handleLogoutClick() {
this.setState({
isLoggedIn: false
})
}
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick}/>
}else {
button = <LoginButton onClick={this.handleLoginClick}/>
}
return (
<div>
<Greeting isLoggedIn={isLoggedIn}/>
</div>
)
}
}
ReactDOM.render(
<LoginControl/>,
document.getElementById('root')
)
与运算符&&
function Mailbox(props) {
const unreadMessages = props.unreadMessage;
return (
<div>
<h1>hello</h1>
{
unreadMessages.length > 0 &&
<h2>
you have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
<Mailbox unreadMessages={messages} />,
document.getElementById('root')
);
// 返回false的表达式,会跳过元素,但会返回该表达式
render() {
const count = 0;
return (
<div>
{ count && <h1>Messages: {count}</h1>}
</div>
);
}
三元运算符
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}
如何阻止组件渲染
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
class Page extends React.Component {
constructor(props) {
super(props);
this.state = {showWarning: true};
this.handleToggleClick = this.handleToggleClick.bind(this);
}
handleToggleClick() {
this.setState(state => ({
showWarning: !state.showWarning
}));
}
render() {
return (
<div>
<WarningBanner warn={this.state.showWarning} />
<button onClick={this.handleToggleClick}>
{this.state.showWarning ? 'Hide' : 'Show'}
</button>
</div>
);
}
}
ReactDOM.render(
<Page />,
document.getElementById('root')
);
“&&”它跟 “?.” 的区别:
为假时前面返回false,后面返回undefined。
列表
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map(number => {
<li key={number.toString()}>
{number}
</li>
})
return (
<ul>{listItems}</ul>
)
}
const numbers = [1,2,3,4,5];
ReactDOM.render(
<NumberList numbers={numbers}/>,
document.getElementById('root')
);
// 若没有key,会warning a key should be provided for list items
// key可以帮助react diff,最好不⽤index作为key,会导致性能变差;
// 如果不指定显式的 key 值,默认使⽤索引⽤作为列表项⽬的 key 值;
key注意点
key要保留在map的遍历元素上。
// demo1
function ListItem(props) {
// 正确!这⾥不需要指定 key:
return <li>{props.value}</li>;
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在数组的上下⽂中被指定
<ListItem key={number.toString()} value={number} />
);
return (
<ul>
{listItems}
</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);
// demo2
function Blog(props) {
const sidebar = (
<ul>
{props.posts.map((post) =>
<li key={post.id}>
{post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) =>
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar}
<hr />
{content}
</div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
ReactDOM.render(
<Blog posts={posts} />,
document.getElementById('root')
);
// demo3
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
create-react-app
是一个官方支持的创建React单页应用程序的脚手架。它提供了一个零配置的现代化配置设置。
create-react-app创建React应用发生的流程:
- 执行create-react-app projectName命令。
- 先判断node版本是否小于10:
- 是:退出进程。
- 否:继续。
- init()
- 是否传入项目名projectName:
- 否:退出进程。
- 是:继续。
- 当前CRA版本是否小于latest版本:
- 是:退出进程。
- 否:继续。
- 检查projectName是否符合规范:
- 否:退出进程。
- 是:继续。
- 将
name: appName, version: '0.1.0'
写入package.json。 - 判断使用yarn还是npm:处理yarn/npm的相关逻辑。
- run()
- 选择模板:默认是cra-template。若发生错误,进入catch处理(删除相关的垃圾文件),退出进程。
- 确认需要安装的依赖:安装react和template相关依赖。若发生错误,进入catch处理(删除相关的垃圾文件),退出进程。
- install():若发生错误,进入catch处理(删除相关的垃圾文件),退出进程。
- executeNodeScript():通过
require('react-scripts/scripts/init.js
引入文件,进入react-app-scripts/init.js文件的处理阶段。 - init()
- 修改package.json:按照一些规则写入template的package.json。
- 拷贝模板文件
- 初始化git仓库
- 安装项目依赖(其中会判断是否需要TS)
- 删除模板文件
- Success
immutable及immer
immutable
解决的问题:
JavaScript中的对象一般是可变的(Mutable),因为使用了引用赋值,新的对象简单引用了原始对象,改变新的对象将影响到原始对象。
如foo={a:1}; bar=foo; bar.a=2
。此时foo.a也变成了2。
虽然这样做可以节约内存,但当应用复杂后,就造成了非常大的隐患,Mutable带来的优点变得得不偿失。
为了解决这个问题,一般的做法是使用shallowCopy(浅拷贝)或deepCopy(深拷贝)来避免被修改,但这样做造成了CPU和内存的浪费。
什么是immutable data?
- immutable data就是一旦创建,就不能再被更改的数据。
- 对immutable对象的任何修改或添加操作都会返回一个新的immutable对象。
- immutable实现的原理是Persistent Data Structure(持久化数据结构):即使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免deepCopy把所有节点都复制一遍带来的性能损耗,immutable使用了Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其他节点则进行共享。
immutable.js:
与 React 同期出现,但没有被默认放到 React ⼯具集里(React 提供了简化的 Helper)。
它内部实现了⼀套完整的 Persistent Data Structure,还有很多易用的数据类型。像 Collection、List、Map、Set、Record、Seq。有非常全⾯的map、filter、 groupBy、reduce``find函数式操作⽅法。同时 API 也尽量与 Object 或 Array 类似。
//原本写法
let foo = { a: {b:1} };
let bar = foo;
bar.a.b = 2;
console.log(foo.a.b); //2
console.log(foo === bar); //true
//使用immutable.js
import Immutable from 'immutable';
foo = Immutable.fromJS({a: {b:1}});
bar = foo.setIn(['a', 'b'], 2); //使用setIn赋值
console.log(foo.getIn(['a', 'b'], 2);//使用getIn取值
console.log(foo === bar); //false
immutable.js的优点:
- 降低了mutable带来的复杂性:
function touchAndLog(touchFn) {
let data = { key: 'value' };
touchFn(data);
console.log(data.key);
//因为不知道touchFn进行了什么操作,所以无法预料,但使用immutable肯定是value
}
- 节省内存:会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收。
import {Map} from 'immutable';
let a = Map({
select: 'users',
filter: Map({ name: 'cam' })
});
let b = a.set('select', 'people');
a === b; //false
a.get('filter') === b.get('filter'); //true
- Undo/Redo,Copy/Paste:因为每次数据都是不一样的,所有数据都可以存储在数组里,想回退到哪里就可以拿出对应数据。
immutable的缺点:
- 需要学习新的API
- 容易与原生对象混淆:虽然immutable.js尽量尝试把API设计的原生对象类似,有的时候还是很难区别到底是immutable对象还是原生对象,容易混淆操作。
- immutable中的Map和List虽对应原生Object和Array,但操作非常不同,如要用map.get(‘key’)而不是map.key,array.get(0)而不是array[0]。另外immutable每次修改都会返回新对象,也很容易忘记赋值。
- 当使用外部库时,一般需要使用原生对象,也很容易忘记转换。
immutable.is:
//两个immutable对象可以使用===来比较,这样是直接比较内存地址,性能最好
let map1 = Immutable.Map({a:1, b:1, c:1});
let map2 = Immutable.Map({a:1, b:1, c:1});
map1 === map2;
//为了直接比较对象的值,immutable.js提供了Immutable.is来做值比较
Immutable.is(map1, map2); //true
//Immutable.is比较的是两个对象hashCode或valueOf(对于JavaScript对象)。
//由于immutable内部使用了Trie数据结构来存储,只要两个对象hashCode相等,值就是一样的。
//这样的算法避免了深度遍历比较,性能非常好。
cursor:由于Immutable数据一般嵌套非常深,为了便于访问深层数据,Cursor提供了可以直接访问这个深层数据的引用。
import Immutable from 'immutable';
import Cursor from 'immutable/contrib/cursor';
let data = Immutable.fromJS({a: {b: {c:1}}});
//让cursor指向{c:1}
let cursor = Cursor.from(data, ['a', 'b'], newData => {
//当cursor或其子cursor执行update时调用
console.log(newData);
});
cursor.get('c'); //1
cursor = cursor.update('c', x=>x+1);
cursor.get('c'); //2
使用immutable.js优化react:
- React可以使用shouldComponentUpdate()进行性能优化,但它默认返回true,即始终会执行render()方法,然后做Virtual DOM比较,并得出是否需要做真实DOM更新。
- 可以在shouldComponentUpdate周期里执行deepCopy和deepCompare避免无意义的render,但deepFn也很耗时。
import { is } from 'immutable';
shouldComponentUpdate: (nextProps = {}, nextState = {}) => {
const thisProps = this.props || {}, this.thisState = this.state || {};
if(Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) {
return true;
}
for(const key in nextProps) {
if(!is(thisProps[key], nextProps[key])) {
return true;
}
}
for(const key in nextState) {
if(thisState[key] !== nextState[key] && !is(thisState[key], nextState[key])) {
return true;
}
}
return false;
}
immer
let currentState = {
p: {x:[2]}
}
//Q1
let o1 = currentState;
o1.p = 1;
o1.p.x = 1;
//Q2
fn(currentState);
function fn(o) {
o.p1 = 1;
return o;
}
//Q3
let o3 = {
...currentState
};
o3.p.x = 1;
//Q4
let o4 = currentState;
o4.p.x.push(1);
上述各种做法均会被修改。
如何解决引用类型对象被修改:
- 深度拷贝:成本较高,影响性能。
- ImmutableJS:非常棒的一个不可变数据结构的库,可解决上述问题,但跟Immer比起来,ImmutableJS有两个较大的不足:
- 需要使用者学习它的数据结构操作方式,没有Immer提供的使用原生对象的操作简单、易用;
- 它的操作结果需要通过toJS方法才能得到原生对象,这使得在操作一个对象时,时刻要注意操作的是原生对象还是Immutable的返回结果,稍不注意就会产生问题。
//使用immer解决上述问题
//Q1 Q3
import produce from 'immer';
let o1 = produce(currentState, draft => {
draft.p.x = 1;
});
//Q2
import produce from 'immer';
fn(currentState);
function fn(o) {
return produce(o, draft => {
draft.p1 = 1;
})
}
//Q4
import produce from 'immer';
let o4 = produce(currentState, draft => {
draft.p.x.push(1);
});
- currentState:被操作对象的最初状态。
- draftState:根据 currentState 生成的草稿状态,它是 currentState 的代理,对 draftState 所做的 任何修改都将被记录并⽤于⽣成 nextState 。在此过程中,currentState 将不受影响。
- nextState:根据 draftState 生成的最终状态。
- produce:用来生成 nextState 或 producer 的函数。
- producer:通过 produce 生成,用来生产 nextState。
- recipe:用来操作 draftState 的函数。
produce的使用:
- produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState:
- 对 draftState 的修改都会反应到 nextState;
- Immer 使⽤的结构是共享的,nextState 在结构上⼜与 currentState 共享未修改的部分; immer⽀持⾃动冻结:通过produce⽣产的nextState是被Object.freeze的。
// Q1
let nextState = produce(currentState, (draft) => {
})
currentState === nextState; // true
// Q2
let currentState = {
a: [],
p: {
x: 1
}
}
let nextState = produce(currentState, (draft) => {
draft.a.push(2);
})
currentState.a === nextState.a; // false
currentState.p === nextState.p; // true
const currentState = {
p: {
x: [2],
},
};
const nextState = produce(currentState, draftState => {
draftState.p.x.push(3);
});
console.log(nextState.p.x); // [2, 3]
nextState.p.x = 4;
console.log(nextState.p.x); // [2, 3]
nextState.p.x.push(5); // 报错
- produce(recipe: (draftState) => void | draftState, ?PatchListener)(currentState): nextState 利用高阶函数的特点,提前⽣成⼀个 producer。
let producer = produce((draft) => {
draft.x = 2
});
let nextState = producer(currentState);
使用immerse优化react:
// 定义state
state = {
members: [
{
name: 'ronffy',
age: 30
}
]
}
// 如何给member中第⼀个元素的age+1
// error
this.state.members[0].age++;
// setState
const { members } = this.state;
this.setState({
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1),
]
})
// 使⽤reducer
const reducer = (state, action) => {
switch (action.type) {
case 'ADD_AGE':
const { members } = state;
return {
...state,
members: [
{
...members[0],
age: members[0].age + 1,
},
...members.slice(1),
]
}
default:
return state
}
}
// 使⽤immer
this.setState(produce(draft => {
draft.members[0].age++;
}))
// 使⽤immer结合reduce
// 注意: produce 内的 recipe 回调函数的第2个参数与obj对象是指向同⼀块内存
let obj = {};
let producer = produce((draft, arg) => {
obj === arg; // true
});
let nextState = producer(currentState, obj);
const reducer = (state, action) => produce(state, draft => {
switch (action.type) {
case 'ADD_AGE':
draft.members[0].age++;
}
})
一些面试题
setState异步顺序
- setState只在合成事件和生命周期中是异步的,在原生事件和setTimeout中都是同步的。
- setState的异步:并不是说内部由异步代码实现,而是本身执行过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新1之前,导致合成事件和钩子函数中没法立马拿到更新后的值,形成了异步。
- 可以通过setState(paritialState, callback)中的第二个参数callback拿到更新后的结果。
- setState的批量更新优化也是建立在异步(合成事件、钩子函数)之上的,在原生事件和setTimeout中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并。
使用哪些生命周期可以完成性能优化
shouldComponentUpdate:判断是否每次state变化都要更新。
如何不使用ES6实现一个React组件?
使用create-react-class代替:
class Greeting extends React.Component {
render() {
return <h1>hello, {this.props.name}</h1>
}
}
//create-react-class
var createReactClass = require('create-react-class');
var Greeting = createReactClass({
render: function() {
return <h1>hello, {this.props.name}</h1>;
}
});
//1.默认属性声明
class Greeting extends React.Component {}
Greeting.defaultProps = {
name: 'Mary'
};
var Greeting = createReactClass({
getDefaultProps: function() {
return {
name: 'mary'
};
},
});
//2.初始化state
class SayHello extends React.Component {
constructor(props) {
super(props);
this.sttae = {
message: 'hello'
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
alert(this.state.message);
}
render() {
return (
<button onClick={this.handleClick}>
sayHello
</button>
)
}
}
//createReactClass创建的实例里,组件的方法都会自动绑定上
var SayHello = createReactClass({
getInitialState: function() {
return {
message: 'hello'
};
},
handleClick: function() {
alert(this.state.message);
},
render: function() {
return (
<button onClick={this.hanelClick}>sayHello</button>
)
}
})