vue-router源码分析(六)

499 阅读3分钟

六、router-view组件

router-view组件在执行vue.use的时候,通过Vue.component进行初始化,router-view组件的编写是通过functional式组件(使组件无状态 (没有 data) 和无实例 (没有 this 上下文)。他们用一个简单的 render 函数返回虚拟节点使它们渲染的代价更小。)的方式创建组件。在RouterView的render函数中,首先会通过第二个参数中的拿到parent.$createElementparent就是当前组件的父组件,假如我们把router-view放入app.vue中,那么他的parent就会指向app.vue。然后通过const name = props.name拿到传入的name,这是为了支持router的命名视图部分,然后执行const route = parent.$route$route在调用install的时候通过Object.defineProperty在vue.prototype上进行了挂载,他最终的定义是this._routerRoot._routethis._routerRoot在执行beforeCreate钩子的时候会进行挂载,指向根vue实例的this,_route通过Vue.util.defineReactive挂载到了根部实例上,是一个响应式的数据,他返回的是this._router.history.current,也就是 this.$options.router.history.currentthis.$options.router就是VueRouter实例,history.current就是当前的Route。之后他会首先定义一个depth,然后执行while循环,假如是app.vue中的router-view,那么当执行while中判断的时候,首先当前的parent也就是app.vue他是有的并且他的_routerRoot(vue根部实例)并不等于app.vue,所以进入循环,首先他会拿到parent.$vnode.data,判断其中有没有routerView属性,如果有的话,那么 depth++,然后让parent = parent.$parent,也就是继续向上找父组件的父组件,data.routerView定义在render函数刚开始的时候,也就是说,如果对于app.vue来说,他的首次render的父组件if (vnodeData.routerView)一定是false,也就是说,他的depth是0。那假设这样一个场景,App.vue中写着router-view组件,其中渲染/a的a组件,/a的a组件又嵌套这/a/b 的b组件,/a/b的b组件又嵌套这/a/b/c 的c组件,形成了这样一个嵌套路由的关系,那么b组件的router-view,他首先会找到b组件,b组件在执行router-view的render函数的时候会设置,data.routerView = true,这样的话,当c组件在执行render的时候parent.$vnode.routerView就可以拿到之前在执行b组件的render的data.routerView = true,也就是说会执行depth++,c组件会先找到b组件,然后parent = parent.$parent,也就是在执行while循环的时候,就成了a组件,那么a组件因为之前执行了render,也会拿到routerView是true,这样又会找到app组件,最后找到根部实例,所以c组件最终的depth为2。那么为什么要拿到depth,const matched = route.matched[depth],是为了配合之前的route.matchedmatched会存放当前路由的record和他的父路由的record,也就是说,matched可以正确的取到当前的record。然后再执行const component = matched && matched.components[name],根据传入的name取到对应的component,最后执行return h(component, data, children)进行渲染。

// src/history
export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // used by devtools to display a router-view badge
    data.routerView = true
    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    ...
    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth
    ...
    const matched = route.matched[depth]
    const component = matched && matched.components[name]
    ...
    return h(component, data, children)
  }
}

router-view组件中还定了data.registerRouteInstance,当组件触发beforeCreate的时候,会执行registerInstance(this, this)registerInstance函数会执行vm.$options._parentVnode.data.registerRouteInstance(vm,callVal),也就是router-view中的registerRouteInstance函数,registerRouteInstance函数会执行matched.instances[name] = val,这样就对record的instances进行了赋值,赋值为当前渲染出的组件的this。这样在执行导航守卫的时候,flatMapComponents函数就可以通过m.instances[key]拿到当前组件的this。当组件执行destroyed的时候,同样会执行 matched.instances[name] = val,把当前的instances重新赋值为空

// src/history
export default {
  name: 'RouterView',
  functional: true,
  ...
  render (_, { props, children, parent, data }) {
    data.registerRouteInstance = (vm, val) => {
        // val could be undefined for unregistration
        const current = matched.instances[name]
        if (
          (val && current !== vm) ||
          (!val && current === vm)
        ) {
          matched.instances[name] = val
        }
      }
  }
}

// src/install.js
  const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
  }
  
  Vue.mixin({
    beforeCreate () {
      ...
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

假如我当前访问的路径是/,那么router-view的render函数,在执行到const matched = route.matched[depth],此时route.matched为空,所以matched也为空,他会执行下边的return h()也就是往页面中router-view的位置,渲染了一个注释节点。那么当切换路由为/a,此时会再次触发router-view的render,render函数,只有当组件被重新渲染的时候,才会触发,那么router-view是如何做到重新渲染的。在触发vue根部实例的beforeCreate的时候,会执行 Vue.util.defineReactive(this, '_route', this._router.history.current),他会把this._route定义为一个响应式的对象,值为this._router.history.current,在router-view的render函数中,会通过const route = parent.$route,访问到this._route,触发他的getter进行依赖收集,那么当这个值发生了改变,会触发相应的渲染watcher的update,会进行页面的重新渲染。那么什么时候执行了setter,在根部vue组件执行beforeCreate的时候,会执行VueRouter的init,执行history.listen,给history的cb赋值为传入的回调函数,这个回调函数,是对app._route的重新赋值,也就是当执行完HistorytransitionTo中的confirmTransitionupdateRoute的时候,会触发这个cb()也就是触发重新的赋值

// src/install.js
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        ...
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } 
     ...
    }
  })
// src/history/base.js
export class History {
  ...
  listen (cb: Function) {
    this.cb = cb
  }
  transitionTo (
    location: RawLocation,
    onComplete?: Function,
    onAbort?: Function
  ) {
    let route
    // catch redirect option https://github.com/vuejs/vue-router/issues/3201
    try {
      route = this.router.match(location, this.current)
    }
    const prev = this.current
    this.confirmTransition(
      route,
      () => {
        this.updateRoute(route)
        ...
      },
      ...
    )
  }
  updateRoute (route: Route) {
    this.current = route
    this.cb && this.cb(route)
  }
  ...
}

七、router-link组件

router-link组件通过render函数渲染,首先拿到router实例和当前的路径,然后调用router.resolve函数,传入this.to(props中传入),current(当前路径)和this.append(props中传入),router的resolve函数,首先通过normalizeLocation拿到location,然后通过this.match也就是this.matcher.match拿到要跳转的route,然后通过createHref把路径和history.base做一个拼接。拿到要跳转的路径之后,根据activeClass->router.options.linkActiveClass->'router-link-active'做这样的一个类名的处理,exactActiveClass也是相同的方式。exactActiveClass,通过isSameRoute函数判断跳转的路由,和当前路由他们是否完全相同,如果相同,则为true。最终把他们作为data.class,传递给createElement。接着他们声明一个handler回调函数,handler首先通过guardEvent做了一层保护,然后触发router.replacerouter.push。之后给on进行赋值,根据传入的event也就是触发方式(默认click),遍历的添加到on中,把handler作为回调函数。然后会判断你传入的tag属性,默认tag是a标签。

  render (h: Function) {
    const router = this.$router
    const current = this.$route
    const { location, route, href } = router.resolve(
      this.to,
      current,
      this.append
    )

    const classes = {}
    const globalActiveClass = router.options.linkActiveClass
    const globalExactActiveClass = router.options.linkExactActiveClass
    // Support global empty active class
    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 = route.redirectedFrom
      ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
      : route

    classes[exactActiveClass] = isSameRoute(current, compareTarget)
    classes[activeClass] = this.exact
      ? classes[exactActiveClass]
      : isIncludedRoute(current, compareTarget)

    const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null

    const handler = e => {
      if (guardEvent(e)) {
        if (this.replace) {
          router.replace(location, noop)
        } else {
          router.push(location, noop)
        }
      }
    }

    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 }

    ...

    if (this.tag === 'a') {
      data.on = on
      data.attrs = { href, 'aria-current': ariaCurrentValue }
    } else {
     ...
      if (a) {
       ...
      } else {
       ...
      }
    }

    return h(this.tag, data, this.$slots.default)
  }