Vuex原理詳解


  一、Vuex是什么

    Vuex是專門為Vuejs應用程序設計的狀態管理工具。它采用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生改變。它集中於MVC模式中的Model層,規定所有數據操作必須通過 action - mutation - statechange 的流程來進行,再結合Vue的數據視圖雙向綁定特效來實現頁面的展示更新。

    1.Vuex框架運行流程

     

  如圖我們可以看到Vuex為Vue Components建立起了一個完整的生態圈,包括開發中的api調用,vuex的簡單分析:

   Vue組件接收交互行為,調用dispatch方法觸發action相關處理,若頁面狀態需要改變,則調用commit方法提交mutation修改state,通過getters獲取到state新值,提供了mapState、MapGetters、MapActions、mapMutations等輔助函數給開發在vm中處理store,重新渲染Vue Components,頁面隨之更新。

二、目錄結構介紹

  

   Vuex提供了非常強大的狀態管理功能,源碼代碼量卻不多,目錄結構的划分也很清晰。先大體介紹下各個目錄文件的功能:

    module:提供module對象與module對象樹的創建功能;

    plugins:提供開發輔助插件,如“時間穿梭”功能,state修改的日志記錄功能等;

    helpers.js:提供action、mutation以及getters的查找API;

    index.js:是源碼主入口文件,提供store的各module構建安裝;

    mixin.js:提供了store在Vue實例上的裝載注入;

    util.js:提供了工具方法如find、deepCopy、forEachValue以及assert等方法。

 三、初始化裝載與注入

了解大概的目錄及對應功能后,下面開始進行源碼分析。

1.裝載實例

 先看一個簡單的🌰:

   

    store.js文件中,加載Vue框架,創建並導出一個空配置的store對象實例。

   

    然后在index.js中,正常初始化一個頁面根級別的Vue組件,傳入這個自定義的store對象。

   那么問題來了,使用Vuex只需要Vue.use(Vuex),並在Vue的配置中傳入一個store對象的實例,store是如何實現注入的?

2.裝載分析

 index.js文件代碼執行開頭,定義局部變量,用於判斷是否已經裝載和減少全局作用域查找。

let Vue

 然后判斷若處於瀏覽器環境下且加載過Vue,則執行install方法。

//auto install in dist mode
if(typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}

  install方法將Vuex裝載到Vue對象上, Vue.use(Vuex) 也是通過它執行,先看下Vue.use方法實現:

function (plugin:Function | Object) {
  /* istanbul ignore if */
  if(plugin.installed) {
    return
  }
  // additional parameters
  const args = toArray(arguments,1)
  args.unshift(this)
  if(typeof plugin.install === 'function') {
    //實際執行插件的install方法
    plugin.install.apply(plugin , args)
  }else {
    plugin.apply(null , args)
  }
  plugin.installed = true
  return this
}

 如果是首次加載,將局部Vue變量賦值為全局的Vue對象,並執行applyMixin方法,install實現如下:

