首页 > 编程语言 >vue2源码学习2vuex&vue-router

vue2源码学习2vuex&vue-router

时间:2022-11-28 16:23:19浏览次数:66  
标签:vue Vue js 源码 vue2 组件 router 路由 history

1.vue插件编写

插件可以实现对象vue的拓展,比如新增全局属性/方法,添加实例方法,使用mixin混入其他配置项等等。

编写插件必须要实现 install 方法,当调用Vue.use()使用插件时,会去执行插件中的install方法。该方法默认会有两个参数
第一个是 Vue 的实例,第二个是配置选项options。

2.hash和history

这两种模式都可以改变当前路径并且不会导致浏览器刷新页面。

hash指url中后面的#号以及后面的字符,hash也称作锚点,本身用来做页面定位的,可以使用hashchange事件进行监听。
当浏览器向指定服务器发送请求时,是不会将hash值带上的,所以hash值得变化并不会导致浏览器刷新页面。但因为hash是
拼接在url上,而url的长度又有限制,所以hash不能传递大量数据。

history会以栈的形式保存着用户所访问过的url,在HTML5中,为history新增了两个API分别是 history.pushState()
和 history.replaceState(),用来在浏览历史中添加和修改记录。使用 popstate事件对浏览器前进后退进行监听,再
利用 history.pushState()和 history.replaceState()修改历史记录,实现路由效果。在触发popstate事件时,可以
在event.state里获取数据,数据的类型和大小无限制。
hash模式由HashHistory类实现、historyHTML5History类实现。

HashHistory类具体的实现,对应源码在history/hash.js中。
HTML5History类的实现,对应源码在history/html5.js中。

3.vue-router源码目录结构

components组件
-view.js(router-view组件)
-link.js(router-link组件)
history各种模式的路由类
-abstract.js非window环境下的路由对象
-base.js所有路由对象的基类
-hash.js hash模式的路由对象
-html5.js HTML5 history模式的路由对象
-error.js 负责处理错误的对象
util工具函数
-async.js 用来处理异步函数
-dom.js 判断浏览器环境
-misc.js 对象复制
-location 用来生成一个Loaction对象
-path.js 对路径的处理
-params.js 用来处理参数
-query.js 用来处理query
-pushState.js 用来实现 pushState方法
-resolve-components 解析异步组件
-route.js 路由对象的一些方法
-scroll.js 路由滚动的行为
-state-key.js
-warn.js 警告信息提示

create-matcher.js 生成一个匹配器,匹配一个路径并找到映射的组件
create-route-map.js 将用户传入的路由列表转换成映射表
index.js 入口文件,这里定义router类
install.js install方法, Vue.use()会调用这个方法

4.install()的实现

文件在源码的 src/install.js 中。

确保只执行一次的方法:
if(install.installed && _Vue === Vue) return
install.installed = true;//这个是确定install()方法不会被重复执行。

使用内部变量接收Vue实例:
export let _Vue;
export function install(Vue){
_Vue = Vue;
}

