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

vue2源码学习2vuex&vue-router的更多相关文章

  1. Vue源码学习1——Vue构造函数

    Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...

  2. Vue源码学习二 ———— Vue原型对象包装

    Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...

  3. Vue源码学习三 ———— Vue构造函数包装

    Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ...

  4. Vue2.x源码学习笔记-Vue构造函数

    我们知道使用vue.js开发应用时,都是new Vue({}/*options*/) 那Vue构造函数上有哪些静态属性和方法呢?其原型上又有哪些方法呢? 一般我都会在浏览器中输入Vue来look se ...

  5. Vue2.x源码学习笔记-Vue源码调试

    如果我们不用单文件组件开发,一般直接<script src="dist/vue.js">引入开发版vue.js这种情况下debug也是很方便的,只不过vue.js文件代 ...

  6. vue 源码学习三 vue中如何生成虚拟DOM

    vm._render 生成虚拟dom 我们知道在挂载过程中, $mount 会调用 vm._update和vm._render 方法,vm._updata是负责把VNode渲染成真正的DOM,vm._ ...

  7. Vue源码学习一 ———— Vue项目目录

    Vue 目录结构 可以在 github 上通过这款 Chrome 插件 octotree 查看Vue的文件目录.也可以克隆到本地.. Vue 是如何规划目录的 scripts ------------ ...

  8. Vue2.x源码学习笔记-Vue实例的属性和方法整理

    还是先从浏览器直观的感受下实例属性和方法. 实例属性: 对应解释如下: vm._uid // 自增的id vm._isVue // 标示是vue对象,避免被observe vm._renderProx ...

  9. Vue2.x源码学习笔记-Vue静态方法和静态属性整理

    Vue静态方法和静态属性,其实直接在浏览器中可以查看到的,如下 圈起来的是其静态属性,但是有的属性对象中的属性的值又是函数.未圈起来的则是函数. 其实它来自如下各个目录下的js文件 // src/co ...

  10. Vue源码学习(一):调试环境搭建

    最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...

随机推荐

  1. 【KAWAKO】从mac上定时将腾讯云的数据备份到本地

    目录 前言 需求 宝塔面板 备份网站 备份数据库 mac端 创建工程文件夹 rua.py rua stdout plist Reference 前言 不信任一切云端平台,把数据牢牢握在自己手中才是最安 ...

  2. 调式源码解决 seata 报错 can not get cluster name 问题

    最近在使用Spring Cloud整合分布式事务seata,项目启动之后,控制台一直报错: can not get cluster name in registry config 'service.v ...

  3. C语言使用fopen出现C4996错误解决方法

    Visual Studio 不安全提醒屏蔽方法: 打开项目----项目属性---配置属性----C/C++ ----预处理器----预处理定义,添加 _CRT_SECURE_NO_DEPRECATE ...

  4. Sequelize.js + Express.js 开发 API

    什么是 Sequelize 我们知道 Web 应用开发中的 Web 后端开发一般都是 Java.Python.ASP.NET 等语言.十年前,Node.js 的出现使得原本仅限于运行在浏览器中的 Ja ...

  5. ABP微服务系列学习-对接前端界面

    前面我们把后端的微服务架子基本搭建完成并成功启动了,现在我们可以对接前端界面了.这里我们直接用ABP模板里面的Angular的前端界面. 创建应用程序模板 使用ABPCli创建一个应用程序模板,前端选 ...

  6. Vscode报错: error:0308010C:digital envelope routines::unsupported错误记录解决

    Vscode报错: error:0308010C:digital envelope routines::unsupported错误记录解决 因为安装了新版本的node才报的错误:node版本: v18 ...

  7. label 与input其中input的 id与name

    <div> <label for="myfile">新头像 {% load static %} <img src="{% static 'i ...

  8. 97、UserAgentUtils

    user-agent-utils 是一个用来解析 User-Agent 字符串的 Java 类库. 其能够识别的内容包括: 超过150种不同的浏览器: 7种不同的浏览器类型: 超过60种不同的操作系统 ...

  9. Chisel项目中,添加了一个文件,新增了一个模块,但是却编译不出来相应的.v文件,什么原因?

    回答:最可能的原因是你新增的模块是 DPI-C 模块,在 setInLine 那里指定的 .v 文件名指定错了 记得要修改指定的 .v 文件名,还要修改 verilog 模块的模块名

  10. risv RV32I寄存器全称