React笔记
背景
挖坑看了vue后准备看一下react,是国外最火的前端框架.
介绍
截止目前,国际上使用量最大的前端框架.
2010年Facebook开源了PHP的语法扩展XHP,支持直接写html代码
2011年,员工Jordan Walke创建了FaxJS,是React的早期原型.
2011年用于newsfeed,2012年用于Instagram,2013年开源.
特点:
- 虚拟DOM的使用使其性能好
- 专注与使用JSX实现UI层面的功能,因此小而美.其他功能(路由,网络请求等)不在范围内.
- 概念简单,没有约定的根组件,没有指令,没有过滤器,数据绑定也是单向的,没有强调事件接口,非常自然.
- 逻辑简单,官方倡导使用不可变性简化开发,同时也提出了一套开发哲学,这是其他框架没有的.
初看感受
- 借助JSX来写html代码,一切特性我都已经知道,这才是真正的亲切
- 语法靠近html,同时修改了html的不合理之处
- 借助原生的js写法,可以顺利完成其他框架借助指令才能完成的功能.
- 让人怀念使用anko layout来写安卓界面的日子,用同一种语言就能做所有事情,而不必专门为xml配置一套IDE配置
- 使用不可变性质来完成数据的变更检测,历史回朔,想法挺好.
以前觉得数据的回朔是个问题,什么时候变更了什么地方,
现在在react中,检索代码可能会更加方便. - 但为什么不支持typescript呢,明明挺好的.还有rxjs呢.
- 可能是嫌弃rxjs的写法太不通俗
- react的官方教程简单易懂,信息文字比大.
其哲学更是给人一种王侯将相宁有种乎,没有天然约定的上下级关系的感觉.
这些都比vue不知道好到那里去了.
可惜在浅尝则止,许多想知道的用法并没有说.就像看了一段利维坦
.
也让人摸不着头脑,接下来该看些什么.
hello-world
需要先安装 create-react-app
包.
然后使用命令
1 | npx create-react-app my-app |
生成的文件结构如下
1 | . |
组件和引导
1 | // react的基础单元依然是组件.而且使用了ES6的class来定义. |
而index.html可以如下
1 |
|
组件基础
定义和使用
定义用class,使用直接用类名作selector,不知道比vue规范多少.
使用class定义
1 | class Welcome extends React.Component { |
或者使用function形式
1 | function Welcome(props) { |
使用
1 | const element = <Welcome name="Sara" />; |
render函数定义模板
就像上面说的那样,通常一个组件的render函数只会执行一次.
之所以数据能够更新,是因为使用了下面的state.
props来传递数据和方法
事实上从function形式的组件就能看出来,props是外部调用组件时指定的非常重要的参数.
相比其他框架,react的props的特点有:
- 简单,直接使用
this.props.xxx
即可,不需要专门声明为@Input
或props:
- 只读,能变化的那个叫state
另外react还提供了一个哲学:
当一个组件接收的外来数据过多,说明难以复用,需要考虑拆分成多个组件了.
state来保存组件中的状态
如果不想从外部传入变量,则需要在内部的一个地方保存,
本来直接以类属性存放是可以的,
但为了保持丰富的响应式特性,专门放在约定的 this.state
当中.
state本身是一个对象,里面会存放许多东西.
使用时只需要使用 this.state.xxx
, 比vue那种 .value
时而可省时而不可的规定省心多了.
不知道比vue规范多少.
-
state的更新
如果强行更新数据,可以,但不会重新渲染组件
1
this.state.comment = "hello";
需要使用
setState
来触发组件更新.1
this.setState({comment: "hello"});
-
更新时考虑异步
不要依赖state的值来更新state,因为state的更新可能是异步的,导致现在拿到的state不是真正想要拿到的state
1
2
3this.setState({
counter: this.state.counter + this.props.increment,
});非要实现这个功能的话,可以使用函数作为
setState
的参数.1
2
3this.setState((state, props) => ({
counter: state.counter + props.increment
})); -
state更新时可以只更新一部分
如果state里面有两个字段,更新时可以只更新一个,另外一个会不变的.
这样也会比较合理,防止产生太多的重复代码.
这种做法称为 浅合并 -
与props的关系
组件对外交流全用props,
这样在外部组件看来,该组件是无状态的,逻辑上更简单明确,控制起来更方便.如果两个组件需要交流信息,通常一个保持响应性的方法是
1
子组件props <= 父组件state
这样子组件的样子就会根据父组件的state改变而改变.
constructor用于处理props和声明state
参数和function形式的组件一样,是props.
里面必须使用 super(props)
.
然后可以定义一些state的内容.
生命周期函数
目前已知的生命周期函数,数量稀少,非常简单
- componentDidMount 加载后
- componentWillUnmount 卸载前
尽管在命名上,可能 mounted
, onDistroy
, preUnmount
等更直白一些,
但React这样的命名更加规范,整齐.
用户自定义函数
放在class里就好了
完整例子
1 | class Clock extends React.Component { |
JSX与模板基础
JSX基础
js的一种语法扩展.
-
语法像html,一定程度上做了兼容以让用户习惯.另外也修改了html里面许多不人性化的地方.变得更好
(class => className,tabindex => tabIndex) -
本质上还是js,可以直接给变量赋值为JSX元素.
1
const element = <h1>Hello World</h1>;
-
天然的防注入攻击,比html好许多.vue似乎提都没提.
有一些规定
- 标签没有内容可以随时用 “/>” 关闭标签
- 多行时最用用()括起来,以免受自动加分号的影响.
原理上:
最终会翻译成 React.createElement
函数.
元素渲染
-
JSX最终会转换成
createElement
函数,不使用JSX则需要使用较为麻烦的API. -
元素一经创建,不可更改.如果要更改页面上某个元素的UI,
唯一办法只有创建一个全新的元素,并用ReactDOM.render()
来重新绘制1
2
3
4
5
6
7
8
9
10
11function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(element, document.getElementById('root')); // 需要不断用render来重新绘制
}
setInterval(tick, 1000); -
在更新数据时,react会设法计算出修改的最小限度,尽量不修改整个页面.
变量绑定
字面量用引号,变量用大括号,没有话里胡哨的例外,不知道比vue规范多少
1 | const element = <div tabIndex="0"></div>; |
大括号中也可以使用各种表达式,xxx,2+2,f(x).
事件处理
-
基本使用
JSX仿照了html的原生方法,依然使用onclick等,
不同点在于- 对html语法做了改进,能够使用
onClick
等有大小写区分的方法. - 不能通过返回false来阻止默认行为,因为这样语意不明显.需要手动调用
preventDefault
1
2
3
4
5
6
7
8
9
10
11
12function ActionLink() {
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
return (
<a href="#" onClick={handleClick}>
Click me
</a>
);
}如代码所示,通常处理事件的函数会命名成
handleXXX
.但没有这种硬性规定.
由于是完全在js里写内容,处理事件时不需要考虑- 传的是字面量还是变量
- 如果是变量该如何绑定,使用什么指令,圆括号还是方括号.
- 对html语法做了改进,能够使用
-
class中this的问题
另外一个常用的场景: class形式的组件中,通常会遇到
this
的问题,
通常有两种解决方法-
使用bind
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 首先绑定一下
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({ // 这里的this才能有意义
isToggleOn: !state.isToggleOn
}));
}
render() {
return (
<button onClick={this.handleClick}> // 正常的使用,bind和不bind都不影响这里写成神什么样子
{this.state.isToggleOn ? 'ON' : 'OFF'}
</button>
);
}
} -
将函数定义为类的public成员(而不是一个单独定义的function),当然这需要使用箭头函数.
这样也产生了两种做法-
比较先进,使用时也优雅,但要求开启public class fields语法,create-react-app默认会启用
1
2
3
4
5
6
7
8
9
10
11
12
13class LoggingButton extends React.Component {
handleClick = () => { // handleClick是一个箭头函数,而不是function定义的函数
console.log('this is:', this);
}
render() {
return (
<button onClick={this.handleClick}> // 使用时不受任何影响,就正常使用
Click me
</button>
);
}
} -
如果没有public类成员,只能变通一下
1
2
3
4
5
6
7
8
9
10
11
12
13class LoggingButton extends React.Component {
handleClick() {
console.log('this is:', this);
}
render() {
return (
<button onClick={() => this.handleClick()}> // 虽然影响到了使用,但还是可以将就一下
Click me
</button>
);
}
}
-
-
-
传递参数
由于解决this上有两种方案,传递参数时也有了两种方案
1
2<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>值得注意的是事件参数
e
是一个合成事件,不必担心跨浏览器和跨平台的问题.
if
利用简单的if表达式可以写出众多逻辑
1 | function getGreeting(user) { |
如果嫌麻烦,还可以利用与运算符的特性
- 如果前面true,一定会返回后面的值
- 如果前面是false,后面会跳过.但这造成渲染的内容不正确,需要小小处理一下
1 | render() { |
这里的代码相当于,如果count不为0,则渲染成h1元素.
但如果count为0则会渲染一个朴素的0.
这种情况下可以使用条件表达式
1 | render() { |
条件表达式算得上是一个比较折衷的办法,也能代替上面的if
1 | render() { |
一个很常见的需求是,如果 msg.warn
为空,则不渲染这个div.
那么也可以通过null来告诉react不要渲染
1 | function WarningBanner(props) { |
循环
循环就不能用for来多次return了,可以用map.
这非常js也非常html,总之比 *ngFor
和 v-for
都要自然.
1 | function NumberList(props) { |
有循环就要有key.用来指定排序依据.
map可以使用的也不仅仅有值,还可以有index.
于是就能在循环中使用index作为key.
1 | const todoItems = todos.map((todo, index) => |
当然用index作为key很不好.而且是作为最后才用的一种手段.
通常都会用一些id什么的,最次也是toString.
1 | const todoItems = todos.map((todo) => |
最后注意key是传递给react的而没有传递给组件
意味着下面的Post组件,无法使用 this.props.key
获取内容.
1 | const content = posts.map((post) => |
如果想要获取,需要手动以其他名义传一次
1 | const content = posts.map((post) => |
表单
react提供了一个基本的表单,概念上非常简单:
以state为唯一数据源
- 自定义函数来响应用户的输入
- 响应用户输入时需要更新state
- 以state内容更新显示内容
- 由于以state为唯一内容,也推荐直接以定值作为表单的内容(输入定值会导致无法编辑)
以这种方式建立的组件也称为 受控组件
1 | class NameForm extends React.Component { |
但是这显然无法应对大量的表单元素,因为这意味着数量巨大的event handler.
因此也有着破例的非受控组件,以后再说.
另外JSX对于html的语法做了一些改进,让不同的元素,在使用时能够接近普通的input
-
textarea
1
2
3
4
5
6
7
8
9
10
11
12// 定义
this.state = {
value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'
};
// 使用
<textarea value={this.state.value} onChange={this.handleChange} />
// 更新,后略
handleChange(event) {
this.setState({value: event.target.value});
}这样textarea能够看起来像个input
-
select
原本的html里面,select是没有值的,获取被选取的值也不方便,靠一个selected标记
1
2
3
4
5
6<select>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option selected value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>但JSX做了修改,selected直接用value保存被选项的值即可
1
2
3
4
5
6<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>如果value为数组,则表示还可以多选
1
<select multiple={true} value={['B', 'C']} />
-
文件input
对于html的文件input
1
<input type="file" />
react暂时没有办法以受控组件的形式使用,以后再说
-
多个输入
当有多个输入时,为了让onChange能够更好地区分,可以使用
name
属性标记,
然后使用event.target.name
获取.
这样的一个好处在于,可以一定程度上减少重复写onChange的处理函数.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class Reservation extends React.Component {
// ...
handleInputChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name; // 获取name
this.setState({
[name]: value // 便于批量设置响应函数
});
}
render() {
return (
<form>
<label>
参与:
<input name="isGoing" 略 onChange={this.handleInputChange} /> // 使用name打标记
</label>
<br />
<label>
来宾人数:
<input name="numberOfGuests" 略 onChange={this.handleInputChange} />
</label>
</form>
);
}
}
组合而非继承
设想已有一个Dialog组件,想建立一个显示warning的Dialog窗口,可以有两种方法
- 新的WarningDialog组件继承Dialog组件,然后重写render函数,为了方便可以把具体渲染过程拆分到用户自定义的函数中
- 组合,使用组件时再具体定义其外观,然后插入到模板当中.相当于外框和子元素组合成了新的元素.
React偏好第二种方式,可能的原因是js没有特别规范的继承理念.
具体方法是:
- 在render函数中使用
props.children
表示组件使用时的子元素 - 在使用组件时直接在组件的子元素位置写内容
1 | function FancyBorder(props) { |
使用时
1 | function WelcomeDialog() { |
虽然使用了类似slot的概念,但即使没有 props.children
,
但react依然可以使用 props.xxx
来完成内容的注入和组合.
尤其是当希望使用多个slot的时候,就可以使用这种原始一些的方式来做.
1 | function SplitPane(props) { |
这种尽量不制造特殊变量(比如 this.slot
)的方式深得我心.
方法论和哲学
状态提升
开发中通常会形成形式上的父子组件结构.
有时为了两个子组件的通信,或者是对子组件的state进行统一管理.
就需要将数据从子组件的state拿到父组件的state当中,
而react希望props是只读的,因此原本在子组件之中操作数据的方法,也需要一起移动到父组件之中.
一个显而易见的结果是,子组件中不再使用 this.state.xxx
而使用 this.props.xxx
.
即使换成了 this.props.xxx
, 子组件依然有响应性.
子组件可以如下定义
1 | <input value={this.props.temprature} onChange={this.props.onTemperatureChange} /> |
调用时可以使用
1 | <TemperatureInput |
此时可以看到,父子组件约定了一个 onTemperatureChange
的自定义事件.
而比较方便的是,不需要再声明为 @Output
.
一般方法论
react提供了一种建立项目的方法论,称之为哲学.具体的步骤是
-
划分组件层级
- 有时可以按照美工的图层来直接划分组件
- 单一功能原则, 一个组件只提供一个功能
- UI结构可以和数据结构一一对应.
比如一个显示搜索结果的可以分成5部分
- 应用整体
- 接受用户输入
- 展示数据内容,负责数据过滤
- 展示标题
- 展示具体数据
-
创建一个没有数据的静态版本
- 简单软件通常使用自上而下
- 复杂软件通常需要自下而上(还要对下层的做好足够的测试)
-
确定state内容
- 也叫state的最小完整表示
- 需要参考 DRY原则 (Don’t Repeat Yourself)
- 确定数据是state还是props,通常的判断方法是
- 如果从外部传递而来,不是state
- 如果可以随着时间推移而不变,则不是state
- 如果可以由其他数据计算得出,则不是state,比如有整体数据,就不要再另外获得一次过滤后数据了,自行计算即可
-
确定state的存放位置
- 先找到一个使用该state数据的所有组件
- 这些所有组件如果有一个共同的父级,则state由父级保存
- 如果实在找不到,可以直接建立一个父级组件用来保存数据
-
添加反向数据流
- 即为没能直接操作state的组件,建立获取父级处理逻辑的通道.
- 同时也让下级的组件暴露一些函数接口,然父级方便将处理逻辑下放
一些其他问题
-
表单的验证是如何做的
需要使用外部库,有许多东西- Formik github 27k 官方教程似乎也推荐了一下,怕是给了广告费啊,明明和react哲学不接近
- react-hook-form 看起来更接近原生 20.2k
功能上大同小异,写法上萝卜青菜,不过我关心哪一个在哲学上和react更接近,
也不知道谁能回答这个问题 -
http请求是如何做的
官方推荐了好多- Axios
- jQuery AJAX 看着就老
- window.fetch 不支持取消请求,查看进度…
Axios虽然是基于promise的,但如果喜欢,可以使用rxjs进行封装,毕竟rxjs的作用不仅仅是进行网络请求.
目前默认就是rxjs风格的请求库恐怕只有angular自带的库了.那么有人做axios的rxjs包装工作吗
-
如何进行路由,路由守卫靠什么?
好在react的路由库只有一个,react-router -
如何测试
通常是使用Jest -
如何快速搭建UI
material UI依然可用,但就是不知道怎么用
为什么源码没有用TypeScript
参照知乎的说法,
- 曾经是超前的,只不过现在不是主流,何必要移动过来
- 就算不如主流的好,移动过来也有风险
- TypeScript会全部读到内存处理,而Fackbook的代码又多到不能全部读取到内存中.