function install (_Vue) {
  if(Vue) {
    console.error (
      '[vuex] already installed. Vue.use(Vuex) should be called only once.'
    )
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

 來看下applyMinxin方法內部代碼。如果是2.x以上版本,可以使用hook的形式進行注入,或使用封裝並替換Vue對象原型的_init方法,實現注入。

export default function (Vue) {
  //獲取vue版本號

  const version = Number(Vue.version.split('.')[0])
  //判斷版本是否大於2
  if(version >= 2) {
    const useInit = Vue.config._lifecycleHooks.indexOf('init') > -1
    Vue.mixin(useInit ? { init: vuexInit } : { beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure
    // for 1.x backwards compatibility.
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
      ? [vuexInit].concat(options.init)
      : vuexInit
      _init.call(this, options)
    }
  }
}
function vuexInit () {
  //通過 this.$options 對象獲取任何數據和方法
  const options = this.$options
  //store injection
  if(options.store) {
    this.$store = options.store
  } else if (options.parent && options.parent.$store) {
    this.$store = options.parent.$store
  }
}

 這段代碼的作用就是在 Vue 的生命周期中的初始化(1.0 版本是 init,2.0 版本是 beforeCreated)鈎子前插入一段 Vuex 初始化代碼。這里做的事情很簡單——將初始化Vue根組件時傳入的store設置到this對象的$store屬性上,子組件從其父組件引用$store屬性,層層嵌套進行設置,在任意組件中執行this.$store都能找到裝載的那個store對象。

幾張圖理解下store的傳遞。

  頁面Vue結構:

      

 

   對應store流向:

      

 

 四、store對象構造

  上面對Vuex框架的裝載以及注入自定義store對象進行了分析,接下來詳細分析store對象的內部功能和具體實現,那么問題又來了,為什么actions、getters、mutations中能從arguments[0]中拿到store的相關數據?

  store對象實現邏輯比較復雜,先看下構造方法的整體邏輯流程來幫助后面的理解:

  

4.1 環境判斷

  開始分析store的構造函數,分小節逐函數逐行的分析其功能。

constructor (options = {}) {
  assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
  assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
}

 在store構造函數中執行環境判斷,以下都是Vuex工作的必要條件:

  1.已經執行安裝函數進行裝載;

  2.支持Promise語法

 

 assert函數是一個簡單的斷言函數的實現,一行代碼即可實現

function assert (condition, msg) {
  if(!condition) throw new Error(`[vuex] ${msg}`)
}

 

4.2 數據初始化、module樹構造

 環境判斷后,根據new構造傳入的options或默認值,初始化內部數據。

const {
  state = {},
  plugins = [],
  strict = false
} = options

//store狀態初始化
this._committing = false  // 是否在進行提交狀態標識
this._actions = Object.create(null)  // actions操作對象
this._mutations = Object.create(null)  // mutations操作對象
this._wrappedGetters = Object.create(null)  // 封裝后的getters集合對象
this._modules = new ModuleCollection(options)  // vuex支持store分模塊傳入,存儲分析后的modules
this._modulesNamespaceMap = Object.create(null)  // 模塊命名空間
this._subscribers = []  // 訂閱函數集合,Vuex提供了subscribe功能
this._watcherVM = new Vue()  // Vue組件用於watch監視變化

調用 new Vuex.store(options)時傳入的options對象,用於構造ModuleCollection類,下面看看其功能

constructor(rawRootModule) {
  // register root module (Vuex.store options)
  this.root = new Module(rawRootModule, false)

  // register all nested modules
  if(rawRootModule.modules) {
    forEachValue(rawRootModule.modules, (rawModule, key) => {
      this.register([key], rawModule, false)
    })
  }
}

ModuleCollection主要將傳入的options對象整個構造為一個modele對象,並循環調用 this.register([key], rawModule, false) 為其中的modules屬性進行模塊注冊,使其都成為module對象,最后options對象被構造成一個完整的組件樹。ModuleCollection類還提供了modules的更替功能,詳細實現可以查看源文件 module-collection.js

4.3 dispatch與commit設置

 繼續回到store的構造函數代碼。

// bind commit and dispatch to self
const store = this;
const { dispatch, commit } = this;

this.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)
}

 封裝替換原型中的dispatch和commit方法,將this指向當前store對象。

 dispatch和commit方法具體實現如下:

dispatch (_type, _payload) {
  //check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)  // 配置參數處理
  
  // 當前type下所有action處理函數集合
  const entry = this._actions[type]
  if (!entry) {
    console.error(`[vuex] unknown action type: ${type}`)
    return
  }
  return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
}

  前面提到,dispatch的功能是觸發並傳遞一些參數(payload)給對應的type的action。因為其支持2種調用方法,所以在dispatch中,先進行參數(payload)給對應type的action。因為其支持2種調用方法,所以在dispatch中,先進行參數的適配處理,然后判斷action type是否存在,若存在就逐個執行(注:上面代碼中的this._actions[type] 以及下面的this._mutations[type]均是處理過的函數集合,具體內容留到后面進行分析)。

 commit方法和dispatch相比雖然都是觸發type,但是對應的處理卻相對復雜,代碼如下。

commit (_type, _payload, _options) {
  //check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type]
  if(!entry) {
    console.error(`[vuex] unknown mutation type: ${type}`)
    return
  }
  //專用修改state方法,其他修改state方法均是非法修改
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  //訂閱者函數遍歷執行,傳入當前的mutation對象和當前的state
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if(options && options.silent) {
    console.warn(
      `
        [vuex] mutation type: ${type}. Silent option has been removed. ` + `
        Use the filter functionality in the vue-devtools
      `
    )
  }
}

 該方法同樣支持2種調用方法。先進行參數適配,判斷觸發mutation type,利用_widthCommit方法執行本次批量觸發mutation處理函數,並傳入payload參數。執行完成后,通知所有_subscribers(訂閱函數)本次操作的mutation對象以及當前的state狀態,如果傳入了已經移除的silent選項則進行提示警告。

4.4 state修改方法

  _withCommit是一個代理方法,所有觸發mutation的進行state修改的操作都經過它,由此來統一管理監控state狀態的修改。實現代碼如下。

_widthCommit (fn) {
  // 保存之前的提交狀態
  const committing = this._committing
  
  //進行本次提交,若不設置為true,直接修改state,strict模式下,vuex將會產生非法修改state的警告
  this._committing = true

  //執行state的修改操作
  fn()

  //修改完成,還原本次修改之前的狀態
  this._committing = committing
}

  緩存執行時的committing狀態將當前狀態設置為true后進行本次提交操作,待操作完畢后,將committing狀態還原為之前的狀態。

4.5 module安裝

   綁定dispatch和commit方法之后,進行嚴格模式的設置,以及模塊的安裝(installModule)。 由於占用資源較多影響頁面性能,嚴格模式建議只在開發模式開啟,上線后需要關閉。

// strict mode
this.strict = strict

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)

