效能優化方法有:1、使用「v-slot:slotName」,而不是「slot="slotName"」;2、避免「v-for」和「v-if」同時使用;3、始終為「v-for」新增key,並且不要將index作為的key;4、使用延遲渲染等等。
本教學操作環境:windows7系統、vue2.9.6版,DELL G3電腦。
我們在使用 Vue 或其他框架的日常開發中,或多或少的都會遇到一些效能問題,儘管 Vue 內部已經幫助我們做了許多優化,但是還是有些問題是需要我們主動去避免的。我在我的日常開中,以及網上各種大佬的文章中總結了一些容易產生效能問題的場景以及針對這些問題優化的技巧,這篇文章就來探討下,希望對你有所幫助。
v-slot:slotName
,而不是slot="slotName"
v-slot
是 2.6 新增的語法,具體可檢視:Vue2.6,2.6 釋出已經是快兩年前的事情了,但是現在仍然有不少人仍然在使用slot="slotName"
這個語法。雖然這兩個語法都能達到相同的效果,但是內部的邏輯確實不一樣的,下面來看下這兩種方式有什麼不同之處。
我們先來看下這兩種語法分別會被編譯成什麼:
使用新的寫法,對於父元件中的以下模板:
<child> <template v-slot:name>{{name}}</template> </child>
會被編譯成:
function render() { with (this) { return _c('child', { scopedSlots: _u([ { key: 'name', fn: function () { return [_v(_s(name))] }, proxy: true } ]) }) } }
使用舊的寫法,對於以下模板:
<child> <template slot="name">{{name}}</template> </child>
會被編譯成:
function render() { with (this) { return _c( 'child', [ _c( 'template', { slot: 'name' }, [_v(_s(name))] ) ], ) } }
通過編譯後的程式碼可以發現,舊的寫法是將插槽內容作為 children 渲染的,會在父元件的渲染函數中建立,插槽內容的依賴會被父元件收集(name 的 dep 收集到父元件的渲染 watcher),而新的寫法將插槽內容放在了 scopedSlots 中,會在子元件的渲染函數中呼叫,插槽內容的依賴會被子元件收集(name 的 dep 收集到子元件的渲染 watcher),最終導致的結果就是:當我們修改 name 這個屬性時,舊的寫法是呼叫父元件的更新(呼叫父元件的渲染 watcher),然後在父元件更新過程中呼叫子元件更新(prePatch => updateChildComponent),而新的寫法則是直接呼叫子元件的更新(呼叫子元件的渲染 watcher)。
這樣一來,舊的寫法在更新時就多了一個父元件更新的過程,而新的寫法由於直接更新子元件,就會更加高效,效能更好,所以推薦始終使用v-slot:slotName
語法。
這一點已經被提及很多次了,計算屬性最大的一個特點就是它是可以被快取的,這個快取指的是隻要它的依賴的不發生改變,它就不會被重新求值,再次存取時會直接拿到快取的值,在做一些複雜的計算時,可以極大提升效能。可以看以下程式碼:
<template> <div>{{superCount}}</div> </template> <script> export default { data() { return { count: 1 } }, computed: { superCount() { let superCount = this.count // 假設這裡有個複雜的計算 for (let i = 0; i < 10000; i++) { superCount++ } return superCount } } } </script>
這個例子中,在 created、mounted 以及模板中都存取了 superCount 屬性,這三次存取中,實際上只有第一次即created
時才會對 superCount 求值,由於 count 屬性並未改變,其餘兩次都是直接返回快取的 value。
對於某些元件,如果我們只是用來顯示一些資料,不需要管理狀態,監聽資料等,那麼就可以用函數式元件。函數式元件是無狀態的,無範例的,在初始化時不需要初始化狀態,不需要建立範例,也不需要去處理生命週期等,相比有狀態元件,會更加輕量,同時效能也更好。具體的函數式元件使用方式可參考官方檔案:函數式元件
我們可以寫一個簡單的 demo 來驗證下這個優化:
// UserProfile.vue <template> <div class="user-profile">{{ name }}</div> </template> <script> export default { props: ['name'], data() { return {} }, methods: {} } </script> <style scoped></style> // App.vue <template> <div id="app"> <UserProfile v-for="item in list" :key="item" : /> </div> </template> <script> import UserProfile from './components/UserProfile' export default { name: 'App', components: { UserProfile }, data() { return { list: Array(500) .fill(null) .map((_, idx) => 'Test' + idx) } }, beforeMount() { this.start = Date.now() }, mounted() { console.log('用時:', Date.now() - this.start) } } </script> <style></style>
UserProfile 這個元件只渲染了 props 的 name,然後在 App.vue 中呼叫 500 次,統計從 beforeMount 到 mounted 的耗時,即為 500 個子元件(UserProfile)初始化的耗時。
經過我多次嘗試後,發現耗時一直在 30ms 左右,那麼現在我們再把改成 UserProfile 改成函數式元件:
<template functional> <div class="user-profile">{{ props.name }}</div> </template>
此時再經過多次嘗試後,初始化的耗時一直在 10-15ms,這些足以說明函數式元件比有狀態元件有著更好的效能。
以下是兩個使用 v-show 和 v-if 的模板
<template> <div> <UserProfile :user="user1" v-if="visible" /> <button @click="visible = !visible">toggle</button> </div> </template>
<template> <div> <UserProfile :user="user1" v-show="visible" /> <button @click="visible = !visible">toggle</button> </div> </template>
這兩者的作用都是用來控制某些元件或 DOM 的顯示 / 隱藏,在討論它們的效能差異之前,先來分析下這兩者有何不同。其中,v-if 的模板會被編譯成:
function render() { with (this) { return _c( 'div', [ visible ? _c('UserProfile', { attrs: { user: user1 } }) : _e(), _c( 'button', { on: { click: function ($event) { visible = !visible } } }, [_v('toggle')] ) ], ) } }
可以看到,v-if 的部分被轉換成了一個三元表示式,visible 為 true 時,建立一個 UserProfile 的 vnode,否則建立一個空 vnode,在 patch 的時候,新舊節點不一樣,就會移除舊的節點或建立新的節點,這樣的話UserProfile
也會跟著建立 / 銷燬。如果UserProfile
元件裡有很多 DOM,或者要執行很多初始化 / 銷燬邏輯,那麼隨著 visible 的切換,勢必會浪費掉很多效能。這個時候就可以用 v-show 進行優化,我們來看下 v-show 編譯後的程式碼:
function render() { with (this) { return _c( 'div', [ _c('UserProfile', { directives: [ { name: 'show', rawName: 'v-show', value: visible, expression: 'visible' } ], attrs: { user: user1 } }), _c( 'button', { on: { click: function ($event) { visible = !visible } } }, [_v('toggle')] ) ], ) } }
v-show
被編譯成了directives
,實際上,v-show 是一個 Vue 內部的指令,在這個指令的程式碼中,主要執行了以下邏輯:
el.style.display = value ? el.__vOriginalDisplay : 'none'
它其實是通過切換元素的 display 屬性來控制的,和 v-if 相比,不需要在 patch 階段建立 / 移除節點,只是根據v-show
上繫結的值來控制 DOM 元素的style.display
屬性,在頻繁切換的場景下就可以節省很多效能。
但是並不是說v-show
可以在任何情況下都替換v-if
,如果初始值是false
時,v-if
並不會建立隱藏的節點,但是v-show
會建立,並通過設定style.display='none'
來隱藏,雖然外表看上去這個 DOM 都是被隱藏的,但是v-show
已經完整的走了一遍建立的流程,造成了效能的浪費。
所以,v-if
的優勢體現在初始化時,v-show
體現在更新時,當然並不是要求你絕對按照這個方式來,比如某些元件初始化時會請求資料,而你想先隱藏元件,然後在顯示時能立刻看到資料,這時候就可以用v-show
,又或者你想每次顯示這個元件時都是最新的資料,那麼你就可以用v-if
,所以我們要結合具體業務場景去選一個合適的方式。
在動態元件的場景下:
<template> <div> <component :is="currentComponent" /> </div> </template>
這個時候有多個元件來回切換,currentComponent
每變一次,相關的元件就會銷燬 / 建立一次,如果這些元件比較複雜的話,就會造成一定的效能壓力,其實我們可以使用 keep-alive 將這些元件快取起來:
<template> <div> <keep-alive> <component :is="currentComponent" /> </keep-alive> </div> </template>
keep-alive
的作用就是將它包裹的元件在第一次渲染後就快取起來,下次需要時就直接從快取裡面取,避免了不必要的效能浪費,在討論上個問題時,說的是v-show
初始時效能壓力大,因為它要建立所有的元件,其實可以用keep-alive
優化下:
<template> <div> <keep-alive> <UserProfileA v-if="visible" /> <UserProfileB v-else /> </keep-alive> </div> </template>
這樣的話,初始化時不會渲染UserProfileB
元件,當切換visible
時,才會渲染UserProfileB
元件,同時被keep-alive
快取下來,頻繁切換時,由於是直接從快取中取,所以會節省很多效能,所以這種方式在初始化和更新時都有較好的效能。
但是keep-alive
並不是沒有缺點,元件被快取時會佔用記憶體,屬於空間和時間上的取捨,在實際開發中要根據場景選擇合適的方式。
這一點是 Vue 官方的風格指南中明確指出的一點:Vue 風格指南
如以下模板:
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id"> {{ user.name }} </li> </ul>
會被編譯成:
// 簡化版 function render() { return _c( 'ul', this.users.map((user) => { return user.isActive ? _c( 'li', { key: user.id }, [_v(_s(user.name))] ) : _e() }), ) }
可以看到,這裡是先遍歷(v-for),再判斷(v-if),這裡有個問題就是:如果你有一萬條資料,其中只有 100 條是isActive
狀態的,你只希望顯示這 100 條,但是實際在渲染時,每一次渲染,這一萬條資料都會被遍歷一遍。比如你在這個元件內的其他地方改變了某個響應式資料時,會觸發重新渲染,呼叫渲染函數,呼叫渲染函數時,就會執行到上面的程式碼,從而將這一萬條資料遍歷一遍,即使你的users
沒有發生任何改變。
為了避免這個問題,在此場景下你可以用計算屬性代替:
<template> <div> <ul> <li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li> </ul> </div> </template> <script> export default { // ... computed: { activeUsers() { return this.users.filter((user) => user.isActive) } } } </script> 複製程式碼
這樣只會在users
發生改變時才會執行這段遍歷的邏輯,和之前相比,避免了不必要的效能浪費。
這一點是 Vue 風格指南中明確指出的一點,同時也是面試時常問的一點,很多人都習慣的將 index 作為 key,這樣其實是不太好的,index 作為 key 時,將會讓 diff 演演算法產生錯誤的判斷,從而帶來一些效能問題,你可以看下 ssh 大佬的文章,深入分析下,為什麼 Vue 中不要用 index 作為 key。在這裡我也通過一個例子來簡單說明下當 index 作為 key 時是如何影響效能的。
看下這個例子:
const Item = { name: 'Item', props: ['message', 'color'], render(h) { debugger console.log('執行了Item的render') return h('div', { style: { color: this.color } }, [this.message]) } } new Vue({ name: 'Parent', template: ` <div @click="reverse" class="list"> <Item v-for="(item,index) in list" :key="item.id" :message="item.message" :color="item.color" /> </div>`, components: { Item }, data() { return { list: [ { id: 'a', color: '#f00', message: 'a' }, { id: 'b', color: '#0f0', message: 'b' } ] } }, methods: { reverse() { this.list.reverse() } } }).$mount('#app')
這裡有一個 list,會渲染出來a b
,點選後會執行reverse
方法將這個 list 顛倒下順序,你可以將這個例子複製下來,在自己的電腦上看下效果。
我們先來分析用id
作為 key 時,點選時會發生什麼,
由於 list 發生了改變,會觸發Parent
元件的重新渲染,拿到新的vnode
,和舊的vnode
去執行patch
,我們主要關心的就是patch
過程中的updateChildren
邏輯,updateChildren
就是對新舊兩個children
執行diff
演演算法,使盡可能地對節點進行復用,對於我們這個例子而言,此時舊的children
是:
;[ { tag: 'Item', key: 'a', propsData: { color: '#f00', message: '紅色' } }, { tag: 'Item', key: 'b', propsData: { color: '#0f0', message: '綠色' } } ]
執行reverse
後的新的children
是:
;[ { tag: 'Item', key: 'b', propsData: { color: '#0f0', message: '綠色' } }, { tag: 'Item', key: 'a', propsData: { color: '#f00', message: '紅色' } } ]
此時執行updateChildren
,updateChildren
會對新舊兩組 children 節點的迴圈進行對比:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 對新舊節點執行patchVnode // 移動指標 } else if (sameVnode(oldEndVnode, newEndVnode)) { // 對新舊節點執行patchVnode // 移動指標 } else if (sameVnode(oldStartVnode, newEndVnode)) { // 對新舊節點執行patchVnode // 移動oldStartVnode節點 // 移動指標 } else if (sameVnode(oldEndVnode, newStartVnode)) { // 對新舊節點執行patchVnode // 移動oldEndVnode節點 // 移動指標 } else { //... } }
通過sameVnode
判斷兩個節點是相同節點的話,就會執行相應的邏輯:
function sameVnode(a, b) { return ( a.key === b.key && ((a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b)) || (isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error))) ) }
sameVnode
主要就是通過 key 去判斷,由於我們顛倒了 list 的順序,所以第一輪對比中:sameVnode(oldStartVnode, newEndVnode)
成立,即舊的首節點和新的尾節點是同一個節點,此時會執行patchVnode
邏輯,patchVnode
中會執行prePatch
,prePatch
中會更新 props,此時我們的兩個節點的propsData
是相同的,都為{color: '#0f0',message: '綠色'}
,這樣的話Item
元件的 props 就不會更新,Item
也不會重新渲染。再回到updateChildren
中,會繼續執行"移動oldStartVnode節點"
的操作,將 DOM 元素。移動到正確位置,其他節點對比也是同樣的流程。
可以發現,在整個流程中,只是移動了節點,並沒有觸發 Item 元件的重新渲染,這樣實現了節點的複用。
我們再來看下使用index
作為 key 的情況,使用index
時,舊的children
是:
;[ { tag: 'Item', key: 0, propsData: { color: '#f00', message: '紅色' } }, { tag: 'Item', key: 1, propsData: { color: '#0f0', message: '綠色' } } ]
執行reverse
後的新的children
是:
;[ { tag: 'Item', key: 0, propsData: { color: '#0f0', message: '綠色' } }, { tag: 'Item', key: 1, propsData: { color: '#f00', message: '紅色' } } ]
這裡和id
作為 key 時的節點就有所不同了,雖然我們把 list 順序顛倒了,但是 key 的順序卻沒變,在updateChildren
時sameVnode(oldStartVnode, newStartVnode)
將會成立,即舊的首節點和新的首節點相同,此時執行patchVnode -> prePatch -> 更新props
,這個時候舊的 propsData 是{color: '#f00',message: '紅色'}
,新的 propsData 是{color: '#0f0',message: '綠色'}
,更新過後,Item 的 props 將會發生改變,會觸發 Item 元件的重新渲染。
這就是 index 作為 key 和 id 作為 key 時的區別,id 作為 key 時,僅僅是移動了節點,並沒有觸發 Item 的重新渲染。index 作為 key 時,觸發了 Item 的重新渲染,可想而知,當 Item 是一個複雜的元件時,必然會引起效能問題。
上面的流程比較複雜,涉及的也比較多,可以拆開寫好幾篇文章,有些地方我只是簡略的說了一下,如果你不是很明白的話,你可以把上面的例子複製下來,在自己的電腦上調式,我在 Item 的渲染函數中加了列印紀錄檔和 debugger,你可以分別用 id 和 index 作為 key 嘗試下,你會發現 id 作為 key 時,Item 的渲染函數沒有執行,但是 index 作為 key 時,Item 的渲染函數執行了,這就是這兩種方式的區別。
延遲渲染就是分批渲染,假設我們某個頁面裡有一些元件在初始化時需要執行復雜的邏輯:
<template> <p> <!-- Heavy元件初始化時需要執行很複雜的邏輯,執行大量計算 --> <Heavy1 /> <Heavy2 /> <Heavy3 /> <Heavy4 /> </p> </template>
這將會佔用很長時間,導致幀數下降、卡頓,其實可以使用分批渲染的方式來進行優化,就是先渲染一部分,再渲染另一部分:
參考黃軼老師揭祕 Vue.js 九個效能優化技巧中的程式碼:
<template> <p> <Heavy v-if="defer(1)" /> <Heavy v-if="defer(2)" /> <Heavy v-if="defer(3)" /> <Heavy v-if="defer(4)" /> </p> </template> <script> export default { data() { return { displayPriority: 0 } }, mounted() { this.runDisplayPriority() }, methods: { runDisplayPriority() { const step = () => { requestAnimationFrame(() => { this.displayPriority++ if (this.displayPriority < 10) { step() } }) } step() }, defer(priority) { return this.displayPriority >= priority } } } </script>
其實原理很簡單,主要是維護displayPriority
變數,通過requestAnimationFrame
在每一幀渲染時自增,然後我們就可以在元件上通過v-if="defer(n)"
使displayPriority
增加到某一值時再渲染,這樣就可以避免 js 執行時間過長導致的卡頓問題了。
在 Vue 元件初始化資料時,會遞迴遍歷在 data 中定義的每一條資料,通過Object.defineProperty
將資料改成響應式,這就意味著如果 data 中的資料量很大的話,在初始化時將會使用很長的時間去執行Object.defineProperty
, 也就會帶來效能問題,這個時候我們可以強制使資料變為非響應式,從而節省時間,看下這個例子:
<template> <p> <ul> <li v-for="item in heavyData" :key="item.id">{{ item.name }}</li> </ul> </p> </template> <script> // 一萬條資料 const heavyData = Array(10000) .fill(null) .map((_, idx) => ({ name: 'test', message: 'test', id: idx })) export default { data() { return { heavyData: heavyData } }, beforeCreate() { this.start = Date.now() }, created() { console.log(Date.now() - this.start) } } </script>
heavyData
中有一萬條資料,這裡統計了下從beforeCreate
到created
經歷的時間,對於這個例子而言,這個時間基本上就是初始化資料的時間。
我在我個人的電腦上多次測試,這個時間一直在40-50ms
,然後我們通過Object.freeze()
方法,將heavyData
變為非響應式的再試下:
//... data() { return { heavyData: Object.freeze(heavyData) } } //...
改完之後再試下,初始化資料的時間變成了0-1ms
,快了有40ms
,這40ms
都是遞迴遍歷heavyData
執行Object.defineProperty
的時間。
那麼,為什麼Object.freeze()
會有這樣的效果呢?對某一物件使用Object.freeze()
後,將不能向這個物件新增新的屬性,不能刪除已有屬性,不能修改該物件已有屬性的可列舉性、可設定性、可寫性,以及不能修改已有屬性的值。
而 Vue 在將資料改造成響應式之前有個判斷:
export function observe(value, asRootData) { // ...省略其他邏輯 if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } // ...省略其他邏輯 }
這個判斷條件中有一個Object.isExtensible(value)
,這個方法是判斷一個物件是否是可延伸的,由於我們使用了Object.freeze()
,這裡肯定就返回了false
,所以就跳過了下面的過程,自然就省了很多時間。
實際上,不止初始化資料時有影響,你可以用上面的例子統計下從created
到mounted
所用的時間,在我的電腦上不使用Object.freeze()
時,這個時間是60-70ms
,使用Object.freeze()
後降到了40-50ms
,這是因為在渲染函數中讀取heavyData
中的資料時,會執行到通過Object.defineProperty
定義的getter
方法,Vue 在這裡做了一些收集依賴的處理,肯定就會佔用一些時間,由於使用了Object.freeze()
後的資料是非響應式的,沒有了收集依賴的過程,自然也就節省了效能。
由於存取響應式資料會走到自定義 getter 中並收集依賴,所以平時使用時要避免頻繁存取響應式資料,比如在遍歷之前先將這個資料存在區域性變數中,尤其是在計算屬性、渲染函數中使用,關於這一點更具體的說明,你可以看黃奕老師的這篇文章:Local variables
但是這樣做也不是沒有任何問題的,這樣會導致heavyData
下的資料都不是響應式資料,你對這些資料使用computed
、watch
等都不會產生效果,不過通常來說這種大量的資料都是展示用的,如果你有特殊的需求,你可以只對這種資料的某一層使用Object.freeze()
,同時配合使用上文中的延遲渲染、函數式元件等,可以極大提升效能。
Vue 專案不僅可以使用 SFC 的方式開發,也可以使用渲染函數或 JSX 開發,很多人認為僅僅是隻是開發方式不同,卻不知這些開發方式之間也有效能差異,甚至差異很大,這一節我就找些例子來說明下,希望你以後在選擇開發方式時有更多衡量的標準。
其實 Vue2 模板編譯中的效能優化不多,Vue3 中有很多,Vue3 通過編譯和執行時結合的方式提升了很大的效能,但是由於本篇文章講的是 Vue2 的效能優化,並且 Vue2 現在還是有很多人在使用,所以我就挑 Vue2 模板編譯中的一點來說下。
下面這個模板:
<p>你好! <span>Hello</span></p>
會被編譯成:
function render() { with (this) { return _m(0) } }
可以看到和普通的渲染函數是有些不一樣的,下面我們來看下為什麼會編譯成這樣的程式碼。
Vue 的編譯會經過optimize
過程,這個過程中會標記靜態節點,具體內容可以看黃奕老師寫的這個檔案:Vue2 編譯 - optimize 標記靜態節點。
在codegen
階段判斷到靜態節點的標記會走到genStatic
的分支:
function genStatic(el, state) { el.staticProcessed = true const originalPreState = state.pre if (el.pre) { state.pre = el.pre } state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) state.pre = originalPreState return `_m(${state.staticRenderFns.length - 1}${ el.staticInFor ? ',true' : '' })` }
這裡就是生成程式碼的關鍵邏輯,這裡會把渲染函數儲存在staticRenderFns
裡,然後拿到當前值的下標生成_m
函數,這就是為什麼我們會得到_m(0)
。
這個_m
其實是renderStatic
的縮寫:
export function renderStatic(index, isInFor) { const cached = this._staticTrees || (this._staticTrees = []) let tree = cached[index] if (tree && !isInFor) { return tree } tree = cached[index] = this.$options.staticRenderFns[index].call( this._renderProxy, null, this ) markStatic(tree, `__static__${index}`, false) return tree } function markStatic(tree, key) { if (Array.isArray(tree)) { for (let i = 0; i < tree.length; i++) { if (tree[i] && typeof tree[i] !== 'string') { markStaticNode(tree[i], `${key}_${i}`, isOnce) } } } else { markStaticNode(tree, key, isOnce) } } function markStaticNode(node, key, isOnce) { node.isStatic = true node.key = key node.isOnce = isOnce }
renderStatic
的內部實現比較簡單,先是獲取到元件範例的_staticTrees
,如果沒有就建立一個,然後嘗試從_staticTrees
上獲取之前快取的節點,獲取到的話就直接返回,否則就從staticRenderFns
上獲取到對應的渲染函數執行並將結果快取到_staticTrees
上,這樣下次再進入這個函數時就會直接從快取上返回結果。
拿到節點後還會通過markStatic
將節點打上isStatic
等標記,標記為isStatic
的節點會直接跳過patchVnode
階段,因為靜態節點是不會變的,所以也沒必要 patch,跳過 patch 可以節省效能。
通過編譯和執行時結合的方式,可以幫助我們很好的提升應用效能,這是渲染函數 / JSX 很難達到的,當然不是說不能用 JSX,相比於模板,JSX 更加靈活,兩者有各自的使用場景。在這裡寫這些是希望能給你提供一些技術選型的標準。
Vue2 的編譯優化除了靜態節點,還有插槽,createElement 等。
【相關推薦:】
以上就是vue有什麼效能優化方法的詳細內容,更多請關注TW511.COM其它相關文章!