vue2源码学习2vuex&vue-router
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
类实现、history
由HTML5History
类实现。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()
向所有组件混入beforeCreate
和destroyed
钩子,进行路由的一系列初始化、以及将根组件挂载到所有组件上。 - 利用了组件的渲染顺序,在渲染根组件的时候,添加
_routeRoot
属性,指向根组件本身,当渲染子组件的时候就可以通过this.$parent
属性获取到已经添加_routeRoot
属性的父组件,从而获取到根组件添加在自己的_routeRoot
属性身上,这样就可以让所有组件都会添加上_routeRoot
属性指向根实例。 - 通过
Object.defineProperty
将$router
、$route
全局挂载,所有的组件都可以获取到这两个属性。访问的是挂载在根实例上的_router
和_route
属性。同时这种写法还防止开发者对这两个属性进行篡改,导致未知的错误。 - 生成全局组件
router-view
和router-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对象
内部配置内容的一个具体体现。这个函数最终会返回一个包含pathList
、pathMap
、nameMap
三个属性的对象。pathList
属性存放着路由配置表中的所有路径。pathMap
存放着每一个路径和相对应的record
对象的映射关系。nameMap
则是每一个name
和record
对象的映射关系。- 在
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 {
}
导航解析过程:
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创建好的组件实例会作为回调函数的参数传入。
小结:
vue-router
在非浏览器环境下使用的是abstract模式
,其原理是模拟浏览器管理历史堆栈的方式,创建一个栈对路由历史记录进行管理。History
类是三大模式的基类,将三个模式的共同的行为进行了封装。transitionTo()
方法是vue-router
进行路由切换的核心方法,主要作用有:- 匹配相应的
route
对象 - 执行导航守卫
- 更新路由,触发页面更新
- 匹配相应的
beforeRouteEnter
能够通过回调来获取vue
实例的原理:在执行该守卫时,如果在next
函数传参为一个函数,会将其先收集起来,等到后面能够获取组件实例的时候,再执行收集的函数,并将组件实例作为参数传入。
8.router-view实现
router-view
是vue-router
中非常重要的组件之一,它用来渲染路由所对应的视图组件。当路由进行切换时,匹配到的视图组件最终会在这里被渲染出来,源码位置位于components/view.js
中。
函数式组件:
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
...
}
什么是函数式组件呢?简单点来说,它和真正的组件实例相比起来,结构更加的简单,具有以下特点:
- 没有生命周期、没有计算属性
computed
、没有watch
等。 - 没有自己的数据,所有数据都依靠
props
。 - 不需要实例化,没有
this
(可以指定)。 - 渲染的开销小。
注册组件实例:
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'
}
},
}
总结:
- 设置激活连接的
class
时,优先选择组件内部传入的class
,然后才是全局定义的class
。 - 守卫函数
guardEvent()
会在发生跳转前执行,对于一些操作会停止跳转。 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.针对具有命名空间的模块,会单独为其生成局部的getter
、state
、commit()
和dispatch()
,目的就是为了让开发者可以不需要修改模块内的代码,不需要手动去调整getter
、mutation
和action
的命名。
但是实际上是内部进行了调整,访问的还是修改命名之后的getter
、mutation
和action
。3.state
的响应式、以及getter
的计算属性实现,其实是生成了一个Vue
实例,然后将state
放在了data
属性中,而getter
则被注册成了计算属性。
4.为了确保state是被commit()
所修改,用了一个标识符来标注。使用了$watch()
方法监听state
的变化,每次state
变化时检测通过标识符来确定是否为commit()
修改。
辅助函数实现:
vuex提供了几个辅助函数,分别是mapState
、mapGetters
、mapActions
以及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
调用。
总结:
其实四个辅助函数的实现思路都是相同的,大致分为以下几步:
- 定义一个对象进行收集
- 将传入的所有参数进行处理,包装成一个个执行函数。
- 收集对象对函数进行收集
- 返回收集对象
参考链接:
https://github.com/LuckyMan199710/Vue-sourceCode-study/tree/master/vue-router
vue2源码学习2vuex&vue-router的更多相关文章
- Vue源码学习1——Vue构造函数
Vue源码学习1--Vue构造函数 这是我第一次正式阅读大型框架源码,刚开始的时候完全不知道该如何入手.Vue源码clone下来之后这么多文件夹,Vue的这么多方法和概念都在哪,完全没有头绪.现在也只 ...
- Vue源码学习二 ———— Vue原型对象包装
Vue原型对象的包装 在Vue官网直接通过 script 标签导入的 Vue包是 umd模块的形式.在使用前都通过 new Vue({}).记录一下 Vue构造函数的包装. 在 src/core/in ...
- Vue源码学习三 ———— Vue构造函数包装
Vue源码学习二 是对Vue的原型对象的包装,最后从Vue的出生文件导出了 Vue这个构造函数 来到 src/core/index.js 代码是: import Vue from './instanc ...
- Vue2.x源码学习笔记-Vue构造函数
我们知道使用vue.js开发应用时,都是new Vue({}/*options*/) 那Vue构造函数上有哪些静态属性和方法呢?其原型上又有哪些方法呢? 一般我都会在浏览器中输入Vue来look se ...
- Vue2.x源码学习笔记-Vue源码调试
如果我们不用单文件组件开发,一般直接<script src="dist/vue.js">引入开发版vue.js这种情况下debug也是很方便的,只不过vue.js文件代 ...
- vue 源码学习三 vue中如何生成虚拟DOM
vm._render 生成虚拟dom 我们知道在挂载过程中, $mount 会调用 vm._update和vm._render 方法,vm._updata是负责把VNode渲染成真正的DOM,vm._ ...
- Vue源码学习一 ———— Vue项目目录
Vue 目录结构 可以在 github 上通过这款 Chrome 插件 octotree 查看Vue的文件目录.也可以克隆到本地.. Vue 是如何规划目录的 scripts ------------ ...
- Vue2.x源码学习笔记-Vue实例的属性和方法整理
还是先从浏览器直观的感受下实例属性和方法. 实例属性: 对应解释如下: vm._uid // 自增的id vm._isVue // 标示是vue对象,避免被observe vm._renderProx ...
- Vue2.x源码学习笔记-Vue静态方法和静态属性整理
Vue静态方法和静态属性,其实直接在浏览器中可以查看到的,如下 圈起来的是其静态属性,但是有的属性对象中的属性的值又是函数.未圈起来的则是函数. 其实它来自如下各个目录下的js文件 // src/co ...
- Vue源码学习(一):调试环境搭建
最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...
随机推荐
- C# HttpClient使用和注意事项,.NET Framework连接池并发限制
System.Net.Http.HttpClient 类用于发送 HTTP 请求以及从 URI 所标识的资源接收 HTTP 响应. HttpClient 实例是应用于该实例执行的所有请求的设置集合,每 ...
- 代码随想录算法训练营day08 | leetcode 344.反转字符串/541. 反转字符串II / 剑指Offer05.替换空格/151.翻转字符串里的单词/剑指Offer58-II.左旋转字符串
基础知识 // String -> char[] char[] string=s.toCharArray(); // char[] -> String String.valueOf(str ...
- PostgreSQL的10进制与16进制互转
1.10进制转16进制Postgres里面有一个内置的10进制转16进制的函数:to_hex(int)/to_hex(bigint) [postgres@localhost ~]$ psql Pass ...
- 【磐河旅行】之酒店API接口对接实录
1.项目需求概述: 通过对接第三方磐河旅行的酒店API接口实现在我们的APP .微信小程序.H5上可提供用户酒店查询.酒店预订.退订等功能.效果如下图: 2.酒店接口功能拆分 除了酒店静态数据字典(如 ...
- 【11】java之抽象类
一.抽象类基本概念 1.1 抽象类 抽象类:是指在普通类的结构里增加抽象方法的组成部分,抽象类要使用 abstract 声明. 抽象方法:没有方法体且必须使用 abstract 关键字进行定义. 拥有 ...
- Min_25 Sieve 学习笔记
这个东西不是人想的. 解决问题:积性函数前缀和. 适用条件:可以快速计算 \(f(p)\) 的前缀和,\(f(p^k)\) 可以被表示成若干完全积性函数的线性组合(指对应项可以快速组合出来). 时空复 ...
- vue 2 中防抖节流在当前页面里写
isfilter(val) { // 过滤 this.debounce(() => { this.init(val); }, 1000); ...
- C++用递归实现求解相关函数
//递归实现Hanoi塔问题#include<iostream>#include<cstdlib>using namespace std;#define MAXSIZE 100 ...
- SpringMVC学习笔记【狂神说】
1.MVC是什么 MVC是模型(Model).视图(View).控制器(Controller)的简写,是一种软件设计规范. 是将业务逻辑.数据.显示分离的方法来组织代码. MVC主要作用是降低了视图与 ...
- 十二、21.提交本地代码到Git仓库并推送到码云
查看分支 运行git add . 把所有修改过后文件添加到暂存区 git commit 把当前所有的代码提交到rights分支 加-m加一个消息 到此所有的功能模块都已经提交到了rights这个分支里 ...