React系列——React-Router4解析之Link组件实现

Link组件的用法

React-Router4中的Link是一个react组件,先从组件的用法入手,让你更好的理解他的实现。

1、基本用法

<Link to={`/user`}>
</Link>

经过Link组件的处理后:

<a href="#/user">
</a>

2、增加其他配置

<Link 
    to={`/user`}
    innerRef={(refLink) => this.refLink = refLink}
    className={`item`}
>
</Link>

经过Link组件的处理后:

<a 
    href="#/user"
    ref={(refLink) => this.refLink = refLink} //ref实际上不会显示在DOM上面,而是在js中绑定
    classname="item"
>
</a>

你还可以给Link设置id等其他自定义的属性,下面我们就来探索Link组件的源码,看看它是怎么实现的。

Link组件的实现

从上面的例子中,我们知道了Link组件最重要的属性to,还有就是自定义属性的功能。我们先不看源码,假设就这2个需求,自己去写一个Link组件。

1、to的实现

import React from 'react'
export default class Link extends React.Component {
    render() {
        const { to } = this.props
        return <a href={to} />
    }
}

2、其他属性配置的实现
要想实现自定义属性配置,只需要用到es6的解构。

import React from 'react'
export default class Link extends React.Component {
    render() {
        const { to,...props } = this.props
        return <a href={to} {...props} />
    }
}

如果你拿着这个7行代码的Link组件去使用,通常情况下也行,但是并不完美,因为官方还考虑到了其他需求。

Link官方源码

import React from 'react'
import PropTypes from 'prop-types'
import invariant from 'invariant'
import { createLocation } from 'history'

const isModifiedEvent = (event) =>
  !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)

class Link extends React.Component {
  static propTypes = {
    onClick: PropTypes.func,target: PropTypes.string,replace: PropTypes.bool,to: PropTypes.oneOfType([
      PropTypes.string,PropTypes.object
    ]).isRequired,innerRef: PropTypes.oneOfType([
      PropTypes.string,PropTypes.func
    ])
  }

  static defaultProps = {
    replace: false
  }

  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,replace: PropTypes.func.isRequired,createHref: PropTypes.func.isRequired
      }).isRequired
    }).isRequired
  }

  handleClick = (event) => {
    if (this.props.onClick)
      this.props.onClick(event)

    if (
      !event.defaultPrevented && // onClick prevented default
      event.button === 0 && // ignore everything but left clicks
      !this.props.target && // let browser handle "target=_blank" etc.
      !isModifiedEvent(event) // ignore clicks with modifier keys
    ) {
      event.preventDefault()

      const { history } = this.context.router
      const { replace,to } = this.props

      if (replace) {
        history.replace(to)
      } else {
        history.push(to)
      }
    }
  }

  render() {
    const { replace,to,innerRef,...props } = this.props

    invariant(
      this.context.router,'You should not use <Link> outside a <Router>'
    )

    const { history } = this.context.router
    const location = typeof to === 'string' ? createLocation(to,null,history.location) : to

    const href = history.createHref(location)
    return <a {...props} onClick={this.handleClick} href={href} ref={innerRef}/>
  }
}

export default Link

官方源码还做了下面几样事情:

1、增加PropTypes确保传入的值是合法的,这一步有人可能觉得没什么必要,包括我自己写react组件的时候,也不是每次都会写PropTypes,实在是每个组件都写这些太累人了,当然,最好你还是写上。当传入值格式不对的时候,会有报错提示,可以快速排查错误。

static propTypes = {
    onClick: PropTypes.func,//点击事件得是个函数
    target: PropTypes.string,//target属性也是a标签常用到的
    replace: PropTypes.bool,//replace是个布尔值,作用下面会讲到
    to: PropTypes.oneOfType([
      PropTypes.string,//to可以是字符串、对象
    innerRef: PropTypes.oneOfType([
      PropTypes.string,PropTypes.func
    ]) //innerRef可以是字符串、函数
  }

2、设置默认的replace值
至今我们还不知道replace用来做什么,你可以从他的中文意思或者是location的API去猜测:取代

static defaultProps = {
    replace: false
  }

3、上下文设置
react中不推荐使用上下文,但在react-router,使用上下文是为了父子组件的通信。我们知道,react-router4中,必须使用<Router></Router>作为容器,在容器内部使用Link,我们先不管Router是如何实现的,在这里需要知道router作为上下文使用即可。

static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.shape({
        push: PropTypes.func.isRequired,//push新增一个浏览器记录
        replace: PropTypes.func.isRequired,//替换当前的浏览器记录
        createHref: PropTypes.func.isRequired //创建href
      }).isRequired
    }).isRequired
  }

4、render方法的实现

a、和文章开头自己写的Link不同的是,官方Link增加了invariant()来判断是否存在上下文router,推断外层有无<Router></Router>。

render() {
    const { replace,...props } = this.props
    invariant(
      this.context.router,history.location) : to

    const href = history.createHref(location)
    return <a {...props} onClick={this.handleClick} href={href} ref={innerRef}/>
  }

b、如果存在上下文router,即读取history对象,那么history有什么方法呢?本文讲的是hashHistory,
browserHistory中的history实现有所不同,但他们的API是完全一样的。

//hashHistory和browserHistory的API
history = {
    length: globalHistory.length,action: 'POP',location: initialLocation,createHref: createHref,push: push,replace: replace,go: go,goBack: goBack,goForward: goForward,block: block,listen: listen
  }

c、用typeof判断传入的to是否是字符串,如果是就使用createLocation()方法处理,createLocation有4个参数,路径、状态、键名、当前的location,createLocation(path,state,key,currentLocation)。
currentLocation函数也挺有意思的,这里提高该函数处理后,返回的location格式如下:

location = {
  pathname: '',hash: '',state: *
}

记得to可以传字符串或者对象吗?如果传入的是对象,就不使用createLocation函数处理。也就是下面这样定义的情况:

<Link to={{
  pathname: '/courses',search: '?sort=name',hash: '#the-hash',state: { fromDashboard: true }
}}/>

d、处理后的location可以直接绑定给href属性吗?不行,因为他是个对象,而实际上href得是个字符串,所以官方代码使用了history.createHref()方法转换了location。

const href = history.createHref(location)
    return <a href={href} />

5、现在还剩最后一个环节,onClick事件的处理。有2个关键点,isModifiedEvent()和replace揭秘。
isModifiedEvent和if条件里面的几个event的属性,你可以看这篇文章:event属性介绍
replace如果设置为true,那么就使用history.replace(to)替换当前页面。

const isModifiedEvent = (event) =>
  !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey)

handleClick = (event) => {
    if (this.props.onClick)
    //如果存在自定义的onClick,则执行此自定义回调函数
      this.props.onClick(event)

    if (
      !event.defaultPrevented && // 事件的默认动作没有被禁用
      event.button === 0 && // 当鼠标左键被点击
      !this.props.target && // 没有传入类似target=_blank的属性
      !isModifiedEvent(event) // 点击操作时忽略其他几个键盘的点击
    ) {
      event.preventDefault()

      const { history } = this.context.router
      const { replace,to } = this.props

      if (replace) {
        history.replace(to)
      } else {
        history.push(to)
      }
    }
  }

总结

一个只有81行的组件被我讲的那么长篇大论,前半部分的实现已经够用,后半部分主要是react-router自身的架构需求。理解了就不难,你也可以自己尝试写一个简化版的Link,不需要上下文那种。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom