小白看源碼之Vue篇-2:組件化的過程

前言

又到了週末,開始梳理Vue源碼的第二篇,在這篇中,咱們會講到初始化Vue過程當中組件化的過程,包括如何建立Vnode的過程。以及搞清楚vm.$vnodevm._vnode的區別和關係。上一篇文章咱們講到vm.$mount函數最後執行到了vm._update(vm._render(), hydrating)那麼這一篇咱們將接着上一篇的結尾開始講。html

建立Vnode

vm._update(vm._render(), hydrating)這個行代碼,咱們首先要看的是vm._render()他到底幹了什麼。vue

1、vm._render()幹了什麼

首先,咱們來到vm._render()的定義處src/core/instance/render.jsnode

export function renderMixin (Vue: Class<Component>{  //Vue類初始化的時候掛載render函數
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function{
    return nextTick(fn, this)
  }

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

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode //首次掛載爲undefined
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)      //Vnode的具體實現經過vm.$createElement
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it  只能有一個根節點
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // 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
  }
}
複製代碼

能夠經過代碼看到,vm._render()是在Vue的renderMixin過程當中建立的,先不細究每一處代碼,咱們能夠看到vm._render()最終返回的就是一個vnode。而後做爲vm._update(vnode,hydrafting)的參數傳入。 咱們重點看其中的幾個步驟,細節能夠暫時忽略:react

一、const { render, _parentVnode } = vm.$options咱們從vm.$options拿到了render函數和他的父親vnode(首次實例化確定是空的);git

二、vm.$vnode = _parentVnode註釋寫的很是清楚這個節點可讓render函數拿到佔位符節點的數據。後續組件建立的時候會講到是如何拿到佔位符節點的數據的,能夠這麼理解vm.$vnode就是父親節點,也就是佔位符節點,例如element-ui的<el-button></el-button>就是一個佔位符節點。github

三、vnode = render.call(vm._renderProxy, vm.$createElement)能夠看到,實際上vnode是經過vm.$createElement生成的,而vm.$createElement也就是咱們常說的h函數:element-ui

new Vue({
  renderh => h(App),
}).$mount('#app')
複製代碼

至於vm._renderProxy則是在vm._init()的時候定義的,開發環境下就是vm自己。api

2、vm.$createElement

那麼接下來咱們來看vm.$createElement作了什麼。代碼也在src/core/instance/render.js,是經過vm._init()進行的initRender(vm)來初始化vm.$createElement數組

export function initRender (vm: Component{
  //.....some codes
  // 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)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  //... somecodes
}
複製代碼

能夠看到vm.$createElement實際上就是返回了createElement(vm, a, b, c, d, true)的結果,接下來看一下createElement(vm, a, b, c, d, true)幹了什麼。瀏覽器

3、createElement_createElement

3-1:createElement

createElement定義在src/core/vdom/create-elements.js下:

export function createElement ( //對_createElement函數的封裝
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode
{
  if (Array.isArray(data) || isPrimitive(data)) { //若是未傳入data 則把變量值往下傳遞
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}
複製代碼

看到傳入的參數你們就會以爲很是的熟悉了,就是vue官方文檔中的creatElement參數中定義的那些字段。咱們在這裏傳入的固然是App.vue導出的這個App對象了,當作是tag傳入。能夠看到createElement()方法實際就是若是未傳入data參數,就把childer之類的參數所有向下移位,防止參數調用錯誤。真正的邏輯則在其返回的函數_createElement

3-2:_createElement

_createElement相關定義也在src/core/vdom/create-elements.js下:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode
{
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefinedundefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefinedundefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}
複製代碼

代碼很長,咱們關注幾個重點便可:

一、children參數的處理:children參數有兩種處理狀況,根據傳入的參數normalizationType的值來肯定。若是是ALWAYS_NORMALIZE調用normalizeChildren(children)若是是SIMPLE_NORMALIZE則調用simpleNormalizeChildren(children)。這兩個函數的定義在同級目錄的./helper/normolize-children.js下,這裏不細展開來說了,本身看一看就大概瞭解了,一個是直接concat()拍平數組,一個就是遞歸push進一個數組當中:

if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
}
複製代碼

二、判斷tag類型:一、若是是string類型,則繼續判斷是不是原生的html標籤,若是是,就new Vnode(),不然該tag是經過vue.component()或者局部component註冊的組件(後續會講),則走createComponent()生成組件vnode;二、若是不是string類型,那麼則是相似咱們上面那種狀況,傳入的是App這個對象,則走createComponent()方法建立vnode:

if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        //生產環境相關
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefinedundefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      //未知命名空間
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
 }
複製代碼

三、return Vnode沒啥好說的。

到這裏那麼vnode就生成了,那麼咱們接下來去看一下createComponent(tag, data, context, children)都幹了些什麼。

4、createComponent()

createComponent()是一個比較複雜的環節,但願你們可以反覆的琢磨這裏面的奧妙,本小白也是在這個地方停留了好久,有很是多的細節須要注意的。 createComponent()定義在src/core/vdom/create-component.js中:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void, //傳入的對象
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void 
{
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base  //就是Vue自己,做爲一個基類構造器

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor) // 轉化爲一個構造器,瞭解Vue.extend的實現
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    if (process.env.NODE_ENV !== 'production') {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data) //

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefinedundefinedundefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}
複製代碼

咱們關注其中的幾個重點:

createComponent之1、Ctor = baseCtor.extend(Ctor)即 Vue.extend()幹了什麼

baseCtor = context.$options._base首次渲染的時候,指的就是vue自己,定義在src/core/global-api/index.js中: Vue.options._base = Vue 那麼vue.extend()到底對咱們的App對象作了什麼呢,Vue.extend接下去定義在src/core/global-api/extend.js

