没有文案 。。。
流程:
-
Vue.use时调用VueRouter.install方法接收Vue并导出一份供插件其它地方使用,通过Vue.mixin混入一个生命周期钩子beforeCreate
-
VueRouter的constructor中
-
调用createMatcher方法创建一个全局匹配器:this.matcher,其中包含2个重要方法:
- match:用于查找createMatcher函数闭包中的 pathMap匹配表中的对应route数据
- addRoutes:调用createRouteMap方法添加新的route匹配表,也是router动态添加的核心方法
-
createRouteMap方法中递归迭代route中的每一项,将其扁平化,返回一个以path为健route为值的匹配表,同时表中会记录parent供渲染组件时向上查找
-
通过判断mode,选择new哪个类(hash/history),得到的实例赋值到router.history上,
- 实例中有current属性,current属性中包含path和matched,
- matched是一个数组 存放的当前path所关联的所有父级route数据
- 后面router-view渲染视图时会取出matched逐个渲染,并通过其中的parent属性判断父子关系
-
hash类和history类用于区分hash模式和history模式,都继承base类,base类中有关键的跳转方法:transtionTo
- 先从闭包中的pathMap中取出匹配的record (通过router.match(path)方法,调用router.matcher的match方法从pathMap中查找)
- 拿到record后调用createRoute方法,遍历找出当前path所关联的所有父级,放入matched中并返回
- 比对path和current.patch,route.matched和current.matched,判断是否重复跳转
- 执行收集的beforeHooks钩子函数,通过runQueue+迭代函数,执行hook函数并将route,current,和next回传,next中执行updateRoute和初始化时创建监听器的函数
- updateRoute中将新的route赋值给current,相当于更新this.router.histore.current,同时还要更新route,以确保vm. route为最新 === this.$route
-
hash和history有很多区别,需要有单独的处理逻辑,所以拆分两个类
- 获取路径:hash > window.location.hash.slice(1);history > window.location.pathname
- 监听跳转:hash > 监听hashchange;history > 监听popState
- 跳转方式:hash > window.location.hash = xxx;history > history.pushState
-
-
VueRouter初始话完成后,router实例上有了matcher方法,new Vue时将router作为选项传入,vm.$options上也有了router属性,此时回到install的beforeCreate中,
- 将router挂载到this.touter,this. routerRoot = this,后代组件通过routerRoot可找到父级的router也就是router实例,以此传递给每一个后代组件。
- 调用router.init方法初始化
- 借用Vue.util.defineReactive方法劫持this上的route属性,指向router.history.current
- defineProperty将$route的访问代理到this.routerRoot. route
- defineProperty将$router的访问代理到this.routerRoot. router
-
router-view
-
使用函数组件,无状态,直接用render编译,不用new,提升性能
-
render中通过上下文中的parent获取到$route,也就是router.history.current,
- 通过$route上的matched数组,while递归找父级,
- 标记routerView属性做递归出口判断,获取对应的index,
- 通过matched[index]取出components,
- 最后通过render的h函数渲染视图。
-
-
router-link
- 函数组件,parent.$router.push > VueRouter类的方法,调用history.transtionTo进行跳转,调用history.pushState改变hash/history
- slots().default
1.注册
install注册插件,注册两个全局组件router-link 和 router-view
VueRouter构造类中,通过传入的options初始化mod、beforeHooks、matcher匹配表、history等,并根据mode选择使用history实例
VueRouter
import install, { Vue } from './install';
class VueRouter {
constructor(options = {}) {
const routes = options.routes;
this.mode = options.mode || 'hash';
this.beforeHooks = []
this.matcher = createMatcher(options.routes || []); // 路径匹配表
switch(this.mode) {
case 'hash': // location.hash => push
this.history = new Hash(this)
break
case 'history': // pushState => push
this.history = new HTML5History(this);
break
}
}
}
VueRouter.install = install;
install
export let Vue;
import RouterLink from './components/link';
import RouterView from './components/view'
export default function install(_Vue){
Vue = _Vue;
Vue.mixin({
beforeCreate(){
}
})
Vue.component('router-link',RouterLink)
Vue.component('router-view',RouterView)
}
2. 创建匹配器
- createMatcher方法将oprions.routes解析成pathMap路径映射表,通过createRouteMap方法递归将routes扁平化得到pathMap,返回以一个以path为键route为值的对象,后续根据该对象进行组件渲染;返回match方法(通过path查找pathMap中的对应route)和addRoutes方法(添加新的route)
- createRouteMap方法中初始化pathMap映射表,通过遍历传入的routes,调用addRouteRecord方法往pathMap上面添加映射属性,随后将pathMap对象返回,
- addRouteRecord方法接收递归往pathMap上添加包含path、component、props、parent的内容,
createMatcher
import { createRouteMap } from "./create-route-map";
export function createMatcher(routes){
// 路径和记录匹配 / record
let {pathMap} = createRouteMap(routes); // 创建映射表
function match(path){
return pathMap[path]; // 帮你去pathMap中找到对应的记录
};
function addRoutes(routes){
// 面试会问 动态路由的实现 就是将新的路由插入到老的路由的映射表中
createRouteMap(routes,pathMap); // 将新的路由添加到 pathMao中
}
return {
addRoutes,
match
}
}
createRouteMap
export function createRouteMap(routes,oldPathMap){
// 如果有oldPathMap 我需要将routes格式化后 放到oldPathMap中
// 如果没有传递 需要生成一个映射表
let pathMap = oldPathMap || {}
routes.forEach(route=>{
addRouteRecord(route,pathMap);
})
return {
pathMap
}
}
addRouteRecord
function addRouteRecord(route,pathMap,parent){
let path =parent? `${parent.path}/${route.path}` :route.path ;
// 将记录 和 路径关联起来
let record = { // 最终路径 会匹配到这个记录,里面可以自定义属性等
path,
component:route.component, // 组件
props:route.props || {},
parent
}
pathMap[path] = record;
route.children && route.children.forEach(childRoute=>{
// 在循环儿子的时候将父路径也同时传入,目的是为了在子路由添加的时候可以拿到父路径
addRouteRecord(childRoute,pathMap,record);
})
}
3. 初始化vuerouter.history
根据mode属性选择性new Hash/History,两个构造类都继承了Base类,base类中初始化router和调用createRoute方法初始化current
createRoute方法接收route和location,目的为了确认当前route的父级是谁,将关系链上所有route都放到一个matched数组中,后面router-view组件将通过该数组渲染所关联的所有组件;通过while循环传入的route向上找parent,同时往matched上unshift,最后返回location和matched的对象
switch(this.mode) {
case 'hash': // location.hash => push
this.history = new Hash(this)
break
case 'history': // pushState => push
this.history = new HTML5History(this);
break
}
base.js
function createRoute(record, location) { // 创建路由
const matched = [];
// 不停的去父级查找
if (record) {
while (record) {
matched.unshift(record);
record = record.parent;
} // /about/a => [about,aboutA]
}
return { ...location,matched }
}
export default class History {
constructor(router) {
this.router = router;
// 有一个数据来保存路径的变化
this.current = createRoute(null, {
path: '/'
}); // => {path:'/',matched:[]}
}
}
Hash.js / history.js
import History from './base'
export default class Hash extends History {
constructor(router) {
super(router);
ensureHash();
}
}
function ensureHash() {
// hash路由初始化的时候 需要增加一个默认hash值 /#/
if (!window.location.hash) {
window.location.hash = '/';
}
}
4.初始化钩子函数
VueRouter原型方法beforEach将传入的fn维护进beforeHooks中,在通过transtionTo方法进行跳转的时候,执行该回调函数
还有其他钩子函数原理也是一样的,就是发布订阅模式
VueRouter中
class VueRouter {
constructor(options = {}) {
// ……
this.beforeHooks = []
}
beforeEach(fn){
this.beforeHooks.push(fn);
}
}
transtionTo
function runQueue(queue,iterator,cb){
function step(index){
if(index >= queue.length) return cb();
let hook = queue[index];
iterator(hook,()=>step(index+1)); // 第二个参数什么时候调用就走下一次的
}
step(0);
}
transitionTo(path, cb) { // {path:'/',matched:[record]}
// ……
let queue = this.router.beforeHooks; // hooks
const iterator = (hook,next) =>{ // 此迭代函数可以拿到对应的hook
hook(route,this.current,next);
}
runQueue(queue,iterator,()=>{
this.updateRoute(route);
cb && cb(); // 默认第一次cb是hashchange
// 后置的钩子
})
}
updateRoute(route){
this.current = route; // 不光要改变current , 还有改变_route
this.cb && this.cb(route);
}
5.完善install
此时this上已经有了router属性(new VueRouter的实例,传入main.js入口的new Vue构造函数中),通过$options.router获取
利用Vue的mixin+beforeCreate将options传入的router数组 添加到this_router上,劫持$router指向_router
利用Vue.util.defineReactive将router.history.current变成响应式数据,劫持$route指向route,history.current就是通过判断mode后new Hash/History创建的route树,通过递归遍历routes创建的,属性上有matched表,同时缓存中还有一个pathMap映射表
new Hash时 执行ensureHash函数默认跳转一次(将/改为/#/)
初始化init 调用_router.init
install.js中
export let Vue;
import RouterLink from './components/link';
import RouterView from './components/view'
export default function install(_Vue){
Vue = _Vue;
Vue.mixin({
beforeCreate(){
if(this.$options.router){
this._router = this.$options.router;
this._routerRoot = this; // 表示根组件上有一个唯一的标识叫_routerRoot 指向了自己
// 初始化路由的逻辑 只初始化一次
this._router.init(this); // this整个应用的根
// vuex中的state 在哪里使用就会收集对应的watcher
// vueRouter中current里面的属性在哪使用,就会收集对应的watcher
Vue.util.defineReactive(this,'_route',this._router.history.current);
}else{
this._routerRoot = this.$parent && this.$parent._routerRoot
}
}
})
Object.defineProperty(Vue.prototype,'$router',{ // 方法
get(){ return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype,'$route',{ // 属性
get(){ return this._routerRoot._route}
});
Vue.component('router-link',RouterLink)
Vue.component('router-view',RouterView)
}
6. router.init
调用transtionTo方法进行一次默认跳转,传入一个路由监听函数setUpListener
调用history实例的listener方法订阅一个监听函数,传入一个函数,函数对_route重新赋值(关联的响应式数据),Base构造类中接收函数并保存到this上,路由变化时调用该函数
VueRouter中
class VueRouter {
constructor(options = {}) {
// ……
}
init(vm) {
const history = this.history; // 当前管理路由的
// 页面初始化完毕后 需要先进行一次跳转 跳转到某个路径
const setUpListener = () =>{
history.setUpListener(); // hash和history各自监听url变化的方法
}
history.transitionTo(
history.getCurrentLocation(), // hash和history各自的获取路径方法
setUpListener
);
history.listen((route)=>{
vm._route = route; // 监听 监听如果current变化了 就重新的给 _route赋值
})
}
}
base.js中
export default class History {
constructor(router) {
// ……
}
listen(cb){
this.cb = cb; // 保存当前的监听函数,该函数用于修改_route
}
transitionTo(path, cb) { // {path:'/',matched:[record]}
// ……
runQueue(queue,iterator,()=>{
this.updateRoute(route);
// ……
})
}
updateRoute(route){
this.current = route; // 不光要改变current , 还有改变_route
this.cb && this.cb(route); // 执行cb修改_route
}
}
7.transitionTo
跳转的核心方法,所有路由跳转都通过该方法拦截实现
先找到path对应的pathMap表中的数据,通过该条数据,调用createRoute方法找出其关系链上的所有数据(matched) 得到route
判断path和current上的path以及route和current.matched的长度是否相等 如果为true 则说明重复跳转,直接return
runQueue方法执行beforeHooks中收集的钩子回调函数,随后调用updateRoute方法和初始化时传入的回调函数(监听路由变化的函数)
updateRoute方法重新赋值current,以及执行收集的listener监听函数(修改响应式的route,current变了响应式route也要变)
create-matcher.js
export function createMatcher(routes){
let {pathMap} = createRouteMap(routes); // 创建映射表
function match(path){
return pathMap[path]; // 帮你去pathMap中找到对应的记录
};
}
VueRouter中
class VueRouter {
// ……
match(location){
return this.matcher.match(location);
}
}
base.js中
export default class History {
// ……
transitionTo(path, cb) {
let record = this.router.match(path); // 匹配到后
let route = createRoute(record, { path });
if (path === this.current.path && route.matched.length === this.current.matched.length) {
return
}
let queue = this.router.beforeHooks;
const iterator = (hook,next) =>{
hook(route,this.current,next);
}
runQueue(queue,iterator,()=>{
this.updateRoute(route);
cb && cb(); // 默认第一次cb是hashchange
})
}
updateRoute(route){
this.current = route; // 不光要改变current , 还有改变_route
this.cb && this.cb(route);
}
}
8. push方法
调用histore.transtionTo进行跳转,跳转完后调用pushState方法修改hash值
class VueRouter {
// ……
push(location){
// 跳转页面
this.history.transitionTo(location,()=>{
// pushState
this.history.pushState(location);
})
}
// ……
}
9.router-view
函数式组件中 通过render函数接收h函数和context,context中包含parent和data,parent中有$route属性,
通过$route中的matched找到对应的关系链表,该表中存放的是组件数据,h函数渲染component
export default {
functional: true,
render(h, { parent, data }) { // current = {matched:[]} .$route // data里面我可以增加点标识
// 内部current变成了响应式的
// 真正用的是$route this.$route = current; current = xxx
let route = parent.$route; // 获取current对象
// 依次的将matched 的结果赋予给每个router-view
// 父 * 父 * -> 父 * -> 子 *
let depth = 0;
while (parent) { // 1.得是组件 <router-view></router-view> <app></app>
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++;
}
parent = parent.$parent; // 不停的找父亲
}
// 两个router-view [ /about /about/a] /about/a
let record = route.matched[depth]; // 默认肯定先渲染第一层
if (!record) {
return h() // 空
}
// 渲染匹配到的组件,这里一直渲染的是第一个
data.routerView = true;
return h(record.component, data); // <router-view routeView=true></router-view>
}
}
10.router-link
函数组件,render接收的context中,通过parent找到router上的push方法
export default {
functional: true, // 函数式组件, 会导致render函数中没有this了
// 正常组件是一个类 this._init() 如果是函数式组件就是一个普通函数
props: { // 属性校验
to: {
type: String,
required: true
}
},
// render的第二个函数 是内部自己声明一个对象
render(h, { props, slots, data, parent }) { // render 方法和 template 等价的 -> template语法需要被编译成render函数
const click = () => {
// 组件中的$router
parent.$router.push(props.to)
}
// jsx 和 react语法一样 < 开头的表示的是html {} js属性
return <a onClick={click} > {slots().default} </a>
}
}
11. 总结
1.注册及数据劫持:
install注册插件,注册两个全局组件router-link 和 router-view
VueRouter构造类中,通过传入的options初始化mod、beforeHooks、matcher匹配表、history等,并根据mode选择使用history实例
2.创建匹配表
createMatcher方法将oprions.routes解析成pathMap路径映射表,通过createRouteMap方法递归将routes扁平化得到pathMap,返回以一个以path为键route为值的对象,后续根据该对象进行组件渲染;返回match方法(通过path查找pathMap中的对应route)和addRoutes方法(添加新的route)
createRouteMap方法中初始化pathMap映射表,通过遍历传入的routes,调用addRouteRecord方法往pathMap上面添加映射属性,随后将pathMap对象返回,
addRouteRecord方法接收递归往pathMap上添加包含path、component、props、parent的内容,
3.初始化history
根据mode属性选择性new Hash/History,两个构造类都继承了Base类,base类中初始化router和调用createRoute方法初始化current
createRoute方法接收route和location,目的为了确认当前route的父级是谁,将关系链上所有route都放到一个matched数组中,后面router-view组件将通过该数组渲染所关联的所有组件;通过while循环传入的route向上找parent,同时往matched上unshift,最后返回location和matched的对象
4.初始化beforeEach
VueRouter原型方法beforEach将传入的fn维护进beforeHooks中,在通过transtionTo方法进行跳转的时候,执行该回调函数
还有其他钩子函数原理也是一样的,就是发布订阅模式
5.完善install
此时this上已经有了router属性(new VueRouter的实例,传入main.js入口的new Vue构造函数中),通过$options.router获取
利用Vue的mixin+beforeCreate将options传入的router数组 添加到this_router上,劫持$router指向_router
利用Vue.util.defineReactive将router.history.current变成响应式数据,劫持$route指向route,history.current就是通过判断mode后new Hash/History创建的route树,通过递归遍历routes创建的,属性上有matched表,同时缓存中还有一个pathMap映射表
new Hash时 执行ensureHash函数默认跳转一次(将/改为/#/)
初始化init 调用_router.init
6.执行router.init
调用transtionTo方法进行一次默认跳转,传入一个路由监听函数setUpListener
调用history实例的listener方法订阅一个监听函数,传入一个函数,函数对_route重新赋值(关联的响应式数据),Base构造类中接收函数并保存到this上,路由变化时调用该函数
7.transtionTo方法
跳转的核心方法,所有路由跳转都通过该方法拦截实现
先找到path对应的pathMap表中的数据,通过该条数据,调用createRoute方法找出其关系链上的所有数据(matched) 得到route
判断path和current上的path以及route和current.matched的长度是否相等 如果为true 则说明重复跳转,直接return
runQueue方法执行beforeHooks中收集的钩子回调函数,随后调用updateRoute方法和初始化时传入的回调函数(监听路由变化的函数)
updateRoute方法重新赋值current,以及执行收集的listener监听函数(修改响应式的route,current变了响应式route也要变)
8.push方法
调用histore.transtionTo进行跳转,跳转完后调用pushState方法修改hash值
9. router-view组件
函数式组件中 通过render函数接收h函数和context,context中包含parent和data,parent中有$route属性,
通过$route中的matched找到对应的关系链表,该表中存放的是组件数据,h函数渲染component
10. router-link组件
函数组件,render接收的context中,通过parent找到router上的push方法