聊聊Vuex原理

news2024/11/19 20:33:42

背景

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。如果你已经灵活运用,但是依然好奇它底层实现逻辑,不妨一探究竟。

Vue 组件开发

我们知道开发 Vue 插件,安装的时候需要执行 Vue.use(Vuex)

import Vue from 'vue'
import Vuex from '../vuex'

Vue.use(Vuex)

通过查看 Vue API Vue-use 开发文档,我们知道安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。该方法需要在调用 new Vue() 之前被调用。当 install 方法被同一个插件多次调用,插件将只会被安装一次。

为了更好了的去理解源码意思,这里写了一个简单的测试实例。

测试实例代码

import Vue from 'vue'
import Vuex from '../vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  plugins: [],
  state: {
    time: 1,
    userInfo: {
      avatar: '',
      account_name: '',
      name: ''
    },
  },
  getters: {
    getTime (state) {
      console.log('1212',state)
      return state.time
    }
  },
  mutations: {
    updateTime(state, payload){
      state.time = payload
    }
  },
  actions: {
    operateGrou({ commit }) {
      // commit('updateTime', 100)
      return Promise.resolve().then(()=>{
        return {
          rows: [1,2,3]
        }
      })
    }
  },
  modules: {
    report: {
      namespaced: true,
      state: {
        title: '',
      },
      getters: {
        getTitle (state) {
          return state.title
        }
      },
      mutations: {
        updateTitle(state, payload){
          state.title = payload
        }
      },
      actions: {
        operateGrou({ commit }) {
          commit('updateTitle', 100)
          return Promise.resolve().then(()=>{
            return {
              rows: [1,2,2,3]
            }
          })
        }
      },
      modules: {
        reportChild: {
          namespaced: true,
          state: {
            titleChild: '',
          },
          mutations: {
            updateTitle(state, payload){
              state.title = payload
            }
          },
          actions: {
            operateGrou({ commit }) {
              commit('updateTitle', 100)
              return Promise.resolve().then(()=>{
                return {
                  rows: [1,2,2,3]
                }
              })
            }
          },
        }
      }
    },
    part: {
      namespaced: true,
      state: {
        title: '',
      },
      mutations: {
        updateTitle(state, payload){
          state.title = payload
        },
        updateTitle1(state, payload){
          state.title = payload
        }
      },
      actions: {
        operateGrou({ commit }) {
          commit('updateTitle', 100)
          return Promise.resolve().then(()=>{
            return {
              rows: [1,2,2,3]
            }
          })
        }
      },
      modules: {
        partChild: {
          namespaced: true,
          state: {
            titleChild: '',
          },
          getters: {
            getTitleChild (state) {
              return state.titleChild
            }
          },
          mutations: {
            updateTitle(state, payload){
              state.titleChild = payload
            }
          },
          actions: {
            operateGrou({ commit }) {
              commit('updateTitle', 1000)
              return Promise.resolve().then(()=>{
                return {
                  rows: [1,2,2,3]
                }
              })
            }
          },
          modules: {
            partChildChild: {
              namespaced: true,
              state: {
                titleChild: '',
              },
              getters: {
                getTitleChild (state) {
                  return state.titleChild
                }
              },
              mutations: {
                updateTitle(state, payload){
                  state.titleChild = payload
                }
              },
              actions: {
                operateGrou({ commit }) {
                  commit('updateTitle', 1000)
                  return Promise.resolve().then(()=>{
                    return {
                      rows: [1,2,2,3]
                    }
                  })
                }
              },
            }
          }
        }
      }
    }
  }
})

参考vue实战视频讲解:进入学习

Graphviz 父子结点关系图

用 Graphviz 图来表示一下父子节点的关系,方便理解

组件开发第一步 install & mixin

在调用 Vuex 的时候会找其 install 方法,并把组件实例传递到 install 方法的参数中。

let Vue;
class Store {

}

const install = _Vue => {
    Vue = _Vue;
    Vue.mixin({
        beforeCreate(){
           console.log(this.$options.name);
        }
    })
};

