vuex工作原理详解

article/2025/4/22 9:11:27

前言

vuex作为vue官方出品的状态管理框架,以及其简单API设计、便捷的开发工具支持,在中大型的vue项目中得到很好的应用。作为flux架构的后起之秀,吸收了前辈redux的各种优点,完美的结合了vue响应式数据,个人认为开发体验已经超过了React + Redux这对基友。

在项目启动vue开发后的这几个月中,越发对vuex的原理感到好奇,今天将这几日的所学总结成文,希望能帮到对vuex好奇的童鞋们。

理解computed

使用vuex中store中的数据,基本上离不开vue中一个常用的属性computed。官方一个最简单的例子如下

var vm = new Vue({el: '#example',data: {message: 'Hello'},computed: {// 计算属性的 getterreversedMessage: function () {// `this` 指向 vm 实例return this.message.split('').reverse().join()}}
})

不知大家有没有思考过,vue的computed是如何更新的,为什么当vm.message发生变化时,vm.reversedMessage也会自动发生变化?

我们来看看vue中data属性和computed相关的源代码。

// src/core/instance/state.js
// 初始化组件的state
export function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (opts.props) initProps(vm, opts.props)if (opts.methods) initMethods(vm, opts.methods)// 当组件存在data属性if (opts.data) {initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}// 当组件存在 computed属性if (opts.computed) initComputed(vm, opts.computed)if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}
}

initState方法当组件实例化时会自动触发,该方法主要完成了初始化data,methods,props,computed,watch这些我们常用的属性,我们来看看我们需要关注的initDatainitComputed(为了节省时间,去除了不太相关的代码)

先看看initData这条线

// src/core/instance/state.js
function initData (vm: Component) {let data = vm.$options.datadata = vm._data = typeof data === 'function'? getData(data, vm): data || {}// .....省略无关代码// 将vue的data传入observe方法observe(data, true /* asRootData */)
}// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {if (!isObject(value)) {return}let ob: Observer | void// ...省略无关代码ob = new Observer(value)if (asRootData && ob) {ob.vmCount++}return ob
}

在初始化的时候observe方法本质上是实例化了一个Observer对象,这个对象的类是这样的

// src/core/observer/index.js
export class Observer {value: any;dep: Dep;vmCount: number; // number of vms that has this object as root $dataconstructor (value: any) {this.value = value// 关键代码 new Dep对象this.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)// ...省略无关代码this.walk(value)}walk (obj: Object) {const keys = Object.keys(obj)for (let i = 0; i < keys.length; i++) {// 给data的所有属性调用defineReactivedefineReactive(obj, keys[i], obj[keys[i]])}}
}

在对象的构造函数中,最后调用了walk方法,该方法即遍历data中的所有属性,并调用defineReactive方法,defineReactive方法是vue实现 MDV(Model-Driven-View)的基础,本质上就是代理了数据的set,get方法,当数据修改或获取的时候,能够感知(当然vue还要考虑数组,Object中嵌套Object等各种情况,本文不在分析)。我们具体看看defineReactive的源代码

// src/core/observer/index.js
export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean
) {// 重点,在给具体属性调用该方法时,都会为该属性生成唯一的dep对象const dep = new Dep()// 获取该属性的描述对象// 该方法会返回对象中某个属性的具体描述// api地址https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptorconst property = Object.getOwnPropertyDescriptor(obj, key)// 如果该描述不能被更改,直接返回,因为不能更改,那么就无法代理set和get方法,无法做到响应式if (property && property.configurable === false) {return}// cater for pre-defined getter/settersconst getter = property && property.getconst setter = property && property.setlet childOb = !shallow && observe(val)// 重新定义data当中的属性,对get和set进行代理。Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {const value = getter ? getter.call(obj) : val// 收集依赖, reversedMessage为什么会跟着message变化的原因if (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend()}if (Array.isArray(value)) {dependArray(value)}}return value},set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val/* eslint-disable no-self-compare */if (newVal === value || (newVal !== newVal && value !== value)) {return}if (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = !shallow && observe(newVal)// 通知依赖进行更新dep.notify()}})
}