4.5.1 初始化rootState

   上述代碼的備注中,提到installModule方法初始化組件樹根組件、注冊所有子組件,並將其中所有的getters存儲到this._wrappedGetters屬性中,讓我們看看其中的代碼實現。

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  //register in namespace map
  if(namespace) {
    store._modulesNamespaceMap[namespace] = module
  }

  //非根組件設置state方法
  if(!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }
}

  判斷是否是根目錄,以及是否設置了命名空間,若存在則在namespace中進行module的存儲,在不是根組件且不是hot條件的情況下,通過getNestedState方法拿到該module父級的state,拿到其所在的moduleName,調用Vue.set(parentState, moduleName, module.state) 方法將其state設置到父級對象的moduleName屬性中,由此實現該模塊的state注冊(首次執行這里,因為是根目錄注冊,所以並不會執行該條件中的方法)。getNestedState方法代碼很簡單,分析path拿到state,如下。

function getNestedState(state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

4.5.2 module上下文環境設置

const local = module.context = makeLoaclContext(store, namespace, path)

  命名空間和根目錄條件判斷完畢后,接下來定義local變量和module.context的值,執行makeLocalContext方法,為該module設置局部的dispatch、commit方法以及getter和state(由於namespace的存在需要做兼容處理)。

4.5.3 mutations、actions以及getters注冊

 定義local環境后,循環注冊我們在options中配置的action以及mutation等。逐個分析各注冊函數之前,先看下模塊間的邏輯關系流程圖:

 

 

 下面分析代碼邏輯:

//注冊對應模塊的mutation,供state修改使用
module.forEachMutation((mutation, key) => {]
  const namespaceType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})

//注冊對應模塊的action,供數據操作、提交mutation等異步操作使用
module.forEachAction((action, key) => {
  const namespaceType = namespace + key
  registerAction(store, namespaceType, action, local)
})

//注冊對應模塊的getters,供state讀取使用
module.forEachGetter(getter, key) {
  const namespaceType = namespace + key
  registerGetter(store, namespacedTyp, getter, local)
})

  registerMutation方法中,獲取store中的對應mutation type的處理函數集合,將新的處理函數push進去。這里將我們設置在mutation type上對應的handler進行了封裝,給原函數傳入了state。在執行commit('xxx', payload)的時候,type為xxx的mutation的所有handler都會接受到state以及payload,這就是在handler里面拿到state的原因。