export default {
    Store,
    install
}

到这里说一下 Vuex 实现的思想,在 Vuex 的 install 方法中,可以获取到 Vue 实例。
我们在每个 Vue 实例上添加 $store 属性,可以让每个属性访问到 Vuex 数据信息;
我们在每个 Vue 实例的 data 属性上添加上 state,这样 state 就是响应式的;
收集我们传入 new Vuex.Store(options) 即 options 中所有的 mutaions、actions、getters;
接着当我们 dispatch 的时候去匹配到 Store 类中存放的 actions 方法,然后去执行;
当我们 commit 的时候去匹配到 Store 类中存放的 mutations 方法,然后去执行;
这其实就是一个发布订阅模式,先存起来,后边用到再取再执行。好了解这些,我们开始真正的源码分析;

Vue 实例注入 $store

为了更好理解,我们打印出 Vue 实例,可以看到注入了 $store,见下图。

image.png

具体实现关键点

const install = (_Vue) => {
  Vue = _Vue
  Vue.mixin({
    beforeCreate(){
      // 我们可以看下面 main.js 默认只有我们的根实例上有 store,故 this.$options.store 有值是根结点
      if(this.$options.store) { 
        this.$store = this.$options.store // 根结点赋值
      } else {
        this.$store = this.$parent && this.$parent.$store // 每个实例都会有父亲。故一层层给实例赋值
      }
    }
  })
}
  • main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

$store.state 响应式

响应式核心就是挂载到实例 data 上,让 Vue 内部运用 Object.defineProperty 实现响应式。

class Store{
  constructor (options) { // 我们知道 options 是用户传入 new Vuex.Store(options) 参数
    this.vm = new Vue({
      data: {
        state: options.state
      }
    })
  }
}

$store.mutations & commit

来看下 用户传入的 mutations 变成了什么,数据采用 最上面的测试实例代码。
我们可以看到 mutations 是一个对象,里面放了函数名,值是数组,将相同函数名对应的函数存放到数组中。

image.png

mutations

  • 实际上就是收集用户传入的 mutations, 放到一个对象中。
 const setMoutations = (data, path = []) => {
    const mutations = data.mutations
    Object.keys(mutations).map(item => {
      this.mutations[item] = this.mutations[item] || [] // 之前的旧值
      this.mutations[item].push(mutations[item]) // 存起来
    })
    const otherModules = data.modules || {} // 有子 modules 则递归
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setMoutations(otherModules[item], path.concat(item))
      })
    }
  }
  setMoutations(options) // 这里 options 是用户传入的 new Vuex.Store(options) 的参数

commit

  • 实际上就是从收集 mutaitons 中找到用户传入的mutationName对应的数组方法,然后遍历执行。通知到位。
class Store{
    commit = (mutationName, payload) => {
    this.mutations[mutationName].map(fn => {
      fn(this.state, payload)
    })
  }
}

$store.actions & dispatch

actions

  • actions 与 mutations 实现是一样的
  const setAction = (data, path = []) => {
    const actions = data.actions
    Object.keys(actions).map(item => {
      this.actions[item] = this.actions[item] || []
      this.actions[item].push(actions[item])
    })
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setAction(otherModules[item], path.concat(item))
      })
    }
  }
  setAction(options)

dispatch

class Store{
  dispatch = (acitonName, payload) => {
    this.actions[acitonName].map(fn => {
      fn(this, payload) // this.$store.dispatch('operateGrou')
    })
  }
}

$store.getters

const setGetter = (data, path = []) => {
  const getter = data.getters || {}
  const namespace = data.namespaced
  Object.keys(getter).map(item => {     
    // 跟 Vue 计算属性底层实现类似,当从 store.getters.doneTodos 取值的时候,实际会执行 这个方法。
    Object.defineProperty(this.getter, item, { 
      get:() => {
        return options.state.getters[item](this.state)
      }
    })
  })

  const otherModules = data.modules || {}
  if (Object.keys(otherModules).length > 0){
    Object.keys(otherModules).map(item => {
      setGetter(otherModules[item], path.concat(item))
    })
  }
}
setGetter(options)

namespaced

上面讨论的是没有 namespaced 的情况,加上 namespaced 有什么区别呢,见下图。

image.png

瞬间拨云见日了,平常写上面基本上都要加上 namespaced,防止命名冲突,方法重复多次执行。
现在就算每个 modules 的方法命一样,也默认回加上这个方法别包围的所有父结点的 key。
下面对 mutations actions getters 扩展一下,让他们支持 namespaced。核心就是 path 变量

mutations

// 核心点在 path
const setMoutations = (data, path = []) => {
    const mutations = data.mutations
    const namespace = data.namespaced
    Object.keys(mutations).map(item => {
      let key = item
      if (namespace) {
        key = path.join('/').concat('/'+item) // 将所有父亲用 斜杠 相关联
      }
      this.mutations[key] = this.mutations[key] || []
      this.mutations[key].push(mutations[item])
    })
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setMoutations(otherModules[item], path.concat(item)) // path.concat 不会修改 path 原来的值
      })
    }
  }
  setMoutations(options)

actions

actions 与 mutations 是一样的

const setAction = (data, path = []) => {
    const actions = data.actions
    const namespace = data.namespaced
    Object.keys(actions).map(item => {
      let key = item
      if (namespace) {
        key = path.join('/').concat('/'+item)
      }
      this.actions[key] = this.actions[key] || []
      // this.actions[key].push(actions[item]) 
      this.actions[key].push((payload) => {
        actions[item](this, payload);
      })
    })
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setAction(otherModules[item], path.concat(item))
      })
    }
  }
  setAction(options)

getter

 const setGetter = (data, path = []) => {
    const getter = data.getters || {}
    const namespace = data.namespaced
    Object.keys(getter).map(item => {
      let key = item
      if (namespace) {
        key = path.join('/').concat('/'+item)
      }
      Object.defineProperty(this.getter, key, {
        get: () => {
          return getter[item](this.state)
        }
      })
    })
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setGetter(otherModules[item], path.concat(item))
      })
    }
  }
  setGetter(options)

我们可以总结来看,namespaces 加与不加的区别实际就下图;

image.png

image.png

具体数据转化方式有很多,核心就是对数据格式的处理,来进行发布与订阅;

actions & mutations

看到这,小伙伴怀疑了,actions 与 mutations,具体实现是一样的,那为什么要说 actions 可以异步执行,mutations,不能异步执行呢?下面我来贴一下核心代码。

class Store{
  constructor () {
    ..... 省略

      if(this.strict){// 严格模式下才给报错提示
      this.vm.$watch(()=>{
          return this.vm.state // 我们知道 commit 是会出发 state 值修改的
      },function () {
            // 此处监听 state 修改,因为在执行 commit 的时候 this._committing 是true 的,你若放了异步方法,this._committing 就会往下执行 变成 false
          console.assert(this._committing,'您异步调用了!')  // 断言  this._committing 为false, 给报错提示
      },{deep:true,sync:true});
    }
  }
  _withCommit(fn){
    const committing = this._committing; // 保留false
    this._committing = true; // 调用 mutation之前, this._committing 更改值是 true
    fn(); // 保证 执行的时候 this._committing 是 true
    this._committing = committing // 结束后重置为 false
  }
  commit = (mutationName, payload) => {
    console.log('1212',mutationName)
    this._withCommit(()=>{
      this.mutations[mutationName] && this.mutations[mutationName].map(fn => {
        fn(this.state, payload)
      })
    })
  }
}

Vuex中的辅助方法

我们经常在 Vuex 中这样使用

import {
  mapState,
  mapGetters
} from 'vuex'

computed: {
  isAfterSale () {
    return this.$route.meta.isAfterSale
  },
  ...mapGetters({
    messageState: 'message/getMessageState'
  }),
  ...mapGetters({
    messageNum: 'message/getMessageNum'
  }),
 ...mapGetters([    'doneTodosCount',    'anotherGetter',    // ...  ])
},

 methods: {
  ...mapActions([    'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`    // `mapActions` 也支持载荷:    'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`  ]),
  ...mapActions({
    add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
  })
}

mapState

export const mapState = (stateArr) => { // {age:fn}
    let obj = {};
    stateArr.forEach(stateName => {
        obj[stateName] = function () {
            return this.$store.state[stateName]
        }
    });
    return obj;
}

mapGetters

export function mapGetters(gettersArr) {
  let obj = {};
  gettersArr.forEach(getterName => {
      obj[getterName] = function () {
          return this.$store.getters[getterName];
      }
  });
  return obj
}

mapMutations

export function mapMutations(obj) {
  let res = {};
  Object.entries(obj).forEach(([key, value]) => {
      res[key] = function (...args) {
          this.$store.commit(value, ...args)
      }
  })
  return res;
}

mapActions

export function mapActions(obj) {
  let res = {};
  Object.entries(obj).forEach(([key, value]) => {
      res[key] = function (...args) {
          this.$store.dispatch(value, ...args)
      }
  })
  return res;
}

插件

Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数:

实际上 具体实现是发布订阅着模式,通过store.subscribe 将需要执行的函数保存到 store subs 中,
当 state 值发生改变时,this.subs(fn=>fn()) 执行。

const vuePersists = store => {
     let local = localStorage.getItem('VuexStore');
  if(local){
    store.replaceState(JSON.parse(local)); // 本地有则赋值
  }
  store.subscribe((mutation,state)=>{
    localStorage.setItem('VuexStore',JSON.stringify(state)); // state 发生变化执行
  });
}
const store = new Vuex.Store({
  // ...
  plugins: [vuePersists]
})
class Store{
  constructor () {
        this.subs = []
        const setMoutations = (data, path = []) => {
      const mutations = data.mutations
      const namespace = data.namespaced
      Object.keys(mutations).map(mutationName => {
        let namespace = mutationName
        if (namespace) {
          namespace = path.join('/').concat('/'+mutationName)
        }
        this.mutations[namespace] = this.mutations[namespace] || []
        this.mutations[namespace].push((payload)=>{ // 之前是直接 push

        mutations[item](options.state, payload)

        this.subs.forEach(fn => fn({ // state 发生改变 则发布通知给插件
            type: namespace,
            payload: payload
          }, options.state));

        })
      })
      const otherModules = data.modules || {}
      if (Object.keys(otherModules).length > 0){
        Object.keys(otherModules).map(item => {
          setMoutations(otherModules[item], path.concat(item)) // path.concat 不会修改 path 原来的值
        })
      }
    }
    setMoutations(options)

  }
  subscribe(fn) {
    this.subs.push(fn);
    }
}

State 处理

state 还没处理呢,别忘记我们 用户传入的 state 只是分module 传的,最终都要挂载到 state 中,见初始值和下图图片。实际上是数据格式转化,相信跟后端对接多的同学,考验处理数据格式的能力了。是的递归跑不了了。

  • 初始值
{
 state: {
    time: 1,
    userInfo: {
      avatar: '',
      account_name: '',
      name: ''
    },
  },
  modules: {
      report: {
      state: {
        title: '',
      },
    },
    part: {
        state: {
          title: '',
        },
            modules: {
        partChild: {
          state: {
            titleChild: '',
          },
        }
      }
    },
  }
}
  • 转化称下面这种格式

image.png

可以看到核心方法还是 path, path.slice 来获取每次递归的父结点。

 const setState = (data, path = []) => {
   if (path.length > 0) {
      let parentModule = path.slice(0, -1).reduce((next, prev)=>{
        return next[prev]
      }, options.state)
      Vue.set(parentModule, path[path.length - 1], data.state); // 为了 State 每个属性添加 get set 方法
    // parentModule[path[path.length - 1]] = data.state // 这样修改 Vue 是不会监听的
    }  
    const otherModules = data.modules || {}
    if (Object.keys(otherModules).length > 0){
      Object.keys(otherModules).map(item => {
        setState(otherModules[item], path.concat(item))
      })
    }
  }
  setState(options)

收集模块

原谅我,最重要的一块放到最后才说。上面所有方法都是基于用户传的 options (new Vuex.Store(options)) 来实现的。但是用户输入的我们怎能轻易详细,我们还是要对模块进行进一步格式处理,转化成我们需要的数据。转化成见下图。

image.png

我们可以分析出收集模块,实际也是递归,转化成固定格式数据 _children、state、rawModule。
核心代码是 register 方法,实际也是数据格式的转化。

总结

通篇看下来,还是需要自己手敲一下,在实践的过程中,才能发现问题(this 指向、父子结点判断、异步方法保存提示的巧妙)。当你明白具体实现,那每次使用就轻而易举了,每一步使用都知道干了什么的感觉真好。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/10999.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

docker系统笔记-03镜像的创建管理和发布

镜像的获取 pull from registry (online) 从registry拉取 public(公有)private(私有) build from Dockerfile (online) 从Dockerfile构建load from file (offline) 文件导入 (离线) 镜像的基本操作 dock…

概率 | 【提神醒脑】自用笔记串联二 —— 数字特征、大数定律、统计量

本文总结参考于 kira 2023概率提神醒脑技巧班。 笔记均为自用整理。加油!ヾ(◍∇◍)ノ゙ 第一部分笔记详见 概率 | 【提神醒脑】自用笔记串联一 —— 事件、随机变量及其分布_西皮呦的博客-CSDN博客 一研为定! 四、随机变量的数字特…

cocos2dx 3D物理相关知识点汇总

(一)3D相关基础知识 网格(Mesh) 通常说的网格其实就是3D建模出来的形状。因为模型是由很多三角形组成,所以,就像网格一样。 纹理 纹理的作用就是给网格上色。 怎么上色的? 举个简单的例子。…

m基于GA遗传优化的生产工艺设备布置优化matlab仿真

目录 1.算法概述 2.仿真效果预览 3.核心MATLAB程序 4.完整MATLAB程序 1.算法概述 在设备布置的问题上,本文将作业车间设备布置这个多目标优化问题看成是包含布局面积,物流成本和生产工艺的连续优化的多行设备布置问题,使之更具有实际意义…

如何在 Rocky Linux 上安装 Apache Kafka?

Apache Kafka 是一种分布式数据存储,用于实时处理流数据,它由 Apache Software Foundation 开发,使用 Java 和 Scala 编写,Apache Kafka 用于构建实时流式数据管道和适应数据流的应用程序,特别适用于企业级应用程序和关…

robots.txt漏洞

robots.txt漏洞描述: 搜索引擎可以通过robots文件可以获知哪些页面可以爬取,哪些页面不可以爬取。Robots协议是网站国际互联网界通行的道德规范,其目的是保护网站数据和敏感信息、确保用户个人信息和隐私不被侵犯,如果robots.txt文件编辑的太过详细,反而会泄露网站的敏感…

[附源码]java毕业设计基于学生信息管理系统

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

Delphi中关于PChar、Char数组、string[](ShortString)及结构体长度及占用空间的一些特性说明和测试

关于特性 1,string和Char数组都是一块内存, 其中存放连续的字符. string保存具体字符的内存对用户 是透明的, 由Delphi管理它的分配, 复制和释放, 用户不能干预2,关于ShortString,内存中用第一个字节来表示字符串的长度。FF255,所以这个特性…

【MySQL】MySQL复制与高可用水平扩展架构实战(MySQL专栏启动)

📫作者简介:小明java问道之路,专注于研究 Java/ Liunx内核/ C及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。 &#x1…

天王刘德华走红毯,到哪他都是最耀眼的明星

第三十五届金鸡奖,已经在福建厦门落下帷幕,如果要说本届金鸡奖谁收获最大,无疑是天王刘德华。在金鸡奖颁奖典礼现场,功夫巨星吴京登上热搜,然而热搜的主角却不是他,而是天王刘德华。 在本届金鸡奖颁奖典礼现…

cubeIDE开发, stm32调试信息串口通信输出显示

关于cubeIDE开发基本技巧及流程,本文不详细叙述,请参考:cubeIDE快速开发流程_py_free的博客-CSDN博客_cubeide汉化 一、stm32串口配置 本文采用的开发板是stm32L496VGT3,其有两个 USB 接口,一个为 USB ST-link 复用接口&#xff…

代码随想录——最长递增子序列的个数

题目 给定一个未排序的整数数组,找到最长递增子序列的个数。 示例 1: 输入: [1,3,5,4,7] 输出: 2 解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。 示例 2: 输入: [2,2,2,2,2] 输出: 5 解释: 最长递增子序列的长度是1,并且…

Oracle 表创建和表管理

1.表的命名 必须以字母开头字符长度在1-30之间只能包含A-Z,a-z,0-9,_,$和#被同一个用户拥有的对象不能有重复的名字 2.表的创建 SQL> create table t01(id number(4),name varchar2(15));Table created.SQL> desc t01Name …

现场直击!维视智造携多款明星产品亮相VisionChina 2022深圳机器视觉展

11月15日,2022年中国(深圳)机器视觉展在深圳国际会展中心(宝安新馆)盛大开幕,维视智造携MV-CR读码相机、3D线激光相机、VisionBank AI多相机智能视觉系统等多款行业领先产品及解决方案亮相。 1 ►现场速击 …

C基础--内存对齐问题(结构体对齐)

问题现象 在调试一个软件功能时,发现一个结构体对齐的问题,以前没有太关注,现在把它总结出来。先看示例: 结构体1: typedef struct {char magic[4];uint32_t crc32;uint32_t lenght;uint16_t ver;uint16_t IFrameCnt…

多线程DPDK应用的内存优化

作者 Conor Walsh is a software engineering intern with the Architecture Team of Intel’s Network Platform Group (NPG), based in Intel Shannon (Ireland). 引言 高速包处理是一种资源密集型应用。一种解决方案是将包处理流水线(pipeline)分离到多线程以提高程序性能…

大一新生HTML期末作业,网页制作作业——海鲜餐饮网站登录页面(单页面)HTML+CSS+JavaScript

👨‍🎓静态网站的编写主要是用HTML DIVCSS JS等来完成页面的排版设计👩‍🎓,常用的网页设计软件有Dreamweaver、EditPlus、HBuilderX、VScode 、Webstorm、Animate等等,用的最多的还是DW,当然不同软件写出的…

STM32G0开发笔记-Platformio+libopencm3-FreeRTOS和FreeModbus库使用

title: STM32G0开发笔记-Platformiolibopencm3-FreeRTOS和FreeModbus库使用 tags: STM32MCUSTM32G070libopencm3MonkeyPiFreeRTOSModbus categories: STM32 date: 2022-9-11 19:52:05 [原文:makerinchina.cn] 使用Platformio平台的libopencm3开发框架来开发STM32…

docker -- 入门篇 (数据卷、自定义镜像、安装mysql redis)

1 数据卷 采用上一章节创建的centos镜像启动容器 doc01 docker run -it --name doc01 lhy/centos:1.00 2 数据卷容器 启动子容器doc02 实现继承doc01的关系 docker run -it --name doc02 --volumes-from doc01 lhy/centos:1.00 启动子容器doc03 实现继承doc01的关系 docker…

【计算机毕业设计】病人跟踪治疗信息管理系统源码

一、系统截图(需要演示视频可以私聊) 摘 要 病人跟踪治疗信息管理系统采用B/S模式,促进了病人跟踪治疗信息管理系统的安全、快捷、高效的发展。传统的管理模式还处于手工处理阶段,管理效率极低,随着病人的不断增多&a…