声明式,而非直接操作 DOM 的命令式
组件化,而非 js 那样的模块化,提高复用性
一次学习,随处编写(React native)
单向数据流,而非 Vue 那样的双向数据流. 父组件可以向子组件传递数据, 子组件可以读取但不要直接对数据进行修改. 修改时 react 会报只读错误. 这样方便维护和 debug, 若渲染和期望不一致直接去父组件找问题就可以, 不需要挨个查子组件是否也有问题.
高效(虚拟 DOM,编码人员不直接操作 DOM。DOM Diff算法,最小化页面重绘)
视图层框架. 复杂项目需借助 redux 等数据层框架.
函数式编程. 方便测试, 方便拆分复杂函数等.
可以与其他框架并存. 只管理指定的 DOM 节点.
jsx = javascript + xml
虚拟 DOM 可直接用 javascript 表达式创建, 也可以用 jsx 表达式创建.
先运行 javascript, 再运行 babel。故不论先后顺序, babel 标签中的代码可以引用 javascript 标签中定义的变量. (javascript中只有两个作用域: 全局作用域和函数作用域. 对于 let 定义的变量有块作用域)
babel 会将 jsx 表达式转化为 javascript 表达式(React.createElement(component, props, …children))。
const element = {
type: "h1",
props: {
className: "greeting",
children: "hello, world!"
}
}
js 中可以使用 jsx 表达式;jsx 表达式中也可以嵌套 js 表达式(js 语句不可)。可层层嵌套。
比起 HTML,jsx 更像是 js。React DOM 使用驼峰式属性命名方式而非 HTML 属性名。
比如:jsx 中 class
应改为 className
(js 中 class
为关键字,jsx中有可能嵌套js,避免歧义)。
tabindex
应该为 tabIndex
。onclick
应改为onClick
。
jsx 中不允许有多个根标签。一个虚拟 DOM 里最顶级标签唯一。这也导致一个问题: 会有很多多余的根标签, 使得 DOM 树变得比较”深”.
要想解决上条带来的问题, 在 React 16 之后的版本可以使用 Fragment
签, 用它来做最外层的标签. Frangment
最后不会转换成任何标签, 只做占位符. 它还有一个短语法, 可以直接写成空标签<></>
. 它只允许key
这一种属性
import Fragment from 'react'
//...
return (
<Fragment>
<div><input/><button>submit</button></div>
<ul>
<li>Learning English</li>
<li>Learning React</li>
<ul>
</Fragment>
)
由于 jsx 表达式会被编译成 js,jsx 中可以使用 if-else 等 js 控制语句,函数定义等。
建议把 jsx 表达式用括号括起来,避免自动插入分号的陷阱。
<h1 style={ {opacity: 0.5, width: "30px"} }></h1>
onClick
来跳转(href
属性随便设置一个锚点, 不可为空, 不可为普通链接, 不可为javscript函数…).
<a href="#1" onClick=(this.func)>
(<Fragment>
{/* this is comment */}
<div></div>
<Fragment>)
render() {
return (<ul>
{this.getTodoItems()}
</ul>);
}
getTodoItems = () => {
return this.state.list.map((item, i) => (
<Fragment key={item.id}>
<TodoItem item={item} onClick={() => this.handleItemDelete(item.id)} />
</Fragment>
))
}
组件名必须以大写字母开头
// 第一种定义组件的方式,使用工厂函数。首字母大写
function MyComponent() {
console.log(this); // 工厂函数中的this是undefined
return <h1>我是工厂函数创建的组件</h1>
}
class MyComponent2 extends React.Component{
// 重写父类继承的render
render(){
console.log(this); // Mycomponent2组件类的实例对象
return <h1>我是ES6类组件创建的组件</h1>;
}
}
<MyComponent/>
。
ReactDOM.render(<MyComponent2/>, document.getElementById("example2"))
return (<div> <MyComponent1/> <MyComponent2/> </div>)
若组件通过类来定义, 使用react组件标签时会创建组件类的实例对象. 然后会调用里面的render()
方法来获取 jsx 表达式.
Hook
是 React 16.8 的新增特性. 它可以让你在不编写class
的情况下使用state
以及其他的 React 特性.state
是组件类的实例对象的最重要的属性,其值是一个对象。
组件被称为”状态机”,通过更新组件的state
来更新对应的页面显示(重新渲染组件)。state
更新,页面就自动更新。
为了使事件处理函数可以修改组件的state
,需要将该函数与组件类实例绑定起来。(事件处理函数不通过组件类实例调用,将事件处理函数赋值给其它变量后this
丢失,this
不指向组件类实例,为undefined
。render
中this
指向组件实例)。
javascript 中,实例的方法不会自动绑定this。(通过哪个实例调用,哪个实例就是this
,不通过实例调用就是undefined
)
结合2、3、4,为了使事件处理函数中与组件类实例绑定在一起,需要手动将函数与组件类实例绑定。
this
,函数内如果用到this
, 就会自动在函数定义时就向外层寻找this
来绑定, 而不是在调用的时候根据调用位置(执行环境)来决定this
。函数体内的this
对象就是定义时所在的对象,而不是调用时所在的对象。可通过匿名函数来定义事件处理函数,以实现函数与组件的绑定。
myMethod = () => {
console.log(this); // 自动绑定 this
this.setState({isHot: !this.state.isHot});
}
箭头函数中this
是组件的实例对象. 不可以直接用confirm
, 需要通过window.confirm
来使用: 当直接使用confirm
时, 会去this
查找, 不会去window
查找. alert
可以正常使用.
comments = [{}, {}, {}];
// ...
let comments = [...this.state.comments];
// do some change on comments.
this.setState({comments: comments}) // 是修改操作不是覆盖操作,属性有则修改,无则不动。
comments = [{}, {}, {}];
// ...
// 新版react16推荐下面的写法, 这样react会把短时间内连续多个setState合并.
this.setState((prevState) => {
const {comments} = prevState;
// do some change on comments.
return {comments: comments};
});
this.state.comments.push(comment); // 需要通过setState方法来更新
this.state.comments.push(comment);
this.setState({comments: this.state.comments}); // react提倡immutable, 不要直接对state进行修改, 不利于后面的优化.
let comments = this.state.comments;
this.setState({comments: comments}); // 可能出现不更新的情况, 尤其是当comments内容为对象时.
this
render(){
//render
let {isHot} = this.state;
return <h1 className="demo" onClick={this.changeWeather}>今天天气很{isHot ? "炎热" : "凉爽"}</h1>
}
render
调用的次数为 1+n, 1是页面初始化时的调用次数, n是状态state
更新的次数.
一个原则: 状态在哪儿, 更新状态的函数就在哪儿.
setState()
将组件状态的更改放入一个队列. 应该视setState()
为一个请求而不是立即执行的命令. 为了性能上的提升, React 可能会延迟更新, 然后一下子更新多个组件. #1#2
setState()
不总是立即更新组件, 它可能会晚些进行批处理或延迟更新, 这使得在调用完setState()
后立刻读取this.state
或是获取state对应的真实DOM会读取到更新前的数据.
为什么传入函数而不是传入对象: 二者在事件处理函数中都是异步的. 但传入函数允许编程人员在函数内安全的通过参数访问当前的state. 由于setState
调用是批处理的, 这使得你可以将更新串起来, 并且保证他们按序执行, 避免冲突. #
props
到state
. 这不但没有必要(你可以直接用this.props.color
), 还会带来bug(props
更新时, state
不会跟着更新). 只有当你有意用props
来初始化state且忽略props
的更新的时候才用, 此时应将color
命名为initialColor
或defaultColor
.
constructor(props) {
super(props);
// Don't do this!
this.state = { color: props.color };
}
官网各类型示例: https://reactjs.org/docs/typechecking-with-proptypes.html
state
用于保存一些用于动态变化的数据. props
用于保存组件标签的属性.
设想在信息管理中,需要展示很多人的信息.这些人都具有相同的一些属性(姓名, 性别, 年龄). 我们可以把每个人的信息的结构抽象为一个组件. 但是会遇到一个问题: 如何把把不同人的信息传递给组件, 使得可以使用同一个组件类创造出包含不同信息的实例. 在创建类时, 我们可以通过构造函数传参来产生具有相同属性, 不同属性值的实例. 类似的, 我们可以通过设置组件标签属性来向组件内传递信息. 组件标签属性的键值对就保存在props
中, 通过this.props.attrName
来调用.
PropTypes
用于限制传递标签属性的: 类型, 必要性.
import PropTypes from 'prop-types';
//...
class MyComponent extends Component{
static propTypes = {
gender: PropTypes.oneOf(['Male', 'Female']) // 二选一字符串, 非必需
age: PropTypes.instanceOf(Number), // 数值, 非必需
item: PropTypes.exact({ // 用于限制对对象, 比objectOf和shape更严格
id: PropTypes.string.isRequired,
name: PropTypes.oneOfType([PropTypes.string, PropTypes.string]), // 二选一类型
}),
onClick: PropTypes.func
}
//...
}
defaultProps
属性来设置默认值.
import PropTypes from 'prop-types';
//...
class MyComponent extends Component{
static defaultProps = {
name: "anonymous",
age:10
}
//...
}
propTypes
在运行阶段检查, typescript 在编译阶段检查.
现实开发中一般不限制props
的类型和必要性, 也不设置默认值. 组件要传递的属性太多时, 写起来比较麻烦…
ReactDOM.render(<Person name={persons.name} gender={person.gender} age={person.age}/>, document.getElementById("example1")); // 挨个传递
ReactDOM.render(<Person/>, document.getElementById("example2")); // 省略某些属性的传递
ReactDOM.render(<Person {...person}/>, document.getElementById("example3")); // 一起传展开传递: Object
ES8语法: 浅复制一个对象: personCopy = {...person}
注意,对象默认不可迭代, 不可直接通过扩展运算符展开(console.log(...person); /// error
).
...person
, 这是 react 和 babel 配合的结果. 在 js 脚本中这种写法会报错. 在 babel 脚本中, 标签之外这种写法不会报错, 也不会输出. 在标签内写可以一下子将属性批量传递过去.
comment={id:"", name:"", content:""}
return <Item {...comment}/>;
let {id, name, content} = this.props;
props
. function Person({name, age, sex}) {
return (
<ul>
<li>name</li>
<li>age</li>
<li>sex</li>
</ul>
)
}
key
属性不会放到props
中.
<Item key="213213"/>
react 中并不建议使用ref, 更推荐使用数据驱动的方式编写代码, 尽量不要直接操作 DOM.
ref
属性, 其值唯一, 用于引用. 组件类的实例就会将该标签的DOM元素保存到自己的refs
属性中. 这样就可以在方法中引用相应的标签的DOM元素了. 假设一个标签的id
和ref
值都为input
, 则:
console.log(document.getElementById("input1") === this.refs.input1); // true
render
方法中定义:
<input type="text" ref="input1">
this.refs.input1
render
方法中定义:
<input type="text" ref={(input) => {this.input1 = input}}/>
this.input1
令ref
为一个回调函数. 回调函数在渲染时由 React 自动调用, 其参数是该标签的 DOM 元素本身. 标签不是放在组件实例的refs
属性中, 而是直接作为组件实例的一个顶层的属性. ref 回调函数中, this
是组件类类实例.
// 创建 ref 容器. 专人专用
input1 = React.createRef();
```jsx
// 不是为 ref 赋值, 而是将 ref 所在的标签元素放入定义好的容器中
<input type="text" ref={this.input1}>
this.input1.current
将标签元素放入定义好的 ref 容器中.
event
, 代表当前事件(参考DOM
). 能用它处理, 就不要用ref
.
onClickHandler = (event) => {
alert(event.target.value);
}
<Button onClick={this.onClickHandler}>button</Button>
onClickHandler = (id)=>{
console.log(id);
}
render() {
let {id} = this.props;
return <Button onClick={()=>{this.onClickHandler(id)}}>button</Button>;
}
render() {
let {id} = this.props;
return <Button onClick={this.onClickHandler.bind(this, id)}>button</Button>;
}
onClick={this.onClickHandler}
,组件只会认为它是个名为onClick的prop
.state
等操作, 可以直接在render
中通过this.props
来接收state
或其它操作, 要在componentWillReiceiveProps中接收, 可以在里面利用props
修改state
并重新渲染组件.this
)后, 将该方法传递给子组件. 然后子组件再通过调用该方法修改父组件的属性(如state
).state
属性来保存. (state
发生变化后会重新渲染, 此时通过props
将state
中的数据传递过去即可)单向数据流, 父组件可以向子组件传递数据, 子组件可以读取但不要直接对数据进行修改. 修改时react会报只读错误. 这样方便维护和debug, 若渲染和期望不一致直接去父组件找问题就可以, 不需要挨个查子组件是否也有问题.
map
函数将js数组里的每一项转化成jsx表达式, 并且要为每一项设置一个key.
<ul className="list-group">
{comments.map( comment => <Item key={comment.id} {...comment}/>)}
</ul>
react只是视图层框架, 有时还需借助数据层框架. 对于复杂的项目, 两个组件之间的通信可能会很麻烦, 需要从一个组件沿着虚拟dom树一层一层向上找到公共的组件组件, 再向下传递到另一个组件. 为了解决这个问题, 就产生了flux, redux等工具.
受控组件和非受控组件的区别在于: 是否将html <input>
, html <textarea>
, html <select>
等值可变的标签的值,自动维护到state
中. 即在表单中, 受控组件可以在输入过程中自动收集数据, 非受控组件要在提交时手动读取数据.
设置受控组件:
onchange()
<input type="password" onChange=(this.onChangeHandler)/>
state
用于保存标签的值
state = {
password: ""
}
onChangeHandler
方法, 当标签的value
发生改变时, 存入state
中
onChangeHandler = (event)=>{
let password = event.target.value;
this.setState({password: password});
}
生命周期图示(点击函数可跳到介绍页面): https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
声明周期介绍: https://reactjs.org/docs/react-component.html#reference
组件生命周期: 挂载 -> 更新 -> 卸载
生命周期函数 == 生命周期钩子 == 生命周期回调函数 == 生命周期回调钩子(叫钩子是因为React底层有一个监视组件的监视者, 每当组件处于不同状态时, 就将这些特定函数钩到主线程来执行)
jsx ReactDOM.render(<MyComponent/>
constructor()
-> componentWillMount()
-> render()
-> compnentDidMount()
Props
(父组件重新 render 子组件): 从componentWillReceiveProps()
开始走流程setState()
: 从shouldComponentUpdate()
开始走流程forceUpdate()
: 从componentWillUpdate()
开始走流程componentWillReceiveProps()
-> shouldComponentUpdate()
-> componentWillUpdate()
-> render()
-> componentDidUpdate()
ReactDOM.unmountComponentAtNode()
componentWillUnmount()
getDerivedStateFromProps()
取代componentWillMount()
jsx ReactDOM.render(<MyComponent/>
constructor()
-> getDerivedStateFromProps()
-> render()
-> compnentDidMount()
getDerivedStateFromProps()
取代componentWillReceiveProps()
和componentWillUpdate
: 挂载和更新都需要通过这个钩子.shouldComponentUpdate()
放到getDerivedStateFromProps()
与render()
之间: 先修改状态, 再决定是否需要更新.componentDidUpdate()
前增加getSnapshotBeforeUpdate()
props
(父组件重新 render)和setState()
: 走完整个流程.forceUpdate()
: 跳过shouldComponentUpdate()
getDerivedStateFromProps
(-> shouldComponentUpdate()
) -> render()
-> getSnapshotBeforeUpdate()
-> componentDidUpdate()
ReactDOM.unmountComponentAtNode()
componentWillUnmount()
旧版生命周期函数
生命周期函数 | 描述 |
---|---|
componentWillMount() |
即将过时. 已重命名为UNSAFE_componentWillMount . 组件将要被挂载,(初次渲染) |
componentDidMount() |
组件挂载完毕. 只执行一次. 一般在此方法里开定时器, 发送Ajax请求等. |
shouldComponentUpdate(props, state): boolean |
组件更新前判断是否允许执行更新. 返回一个布尔值用来说明是否更新. |
componentWillUpdate(props, state) |
即将过时. 已重命名为UNSAFE_componentWillUpdate . 组件将要被更新(重新渲染) |
componentDidUpdate(props, state[, value]) |
组件更新完毕. 在旧版生命周期中使用时, 无value 参数. 在新版中使用时, value 是getSnapshotBeforeUpdate 方法的返回值. |
componentWillUnmount() |
一般在此方法里做一些收尾和收集工作. |
componentWillReceiveProps(props) |
即将过时. 已重命名为UNSAFE_comopnentWillReceiveProps . 组件将要接收新的参数. 挂载时不会触发. 从第二次传参开始被调用. |
新版生命周期函数
生命周期函数 | 描述 | this.state 与this.props |
---|---|---|
constructor(props) |
只用于两种目的: (1)通过直接赋值来初始化 this.state (2)为事件处理函数绑定 this |
/ |
render() |
唯一一个必须的方法. 返回值可能是很多种类型. 不应该在里面修改state |
指向新的 |
static getDerivedStateProps(props, state) |
取代了旧版中的componentWillMount , componentWillReceiveProps 以及 componentWillUpdate . 需要设为静态方法. 有两个参数和返回值. 返回值是新的state , 会作为下一步setState(state) 的参数. |
静态方法没有this |
componentDidMount() |
在组件被挂载到真实DOM树之后立刻被调用 | 指向初始state 和props |
sholdComponentUpdate(nextProps, nextState) |
props/state的改变不一定需要引起重新渲染时使用. (1) 只应该用于性能优化. (2) 用之前先考虑是否可以用PureComponent的默认实现. (3) 不推荐在里面使用JSON.stringify或深度相等判断, 十分影响性能 (4) 返回 false 不会阻止子组件的重新渲染 |
指向旧的 |
getSnapshotBeforeUpdate(prevProps, prevState, snapshot): any |
新增的放置在render 与comopnentDidUpdate 之间的钩子. 需要返回一个任意类型的值. 必须与componentDidUpdate(props, state, value) 一起使用, 返回值会作为其value 参数. |
指向新的 |
componentDidUpdate(prevProps, prevState, snapshot) |
在真实DOM树更新后被立刻调用. | 指向新的 |
componentWillUnmount() |
组件被卸载销毁后立刻执行, 在这里面做必要的清理工作 | / |
componentWillxxx: 组件将要xxx. componentDidxxx: 组件做完了xxx. shouldComponentxxx: 组件是否应该xxx. 除了最后删除组件的钩子, 其它三个 componentWillxxx 即将过时.
同render()
, 旧版的生命周期函数不需要自己绑定 this
. getDerivedStateProps()
需要设为静态方法.
state
与页面的更新
state
更新, 页面不更新:shouldComponentUpdate()
返回false
.getDerivedStateProps()
返回空对象.state
不更新, 页面更新:this.forceUpdate()
或更新props
(父组件重新render).React.PureComponent
: React.PureComponent
与React.Component
类似. 它们之间的区别在于React.Component并
未实现shouldComponentUpdate()
, 但是React.PureComponent
却通过浅层的prop和状态比较来实现. 如果您的React组件的render()
函数在相同的props
和state
下呈现相同的结果, 则在某些情况下, 您可以使用React.PureComponent
来提高性能. 此外, React.PureComponent
的shouldComponentUpdate()
会跳过整个组件子树的prop更新.请确保所有子组件也都是”纯”的. #
getDerivedStateFromProps(props, state)
#: 只为一种目的而存在: 允许一个组件根据Props的变化来更新内部状态. 尽量不要使用它, 如果使用先确认一下自己是否知道一些更简单的替代方案
为什么要在componentDidUpdate(prevProps, prevState, snapshot)
之前添加一个getSnapshotBeforeUpdate(prevProps, prevState): any
, 并且传递一个value
?
使得组件能在发生更改之前从 DOM 中捕获一些信息(例如:滚动位置). 此生命周期的任何返回值将作为参数传递给componentDidUpdate()
在只与更新相关的生命周期函数中, 在render前的会接受新的props
和state
, this.props
和this.state
指向旧的. 在render后的与之相反, 且会多出一个snapshot
参数.
componentDidMount
里. 虽然constructor
也只执行一次, 但最好不要将它用于初始化state和为监听函数绑定实例对象之外的功能. 另外由于ajax是异步的, 不能在constructor里的ajax里直接为this.state
赋值.setState()
/forceUpdate
/new props虚拟 DOM + DOM Diff 算法: 最小化页面重绘.
用js生成生成一个js对象(虚拟DOM), 代价是很小的; 生成一个 DOM 元素, 需要调用 web application 级别的API, 代价是非常大的. 生成虚拟 DOM 的时间 + DOM diff + 更新差异 DOM 节点的时间, 大多情况下小于直接更新整个DOM树的时间.
重绘最小粒度为标签(DOM 节点).
若父虚拟 DOM 节点发生变化, 子节点未发生变化, 则子虚拟 DOM 节点会继续重复使用(父节点变化, 子节点的render
也会被调用, 但render
后生成的是虚拟 DOM, 真实页面上子节点的DOM不一定会改变)。
虚拟 DOM 的比对是一层一层的同层比对, 一旦发现一个节点不同, 就会替换以该节点为根节点的子真实 DOM 树的所有节点.
React Diff 算法依赖于 key.
react 与 vue 里的 key 一样.
注意: 如果不存在 添加/删除/排序 操作, 仅用于渲染列表用于展示, 使用 index 作为 key 没有问题.
后端对于列表数据里的每一项都应设一个唯一标识, 用来作为 key. 若没有就去找后端沟通.
虚拟 DOM 树以 key 为索引来构造. 假设存在一个 key 为 1 的虚拟 DOM 节点, 并有若干子节点. 若将它的 key 改为其他值, 并将另一个虚拟节点的 key 改为 1. 那么原来的那些子真实节点会原封不动的挂载到新的 key 为 1 的虚拟节点对应的真实节点上.
yarn add uuid
import 'uuid' from 'uuid'
let id = uuid();