复杂的类组件
那么还是用我们的 +1 功能作为 demo
demo
大家都知道 js 有个 class 关键字,其实就是基于原型链的语法糖
那么类组件的声明就很明显了
我们先写一个展示 n 的功能,后面再来填充
1 | class App extends React.Component { |
上述代码就是一个 react 类组件的基本写法
基本概念
借助上述 demo,我们先来了解一下 react 类组件的基本概念
参数
类组件主要有两种参数
- props,来源于父级的传入
- state,绑定在 this 上,而不是原型
props
例如,在 index.js
中,对 App 传入参数
1 | <App name="myReactApp" /> |
然后在构造函数中获得 props,并通过父类构造函数初始化 props,之后就可以在 App 中访问 this.props.name
来取得这个参数值
一般不建议在子组件中修改 props,因为给出 props 的值的一定是父组件,所以 props 是属于父组件的,应当只能由父组件来修改,便于代码维护
state
state 是当前 App 的私有变量,不在原型上
一般在构造函数中,通过
1 | this.state = { |
来赋予一个对象
要取值时,也是一样通过 this.state.xxx
来取值
渲染
类组件通过 render 函数来进行渲染,return 一段 JSX 就行
生命周期
类比于 vue 的生命周期,react 也有自己的生命周期
首先是最重要的函数,所有类组件都必须实现的,render 函数!
render 函数负责将组件渲染到页面上
其余常用生命周期钩子函数的对照表如下
react | vue |
---|---|
constructor | created |
componentDidMount | mounted |
componentDidUpdate | updated |
componentWillUnmount | beforeDestroy |
不常用的也给出参考
react | vue |
---|---|
shouldComponentUpdate | beforeUpdate |
UNSAFE_componentWillMount | beforeMount |
UNSAFE_componentWillUpdate | beforeUpdate |
可以令 shouldComponentUpdate
函数返回 false 来阻止 render 的执行
更多请看 React.Component 生命周期
魔改
现在修改 onClick,使其具有 +1 的功能
首先想到,写一个 add 方法,然后 +1 行不行
试试看
1 | import React from 'react'; |
运行一下,发现报错,说找不到 state
为什么呢?因为此处事件绑定的机制,实际上是这样的
假设我们令 onClick=fn,其中 fn 是一个函数
那么经过 webpack 转换后,实际上变成了 onclick = fn.call(window),这时候 add 里的 this 指针就变成了指向全局的了!
那怎么办?容易想到一开始就绑定一个 this
1 | <button onClick={this.add.bind(this)}>+1</button> |
运行,发现页面没有变化
emmm,让我们加个输出看看
1 | add() { |
运行,发现输出是正常的,+1 正常执行了
那么应该就是要我们手动 render 一下
但是用前文说过的那种,也太麻烦了,有没有什么好办法呢
答案是有,react 内置了另一种通知页面更新的办法,那就是 setState
1 | add() { |
这样就可以修改 state 中的内容,并且自动通知页面进行更新
现在得到了一个简陋的 +1 demo
1 | import React from 'react'; |
接下来一起优化它吧!
优化
this
首先就是这个 onClick,用 bind 也太丑了,有没有好办法呢
刚才说到,实际绑定的 this 会变成全局的 this,所以我们才用 bind 指定了当前对象的 this
那么显然还有一种方法——不支持 this 的箭头函数!
但是现在的 add 写法,是挂载到原型上的,不能写成 add: ()=>{}
这个样子的箭头函数
所以要挂载到对象上,那么容易想到在构造函数中挂载
1 | constructor(props) { |
现在这样,在 button 中就可以不使用 bind 了
1 | <button onClick={this.add}>+1</button> |
但是这样的话,构造函数未免太长了
于是 ECMA 说好,我再给你个糖,用等号吧
1 | import React from 'react'; |
该写法完全等价于将 add 在构造函数中声明,都是挂载到对象上的
现在就有了一个看起来很完美的 +1 demo!
对象不变性
但是只是看起来完美,实际上还不够完美
假如我们要后退到某一个历史呢?在这个例子中可以直接 -1,但是要是逻辑复杂了,显然不能直接得到历史结果
那么我们应该尽量使得每次 state 的变化都被保存,便于用户在后退操作中引用
容易想到这样的写法来保存 state
1 | add = () => { |
显然,我们用一个新的 state 取代了旧的 state,并且在发生变更前进行了存档
异步更新
现在我们想要看看保存下来的新值是什么
1 | add = () => { |
发现两次输出竟然是一样的,都是输出了旧的 state!
可是页面明显发生更新了啊,怎么回事
这是因为 setState 的更新是异步的,类似于 setTimeout
所幸,setState 可以接收第二个参数,作为其成功回调
1 | add = () => { |
现在,打印就正常了
追加更新
+1 demo 不够用啊,复杂一点吧
+2
让我们做一个 +2 功能
容易想到直接 +2 就行,但是我偏要执行两次 +1!
1 | add = () => { |
一运行,发现不行,实际上只加了 1!
原因如上文中提到的异步,实际上两次取 this.state.n
都取到了相同的值,所以最后等效于 +1
不过 react 已经考虑到了这个问题——setState 可以接受一个函数作为参数!
setState 如果发现传入的是一个函数,那么会向该函数抛出两个参数,按顺序分别是 this.state
和 this.props
那么容易得到如下代码
1 | add = () => { |
现在就可以正常 +2 了
注意,此处不可以写作
1 | () => ({ n: this.state.n + 1}) |
因为 react 会缓冲更新,此时访问 this.state
并不能第一时间获取最新值
多参数
state 只有一个参数也太惨了,我想多搞点,就多一个 m 吧
假设点击 button 会让 n+=1 且 m 不变
可以得到如下代码
1 | class App extends React.Component { |
这样就实现了上述功能
但是感觉不对啊,setState 不应该是完全替换了 state 吗,可是新的 state 中并没有声明 m 啊,为什么 m 不会变成 undefined 呢
其实 react 在这里会自动比对新旧两个 state 的区别,然后只落实有提及部分,没提及的部分会保持原样
嵌套对象
那我想更新一个嵌套对象,怎么办呢
假设现在 state 中有一个 obj 和一个 n,obj 中有 name 和 age,点击 button 会增加 age 的值
容易得到如下代码
1 | class App extends React.Component { |
点击 button 后,发现 name 字段丢失了!
为什么呢?明明 n 字段没有丢失,不是说好 react 会自动对比,保留旧的吗
其实这里的自动保留,只是一个浅拷贝,只能作用于第一层,不能向下深入
这时候一般我们要使用 spread 语法手动展开对象
1 | add = () => { |
这样,在 age 变动的时候就不会导致 name 被置空了
忽悠渲染器
不行,渲染器这么智能,我就想让它不智能一把
我如果在 add 的时候,先 +1 再 -1 呢?渲染器这么聪明,应该知道不用渲染吧
1 | add = () => { |
答案是不行……渲染器虽然会合并渲染,只渲染一次,但还是发生了渲染
这时候一般有两种办法
可以通过生命周期中的 shouldComponentUpdate
函数,来判断前后 state 是否相等,若相等则不更新
但是太麻烦了,所以就有了懒人方案——改继承!
改成
1 | class App extends React.PureComponent |
即可实现自动判断的效果
PureComponent 会在 render 之前对比新 state 和旧 state 的每一个 key,以及新 props 和旧 props 的每一个 key
如果所有 key 的值全都一样,就不会 render
如果有任何一个 key 的值不同,就会 render
但是只不过只一个浅对比,深层的话,还是要手写了2333
最终方案
于是我们得到了 +1 demo 的最终解决方案
1 | import React from 'react'; |
鼓掌!!
感谢阅读