vue-router实现的核心步骤
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js的核心深度集成,让构建单页面应用变得易如反 掌。
1. 应用插件
- use做了什么?
执行了插件的install方法
import Router from 'vue-router'
Vue.use(Router)
2. 创建router实例
export default new Router({...})
3. main.js中配置router实例
- 为什么需要挂到这里?
获取vue-router实例并挂载到Vue.prototype上,这样在全局的时候可以直接通过this.$router 来获取相应的数据
import router from './router'
new Vue({
router,
}).$mount("#app");
4. 添加路由视图
- router-link和router-view是哪来的?
- router-view做了什么?
<router-view></router-view>
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
vue-router源码实现
-
实现vue插件
- vue提供了插件注册机制是,每个插件都需要实现一个静态的
install方法,当执行Vue.use注册插件的时候,就会执行install方法,该方法执行的时候第一个参数强制是Vue对象。 - vue这种插件机制,使得在编写插件的时候,不需要再引入
vue了,注册的时候给插件注入了一个参数就是vue的实例 - install是static方法:类的静态方法用
static关键字定义,不能在类的实例上调用静态方法,只能够通过类本身调用。这里的install只能vue-router类调用,他的实例不能调用(防止vue-router的实例在外部调用)。
- vue提供了插件注册机制是,每个插件都需要实现一个静态的
-
解析routes选项
-
监控url变化
- html5 history api
- hash index#/login
-
实现两个全局组件
- router-link
- router-view
// mVueRouter.js
let Vue // 保存vue构造函数引用
class mVueRouter {
constructor (options) {
this.$options = options
this.routeMap = {}
// 当前的url需要时响应式的
this.app = new Vue({
data: { current: '/' }
})
}
// 初始化
init () {
// 监听hash变化
this.bindEvents()
// 解析routes
this.createRoyteMap()
// 声明组件
this.initComponents()
}
bindEvents () {
window.addEventListener('hashchange', this.onHashChange.bind(this))// 事件的回调函数,这边的this会丢失
}
onHashChange () {
this.app.current = window.location.hash.slice(1) || '/'
}
createRoyteMap () {
// 遍历用户配置的路由
this.$options.routes.forEach(item => {
this.routeMap[item.path] = item
})
}
initComponents () {
// router-link转换目标:<a href="#/">xxx</a>
Vue.component('router-link', {
props: {
to: String
},
render (h) {
// h(tag,data,children)
return h('a', {
attrs: { href: '#' + this.to }
}, [this.$slots.default])
// jsx
// return <a href={'#' + this.to}>{this.$slots.default}</a>
}
})
// 获取path对应的component,并将其渲染出来
Vue.component('router-view', {
render: (h) => {
// 只要render函数中用到的响应式数据,只要响应式数据发生了变化,render函数就会重新执行
const component = this.routeMap[this.app.current].component
return h(component)
}
})
}
}
mVueRouter.install = function (_vue) {
Vue = _vue
// 实现一个混入
Vue.mixin({
beforeCreate () {
// 获取mVueRouter实例并挂载到Vue.prototype上
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
// 路由器初始化
this.$options.router.init()
}
}
})
}
export default mVueRouter
嵌套路由
以下是嵌套路由的核心代码
Vue.component('router-view', {
// 函数式组件
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (h, { props, children, parent, data }) {
data.routerView = true
const name = props.name
const route = parent.$route
let depth = 0
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode ? parent.$vnode.data : {}
if (vnodeData.routerView) {
depth++
}
parent = parent.$parent
}
const matched = route.matched[depth]
const component = matched && matched.components[name]
return h(component, data, children)
}
})
Render函数
简单来说CreateElement就是用来生成Vnode的函数,CreateElement返回的不是真实的DOM元素,是Vnode,render函数所包含的信息会告诉vue页面上需要渲染什么节点及其子节点。
1. createElement 参数
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM property
domProps: {
innerHTML: 'baz'
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
函数式组件
之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。
一个函数式组件就像这样:
1. render函数提供了第二个参数作为上下文
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
}
})
2. context上下文包含的字段对象
组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:
- props:提供所有 prop 的对象
- children:VNode 子节点的数组
- slots:一个函数,返回了包含所有插槽的对象
- scopedSlots:(2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
- data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
- parent:对父组件的引用
- listeners:(2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
- injections:(2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的 property。
在添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context 参数,并将 this.$slots.default 更新为 context.children,然后将 this.level 更新为 context.props.level。
因为函数式组件只是函数,所以渲染开销也低很多。
在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:
程序化地在多个组件中选择一个来代为渲染; 在将 children、props、data 传递给子组件之前操作它们。
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
},
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
}
})
3. vue-router:router-view官方是怎么获取参数的
// 获取path对应的component,并将其渲染出来
Vue.component('router-view', {
// hack的方式:箭头函数取到了vue-router的实例,可是如果要使用当前router-view的this则无法拿到
// render: (h) => {
// // 只要render函数中用到的响应式数据,只要响应式数据发生了变化,render函数就会重新执行
// const component = this.routeMap[this.app.current].component
// return h(component)
// }
// 函数式组件
functional: true,
render (h, { parent }) {
const router = parent.$router
const component = router.routeMap[router.app.current].component
return h(component)
}
})