使用Vue.mixin给混入钩子函数:
Vue.mixin()将一段可以复用的组件options混入到组件中和组件中同名的options进行合并。
Vue.mixin({
beforeCreate(){
//给所有vue组件挂载上_routerRoot属性,该属性指向根组件
//给根组件挂载上 VueRouter实例
//初始化路由
//使用 Vue.util.defineReactive,将 _route变成一个响应式数据并且挂载到根组件上
//注册vue实例
if(isDef(this.$options.router)){
this._routerRoot = this;
this._router = this.$options.router
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
}else{
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed(){
//销毁实例
registerInstance(this)
}
})
实现所有组件挂载 _routeRoot: 要用到 $parent属性,$parent能够获取到当前vue组件的父组件。

获取router实例的过程:
在main.js中,通过import导入VueRouter的构造函数。Vue.use()安装插件,混入生命钩子函数beforeCreate和
destroyed.然后执行构造函数,创建VueRouter实例。创建根组件,将VueRouter实例作为参赛传入,根组件创建时
触发beforeCreate钩子,通过$options获取到并挂载上去。

$route和$router属性的挂载:
使用$router跳转,用$route获取路由传递参赛等。在install()方法中:
Object.defineProperty(Vue.protoype, '$router',{
get(){ return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get(){ return this._routerRoot._route }
})
通过Object.defineProperty将这两个属性挂载到了Vue的原型上,即全局挂载。当访问$router和$route属性时,访问的
是当前组件的 _routeRoot属性,这个属性指向的是根实例。

全局注册router-view, router-link:
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)

总结:
  • 当使用Vue.use()下载插件对象时,会给插件对象的install()方法默认传递一个Vue实例作为参数。使用变量保存Vue实例,方便内部其他地方的使用,解决了显示的引入Vue带来的打包问题。
  • 使用全局Vue.mixin()向所有组件混入beforeCreatedestroyed钩子,进行路由的一系列初始化、以及将根组件挂载到所有组件上。
  • 利用了组件的渲染顺序,在渲染根组件的时候,添加_routeRoot属性,指向根组件本身,当渲染子组件的时候就可以通过this.$parent属性获取到已经添加_routeRoot属性的父组件,从而获取到根组件添加在自己的_routeRoot属性身上,这样就可以让所有组件都会添加上_routeRoot属性指向根实例。
  • 通过Object.defineProperty$router$route全局挂载,所有的组件都可以获取到这两个属性。访问的是挂载在根实例上的_router_route属性。同时这种写法还防止开发者对这两个属性进行篡改,导致未知的错误。
  • 生成全局组件router-viewrouter-link,这两个是路由的核心组件。

 5.VueRouter类的实现

VueRouter的构造函数:

export default class VueRouter{
  static install: () => void;
  static version: string;

  app: any;
  apps: Array<any>;
  ready: boolean;
  readyCbs: Array<Function>;
  options: RouterOptions;
  mode: string;
  history: HashHistory | HTML5History | AbstractHistory;
  matcher: Matcher;
  fallback: boolean;
  beforeHooks: Array<?NavigationGuard>;
  resolveHooks: Array<?NavigationGuard>;
  afterHooks: Array<?AfterNavigationHook>;
 constructor (options: RouterOptions = {}) {
    this.app = null
    this.apps = []
    this.options = options
    this.beforeHooks = []
    this.resolveHooks = []
    this.afterHooks = []
    this.matcher = createMatcher(options.routes || [], this)

    let mode = options.mode || 'hash'
    this.fallback =
      mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {
      mode = 'abstract'
    }
    this.mode = mode

    switch (mode) {
      case 'history':
        this.history = new HTML5History(this, options.base)
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base)
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
init(){
//init方法主要做了以下4件事情:
  //设置实例的钩子函数,当实例销毁之后执行,调用history.teardownListeners()卸载所有监听器。
  //通过history.setupListeners()方法添加监听器,监听路由的变化并作出处理。hash模式和history模式的setupListeners()实现是不同的。
  //调用history.transitionTo()方法,跳转到初始位置。
  //history.listen()监听路由的变化,当路由变化会赋值给_route从而触发响应式
  }
}

全局钩子函数:
beforeEach (fn: Function): Function {
  return registerHook(this.beforeHooks, fn)
}
beforeResolve (fn: Function): Function {
  return registerHook(this.resolveHooks, fn)
}
afterEach (fn: Function): Function {
   return registerHook(this.afterHooks, fn)
}
//registerHook函数
function registerHook (list: Array<any>, fn: Function): Function {
  list.push(fn)
  return () => {
    const i = list.indexOf(fn)
    if (i > -1) list.splice(i, 1)
  }
}

总结:
1.VueRouter类的构造函数主要进行了以下操作:
    初始化属性
    根据不同的模式,生成不同的history实例。
2.init()方法主要是添加了当根实例销毁时卸载所有监听器的处理、监听route的变化更新_route属性触发响应式更新。
3.全局钩子的实现主要是通过registerHook()函数将钩子函数存放到指定的函数数组,返回一个闭包函数用来从数组中取出相应的钩子函数。

6.Matcher实现


 Matcher内部一共有四个方法,下面分别介绍这四个方法:

   match:将当前的路径信息与路由配置表进行匹配,返回一个route对象,包含着需要渲染的组件以及其他内容。

   addRoute:添加路由。 

   addRoutes:同样是添加路由,现已废弃。 

   getRoutes:获取路由列表。


小结:
  • 匹配器Matcher是由createMatcher()函数生成的,这个函数位于create-matcher.js中。
  • Matcher由四个方法构成,分别是匹配方法match()、添加路由的方法addRoute()addRoutes()以及获取路由的方法getRoute()
  • 开发者传入的路由配置表通过createRouteMap()函数进行转换,这个函数位于create-route-map.js中。
  • createRouteMap()通过遍历开发者传入的路由配置表,给每一个RouteConfig对象生成一个相应的record对象。这个对象就是RouteConfig对象内部配置内容的一个具体体现。这个函数最终会返回一个包含pathListpathMapnameMap三个属性的对象。
  • pathList属性存放着路由配置表中的所有路径。pathMap存放着每一个路径和相对应的record对象的映射关系。nameMap则是每一个namerecord对象的映射关系。
  • Matcher的核心方法match中,匹配的优先级是name > path,如果存在name属性会先拿该属性去nameMap进行查找,如果没有name属性,才选择用path进行查找。
  • createRoute()方法最终返回的route对象,就是挂载在根实例上的_route属性。

7.History类

Vue-router支持2种模式,一种是hash模式,另一种是history模式。 在前面的章节说过,对于hash模式和history模式,分别采用了HashHistory类HTML5History类来实现,源码位置位于history.js中。

在源码中,这些行为被抽离出来,以类的形式进行了封装,也就是这章的主角——History类。这是三大模式的基类,无论是HashHistory类、HTML5History类,
还是AbstractHistory类,都是继承自History类。接下来开始对其实现进行解读。源码位置在base.js中。

export class History {

}

导航解析过程:

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用beforeRouteEnter守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
小结:
  • vue-router在非浏览器环境下使用的是abstract模式,其原理是模拟浏览器管理历史堆栈的方式,创建一个栈对路由历史记录进行管理。
  • History类是三大模式的基类,将三个模式的共同的行为进行了封装。
  • transitionTo()方法是vue-router进行路由切换的核心方法,主要作用有:
    1. 匹配相应的route对象
    2. 执行导航守卫
    3. 更新路由,触发页面更新
  • beforeRouteEnter能够通过回调来获取vue实例的原理:在执行该守卫时,如果在next函数传参为一个函数,会将其先收集起来,等到后面能够获取组件实例的时候,再执行收集的函数,并将组件实例作为参数传入。

8.router-view实现

router-viewvue-router中非常重要的组件之一,它用来渲染路由所对应的视图组件。当路由进行切换时,匹配到的视图组件最终会在这里被渲染出来,源码位置位于components/view.js中。

函数式组件:
export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  ...
}

什么是函数式组件呢?简单点来说,它和真正的组件实例相比起来,结构更加的简单,具有以下特点:

  1. 没有生命周期、没有计算属性computed、没有watch等。
  2. 没有自己的数据,所有数据都依靠props
  3. 不需要实例化,没有this(可以指定)。
  4. 渲染的开销小。

注册组件实例:
const registerInstance = (vm, callVal) => {
    let i = vm.$options._parentVnode
    if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
      i(vm, callVal)
    }
}
registerInstance()方法是给route对象注册当前的vue实例。

小结:
  • 因为router-view并不需要有自己的处理逻辑,它只是一个接收props并渲染内容的组件。所以比起使用真正的组件实例,函数式组件才是更好的选择,可以降低渲染开销,提升页面渲染速度。
  • 在matched数组中,存放record对象的顺序和route对象的层级有关,因此可以通过寻找上级的router-view组件来确定深度,也就确定了所需的record对象在matched数组中的位置。
  • router-view内部有一个registerRouteInstance方法,主要是给route对象注册当前的实例。
  • router-view使用根实例的_route属性进行视图组件的渲染,而该属性又是一个响应式数据,所以当发生页面跳转时,_route发生改变,从而触发router-view的更新。

9.router-link实现

export default {
  name: 'RouterLink',
  props: {
  	// 跳转的目标
    to: {
      type: toTypes,
      required: true
    },
    // 最终渲染成的标签
    tag: {
      type: String,
      default: 'a'
    },
    custom: Boolean,
    exact: Boolean, // 是否精确查找
    exactPath: Boolean,
    append: Boolean, // 是否添加基路径
    replace: Boolean, // 调用replace方法,跳转后的记录会覆盖当前的记录
    activeClass: String, // 链接被激活时的class名
    exactActiveClass: String,// 进行精确匹配时 链接被激活时的class名
    ariaCurrentValue: {
      type: String,
      default: 'page'
    },
    event: { // 可以触发导航的事件
      type: eventTypes,
      default: 'click'
    }
  },
}
总结:
  1. 设置激活连接的class时,优先选择组件内部传入的class,然后才是全局定义的class
  2. 守卫函数guardEvent()会在发生跳转前执行,对于一些操作会停止跳转。
  3. router-link默认是会渲染成a标签,如果自定义成其他标签的话,会先去找内部是否存在a标签,如果寻找到了会给其添加事件和属性,否则就给其本身添加事件再渲染出来。
 

10.Vuex

vuex源码文件结构:
   module(vuex模块化)
      -module-collection.js(module树)
      -module.js(单个module)
   plugins(内置插件)
      -devtool.js
      -logger.js
   helper.js(辅助函数)
   mixin.js(定义mixinx方法)
   store.js(store类)
   index.js(入口文件)
   util.js(工具函数)


Vuex挂载:
  vuex使用:
     import Vuex from 'vuex';
     Vue.use(Vuex);
     const store = new Vuex.Store({
       state: {...},
       mutations: {...}
     })
     new Vue({
       router,
       store,
       render: h=>h(App)
     }).$mount('#app')
总结:
  1.vuex是以Vue插件的形式实现的。
  2.挂载$store属性的过程中,针对不同版本的Vue做了不同的处理,2.x版本使用了Vue.mixin()进行全局混入,1.x版本则是重写Vue原型上的init()方法。
  3.使用Vue.mixin()全局混入了beforeCreate()钩子,最开始渲染的根组件先获取到store实例并挂载到自身的$store属性上,后面的组件创建时,
就可以通过父级组件来获取store实例并挂载到自身的$store属性上,来实现所有组件都可以具有$store属性。

实现store类:
总结:
  1.vuex模块化由ModuleCollection类实现,它将实例化store时传入的参数构建成一棵模块树。
  2.针对具有命名空间的模块,会单独为其生成局部的getterstatecommit()dispatch(),目的就是为了让开发者可以不需要修改模块内的代码,不需要手动去调整gettermutationaction的命名。
但是实际上是内部进行了调整,访问的还是修改命名之后的gettermutationaction
  3.state的响应式、以及getter的计算属性实现,其实是生成了一个Vue实例,然后将state放在了data属性中,而getter则被注册成了计算属性。
  4.为了确保state是被commit()所修改,用了一个标识符来标注。使用了$watch()方法监听state的变化,每次state变化时检测通过标识符来确定是否为commit()修改。
辅助函数实现:
vuex提供了几个辅助函数,分别是mapStatemapGettersmapActions以及mapMutations。

mapState():

该函数可以将一些state转变为Vue实例的计算属性。
mapState()函数先是定义了一个对象res,然后将传进来的每一个参数,封装成一个待执行的mappedState()函数,然后被res对象收集起来,最终返回res对象。
mappedState()函数就是最终的计算属性,该函数的内容如下:
1.先判断模块是否有命名空间,存在命名空间的话,就使用局部化的state和getter。
2.因为mapState()支持传入字符串或者是函数,因此还需要对这两者进行区别。如果参数是一个函数,那么就返回该函数的执行结果,如果是一个key值,就从state对象寻找并返回相应的结果。

mapMutations():
mapMutations()函数主要是可以将methods映射为store.commit调用。

mapGetter:
mapGetters 辅助函数将 store 中的 getter 映射到局部计算属性。

mapActions:
mapActions辅助函数将组件的methods映射为store.dispatch调用。

总结:

   其实四个辅助函数的实现思路都是相同的,大致分为以下几步:

  1. 定义一个对象进行收集
  2. 将传入的所有参数进行处理,包装成一个个执行函数。
  3. 收集对象对函数进行收集
  4. 返回收集对象

 

参考链接:

    https://github.com/LuckyMan199710/Vue-sourceCode-study/tree/master/vue-router

 

标签:vue,Vue,js,源码,vue2,组件,router,路由,history
From: https://www.cnblogs.com/sunnyeve/p/16924926.html

相关文章

  • 进度条 vue3 vite
    NProgress.js官网 https://ricostacruz.com/nprogress/安装:npminstallnprogress使用://引入NProgress进度条importNProgressfrom'nprogress'import'nprogres......
  • vue大文件上传demo
    ​ 以ASP.NETCoreWebAPI 作后端 API ,用 Vue 构建前端页面,用 Axios 从前端访问后端 API,包括文件的上传和下载。 准备文件上传的API #region 文件上传......
  • vue的.sync修饰符用法及原理详解
    vue.sync的历史vue.sync修饰符最初存在于vue1.0版本里,但是在2.0中被移除了。但是在2.0发布之后的实际应用中,vue官方发现.sync还是有其适用之处,比如在开发可复......
  • Electron-vue 使用Element-UI el-table 不显示
    在Electron-Vue中引入Element-UI,发现el-table显示空白,查资料发现只需要在.electron-vue/webpack.renderer.config里面白名单模块加上 'element-ui'  //修改前......
  • 分析Java的类加载器与ClassLoader(二):classpath与查找类字节码的顺序,分析ExtClassLoader
    先回顾一下classpathclasspath的作用:    classpath的作用是指定查找类的路径:当使用java命令执行一个类(类中的main方法)时,会从classpath中进行查找这个类。 指定clas......
  • Vue3+Vite项目中 使用WindiCSS.
    之前工作有了解过根据类名来写元素的样式,一听就发出疑问:这样写项目可读性恐怕不是很好吧。。。  之后来到杭州工作后,开始使用WindiCSS后发现真香!!! 由于近期所写的项......
  • vue文件夹上传这么做
    ​ 这里只写后端的代码,基本的思想就是,前端将文件分片,然后每次访问上传接口的时候,向后端传入参数:当前为第几块文件,和分片总数下面直接贴代码吧,一些难懂的我大部分都加上......
  • Vue踩坑日记一
    Vue踩坑日记一:ViewUI添加单击事件不生效问题:使用IviewMenu过程中,出现其子项MenuItem添加@click事件无法生效的问题。解决办法:使用@click.native原生点击事件......
  • Tomcat源码分析使用NIO接收HTTP请求(四)----解析请求头
    User-Agent:PostmanRuntime/7.28.4Accept:text/htmlPostman-Token:c125824d-ae13-4082-9ae0-87c1750476b8Host:localhost:8000Accept-Encoding:gzip,deflate,brCon......
  • 【Vuejs】114-从头开始学习Vuex
    一、前言当我们的应用遇到多个组件共享状态时,会需要多个组件依赖于同一状态抑或是来自不同视图的行为需要变更同一状态。以前的解决办法:a.将数据以及操作数据的行为都定义在......