美文网首页
Vue-router(3.1.6)源码阅读——视图渲染

Vue-router(3.1.6)源码阅读——视图渲染

作者: 李牧敲代码 | 来源:发表于2020-04-12 10:28 被阅读0次

    路由模式,路由匹配,路由切换都搞定了,那最终就要进行视图渲染了。
    vue-router提供了2个内置组件,一个是<router-link>是用于路由切换的,<router-view>是最终路由要渲染的组件要替换的“占位组件”。

    <router-link>

    /* @flow */
    
    import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'
    import { extend } from '../util/misc'
    import { normalizeLocation } from '../util/location'
    import { warn } from '../util/warn'
    
    // work around weird flow bug
    const toTypes: Array<Function> = [String, Object]
    const eventTypes: Array<Function> = [String, Array]
    
    const noop = () => {}
    
    export default {
      name: 'RouterLink',
      props: {
        to: {
          type: toTypes,
          required: true
        },
        tag: {
          type: String,
          default: 'a'
        },
        exact: Boolean,
        append: Boolean,
        replace: Boolean,
        activeClass: String,
        exactActiveClass: String,
        ariaCurrentValue: {
          type: String,
          default: 'page'
        },
        event: {
          type: eventTypes,
          default: 'click'
        }
      },
      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)//redirectedFrom类型是Location
          : 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 => {//router-link要执行的函数
          if (guardEvent(e)) {
            if (this.replace) {
              router.replace(location, noop)
            } else {
              router.push(location, noop)
            }
          }
        }
    
        const on = { click: guardEvent }
        if (Array.isArray(this.event)) {//所有事件都按照handler处理
          this.event.forEach(e => {
            on[e] = handler
          })
        } else {
          on[this.event] = handler
        }
    
        const data: any = { class: classes }
    
        const scopedSlot =
          !this.$scopedSlots.$hasNormal &&
          this.$scopedSlots.default &&
          this.$scopedSlots.default({
            href,
            route,
            navigate: handler,
            isActive: classes[activeClass],
            isExactActive: classes[exactActiveClass]
          })
    
        if (scopedSlot) {
          if (scopedSlot.length === 1) {
            return scopedSlot[0]
          } else if (scopedSlot.length > 1 || !scopedSlot.length) {
            if (process.env.NODE_ENV !== 'production') {
              warn(
                false,
                `RouterLink with to="${
                  this.to
                }" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`
              )
            }
            return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
          }
        }
    
        if (this.tag === 'a') {
          data.on = on
          data.attrs = { href, 'aria-current': ariaCurrentValue }
        } else {
          // find the first <a> child and apply listener and href
          const a = findAnchor(this.$slots.default)
          if (a) {
            // in case the <a> is a static node
            a.isStatic = false
            const aData = (a.data = extend({}, a.data))
            aData.on = aData.on || {}
            // transform existing events in both objects into arrays so we can push later
            for (const event in aData.on) {//和下面那个for一次执行2次循环,把事件的交集拿出来
              const handler = aData.on[event]
              if (event in on) {
                aData.on[event] = Array.isArray(handler) ? handler : [handler]
              }
            }
            // append new listeners for router-link
            for (const event in on) {
              if (event in aData.on) {
                // on[event] is always a function
                aData.on[event].push(on[event])
              } else {
                aData.on[event] = handler
              }
            }
    
            const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
            aAttrs.href = href
            aAttrs['aria-current'] = ariaCurrentValue
          } else {
            // doesn't have <a> child, apply listener to self
            data.on = on
          }
        }
    
        return h(this.tag, data, this.$slots.default)
      }
    }
    
    function guardEvent (e) {
      // don't redirect with control keys
      if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
      // don't redirect when preventDefault called
      if (e.defaultPrevented) return
      // don't redirect on right click
      if (e.button !== undefined && e.button !== 0) return
      // don't redirect if `target="_blank"`
      if (e.currentTarget && e.currentTarget.getAttribute) {
        const target = e.currentTarget.getAttribute('target')
        if (/\b_blank\b/i.test(target)) return
      }
      // this may be a Weex event which doesn't have this method
      if (e.preventDefault) {
        e.preventDefault()
      }
      return true
    }
    
    function findAnchor (children) {//递归找到a标签元素
      if (children) {
        let child
        for (let i = 0; i < children.length; i++) {
          child = children[i]
          if (child.tag === 'a') {
            return child
          }
          if (child.children && (child = findAnchor(child.children))) {
            return child
          }
        }
      }
    }
    

    可以看到它依次干了这么几件事:

    1. 接受4个Props——to, tag,ariaCurrentValue, event,
    2. 获取当前的location,route,href
    3. 设置router-link激活和精确匹配的样式
    4. 创建一个守卫函数, 通过守卫函数的handler处理(根据this.replace决定用replace还是push)
    5. 如果根据tag来寻找a标签,并设置这个a标签的的一系列属性,找不到a标签,则把监听器全部绑定到router-link自身.

    <router-view>

    import { warn } from '../util/warn'
    import { extend } from '../util/misc'
    
    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
        const cache = parent._routerViewCache || (parent._routerViewCache = {})
    
        // 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) {//记录router-view的层级,同时如果vnode树中有失活但是keep-alive的组件则判定该树失活
          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
    
        // render previous view if the tree is inactive and kept-alive
        if (inactive) {
          const cachedData = cache[name]
          const cachedComponent = cachedData && cachedData.component
          if (cachedComponent) {
            // #2301
            // pass props
            if (cachedData.configProps) {
              fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
            }
            return h(cachedComponent, data, children)
          } else {
            // render previous empty view
            return h()
          }
        }
    
        const matched = route.matched[depth]
        const component = matched && matched.components[name]
    
        // render empty node if no matched route or no config component
        if (!matched || !component) {
          cache[name] = null
          return h()
        }
    
        // cache component
        cache[name] = { component }
    
        // attach instance registration hook
        // this will be called in the instance's injected lifecycle hooks
        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
          }
        }
    
        // also register instance in prepatch hook
        // in case the same component instance is reused across different routes
        ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
          matched.instances[name] = vnode.componentInstance
        }
    
        // register instance in init hook
        // in case kept-alive component be actived when routes changed
        data.hook.init = (vnode) => {
          if (vnode.data.keepAlive &&
            vnode.componentInstance &&
            vnode.componentInstance !== matched.instances[name]
          ) {
            matched.instances[name] = vnode.componentInstance
          }
        }
    
        const configProps = matched.props && matched.props[name]
        // save route and configProps in cachce
        if (configProps) {
          extend(cache[name], {
            route,
            configProps
          })
          fillPropsinData(component, data, route, configProps)
        }
    
        return h(component, data, children)
      }
    }
    
    function fillPropsinData (component, data, route, configProps) {
      // resolve props
      let propsToPass = data.props = resolveProps(route, configProps)
      if (propsToPass) {
        // clone to prevent mutation
        propsToPass = data.props = extend({}, propsToPass)
        // pass non-declared props as attrs
        const attrs = data.attrs = data.attrs || {}
        for (const key in propsToPass) {
          if (!component.props || !(key in component.props)) {
            attrs[key] = propsToPass[key]
            delete propsToPass[key]
          }
        }
      }
    }
    
    function resolveProps (route, config) {
      switch (typeof config) {
        case 'undefined':
          return
        case 'object':
          return config
        case 'function':
          return config(route)
        case 'boolean':
          return config ? route.params : undefined
        default:
          if (process.env.NODE_ENV !== 'production') {
            warn(
              false,
              `props in "${route.path}" is a ${typeof config}, ` +
              `expecting an object, function or boolean.`
            )
          }
      }
    }
    
    

    可以看到这个函数依次干了这么几件事:

    1. 首先将这个组件标记为函数式组件, 并将data.routerView 设置true,用于记录层级。
    2. 通过访问$route获得当前整个的route(因为在install.js的时候我们知道这个$route是定义在Vue.prototype上的)
    3. 定义了这个注册路由实例(vue)的方法,这个方法会在init的时候执行。(install.js)
    4. 创建了一个depth标志位根据data.routerView遍历确定每个router-view层级。然后获得对应的component,进而进行渲染。

    总结

    1. router-link是一个普通的组件,router-view是一个函数式组件。
    2. router-link接受4个Props——to, tag,ariaCurrentValue, event,
    3. router-link设置router-link激活和精确匹配的样式
    4. router-link根据tag来寻找a标签,并设置这个a标签的的一系列属性,找不到a标签,则把监听器全部绑定到router-link自身.
    5. router-link创建了一个守卫函数, 通过守卫函数的由handler处理(根据this.replace决定用replace还是push)
    6. 定义了注册路由实例的方法,这个方法会在init的时候执行
    7. 创建了一个depth标志位根据data.routerView遍历确定每个router-view层级。然后获得对应的component,进而进行渲染。

    返回目录

    代码阅读地址含注释

    相关文章

      网友评论

          本文标题:Vue-router(3.1.6)源码阅读——视图渲染

          本文链接:https://www.haomeiwen.com/subject/sdkvmhtx.html