vue-router和vuex的源码实现
预习课程
Vue全家桶 & 原理
资源
- vue-router
2. vuex
3. vue-router源码
4. vuex源码
知识点
vue-router
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单⻚面应用变得易如反
掌。
vue-cli项目中安装:vue add router
核心步骤:
步骤一:使用vue-router插件,router.js
import Router from 'vue-router'
Vue.use(Router)
// 因为vue-router是插件,是以插件形式存在,所以要使用use来调用
步骤二:创建Router实例,router.js ```javascript export default new Router({…})
// 在spa的程序中router是单例的。如果写过服务端渲染,路由器又变成了多例。因此之后在学习SSR时要区分router单例和多例的区别
- 步骤三:在根组件上添加该实例,main.js
```javascript
import router from './router'
new Vue({
router,
}).$mount("#app");
// router挂载在这里的原因
- 步骤四:添加路由视图,App.vue
<router-view></router-view>
// 使用router-view作为路由的出口,显示内容真正的切换
// 可以直接使用这两个组件的原因是在插件中已经声明了全局的组件
- 导航
```javascript
Home About
// 使用router-link作为路由切换 // 可以直接使用这两个组件的原因是在插件中已经声明了全局的组件,他们的原理是什么
```javascript
this.$router.push('/')
this.$router.push('/about')
vue-router源码实现
单⻚面应用程序中,url发生变化时候,不能刷新,显示对应视图内容
需求分析
- spa ⻚面不能刷新(点击各个链接时,传统的网站会请求服务器,得到全新的HTML的页面内容。页面跳转刷新了,但是spa的页面是不会进行跳转的,只是会在当前的页面中做dom的隐藏、删除、追加这种操作。那么这些dom操作需要有相关的实现,在前端有两个策略如下)
- hash #/about
- 在url后面通过hash的方式做路径跳转,浏览器不会刷新。可以监控hash change,这样可以实现
- History api /about
- 因为Hisatory api的特点是:在pushState的时候,浏览器窗口的url会变化,但是浏览器不会发出真正的请求。所以是可以根据pushState来决定跳转到哪个页面
- hash #/about
- 根据url显示对应的内容(监控页面中url的变化,一旦页面中url发生变化则做下面的事情)
- router-view(在router-view的位置做内容的切换)
- 如果使用jquery来做的话:将最新的内容拿到后,将老的内容清空,router-view就是容器,清空老的内容,将新的内容填充进去
- 在vue的底层也是这样做的。Vue巧妙利用Vue的数据响应式。好处:用户可以基于数据做驱动,不需要做dom操作了。其实就是在router-view中只需要监控路径的变化,路径为current,current发生变化时,动态重新渲染router-view中的内容。router-view中的逻辑是将当前的url中所对应的组件生成虚拟dom,再将vdom生成为真实的dom。
- 数据响应式:current变量持有url地址,一旦变化,动态重新执行render
- router-view(在router-view的位置做内容的切换)
任务
- 实现一个插件 —-(vuew-router是插件)
- 实现VueRouter类(在插件中需要实现的需求)
- 处理路由选项
- 监控url变化,hashchange
- url变化后,需要响应式的数据存储url
- 响应这个变化(如何响应)
- 实现install方法 —-(每个插件都需要始兴县install方法)
- $router注册
- 每一个组件中有一个$route和$router的使用。通过$route和$router可以获取路由的参数和路由的导航,重定向,所以$router需要这里挂载注册,让其他所有的组件都可以使用它
- 两个全局组件(router-view和router-link需要实现)
- 不在插件中实现,则在vue中无法使用
- $router注册
- 实现VueRouter类(在插件中需要实现的需求)
github的地址:https://github.com/57code/vue-study
实现一个插件:创建VueRouter类和install方法
在main.js中将官方的router改成自己实现的router文件
krouter/kvue-router.js 自己的router库的实现,插件的定义
krouter/index.js 路由的定义
krouter/index.js
// 引入ue-router的插件
// 这里面有路由表的定义
// 创建vue-router,默认的模式(mode)是hash模式,也可以设置为history模式
创建kvue-router.js
let Vue; // 引用构造函数,VueRouter中要使用
// 保存选项
class VueRouter {
constructor (options) {
this.$options = options;
}
}
// 插件:实现install方法,注册$router
VueRouter.install = function (_Vue) {
// 引用构造函数,VueRouter中要使用
Vue = _Vue;
// 任务 1 :挂载$router
Vue.mixin({
beforeCreate () {
// 只有根组件拥有router选项
if (this.$options.router) {
// vm.$router
Vue.prototype.$router = this.$options.router;
}
}
});
// 任务 2 :实现两个全局组件router-link和router-view
Vue.component('router-link', Link)
Vue.component('router-view', View)
};
export default VueRouter;
为什么要用混入方式写?主要原因是use代码在前,Router实例创建在后,而install逻辑又需要用 到该实例
创建router-view和router-link
创建krouter-link.js
export default {
props: {
// 接收节点上声明属性
to: {
type: String, // 设置类型
required: true, // 设置必填项
},
},
render (h) {
// return <a href={'#'+this.to}>{this.$slots.default}</a>;
return h('a', {
attrs: {
href: '#' + this.to
}
}, [
this.$slots.default
])
}
}
创建krouter-view.js
export default {
render (h) {
// 暂时先不渲染任何内容
return h(null);
}
}
监控url变化
定义响应式的current属性,监听hashchange事件
class VueRouter {
constructor (options) {
// 定义响应式的属性current
const initial = window.location.hash.slice(1) || '/'
Vue.util.defineReactive(this, 'current', initial)
// 监听hashchange事件
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
}
onHashChange () {
this.current = window.location.hash.slice(1)
}
}
动态获取对应组件,krouter-view.js
export default {
render (h) {
// 动态获取对应组件
let component = null;
const route = this.$router.$options.routes.find(route => route.path ===
this.$router.current)
if (route) component = route.component
return h(component);
}
}
提前处理路由表
提前处理路由表避免每次都循环
class VueRouter {
constructor (options) {
// 缓存path和route映射关系
this.routeMap = {}
this.$options.routes.forEach(route => {
this.routeMap[route.path] = route
});
}
}
使用,krouter-view.js
export default {
render (h) {
const { routeMap, current } = this.$router
const component = routeMap[current] ? routeMap[current].component : null;
return h(component);
}
}
Vuex
Vuex 集中式 存储管理应用的所有组件的状态,并以相应的规则保证状态以 可预测 的方式发生变化。
(为什么要使用vuex,为了数据的集中式管理和可预测的变化方式)
设置为单项数据流 所有的数据都集中在store中存储起来,所有需要访问数据的,都从同一个数据源获取,状态发生变化或者被访问都是可以预测的。所以造成了vuex就是下面的结构。程序如果出现问题,调试和找问题都会变得简单。
可预测的变更指的是需要更改状态必须通过commit方式来变更状态。如果有异步/复杂操作可以通过dispatch 所以在做任何变更的时候,vuex是可以将你提交的操作拦截,在更改状态之前插入其他的操作(日志、缓存等提前的操作),甚至都可以拒绝你的操作(你的操作是不合理和不被允许的情况),这个对程序的管理比较强大
整合vuex
在vue-cli的环境下引入(vuex是插件)
vue add vuex
核心概念
- state 状态、数据
- mutations 更改状态的函数
- actions 异步操作
- store 包含以上概念的容器
- getters 派生状态
状态 - state
state保存应用状态
export default new Vuex.Store({
state: { counter: 0 },
})
状态变更 - mutations
mutations用于修改状态,store.js
export default new Vuex.Store({
mutations: {
add (state) {
state.counter++
}
}
})
派生状态 - getters
从state派生出新状态,类似计算属性
export default new Vuex.Store({
getters: {
doubleCounter (state) { // 计算剩余数量
return state.counter * 2;
}
}
})
动作 - actions
添加业务逻辑,类似于controller
export default new Vuex.Store({
actions: {
add ({ commit }) {
setTimeout(() => {
commit('add')
}, 1000);
}
}
})
测试代码:
<p @click="$store.commit('add')">counter: {{$store.state.counter}}</p>
<p @click="$store.dispatch('add')">async counter: {{$store.state.counter}}</p>
<p>double:{{$store.getters.doubleCounter}}</p>
vuex原理解析
任务分析
- 实现插件
- 实现Store类
- 维持一个响应式状态state
- 实现commit()
- 实现dispatch()
- getters
- 挂载$store
- 实现Store类
初始化:Store声明、install实现,kvuex.js:
let Vue;
class Store {
constructor (options = {}) {
this._vm = new Vue({
data: {
$$state: options.state
}
});
}
get state () {
return this._vm._data.$$state
}
set state (v) {
console.error('please use replaceState to reset state');
}
}
function install (_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate () {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store;
}
}
});
}
export default { Store, install };
实现commit:根据用户传入type获取并执行对应mutation
class Store {
constructor (options = {}) {
// 保存用户配置的mutations选项
this._mutations = options.mutations || {}
}
commit (type, payload) {
// 获取type对应的mutation
const entry = this._mutations[type]
if (!entry) {
console.error(`unknown mutation type: ${type}`);
return
}
// 指定上下文为Store实例
// 传递state给mutation
entry(this.state, payload);
}
}
实现actions:根据用户传入type获取并执行对应action
class Store {
constructor (options = {}) {
// 保存用户编写的actions选项
this._actions = options.actions || {}
// 绑定commit上下文否则action中调用commit时可能出问题!!
// 同时也把action绑了,因为action可以互调
const store = this
const { commit, action } = store
this.commit = function boundCommit (type, payload) {
commit.call(store, type, payload)
}
this.action = function boundAction (type, payload) {
return action.call(store, type, payload)
}
}
dispatch (type, payload) {
// 获取用户编写的type对应的action
const entry = this._actions[type]
if (!entry) {
console.error(`unknown action type: ${type}`);
return
}
// 异步结果处理常常需要返回Promise
return entry(this, payload);
}
}
作业
在kvuex中实现getters
思考拓展
- 尝试去看看vue-router的源码,并解决嵌套路由的问题
路由器有可能是嵌套的。如果router-view是嵌套关系,则就是子路由。当前的路由向上找父级,如果父级不是根,则继续向上找,直到找到route-view。router-view需要知道自身的层级的深度,将来在匹配的路由中才能找到合适的匹配项
- 提前了解vue数据响应原理为下节课做准备
作业
1.嵌套路由的解决方案
- router-view的深度标记
- 路由匹配时获取代表深度层级的matched数组
2.实现vuex的getters
3.了解vue的响应式原理
做响应式数据方式:
Object.defineProperty()
Vue.util.defineReactive()
Vue.observable()
new Vue({
data(){}
})
vue中数据响应式的实现Object.defineProperty() -> 这个是最底层的实现
Vue.util.defineReactive()
Vue.observable() -> vue源码