我们可以看到,在所代理的属性get方法中,当dep.Target存在的时候会调用dep.depend()方法,这个方法非常的简单,不过在说这个方法之前,我们要认识一个新的类Dep

Dep 是 vue 实现的一个处理依赖关系的对象,
主要起到一个纽带的作用,就是连接 reactive data 与 watcher,代码非常的简单

// src/core/observer/dep.js
export default class Dep {static target: ?Watcher;id: number;subs: Array<Watcher>;constructor () {this.id = uid++this.subs = []}addSub (sub: Watcher) {this.subs.push(sub)}removeSub (sub: Watcher) {remove(this.subs, sub)}depend () {if (Dep.target) {Dep.target.addDep(this)}}notify () {const subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {// 更新 watcher 的值,与 watcher.evaluate() 类似,// 但 update 是给依赖变化时使用的,包含对 watch 的处理subs[i].update()}}
}// 当首次计算 computed 属性的值时,Dep 将会在计算期间对依赖进行收集
Dep.target = null
const targetStack = []export function pushTarget (_target: Watcher) {// 在一次依赖收集期间,如果有其他依赖收集任务开始(比如:当前 computed 计算属性嵌套其他 computed 计算属性),// 那么将会把当前 target 暂存到 targetStack,先进行其他 target 的依赖收集,if (Dep.target) targetStack.push(Dep.target)Dep.target = _target
}export function popTarget () {// 当嵌套的依赖收集任务完成后,将 target 恢复为上一层的 Watcher,并继续做依赖收集Dep.target = targetStack.pop()
}

代码非常的简单,回到调用dep.depend()方法的时候,当Dep.Target存在,就会调用,而depend方法则是将该dep加入watchernewDeps中,同时,将所访问当前属性dep对象中的subs插入当前Dep.target的watcher.看起来有点绕,不过没关系,我们一会跟着例子讲解一下就清楚了。

讲完了代理的get,方法,我们讲一下代理的set方法,set方法的最后调用了dep.notify(),当设置data中具体属性值的时候,就会调用该属性下面的dep.notify()方法,通过class Dep了解到,notify方法即将加入该dep的watcher全部更新,也就是说,当你修改data中某个属性值时,会同时调用dep.notify()来更新依赖该值的所有watcher

介绍完了initData这条线,我们继续来介绍initComputed这条线,这条线主要解决了什么时候去设置Dep.target的问题(如果没有设置该值,就不会调用dep.depend(), 即无法获取依赖)。

// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {// 初始化watchers列表const watchers = vm._computedWatchers = Object.create(null)const isSSR = isServerRendering()for (const key in computed) {const userDef = computed[key]const getter = typeof userDef === 'function' ? userDef : userDef.getif (!isSSR) {// 关注点1,给所有属性生成自己的watcher, 可以在this._computedWatchers下看到watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions)}if (!(key in vm)) {// 关注点2defineComputed(vm, key, userDef)}}
}

在初始化computed时,有2个地方需要去关注

  1. 对每一个属性都生成了一个属于自己的Watcher实例,并将 { lazy: true }作为options传入
  2. 对每一个属性调用了defineComputed方法(本质和data一样,代理了自己的set和get方法,我们重点关注代理的get方法)

我们看看Watcher的构造函数

// src/core/observer/watcher.js
constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: Object) {this.vm = vmvm._watchers.push(this)if (options) {this.deep = !!options.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.sync} else {this.deep = this.user = this.lazy = this.sync = false}this.cb = cbthis.id = ++uid // uid for batchingthis.active = truethis.dirty = this.lazy // 如果初始化lazy=true时(暗示是computed属性),那么dirty也是true,需要等待更新this.deps = []this.newDeps = []this.depIds = new Set()this.newDepIds = new Set()this.getter = expOrFn // 在computed实例化时,将具体的属性值放入this.getter中// 省略不相关的代码this.value = this.lazy? undefined: this.get()}

除了日常的初始化外,还有2行重要的代码

this.dirty = this.lazy
this.getter = expOrFn

computed生成的watcher,会将watcher的lazy设置为true,以减少计算量。因此,实例化时,this.dirty也是true,标明数据需要更新操作。我们先记住现在computed中初始化对各个属性生成的watcher的dirty和lazy都设置为了true。同时,将computed传入的属性值(一般为funtion),放入watchergetter中保存起来。

我们在来看看第二个关注点defineComputed所代理属性的get方法是什么

// src/core/instance/state.js
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]// 如果找到了该属性的watcherif (watcher) {// 和上文对应,初始化时,该dirty为true,也就是说,当第一次访问computed中的属性的时候,会调用 watcher.evaluate()方法;if (watcher.dirty) {watcher.evaluate()}if (Dep.target) {watcher.depend()}return watcher.value}}
}

第一次访问computed中的值时,会因为初始化watcher.dirty = watcher.lazy的原因,从而调用evalute()方法,evalute()方法很简单,就是调用了watcher实例中的get方法以及设置dirty = false,我们将这两个方法放在一起

// src/core/instance/state.js
evaluate () {this.value = this.get()this.dirty = false
}get () {  
// 重点1,将当前watcher放入Dep.target对象pushTarget(this)let valueconst vm = this.vmtry {// 重点2,当调用用户传入的方法时,会触发什么?value = this.getter.call(vm, vm)} catch (e) {} finally {popTarget()// 去除不相关代码}return value
}

在get方法中中,第一行就调用了pushTarget方法,其作用就是将Dep.target设置为所传入的watcher,即所访问的computed中属性的watcher,
然后调用了value = this.getter.call(vm, vm)方法,想一想,调用这个方法会发生什么?

this.getter 在Watcher构建函数中提到,本质就是用户传入的方法,也就是说,this.getter.call(vm, vm)就会调用用户自己声明的方法,那么如果方法里面用到了 this.data中的值或者其他被用defineReactive包装过的对象,那么,访问this.data.或者其他被defineReactive包装过的属性,是不是就会访问被代理的该属性的get方法。我们在回头看看
get方法是什么样子的。

注意:我讲了其他被用defineReactive,这个和后面的vuex有关系,我们后面在提

get: function reactiveGetter () {const value = getter ? getter.call(obj) : val// 这个时候,有值了if (Dep.target) {// computed的watcher依赖了this.data的depdep.depend()if (childOb) {childOb.dep.depend()}if (Array.isArray(value)) {dependArray(value)}}return value}

代码注释已经写明了,就不在解释了,这个时候我们走完了一个依赖收集流程,知道了computed是如何知道依赖了谁。最后根据this.data所代理的set方法中调用的notify,就可以改变this.data的值,去更新所有依赖this.data值的computed属性value了。

那么,我们根据下面的代码,来简易拆解获取依赖并更新的过程

var vm = new Vue({el: '#example',data: {message: 'Hello'},computed: {// 计算属性的 getterreversedMessage: function () {// `this` 指向 vm 实例return this.message.split('').reverse().join()}}
})
vm.reversedMessage // =>  olleH
vm.message = 'World' // 
vm.reversedMessage // =>  dlroW
  1. 初始化 data和computed,分别代理其set以及get方法, 对data中的所有属性生成唯一的dep实例。
  2. 对computed中的reversedMessage生成唯一watcher,并保存在vm._computedWatchers中
  3. 访问 reversedMessage,设置Dep.target指向reversedMessage的watcher,调用该属性具体方法reversedMessage
  4. 方法中访问this.message,即会调用this.message代理的get方法,将this.message的dep加入reversedMessage的watcher,同时该dep中的subs添加这个watcher
  5. 设置vm.message = 'World',调用message代理的set方法触发dep的notify方法'
  6. 因为是computed属性,只是将watcher中的dirty设置为true
  7. 最后一步vm.reversedMessage,访问其get方法时,得知reversedMessagewatcher.dirty为true,调用watcher.evaluate()方法获取新的值。

这样,也可以解释了为什么有些时候当computed没有被访问(或者没有被模板依赖),当修改了this.data值后,通过vue-tools发现其computed中的值没有变化的原因,因为没有触发到其get方法。

vuex插件

有了上文作为铺垫,我们就可以很轻松的来解释vuex的原理了。

我们知道,vuex仅仅是作为vue的一个插件而存在,不像Redux,MobX等库可以应用于所有框架,vuex只能使用在vue上,很大的程度是因为其高度依赖于vue的computed依赖检测系统以及其插件系统,

通过官方文档我们知道,每一个vue插件都需要有一个公开的install方法,vuex也不例外。其代码比较简单,调用了一下applyMixin方法,该方法主要作用就是在所有组件的beforeCreate生命周期注入了设置this.$store这样一个对象,因为比较简单,这里不再详细介绍代码了,大家自己读一读编能很容易理解。

// src/store.js
export function install (_Vue) {if (Vue && _Vue === Vue) {return}Vue = _VueapplyMixin(Vue)
}
// src/mixins.js
// 对应applyMixin方法
export default function (Vue) {const version = Number(Vue.version.split('.')[0])if (version >= 2) {Vue.mixin({ beforeCreate: vuexInit })} else {const _init = Vue.prototype._initVue.prototype._init = function (options = {}) {options.init = options.init? [vuexInit].concat(options.init): vuexInit_init.call(this, options)}}/*** Vuex init hook, injected into each instances init hooks list.*/function vuexInit () {const options = this.$options// store injectionif (options.store) {this.$store = typeof options.store === 'function'? options.store(): options.store} else if (options.parent && options.parent.$store) {this.$store = options.parent.$store}}
}

我们在业务中使用vuex需要类似以下的写法

const store = new Vuex.Store({state,mutations,actions,modules
});

那么 Vuex.Store到底是什么样的东西呢?我们先看看他的构造函数

// src/store.js
constructor (options = {}) {const {plugins = [],strict = false} = options// store internal statethis._committing = falsethis._actions = Object.create(null)this._actionSubscribers = []this._mutations = Object.create(null)this._wrappedGetters = Object.create(null)this._modules = new ModuleCollection(options)this._modulesNamespaceMap = Object.create(null)this._subscribers = []this._watcherVM = new Vue()const store = thisconst { dispatch, commit } = thisthis.dispatch = function boundDispatch (type, payload) {return dispatch.call(store, type, payload)
}this.commit = function boundCommit (type, payload, options) {return commit.call(store, type, payload, options)
}// strict modethis.strict = strictconst state = this._modules.root.state// init root module.// this also recursively registers all sub-modules// and collects all module getters inside this._wrappedGettersinstallModule(this, state, [], this._modules.root)// 重点方法 ,重置VMresetStoreVM(this, state)// apply pluginsplugins.forEach(plugin => plugin(this))}

除了一堆初始化外,我们注意到了这样一行代码
resetStoreVM(this, state) 他就是整个vuex的关键

// src/store.js
function resetStoreVM (store, state, hot) {// 省略无关代码Vue.config.silent = truestore._vm = new Vue({data: {$$state: state},computed})
}

去除了一些无关代码后我们发现,其本质就是将我们传入的state作为一个隐藏的vue组件的data,也就是说,我们的commit操作,本质上其实是修改这个组件的data值,结合上文的computed,修改被defineReactive代理的对象值后,会将其收集到的依赖的watcher中的dirty设置为true,等到下一次访问该watcher中的值后重新获取最新值。

这样就能解释了为什么vuex中的state的对象属性必须提前定义好,如果该state中途增加一个属性,因为该属性没有被defineReactive,所以其依赖系统没有检测到,自然不能更新。

由上所说,我们可以得知store._vm.$data.$$state === store.state, 我们可以在任何含有vuex框架的工程验证这一点。

 

总结

vuex整体思想诞生于flux,可其的实现方式完完全全的使用了vue自身的响应式设计,依赖监听、依赖收集都属于vue对对象Property set get方法的代理劫持。最后一句话结束vuex工作原理,vuex中的store本质就是没有template的隐藏着的vue组件;

(如果本文对帮助到了大家,欢迎给个赞,谢谢)~

参考文章

深入理解 Vue Computed 计算属性

 



作者:Kaku_fe
链接:https://www.jianshu.com/p/d95a7b8afa06
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。


http://chatgpt.dhexx.cn/article/N5fxBiLf.shtml

相关文章

Vuex配置及Vuex原理图分析,简单明了,一遍就明白

配置Vuex npm i vuex3 或npm i vue4 因为是Vue2 使用的是Vuex3 版本 Vue3使用的是Vuex4版本 根据使用的脚手架而定 然后导入Vuex 代码如下 专门设置一个文件夹用来 放vuex 设置 三个属性 // 该文件是Vuex 核心store// 引入Vuex import Vue from vue import Vuex from vuex …

vuex工作原理与流程

vuex工作原理与流程 Vue组件&#xff08;action里面的dispatch )--> actions(commit方法&#xff09; -->mutations&#xff08;Mutate&#xff09;--> state&#xff08;getter&#xff09; -->store更新所有调用vuex的组件&#xff08;Vue Component组件&#x…

Vue知识点整理(五)- vuex(1)- Vuex简介、Vuex工作原理、搭载Vuex环境、求和案例

目录 一、vuex简介 1.1 vuex是什么 1.2 什么时候使用Vuex 二、Vuex工作原理图 2.1 官方vuex工作原理图 2.2 Vuex工作流程 三、搭建Vuex环境 3.1 安装 3.2 引用Vuex 四、案例练习 - 求和案例 4.1 Count.vue 4.2 App.vue 4.3 index.js 一、vuex简介 1.1 vuex是什么…

Vuex框架原理与源码分析

本文授权转自微信公众号“美团点评技术团队”。 作者简介&#xff1a;明裔&#xff0c;美团外卖高级前端研发工程师&#xff0c;2014年加入美团外卖&#xff0c;负责Web主站开发。先后参与了外卖B端、C端、配送等全业务线系统开发后&#xff0c;目前主要负责商家券活动系统。 责…

vuex原理和下载

vuex&#xff1a;状态管理模式 vue全家桶&#xff1a;vue-cli&#xff08;脚手架&#xff09;、vue-router&#xff08;路由管理器&#xff09;、vuex&#xff08;状态管理模式&#xff09; 原理图示&#xff1a; 原理描述&#xff1a; vuex在vue组件外面进行组件状态的管理…

手写Vuex核心原理,再也不怕面试官问我Vuex原理

手写Vuex核心原理 文章目录 手写Vuex核心原理一、核心原理二、基本准备工作三、剖析Vuex本质四、分析Vue.use五、完善install方法六、实现Vuex的state七、实现getter八、实现mutation九、实现actions 一、核心原理 Vuex本质是一个对象Vuex对象有两个属性&#xff0c;一个是ins…

vuex实现原理

文章目录 vuex是什么&#xff1f;为什么会出现&#xff1f;怎么使用&#xff1f;怎么实现&#xff1f;1、给每个实例注入$store2、设置state响应数据3、getters4、mutations5、actions6、modules7、持久化插件plugins8、辅助函数mapState用法命名空间用法实现原理命名空间原理 …

Vuex的实现原理解析(最清晰)

Vuex的实现原理解析(最清晰) Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,可以帮助我们管理共享状态。 如何在Vue中使用Vuex? 如下先来回顾一下使用Vuex的正确姿势&#xff1a; 引入Vuex插件&#xff1b; // store.js Vue.use(Vuex);将Vuex.Store这个类实例化&…

通俗理解vuex原理---通过vue例子类比

主要通过简单的理解来解释下vuex的基本流程&#xff0c;而这也是vuex难点之一。 首先我们先了解下vuex的作用 vuex其实是集中的数据管理仓库&#xff0c;相当于数据库mongoDB&#xff0c;MySQL等&#xff0c;任何组件都可以存取仓库中的数据。 vuex流程与vue类比 我们看一下…

遇见面试--vuex原理

遇见面试 Vuex原理解析 一、前言 自从学习了VUE框架&#xff0c;其中必不可少的会用到vuex这个核心插件&#xff0c;而且在做项目的时候&#xff0c;基本都会使用&#xff0c;可能你会使用vuex状态管理&#xff0c;但是对vuex原理存在着或多或少的的疑惑或不解&#xff0c;这…

vuex的工作原理

Vuex是做什么的? 官方解释&#xff1a;Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。 它采用 集中式存储管理 应用的所有组件的状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension&#xff0…

vuex 的实现原理

一、Vuex是什么   Vuex是专门为Vuejs应用程序设计的状态管理工具。它采用集中式存储管理应用的所有组件的状态&#xff0c;并以相应的规则保证状态以一种可预测的方式发生变化。 1、Vuex的构成 由上图&#xff0c;我们可以看出Vuex有以下几个部分构成&#xff1a; 1&#x…

vuex原理

目录 一、 为啥会出现vuex 二 、什么是vuex 三、 怎么使用vuex 四、vue和vuex比较 五、vuex的五个属性 5.1 state 5.2 mutations 5.3 getters 5.4 Actions 5.5 Modules 六、为啥state里的数据可以实现响应式 七、为啥每个组件都可以拿到state里的数据 一、 为啥会出…

vuex的原理

1.vuex的作用 vuex是专门为Vuejs应用程序设计的状态管理工具。其实是集中的数据管理仓库。相当于数据库mongoDB等&#xff0c;任何组件都可以存取仓库中的数据。 vuex的组成部分&#xff1a; state:是存储的基本数据。mutations:提交更改数据。getter:对state加工&#xff0c…

vuex 的基本用法

一、vuex是什么&#xff1f; Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态&#xff0c; 这个状态管理应用包含以下几个部分&#xff1a; state&#xff0c;驱动应用的数据源&#xff1b;view&#xff0c;以声明方式将 stat…

DNS风险分析及安全防护研究(三):DNS缓存投毒及防御策略

在前面章节中&#xff0c;我们简单介绍了DNS系统在协议、软件以及结构中脆弱性&#xff0c;并对DNSSEC协议、去中心化结构等安全增强进行了讨论&#xff0c;接下来针对DNS安全所面临的外部攻击威胁和相应的防御策略做下讨论。 1.DNS缓存投毒攻击 在目前各种DNS攻击手段中&…

细说SQL注入:攻击手法与防御策略

一、 SQL注入的基本概念 &#xff08;一&#xff09;什么是SQL注入 SQL注入是一种常见的网络攻击技术&#xff0c;它允许攻击者在Web应用程序中插入并执行恶意的SQL代码。这种攻击通常针对的是数据驱动的应用程序&#xff0c;尤其是那些未对用户输入进行充分过滤和处理的应用程…

服务器被cc攻击的简单防御策略(附代码)

CC攻击&#xff08;也称为网络层攻击或流量攻击&#xff09;是指企图通过向网站或服务器发送大量伪造的请求&#xff0c;以干扰正常的用户访问的攻击。这些请求可能是来自单个设备的&#xff0c;也可能是来自一群被控制的设备的。为了防御CC攻击&#xff0c;你可以考虑使用以下…

DOS防御策略

该方法能解决一些小型服务的DOS防御问题&#xff0c;成本不高&#xff0c;能在一定程度上起到DOS防御的作用。 原本服务架构 我们的服务器向多个客户机提供服务&#xff0c;这里以腾讯云服务器为例&#xff0c;假设服务商租了一台腾讯云的服务器&#xff0c;一旦服务器收到攻击…