万字总结Vue(包含全家桶),希望这一篇可以帮到您

基础使用

原文地址:https://juejin.im/post/5ef959e25188252e974ede2d#heading-25

以下代码均经过自己测试,可以复制直接看效果。注意引入Vue文件

全部代码等所有都更新完成之后会上传GitHub或者码云.我会抓紧时间更新的

Vue基础部分和手写Vue部分(解释还没更新出来)已经上传.需要请点击这里

VueX和VueRouter 部分更新也上传了。需要点击这里

VueX差了辅助函数之类。VueRouter基本完成。后续可能会有单元测试和SSR

渲染优先级

  1. render>template>data的插值表达式
  2. {{}} 放的是表达式的时候会 输出结果,内部转为函数



    
    基本概念
    


显示优先级

  • 第一个是render 有render方法就输出render的渲染结果
  • 第二个是template 有template方法就输出template的内容
  • 最后一个是data,如果两者不存在 则输出data里面的插值表达式
  • {{ }} 当这里面放的是一个表达式的时候,会输出表达式的结果 原因 会转化成一个函数 render

指令修饰符,有好多 自己官网看

{{ msg }}

v-model

v-model 实际上是一个 语法糖




v-model 的应用




    
    v-model
    


{{ msg }}

@input

这个是@chage

v-model 是上面@input的语法糖

@input 和@change 区别 一个是 聚焦的时候 一个是 失去焦点的时候

下拉列表

{{ selected }}

下拉列表多选 这样绑定的值必须是一个列表

{{ selectedMore }}

复选框

{{ checked }}
游泳 洗澡 睡觉

单选框

{{ radioed }}

v-model 修饰符

{{ attr }}

{{ attrText }}作用类似@chage

{{ attrText }} 去除空格

watch

观测值的变化 执行对应函数

三种写法:

  1. 添加deep属性,表明要深度遍历
  2. 添加immediate属性,表明 立即执行
  3. 添加 name属性,执行methods的这个方法



    
    
    
    Document


{{ msg }} {{ name }}

computed

经常使用get,但是还有一个set




    
    
    
    Document


全选:

watch 和computed区别

  1. computed不会立马取值,用到的时候才会取值. 并且有缓存,依赖数据不改变不更新结果
  2. watch 立即执行,会先算出来一个老值.数据变化就执行函数

filter

过滤器,将属性进行格式化后在进行展示

分为 全局局部两种

会接受两个参数,一个是要格式化的数据,一个是格式化的规则




    
    Title
    
    


局部

{{ timer | format1('YYYY:MM:DD') }}

全局

{{ timer | format('YYYY:MM:DD') }}
复制代码

指令

同样分为 局部全局

使用的时候 在想要使用的标签上添加 v-xxx xxx为指令名字就可以




    
    指令
    


自动获取焦点

点击显示 日历效果

日历显示 时间

指令有生命周期.有钩子

  • bind 绑定上的时候会执行一次
  • inserted 插入的时候
  • update 当引用数据发生变化的时候
  • componentUpdate 模板更新
  • unbind 解除绑定
  • 默认写成一个函数 bind+update

指令传入三个参数的含义

  • el 当前元素
  • bindings 有关指令的各个属性
  • vNode 虚拟节点
  • vNode.context Vue实例

实例属性

介绍一些常用的 实例属性

  1. $mount() 挂载,参数写要挂载的节点。如果不写,则挂载的$el属性上,可以手动挂载(比如写Message弹框)
  2. $options 获取用户写的配置
  3. $watch 跟watch 用法一样



    
    实例属性
    


{{ msg }}
复制代码

进阶

动画

动画分为两种,一种是CSS动画,一种是js动画。各位按照需求选择

因为个人推荐使用CSS作动画,所以JS版本就不再写出来了。有兴趣的朋友可以点击这里

css版本

就是把 要做动画的DOM元素用transition包裹一下

然后记住一下6个名字,分别对应动画不同的周期

  1. .v-enter 进入动画时候
  2. .v-enter-active 进入动画过程中
  3. .v-enter-to 进入动画进行到最后
  4. .v-leave 这个没有实际意义,为了美感
  5. .v-leave-active 离开动画过程中
  6. .v-leave-to 离开动画结束



    
    动画
    


transiton 可以有name 属性 给改名字,这样一些 v-leave 则变成的 name-leave

transition Vue动画标签 transition-group 动画组

复制代码

动画组

与上一个不一样的是,这个数多组动画。

区别 使用了 transition-group

动画名称

  • enter-class
  • enter-active-class
  • enter-to-class (2.1.8+)
  • leave-class
  • leave-active-class
  • leave-to-class (2.1.8+)



    
    动画
    
    


vue中动画组

  • {{ arr }}
  • 复制代码

    组件

    组件通讯(重点)

    我总结了 一下,大概以下几种

    1. props+emit
    2. provide+inject 单项 数据流
    3. $parent+$children 直接触发父/子类的事件
    4. $broadcast + $dispatch 自己在原型上写的
    5. $attrs+$listeners 通过所有属性和方法的集合获取
    6. $bus 类似Vuex
    7. Vuex Vue插件

    props+emit

    // parents
    
    
    
    // son
    
    
    复制代码

    第一种是 传递一个属性还有一个函数,子代接收到之后,可以在使用

    第二种是 利用$emit, 直接触发 在父级定义的函数

    特别注意,这个click2不是原生的,你把它叫做 a , b 之类等都可以

    provide+inject

    官方建议

    provideinject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。

    这个就比较简单。类似于reactredux

    // parent
    
    
    
    // son2
    
    
    
    // grandSon
    
    
    复制代码

    写一个Son2的作用,就是让大家明白,隔代也是可以的。一个提供,一个接收之后就可以使用

    $parent+$children

    这个我就直接用上面的代码了。这个比较简单。就是通过$parent/$children 找到它的父/子级。然后 使用或者触发他们的属性或者方法

    $broadcast + $dispatch

    再次引用官方的话

    $dispatch$broadcast 已经被弃用。请使用更多简明清晰的组件间通信和更好的状态管理方案,如:Vuex。

    当然,我们还是介绍一些这两个方法,各位看需要使用(小声bb一下,我觉得Vuex真香)

    // 在main.js上
    import Vue from 'vue'
    import App from './App';
    
    /**
     *  找父节点触发事件
     * @param eventName
     * @param ComName
     * @param Value
     */
    Vue.prototype.$dispatch = function (eventName, ComName = '', Value = '') {
      let parent = this.$parent;
      while (parent) {
        if (ComName && parent.$options.name === ComName) {
          parent.$emit(eventName, Value)
          return
        } else {
          parent.$emit(eventName, Value)
          parent = parent.$parent
        }
      }
    }
    /**
     * 找子节点触发事件
     * @param eventName
     * @param ComName
     * @param value
     */
    Vue.prototype.$broadcast = function (eventName, ComName = '', value = '') {
      let children = this.$children // 获取得是数组
      function broadcast(children) {
        for (let i = 0; i < children.length; i++) {
          let child = children[i]
          if (ComName === child.$options.name) {
            child.$emit(eventName, value)
            return
          } else {
            if (child.$children) {
              broadcast(child)
            }
          }
        }
      }
      broadcast(children)
    }
    复制代码

    这两个方法利用了$parent$children。不断获取父/子节点,触发相对应的事件。

    我这个$dispatchelse写的是,如果不是这个组件的事件,我也触发了。其实应该把这句删除。只 继续往上找就可以

    使用

    // 直接这样使用就好
     
    复制代码

    $attrs+$listeners

    官方定义

    // APP.vue
    
    
    
    // test.vue
    
    
    
    //test2.vue
    
    
    复制代码

    注意

    1. 父级这样传递属性的过程中,会把这个属性绑定在DOM元素上,(被props接收的不会被绑定),可以在子类中使用inheritAttrs:false,来设置取消绑定
    2. 使用得时候,直接使用$attrs.x/$listeners.x使用
    3. 往下一代传递的时候,直接使用v-bind="$attrs" v-on="$listeners",就可以把没有被props接收过的都传给下一代使用

    $bus

    就是挂载了一个Vue实例

    // APP.vue
    
    
    // $bus使用
    
    
    复制代码

    Vuex

    请往后面看

    插槽

    
    
    
    // test1
    
    
    复制代码

    这个比较简单,就不再多多叙述。强调一点,新老版本区别

    1. 新版本只可以用template进心包裹
    2. 老版本可以用div

    总结

    看完上面的内容可以尝试模仿写一下 element-ui的表单组件。他们使用了async-validator作为校验。

    github地址

    Vue

    同样有一个简单版本Vue数据响应式和编译原理分析 和 模拟实战.这个版本没有用到虚拟Dom等。

    虚拟dom。个人也总结了一篇帮你深入了解虚拟DOM和DOM-diff,希望能帮到各位

    仅仅是一个简单的实现。但是实现了 部分指令

    完整部分(即这次总结的,带上虚拟dom等等),这个内容由于太多(标题细分太多。不好去寻找)。我另写了一篇文章,还在整理中,1号大概可以放出来。

    贴一个图证明一下。实在是考虑的太多,所以写出来比较慢

     

    万字总结Vue(包含全家桶),希望这一篇可以帮到您_第1张图片

    vueX

    ​ 推荐一下自己的另一篇文章Vuex的简单实现,感觉这一篇写的相对简单一点

    Vuex 用法

    这个就不多做解释了。不太熟练的朋友可以先去看官方文档

    给出一下我的数据定义

    // store/index.js
    import Vue from 'vue'
    // import Vuex from 'vuex'
    import Vuex from './../vuex2'
    
    Vue.use(Vuex)
    const store = new Vuex.Store({
      state: {
        age: 10
      },
      strict: true,
      getters: {
        myAge(state) {
          return state.age + 30
        }
      },
      mutations: {
        // 同步更改state  在严格模式下不可以使用异步
        change(state, payload) {
          state.age += payload
        }
      },
      actions: {
        // 异步更改state
        asyncChange({ commit }, payload) {
          setTimeout(()=>{
            commit('change', payload)
          }, 1000)
        }
      }
    })
    export default store
    复制代码

    基本Vuex的实现

    install方法

    Vuex作为一个 插件,首先执行的是install方法,我们希望的是,任何组件都可以访问到这里面的数据。组件的渲染是由父到子的,所以我们既可以先进行判断。如果它是根节点,就把这个属性挂载到根节点上,如果不是,就找他父级的这个属性,然后挂载到这个Vue实例上

    // 官方Api 会把Vue作为参数传入
    const install = (_vue)=>{
        Vue = _vue
      Vue.mixin({ // 先父后子
        beforeCreate() {
          if (this.$options.store) { // 跟节点
            this.$store = this.$options.store
          } else { // 往子节点传递
            this.$store = this.$parent && this.$parent.$store
          }
        }
      })
    }
    复制代码

    访问state的实现

    我们平时使用的过程是是这样的

    const store = new Vuex.Store({
        state:{
            
        }
    })
    复制代码

    所以我们发现,我们实际上是new了一个VueX 的 Store 类。接下来我们开始写这个类。

    let Vue
    class Store{
        constructor(options) {
            this.state = options.state
            this.mutations = {}
            this.actions = {}
            this.getters = {}
            this.actions = {}
        }
    }
    // 下面是install 方法
    复制代码

    再其他组件中使用

    // VuexUse.vue
    
    
    复制代码

    大家会看到 输出10.当点击按钮的时候。再次打印,会发现数据已经发生变化,但是视图并没有刷新。我们应该让数据更新之后,视图也跟着刷新。这时候我们就应该想到用Vue的特性。我们改造一下刚才的代码

    let Vue
    class Store{
        constructor(options) {
            this.state = new Vue({ data: { state: options.state } }).state
            this.mutations = options.mutations || {}
            this.actions = options.actions || {}
            this.getters = {}
        }
    }
    // 下面是install 方法
    复制代码

    这样我们就实现了数据改变,就刷新视图

    commit 和dispatch

    VueX,中,更改状态一般需要这两个方法,一个是同步,一个是异步,我们来实现一下这两个方法

    // 使用的时候
    change() {
        this.$store.commit('xxx', 10)
    },
    复制代码

    所以,这两个方法是写在Store类里面的

    let Vue
    class Store{
        constructor(options) {
            this.state = new Vue({ data: { state: options.state } }).state
            this.mutations = options.mutations || {}
            this.actions = options.actions || {}
            this.getters = {}
        }
        commit = (mutationName, payload)=>{
         this.mutations[mutationName](this.state, payload)
      	}	
        dispatch = (actionName, payload)=>{
        	this.actions[actionName](this, payload)
      	}
    }
    复制代码

    commit,我觉得大家都可以看懂,就是找到用户定义的mutations,把参数传入,就可以执行了。

    dispatch,为什么要传入this原因,在定义的时候,使用的是ES6的解构赋值,所以这里要把this传入

    注意,这两种方法还可以使用 柯里化来实现,这样传值的时候只用传入 payload,更方便一点

    getter实现

    首先我们要明白,getter是作什么用的。我 个人理解,需要对访问数据进行一定处理。也就是我们访问这个属性的时候,得到这个函数的返回结果。

    let Vue
    class Store{
        constructor(options) {
            this.state = new Vue({ data: { state: options.state } }).state
            this.mutations = options.mutations || {}
            this.actions = options.actions || {}
           
            // 这下面是修改的部分
            options.getters && this.handleGetters(options.getters)
        }
        
       handleGetters(getters) {
        this.getters = {}
        Object.keys(getters).forEach(key=>{
          Object.defineProperty(this.getters, key, {
            get: ()=>{
              return getters[key](this.state)
            }
          })
        })
      }
    }
    复制代码

    解释一下handleGetters这一段代码

    1. 获取每个函数函数名称
    2. 根据每个函数的名称 设置对应的返回值

    这段代码相对比较简单,这样就实现了getters

    模块(module)功能的实现

    store/index

    import Vue from 'vue'
    // import Vuex from 'vuex'
    import Vuex from './../vuex'
    
    Vue.use(Vuex)
    const store = new Vuex.Store({
      state: {
        age: 10
      },
      strict: true,
      getters: {
        myAge(state) {
          return state.age + 30
        }
      },
      mutations: {
        change(state, payload) {
          state.age += payload
        }
      },
      actions: {
        // 异步更改state
        asyncChange({ commit }, payload) {
          setTimeout(()=>{
            commit('change', payload)
          }, 1000)
        }
      },
      modules: {
        a: {
          namespaced: true,
          state: {
            num: 'a1'
          },
          mutations: {
            // 同步更改state  在严格模式下不可以使用异步
            change(state, payload) {
              console.log(state, payload) // 是自己这个模块内的state
              console.log('a')
            }
          }
        },
        b: {
          state: {
            num: 'b1'
          },
          mutations: {
            // 同步更改state  在严格模式下不可以使用异步
            change(state, payload) {
              console.log('b')
            }
          },
          modules: {
            c: {
              namespaced: true,
              state: {
                num: 'c1'
              },
              mutations: {
                // 同步更改state  在严格模式下不可以使用异步
                change(state, payload) {
                  console.log('c')
                }
              }
            }
          }
        }
      }
    })
    export default store
    
    复制代码

    接下来这一部分可能会难理解一点。我尽力把我学习到给大家清楚的讲出来。这部分会对之前的代码进行大量修改

    先改造一下我们的Store,变会最开始的样子

    class ModuleCollection {
        
    }
    
    let Vue
    class Store{
        constructor(options) {
            this.state = options.state
            this.mutations = {}
            this.actions = {}
            this.getters = {}
            this.modules = new ModuleCollection(options)
            console.log('收集完成的模块')
            console.log(this.modules)
        }
    }
    // 下面是install 方法
    复制代码

    现在,我们需要模块化,所以我们要写一个方法来 格式化数据,变成我们想要的样子

    思路,我们要把这边模块进行遍历 注册,如果模块下面还有子类,则继续遍历。 核心方法, reduce

    ModuleCollection

    /**
     * 循环对象的值
     * @param obj
     * @param cb
     */
    function forEach(obj, cb) {
      Object.keys(obj).forEach(key=>{
        cb(key, obj[key])
      })
    }
    
    
    class ModuleCollection {
      constructor(options) {
        this.register([], options)
      }
      register(path, rootModule) {
          
        // 格式化模块
        const rawModule = { 
          _raw: rootModule, //原来的modules
          _children: {},  // 孩子
          state: rootModule.state // 原来的数据
        }
        
        // 双向记录 把格式化之后的数据记录下来
        rootModule.rawModule = rawModule 
          
        // 判断是不是根的存在
        if (!this.root) {
          // 第一次肯定不存在
          this.root = rawModule
        } else {
          // 核心  返回的是各个module 对应的格式化后的模块
          const parentModel = path.slice(0, -1).reduce((root, current)=>{
            console.log(current)
            return root._children[current]
          }, this.root)
        /----------------------------------------------------/
          parentModel._children[path[path.length - 1]] = rawModule
        }
    
        // 递归,遍历子代。核心逻辑
        if (rootModule.modules) {
          forEach(rootModule.modules, (moduleName, module)=>{
            this.register(path.concat(moduleName), module)
          })
        }
      }
    }
    复制代码

    主要解释一下 /-------------/上下的代码。上面的parentModel,指的是模块

    1. 第一次 parentModelthis.rootrawModulea模块的定义
    2. 第二次 parentModelthis.rootrawModuleb模块的定义
    3. 第三次 parentModelb模块,rawModulec模块的定义

    打印一下 this.modules

     

    万字总结Vue(包含全家桶),希望这一篇可以帮到您_第2张图片

     

     

    现在我们就把所有的模块进行了 格式化。接下来。我们就要对 我们格式化后的数据进行安装。使他们可以访问得到

    总结一下,这个函数的作用就是把 我们传入的modules进行一个格式化,并且将模块进行分类。

    installModule

    这个函数的作用 循环遍历子节点,安装 state action mutation getters

    /**
     * 安装 state action mutation getters  并
     * @param store Vuex 中的store
     * @param rootState 根state
     * @param path      路径
     * @param rawModule   原模块
     */
    function installModule(store, rootState, path, rawModule) {
    
      // 安装state
      if (path.length > 0) { // 证明是子节点 
        const parentState = path.slice(0, -1).reduce((root, current)=>{
          return rootState[current] 
        }, rootState)
        
        // 官方API。
        // 向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新
        Vue.set(parentState, path[path.length - 1], rawModule.state)
      }
    
      // rawModule 里面有 
      // _raw 原来的模块 
      // _children 孩子 
      // state 原来的state
        
        
      // 安装getters 
      // 注意状态的使用,要使用本模块的状态
      const getters = rawModule._raw.getters || {}
      if (getters) {
        forEach(getters, (getterName, getterFun)=>{
          Object.defineProperty(store.getters, getterName, {
            get: ()=>getterFun(rawModule.state) 
          })
        })
      }
    
      // mutations跟actions 差不多。都是把 所有的模块的函数存在 root根模块中  使用的时候直接遍历
      const mutations = rawModule._raw.mutations || {}
      if (mutations) {
        forEach(mutations, (mutationName, mutationFun)=>{
          // 写一个发布订阅模式
          const arr = store.mutations[mutationName] || (store.mutations[mutationName] = [])
          arr.push((payload)=>{
            mutationFun(rawModule.state, payload)
          })
        })
      }
    
      const actions = rawModule._raw.actions || {}
      if (actions) {
        forEach(actions, (actionName, actionsFun)=>{
          const arr = store.actions[actionName] || (store.actions[actionName] = [])
          arr.push((payload)=>{
            actionsFun(store, payload)
          })
        })
      }
    
      // 遍历子节点
      forEach(rawModule._children, (moduleName, rawModule)=>{
        // console.log(rawModule) // 一个个子节点
        installModule(store, rootState, path.concat(moduleName), rawModule)
      })
    }
    复制代码

    storerootState始终是。Vuex中的store 和根上面的state

    1. 第一次 patch[]rawModule模块的定义
    2. 第二次 patch['a']rawModulea模块的定义
    3. 第三次 patch['b']rawModuleb模块的定义
      1. 走进来发现 b下面还有modules,所以patch['b',‘c’]rawModulec模块的定义

    命名空间的实现

    命名空间这个就简单了。只需要在每个方法前面加上x/就可以了

    function installModule(store, rootState, path, rawModule) {
      
      // 命名空间的实现  获取命名
      let root = store.modules.root // 拿到的是格式化之后的结果
      const nameSpace = path.reduce((str, current)=>{
        root = root._children[current]
        str = str + (root._raw.namespaced ? current + '/' : '')
        return str
      }, '')
    
      // 安装state  这里没有发生变化
      if (path.length > 0) {
        // 证明是子节点
        const parentState = path.slice(0, -1).reduce((root, current)=>{
          return rootState[current]
        }, rootState)
        Vue.set(parentState, path[path.length - 1], rawModule.state)
      }
    
     
      // rawModule 里面有 
      // _raw 原来的模块 
      // _children 孩子 
      // state 原来的state
        
      // 安装getters  把方法前面加上 命名
      const getters = rawModule._raw.getters || {}
      if (getters) {
        forEach(getters, (getterName, getterFun)=>{
          Object.defineProperty(store.getters, nameSpace + getterName, {
            get: ()=>getterFun(rawModule.state) // 使用模块中的状态
          })
        })
      }
    
      const mutations = rawModule._raw.mutations || {}
      if (mutations) {
        forEach(mutations, (mutationName, mutationFun)=>{
          // 写一个发布订阅模式
          const arr = store.mutations[nameSpace + mutationName] || (store.mutations[nameSpace + mutationName] = [])
          arr.push((payload)=>{
            mutationFun(rawModule.state, payload)
          })
        })
      }
    
      const actions = rawModule._raw.actions || {}
      if (actions) {
        forEach(actions, (actionName, actionsFun)=>{
          const arr = store.actions[nameSpace + actionName] || (store.actions[nameSpace + actionName] = [])
          arr.push((payload)=>{
            actionsFun(store, payload)
          })
        })
      }
    
      // 遍历子节点
      forEach(rawModule._children, (moduleName, rawModule)=>{
        // console.log(rawModule) // 一个个子节点
        installModule(store, rootState, path.concat(moduleName), rawModule)
      })
    }
    复制代码

    ''(空)字符串开始。根节点不需要命名空间

    registerModule API的实现

    class Store {
      constructor(options) {
        this.state = new Vue({ data: { state: options.state } }).state
        this.mutations = {}
        this.actions = {}
        this.getters = {}
        // 模块收集  并格式化
        this.modules = new ModuleCollection(options)
        console.log('收集完成的模块')
        console.log(this.modules)
        // 模块的安装并访问  store rootState path 根模块  安装全部模块
        installModule(this, this.state, [], this.modules.root)
      }
      // 模块开发完之后的写法
      commit = (mutationName, payload)=>{
        this.mutations[mutationName].forEach(fn=>fn(payload))
      }
      dispatch = (actionName, payload)=>{
        this.actions[actionName].forEach(fn=>fn(payload))
      }
      /**
       * 自定义注册 module
       * @param moduleName
       * @param module
       */
      registerModule(moduleName, module) {
        if (!Array.isArray(moduleName)) {
          moduleName = [moduleName]
        }
        this.modules.register(moduleName, module)
        console.log(this.modules.root)
        // 安装当前模块
        installModule(this, this.state, moduleName, module.rawModule)
      }
    }
    复制代码

    思路很简单,就是把 注册的module,进行格式化之后。再进行安装就可以

    注意,要安装位置要确定好哦

    辅助函数

    VueRouter

    Vuex一样,也写过一篇比较简单的实现 VueRouter的简单实现,感觉这一篇写的相对简单一点

    这部分我个人觉得自己掌握的不是特别好。所以 讲述的不太清楚。仅提供一个思路。

    最开始install方法

    在我们的平常使用过程中,除了router-linkrouter-view。最常用的可能就是this.$router.push(xx)。所以我们还是跟VueX的做法差不多。在每一个实例上挂在一个属性

    const install = (Vue)=>{
      Vue.mixin({
        beforeCreate() {
          if (this.$options.router) {
            // console.log(this) // 指的是一个new Vue
            this._routerRoot = this // 把vue实例挂载到这个属性上
            this._router = this.$options.router // 用户传入得 router
            // 路由的初始化
            this._router.init(this)
          } else {
            this._routerRoot = this.$parent && this.$parent._routerRoot
          }
        }
      })
    }
    export default install
    复制代码

    以上代码只做了两件事。

    1. 挂载属性
    2. 调用路由的初始化方法。对路由进行初始化

    index文件

    首先我们应该分析。我们这个主文件应该有什么。在我们日常使用的过程中,一般是import VueRouter from 'vue-router'

    所以

    1. 我们应该友一个VueRoter类。
    2. 上面得有初始化方法install.。
    3. VueRoter类的constructor中,我们应该对用户传入的数据进行处理。还有就是分析它路由模式
    4. init方法,要可以监听到路由变换,然后跳转到对应的 路由。渲染对应的组件

    分析完之后。我们就开始着手写

    我先把大体框架给大家展示一下

    import install from './install'
    import createMatcher from './createMatcher'
    import HashHistory from './history/hash'
    
    class VueRouter {
      constructor(options) {
        // matcher 匹配器 处理树形结构 将他扁平化
        // 返回两个方法 addStore match 匹配对应结果
        this.matcher = createMatcher(options.routes || [])
    
        // 内部需要用 hash history 进行路由的初始化工作
        // base 表示基类,实现所有路由的公告方法都放在基本类上 保证不同路由API 有相同的使用方法
        this.history = new HashHistory(this)
      }
    
      push(location) {
        
      }
    
      init(app) {
        // app 是顶层Vue 实例
        // 获取到路径 并进行跳转 并渲染对应组件
        // 匹配一次完成后,监听路有变化,完成之后的更新操作
        
      }
    }
    
    VueRouter.install = install
    export default VueRouter
    
    复制代码

    createMatcher方法

    里面出现的方法在下面都会有所解释

    import createRouteMap from './createRouteMap'
    import { createRoute } from './history/base'
    
    export default function createMatcher(routes) {
      // 开始扁平化数据
      const { pathList, pathMap } = createRouteMap(routes)
    
      // 重载s
      function addRoute(routes) {
        createRouteMap(routes, pathList, pathMap)
      }
    
      function match(location) {
        console.log('create里面的match' + location)
        // 从pathMap获取的location
        const record = pathMap[location]
        // console.log(record)
        return createRoute(record, {
          path: location
        })
      }
      return {
        addRoute, match
      }
    }
    复制代码

    我们先通过createRouteMap方法,把传入的routes(即用户传入的配置)进行一个格式化处理,得到一个pathList(地址的列表)pathMap(地址映射,里面有地址,组件等等属性)

    官方API中,有一个交addRotes,也就是再添加进一组路由。

    我们还是利用createRouteMap方法。这个方法具体是什么样的看下面

    match方法的作用是匹配器,匹配传入的location(地址)。返回相对应的 记录

    createRouteMap方法

    export default function createRouteMap(routes, oldPathList, oldPathMap) {
      const pathList = oldPathList || []
      const pathMap = oldPathMap || Object.create(null)
      // Object.create(null) 和 {} 区别  前者没有原型链
      // 数组扁平化
      routes.forEach(route=>{
          addRouteRecord(route, pathList, pathMap)
        }
      )
      return {
        pathList, pathMap
      }
    }
    
    function addRouteRecord(route, pathList, pathMap, parent) {
      const path = parent ? parent.path + '/' + route.path : route.path
      const record = {
        path,
        component: route.component,
        parent
        // todo
      }
      if (!pathList[route]) {
        pathList.push(path)
        pathMap[path] = record
      }
      if (route.children) {
        route.children.forEach(route=>{
          addRouteRecord(route, pathList, pathMap, record)
        })
      }
    }
    

    Object.create(null) 和 {} 区别 前者没有原型链

    {} 会存在一堆的属性

     

    万字总结Vue(包含全家桶),希望这一篇可以帮到您_第3张图片

     

     

     

    万字总结Vue(包含全家桶),希望这一篇可以帮到您_第4张图片

     

    **Object.create(null)**不存在这些

     

    addRouteRecord 是这个的核心方法。它的工作是

    1. 先差找父级元素。如果有。则加上父级 例如 about/a。没有就是本身
    2. 然后生成一条记录record
    3. 判断你传入的route(即每一项路由)是否在pathList里面。在了就跳过。不在就添加进去。 **这个方法就实现了addRoutes**的作用
    4. 递归遍历。如果有孩子继续添加

    createRoute方法

    这个方法及其关键!!!!

    原因:比如我们渲染about/a这个路径的组件。我们是不是必须得渲染about,这样才可以渲染a

    所以这个方法的主要作用就是。把路径的父级也都存下来

    export function createRoute(record, location) {
      const res = [] // 如果匹配到路径 就放进来
      if (record) {
        while (record) {
          res.unshift(record)
          record = record.parent
        }
      } // 把父级路径也存放下来
      console.log(res, location)
      return {
        ...location,
        matched: res
      }
    }
    复制代码

    history方法

    这个即解释this.history = new HashHistory(this)

    为什么要单独列出来?因为有不同的 路由模式,但是有公共的处理方法。当然还需要有不同的方法来处理不同的路由。

    我们这里只考虑hash

    base.js

    export function createRoute(record, location) {
      const res = [] // 如果匹配到路径 就放进来
      if (record) {
        while (record) {
          res.unshift(record)
          record = record.parent
        }
      } // 把父级路径也存放下来
      console.log(res, location)
      return {
        ...location,
        matched: res
      }
    }
    
    class History {
      constructor(router) {
        this.router = router
        this.current = createRoute(null, {
          path: '/'// 默认路径
        })
      }
    
      transitionTo(location, cb) { // 最好屏蔽一下,以防止多次调用
        console.log(location, cb)
        // 得到路径 开始匹配对应的模板
        const r = this.router.match(location)
        this.current = r // 对当前路径进行更新
        // eslint-disable-next-line eqeqeq
        if (location === this.current.path && r.matched.length === this.current.matched) {
          return
        }
        this.cb && this.cb(r)
        cb && cb()
      }
    
      setupListeners() {
        window.addEventListener('hashchange', ()=>{
          this.transitionTo(window.location.hash.slice(1))
        })
      }
    
      listen(cb) {
        this.cb = cb
      }
    }
    
    export default History


     

    可以看出 这个base.js做了几件事

    1. 初始化了一个默认路由
    2. 提供了跳转方法
    3. 监听了路由变化
    4. listen这个等会再说

    transitionTo中间的if判断。是为了防止多次调用的。

    hash.js

    import History from './base'
    
    function ensureSlash() {
      if (window.location.hash) {
        return
      }
      window.location.hash = '/'
    }
    class HashHistory extends History {
      constructor(router) {
        super(router) // super === parent.call(this)   向父级传递router
        this.router = router
        ensureSlash() // 确保有hash值
      }
    
      getCurrentLocation() {
        return window.location.hash.slice(1) // 除了# 号后面的路径
      }
    }
    
    export default HashHistory
    复制代码

    这个就比较简单了。就不再解释了

    重新回到index.js

    import install from './install'
    import createMatcher from './createMatcher'
    import HashHistory from './history/hash'
    
    class VueRouter {
      constructor(options) {
        // matcher 匹配器 处理树形结构 将他扁平化
        // 返回两个方法 addStore match 匹配对应结果
        this.matcher = createMatcher(options.routes || [])
    
        // 内部需要用 hash history 进行路由的初始化工作
        // base 表示基类,实现所有路由的公告方法都放在基本类上 保证不同路由API 有相同的使用方法
        this.history = new HashHistory(this)
      }
    
      match(location) { // 作了一层封装 返回匹配结果
        return this.matcher.match(location)
      }
    
      push(location) {
        this.history.transitionTo(location, ()=>{
          window.location.hash = location// 这样的话 要渲染两遍 一边transitionTo 一边是hash的监听
        }) // hash没有改变 要改变hash
      }
    
      init(app) {
        // app 是顶层Vue 实例
        // console.log(app)
        // 获取到路径 并进行跳转 并渲染对应组件
        // 匹配一次完成后,监听路有变化,完成之后的更新操作
        const history = this.history
        const setupHashListener = ()=>{ // 监听之后回调
          history.setupListeners() // 监听路由变化   父类
        }
        history.transitionTo( // 跳转方法         父类
          history.getCurrentLocation(), // 获取当前路径   分路由 所以是子类
          setupHashListener
        )
        // 订阅好 然后路由 属性变化 更新此方法
        history.listen((route)=>{
          app._route = route
        })
      }
    }
    
    VueRouter.install = install
    export default VueRouter
    复制代码

    改造完之后的index.js做的事,

    1. 监听路由。
    2. 跳转路由。
    3. 设置改变_route的函数(这时候 _route还不是动态的)

    回到install

    import RouterView from './components/router-view'
    import RouterLink from './components/router-link'
    
    const install = (Vue)=>{
      Vue.mixin({
        beforeCreate() {
          if (this.$options.router) {
            // console.log(this) // 指的是一个new Vue
            this._routerRoot = this
            this._router = this.$options.router // 用户传入得 router
            // 路由的初始化
            this._router.init(this)
            // 将current 定义成 响应式的。数据改变则刷新视图
            console.log(this._router)
            // 给当前实例创建了 _route 属性, 取自this._router.history.current
            Vue.util.defineReactive(this, '_route', this._router.history.current)
            // 定义之后 更新_route
          } else {
            this._routerRoot = this.$parent && this.$parent._routerRoot
          }
        }
      })
      Object.defineProperty(Vue.prototype, '$route', {
        get() {
          console.log(this._routerRoot._route)
          return this._routerRoot._route
        }
      })
    
      Object.defineProperty(Vue.prototype, '$router', {
        get() {
          return this._routerRoot._router
        }
      })
      Vue.component('RouterView', RouterView)
      Vue.component('RouterLink', RouterLink)
    }
    export default install

    回到install方法,初始化之后。把_route设置成动态(有getset)。

    之后数据发生改变,视图就会刷新。

    组件

    RouterView

    export default {
      functional: true, // 函数式组件 没有状态 没有this
      render(h, { parent, data }) { // 里面有很多options 这是通过解构赋值出来的
        // console.log(options)
        const route = parent.$route // 被放到了vue 原型上
        console.log(route)
        let depth = 0
        // $vnode表示占位符Vnode
        while (parent) {
          if (parent.$vnode && parent.$vnode.data.routerView) {
            depth++
          }
          parent = parent.$parent
        }
        data.routerView = true
        const record = route.matched[depth]
        console.log(record)
        if (!record) {
          return h()
        }
        return h(record.component, data)
      }
    }
    复制代码

    这段代码中最难理解的就是 depth

    route是属性。 这段代码在history/base.jscreateRoute 返回的结果中有一个match。里面存放了所有的 父级路径

    routerView的解释。自定义属性。看他是否是根节点。 第一次进来的时候 ,渲染App组件(里面放有RouterView)。如果存在,证明要渲染的是下一个节点了

    router是方法

    RouterLink

    export default {
      props: {
        to: {
          type: String,
          require: true
        },
        tag: {
          type: String,
          default: 'a'
        }
      },
      methods: {
        handle() {
          this.$router.push(this.to)
        }
      },
      render(h) {
        const tag = this.tag
        return  { this.$slots.default } < /tag>
      }
    }
    复制代码

    我这里用的是jsx语法。觉得看不懂的可以直接用RouterLink.vue正常写来代替

    钩子函数(路由守卫)

    路由index

    ....
    
    router.beforeEach((to, from, next)=>{
      console.log(1)
      setTimeout(()=>{
        next()
      }, 1000)
    })
    router.beforeEach((to, from, next)=>{
      console.log(2)
      setTimeout(()=>{
        next()
      }, 1000)
    })
    export default router
    复制代码

    简单思路

    这个就比较像expresskoa里面的了。

    简单写个思路就是这样。

    // 储存
    let deps = []
    
    // 放置
    beforeXxx(cb){
        this.deps.push(cb)
    }
    
    // 使用
    // 在视图更新或者跳转前
    this.deps.forEach(dep=>{
        dep()
    })
    复制代码

    beforeEach实现

    1. 首先现在index。js文件夹下(VueRouter/index) 做初始化工作

    写过的我就直接用省略号代替了

    class VueRouter {
      constructor(options) {
     ....
        this.beforeEachs = []
      }
    
      match(location) { // 作了一层封装 返回匹配结果
       ....
      }
    
      push(location) {
    ....
      }
    
      init(app) {
    ....
      }
    
      beforeEach(cb) {
        this.beforeEachs.push(cb)
      }
    }
    复制代码

    先把所有的beforeEach都存起来。在跳转之前执行它

    再道history/base文件夹下,找到transitionTo方法.里面的这三句代码是跳转路由的

    	this.current = r // 对当前路径进行更新
        this.cb && this.cb(r)
        cb && cb()
    复制代码

    下面的代码是 如何实现beforeEach的运行的

    // 1. 取到我们 收集到的队列
    const queue = this.router.beforeEachs
    
    // 辅助函数
    const iterator = (hook, next)=>{
        hook(r, this.current, next)
    }
    
    // 运行队列
    runQueue(queue, iterator, ()=>{
        this.upDateRoute(r, cb)
    })
    
    // 这个方法是对上面三句更新方法的封装
    upDateRoute(r, cb) {
        this.current = r
        this.cb && this.cb(r)
        cb && cb()
      }
    //  runQueue 函数定义  其实就是一个递归调用
    function runQueue(queue, iterator, callback) {
      function step(index) {
        if (index === queue.length) {
          return callback()
        }
        const hook = queue[index]
        iterator(hook, ()=>step(index + 1))
      }
      step(0)
    }
    复制代码

    思路:

    1. 获取到收集的beforeEach队列
    2. 让这个队列 依次执行
    3. 为什么不用forJS单线程,用for的话异步会 出现问题

    完整代码

    // history/bsae。js
    export function createRoute(record, location) {
     ...
    }
    
    function runQueue(queue, iterator, callback) {
      function step(index) {
        if (index === queue.length) {
          return callback()
        }
        const hook = queue[index]
        iterator(hook, ()=>step(index + 1))
      }
      step(0)
    }
    
    class History {
      constructor(router) {
        ...
      }
    
      transitionTo(location, cb) { 
        console.log(location, cb)
        const r = this.router.match(location)
        if (location === this.current.path && r.matched.length === this.current.matched) {
          return
        }
        const queue = this.router.beforeEachs
        const iterator = (hook, next)=>{
          hook(r, this.current, next)
        }
        runQueue(queue, iterator, ()=>{
          this.upDateRoute(r, cb)
        })
      }
    
      upDateRoute(r, cb) {
        this.current = r // 对当前路径进行更新
        this.cb && this.cb(r)
        cb && cb()
      }
    
      setupListeners() {
          ...
      }
    
      listen(cb) {
          ...
      }
    }
    
    export default History

    总结

    这个会实现了之后。其他几个钩子函数也就可以进行实现了。方式大体都差不多。即通过回调函数来执行

    路由权限

    推荐一下花裤衩大佬写的权限登陆的文章。 手摸手,带你用vue撸后台 系列二(登录权限篇)

    是我目前看到的关于路由权限 讲解最好的一篇文章

    自己的总结正在努力

    Vue3

    proxy

    阮一峰老师这一本书关于这部分已经写的很好了。我就不再多做叙述了。详情点击这里地址

    let obj = {
      name: {
        achen: {
          name: '阿琛',
          age: 22,
        },
      },
      sex:'男',
      arr: ['吃', '喝', '玩'],
    }
    
    let handler = {
      // target就是原对象,key是键
      get(target, key) {
        // 懒代理 如果取到了这个对象才会触发,没有取到就不会代理
        if (typeof target[key]=== 'object'){
          // 递归调用
          return new Proxy(target[key],handler)
        }
        console.log('收集')
        // return target[key] 老方法
        return Reflect.get(target,key)
      },
      set(target, key, value) {
        console.log('触发更新')
        let oldValue = target[key]
        console.log(oldValue,value,key)
        if (!oldValue){
          console.log('设置属性')
        }else if (oldValue!==value){
          console.log('修改属性')
        }
        // target[key] = value
        // 有返回值
        return Reflect.set(target,key,value)
      },
    }
    
    // 兼容性差,但是可以代理13中方法
    // defineProperty 他只能对特定属性进行拦截
    
    // 拦截的是整个对象
    let proxy = new Proxy(obj,handler)
    // proxy.sex = 'nv'
    // console.log(proxy.sex)
    
    // 数组
    // proxy.arr.push(132)  // 先走一次obj  再收集 push length 在改值
    // proxy.arr[0] = 100  // 直接触发
    proxy.xxx = 100
    
    复制代码

    关于Reflect

    developer.mozilla.org/zh-CN/docs/…


     

    你可能感兴趣的:(万字总结Vue(包含全家桶),希望这一篇可以帮到您)