1. 虚拟DOM和diff算法
1.1 createEmement 创建组件
createElement 是另一种创建 react 元素的方法,但是相对麻烦,并且难于维护所以了解即可
import React from 'react'
import ReactDOM from 'react-dom'
// 参数1: 标签名
// 参数2: 标签属性
// 参数3: 标签内部的子节点
const jsx = React.createElement(
'div',
{ className: 'top', title: 'hello' },
'你好啊'
)
// <div class="top" title="hello">你好啊</div>
ReactDOM.render(jsx, document.querySelector('#app'))
1.2 JSX转换过程
- JSX 是 createElement() 方法的语法糖 (语法糖:更加直观、简洁、友好)
- JSX 语法会被 @babel/preset-react 插件编译为 createElement 方法
- JSX 对程序员更方便直观、createElement方法运行起来更方便
- createElement 又会被转化为 React元素,React元素是一个对象,能够描述UI结构
// 核心
{
type: 'div',
props: [
{className: 'top'},
{children: 'Hello JSX'}
]
}
1.3 虚拟DOM
虚拟DOM: 本质上是个 js 对象,用来描述页面UI (React 元素就是虚拟DOM)
创建时:
- React组件配合 state 创建一个虚拟DOM树
- 根据虚拟DOM树,生成一个真正的 DOM 树,再渲染到页面中
更新时:
- 当 state 或者 props 变化时,生成一个新的虚拟DOM树
- 新旧虚拟 DOM 树进行对比(diff算法),找到新旧虚拟DOM的差异点
- 将差异点更新到页面上
1.4 diff 算法
- tree diff: 按照树的层级进行比较叫做 tree diff,如果该节点不存在,则整个删除,不再继续比较
- component diff: 每一层中组件的对比叫做 component diff
- 如果前后组件类型相同,暂时不需要更新
- 如果前后组件类型不同,则需要更新
- element diff:如果两个组件类型相同,则需要对比组件中的元素,叫做 element diff
- 如果元素不同,则需要更新
- 如果元素相同,则不需要更新
1.5 递归实现 diff 算法
实现思路:
- 按照面向过程的方式来写代码
- 当发现有雷同代码时,拆分观察,找异同
- 规整代码
案例: 找出新旧不同的节点
var oldTree = {
value: 1,
left: { value1: 2 },
right: { value2: 3, value3: 4}
}
var newTree = {
value: 1,
left: { value1: 2 },
right: { value2: 33, value3: 44 }
}
1)按照面向过程的方式编写代码
/ 声明一个空对象,用来保存找出的不同节点
var result = {}
// 遍历新树
for (var key in newTree) {
// 判断取得的当前节点的类型是否不是对象
if (typeof newTree[key] !== 'object') {
// 不是对象则可以直接比对值
if (newTree[key] !== oldTree[key]) {
// 不相等时,进行保存;相等时不管
result[key] = newTree[key]
}
// 判断取得的当前节点类型是否为对象,是对象时就要继续遍历
} else if (typeof newTree[key] === 'object') {
// 在result上创建一个新的空对象用来保存不同节点
result[key] = {}
// 遍历该对象
for (var k in newTree[key]) {
if (typeof newTree[key][k] !== 'object') {
if (newTree[key][k] !== oldTree[key][k]) {
result[key][k] = newTree[key][k]
}
}
}
// 判断新建的对象是否为空,如果为空则清除
let tmpKeyArr = Object.keys(result[key])
tmpKeyArr.length == 0 ? delete result[key] : ''
}
}
console.log(result)
2)当发现有雷同代码时,拆分观察,找异同
内外层代码一模一样,只是变量不同了
3)规整代码 (封装函数,自己调用自己)
注意: 内层函数调用时传入的参数
function diff (newNode, oldNode) {
var result = {}
for (var key in newNode) {
if (typeof newNode[key] !== 'object') {
if (newNode[key] !== oldNode[key]) {
result[key] = newNode[key]
}
} else if (typeof newNode[key] === 'object') {
result[key] = diff(newNode[key], oldNode[key])
let tmpKeyArr = Object.keys(result[key])
tmpKeyArr.length == 0 ? delete result[key] : ''
}
}
return result
}
var res = diff(newTree, oldTree)
console.log(res)
Object.keys(obj): 获取obj对象的所有属性,并保存到一个数组中
let arr = Object.keys({a:‘Hello’, b:‘World’}) ; arr = [‘a’, ‘b’]
delete Obj.a; 删除 obj 对象中的 a 属性
2. 飞翔的大猪
2.1 鼠标位置
核心思路: 通过鼠标移动事件,获取实时鼠标位置信息,再将位置信息更新在页面上
实现思路:
-
要设置 state,用来保存鼠标的 横纵坐标
-
要注册mousemove事件,实时获取鼠标当前的位置,并保存到state中
mousemove事件要注册给window (在 componentDidMount 方法中进行注册)
-
将 state 的值设置 页面要显示鼠标位置的地方
import React from 'react';
import ReactDOM from 'react-dom';
class Mouse extends React.Component {
// 定义鼠标的xy坐标
state = {
x: 0,
y: 0
}
// 定义鼠标移动事件,实时更新鼠标位置
moveMouse = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 为整个window加上mousemove事件
componentDidMount () {
window.addEventListener('mousemove', this.moveMouse)
}
render () {
return (
<div>
鼠标坐标: {this.state.x} - {this.state.y}
</div>
)
}
}
ReactDOM.render(<Mouse />, document.querySelector('#app'))
2.2 飞翔的大猪
核心思路: 将图片距离顶部的值设置为 y; 距离左侧的值设置成 x
实现思路:
- 能够实时获取鼠标当前的位置
- 图片要设置绝对定位,top的值要实时和鼠标坐标的y相等,left的值要实时和鼠标坐标的x相等
导入图片:
import img from './assets/images/pig.jpg'
render () {
return (
// alt 不能省略
<img src={img} alt="飞翔的大猪" />
)
}
css代码:
img {
position: absolute;
width: 100px;
height: 100px;
left: 200px;
top: 100px;
cursor: none;
border-radius: 50%;
}
import React from 'react';
import ReactDOM from 'react-dom';
import './assets/css/pig.css'
import img from './assets/images/pig.jpg'
class Pig extends React.Component {
// 定义鼠标的xy坐标
state = {
x: 0,
y: 0
}
// 定义鼠标移动事件,实时更新鼠标位置
moveMouse = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
// 为整个window加上mousemove事件
componentDidMount () {
window.addEventListener('mousemove', this.moveMouse)
}
render () {
return (
<div>
<img
src={img}
alt="飞翔的大猪"
// 随时将鼠标位置设置为图片位置
style={{left: this.state.x - 50, top: this.state.y - 50}}
/>
</div>
)
}
}
ReactDOM.render(<Pig />, document.querySelector('#app'))
2.3 总结
- 显示鼠标位置 和 飞翔的大猪 两个案例代码大部分是一样的
- 初始化 x、y 坐标值
state = { x: 0, y: 0 }
- 注册mousemove事件获取鼠标最新位置
- state 和 操作state的方法是一样的
- 初始化 x、y 坐标值
- 只有渲染页面时对鼠标坐标的用法不一样 (render方法内不一样)
3. 组件复用
- 组件复用就是将组件中相同的业务逻辑抽取出来进行封装
- 复用两种东西: state 和 操作 state 的方法
- 实现复用有两种方式: render props 模式 和 高阶组件(HOC)
- 组件复用没有新的API,是由 React 自身特点(编码技巧)演化而来的固定模式
3.1 render props模式
3.1.1 创建鼠标位置组件
核心思想:子组件向父组件传值,父组件渲染页面时,使用子组件提供的state值
实现思路:
- 将要操作的状态(state) 和 操作状态的方法封装到子组件中
- render 函数中不再渲染页面,而是返回一个可供父组件调用的方法,并将state作为参数传入
- 父组件在调用子组件时可以传入函数,函数的形参能够接收到子组件传递的state
实现步骤:
- 创建子组件 Mouse,在该组建中提供状态和修改状态的代码
- Mouse 的 render 函数不再渲染页面,而是通过props向外界提供一个函数,并将state作为参数传入
- 在App组件中渲染Mouse,并在Mouse中使用属性调用函数
Mouse组件 (component/mouse.js)
import React from 'react'
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
moveMouse = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
componentDidMount () {
window.addEventListener('mousemove', this.moveMouse)
}
render () {
// mouse组件不再渲染页面,而是向外提供一个叫做render的方法
// 并且将 state 作为参数传入
return this.props.render(this.state)
}
}
export default Mouse
3.1.2 复用鼠标位置组件
实现方案: 在App组件(父组件)中调用 Mouse组件时,使用 render 方法得到Mouse组件(子组件)中提供的state,再进行页面渲染工作
App组件
import React from 'react'
import ReactDOM from 'react-dom'
import img from './assets/images/pig.jpg'
import './assets/css/img.css'
import Mouse from './components/mouse'
class App extends React.Component{
render() {
return(
<div>
<Mouse render = {(data)=> {
return <span>鼠标坐标x: {data.x} - {data.y}</span>
}} />
{/* render并不是固定的叫法 */}
<Mouse render = {(data)=> {
return <img src={img} alt='猪猪' style={{left:data.x-50,top:data.y-50}} />
}} />
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector('#app'))
3.1.3 children代替render props
return this.props.render(this.state)
中的 render 并不是固定的名称,使用什么都行- render props 核心思想还是利用子向父传值时候的回调函数。父组件向子组件传递一个函数,自组件执行该函数时将子组件中的state作为实参传入。
- children 是一个更好的选择,因为组件如果写成双标签形式,标签内部会被自动认为是 children
实现方案:
- mouse组件中,将返回值的 this.props.render 改为 this.props.children
- 调用Mouse组件时,将函数执行放在Mouse组件中
mouse-1.js
import React from 'react'
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
moveMouse = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
componentDidMount () {
window.addEventListener('mousemove', this.moveMouse)
}
render () {
// 使用 children 替换 render
return this.props.children(this.state)
}
}
export default Mouse
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import img from './assets/images/pig.jpg'
import './assets/css/img.css'
import Mouse from './components/mouse-1'
class App extends React.Component{
mousePosition = (data)=>{
// console.log(data);
return <span>鼠标坐标x: {data.x} - {data.y}</span>
}
pigPosition = (data)=> {
return <img src={img} alt='猪猪' style={{left:data.x-50,top:data.y-50}} />
}
render() {
return(
<div>
<Mouse>
{this.mousePosition}
</Mouse>
<Mouse>
{this.pigPosition}
</Mouse>
</div>
)
}
}
ReactDOM.render(<App />, document.querySelector('#app'))
3.2 高阶组件
-
高阶组件(HOC,Higher-Order-Component) 采用 包装模式 实现组件的复用
-
高阶组件就是一个函数,接收要包装组件,返回一个增强功能的组件
3.2.1 基本使用
核心思想: 父组件向子组件传值,父组件提供state,并在调用子组件时将state传递给子组件;子组件使用porps接收state的值,进行页面渲染
实现步骤:
-
创建增强组件函数
1)创建一个函数,名称必须 以 with 开头
2)指定函数参数,参数名也要以大写字母开头 (实际上是组件名称)
3)在函数内部创建一个类组件,最后要返回该类组件
-
定义子组件,子组件使用 props 接收父组件的state,并进行渲染
-
在 App 组件中调用增强组件
代码实现:
- 创建增强组件函数
// 创建一个以 with 开头的函数
// 参数: 子组件
function withMouse (WarppedComponent) {
// Mouse是父组件,提供state和操作state的方法,调用子组件
class Mouse extends React.Component {
state = {
x: 0,
y: 0
}
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY
})
}
componentDidMount () {
window.addEventListener('mousemove', this.handleMouseMove)
}
// render 方法中渲染子组件
render () {
// render 中调用子组件,并将state的值传入子组件
return <WarppedComponent {...this.state}></WarppedComponent>
// 传统写法
// return <WrappedComponent x={this.state.x} y={this.state.y}></WrappedComponent>
}
}
// 将组件类返回
return Mouse
}
2.定义子组件,子组件使用 props 接收父组件的state,并进行渲染
// 定义组件 组件要传入withMouse中
class Position extends React.PureComponent{
render () {
return (
<div>鼠标位置:{this.props.x} - {this.props.y}</div>
)
}
}
const MousePosition = withMouse(Position)
class Pig extends React.PureComponent{
render () {
return (
<div>
<img src={img} alt='猪猪' style={{left:this.props.x - 50, top:this.props.y- 50}} />
</div>
)
}
}
// 调用withMouse函数 将Position组件传入
// 返回的组件就是 Mouse+Position的增强型组件
const PigPosition = withMouse(Pig)
3.在 App 组件中调用增强组件
class App extends React.Component{
render() {
return (
<div>
<MousePosition></MousePosition>
<PigPosition></PigPosition>
</div>
)
}
}
3.2.2 displayName
- 同一个高阶组件使用两次时,在页面中会展示出两个相同的组件名,这样会造成调试不便的情况
- 默认情况下,高阶组件会以 displayName 属性中的值作为标签名称
- 在没有 displayName 的情况下使用 name 属性中的值作为标签名称
测试:
function withMouse (WrappedComponent) {
class Mouse extends React.PureComponent {
state = { ... }
handleMouseMove = e => { ... }
componentDidMount () { ... }
render () { ... }
}
// 打印 Mouse 对象
console.log(Mouse)
return Mouse
}
- 打印 Mouse 类能看见 name 属性,但是看不见 displayName 属性,所以标签名就使用 name 对应的值
- 但是我们手动设置 displayName 后,它的优先级是高于 name 属性的
function withMouse (WrappedComponent) {
class Mouse extends React.PureComponent {
state = { ... }
handleMouseMove = e => { ... }
componentDidMount () { ... }
render () { ... }
}
// 为 Mouse 增加 displayName 属性
Mouse.displayName = 'MyCom'
// 打印 Mouse 对象
console.log(Mouse)
return Mouse
}
Mouse 现在的结构
页面上的元素
让高阶组件名称差异化的解决方案: 动态设置组件的 displayName
如果组件本身有 displayName 就使用 displayName,如果没有就使用组件的 name
// 动态设置组件的 displayName
Mouse.displayName = `withMouse${getDisplayName(WarppedComponent)}`
function getDisplayName(WarppedComponent) {
return WarppedComponent.displayName || WarppedComponent.name || 'Component'
}
3.2.3 传递 props
渲染高阶组件时,如果给组件传递属性和值会丢失,因为高阶组件没有向下继续再传递值
解决方案: 渲染 WarppedComponent 时,将 state 和 props 一起传递给组件
class App extends React.PureComponent {
render () {
return (
<div>
{/* 渲染高阶组件时,向组件中传入数据 */}
<PositionMouse a={123} />
</div>
)
}
}
function withMouse (WarppedComponent) {
class Mouse extends React.PureComponent {
state = {...}
handleMouseMove = e => {...}
componentDidMount () {...}
render () {
// render 中调用子组件,同时将 state 和 props 都传入 WarppedComponent
return <WarppedComponent {...this.state} {...this.props}></WarppedComponent>
}
}
Mouse.displayName = `withMouse${getDisplayName(WarppedComponent)}`
return Mouse
}
class Position extends React.PureComponent {
render () {
// Position 组件中的 props 即接收了Mouse组件传递的 state 又接收了 props,所以在this.props中保存了
console.log(this.props)
return (
<div>鼠标位置: {this.props.x} - {this.props.y}</div>
)
}
}
4. 路由
现代前端应用都是 SPA (Single Page Application 单页应用程序), 就是只有一个html页面的应用程序。 为了有效使用单个页面管理原来的多页面功能,前端路由就产生了
- 前端路由的功能: 让用户从一个页面导航到另一个页面
- 前端路由是一套 ur l路径 与 组件的对应关系
4.1 基本使用
使用步骤:
- 安装: npm i react-router-dom
- 导入路由的三个核心组件: Router / Route / Link
- 使用 Router 组件包裹整个应用内容
- 使用 Link 定义路由
- 使用 Route 定义路由对应的组件
import React from 'react'
import ReactDOM from 'react-dom'
// 导入路由组件
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
// 定义了两个最基本的组件
const First = () => {
return <div>页面一的内容</div>
}
const Second = () => {
return <div>页面二的内容</div>
}
class App extends React.PureComponent {
render () {
return <Router>
<div>
{/* 使用 Link 定义两个路由 */}
{/* to: 要跳转到的url地址 */}
<Link to="/first">页面一</Link>
<Link to="/second">页面二</Link>
{/* component: url地址对应的组件 */}
<Route path="/first" component={First}></Route>
<Route path="/second" component={Second}></Route>
</div>
</Router>
}
}
ReactDOM.render(<App />, document.querySelector('#app'))
4.2 路由组件
- Router 组件: 包裹整个应用,一个 react 应用只需要一个
- 常用两种 Router: HashRouter 和 BrowserRouter
- (推荐)BroswerRouter 使用 history API 实现 (localhost:3000/first)
import {BroswerRouter as Router, Route, Link } from 'react-router-dom'
- HashRouter 的url地址中多了个 # (localhost:3000/#/first)
import {HashRouter as Router, Route, Link } from 'react-router-dom'
- Link 组件: 用于指定导航链接
- Link 组件会被编译成 a 标签,to属性会被编译成 href 地址
- Route 组件: 定义 url 展示的组件,Route 写在哪里,组件就会被渲染到哪里
4.3 路由的执行过程
- 点击 Link 组件(a 标签)时会修改url地址栏中的 pathname
- 路由监听到 url 地址变化之后,得到最新的 pathname,再遍历所以的 Route 组件。使用 pathname 和 Route 中的 path 进行比对,找到匹配的 Route
- 找到 pathname 对应的 Route 之后,再找到 componet 对应的组件进行编译、渲染
4.4 编程式导航
编程式导航就是通过 js 代码来进行跳转
- this.props.history.push( path )
- path 是要跳转到的url地址
- this.props.history.go(n)
- n: 前进或者后退几个页面
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter as Router, Route, Link} from 'react-router-dom'
// 登录组件, 点击 "登录" 按钮跳转到 /home
class Login extends React.Component {
login = () => {
this.props.history.push('/home')
}
render () {
return (
<div>
账号: <input type="text" /><br />
密码: <input type="password" /><br />
<input type="button" value="登录" onClick={this.login} />
</div>
)
}
}
// Home组件,点击 "退出" 按钮跳转回 /login
class Home extends React.Component {
logout = () => {
this.props.history.push('/login')
// this.props.history.go(-1)
}
render () {
return (
<div>
<button onClick={this.logout}>退出</button>
这是首页
</div>
)
}
}
class App extends React.Component {
render () {
return (
<Router>
<Link to="/login">登录</Link>
<Route path="/login" component={Login}></Route>
<Route path="/home" component={Home}></Route>
</Router>
)
}
}
ReactDOM.render(<App />, document.querySelector('#app'))
点击退出按钮即会回到登录界面
4.5 默认路由
目标: 浏览器地址栏是 localhost:3000/ 时,就能看到 login 页面
实现方式: 在 Route 组件配置path时,直接设置为 / 即可
<Router>
<Route path="/" component={Login}></Route>
<Route path="/home" component={Home}></Route>
</Router>
4.6 匹配模式
上例中的问题: 点击 “登录” 按钮时,会同时显示 login 组件和 home 组件
原因: 默认情况下, React 使用的是 模糊匹配模式
模糊匹配规则: 只要 pathname 以 path 开头就能匹配成功
http://localhost:3000/login (/login 是 pathname)
<Route path="/" component={Login} /> (/ 是 path)
path | 能够匹配的路由 |
---|---|
/list | /list、 /list/a、 /list/1 |
/admin | /admin、 /admin/login、 /admin/home、/admin/userlist/a/b/c |
/ | 匹配所有路由 / 、 /login 、 /home、 /admin/userlist … |
解决方案: 使用精确路由
精确路由: 为Route 增加 exact 属性, 当使用了精确匹配时, path 和 pathname 必须完全一样才能跳转
<Route exact path="/" component={Login}></Route>
<Route path="/home" component={Home}></Route>