Vue.cid = 0
let cid = 1
Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this //指向Vue靜態方法
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) { //單例模式,若是是複用的組件,那麼 直接返回緩存在cachedCtors[SuperId]上的Sub
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name) // 校驗component的name屬性
    }

    const Sub = function VueComponent (options{
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype) //原型繼承
    Sub.prototype.constructor = Sub //再把constructor指回sub
    Sub.cid = cid++
    Sub.options = mergeOptions(  //合併options
      Super.options,
      extendOptions
    )
    Sub['super'] = Super //super指向Vue

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type{
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
}
複製代碼

tips:可能不少同窗看到這裏會有些暈了,畢竟文章是按順序往下寫的,可能沒有那麼直觀,很容易形成架構的混淆之類的,這裏貼一下我整理的層級幕布圖,僅供參考,但願能有所幫助:Vue源碼解讀

Vue.extend()大體作了如下五件事情。

一、cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})建立該組件實例的構造方法屬性,單例模式,優化性能:

if (cachedCtors[SuperId]) { //單例模式,若是是複用的組件,那麼 直接返回緩存在cachedCtors[SuperId]上的Sub
 return cachedCtors[SuperId]
}
複製代碼

二、原型繼承模式,建立組件的構造函數:

const Sub = function VueComponent (options{
 this._init(options)//找到Vue的_init
}
Sub.prototype = Object.create(Super.prototype) //原型繼承
Sub.prototype.constructor = Sub //再把constructor指回sub
Sub.cid = cid++
Sub.options = mergeOptions(  //合併options
 Super.options,
    extendOptions
)
Sub['super'] = Super //super指向Vue
複製代碼

三、提早數據雙向綁定,防止patch實例化的時候在此調用object.defineProperty

    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }
複製代碼

四、繼承Vue父類的全局方法:

    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use
複製代碼

五、緩存對象並返回:

    cachedCtors[SuperId] = Sub
    return Sub
複製代碼

createComponent之剩下其餘:

createcomponent()介紹瞭如下extend方法,接下來還有一些比較重的地方的,日後會在patch環節重點講到,這邊先將其列出,但願讀者可以略做留意。

一、const propsData = extractPropsFromVNodeData(data, Ctor, tag)這條代碼將是日後咱們從佔位符節點獲取自定義props屬性並傳入子組件的關鍵所在,patch過程會詳細說明。

二、函數式組件:

if (isTrue(Ctor.options.functional)) {
 return createFunctionalComponent(Ctor, propsData, data, context, children)
}
複製代碼

三、installComponentHooks(data)又是一個重點,註冊組件上面的鉤子函數,鉤子函數定義在同一個文件下面:分爲init、prepatch、insert、destroy四個鉤子函數,patch過程講到會專門介紹:

const hooksToMerge = Object.keys(componentVNodeHooks)
function installComponentHooks (data: VNodeData{
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    const key = hooksToMerge[i]
    const existing = hooks[key]
    const toMerge = componentVNodeHooks[key]
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}
複製代碼

四、new Vnode() & retrun vnode:

const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefinedundefinedundefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
)
return vnode
複製代碼

能夠看到組件的構造函數等一些被放在了一個對象當中傳入了構造函數。

那麼到這麼,createComponent()的過程也大體過了一遍了。

5、Virtual-Dom

講了半天的vnode建立,也沒有講到vnode類的實現,放到最後面講是有緣由的,實在是建立vnode狀況太多了,上面先鋪開,再到最後用vnode一收,但願能狗加深各位的印象。

5-一、爲何要使用Vnode?

一、建立真實DOM的代價高

真實的DOM節點node實現的屬性不少,而vnode僅僅實現一些必要的屬性,相比起來,建立一個vnode的成本比較低。因此建立vnode可讓咱們更加關注需求自己。

二、觸發屢次瀏覽器重繪及迴流

是使用vnode,至關於加了一個緩衝,讓一次數據變更所帶來的全部node變化,先在vnode中進行修改,而後diff以後對全部產生差別的節點集中一次對DOM tree進行修改,以減小瀏覽器的重繪及迴流。這一點咱們會在深刻響應原理的章節當中詳細介紹,包括渲染watcher更新隊列,組件的更新等等~

5-二、Class Vnode{}

Vnode類定義在src/core/vdom/vnode.js中:

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void// rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void// component instance
  parent: VNode | void// component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void// async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void// real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object// used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    //somecodes
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}
複製代碼

看起來這個Vnode類已經很是的長了,實際的dom節點要比這個更加的龐雜巨大,其中的屬性,有些咱們暫時能夠不用關心,咱們重點放在構造函數上:

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
複製代碼

咱們來對照一下createComponent()中建立的vnode傳入的參數是哪幾個:

const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefinedundefinedundefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
)
複製代碼

能夠看到,實際上createComponent()中就傳入了一個tag、data、當前的vm:context還有一個componentOptions,固然精華都是在這個componentOptions裏面的,後面的_update也就是圍繞這個componentOptions展開patch的,那麼就放到下一篇:《小白看源碼之Vue篇-3:vnode的patch過程》!

總結

那麼vm._render()建立虛擬dom的過程咱們也就大體過了一遍,文章一遍寫下來,實際不少的地方須要讀者本身反覆的琢磨,不是看一遍就瞭解的,本小白也是在反覆掙扎後才略知一二的。

本節的重點如何建立一個vnode,以及不一樣狀況下的不一樣建立方式,暫不要深究細節,日後慢慢會涉及到鋪開,你在回過頭來,恍然大明白!加深印象!

最後的最後打個小小的廣告~

有大佬們須要短信業務,或者號碼認證的能力的話,能夠看看這裏!中國聯通創新能力平臺 運營商官方平臺!沒有中間商賺差價~

相關文章
相關標籤/搜索