快捷搜索:  汽车  科技

史上最强vue总结(万字总结Vue包含全家桶)

史上最强vue总结(万字总结Vue包含全家桶)// VuexUse.vue <template> <div> VueX中的属性{{$store.state.age}} <button @click="change2">=100</button> </div> </template> <script> export default { name: 'VuexUse' mounted() { console.log(this.$store) } methods: { change2() { this.$store.state.age = 100 console.log(this.$store)

史上最强vue总结(万字总结Vue包含全家桶)(1)

作者:叫我阿琛

转发链接:https://mp.weixin.qq.com/s/ph3aUt-H4QtBgw9z-VFlHA

目录

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

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

基本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 <template> <div> VueX中的属性{{$store.state.age}} <button @click="change2">=100</button> </div> </template> <script> export default { name: 'VuexUse' mounted() { console.log(this.$store) } methods: { change2() { this.$store.state.age = 100 console.log(this.$store) } } } </script>

大家会看到 输出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. 第一次 parentModel是this.root,rawModule是a模块的定义
  2. 第二次 parentModel是this.root,rawModule是b模块的定义
  3. 第三次 parentModel是b模块,rawModule是c模块的定义

打印一下 this.modules

史上最强vue总结(万字总结Vue包含全家桶)(2)

UTOOLS1592985514369.png

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

「总结一下」,这个函数的作用就是把 我们传入的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) }) }

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

  1. 第一次 patch是[],rawModule是根模块的定义
  2. 第二次 patch是['a'],rawModule是a模块的定义
  3. 第三次 patch是['b'],rawModule是b模块的定义
    1. 走进来发现 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) }) }

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

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

以上代码只做了两件事。

  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总结(万字总结Vue包含全家桶)(3)

UTOOLS1593155710472.png

史上最强vue总结(万字总结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设置成动态(有get和set)。

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

组件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.js 。createRoute 返回的结果中有一个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 <tag onclick = { this.handle } > { this.$slots.default } < /tag> } }

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

钩子函数(路由守卫)

这个就比较像express和koa里面的了。

简单写个思路就是这样。这一点我还没加上。思路比较简单,我还没有去具体实现

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

正在努力

Vue3proxy

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

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

本篇已完结

作者:叫我阿琛

转发链接:https://mp.weixin.qq.com/s/ph3aUt-H4QtBgw9z-VFlHA

猜您喜欢: