React基础学习-3

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 鼠标位置

核心思路: 通过鼠标移动事件,获取实时鼠标位置信息,再将位置信息更新在页面上

实现思路:

  1. 要设置 state,用来保存鼠标的 横纵坐标

  2. 要注册mousemove事件,实时获取鼠标当前的位置,并保存到state中

    mousemove事件要注册给window (在 componentDidMount 方法中进行注册)

  3. 将 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

实现思路:

  1. 能够实时获取鼠标当前的位置
  2. 图片要设置绝对定位,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的方法是一样的
  • 只有渲染页面时对鼠标坐标的用法不一样 (render方法内不一样)

3. 组件复用

  • 组件复用就是将组件中相同的业务逻辑抽取出来进行封装
  • 复用两种东西: state 和 操作 state 的方法
  • 实现复用有两种方式: render props 模式 和 高阶组件(HOC)
  • 组件复用没有新的API,是由 React 自身特点(编码技巧)演化而来的固定模式

3.1 render props模式

3.1.1 创建鼠标位置组件

核心思想:子组件向父组件传值,父组件渲染页面时,使用子组件提供的state值

实现思路:

  • 将要操作的状态(state) 和 操作状态的方法封装到子组件中
  • render 函数中不再渲染页面,而是返回一个可供父组件调用的方法,并将state作为参数传入
  • 父组件在调用子组件时可以传入函数,函数的形参能够接收到子组件传递的state

实现步骤:

  1. 创建子组件 Mouse,在该组建中提供状态和修改状态的代码
  2. Mouse 的 render 函数不再渲染页面,而是通过props向外界提供一个函数,并将state作为参数传入
  3. 在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

实现方案:

  1. mouse组件中,将返回值的 this.props.render 改为 this.props.children
  2. 调用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. 创建增强组件函数

    1)创建一个函数,名称必须 以 with 开头

    2)指定函数参数,参数名也要以大写字母开头 (实际上是组件名称)

    3)在函数内部创建一个类组件,最后要返回该类组件

  2. 定义子组件,子组件使用 props 接收父组件的state,并进行渲染

  3. 在 App 组件中调用增强组件

代码实现:

  1. 创建增强组件函数
// 创建一个以 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 基本使用

使用步骤:

  1. 安装: npm i react-router-dom
  2. 导入路由的三个核心组件: Router / Route / Link
  3. 使用 Router 组件包裹整个应用内容
  4. 使用 Link 定义路由
  5. 使用 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 路由的执行过程

  1. 点击 Link 组件(a 标签)时会修改url地址栏中的 pathname
  2. 路由监听到 url 地址变化之后,得到最新的 pathname,再遍历所以的 Route 组件。使用 pathname 和 Route 中的 path 进行比对,找到匹配的 Route
  3. 找到 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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值