新聞中心
前面的文章分析了 Concurrent 模式下異步更新的邏輯,以及 Fiber 架構(gòu)是如何進(jìn)行時間分片的,更新過程中的很多內(nèi)容都省略了,評論區(qū)也收到了一些同學(xué)對更新過程的疑惑,今天的文章就來講解下 React Fiber 架構(gòu)的更新機制。

為定襄等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計制作服務(wù),及定襄網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為網(wǎng)站設(shè)計制作、成都做網(wǎng)站、定襄網(wǎng)站設(shè)計,以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會得到認(rèn)可,從而選擇與我們長期合作。這樣,我們也可以走得更遠(yuǎn)!
Fiber 數(shù)據(jù)結(jié)構(gòu)
我們先回顧一下 Fiber 節(jié)點的數(shù)據(jù)結(jié)構(gòu)(之前文章省略了一部分屬性,所以和之前文章略有不同):
- function FiberNode (tag, key) {
- // 節(jié)點 key,主要用于了優(yōu)化列表 diff
- this.key = key
- // 節(jié)點類型;FunctionComponent: 0, ClassComponent: 1, HostRoot: 3 ...
- this.tag = tag
- // 子節(jié)點
- this.child = null
- // 父節(jié)點
- this.return = null
- // 兄弟節(jié)點
- this.sibling = null
- // 更新隊列,用于暫存 setState 的值
- this.updateQueue = null
- // 新傳入的 props
- this.pendingProps = pendingProps;
- // 之前的 props
- this.memoizedProps = null;
- // 之前的 state
- this.memoizedState = null;
- // 節(jié)點更新過期時間,用于時間分片
- // react 17 改為:lanes、childLanes
- this.expirationTime = NoLanes
- this.childExpirationTime = NoLanes
- // 對應(yīng)到頁面的真實 DOM 節(jié)點
- this.stateNode = null
- // Fiber 節(jié)點的副本,可以理解為備胎,主要用于提升更新的性能
- this.alternate = null
- // 副作用相關(guān),用于標(biāo)記節(jié)點是否需要更新
- // 以及更新的類型:替換成新節(jié)點、更新屬性、更新文本、刪除……
- this.effectTag = NoEffect
- // 指向下一個需要更新的節(jié)點
- this.nextEffect = null
- this.firstEffect = null
- this.lastEffect = null
- }
緩存機制
可以注意到 Fiber 節(jié)點有個 alternate 屬性,該屬性在節(jié)點初始化的時候默認(rèn)為空(this.alternate = null)。這個節(jié)點的作用就是用來緩存之前的 Fiber 節(jié)點,更新的時候會判斷 fiber.alternate 是否為空來確定當(dāng)前是首次渲染還是更新。下面我們上代碼:
- import React from 'react';
- import ReactDOM from 'react-dom';
- class App extends React.Component {
- state = { val: 0 }
- render() {
- return
val: { this.state.val }- }
- }
- ReactDOM.unstable_createRoot(
- document.getElementById('root')
- ).render(
)
在調(diào)用 createRoot 的時候,會先生成一個FiberRootNode,在 FiberRootNode 下會有個 current 屬性,current 指向 RootFiber 可以理解為一個空 Fiber。后續(xù)調(diào)用的 render 方法,就是將傳入的組件掛載到 FiberRootNode.current(即 RootFiber) 的空 Fiber 節(jié)點上。
- // 實驗版本對外暴露的 createRoot 需要加上 `unstable_` 前綴
- exports.unstable_createRoot = createRoot
- function createRoot(container) {
- return new ReactDOMRoot(container)
- }
- function ReactDOMRoot(container) {
- var root = new FiberRootNode()
- // createRootFiber => createFiber => return new FiberNode(tag);
- root.current = createRootFiber() // 掛載一個空的 fiber 節(jié)點
- this._internalRoot = root
- }
- ReactDOMRoot.prototype.render = function render(children) {
- var root = this._internalRoot
- var update = createUpdate()
- update.payload = { element: children }
- const rootFiber = root.current
- // update對象放到 rootFiber 的 updateQueue 中
- enqueueUpdate(rootFiber, update)
- // 開始更新流程
- scheduleUpdateOnFiber(rootFiber)
- }
render 最后調(diào)用 scheduleUpdateOnFiber 進(jìn)入更新任務(wù),該方法之前有說明,最后會通過 scheduleCallback 走 MessageChannel 消息進(jìn)入下個任務(wù)隊列,最后調(diào)用 performConcurrentWorkOnRoot 方法。
- // scheduleUpdateOnFiber
- // => ensureRootIsScheduled
- // => scheduleCallback(performConcurrentWorkOnRoot)
- function performConcurrentWorkOnRoot(root) {
- renderRootConcurrent(root)
- }
- function renderRootConcurrent(root) {
- // workInProgressRoot 為空,則創(chuàng)建 workInProgress
- if (workInProgressRoot !== root) {
- createWorkInProgress()
- }
- }
- function createWorkInProgress() {
- workInProgressRoot = root
- var current = root.current
- var workInProgress = current.alternate;
- if (workInProgress === null) {
- // 第一次構(gòu)建,需要創(chuàng)建副本
- workInProgress = createFiber(current.tag)
- workInProgress.alternate = current
- current.alternate = workInProgress
- } else {
- // 更新過程可以復(fù)用
- workInProgress.nextEffect = null
- workInProgress.firstEffect = null
- workInProgress.lastEffect = null
- }
- }
開始更新時,如果 workInProgress 為空會指向一個新的空 Fiber 節(jié)點,表示正在進(jìn)行工作的 Fiber 節(jié)點。
- workInProgress.alternate = current
- current.alternate = workInProgress
fiber tree
構(gòu)造好 workInProgress 之后,就會開始在新的 RootFiber 下生成新的子 Fiber 節(jié)點了。
- function renderRootConcurrent(root) {
- // 構(gòu)造 workInProgress...
- // workInProgress.alternate = current
- // current.alternate = workInProgress
- // 進(jìn)入遍歷 fiber 樹的流程
- workLoopConcurrent()
- }
- function workLoopConcurrent() {
- while (workInProgress !== null && !shouldYield()) {
- performUnitOfWork()
- }
- }
- function performUnitOfWork() {
- var current = workInProgress.alternate
- // 返回當(dāng)前 Fiber 的 child
- const next = beginWork(current, workInProgress)
- // 省略后續(xù)代碼...
- }
按照我們前面的案例, workLoopConcurrent 調(diào)用完成后,最后得到的 fiber 樹如下:
- class App extends React.Component {
- state = { val: 0 }
- render() {
- return
val: { this.state.val }- }
- }
fiber tree
最后進(jìn)入 Commit 階段的時候,會切換 FiberRootNode 的 current 屬性:
- function performConcurrentWorkOnRoot() {
- renderRootConcurrent() // 結(jié)束遍歷流程,fiber tree 已經(jīng)構(gòu)造完畢
- var finishedWork = root.current.alternate
- root.finishedWork = finishedWork
- commitRoot(root)
- }
- function commitRoot() {
- var finishedWork = root.finishedWork
- root.finishedWork = null
- root.current = finishedWork // 切換到新的 fiber 樹
- }
fiber tree
上面的流程為第一次渲染,通過 setState({ val: 1 }) 更新時,workInProgress 會切換到 root.current.alternate。
- function createWorkInProgress() {
- workInProgressRoot = root
- var current = root.current
- var workInProgress = current.alternate;
- if (workInProgress === null) {
- // 第一次構(gòu)建,需要創(chuàng)建副本
- workInProgress = createFiber(current.tag)
- workInProgress.alternate = current
- current.alternate = workInProgress
- } else {
- // 更新過程可以復(fù)用
- workInProgress.nextEffect = null
- workInProgress.firstEffect = null
- workInProgress.lastEffect = null
- }
- }
fiber tree
在后續(xù)的遍歷過程中(workLoopConcurrent()),會在舊的 RootFiber 下構(gòu)建一個新的 fiber tree,并且每個 fiber 節(jié)點的 alternate 都會指向 current fiber tree 下的節(jié)點。
fiber tree
這樣 FiberRootNode 的 current 屬性就會輪流在兩棵 fiber tree 不停的切換,即達(dá)到了緩存的目的,也不會過分的占用內(nèi)存。
更新隊列
在 React 15 里,多次 setState 會被放到一個隊列中,等待一次更新。
- // setState 方法掛載到原型鏈上
- ReactComponent.prototype.setState = function (partialState, callback) {
- // 調(diào)用 setState 后,會調(diào)用內(nèi)部的 updater.enqueueSetState
- this.updater.enqueueSetState(this, partialState)
- };
- var ReactUpdateQueue = {
- enqueueSetState(component, partialState) {
- // 在組件的 _pendingStateQueue 上暫存新的 state
- if (!component._pendingStateQueue) {
- component._pendingStateQueue = []
- }
- // 將 setState 的值放入隊列中
- var queue = component._pendingStateQueue
- queue.push(partialState)
- enqueueUpdate(component)
- }
- }
同樣在 Fiber 架構(gòu)中,也會有一個隊列用來存放 setState 的值。每個 Fiber 節(jié)點都有一個 updateQueue 屬性,這個屬性就是用來緩存 setState 值的,只是結(jié)構(gòu)從 React 15 的數(shù)組變成了鏈表結(jié)構(gòu)。
無論是首次 Render 的 Mount 階段,還是 setState 的 Update 階段,內(nèi)部都會調(diào)用 enqueueUpdate 方法。
- // --- Render 階段 ---
- function initializeUpdateQueue(fiber) {
- var queue = {
- baseState: fiber.memoizedState,
- firstBaseUpdate: null,
- lastBaseUpdate: null,
- shared: {
- pending: null
- },
- effects: null
- }
- fiber.updateQueue = queue
- }
- ReactDOMRoot.prototype.render = function render(children) {
- var root = this._internalRoot
- var update = createUpdate()
- update.payload = { element: children }
- const rootFiber = root.current
- // 初始化 rootFiber 的 updateQueue
- initializeUpdateQueue(rootFiber)
- // update 對象放到 rootFiber 的 updateQueue 中
- enqueueUpdate(rootFiber, update)
- // 開始更新流程
- scheduleUpdateOnFiber(rootFiber)
- }
- // --- Update 階段 ---
- Component.prototype.setState = function (partialState, callback) {
- this.updater.enqueueSetState(this, partialState)
- }
- var classComponentUpdater = {
- enqueueSetState: function (inst, payload) {
- // 獲取實例對應(yīng)的fiber
- var fiber = get(inst)
- var update = createUpdate()
- update.payload = payload
- // update 對象放到 rootFiber 的 updateQueue 中
- enqueueUpdate(fiber, update)
- scheduleUpdateOnFiber(fiber)
- }
- }
enqueueUpdate 方法的主要作用就是將 setState 的值掛載到 Fiber 節(jié)點上。
- function enqueueUpdate(fiber, update) {
- var updateQueue = fiber.updateQueue;
- if (updateQueue === null) {
- // updateQueue 為空則跳過
- return;
- }
- var sharedQueue = updateQueue.shared;
- var pending = sharedQueue.pending;
- if (pending === null) {
- update.next = update;
- } else {
- update.next = pending.next;
- pending.next = update;
- }
- sharedQueue.pending = update;
- }
多次 setState 會在 sharedQueue.pending 上形成一個單向循環(huán)鏈表,具體例子更形象的展示下這個鏈表結(jié)構(gòu)。
- class App extends React.Component {
- state = { val: 0 }
- click () {
- for (let i = 0; i < 3; i++) {
- this.setState({ val: this.state.val + 1 })
- }
- }
- render() {
- return
{- this.click()
- }}>val: { this.state.val }
- }
- }
點擊 div 之后,會連續(xù)進(jìn)行三次 setState,每次 setState 都會更新 updateQueue。
第一次 setState
第二次 setState
第三次 setState
更新過程中,我們遍歷下 updateQueue 鏈表,可以看到結(jié)果與預(yù)期的一致。
- let $pending = sharedQueue.pending
- // 遍歷鏈表,在控制臺輸出 payload
- while($pending) {
- console.log('update.payload', $pending.payload)
- $pending = $pending.next
- }
鏈表數(shù)據(jù)
遞歸 Fiber 節(jié)點
Fiber 架構(gòu)下每個節(jié)點都會經(jīng)歷遞(beginWork)和歸(completeWork)兩個過程:
- beginWork:生成新的 state,調(diào)用 render 創(chuàng)建子節(jié)點,連接當(dāng)前節(jié)點與子節(jié)點;
- completeWork:依據(jù) EffectTag 收集 Effect,構(gòu)造 Effect List;
先回顧下這個流程:
- function workLoopConcurrent() {
- while (workInProgress !== null && !shouldYield()) {
- performUnitOfWork()
- }
- }
- function performUnitOfWork() {
- var current = workInProgress.alternate
- // 返回當(dāng)前 Fiber 的 child
- const next = beginWork(current, workInProgress)
- if (next === null) { // child 不存在
- completeUnitOfWork()
- } else { // child 存在
- // 重置 workInProgress 為 child
- workInProgress = next
- }
- }
- function completeUnitOfWork() {
- // 向上回溯節(jié)點
- let completedWork = workInProgress
- while (completedWork !== null) {
- // 收集副作用,主要是用于標(biāo)記節(jié)點是否需要操作 DOM
- var current = completedWork.alternate
- completeWork(current, completedWork)
- // 省略構(gòu)造 Effect List 過程
- // 獲取 Fiber.sibling
- let siblingFiber = workInProgress.sibling
- if (siblingFiber) {
- // sibling 存在,則跳出 complete 流程,繼續(xù) beginWork
- workInProgress = siblingFiber
- return
- }
- completedWork = completedWork.return
- workInProgress = completedWork
- }
- }
遞(beginWork)
先看看 beginWork 進(jìn)行了哪些操作:
- function beginWork(current, workInProgress) {
- if (current !== null) { // current 不為空,表示需要進(jìn)行 update
- var oldProps = current.memoizedProps // 原先傳入的 props
- var newProps = workInProgress.pendingProps // 更新過程中新的 props
- // 組件的 props 發(fā)生變化,或者 type 發(fā)生變化
- if (oldProps !== newProps || workInProgress.type !== current.type) {
- // 設(shè)置更新標(biāo)志位為 true
- didReceiveUpdate = true
- }
- } else { // current 為空表示首次加載,需要進(jìn)行 mount
- didReceiveUpdate = false
- }
- // tag 表示組件類型,不用類型的組件調(diào)用不同方法獲取 child
- switch(workInProgress.tag) {
- // 函數(shù)組件
- case FunctionComponent:
- return updateFunctionComponent(current, workInProgress, newProps)
- // Class組件
- case ClassComponent:
- return updateClassComponent(current, workInProgress, newProps)
- // DOM 原生組件(div、span、button……)
- case HostComponent:
- return updateHostComponent(current, workInProgress)
- // DOM 文本組件
- case HostText:
- return updateHostText(current, workInProgress)
- }
- }
首先判斷 current(即:workInProgress.alternate) 是否存在,如果存在表示需要更新,不存在就是首次加載,didReceiveUpdate 變量設(shè)置為 false,didReceiveUpdate 變量用于標(biāo)記是否需要調(diào)用 render 新建 fiber.child,如果為 false 就會重新構(gòu)建fiber.child,否則復(fù)用之前的 fiber.child。
然后會依據(jù) workInProgress.tag 調(diào)用不同的方法構(gòu)建 fiber.child。關(guān)于 workInProgress.tag 的含義可以參考 react/packages/shared/ReactWorkTags.js,主要是用來區(qū)分每個節(jié)點各自的類型,下面是常用的幾個:
- var FunctionComponent = 0; // 函數(shù)組件
- var ClassComponent = 1; // Class組件
- var HostComponent = 5; // 原生組件
- var HostText = 6; // 文本組件
調(diào)用的方法不一一展開講解,我們只看看 updateClassComponent:
- // 更新 class 組件
- function updateClassComponent(current, workInProgress, newProps) {
- // 更新 state,省略了一萬行代碼,只保留了核心邏輯,看看就好
- var oldState = workInProgress.memoizedState
- var newState = oldState
- var queue = workInProgress.updateQueue
- var pendingQueue = queue.shared.pending
- var firstUpdate = pendingQueue
- var update = pendingQueue
- do {
- // 合并 state
- var partialState = update.payload
- newState = Object.assign({}, newState, partialState)
- // 鏈表遍歷完畢
- update = update.next
- if (update === firstUpdate) {
- // 鏈表遍歷完畢
- queue.shared.pending = null
- break
- }
- } while (true)
- workInProgress.memoizedState = newState // state 更新完畢
- // 檢測 oldState 和 newState 是否一致,如果一致,跳過更新
- // 調(diào)用 componentWillUpdate 判斷是否需要更新
- var instance = workInProgress.stateNode
- instance.props = newProps
- instance.state = newState
- // 調(diào)用 Component 實例的 render
- var nextChildren = instance.render()
- reconcileChildren(current, workInProgress, nextChildren)
- return workInProgress.child
- }
首先遍歷了之前提到的 updateQueue 更新 state,然后就是判斷 state 是否更新,以此來推到組件是否需要更新(這部分代碼省略了),最后調(diào)用的組件 render 方法生成子組件的虛擬 DOM。最后的 reconcileChildren 就是依據(jù) render 的返回值來生成 fiber 節(jié)點并掛載到 workInProgress.child 上。
- // 構(gòu)造子節(jié)點
- function reconcileChildren(current, workInProgress, nextChildren) {
- if (current === null) {
- workInProgress.child = mountChildFibers(
- workInProgress, null, nextChildren
- )
- } else {
- workInProgress.child = reconcileChildFibers(
- workInProgress, current.child, nextChildren
- )
- }
- }
- // 兩個方法本質(zhì)上一樣,只是一個需要生成新的 fiber,一個復(fù)用之前的
- var reconcileChildFibers = ChildReconciler(true)
- var mountChildFibers = ChildReconciler(false)
- function ChildReconciler(shouldTrackSideEffects) {
- return function (returnFiber, currentChild, nextChildren) {
- // 不同類型進(jìn)行不同的處理
- // 返回對象
- if (typeof newChild === 'object' && newChild !== null) {
- return placeSingleChild(
- reconcileSingleElement(
- returnFiber, currentChild, newChild
- )
- )
- }
- // 返回數(shù)組
- if (Array.isArray(newChild)) {
- // ...
- }
- // 返回字符串或數(shù)字,表明是文本節(jié)點
- if (
- typeof newChild === 'string' ||
- typeof newChild === 'number'
- ) {
- // ...
- }
- // 返回 null,直接刪除節(jié)點
- return deleteRemainingChildren(returnFiber, currentChild)
- }
- }
篇幅有限,看看 render 返回值為對象的情況(通常情況下,render 方法 return 的如果是 jsx 都會被轉(zhuǎn)化為虛擬 DOM,而虛擬 DOM 必定是對象或數(shù)組):
- if (typeof newChild === 'object' && newChild !== null) {
- return placeSingleChild(
- // 構(gòu)造 fiber,或者是復(fù)用 fiber
- reconcileSingleElement(
- returnFiber, currentChild, newChild
- )
- )
- }
- function placeSingleChild(newFiber) {
- // 更新操作,需要設(shè)置 effectTag
- if (shouldTrackSideEffects && newFiber.alternate === null) {
- newFiber.effectTag = Placement
- }
- return newFiber
- }
歸(completeWork)
當(dāng) fiber.child 為空時,就會進(jìn)入 completeWork 流程。而 completeWork 主要就是收集 beginWork 階段設(shè)置的 effectTag,如果有設(shè)置 effectTag 就表明該節(jié)點發(fā)生了變更, effectTag 的主要類型如下(默認(rèn)為 NoEffect ,表示節(jié)點無需進(jìn)行操作,完整的定義可以參考 react/packages/shared/ReactSideEffectTags.js):
- export const NoEffect = /* */ 0b000000000000000;
- export const PerformedWork = /* */ 0b000000000000001;
- // You can change the rest (and add more).
- export const Placement = /* */ 0b000000000000010;
- export const Update = /* */ 0b000000000000100;
- export const PlacementAndUpdate = /* */ 0b000000000000110;
- export const Deletion = /* */ 0b000000000001000;
- export const ContentReset = /* */ 0b000000000010000;
- export const Callback = /* */ 0b000000000100000;
- export const DidCapture = /* */ 0b000000001000000;
我們看看 completeWork 過程中,具體進(jìn)行了哪些操作:
- function completeWork(current, workInProgress) {
- switch (workInProgress.tag) {
- // 這些組件沒有反應(yīng)到 DOM 的 effect,跳過處理
- case Fragment:
- case MemoComponent:
- case LazyComponent:
- case ContextConsumer:
- case FunctionComponent:
- return null
- // class 組件
- case ClassComponent: {
- // 處理 context
- var Component = workInProgress.type
- if (isContextProvider(Component)) {
- popContext(workInProgress)
- }
- return null
- }
- case HostComponent: {
- // 這里 Fiber 的 props 對應(yīng)的就是 DOM 節(jié)點的 props
- // 例如:id、src、className ……
- var newProps = workInProgress.pendingProps // props
- if (
- current !== null &&
- workInProgress.stateNode != null
- ) { // current 不為空,表示是更新操作
- var type = workInProgress.type
- updateHostComponent(current, workInProgress, type, newProps)
- } else { // current 為空,表示需要渲染 DOM 節(jié)點
- // 實例化 DOM,掛載到 fiber.stateNode
- var instance = createInstance(type, newProps)
- appendAllChildren(instance, workInProgress, false, false);
- workInProgress.stateNode = instance
- }
- return null
- }
- case HostText: {
- var newText = workInProgress.pendingProps // props
- if (current && workInProgress.stateNode != null) {
- var oldText = current.memoizedProps
- // 更新文本節(jié)點
- updateHostText(current, workInProgress, oldText, newText)
- } else {
- // 實例文本節(jié)點
- workInProgress.stateNode = createTextInstance(newText)
- }
- return null
- }
- }
- }
與 beginWork 一樣,completeWork 過程中也會依據(jù) workInProgress.tag 來進(jìn)行不同的處理,其他類型的組件基本可以略過,只用關(guān)注下 HostComponent、HostText,這兩種類型的節(jié)點會反應(yīng)到真實 DOM 中,所以會有所處理。
- updateHostComponent = function (
- current, workInProgress, type, newProps
- ) {
- var oldProps = current.memoizedProps
- if (oldProps === newProps) {
- // 新舊 props 無變化
- return
- }
- var instance = workInProgress.stateNode // DOM 實例
- // 對比新舊 props
- var updatePayload = diffProperties(instance, type, oldProps, newProps)
- // 將發(fā)生變化的屬性放入 updateQueue
- // 注意這里的 updateQueue 不同于 Class 組件對應(yīng)的 fiber.updateQueue
- workInProgress.updateQueue = updatePayload
- };
updateHostComponent 方法最后會通過 diffProperties 方法獲取一個更新隊列,掛載到 fiber.updateQueue 上,這里的 updateQueue 不同于 Class 組件對應(yīng)的 fiber.updateQueue,不是一個鏈表結(jié)構(gòu),而是一個數(shù)組結(jié)構(gòu),用于更新真實 DOM。
下面舉一個例子,修改 App 組件的 state 后,下面的 span 標(biāo)簽對應(yīng)的 data-val、style、children 都會相應(yīng)的發(fā)生修改,同時,在控制臺打印出 updatePayload 的結(jié)果。
- import React from 'react'
- class App extends React.Component {
- state = { val: 1 }
- clickBtn = () => {
- this.setState({ val: this.state.val + 1 })
- }
- render() {
- return (
)- data-val={this.state.val}
- style={{ fontSize: this.state.val * 15 }}
- >
- { this.state.val }
- }
- }
- export default App
console
副作用鏈表
在最后的更新階段,為了不用遍歷所有的節(jié)點,在 completeWork 過程結(jié)束后,會構(gòu)造一個 effectList 連接所有 effectTag 不為 NoEffect 的節(jié)點,在 commit 階段能夠更高效的遍歷節(jié)點。
- function completeUnitOfWork() {
- let completedWork = workInProgress
- while (completedWork !== null) {
- // 調(diào)用 completeWork()...
- // 構(gòu)造 Effect List 過程
- var returnFiber = completedWork.return
- if (returnFiber !== null) {
- if (returnFiber.firstEffect === null) {
- returnFiber.firstEffect = completedWork.firstEffect;
- } 分享題目:React 架構(gòu)的演變 - 更新機制
URL標(biāo)題:http://m.5511xx.com/article/djjcpip.html


咨詢
建站咨詢