function registerMutation (store, type, handler, local) {
  //取出對應type的mutations-handler集合
  const entry = store._mutations[type] || (store._actions[type] = [])
  //存儲新的封裝過的action-handler
  entry.push(function wrapperActionHandler (payload, cb) {
    //傳入state 等對象供我們原action-handler使用
    let res = handler({
      dispatch: local.dispatch,  
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    //action需要支持promise進行鏈式調用,這里進行兼容處理
    if(!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if(store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

function registerGetter (store, type, rawGetter, local) {
  //getters值允許存在一個處理函數,若重復需要報錯
  if(store._wrappedGetters[type]) {
    console.error(`[vuex] duplicats getter key: ${type}`)
    return
  }

  //存儲封裝過的getters處理函數
  store._wrappedGetters[type] = function wrappedGetter (store) {
    //為原getters傳入對應狀態
    return rawGetter(
      local.state,  //local state
      local.getters,  //local getters
      store.state,  //root state
      store.getters  //root getters
    )
  }
}

  action handler比mutation handler以及getter wrapper多拿到dispatch和commit操作方法,因此action可以進行dispatch  action和commit mutation操作。

4.5.4 子module安裝

  注冊完了根組件的actions、mutations以及getter后,遞歸調用自身,為子組件注冊其state,actions、mutation以及getters等。

module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})

4.5.5實例結合

  前面介紹了dispatch和commit方法以及actions等的實現,下面結合一個官方的購物車實例中的部分代碼來加深理解。

Vuex配置代碼:

/*
*  store-index.js store配置文件
*/

import Vue from 'vue'

import Vuex from 'vuex'

import * as actions from './actions'

import * as getters from './getters'

import cart from './modules/cart'

import products from './modules/products'

import createLogger from '../../../src/plugin/logger'

 

Vue.use(Vuex)

const debug = process.env.NODE_ENV !== 'production'

export default new Vuex.Store({

  actions,

  getters,

  modules: {

    cart,

    products

  },

  strict: debug,

  plugins: debug ? [createLogger()] : []

}) 

Vuex組件module中各模塊state配置代碼部分:

/**
* cart.js
*
**/

const state = {
  added: [],
  checkoutStatus: null
}
/**
* products.js
*
**/
const state = {
  all: []
}

  加載上述配置后,頁面state結構如下圖:

            

 

 

   state中的屬性配置都是按照option配置中module path的規則來進行的,下面看action的操作實例。

  Vuecart組件代碼部分:

/**
* Cart.vue  省略template代碼,只看script部分
*
**/
export default {
  methods: {
    //購物車中的購買按鈕,點擊后會觸發結算,源碼中會調用dispatch方法
    checkout (products) {
      this.&store.dispatch('checkout',products)
    }
  }
}

  Vuexcart.js組件action配置代碼部分:

const action = {
  checkout ({ commit, state }, products) {
    const saveCartItems = [...state.added]  //存儲添加到購物車的商品
    commit(types.CHECKOUT_REQUEST)   //設置提交結算狀態
    shop.butProducts(  //提交api請求,並傳入成功與失敗的cb-func
      products,
      () => commit(type.CHECKOUT_SUCCESS),   //請求返回成功則設置提交成功狀態
      () => commit(types.CHECKOUT_FALLURE, { savedCartItems })  //請求返回失敗則設置提交失敗狀態
    )
  }
}

  Vue組件中點擊購買執行當前module的dispatch方法,傳入type值為'checkout', payload值為'products',在源碼中dispatch方法在所有注冊過的actions中查找'checkout'的對應執行數組,取出循環執行。執行的是被封裝過的被命名為wrappedActionHandler的方法,真正傳入的checkout的執行函數在wrappedActionHandler這個方法中被執行,源碼如下(注:前面貼過,這里看一次):

function wrappedActionHandler (payload, cb) {
  let res = handler({
    dispatch: local.dispatch,
    commit: local.commit,
    getters: local.getters,
    state: local.state,
    rootGetters: store.getters,
    rootState: store.state
  }, payload, cb)
  if(!isPromise(res)) {
    res = Promise.resolve(res)
  }
  if(store._devtoolHook) {
    return res.catch(err => {
      store._devtoolHook.emit('vuex:error', err)
      throw err
    })
  } else {
    return res
  }
}

  handler在這里就是傳入的checkout函數,其執行需要的commit以及state就是在這里被傳入,payload也傳入了,在實例中對應接受的參數名為products。commit的執行也是同理的,實例中checkout還進行了一次commit操作,提交一次type值為types.CHECKOUT_REQUEST的修改,因為mutation名字是唯一的,這里進行了常量形式的調用,防止命名重復,執行跟源碼分析中一致,調用function wrappedMutationHandler (payload) { handler(local.state, payload)} 封裝函數來實際調用配置的mutation方法。

看完源碼分析和上面的小實例,應該能理解dispatch action 和 commit mutation的工作原理了。接着看源碼,看看getters是如何實現state實時訪問的。

4.6 store._vm組件設置

  執行完各module的install后,執行resetStoreVM方法,進行store組件的初始化。

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

  綜合前面的分析可以了解到,Vuex其實構建的就是一個名為store的vm組件,所有配置的state、action、mutation以及getters都是其組件的屬性,所有的操作都是對這個vm組件進行的。

  resetStoreVM方法的內部實現:

function resetStoreVM(store, state) {
  const oldVm = store._vm  //緩存前vm組件

  //bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  //循環所有處理過的getters,並新建computed對象進行存儲,通過Object.defineProperty方法為getters對象建立屬性,使得我們通過
  //this.$store.getters.xxxgetter能夠訪問到該getters
  forEachValue(wrappedGetters, (fn, key) => {
    //use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get:() => store.vm_[key],
      enumerable: true //for local getters
    })
  })

  //use a Vue instance to store the state tree
  //suppress warnings just in case the user has added
  //some funky global mixins
  const silent = Vue.config.silent

  //暫時將vue設為靜默模式,避免報出用戶加載的某些插件觸發的警告
  Vue.config.silent = true
  //設置新的storeVm,將當前初始化的state以及getters作為computed屬性(剛剛遍歷生成的)
  store._vm = new Vue({
    data: {state},
    computed
  })
  //恢復Vue的模式
  Vue.config.silent = silent
  
  //enable strict mode for new vm
  is(store.strict) {
    //該方法對state執行$watch以禁止從mutation外部修改state
    enableStrictMode(store)
  }
  //若不是初始化過程執行的該方法,將舊的組件state設置為null,強制更新所有監聽者(watchers),待更新生效,DOM更新完成后,執行vm
  //組件的destroy方法進行銷毀,減少內存的占用
  if(oldVm) {
    //dispatch change in all subscribed watchers
    //to force getter re-evaluation.
    store._withCommit(() => {
      oldVm.state = null
    })
    Vue.nextTick(() => oldVm.$destroy())
  }
}

  resetStoreVm方法創建了當前store實例的_vm組件,至此store就創建完畢了。上面代碼涉及到了嚴格模式的判斷,看一下嚴格模式

如何實現的。

function enableStrictMode(store) {
  store._vm.$watch('state', () => {
    assert(store._committing,`Do not mutate vuex store state ourside mutation handlers.`)
  }, {deep: true, sync: true})
}

很簡單的應用,監視state的變化,如果沒有通過this._withCommit()方法進行state修改,則報錯。

4.7 pugin注入

最后執行plugin的植入。

plugins.concat(devtoolPlugin).forEach(plugin => plugin(this))

devtoolPlugin提供的功能有3個:

// 1.觸發Vuex組件初始化的hook
devtoolHook.emit('vuex:init', store)

// 2.提供"時空穿梭"功能,即state操作的前進和倒退
devtoolHook.on('vuex:travel-to-state', targetState => {
  store.replaceState(targetState)
})

// 3.mutation被執行時,觸發hook,並提供被觸發的mutation函數和當前的state狀態
store.subscribe((mutation, state) => {
  devtoolHook.emit('vuex:mutation', mutation, state)
})

五、總結

   1.問:使用Vuex只需執行 Vue.use(Vuex),並在Vue的配置中傳入一個store對象的示例,store是如何實現注入的?

  “

    答: Vue.use(Vuex) 方法執行的是install方法,它實現了Vue實例對象的init方法封裝和注入,使傳入的store對象被

       設置到Vue上下文環境的&store中。因此在Vue Component任意地方都能夠通過this.$store訪問到該store。

  ”  

   2.問:state內部支持模塊配置和模塊嵌套,如何實現的?

  “

    答:在store構造方法中有makeLocalContext方法,所有module都會有一個local context,根據配置時的path進行

      匹配。所以執行如dispatch('submitOrder', payload)這類action時,默認的拿到都是module的local state,如

      果要訪問最外層或者是其他module的state,只能從rootState按照path路徑逐步進行訪問。

  ”

   3.問:在執行dispatch觸發action(commit同理)的時候,只需傳入(type,payload),action執行函數中第一個參數

     store從哪里獲取的?

  “

    答:store初始化時,所有配置的action和mutation以及getters均被封裝過。在執行如dispatch('submitOrder', payload)

      的時候,action中type為submitOrder的所有處理方法都是被封裝后的,其第一個參數為當前的store對象,所以能夠

      獲取到{dispatch, commit, state, rootState}等數據。

  ”

   4.問:Vuex如何區分state是外部直接修改,還是通過mutation方法修改的?

  “

    答:Vuex中修改state的唯一渠道就是執行commit('xx', payload) 方法,其底層通過執行 this._withCommit(fn) 設置

      _committing標志變量為true,然后才能修改state,修改完畢還需要還原_committing變量。外部修改雖然能夠

      直接修改state,但是並沒有修改_committing標志位,所以只要watch一下state,state change時判斷是否

      _committing值為true,即可判斷修改的合法性。

  ”

  5.問:調試時的“時空穿梭”功能是如何實現的?

  “

    答:devtoolPlugin中提供了此功能。因為dev模式下所有的state change都會被記錄下來,‘時空穿梭’功能其實就是

      將當前的state替換為記錄中某個時刻的state狀態,利用store.replaceState(targetState)方法將執行

      this._vm.state = state實現。

  ”

   原文地址:https://tech.meituan.com/2017/04/27/vuex-code-analysis.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM