Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vue-router 源码分析-组件 #54

Open
dwqs opened this issue Jun 29, 2017 · 1 comment
Open

vue-router 源码分析-组件 #54

dwqs opened this issue Jun 29, 2017 · 1 comment

Comments

@dwqs
Copy link
Owner

dwqs commented Jun 29, 2017

上篇中, 大致分析了 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 源码分析-整体流程

@cobish
Copy link

cobish commented Sep 1, 2018

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

data.routerViewDepth = depth
data.hook = {}

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

parent._routerViewCache
parent._inactive

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants