dwqs / blog

:dog: :clap: :star2: Welcome to star

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

vue-router 源码分析-组件

dwqs opened this issue · comments

上篇中, 大致分析了 vue-router 的整体流程. VueRouter 除了提供 router 功能, 还提供了两个组件: router-linkrouter-view, 源码均在 src/components 目录下.

本文分析的 vue-router 的版本为 2.6.0.

Link

<router-link> 组件通过 to 属性指定目标地址, 为用户提供路由导航. 默认渲染成带有正确链接的 <a> 标签. 其源码在 src/components/link.js:

/* @flow */

import {createRoute, isSameRoute, isIncludedRoute} from '../util/route'
import {_Vue} from '../install'

// 类型定义
const toTypes: Array<Function> = [String, Object]
const eventTypes: Array<Function> = [String, Array]

export default {
	 // 组件名
    name: 'router-link',
    props: {
    	 // 目标路由
        to: {
            type: toTypes,
            required: true
        },
        // 目标标签
        tag: {
            type: String,
            default: 'a'
        },
        // 完整模式, 如果为 true 那么也就意味着
        // 绝对相等的路由才会增加 activeClass
        // 否则是包含关系, 默认是 false
        exact: Boolean,
        // 是否在当前路径路径添加基路径 默认 false
        append: Boolean,
        // 是否使用 router.replace() 来替换 router.push() 默认 false
        replace: Boolean,
        // 链接激活时使用的 CSS 类名
        activeClass: String,
        // 完整模式下链接激活时使用的 CSS 类名
        exactActiveClass: String,
        // 触发导航的事件
        event: {
            type: eventTypes,
            default: 'click'
        }
    },
    render (h: Function) {
        // 得到 router 实例以及当前激活的 route 对象
        const router = this.$router
        const current = this.$route

        // 获取当前匹配的 route信息
        const {location, route, href} = router.resolve(this.to, current, this.append)
			
        const classes = {}
        const globalActiveClass = router.options.linkActiveClass
        const globalExactActiveClass = router.options.linkExactActiveClass
        
        // 获取 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 = location.path
            ? createRoute(null, location, null, router)
            : route

        classes[exactActiveClass] = isSameRoute(current, compareTarget)
        
        // 完成模式还是包含模式
        classes[activeClass] = this.exact
            ? classes[exactActiveClass]
            : isIncludedRoute(current, compareTarget)
		 
       // 事件处理
        const handler = e => {
            if (guardEvent(e)) {
                if (this.replace) {
                    router.replace(location)
                } else {
                    router.push(location)
                }
            }
        }
		 
        // 事件监听
        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}
        } else {
            // 找到第一个 <a> 并绑定事件和 href 属性
            const a = findAnchor(this.$slots.default)
            if (a) {
                // in case the <a> is a static node
                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)
    }
}

// router-link 的 event 绑定
function guardEvent(e) {
    // 忽略功能键的点击跳转
    if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return

    // 已经阻止
    if (e.defaultPrevented) return
    
    // 右击不跳转
    if (e.button !== undefined && e.button !== 0) return
    
    // 忽略 `target="_blank"
    if (e.currentTarget && e.currentTarget.getAttribute) {
        const target = e.currentTarget.getAttribute('target')
        if (/\b_blank\b/i.test(target)) return
    }
    
    // 阻止默认行为
    if (e.preventDefault) {
        e.preventDefault()
    }
    return true
}

function findAnchor(children) {
    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
            }
        }
    }
}

从上述代码可以看出, router-link 组件会根据绑定的事件类型和 to 属性, 去调用 push 或者 replace 更新路由, 同时根据 exact 属性来添加 active class.

View 组件

router-view 组件用于渲染与路由匹配的 components, 其源码在 src/components/view.js 中定义的:

import {warn} from '../util/warn'

export default {
    // 组件名
    name: 'router-view',
    // 显示指定为该组件是函数式组件
    // 函数式组件: https://cn.vuejs.org/v2/guide/render-function.html#函数化组件
    functional: true,
    
    props: {
    	 // 视图名称, 默认是 default	
        name: {
            type: String,
            default: 'default'
        }
    },
    render (_, {props, children, parent, data}) {
        data.routerView = true
        
        // 渲染函数
        const h = parent.$createElement
        const name = props.name
        // route 对象
        const route = parent.$route
        // 缓存
        const cache = parent._routerViewCache || (parent._routerViewCache = {})

        // 组件所在深度
        let depth = 0
        let inactive = false
        
        // 当 _routerRoot 指向 Vue 实例时就终止循环
        while (parent && parent._routerRoot !== parent) {
            if (parent.$vnode && parent.$vnode.data.routerView) {
                depth++
            }
            // 处理 keep-alive 组件
            if (parent._inactive) {
                inactive = true
            }
            parent = parent.$parent
        }
        data.routerViewDepth = depth

        // 渲染缓存的 keep-alive 组件
        if (inactive) {
            return h(cache[name], data, children)
        }

        // 根据组件深度获取对应的 route
        const matched = route.matched[depth]
        
        if (!matched) {
        	  // 没有对应的 route 就渲染一个空节点
            cache[name] = null
            return h()
        }

        // 得到要渲染组件
        const component = cache[name] = matched.components[name]

        // 添加注册钩子, 钩子会被注入到组件的生命周期钩子中
        // 在 src/install.js, 会在 beforeCreate 钩子中调用
        data.registerRouteInstance = (vm, val) => {
            // val 为空就注销注册
            const current = matched.instances[name]
            if (
                (val && current !== vm) ||
                (!val && current === vm)
            ) {
                matched.instances[name] = val
            }
        }

        // 在 prepatch 钩子中注册组件实例, 
        // 因为不同路由可能使用同一个组件, 便于组件复用
        ;
        (data.hook || (data.hook = {})).prepatch = (_, vnode) => {
            matched.instances[name] = vnode.componentInstance
        }

        // resolve props
        data.props = resolveProps(route, matched.props && matched.props[name])

        // 调用 createElement 函数 渲染匹配的组件
        return h(component, data, children)
    }
}

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.`
                )
            }
    }
}

router-view 被定义为一个无状态组件, 因为 router-view 只是一个函数, 用于渲染与路由对应的组件, 不需要管理或者监听任何传递给它的状态, 也没有生命周期方法, 所以渲染开销会低很多.

相关文章

vue-router 源码分析-整体流程

有两个疑惑,像以下这些赋值给 data 的属性是给谁用的?

data.routerViewDepth = depth
data.hook = {}

还有 parent 自带的一些属性是 vue 里自带的吗?

parent._routerViewCache
parent._inactive