手写vue-router(万字详解核心原理)

175 阅读7分钟

没有文案 。。。

流程:

  1. Vue.use时调用VueRouter.install方法接收Vue并导出一份供插件其它地方使用,通过Vue.mixin混入一个生命周期钩子beforeCreate

  2. 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
  3. 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
  4. router-view

    • 使用函数组件,无状态,直接用render编译,不用new,提升性能

    • render中通过上下文中的parent获取到$route,也就是router.history.current,

      • 通过$route上的matched数组,while递归找父级,
      • 标记routerView属性做递归出口判断,获取对应的index,
      • 通过matched[index]取出components,
      • 最后通过render的h函数渲染视图。
  5. 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,点击时调用router,点击时调用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,点击时调用router,点击时调用router上的push方法