您的位置:首页 > Web前端 > Vue.js

Vue 技术栈 带你探究 vue-router 源码 手写vue-router

2020-04-07 18:31 1551 查看

写在开头

学习完了ES 6基础,推荐阅读:ECMAScript 6 全套学习目录 整理 完结

现在开始逐步深入Vue 技术栈,想了想,技术栈专栏的主要内容包括:

1、Vue源码分析
2、手把手教 保姆级 撸代码
3、无惧面试,学以致用,继承创新
4、谈谈前端发展与学习心得
5、手写源码技术栈,附上详细注释
6、从源码中学习设计模式,一举两得
7、编程思想的提升及代码质量的提高
8、通过分析源码学习架构,看看优秀的框架
9、项目实战开发
10、面试准备,完善个人简历

暂时想到的就这么多,把这列举的10点做好了,我觉得也OK了,欢迎一起学习,觉得不错的话,可以关注博主,专栏会不断更新,可以关注一下,传送门~

学习目录

为了方便自己查阅与最后整合,还是打算整个目录,关于Vue技术栈优秀的文章:

Vue 技术栈 手写响应式原理 到 探索设计模式

Vue 技术栈 教你玩"坏" v8引擎 吃透 js 内存回收机制

文章目录

  • vue插件
  • hash 与 history
  • vue插件基础知识
  • Vue插件开发一系列api(开始探索源码)
  • 手写Vue-router(核心)
  • 总结
  • 正文

    Vue路由的工作流程

    前端路由和后端路由的区别

    自从前后端分离后,说路由不再仅是说后端路由了,我们前端也有了路由。路由简单来说,就是分发请求,将对应的请求分发到应该到的位置。

    后端路由-mvc的时代:

    • 输入url -》 请求发送到服务器 -》 服务器请求解析的路径 -》 拿取对应页面 -》 返回出去

    前端路由-spa应用:

    • 输入url -》js解析地址 -》 找到对应地址的页面 -》 执行页面生成的js -》 生成页面

    前端路由无需发送服务器,通过js进行解析,在浏览器上进行导向

    vue-router 工作流程

    vue插件

    请读者阅读以下代码,这就是vue-router的默认配置,最终返回给vue的是一个new VueRouter,也就是说是一个对象,而这个对象里面就有我们之前图示流程的

    current
    变量。

    import Vue from 'vue'import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'
    
    Vue.use(VueRouter)
    
    const routes = [
    {
    path: '/',
    name: 'Home',
    component: Home
    },
    {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
    }
    ]
    
    const router = new VueRouter({
    routes
    })
    
    export default router
    vue 与 vue-router工作过程

    再次回到我们的vue路由的工作流程,最下面三个部分是由

    vue-router
    来实现的,而上面是
    vue
    来工作的,vue一直监视着
    current
    变量,而vue-router能改变
    current
    ,一旦改变,就会触发监听事件,根据current来获取新的组件,然后vue去
    渲染
    新的组件,客户就能看到
    新的界面
    了。总的来说就是:(两个监听,一个渲染)

    上述文字类的表述或许不能让你恍然大悟,接下来我们就化繁为简,将整个路由过程进行实现:

    PS:但是在研究深入一点的知识前,为了照顾小白,还是从基础开始讲起,已经熟悉的读者可以选择性阅读。

    hash 与 history

    vue-router是怎么触发监听事件的呢?

    其实就是用到了hash,这里对于前端来说就着重介绍hash了,history记得会有一定兼容性问题。

    hash

    1、#号后的就是hash的内容
    2、可以通过location.hash拿到
    3、可以通过onhashchange监听hash的改变

    history

    1、history即正常的路径
    2、可以通过location.pathname拿到
    3、可以通过onpopstate监听history的改变

    对于hash,我们可以在控制台通过

    location.hash
    获取值(如果没有#就获得空字符串),如下所示:

    监听hash的改变

    window.onhashchange=function(){
    console.log('hash值已改变!')
    }


    history模式与上述方法类似

    vue插件基础知识

    vue-router、vuex等其实都是属于vue的插件,这些插件都是我们平常很多次使用的,下文将会循序渐进教你vue插件是如何开发的,我们怎样开发一个vue插件

    我们不管是使用vue-router还是vuex都会调用Vue.use()这个方法,如下图所示,但是你有思考过Vue.use()到底是干什么用的呢?有什么作用呢?

    进行实践,在

    main.js
    中我们进行如下操作,定义一个方法a,然后调用
    Vue.use()
    方法

    import Vue from 'vue'import App from './App.vue'
    import router from './router'
    
    Vue.config.productionTip = false
    
    function a(){
    console.log(6);
    }
    
    Vue.use(a);
    new Vue({
    router,
    render: h => h(App)
    }).$mount('#app')

    执行结果:

    从上述结果来看的话,我们给Vue一个方法,它就会执行一遍

    那么,我们给a一个

    install
    属性,看看会打印什么:

    import Vue from 'vue'import App from './App.vue'
    import router from './router'
    
    Vue.config.productionTip = false
    
    function a(){
    console.log(6);
    }
    a.install=function(){
    console.log('install!');
    }
    Vue.use(a);
    new Vue({
    router,
    render: h => h(App)
    }).$mount('#app')

    执行结果:

    Vue.use( ) 作用

    从上述两个例子来看,Vue.use()作用就是把你给的方法执行一遍,但如果有install属性的话,会执行install属性。

    疑惑:如果只是为了执行这个方法或者拥有install属性的某个方法,那干脆自己调用一下好了,为啥还要用

    Vue.use()
    执行呢?

    解决:其实,在install属性的可以有一个参数传进来,我们将上述代码进行更改:

    a.install=function(vue){
    console.log(vue);
    }

    打印结果:

    ƒ Vue (options) {
    if ( true &&
    !(this instanceof Vue)
    ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
    }

    从打印结果来看的话,其实就是一个Vue的类,与下述代码类似的一个类:

    import Vue from 'vue'
    Vue.mixin( )初识

    对于Vue.use( )确实只是执行了一遍给的方法,但完成功能方面、起核心作用的还是

    vue.mixin()
    方法,

    请看如下代码,在

    main.js
    文件内,我们在
    vue.mixin()
    中混入
    data
    ,里面写一个c

    import Vue from 'vue'import App from './App.vue'
    import router from './router'
    
    Vue.config.productionTip = false
    
    function a(){
    console.log(6);
    }
    a.install=function(vue){
    //console.log(vue);
    //全局混入vue实例
    vue.mixin({
    data(){
    return {
    c:'欢迎访问超逸の博客'
    }
    }
    });
    }
    Vue.use(a);
    new Vue({
    router,
    render: h => h(App)
    }).$mount('#app')
    
    

    然后在

    HelloWorld.vue
    组件内,显示上文的
    c


    打开界面,查看如下:

    由上文可知,在HelloWorld组件里是没有c这个变量的,但是可以进行渲染显示到我们的页面,那么mixin是可以混入全局变量,任何组件可以拿到mixin混入的实例

    除了混入data外,我们还可以混入方法,举个栗子:

    a.install=function(vue){
    //console.log(vue);
    //全局混入vue实例
    vue.mixin({
    data(){
    return {
    c:'欢迎访问超逸の博客'
    }
    },
    methods::{
    globalMethods:function(){
    
    }
    }
    });
    }

    那么,其它组件都可以调用上述的方法,那么这样做有什么好处呢?

    一提及到全局可以使用,应该可以想到

    可复用性
    这个特点,比如我们开发常见的有些组件需要消息弹窗,可能大部分人会在每个组件进行import注册等等,但是有了mixin()后,我们可以定义一个全局的方法,首先在App.vue写好我们的消息弹窗的方法,用全局的方法去操作App.vue写好的方法,那么就有很高的复用性。

    但是

    data
    methods
    并不是我们mixin方法的关键,最牛的还是可以进行全局生命周期注入 比如
    created
    beforecreated
    mounted
    等等

    Vue插件开发一系列api(开始探索源码)

    console.log(Vue.util);

    执行结果:

    Vue.util.defineReactive

    很重要的一个就是:

    Vue.util.defineReactive
    ,它就是Vue监听
    current
    变量重要执行者

    不妨从源码来学习:

    /**
    * Define a reactive property on an Object.
    */
    //Vue的data监听,也是通过这个方法
    function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
    ) {
    //依赖收集者
    var dep = new Dep();
    
    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
    return
    }
    
    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
    }
    
    var childOb = !shallow && observe(val);
    //双向绑定
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
    var value = getter ? getter.call(obj) : val;
    if (Dep.target) {
    //进行依赖收集
    dep.depend();
    /*采用依赖收集的原因:*/
    //1.data里面的数据并不是所有地方都要用到
    //2.如果我们直接更新整个视图,会造成资源浪费
    //3.将依赖于某个变量的组件收集起来
    if (childOb) {
    childOb.dep.depend();
    if (Array.isArray(value)) {
    dependArray(value);
    }
    }
    }
    return value
    },
    set: function reactiveSetter (newVal) {
    var value = getter ? getter.call(obj) : val;
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
    return
    }
    /* eslint-enable no-self-compare */
    if (customSetter) {
    customSetter();
    }
    // #7981: for accessor properties without setter
    if (getter && !setter) { return }
    if (setter) {
    setter.call(obj, newVal);
    } else {
    val = newVal;
    }
    childOb = !shallow && observe(newVal);
    //触发依赖的组件产生更新
    dep.notify();
    }
    });
    }
    双向绑定

    上述关于响应式 双向绑定,强烈推荐之前写过的一篇文章:

    推荐阅读:Vue 技术栈 手写响应式原理 到 探索设计模式

    手写实现defineReactive

    我们可以通过

    defineReactive
    来实现Vue监听current的监视者,监听某个第三方的变量

    手写:

    import Vue from 'vue'import App from './App.vue'
    import router from './router'
    
    Vue.config.productionTip = false
    
    //Vue插件开发一系列api
    //console.log(Vue.util.defineReactive);
    //test是属于window的对象
    var test={
    testa:'计时开始'
    }
    //设置定时器
    setTimeout(function(){
    test.testa='计时结束'
    },2000)
    function a(){
    console.log(6);
    }
    a.install=function(vue){
    //console.log(vue);
    //监听testa
    Vue.util.defineReactive(test,'testa');
    //全局混入vue实例
    vue.mixin({
    data(){
    return {
    c:'欢迎访问超逸の博客'
    }
    },
    methods:{
    },
    beforeCreate:function(){
    this.test=test;
    },
    //全局生命周期注入
    created:function(){
    //console.log(this)
    }
    
    });
    }
    Vue.use(a);
    new Vue({
    router,
    render: h => h(App)
    }).$mount('#app')
    
    

    然后我们在HelloWorld组件进行渲染,查看页面

    执行结果:

    疑问:为什么要写在

    beforeCreate
    里面?

    解决:因为

    create
    阶段组件已经生成了,this实例已经创建了,而
    beforeCreate
    才刚开始。这样HelloWorld组件可以this调用来获取testa的值

    Vue.util.extend 与 Vue.extend 的区别

    关于这个问题,我百度了一下,貌似很少有人去探究这个问题,既然查不到,那么我们就从源码来学习,这就是一个比较好的方法。源码能够给你答案

    console.log(Vue.util.extend);
    console.log(Vue.extend);
    /**
    * Mix properties into target object.
    */
    
    //Vue.util.extend
    //其实就是拷贝一份,以后可以直接调用即可
    function extend (to, _from) {
    for (var key in _from) {
    to[key] = _from[key];
    }
    return to
    }
    
    /**
    * Class inheritance
    */
    
    //Vue.extend
    Vue.extend = function (extendOptions) {
    extendOptions = extendOptions || {};
    var Super = this;
    var SuperId = Super.cid;
    var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
    if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
    }
    
    var name = extendOptions.name || Super.options.name;
    if (name) {
    validateComponentName(name);
    }
    
    var Sub = function VueComponent (options) {
    this._init(options);
    };
    Sub.prototype = Object.create(Super.prototype);
    Sub.prototype.constructor = Sub;
    Sub.cid = cid++;
    Sub.options = mergeOptions(
    Super.options,
    extendOptions
    );
    Sub['super'] = Super;
    
    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
    initProps$1(Sub);
    }
    if (Sub.options.computed) {
    initComputed$1(Sub);
    }
    
    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend;
    Sub.mixin = Super.mixin;
    Sub.use = Super.use;
    
    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type];
    });
    // enable recursive self-lookup
    if (name) {
    Sub.options.components[name] = Sub;
    }
    
    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options;
    Sub.extendOptions = extendOptions;
    Sub.sealedOptions = extend({}, Sub.options);
    
    // cache constructor
    cachedCtors[SuperId] = Sub;
    return Sub
    };
    }
    单元测试

    关于Vue.extend我们以下面这个

    单元测试
    例子来讲解:

    由下图可知,我们获取到了HelloWorld的构造函数,然后再拿到组件。简单来说,你可以在任何地方,拿到任何组件,这对于单元测试方面是比较方便的,你可以拿到任何组件里的方法进行测试。

    手写Vue-router(核心)

    开始前准备
    • 在src下创建一个新的文件夹

      myrouter
      ,新建一个
      index.js
      的文件

    • 将之前写过的代码都注释掉,返回最初的模样

    • 将VueRouter引用改为我们自己所写的

      myrouter

    根据上文的流程图,手写vue-router

    //记录路由
    class historyRouter{
    constructor() {
    this.current=null;
    }
    }
    class vueRouter{
    constructor(options) {
    this.mode=options.mode||'hash';
    this.routes=options.routes||[];
    this.history=new historyRouter;
    //创建routesMap 将数组形式的转换成key-value形式的路由
    this.routesMap=this.createMap(this.routes);
    //事件监听
    this.init();
    }
    init(){
    if(this.mode=='hash'){
    location.hash? '':location.hash='/';
    window.addEventListener('load',()=>{
    this.history.current=location.hash.slice(1);
    });
    window.addEventListener('hashchange',()=>{
    this.history.current=location.hash.slice(1);
    })
    
    }else{
    location.pathname? '':location.pathname='/';
    window.addEventListener('load',()=>{
    this.history.current=location.pathname;
    });
    window.addEventListener('popstate',()=>{
    this.history.current=location.pathname;
    })
    }
    }
    createMap(routes){
    return routes.reduce((memo,current)=>{
    memo[current.path]=current.component;
    return memo;
    },{})
    }
    }
    
    //Vue监视current变量
    vueRouter.install=function(Vue){
    Vue.mixin({
    beforeCreate(){
    if(this.$options&&this.$options.router){
    this._root=this;
    this._router=this.$options.router;
    Vue.util.defineReactive(this,'current',this._router.history);
    }else{
    //嵌套路由,如果没有路由,去找父组件
    this._root=this.$parent._root;
    }
    }
    })
    //获取新组件以及render
    Vue.component('router-view',{
    //渲染新组件
    render(h){
    let current=this._self._root._router.history.current;
    //console.log(current);
    let routesMap=this._self._root._router.routesMap;.
    //console.log(routesMap);
    return h(routesMap[current]);
    }
    })
    }
    //将类暴露出去
    export default vueRouter;

    总结

    对于最后手写的vue-router读者只要弄懂它的思想即可,作为前端开发,我们不能只局限于写业务代码,造轮子等,我们要提高我们的编程思维,弄懂其中的思想与原理,了解底层才能不是一个简单的搬砖工!

    附本篇学习源码

    链接:https://pan.baidu.com/s/11xAkcdSyMxGTCPyQJafPGg
    提取码:0z9j

    (链接失效请评论区留言)

    结尾

    本篇文章是自学而写,当然还会有很多不足的地方,希望您来指正,感激不尽!

    学如逆水行舟,不进则退
    • 点赞 2
    • 收藏
    • 分享
    • 文章举报
    一百个Chocolate 博客专家 发布了600 篇原创文章 · 获赞 2156 · 访问量 34万+ 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: