1. vue-routervuex的源码实现

预习课程

image.png
image.png
image.png
image.png

Vue全家桶 & 原理

资源

  1. 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

    1. import Router from 'vue-router'
    2. Vue.use(Router)
    3. // 因为vue-router是插件,是以插件形式存在,所以要使用use来调用
  • 步骤二:创建Router实例,router.js ```javascript export default new Router({…})

// 在spa的程序中router是单例的。如果写过服务端渲染,路由器又变成了多例。因此之后在学习SSR时要区分router单例和多例的区别

  1. - 步骤三:在根组件上添加该实例,main.js
  2. ```javascript
  3. import router from './router'
  4. new Vue({
  5. router,
  6. }).$mount("#app");
  7. // router挂载在这里的原因
  • 步骤四:添加路由视图,App.vue
  1. <router-view></router-view>
  2. // 使用router-view作为路由的出口,显示内容真正的切换
  3. // 可以直接使用这两个组件的原因是在插件中已经声明了全局的组件
  • 导航 ```javascript Home About

// 使用router-link作为路由切换 // 可以直接使用这两个组件的原因是在插件中已经声明了全局的组件,他们的原理是什么

  1. ```javascript
  2. this.$router.push('/')
  3. 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来决定跳转到哪个页面
  • 根据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

任务

  • 实现一个插件 —-(vuew-router是插件)
    • 实现VueRouter类(在插件中需要实现的需求)
      • 处理路由选项
      • 监控url变化,hashchange
        • url变化后,需要响应式的数据存储url
      • 响应这个变化(如何响应)
    • 实现install方法 —-(每个插件都需要始兴县install方法)
      • $router注册
        • 每一个组件中有一个$route和$router的使用。通过$route和$router可以获取路由的参数和路由的导航,重定向,所以$router需要这里挂载注册,让其他所有的组件都可以使用它
      • 两个全局组件(router-view和router-link需要实现)
        • 不在插件中实现,则在vue中无法使用

github的地址:https://github.com/57code/vue-study

实现一个插件:创建VueRouter类和install方法

在main.js中将官方的router改成自己实现的router文件

krouter/kvue-router.js 自己的router库的实现,插件的定义
krouter/index.js 路由的定义

krouter/index.js

  1. // 引入ue-router的插件
  2. // 这里面有路由表的定义
  3. // 创建vue-router,默认的模式(mode)是hash模式,也可以设置为history模式

创建kvue-router.js

  1. let Vue; // 引用构造函数,VueRouter中要使用
  2. // 保存选项
  3. class VueRouter {
  4. constructor (options) {
  5. this.$options = options;
  6. }
  7. }
  8. // 插件:实现install方法,注册$router
  9. VueRouter.install = function (_Vue) {
  10. // 引用构造函数,VueRouter中要使用
  11. Vue = _Vue;
  12. // 任务 1 :挂载$router
  13. Vue.mixin({
  14. beforeCreate () {
  15. // 只有根组件拥有router选项
  16. if (this.$options.router) {
  17. // vm.$router
  18. Vue.prototype.$router = this.$options.router;
  19. }
  20. }
  21. });
  22. // 任务 2 :实现两个全局组件router-link和router-view
  23. Vue.component('router-link', Link)
  24. Vue.component('router-view', View)
  25. };
  26. export default VueRouter;

为什么要用混入方式写?主要原因是use代码在前,Router实例创建在后,而install逻辑又需要用 到该实例

创建router-view和router-link

创建krouter-link.js

  1. export default {
  2. props: {
  3. // 接收节点上声明属性
  4. to: {
  5. type: String, // 设置类型
  6. required: true, // 设置必填项
  7. },
  8. },
  9. render (h) {
  10. // return <a href={'#'+this.to}>{this.$slots.default}</a>;
  11. return h('a', {
  12. attrs: {
  13. href: '#' + this.to
  14. }
  15. }, [
  16. this.$slots.default
  17. ])
  18. }
  19. }

创建krouter-view.js

  1. export default {
  2. render (h) {
  3. // 暂时先不渲染任何内容
  4. return h(null);
  5. }
  6. }

监控url变化

定义响应式的current属性,监听hashchange事件

  1. class VueRouter {
  2. constructor (options) {
  3. // 定义响应式的属性current
  4. const initial = window.location.hash.slice(1) || '/'
  5. Vue.util.defineReactive(this, 'current', initial)
  6. // 监听hashchange事件
  7. window.addEventListener('hashchange', this.onHashChange.bind(this))
  8. window.addEventListener('load', this.onHashChange.bind(this))
  9. }
  10. onHashChange () {
  11. this.current = window.location.hash.slice(1)
  12. }
  13. }

动态获取对应组件,krouter-view.js

  1. export default {
  2. render (h) {
  3. // 动态获取对应组件
  4. let component = null;
  5. const route = this.$router.$options.routes.find(route => route.path ===
  6. this.$router.current)
  7. if (route) component = route.component
  8. return h(component);
  9. }
  10. }

提前处理路由表

提前处理路由表避免每次都循环

  1. class VueRouter {
  2. constructor (options) {
  3. // 缓存path和route映射关系
  4. this.routeMap = {}
  5. this.$options.routes.forEach(route => {
  6. this.routeMap[route.path] = route
  7. });
  8. }
  9. }

使用,krouter-view.js

  1. export default {
  2. render (h) {
  3. const { routeMap, current } = this.$router
  4. const component = routeMap[current] ? routeMap[current].component : null;
  5. return h(component);
  6. }
  7. }

Vuex

Vuex 集中式 存储管理应用的所有组件的状态,并以相应的规则保证状态以 可预测 的方式发生变化。
(为什么要使用vuex,为了数据的集中式管理和可预测的变化方式)

设置为单项数据流 所有的数据都集中在store中存储起来,所有需要访问数据的,都从同一个数据源获取,状态发生变化或者被访问都是可以预测的。所以造成了vuex就是下面的结构。程序如果出现问题,调试和找问题都会变得简单。

可预测的变更指的是需要更改状态必须通过commit方式来变更状态。如果有异步/复杂操作可以通过dispatch 所以在做任何变更的时候,vuex是可以将你提交的操作拦截,在更改状态之前插入其他的操作(日志、缓存等提前的操作),甚至都可以拒绝你的操作(你的操作是不合理和不被允许的情况),这个对程序的管理比较强大

image.png

整合vuex

在vue-cli的环境下引入(vuex是插件)

  1. vue add vuex

核心概念

  • state 状态、数据
  • mutations 更改状态的函数
  • actions 异步操作
  • store 包含以上概念的容器
  • getters 派生状态

状态 - state

state保存应用状态

  1. export default new Vuex.Store({
  2. state: { counter: 0 },
  3. })

状态变更 - mutations

mutations用于修改状态,store.js

  1. export default new Vuex.Store({
  2. mutations: {
  3. add (state) {
  4. state.counter++
  5. }
  6. }
  7. })

派生状态 - getters

从state派生出新状态,类似计算属性

  1. export default new Vuex.Store({
  2. getters: {
  3. doubleCounter (state) { // 计算剩余数量
  4. return state.counter * 2;
  5. }
  6. }
  7. })

动作 - actions

添加业务逻辑,类似于controller

  1. export default new Vuex.Store({
  2. actions: {
  3. add ({ commit }) {
  4. setTimeout(() => {
  5. commit('add')
  6. }, 1000);
  7. }
  8. }
  9. })

测试代码:

  1. <p @click="$store.commit('add')">counter: {{$store.state.counter}}</p>
  2. <p @click="$store.dispatch('add')">async counter: {{$store.state.counter}}</p>
  3. <p>double:{{$store.getters.doubleCounter}}</p>

vuex原理解析

任务分析

  • 实现插件
    • 实现Store类
      • 维持一个响应式状态state
      • 实现commit()
      • 实现dispatch()
      • getters
    • 挂载$store

初始化:Store声明、install实现,kvuex.js:

  1. let Vue;
  2. class Store {
  3. constructor (options = {}) {
  4. this._vm = new Vue({
  5. data: {
  6. $$state: options.state
  7. }
  8. });
  9. }
  10. get state () {
  11. return this._vm._data.$$state
  12. }
  13. set state (v) {
  14. console.error('please use replaceState to reset state');
  15. }
  16. }
  17. function install (_Vue) {
  18. Vue = _Vue;
  19. Vue.mixin({
  20. beforeCreate () {
  21. if (this.$options.store) {
  22. Vue.prototype.$store = this.$options.store;
  23. }
  24. }
  25. });
  26. }
  27. export default { Store, install };

实现commit:根据用户传入type获取并执行对应mutation

  1. class Store {
  2. constructor (options = {}) {
  3. // 保存用户配置的mutations选项
  4. this._mutations = options.mutations || {}
  5. }
  6. commit (type, payload) {
  7. // 获取type对应的mutation
  8. const entry = this._mutations[type]
  9. if (!entry) {
  10. console.error(`unknown mutation type: ${type}`);
  11. return
  12. }
  13. // 指定上下文为Store实例
  14. // 传递state给mutation
  15. entry(this.state, payload);
  16. }
  17. }

实现actions:根据用户传入type获取并执行对应action

  1. class Store {
  2. constructor (options = {}) {
  3. // 保存用户编写的actions选项
  4. this._actions = options.actions || {}
  5. // 绑定commit上下文否则action中调用commit时可能出问题!!
  6. // 同时也把action绑了,因为action可以互调
  7. const store = this
  8. const { commit, action } = store
  9. this.commit = function boundCommit (type, payload) {
  10. commit.call(store, type, payload)
  11. }
  12. this.action = function boundAction (type, payload) {
  13. return action.call(store, type, payload)
  14. }
  15. }
  16. dispatch (type, payload) {
  17. // 获取用户编写的type对应的action
  18. const entry = this._actions[type]
  19. if (!entry) {
  20. console.error(`unknown action type: ${type}`);
  21. return
  22. }
  23. // 异步结果处理常常需要返回Promise
  24. return entry(this, payload);
  25. }
  26. }

作业

在kvuex中实现getters

思考拓展

  1. 尝试去看看vue-router的源码,并解决嵌套路由的问题
    1. 路由器有可能是嵌套的。如果router-view是嵌套关系,则就是子路由。当前的路由向上找父级,如果父级不是根,则继续向上找,直到找到route-viewrouter-view需要知道自身的层级的深度,将来在匹配的路由中才能找到合适的匹配项
    image.png

image.png

image.png

  1. 提前了解vue数据响应原理为下节课做准备

作业

1.嵌套路由的解决方案

  • router-view的深度标记
  • 路由匹配时获取代表深度层级的matched数组

2.实现vuex的getters

3.了解vue的响应式原理

  1. 做响应式数据方式:
  2. Object.defineProperty()
  3. Vue.util.defineReactive()
  4. Vue.observable()
  5. new Vue({
  6. data(){}
  7. })

vue中数据响应式的实现Object.defineProperty() -> 这个是最底层的实现

Vue.util.defineReactive()

Vue.observable() -> vue源码