原文地址:https://juejin.im/post/5ef959e25188252e974ede2d#heading-25
以下代码均经过自己测试,可以复制直接看效果。注意引入Vue文件
全部代码等所有都更新完成之后会上传GitHub或者码云.我会抓紧时间更新的
Vue基础部分和手写Vue部分(解释还没更新出来)已经上传.需要请点击这里
VueX和VueRouter 部分更新也上传了。需要点击这里
VueX差了辅助函数之类。VueRouter基本完成。后续可能会有单元测试和SSR
render
>template
>data
的插值表达式
基本概念
显示优先级
- 第一个是render 有render方法就输出render的渲染结果
- 第二个是template 有template方法就输出template的内容
- 最后一个是data,如果两者不存在 则输出data里面的插值表达式
{{ }} 当这里面放的是一个表达式的时候,会输出表达式的结果 原因 会转化成一个函数 render
指令修饰符,有好多 自己官网看
{{ msg }}
v-model
实际上是一个 语法糖
v-model
{{ msg }}
@input
这个是@chage
v-model 是上面@input的语法糖
@input 和@change 区别 一个是 聚焦的时候 一个是 失去焦点的时候
下拉列表
{{ selected }}
下拉列表多选 这样绑定的值必须是一个列表
{{ selectedMore }}
复选框
{{ checked }}
游泳
洗澡
睡觉
单选框
{{ radioed }}
男
女
v-model 修饰符
{{ attr }}
{{ attrText }}作用类似@chage
{{ attrText }} 去除空格
观测值的变化 执行对应函数
三种写法:
deep
属性,表明要深度遍历immediate
属性,表明 立即执行name
属性,执行methods
的这个方法
Document
{{ msg }}
{{ name }}
经常使用get
,但是还有一个set
Document
全选:
过滤器,将属性进行格式化后在进行展示
分为 全局和 局部两种
会接受两个参数,一个是要格式化的数据,一个是格式化的规则
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实例
介绍一些常用的 实例属性
$mount()
挂载,参数写要挂载的节点。如果不写,则挂载的$el
属性上,可以手动挂载(比如写Message弹框)$options
获取用户写的配置$watch
跟watch 用法一样
实例属性
{{ msg }}
复制代码
动画分为两种,一种是CSS
动画,一种是js
动画。各位按照需求选择
因为个人推荐使用CSS
作动画,所以JS
版本就不再写出来了。有兴趣的朋友可以点击这里
css版本
就是把 要做动画的DOM元素用transition
包裹一下
然后记住一下6个名字,分别对应动画不同的周期
.v-enter
进入动画时候.v-enter-active
进入动画过程中.v-enter-to
进入动画进行到最后.v-leave
这个没有实际意义,为了美感.v-leave-active
离开动画过程中.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 }}
复制代码
我总结了 一下,大概以下几种
props
+emit
provide
+inject
单项 数据流$parent
+$children
直接触发父/子类的事件$broadcast
+ $dispatch
自己在原型上写的$attrs
+$listeners
通过所有属性和方法的集合获取$bus
类似Vuex
Vuex
Vue
插件// parents
Parent
第一种
第二中方法 click2是自己定义的名字,不是原生事件
// son
Son
子组件接收到之后,利用props 属性接受,然后可以直接使用
子组件可以使用父组件传递的属性和函数
我是爸爸给我的钱{{ money }}
第一种
第二种方法
复制代码
第一种是 传递一个属性还有一个函数,子代接收到之后,可以在使用
第二种是 利用$emit
, 直接触发 在父级定义的函数
特别注意,这个click2
不是原生的,你把它叫做 a , b 之类等都可以
provide
+inject
官方建议:
provide
和inject
主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。
这个就比较简单。类似于react
的redux
// parent
Parent
关于son2 跨代通讯
// son2
Son2
// grandSon
GrandSon
跨代通讯的值
{{ parent.money }}
复制代码
写一个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
。不断获取父/子节点,触发相对应的事件。
我这个$dispatch
的else
写的是,如果不是这个组件的事件,我也触发了。其实应该把这句删除。只 继续往上找就可以
使用
// 直接这样使用就好
复制代码
$attrs
+$listeners
官方定义
// APP.vue
// test.vue
我是test
使用$attrs可以获得,但是会绑定在DOM元素上
- 设置 inheritAttrs:false 就不会绑定了
- 当props接收后,arrts将不会显示已经被接收的
{{ $attrs }}
- 这样子代传递
//test2.vue
我是test2
{{ $attrs }}
复制代码
注意
props
接收的不会被绑定),可以在子类中使用inheritAttrs:false,
来设置取消绑定$attrs.x
/$listeners.x
使用v-bind="$attrs" v-on="$listeners"
,就可以把没有被props
接收过的都传给下一代使用$bus
就是挂载了一个Vue
实例
// APP.vue
子组件如何监听父组件的mounted
组件挂载, 先挂载父组件 -》渲染子组件,子mounted -》 父mounted
可以实现任意组件之间的通讯,但只适合小规模的
// $bus使用
bus
{{ $bus.a }}
复制代码
请往后面看
插槽
我是标签里面的内容
具名插槽
新版写法只可以用 template
这里插值表达式的数据用的是父类的
header {{ msg }}
footer
作用域插槽
这样用的是子类的数据
{{ a }},{{ b }}
// test1
我是test1
复制代码
这个比较简单,就不再多多叙述。强调一点,新老版本区别
template
进心包裹div
等看完上面的内容可以尝试模仿写一下 element-ui
的表单组件。他们使用了async-validator
作为校验。
github地址
同样有一个简单版本Vue数据响应式和编译原理分析 和 模拟实战.这个版本没有用到虚拟Dom
等。
虚拟dom
。个人也总结了一篇帮你深入了解虚拟DOM和DOM-diff,希望能帮到各位
仅仅是一个简单的实现。但是实现了 部分指令
完整部分(即这次总结的,带上虚拟dom
等等),这个内容由于太多(标题细分太多。不好去寻找)。我另写了一篇文章,还在整理中,1号大概可以放出来。
贴一个图证明一下。实在是考虑的太多,所以写出来比较慢
推荐一下自己的另一篇文章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
方法,我们希望的是,任何组件都可以访问到这里面的数据。组件的渲染是由父到子的,所以我们既可以先进行判断。如果它是根节点,就把这个属性挂载到根节点上,如果不是,就找他父级的这个属性,然后挂载到这个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
}
}
})
}
复制代码
我们平时使用的过程是是这样的
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
VueX中的属性{{$store.state.age}}
复制代码
大家会看到 输出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 方法
复制代码
这样我们就实现了数据改变,就刷新视图
在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
是作什么用的。我 个人理解,需要对访问数据进行一定处理。也就是我们访问这个属性的时候,得到这个函数的返回结果。
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
这一段代码
这段代码相对比较简单,这样就实现了getters
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
/**
* 循环对象的值
* @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
,指的是模块
parentModel
是this.root
,rawModule
是a
模块的定义parentModel
是this.root
,rawModule
是b
模块的定义parentModel
是b
模块,rawModule
是c
模块的定义打印一下 this.modules
现在我们就把所有的模块进行了 格式化。接下来。我们就要对 我们格式化后的数据进行安装。使他们可以访问得到
总结一下,这个函数的作用就是把 我们传入的modules
进行一个格式化,并且将模块进行分类。
这个函数的作用 循环遍历子节点,安装 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)
})
}
复制代码
store
和 rootState
始终是。Vuex
中的store
和根上面的state
patch
是[]
,rawModule
是根
模块的定义patch
是['a']
,rawModule
是a
模块的定义patch
是['b']
,rawModule
是b
模块的定义
b
下面还有modules
,所以patch
是['b',‘c’]
,rawModule
是c
模块的定义命名空间这个就简单了。只需要在每个方法前面加上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)
})
}
复制代码
从''(空)
字符串开始。根节点不需要命名空间
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
,进行格式化之后。再进行安装就可以
注意,要安装位置要确定好哦
跟Vuex
一样,也写过一篇比较简单的实现 VueRouter的简单实现,感觉这一篇写的相对简单一点
这部分我个人觉得自己掌握的不是特别好。所以 讲述的不太清楚。仅提供一个思路。
install
方法在我们的平常使用过程中,除了router-link
和router-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
复制代码
以上代码只做了两件事。
index
文件首先我们应该分析。我们这个主文件应该有什么。在我们日常使用的过程中,一般是import VueRouter from 'vue-router'
所以
VueRoter
类。install
.。VueRoter
类的constructor
中,我们应该对用户传入的数据进行处理。还有就是分析它路由模式
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
复制代码
里面出现的方法在下面都会有所解释
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(地址)
。返回相对应的 记录
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) 和 {} 区别 前者没有原型链
{} 会存在一堆的属性
**Object.create(null)**不存在这些
addRouteRecord
是这个的核心方法。它的工作是
about/a
。没有就是本身record
route(即每一项路由)
是否在pathList
里面。在了就跳过。不在就添加进去。 **这个方法就实现了addRoutes
**的作用这个方法及其关键!!!!
原因:比如我们渲染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
}
}
复制代码
这个即解释this.history = new HashHistory(this)
为什么要单独列出来?因为有不同的 路由模式,但是有公共的处理方法。当然还需要有不同的方法来处理不同的路由。
我们这里只考虑hash
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
做了几件事
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
复制代码
这个就比较简单了。就不再解释了
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
做的事,
_route
的函数(这时候 _route
还不是动态的)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
设置成动态(有get
和set
)。
之后数据发生改变,视图就会刷新。
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.js
。createRoute
返回的结果中有一个match
。里面存放了所有的 父级路径
routerView
的解释。自定义属性。看他是否是根节点。 第一次进来的时候 ,渲染App
组件(里面放有RouterView
)。如果存在,证明要渲染的是下一个节点了
router
是方法
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
正常写来代替
....
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
复制代码
这个就比较像express
和koa
里面的了。
简单写个思路就是这样。
// 储存
let deps = []
// 放置
beforeXxx(cb){
this.deps.push(cb)
}
// 使用
// 在视图更新或者跳转前
this.deps.forEach(dep=>{
dep()
})
复制代码
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)
}
复制代码
思路:
beforeEach
队列for
。JS单线程,用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撸后台 系列二(登录权限篇)
是我目前看到的关于路由权限 讲解最好的一篇文章
自己的总结正在努力
阮一峰老师这一本书关于这部分已经写的很好了。我就不再多做叙述了。详情点击这里地址
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
复制代码
developer.mozilla.org/zh-CN/docs/…