Vue-Router3源码拾遗【6】配套组件的实现

如下面章节标题所示:router-view 首次渲染和重新渲染的原理 和 router-link 的原理。

至此,vue-router3 源码系列文章,完结。

router-view 首次渲染的逻辑

router-view 只不过是实现了render函数,算出要渲染哪个组件即可,没有黑科技。

路由最终的渲染离不开组件,Vue-Router 内置了 <router-view> 组件,它的定义在 src/components/view.js 中。

  • 前置知识:
    • 用到的数据:$route 是定义在 Vue.prototype 上。每个组件实例访问 $route 属性,就是访问根实例的 _route,也就是当前的路由线路。(即数据代理:先前已Object.defineProperty定义Vue.prototype的$route为this._routerRoot._route)
    • <router-view> 是一个 functional 组件,它的渲染也是依赖 render 函数

因此直接看render即可:

  • router-view的render函数,即整体架构:
    • (props name 也就是 <router-view name="xxxx" />)
    • 【首先获取当前的路径】route = parent.$route 也就是可以粗浅理解为是当前路径 【见前面解释】
    • 【得出应该渲染哪个组件,支持嵌套】因为router-view和 路由配置表(和routerRecord)是映射关系,所以要根据它计算出对应显示的是啥组件
      • ( 面向结果编程即可,循环判断父节点是否符合条件,没有黑科技就不看了 )
    • 定义一个回调函数,以备之后更新 matched.instances[name]
      • (定义data.registerRouteInstance,方便后面实现matched.instances[name] = val。这样每个组件的导航钩子,都能拿到对应的组件实例)
    • 调用h函数渲染
export default {
  name: 'RouterView',
  functional: true,
  props: { /**/name },
  render (_, { props, children, parent, data }) {
    data.routerView = true

    // 定义变量,飞速略过:
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    // while (parent && parent._routerRoot !== parent) {

    // 【直接看到这里】
    const matched = route.matched[depth]
    if (!matched) {
      cache[name] = null
      return h()
    }

    const component = cache[name] = matched.components[name]
    // 搞 matched.instances[name]
    data.registerRouteInstance = (vm, val) => {/*  */ matched.instances[name] = val }
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => { matched.instances[name] = vnode.componentInstance }

    // 关于data.props的逻辑先略过 
    return h(component, data, children)
  }
}

整体架构就是这样,其中个别模块内容即便展开也没有黑科技,就不作详解了。

vue-router 中的响应式的实现 及 router-view 重新渲染

  • router-view和普通视图template都是调用了渲染watcher。
  • $route:defineReactive + 可以有Dep.target

router-view同样有响应式原理(组件的渲染watcher),这就是它 重新渲染 的原理:

在我们混入的 beforeCreate 钩子函数中有这么一段逻辑:

Vue.mixin({
  beforeCreate () {
    if (isDef(this.$options.router)) {
      Vue.util.defineReactive(this, '_route', this._router.history.current)
    }
    // ...
  }
})
  • 依赖收集:我们在每个 <router-view> 执行 render 函数的时候,都会访问 parent.$route,如我们之前分析会访问 this._routerRoot._route,触发了它的 getter,相当于 <router-view> 对它有依赖
  • 派发更新:在执行完 transitionTo 后,修改 app._route 的时候,又触发了setter,因此会通知 <router-view> 的渲染 watcher 更新,重新渲染组件,也就是调用它的render函数。
    • 因为vue在调用每个vm.$mount都会执行new Watcher,意味着每个普通视图组件实例都有渲染watcher

另,可以证明是,每个组件实例访问 $route 属性,就是访问根实例的 _route,也就是当前的路由线路,这是通过数据代理实现的。

首先看看“官方文档”是怎么对比router-link和a标签的选择的:

  • 无论是 HTML5 history 模式还是 hash 模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash 模式,无须作任何变动。
  • 在 HTML5 history 模式下,router-link 会守卫点击事件,让浏览器不再重新加载页面。
  • 当你在 HTML5 history 模式下使用 base 选项之后,所有的 to 属性都不需要写 (基路径) 了。

总结成1句话就是,router-link底层利用 HTML5 browser history 或者 hash history,这样能够维持提供单页应用并且可实现页面局部更新。

再用文本总结下router-link的源码:(具体源码可以自行翻阅,里面没有复杂的东西)

  • 作为vue组件,实现了option render函数
    • router.resolve:计算出最终跳转的 href
    • “未来的html元素”的class处理
    • 按要求绑定click等事件回调,对于一般click回调有些细节处理,最终执行 this实例.historypushreplace

源码的定义在 src/components/link.js 中:(render属性其实就是h(this.tag, data, this.$slots.default))

export default {
  name: 'RouterLink',
  props: {
    to, tag, exact, append, replace, event: /**/
    activeClass: String,
    exactActiveClass: String
  },
  render (h: Function) {
    const [router, current] = [this.$router, this.$route]
    const { location, route, href } = router.resolve(this.to, current, this.append)

    // 对于class做处理
    const classes = {}
    const globalActiveClass = router.options.linkActiveClass // 全局配置
    const globalExactActiveClass = router.options.linkExactActiveClass // 全局配置
    // 降级:
    const activeClassFallback = globalActiveClass == null
            ? 'router-link-active'
            : globalActiveClass
    const exactActiveClassFallback = globalExactActiveClass == null
            ? 'router-link-exact-active'
            : globalExactActiveClass
    const activeClass = this.activeClass == null
            ? activeClassFallback
            : this.activeClass
    const exactActiveClass = this.exactActiveClass == null
            ? exactActiveClassFallback
            : this.exactActiveClass


    const compareTarget = location.path
      ? createRoute(null, location, null, router)
      : route

    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget) // 是否是包含关系
    // 以上都是处理 activeClass和 exactActiveClass

    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location) // 里面就是this.history.replace
        } else {
          router.push(location) // 里面就是this.history.push
        }
      }
    }

    const on = { click: guardEvent }
    if (Array.isArray(this.event)) {
      this.event.forEach(e => { on[e] = handler })
    } else {
      on[this.event] = handler
    }

    const data: any = {
      class: classes
    }


    // 最后判断当前 `tag` 是否是 `<a>` 标签,`<router-link>` 默认会渲染成 `<a>` 标签,当然我们也可以修改 `tag` 的 `prop` 渲染成其他节点,这种情况下会尝试找它子元素的 `<a>` 标签,如果有则把事件绑定到 `<a>` 标签上并添加 `href` 属性,否则绑定到外层元素本身。
    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href }
    } else {
      const a = findAnchor(this.$slots.default)
      if (a) {
        a.isStatic = false
        const extend = _Vue.util.extend
        const aData = a.data = extend({}, a.data)
        aData.on = on
        const aAttrs = a.data.attrs = extend({}, a.data.attrs)
        aAttrs.href = href
      } else {
        data.on = on
      }
    }

    return h(this.tag, data, this.$slots.default)
  }
}
文章目录
  1. router-view 首次渲染的逻辑
  2. vue-router 中的响应式的实现 及 router-view 重新渲染
  3. router-link 内置组件