第一章 入门
1.1 简介
-
是什么?
构建用户界面的Javascript库,将数据渲染成html视图的开源js库
-
谁开发的?
Facebook的 Jordan Walke开发,开源
-
为什么要学?
-
原生JS操作DOM繁琐、效率低(DOM-API操作UI)
-
使用JS操作DOM,浏览器会进行大量的重绘重排
-
原生JS没有组件化编码方案,代码复用率低。
直白来说,模块化是拆解js代码,组件化不仅仅拆解js,还包括html/css/图片等
-
-
React的特点
-
采用组件化模式,声明式编码,提高开发效率及组件复用率。
- 命令式编程(Imperative):详细告诉机器怎么去处理一件事情以叨叨你想要的结果,其中一个环节出问题,一整个任务就完成不了
- 声名式编程(Declarative):告诉机器你想要的结果,机器自己摸索过程
更具体内容可参考https://developer.aliyun.com/article/270068
-
React Native中可以使用react语法进行移动端开发(编写安卓、IOS应用)
-
使用虚拟DOM+优秀的Diffing算法,尽量减少与真实DOM的交互
-
1.2 基本使用
-
使用
-
准备一个容器
-
先引入核心库,再引入react-dom,最后引入babel
<div id="test"></div> <script src="../js/react.development.js"></script> <script src="../js/react-dom.development.js"></script> <script src="../js/babel.min.js"></script>
-
<script type="text/babel"> //一定要写babel // 创建虚拟DOM const VDOM = <h1>Hello React</h1> //此处不要写引号,因为不是字符串 // 渲染虚拟dom到页面 ReactDOM.render(VDOM,document.getElementById('test')) </script>
虚拟dom的创建为什么使用jsx,不使用js?
使开发人员方便创建虚拟dom,其实就是原生js创建虚拟dom的语法糖
-
-
虚拟DOM
- 虚拟DOM是什么?
- 本质是一个一般对象
- 虚拟dom比较“轻”,真实dom比较“重”,因为虚拟dom是在react内部用,无需真实dom上那么多特性
- 虚拟DOM最终会被React转化为真实dom,呈现在页面上
- 虚拟DOM是什么?
-
JSX
jsx语法规则:
-
定义虚拟dom时,不要写引号
-
标签中混入js表达式时要用
{}
表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方
- a
- a+b
- demo(1)
- arr.map()
- function test(){}
js语句/代码:
- if(){}
- for(){}
- switch(){case:xxx}
-
样式的类名指定不要用class,要用className
-
内联样式,要用
style={{key:value}}
的形式去写 -
只能有一个根标签
-
标签必须闭合
-
标签首字母
- 小写字母开头,则将该标签转为html中同名元素,若html无该标签对应的同名元素,则报错
- 大写字母开头,react就去渲染对应的组件,若组件没有定义,则报错
<body> <!-- 创建一个容器 --> <div id="test"></div> <!-- 引入JS库 --> <script src="./js/react.development.js"></script> <script src="./js/react-dom.development.js"></script> <script src="./js/babel.min.js"></script> <script type="text/babel"> const data = ['Angular','React','Vue']; // 创建虚拟dom const VDOM = ( <div> <h1>前端js框架列表</h1> <ul> {data.map((num,index)=>{ return <li key={index}>{num}</li> })} </ul> </div> ) // 渲染虚拟dom到页面 ReactDOM.render(VDOM,document.getElementById('test')) </script> </body>
1.3 模块与组件、模块化与组件化的理解
-
模块
- 理解:向外提供特定功能的js程序,一般就是一个Js文件
- 为什么:随着业务逻辑的增加,代码越来越多且越复杂
- 作用:复用js,简化js的编写,提高js的运行效率
-
组件
- 定义:用来实现局部功能效果的代码和资源的集合(html/css/js/img/video)
- 为什么:一个界面的功能更复杂
- 作用:复用编码,简化项目编码,提高运行效率
-
模块化
当js应用以模块来编写
-
组件化
应用以多组件的方式实现
第二章 面向组件编程
2.1 基本理解和使用
-
函数式组件
<body> <div id="test"></div> <script src="../js/react.development.js"></script> <script src="../js/react-dom.development.js"></script> <script src="../js/babel.min.js"></script> <script type="text/babel"> // 创建函数式组件 function Demo(){ console.log(this);//此处的this是undefined,babel编译后开启了严格模式 `use strict` return <h2>我是用函数定义的组件(适用于简单组件)</h2> } // 渲染组件到页面 ReactDOM.render(<Demo/>,document.getElementById('test')); /* 执行了ReactDOM.render()之后发生了什么? 1. React解析组件标签,找到Demo组件 2. 发现组件是使用函数定义的,随后调用该函数,将返回的虚拟DOM转为真实DOM,随后呈现 在页面中 */ </script> </body>
补充一下严格模式的相关知识
- “use strict” 不是一条语句,是一个字面量表达式
- 特点:
- 不能使用未声明的变量
- 不允许删除变量、对象、函数
- 不允许变量重名
- 不允许使用八进制、转义字符
- 禁止this指向全局对象
详细内容可参考https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
-
类式组件
-
类的基本知识复习
类的定义、类的继承
总结:
- 类中的构造器不是必须写的,要对实例进行一些初始化操作,如添加指定属性时才写
- 若A类继承了B类,且A类中定义了构造器,那么A类构造器中的super方法是必须要调用的
- 类中所定义的方法,都是放在类的原型对象上,供实例去使用
-
<script type="text/babel"> // 创建类式组件 //extends不能漏掉!!! class Demo extends React.Component{ //render放在Demo的原型对象上 //render中的this指向Demo组件实例对象 render(){ return ( <div> hello react </div> ) } } // 渲染组件到页面 ReactDOM.render(<Demo/>,document.getElementById('test')); /* 执行了ReactDOM.render()之后发生了什么? 1. React解析组件标签,找到Demo组件 2. 发现组件是使用类定义的,随后new该类的实例,并通过该实例调用原型上的render 3. 将render返回的虚拟dom转为真实dom,随后呈现在页面中。 */ </script>
-
-
简单组件与复杂组件
简单–无状态,复杂–有状态
2.2 组件实例的三大核心属性1:state
组件实例对象生成state,状态存放数据,状态驱动页面
-
理解:
- state是组件对象中最重要的属性,其属性值是对象(可以包含多个key-value的组合)
state={key:value}
- 组件被称为“状态机”,通过更新组件的state来更新对应的页面显示(重新渲染组件)
- state是组件对象中最重要的属性,其属性值是对象(可以包含多个key-value的组合)
-
状态的改变:不可以直接更改,要借助内置API–setState({}) 更改
-
类中可以直接写赋值语句–相当于给类添加了一个属性
class car { constructor(name,price){ this.name=name; this.price=price; } //相当于给类添加了一个名为a的属性,属性值为1 a=1; }
-
类式组件的简写
<script type="text/babel"> class Weather extends React.Component { // 初始化状态 state = { isHot: true, wind: '微风' }; render() { const { isHot, wind } = this.state; return ( <h2 onClick={this.changeWeather}> 今天天气很{isHot ? '炎热' : '凉爽'},{wind} </h2> ) } // 自定义方法--赋值语句的形式+箭头函数 changeWeather = () => { const isHot = this.state.isHot; this.setState({ isHot: !isHot }) } } // 组件渲染到页面 ReactDOM.render(<Weather />, document.getElementById('test')); </script>
-
强烈注意
- 组件中render方法中this指向组件实例对象
- 组件自定义的方法中this为undefined(作为事件函数),如何解决?
- 强制绑定this:通过函数对象的bind()方法
- 使用箭头函数(赋值语句形式+箭头函数)
- 状态数据不能直接修改或更新,要借助API—setState()
2.3 组件实例三大核心属性2:props
-
Demo—自定义用来显示一个人员信息的组件
- 姓名必须指定,且为字符串类型
- 性别为字符串类型,若性别没有指定,默认为男
- 年龄必须指定,且为数字类型
<script type="text/babel"> class Person extends React.Component { // state = { name: 'tom', sex: '女', age: 18 } render() { const {name,sex,age}=this.props; return ( <ul> <li>姓名:{name}</li> <li>性别:{sex}</li> <li>年龄:{age}</li> </ul> ) } } ReactDOM.render(<Person name='tom' sex='女' age='18' />, document.getElementById('test1')); ReactDOM.render(<Person name='mike' sex='男' age='18' />, document.getElementById('test2')); ReactDOM.render(<Person name='lily' sex='女' age='20' />, document.getElementById('test3')); </script>
-
理解:
每个组件对象都会有props属性
组件标签的所有属性都保存在props中
-
作用:
通过标签属性从组件外向组件内传递变化的数据
组件内部不要修改props数据
-
对props的一些操作
-
对属性值进行限制
使用prop-types库
Person.propTypes={ name:PropTypes.string.isRequired, age:PropTypes.number }
-
设置默认属性值
Person.defaultProps={ name:'TOM', age:18 }
-
扩展属性:将对象的所有属性通过props传递
<Person {...person}
-
2.4 组件实例三大核心属性3:refs与事件处理
-
理解:组件内的标签可以通过ref属性来标识自己
-
ref编码形式
-
字符串形式的ref
<input ref="input1"/>
-
回调形式
<input ref={c=>this.input1=c}/>
-
createRef
myRef = React.createRef() <input ref={this.myRef}/>
-
-
事件处理:
通过onXxxx属性指定时间处理函数(注意大小写)
- React使用的是自定义(合成)事件,而不是使用原生DOM事件
- React中的事件是通过事件委托方式处理的(委托给组件最外层的元素)
- 通过event.target得到发生事件的DOM元素对象
2.5 收集表单数据
- 受控组件
- 非受控组件
2.6 组件生命周期
-
理解
- 组件从创建到死亡会经历一些特定的阶段
- react组件中包含一系列钩子函数(生命周期回调函数),会在特定时刻调用
- 我们在定义组件时,会在特定的生命周期回调函数中做特定工作
-
旧生命周期
-
初始化阶段–由ReactDOM.render() 触发–初次渲染
-
constructor()
-
componentWillMount()
-
render()
必须使用
-
componentDidMount()
常用,一般在这个钩子中做初始化的工作,例如开启定时器,发送网络请求,订阅消息等
-
-
更新阶段–由组件内部this.state() 或者父组件重新render触发
-
componentWillReceiveProps()
-
shouldComponentUpdate()
控制组件更新的阀门,是否允许更新,返回true则允许,返回 false则禁止
若不写该钩子函数,默认返回true
-
componentWillUpdate()
-
render()
-
componentDidUpdate()
-
-
卸载组件–由ReactDOM.unmountComponentAtNode() 触发
-
componentWillUnmount()
一般做收尾的工作,例如:关闭定时器、取消订阅消息
-
-
-
新生命周期
-
即将废除,尽量避免使用
UNSAFE–不是指不安全,这些方法经常被误解或滥用
UNSAFE_componentWillMount()
UNSAFE_componentWillUpdate()
UNSAFE_componentWillReceiveProps()
-
新生命周期
-
初始化阶段–由ReactDOM.render() 触发–初次渲染
-
constructor()
-
getDerivedStateFromProps(props,state)
-
使用概率低
-
要定义成 static,必须返回一个状态对象(state object)或者null
-
若state的值在任何时候都取决于props,可使用getDerivedFromProps(props){},
-
派生状态会导致代码冗余,并使组件难以维护。
-
-
render()
必须使用
-
componentDidMount()
常用,一般在这个钩子中做初始化的工作,例如开启定时器,发送网络请求,订阅消息等
-
-
更新阶段–由组件内部this.state() 或父组件重新render或forceUpdate()触发
-
getDerivedStateFromProps()
-
shouldComponentUpdate()
控制组件更新的阀门,是否允许更新,返回true则允许,返回 false则禁止
若不写该钩子函数,默认返回true*
-
render()
-
getSnapshotBeforeUpdate()
- 在最近一次渲染输出(提到DOM节点)之前调用。它使得组件能子啊发生更改之前从DOM中捕获一些信息(比如:滚动位置)。
- 返回一个快照值(任何类型)或者null,返回值将作为参数传递给componentDidUpdate()
-
componentDidUpdate(preProps,preState)
这里的状态是先前的state,props是先前的
-
-
卸载组件–由ReactDOM.unmountComponentAtNode() 触发
-
componentWillUnmount()
一般做收尾的工作,例如:关闭定时器、取消订阅消息
-
-
-
2.7 虚拟DOM与DOM Diff算法
-
对比的最小单位是标签
-
key
面试题:
- react/vue中的key有什么作用?
- 为什么遍历列表时,key最好不要用index?
-
虚拟DOM中key的作用
-
简单来说,key是虚拟DOM对象的标识,在更新显示时key起着极其重要的作用
-
详细的说,当状态中的数据发生改变时,react会根据【新数据】生成【新的虚拟DOM】,随后react进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:
-
旧虚拟DOM中找到了与新虚拟DOM相同的key:
- 若虚拟DOM内容没变,直接使用之前真实DOM
- 若虚拟DOM内容改变,则生成新的真实DOM,随后替换掉页面中之前的真实DOM
-
旧虚拟DOM中未找到与新虚拟DOM相同的key:
根据数据创建新的真实DOM,随后渲染到页面
-
-
-
用Index作为key可能引发的问题:
-
若对数据进行:逆序添加、逆序删除等破坏顺序操作
会产生没有必要的真实DOM更新 ==》 界面效果没问题,但效率低
-
若结构中还包含输入类的DOM:
会产生错误DOM更新 ==》 界面有问题
-
注意:若不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index时没有问题的。
-
-
开发中如何选择key
- 最好使用每条数据的唯一标识作为key,比如id、手机号、身份证号、学号等唯一值
- 若确定只是简单的展示数据,用Index也是可以的
第三章 React应用(基于脚手架)
3.1 使用create-react-app创建react应用
-
react脚手架
- 用来帮助程序员快速创建一个基于xxx库的模板项目
- 包含所有配置
- 下载好所有相关依赖
- 可直接运行一个简单效果
- react提供了一个用于创建react项目的脚手架库:create-react-app
- 整体架构:react+webpack+es6+eslint
- 用来帮助程序员快速创建一个基于xxx库的模板项目
-
创建项目并启动
$ npm i -g create-react-app //全局安装 $ create-react-app my-app //创建react项目 或者 $ npx create-react-app my-app $ cd my-app //进入项目 $ npm/yarn start //启动项目 $ npm/yarn build //将项目进行打包,生成静态文件,一般在应用完成 $ npm/yarn eject //react默认将webpack配置/相关文件隐藏,将所有webpack相关配置暴露出来,一旦执行该指令,无法再隐藏起来
-
功能界面的组件化编码流程
- 拆分组件:拆分界面,抽取组件
- 实现静态组件:使用组件实现静态页面效果
- 实现动态组件:
- 动态显示初始化数据
- 数据类型
- 数据名称
- 保存在哪个组件
- 交互–从绑定事件监听开始
- 动态显示初始化数据
第四章 react ajax
4.1 理解
-
react只关注界面,并不包含发送ajax请求的代码
-
jquery比较重,不建议使用
-
axios:轻量级,建议使用
-
封装XMLHttpRequest对象的ajax
-
promise风格
-
可以在浏览器和node服务器端
-
4.2 脚手架配置代理
产生跨域的本质是–ajax引擎拦截响应
-
方法一:
在package.json中配置一个代理
"proxy":"http://localhost:5000"
-
优点:配置简单,前端请求资源时可以不加任何前缀
-
缺点:不能配置多个代理
-
工作方式:上述方式配置代理,当请求了3000不存在的资源时,该请求会转发给5000(优先匹配前端资源)
-
-
方法二
在src文件中创建setupProxy.js
- 文件名是固定的,不可改变
- 不能用ES6来写代码,用CJS来写
- 不是作为前端代码来执行,react脚手架将它交给webpack,由webpack处理
const proxy = require('http-proxy-middleware')
module.exports = function(app){
app.use(
//遇见'/api1'前缀的请求,就会触发该代理配置
proxy('/api1',{
//请求转发给谁,配置转发目标地址(能返回数据的服务器地址)
target:'http://localhost:5000',
//控制服务器收到的请求头中的Host字段的值
//请求来自哪里
//true--host:localhost:5000
//false--hosy:localhost:3000
//默认值为false,但一般将它设置为true
changeOrigin:true,
//重写请求路径,一定要加,
//在送给服务器时去掉'/api1'
//保证交给后台服务器是正常请求地址
pathRewrite:{'^/api1':''}
}),
proxy('/api2',{
target:'http://localhost:5001',
changeOrigin:true,
pathRewrite:{'^/api2':''}
}),
)
}
- 优点:可配置多个代理,可灵活的控制请求是否走代理
- 缺点:配置繁琐,前端请求资源时必须加前缀
4.3 消息订阅-发布机制
适用于任意组件通信
要在组件的componentWillUnmount中取消订阅
- 下载:
npm i pubsub-js
-
//导入 import PubSub from 'pubsub-js' //订阅消息 this.token=PubSub.subscribe('my-topic',callback) //发布消息 PubSub.publish('my-topic',callback) //取消订阅 PubSub.unsubscribe(this.token)
4.4 fetch
-
参考文档
-
why fetch
XMLHttpRequest是一个设计粗糙的API,不关注分离原则,配置和调用不简便
//传统XHR const xhr = new XMLHttpRequest(); xhr.open('GET',url); xhr.onload=function (){ console.log(xhr.response) }; xhr.onerror=function (){ console.log('error') }; xhr.send(); //fetch+await try { const res = await fetch(url); const data = await res.json(); console.log(data); }catch(error){ console.log(error) }
-
特点:
- 关注分离
- 是原生函数,不再使用XmlHttpRequest对象提交ajax请求
- 老版本浏览器可能不支持
第五章 React 路由
5.1 相关概念
-
SPA(single page web application)单页面应用
- 只有一个主页面
- 点击页面中的链接不会刷新页面,只会做页面的局部更新
- 数据都需要通过ajax请求获取,并在前端异步展现
-
路由
-
概念:
一个路由就是一个映射关系(key:value)
key–path , value–function/component
-
分类
-
后端路由
- value 为function
- 注册路由
route.get(path,function(req,res){})
- 工作原理:当node接收到一个请求时,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回数据
-
前端路由
- value为component
- 注册路由:
<Route path='/test' component={Test}
- 工作原理:当浏览器的path变为‘/test’时,组件变为Test组件
-
浏览器中的历史记录是栈结构
History对象
//方法一,直接使用H5推出的history身上的API let history = History.createBrowserHistory() //方法二:hash值/锚点 let history = History.createHashHistory() //留下历史记录 function push (path) { history.push(path) return false } //替换历史记录的栈顶 function replace (path) { history.replace(path) } //回退 function back() { history.goBack() } //前进 function forword() { history.goForward() } //监听历史记录 history.listen((location) => { console.log('请求路由路径变化了', location) })
-
-
5.2 基本路由使用
//1.明确好界面中的导航区、展示区
//2.编写路由链接
<BrowserRouter(HashRouter)>
<Link className="list-group-item" to="/about">About</Link>
<Link className="list-group-item" to="/home">Home</Link>
</BrowserRouter(HashRouter)>
//3.匹配路由,注意这里的component首字母不要大写!
<Route path='/about' component={About}/>
<Route path='/home' component={Home}/>
//4.<App>的最外侧包裹一个<BrowserRouter>或者<HashRouter>
5.2.1 路由组件和一般组件
-
写法不同:
一般:
<Demo/>
路由:
<Route path='/demo',component={Demo}/>
-
存放位置不同
一般:components
路由:pages
-
接收到的props不同:
一般组件:写组件标签时传递什么就收到什么
路由:接收到三个固定属性
history: action: "POP" block: ƒ block(prompt) createHref: ƒ createHref(location) go: ƒ go(n) goBack: ƒ goBack() goForward: ƒ goForward() length: 8 listen: ƒ listen(listener) location: {pathname: "/about", search: "", hash: "", state: undefined} push: ƒ push(path, state) replace: ƒ replace(path, state) __proto__: Object location: hash: "" pathname: "/about" search: "" state: undefined match: isExact: true params: {} path: "/about" url: "/about" __proto__: Object staticContext: undefined
5.2.2 NavLink与封装NavLink
- NavLink可实现路由链接的高亮,通过
activeClassName
指定样式名 - 标签体内容是一个特殊的标签属性
- 通过this.props.children 可以获取标签体内容
<MyNavLink to='about'>About</MyNavLink>
//封装
<NavLink activeClassName="atguigu" className="list-group-item" {...this.props}></NavLink>
5.2.3 Switch组件
提高匹配路由效率(单一匹配)
<Switch></Switch>
5.2.4 解决多级路径刷新页面时样式丢失问题
- 去掉 ‘.’ ,
./
从当前文件出发 - 在样式路径最前面添加
%PUBLIC_URL%
- 用
HashRouter
,#后面都是哈希值(少见)
5.2.5 路由的精准匹配和模糊匹配
- 默认使用模糊匹配:输入路径必须包含匹配路径,且顺序要一致
- 开启严格匹配:
<Route exact={true} path="/about" component={About}/>
- 严格匹配不要随便开启,需要再开,有时开启会导致无法继续匹配二级路由
5.2.6 Redirect
-
一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由
-
<Switch> <Route path='/about' component={About}/> <Route path='/home' component={Home}/> <Redirect to='about'/> </Switch>
5.3 路由的嵌套
- 注册子路由时要写上父路由的path值
- 路由的匹配是按照注册路由的顺序进行的
5.4 向路由组件传递参数
-
param参数
//1. 路由链接(携带参数) <Link to='/home/message/detail/tom/18'>详情</Link> //2. 注册路由(接受参数) <Route path='/home/message/detail/:name/:age' component={Detail} /> //3. 接收参数 const {name,age} = this.props.match.params;
-
search参数
//1. 路由链接(携带参数) <Link to='/home/message/detail?name=tom&age=18'>详情</Link> //2. 注册路由(无需声明接收参数) <Route path='/home/message/detail' component={Detail} /> //3. 接收参数,获取到的是urlencode编码字符串,需要借助querystring解析 const {name,age}=qs.parse(this.props.location.search.slice(1))
-
state参数
//1. 路由链接(携带参数) <Link to={{pathname:'/home/message/detail',state:{name:tom,age:18}}}>详情</Link> //2. 注册路由 <Route path='/home/message/detail' component={Detail} /> //3. 接收参数 const {name,age}=this.props.location.state; //刷新也可以保留参数
5.6 多种路由跳转方式
-
push
-
replace
<Link replace=true to='/home'>home</Link>
-
编程式路由导航
借助this.props.history对象上的API操作路由跳转、前进、后退
this.props.history.push/replace/goBack/goForward/go()
-
withRouter
作用:加工一般组件使其具有路由组件上的history对象,其返回值是一个新的组件
5.7 HashRouter与BrowserRouter的区别
HashRouter | BrowserRouter | |
---|---|---|
底层原理 | URL的哈希值 | H5新增的history API,不兼容IE9及以下版本 |
path表现形式 | 路径包含 # | 路径不包含# |
刷新后对路由state参数的影响 | state参数丢失 | 对state没有影响,因为state保存在history中 |