Vue2.x源碼學習筆記-從一個小例子查看vm實例生命周期


學習任何一門框架,都不可能一股腦兒的從入口代碼從上到下,把代碼看完,

這樣其實是很枯燥的,我想也很少有人這么干,或者這么干着干着可能干不下去了。

因為肯定很無聊。

我們先從一個最最簡單的小例子,來查看new Vue(options)實例,這個過程發生了什么。

vm實例上的屬性又如何添加上去的,又如何渲染到瀏覽器頁面上的。

關於vue的數據依賴和虛擬dom都是重點,必然會在以后的帖子記錄。

這篇帖子就根據下例子,看看實例化一個vm實例做了啥吧。

先把小例子貼出來:

      <div id="app">
          <p>這是<span>靜態內容</span></p>
          <p>{{message}}</p>
      </div>
      <script src="../../dist/vue.js"></script>
      <script>
          var vm = new Vue({
              el: '#app',
              data: {
                  message: 'hi vue!'
              }
          })
          console.log(vm)
      </script>

根據上篇介紹了vue的調式筆記,那我們快快進入源碼吧

根據vue構造函數那篇筆記,我們知道了Vue原型上有哪些方法,_init方法就是其中一個方法

我們看到_init就把實例要做的事情都做完了,當然其中有的語句所做的事,太多了。我們先一點一點開see see吧。

看圖不好玩,我把源碼取出 來 好好瞧瞧

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid vm實例唯一標識
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if 性能統計相關 */ 
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed 監聽對象變化時用於過濾vm
    vm._isVue = true
    // merge options  _isComponent是內部創建子組件時才會添加為true的屬性
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options) // 初始化內部組件
    } else {
      // mergeOptions 方法 合並構造器及構造器父級上定義的options
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

1. 給實例添加了唯一標識uid

2.性能統計相關,先忽略

3. 給實例添加_isVue,監聽對象變化時用於過濾vm

4. 選項對象如果有_isComponent,就初始化內部組件,_isComponent是內部創建子組件時才會添加為true的屬性

5. 小例子會走分支,mergeOptions 方法 合並構造器及構造器父級上定義的options,resolveConstructorOptions方法后面筆記會詳解,

  mergeOptions方法接受3個參數。我們先簡單看下resolveConstructorOptions方法的定義

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  // 有super屬性,說明Ctor是通過Vue.extend()方法創建的子類
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

可以看出Ctor.options其實就是Vue構造函數自身,在Vue構造函數靜態屬性那篇筆記,Vue是擁有options屬性的,且有截圖,等下會再截圖看下,

接着在該函數中有個if語句,我們小例子會跳過的,直接返回options。因為有super屬性,說明Ctor是通過Vue.extend()方法創建的子類。那么

options是啥呢,如下圖,

回到_init方法中,mergeOptions方法的第二個參數就是我們傳入的options,第三個參數就是vm實例,把參數一起截個圖吧,好回憶

mergeOptions是Vue中處理屬性的合並策略的地方, 先看下它的定義

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    // 如果有options.components,則判斷是否組件名是否合法
    checkComponents(child)
  }
  // 格式化child的props
  normalizeProps(child)
  // 格式化child的directives
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = typeof extendsFrom === 'function'
      ? mergeOptions(parent, extendsFrom.options, vm)
      : mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      let mixin = child.mixins[i]
      if (mixin.prototype instanceof Vue) {
        mixin = mixin.options
      }
      parent = mergeOptions(parent, mixin, vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
} 

該函數主要返回一個數據合並后的options,
我們的小例子比較簡單, so關於判斷是否組件名是否合法,
格式化child的props, 格式化child的directives
extends, mixins 先跳過。
我們直接看怎么把屬性合並到options = {}這個對象上的
首先遍歷parent對象,然后通過mergeField函數,把components,
directives, filters, _base屬性先添加到options對象上,值為
strats對象上的靜態方法。
然后遍歷child對象,把el, data屬性也添加到options = {} 這個對象上
值為strats對象上對應的靜態方法。
那我們先看看strats這個對象上有哪些靜態方法,源碼如下(src/util/options.js)

