本文是我为最近一个月 React 学习和实践的技术总结,文章的前半部分主要是总结 React 开发使用的技术栈和基础知识,在文章写到后半部分,我深入地思考了 React 框架的特性,得出了 React 是一门融合了 函数式编程思想 的本质上是 面向对象 的框架 这一结论.
语言:JSX(一种 JavaScript 的语法扩展 JavaScript XML),Less(CSS 的语法扩展),H5
IDE:VSCode
特点:组件,元素
文件后缀: .jsx .less (并不需要.html)
vscode 插件:
�
技术栈
React提出了以组件化的形式重新构建页面内容,所谓组件就是将页面的内容按特征分块,然后将特定块中的HTML、CSS、JS封装在一起,最后用组件来构建页面内容。然而React不是一个完整的MVC,它只是V,也就是视图表现,采用React搭建完整的页面还需要其它技术支持:npm、ES6、JSX、bable、less、Router、Redux、WebPack
NPM(node package manager)
通常称为node包管理器。顾名思义,它的主要功能就是管理node包,包括:安装、卸载、更新、查看、搜索、发布等。
npm的背后,是基于couchdb的一个数据库,详细记录了每个包的信息,包括作者、版本、依赖、授权信息等。它的一个很重要的作用就是:将开发者从繁琐的包管理工作(版本、依赖等)中解放出来,更加专注于功能的开发。
Node.js 就是运行在服务端的 JavaScript,是一个基于Chrome JavaScript 运行时建立的一个平台,赋予了JavaScript很多面向对象语言的特性
Node.js是一个事件驱动I/O服务端JavaScript环境,基于Google的V8引擎,V8引擎执行Javascript的速度非常快,性能非常好。
ES6,bable
ES6即ECMAScript 2015,是 JavaScript 语言的下一代标准,它和JavaScript的关系就是,JS是ES的一种实现。
ES6中有很多新的特性,比如箭头函数、匹配赋值等,使的其更像是一门面向对象的语言。
目前主流的浏览器还不支持ES6,所以需要通过转码器将ES6转为ES5
bable就是目前主流的转码器,同时也将 JSX 解析,当遇到“</>”,JSX就当HTML解析,遇到“{}”就当JavaScript解析。
Less (Leaner Style Sheets 的缩写)
是一门向后兼容的 CSS 扩展语言。他丰富了css选择器的种类,使程序员能够在css中定义变量,函数,减少了css编写的代码量。同时他还支持选择器之间的嵌套,这样css更有逻辑,更易维护,也更符合React组件化的思想,我们可以将一个React组件显得选择器封装在一个嵌套层中,这样逻辑更加清晰。
Router
传统的web前端可以通过HTML文件的路径来跳转,而React中都是组件,那么就不能通过传统的方式来跳转了,这时候Router就来了,他就是来解决这个问题的。
React Router 是一个基于 [React]之上的强大路由库,它可以让你向应用中快速地添加视图和数据流,同时保持页面与 URL 间的同步。
Router还提供了<Link/>等组件方便使用
Ant design: UI 库
文件结构:
src:项目结构最主要的目录
index.js:当前目录的入口文件
运行 react 的命令:npm start
在编写代码之前:
import React from 'react'
import ReactDOM from 'react-dom'
其中,react.js 是 React 的核心库,react-dom.js 是提供与 DOM 相关的功能
JSX:
React 并不强制使用 JSX,但使用 JSX 会在视觉上有辅助作用
JSX 可以直接声明一个元素为一个变量
const element = <h1>Hello, {name}</h1>;
JSX 可以在标签内使用大括号{}来嵌入一个 JS 变量或表达式
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
JSX 可以在 if 语句和 for 循环的代码块中使用,也可作为参数和返回值来传递
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX 中可以使用引号(单引号或双引号)来将属性指定为字符串字面量
const element = <div tabIndex="0"></div>;
也可以使用大括号,在属性中插入一个 JS 表达式
const element = <img src={user.avatarUrl}></img>;
JSX 可以使用 /> 来闭合标签
const element = <img src={user.avatarUrl} />;
JSX 标签里可以包含多个子元素
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX 语法本质上是一个名为 React.createElement() 的函数
我们并不需要关心其本来面目是什么样子的
基于babel/babel-loader: 把jsx语法编译为react.create-Element这种模式
create-Element 至少有两个参数,没有上限
第一个: 标签名(字符串)
第二个:属性(没有给元素设置null)
其他:当前元素的所有子元素内容
执行create-Element 把传递的参数最后处理成为一个对象
元素渲染
使用 ReactDOM.render(元素名,根 DOM 节点) 来实现元素的渲染
ReactDOM.render(<组件名 />, document.getElementById('root'));
更新 UI 唯一的方式是创建一个全新的元素,并将其传入 ReactDOM.render().
而在实践中,大多数 React 应用只会调用一次 ReactDOM.render(),状态更新是通过改变 State 来实现的.
组件
*每一个 React 组件即是一个 ES6 对象,对象内部可以有属性和方法,从而更好的操作组件内部的页面元素。React为了统一检测变化,为每个React组件定义了两个固定属性,prop和state,prop是React父组件在使用React组件时所传入的属性,是由外部赋值的,而state只能通过内部的构造函数和setState方法赋值和修改。
组件分为函数组件与 class 组件
函数组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class 组件:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
组件可以接收参数,使用 this.props.参数名 来访问该参数
const element = <Welcome name="Sara" />;
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
props 是只读的,若要改变 props 的值,需要通过状态提升和接口回调改变父组件中的值.
每个组件对象都会有props(properties的简写)属性
组件标签的所有属性都保存在props中
对 props 中的属性值进行类型限制和必要性限制
Person.propTypes = {
name: React.PropTypes.string.isRequired,
age: React.PropTypes.number.isRequired
}
组件名称必须以大写字母开头,React 会将以小写字母开头的组件视为原生 DOM 标签
*虽然组件可以有两种定义方式,但在后期实践中,为了保证代码的一致性,我通常使用 class 组件
1.class 组件可以指定 state.
2.class 组件可以为事件回调函数绑定 this.
3.定义为 class 更符合封装组件的思想,而 function 容易使人产生误解
组合组件
组件可以互相嵌套
<组件 1>
<组件 2 />
</ 组件 1>
提取组件
当组件代码被重复书写时,就应当考虑提取组件.
组件可以理解为移动端的 view,我通常将提取一个组件形容为"抽象成一个类",这和前端的代码习惯是不同的.
State
State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件。
只有 class 组件可以使用 State
可以被理解为私有变量
state是组件对象最重要的属性, 值是对象(可以包含多个数据)
组件被称为"状态机", 通过更新组件的state来更新对应的页面显示(重新渲染组件)
在 constructor(props) (class 构造函数)中可以为 state 赋初始值
class Clock extends React.Component { //只有 class 组件可以使用 State
constructor(props) { //这是一个 class 构造函数
super(props); //将 props 传递到父类的构造函数中:
this.state = {属性名: 初始值}; //给 state 赋初始值
}
render() {
return (
<div>{this.state.属性名}</div>
);
}
}
使用组件时的注意事项:
不要直接修改 state,而应当使用 this.setState({属性名:属性值})来修改 state
this.state.comment = 'Hello';//这种修改方式是无效的
构造函数是唯一可以采用上述语句给 state 赋值的地方
state 的更新可能是异步的,若要立即更新state,可以使用函数
//箭头函数
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
或
//普通函数
this.setState(function(state, props) {
return {
counter: state.counter + props.increment
};
});
setState()方法可以单独更新每一个变量,而不会影响其他变量
数据是向下流动的,父组件的 state 可以作为 props 传递给子组件,此特性也被称为单向数据流
生命周期方法
componentDidMount() {
//组件已被挂载
}
componentWillUnmount() {
组件即将被卸载
}
生命周期
生命周期详述
- 组件的三个生命周期状态:
- Mount:插入真实 DOM
- Update:被重新渲染
- Unmount:被移出真实 DOM
- React 为每个状态都提供了勾子(hook)函数
- componentWillMount()
- componentDidMount()
- componentWillUpdate()
- componentDidUpdate()
- componentWillUnmount()
- 生命周期流程:
a. 第一次初始化渲染显示: ReactDOM.render()
- constructor(): 创建对象初始化state
- componentWillMount() : 将要插入回调
- render() : 用于插入虚拟DOM回调
- componentDidMount() : 已经插入回调
b. 每次更新state: this.setSate()
- componentWillUpdate() : 将要更新回调
- render() : 更新(重新渲染)
- componentDidUpdate() : 已经更新回调
c. 移除组件: ReactDOM.unmountComponentAtNode(containerDom)
- componentWillUnmount() : 组件将要被移除回调
重要的勾子
render(): 初始化渲染或更新渲染调用
componentDidMount(): 开启监听, 发送ajax请求
componentWillUnmount(): 做一些收尾工作, 如: 清理定时器
事件处理
React 事件的命名采用小驼峰式
使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
//html
<button onclick="activateLasers()" />
//React
<button onClick={activateLasers} />
在 React 中,想要阻止默认事件的行为,需要调用 preventDefault 方法.
function handleClick(e) {
e.preventDefault();
console.log('The link was clicked.');
}
在回调函数中想要使用 this,必须在构造函数中为该回调函数绑定 this
// 为了在回调中使用 this
,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this);
可以通过以下方法给回调函数传参,e 会作为第二个参数隐式传递
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
通过 event.target 得到发生事件的DOM元素对象
<input onFocus={this.handleClick}/>
handleFocus(event) {
event.target //返回input对象
}
条件渲染
可以通过 if 语句结合 return 组件 来决定显示哪个组件
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} />
{button}
</div>
);
}
}
也可以通过 && (逻辑与运算符)的短路特性来决定是否渲染组件
function Mailbox(props) {
return (
<div>
{条件 && 组件}
</div>
);
}
之所以能这样做,是因为在 JavaScript 中,true && expression 总是会返回 expression, 而 false && expression 总是会返回 false。
也可以通过三目运算符来决定渲染哪个组件
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{条件 ? (<组件 1 />) : (<组件 2 />)}
</div>
);
}
可以通过让 rander() 方法直接返回 null 来阻止组件渲染
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
在组件的 render 方法中返回 null 并不会影响组件的生命周期。
componentDidUpdate 依然会被调用。
key
key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
在一个列表中,如果没有传递 key,将会看到一个警告 a key should be provided for list items,意思是当你创建一个元素时,必须包括一个特殊的 key 属性。
一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用来自数据 id 来作为元素的 key
当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key:
如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。
总之,一个好的经验法则是:在 map() 方法中的元素需要设置 key 属性。
在同级元素中,key 必须是独一无二的.
在 JSX 中可以直接嵌入 map()
const listItems = numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
);
其返回值为一个包含条件函数中元素组成的数组
//可以直接在大括号中嵌入
function NumberList(props) {
const numbers = props.numbers;
return (
<ul>
{numbers.map((number) =>
<ListItem key={number.toString()}
value={number} />
)}
</ul>
);
}
*这么做有时可以使你的代码更清晰,但有时这种风格也会被滥用。就像在 JavaScript 中一样,何时需要为了可读性提取出一个变量,这完全取决于你。但请记住,如果一个 map()
嵌套了太多层级,那可能就是你提取组件的一个好时机。
*表单元素没用过
表单&受控组件
在 React 里,HTML 表单元素的工作方式和其他的 DOM 元素有些不同,这是因为表单元素通常会保持一些内部的 state。
使用受控组件来实现表单元素的效果,理解起来非常简单:
设置表单元素的 value={this.state.value},并监听元素的 onChange 方法,改变 state
当 value 设置为 undefined 或者 null 的时候,受控组件是可被编辑的,避免这种情况可以给 state 赋一个初值.
状态提升
通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。
步骤:
1.父组件定义一个 state 的属性,将该属性作为 props 传递至子组件中
2.子组件中使用 this.props 来访问该属性
3.当父组件中的属性产生变化时,子组件会相应地被更新
4.当一个属性需要由子组件传递给父组件时,需要采用回调
5.父组件定义并实现一个回调方法,在该方法中接收参数改变属性,将该方法传递给子组件
6.当该状态需要改变时,子组件调用该方法,并传递参数
7.父组件中的状态被修改,引发状态变化,子组件被刷新
在 React 应用中,任何可变数据应当只有一个相对应的唯一“数据源”。通常,state 都是首先添加到需要渲染数据的组件中去。然后,如果其他组件也需要这个 state,那么你可以将它提升至这些组件的最近共同父组件中。你应当依靠[自上而下的数据流],而不是尝试在不同组件间同步 state。
虽然提升 state 方式比双向绑定方式需要编写更多的“样板”代码,但带来的好处是,排查和隔离 bug 所需的工作量将会变少。由于“存在”于组件中的任何 state,仅有组件自己能够修改它,因此 bug 的排查范围被大大缩减了。此外,你也可以使用自定义逻辑来拒绝或转换用户的输入。
*这其实就是 block 传值
其他:
refs
组件内的标签都可以定义 ref 属性来标识自己
在父组件调用子组件方法时会使用到 ref
React 和 Angular 的区别:
React 涉及到更多的 JavaScript 代码(JavaScript ES6,JSX),Angular 则更多写 HTML(特殊的 html 语法),逻辑部分采用 TypeScript
React 单向数据绑定,Angular 双向数据绑定
React 一个组件几乎不需要 .html 文件,Angular 通常需要一个.html 文件(可以说 Angular 没有组件化的概念)
React 一个.ts 文件中可以有多个组件,Angular 一个.ts 文件是一个组件
React 项目中使用 .less,Angular 项目中使用 .scss(并不绝对,但我们目前做的两个项目是这样的)
相对来说,React 比 Angular 更加贴近移动端构建界面的思想,对我这种移动端开发人员来说也更容易理解.
组合
React 有十分强大的组合模式。我们推荐使用组合而非继承来实现组件间的代码重用。
组件的组合
组件化编写功能流程:
1.拆分组件
2.实现静态组件(只有静态界面,没有动态数据和交互)
3.实现动态组件
- 实现初始化数据动态显示
- 实现交互功能
我采用的界面编写流程:自上而下地构建界面
区别一下组件的 props 和 state 属性
state: 组件自身内部可变化的数据
props: 从组件外部向组件内部传递数据, 组件内部只读不修改
React 并不需要继承,所以其核心和面向对象的特性(封装,继承,多态)有一定的区别
个人理解:
其中封装可以理解为组件的提取
由于没有继承关系,所以也不存在方法覆盖的概念,也就没有多态,同时也就引发了诸如界面样式没有公开 API 就无法修改之类的问题,在面向对象中可以使用方法覆盖来实现这一点
其本质我认为是函数式编程的思想,通过其组件定义方式 function ,和新特性 HOOK 中极力避免使用 class 关键字等行为也可以看出这一点
在文章写到这里,我深入地思考了 React 框架的特性,得出了其思想是 既是面向对象编程,又注重函数式编程 的结论.
React 还是无法避免面向对象的特点,因为组件是可以使用 extends 关键字继承的,尽管在开发时我们经常忽略它.
React 同时具有函数式编程的诸多特征,
1.函数不能更改输入参数,
2.自上而下的单向数据流,
3.无状态组件(不需要 state 的组件)也是通过 function 来创建,
4.高阶组件的特性也更偏向于函数思想而不是继承.
(高阶组件:接收一个组件作为参数,返回一个包含参数组件的新组件,可以理解为接收一个子视图,返回一个包含有子视图的父视图,这种思想类似于高阶函数,即接收一个函数作为参数,并返回一个经过参数函数运算过的返回值).
这对我来说这是一种全新的体验,关于编程思想上次对我造成如此大的冲击还是在学习和使用 swift 的时候.
函数式编程的特点:
1.闭包和高阶函数
闭包即接口,可以通过传值来操作对象
高阶函数即接收一个函数作为参数,并返回一个经过参数函数运算过的返回值,甚至返回值可以是一个函数
2.惰性计算
在惰性计算中,表达式不是在绑定到变量时立即计算,而是在求值程序需要产生表达式的值时进行计算。
可以理解为 state 改变时只对必要的组件记性改变,而其余部分不变.
3.递归
简单来说就是自己调用自己
4.函数是"一等公民"
函数可以作为参数传递,也可以作为返回值
5.没有副作用
即不改变入参,只传递一个新参数作为返回值
6.确定性
给定的输入永远产生同样的输出,这和多态的思想是相反的
函数式编程提倡使用函数的组合来实现某些功能,这和面向对象中使用继承的思想是相悖的,我认为造成其编程思想根本区别的点在于前端使用的 html 语言其实就是通过组合各种基本元素来实现各种效果的,而且 React 框架能实现的部分按照传统的 MVC 架构来看其实只有 V 和 C,也就是视图和控制器,这里的控制器也只是进行网络请求和页面逻辑的处理,这和移动端与后端的思想是不同的,移动端和后端需要进行业务逻辑处理而不仅仅是界面逻辑,而在单纯的前端开发中,这些工作大多是由后端完成的,所以函数式编程的思想更适用于前端开发.
关于不同的编程范式,其实没有优劣之分,完全可以做到博取众长,互相融合.在移动端开发时我也接触过函数式编程,swift 就是一门融合了函数式编程思想的语言,swift 中的四大基本类型包括 类,函数,结构体和枚举.
swift 中的基本数据类型都是结构体类型
swift 中的数组,字典,字符串都是类类型
swift 中的函数也是一种类型,其可以作为参数被传递,也可以作为一种类型被接口声明
有的时候我们只是专注于写代码,实现各种业务和功能,其实程序员不仅仅是一个单纯写代码完成任务的职业,我们是在用代码描述世界,甚至是创造世界,所以有的时候我写代码写着写着就陷入了对这个世界深深的思考当中...这不仅仅是空想,其实程序员们在如何构建世界的问题上已经陷入了一个大坑,那就是人工智能,人工智能就其本质而言,是对人的思维的信息过程的模拟,人工智能到底该如何实现,我们可以在百度上查看到人工智能涉及到的学科包括
哲学和认知科学
数学
神经生理学
心理学
仿生学
语言学
计算机科学
自动化
信息论
控制论
不定性论
社会结构学
甚至还有科学发展观
可见这是一门极其深奥的技术,这也侧面印证了我们程序员其实是在想办法在计算机上描述和重现这个世界.所以程序员在生活中,相比普通人来说,需要对这个世界有更加深刻的认知和理解才行,程序员需要走的路还有很长很长.
当然这和 React 开发没有什么联系了..言归正传.
React 是我第一个系统学习的前端框架,也是第一次接触 JavaScript ,并有了项目实战经验,在接下来的 Angular 学习中,我会继续探索 JS 和前端开发的特性,总结经验,提升自我.