const strats = config.optionMergeStrategies

if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
     /**/
  }
}

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  /**/
}

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

config._lifecycleHooks.forEach(hook => {
  strats[hook] = mergeHook
})

function mergeAssets (parentVal: ?Object, childVal: ?Object): Object {
  /**/
}

config._assetTypes.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

strats.watch = function (parentVal: ?Object, childVal: ?Object): ?Object {
  /* istanbul ignore if */
  /**/
}

strats.props =
strats.methods =
strats.computed = function (parentVal: ?Object, childVal: ?Object): ?Object {
  if (!childVal) return Object.create(parentVal || null)
  if (!parentVal) return childVal
  /**/
}

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

以上是縮減版的代碼,其實看下截圖,會一目了然

可以看到其實就是我們new Vue(options) 中的options對象中的可選參數。我們小例子只傳了el, data,

我們看看通過mergeOptions方法合並后的options長的什么鳥樣,如圖:

其實小例子只是走個過程,沒必要把所有函數代碼弄懂,先把大體流程走完,后續代碼在一一分析。

6. 回到vm_init()方法中,接着走initProxy(vm)這個語句,這個語句其實就是給vm實例添加了一個_renderProxy屬性,值為為一個Proxy代理對象,生產環境就是vm自身。

接下來的每個語句都有好多代碼啊,我們一個個 look see see

 7. initLifecycle 方法的定義

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
} 

初始化生命周期,該函數只是給vm添加了
$parent, $root, $children, $refs, _watcher,
_inactive, _directInactive, _isMounted, _isDestroyed
_isBeingDestroyed屬性。

options.abstract用於判斷是否是抽象組件,
組件的父子關系建立會跳過抽象組件,抽象組件比如keep-alive、transition等。
所有的子組件$root都指向頂級組件。

8. initEvents方法的定義

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
} 

該方法初始化事件相關的屬性,給vm實例添加了_events, _hasHookEvent屬性
_parentListeners是父組件中綁定在自定義標簽上的事件,供子組件處理。

9. initRender方法的定義

export function initRender (vm: Component) {
  vm.$vnode = null // the placeholder node in parent tree
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null
  const parentVnode = vm.$options._parentVnode
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

這里先給vm添加了$vnode, _vnode, _staticTrees, $slots, $scopedSlots, _c, $createElement
屬性或者方法(添加了一些虛擬dom、slot等相關的屬性和方法)

10. 調用beforeCreate鈎子

11. 調用initInjections(vm)方法,我們小例子比較簡單,不會進入if語句中

export function initInjections (vm: Component) {
  const inject: any = vm.$options.inject
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    // isArray here
    const isArray = Array.isArray(inject)
    const keys = isArray
      ? inject
      : hasSymbol
        ? Reflect.ownKeys(inject)
        : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      const provideKey = isArray ? key : inject[key]
      let source = vm
      while (source) {
        if (source._provided && provideKey in source._provided) {
          /* istanbul ignore else */
          if (process.env.NODE_ENV !== 'production') {
            defineReactive(vm, key, source._provided[provideKey], () => {
              warn(
                `Avoid mutating an injected value directly since the changes will be ` +
                `overwritten whenever the provided component re-renders. ` +
                `injection being mutated: "${key}"`,
                vm
              )
            })
          } else {
            defineReactive(vm, key, source._provided[provideKey])
          }
          break
        }
        source = source.$parent
      }
    }
  }
}

將父組件provide中定義的值,通過inject注入到子組件,且這些屬性不會被觀察

12. initState(vm)

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch) initWatch(vm, opts.watch)
}

該方法主要就是操作數據了,props、methods、data、computed、watch,
從這里開始就涉及到了Observer、Dep和Watcher,下個筆記再記錄

13. initProvide(vm)

 export function initProvide (vm: Component) {
   const provide = vm.$options.provide
   if (provide) {
     vm._provided = typeof provide === 'function'
       ? provide.call(vm)
       : provide
   }
 }

也不會進分支,先略過

14. 調用created鈎子函數。

可以看到在created鈎子函數調用前, 基本就是對傳入數據的格式化、數據的雙向綁定、以及一些屬性的初始化。

現在來看下實例的屬性和方法

接下來看看怎么把html模板中的屬性出來的

15. vm.$mount(vm.$options.el)

 

const mount = Vue.prototype.$mount
// 重寫Vue構造函數原型上的$mount方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

該方法主要拿到template模板,然后通過compileToFunctions方法的返回值給vm實例
的$options添加render屬性,值為一個匿名函數該匿名函數返回值為:
with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v(" "),_c('p',[_v(_s(message))])])}
還添加了一個staticRenderFns屬性,值為一個數組,數組元素為匿名函數:

anonymous() {
with(this){return _c('p',[_v("這是"),_c('span',[_v("靜態內容")])])}
}

至於compileToFunctions函數先不拿出來看了,目前先知道它干了啥,就行了(至於實例的_c,_v,_m這些方法何時掛載上去的,前面筆記已經說過了)

 

 

之后調用 mount.call(this, el, hydrating) 方法

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
} 

該方法又調用mountComponent(this, el, hydrating)

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  vm._watcher = new Watcher(vm, updateComponent, noop)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

小例子在該方法大致流程是這么走的

因為vm.$options.render就是給匿名函數,所以不會走if分支,
然后調用beforeMount鈎子函數
再然后定義一個updateComponent函數,這個函數怎么執行是個關鍵
然后給實例添加了一個_watcher屬性,值為Watcher實例
然后如果vm.$vnode == null則把vm._isMounted變量置為true,然后調用mounted鈎子函數
最后返回vm實例,可以鏈式調用。

觸發updateCOMPONENT函數是new Watcher,先看看Watcher類的定義

constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    ...
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''

    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    if (this.user) {
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      }
    } else {
      value = this.getter.call(vm, vm)
    }

    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    return value
  }

在構造函數中,expOrFn也就是updateComponent賦值給this.getter,
並且在獲取this.value的值時會調用this.get(),這里的this.lazy默認值是false,
在computed屬性中創建的Watcher會傳入true。

在this.get()中,會調用this.getter,所以上面的例子中,updateComponent方法會被調用,

updateComponent = () => {
    vm._update(vm._render(), hydrating)
}

回到該函數,先執行實例的_render函數,該函數主要生成虛擬dom
然后執行實例的update方法

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const {
      render,
      staticRenderFns,
      _parentVnode
    } = vm.$options

    if (vm._isMounted) {
      // clone slot nodes on re-renders
      for (const key in vm.$slots) {
        vm.$slots[key] = cloneVNodes(vm.$slots[key])
      }
    }

    vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject

    if (staticRenderFns && !vm._staticTrees) {
      vm._staticTrees = []
    }
    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render function`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        vnode = vm.$options.renderError
          ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
          : vm._vnode
      } else {
        vnode = vm._vnode
      }
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

該函數主要是 vnode = render.call(vm._renderProxy, vm.$createElement)語句
函數調用過程中的this,是vm._renderProxy,是一個Proxy代理對象或vm本身。暫且把它當做vm本身。

_c是(a, b, c, d) => createElement(vm, a, b, c, d, false)。
createElement函數做了這些事:
a是要創建的標簽名,這里是div。
接着b是data,也就是模板解析時,添加到div上的屬性等。
c是子元素數組,所以這里又調用了_c來創建一個p標簽。
_v是createTextVNode,也就是創建一個文本結點。
_s是_toString,也就是把message轉換為字符串,在這里,因為有with(this),
所以message傳入的就是我們data中定義的第一個vue實例。
所以,render函數返回的是一個VNode對象,也就是我們的虛擬dom對象。
它的返回值,將作為vm._update的第一個參數。

接着看下update方法

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

從mountComponent中知道創建Watcher對象先於vm._isMounted = true。
所以這里的vm._isMounted還是false,不會調用beforeUpdate鈎子函數。

下面會調用vm.__patch__,在這一步之前,
頁面的dom還沒有真正渲染。該方法包括真實dom的創建、虛擬dom的diff修改、dom的銷毀等,
具體細節后續筆記在記錄。


免責聲明!

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



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