1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Vue源碼解析,keep-alive是如何實(shí)現(xiàn)緩存的?

        共 12929字,需瀏覽 26分鐘

         ·

        2020-10-20 19:35

        前言

        在性能優(yōu)化上,最常見的手段就是緩存。對需要經(jīng)常訪問的資源進(jìn)行緩存,減少請求或者是初始化的過程,從而降低時(shí)間或內(nèi)存的消耗。Vue 為我們提供了緩存組件 keep-alive,它可用于路由級別或組件級別的緩存。

        但其中的緩存原理你是否了解,組件緩存渲染又是如何工作。那么本文就來解析 keep-alive 的原理。

        LRU策略

        在使用 keep-alive 時(shí),可以添加 prop 屬性 includeexclude、max 允許組件有條件的緩存。既然有限制條件,舊的組件需要刪除緩存,新的組件就需要加入到最新緩存,那么要如何制定對應(yīng)的策略?

        LRU(Least recently used,最近最少使用)策略根據(jù)數(shù)據(jù)的歷史訪問記錄來進(jìn)行淘汰數(shù)據(jù)。LRU 策略的設(shè)計(jì)原則是,如果一個(gè)數(shù)據(jù)在最近一段時(shí)間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當(dāng)限定的空間已存滿數(shù)據(jù)時(shí),應(yīng)當(dāng)把最久沒有被訪問到的數(shù)據(jù)淘汰。

        1. 現(xiàn)在緩存最大只允許存3個(gè)組件,ABC三個(gè)組件依次進(jìn)入緩存,沒有任何問題
        2. 當(dāng)D組件被訪問時(shí),內(nèi)存空間不足,A是最早進(jìn)入也是最舊的組件,所以A組件從緩存中刪除,D組件加入到最新的位置
        3. 當(dāng)B組件被再次訪問時(shí),由于B還在緩存中,B移動到最新的位置,其他組件相應(yīng)的往后一位
        4. 當(dāng)E組件被訪問時(shí),內(nèi)存空間不足,C變成最久未使用的組件,C組件從緩存中刪除,E組件加入到最新的位置

        keep-alive 緩存機(jī)制便是根據(jù)LRU策略來設(shè)置緩存組件新鮮度,將很久未訪問的組件從緩存中刪除。了解完緩存機(jī)制,接下來進(jìn)入源碼,看看keep-alive組件是如何實(shí)現(xiàn)的。

        組件實(shí)現(xiàn)原理

        //?源碼位置:src/core/components/keep-alive.js
        export?default?{
        ??name:?'keep-alive',
        ??abstract:?true,
        ??props:?{
        ????include:?patternTypes,
        ????exclude:?patternTypes,
        ????max:?[String,?Number]
        ??},
        ??created?()?{
        ????this.cache?=?Object.create(null)
        ????this.keys?=?[]
        ??},
        ??destroyed?()?{
        ????for?(const?key?in?this.cache)?{
        ??????pruneCacheEntry(this.cache,?key,?this.keys)
        ????}
        ??},
        ??mounted?()?{
        ????this.$watch('include',?val?=>?{
        ??????pruneCache(this,?name?=>?matches(val,?name))
        ????})
        ????this.$watch('exclude',?val?=>?{
        ??????pruneCache(this,?name?=>?!matches(val,?name))
        ????})
        ??},
        ??render?()?{
        ????const?slot?=?this.$slots.default
        ????const?vnode:?VNode?=?getFirstComponentChild(slot)
        ????const?componentOptions:??VNodeComponentOptions?=?vnode?&&?vnode.componentOptions
        ????if?(componentOptions)?{
        ??????//?check?pattern
        ??????const?name:??string?=?getComponentName(componentOptions)
        ??????const?{?include,?exclude?}?=?this
        ??????if?(
        ????????//?not?included
        ????????(include?&&?(!name?||?!matches(include,?name)))?||
        ????????//?excluded
        ????????(exclude?&&?name?&&?matches(exclude,?name))
        ??????)?{
        ????????return?vnode
        ??????}

        ??????const?{?cache,?keys?}?=?this
        ??????const?key:??string?=?vnode.key?==?null
        ????????//?same?constructor?may?get?registered?as?different?local?components
        ????????//?so?cid?alone?is?not?enough?(#3269)
        ??????????componentOptions.Ctor.cid?+?(componentOptions.tag???`::${componentOptions.tag}`?:?'')
        ????????:?vnode.key
        ??????if?(cache[key])?{
        ????????vnode.componentInstance?=?cache[key].componentInstance
        ????????//?make?current?key?freshest
        ????????remove(keys,?key)
        ????????keys.push(key)
        ??????}?else?{
        ????????cache[key]?=?vnode
        ????????keys.push(key)
        ????????//?prune?oldest?entry
        ????????if?(this.max?&&?keys.length?>?parseInt(this.max))?{
        ??????????pruneCacheEntry(cache,?keys[0],?keys,?this._vnode)
        ????????}
        ??????}
        ??????vnode.data.keepAlive?=?true
        ????}
        ????return?vnode?||?(slot?&&?slot[0])
        ??}
        }

        kepp-alive 實(shí)際是一個(gè)抽象組件,只對包裹的子組件做處理,并不會和子組件建立父子關(guān)系,也不會作為節(jié)點(diǎn)渲染到頁面上。在組件開頭就設(shè)置 abstracttrue,代表該組件是一個(gè)抽象組件。

        //?源碼位置:src/core/instance/lifecycle.js
        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
        ??//?...
        }

        那么抽象組件是如何忽略這層關(guān)系的呢?在初始化階段會調(diào)用 initLifecycle,里面判斷父級是否為抽象組件,如果是抽象組件,就選取抽象組件的上一級作為父級,忽略與抽象組件和子組件之間的層級關(guān)系。

        回到 keep-alive 組件,組件是沒有編寫template 模板,而是由 render 函數(shù)決定渲染結(jié)果。

        const?slot?=?this.$slots.default
        const?vnode:?VNode?=?getFirstComponentChild(slot)

        如果 keep-alive 存在多個(gè)子元素,keep-alive 要求同時(shí)只有一個(gè)子元素被渲染。所以在開頭會獲取插槽內(nèi)的子元素,調(diào)用 getFirstComponentChild 獲取到第一個(gè)子元素的 VNode。

        //?check?pattern
        const?name:??string?=?getComponentName(componentOptions)
        const?{?include,?exclude?}?=?this
        if?(
        ??//?not?included
        ??(include?&&?(!name?||?!matches(include,?name)))?||
        ??//?excluded
        ??(exclude?&&?name?&&?matches(exclude,?name))
        )?{
        ??return?vnode
        }

        function?matches?(pattern:?string?|?RegExp?|?Array,?name:?string):?boolean?{
        ??if?(Array.isArray(pattern))?{
        ????return?pattern.indexOf(name)?>?-1
        ??}?else?if?(typeof?pattern?===?'string')?{
        ????return?pattern.split(',').indexOf(name)?>?-1
        ??}?else?if?(isRegExp(pattern))?{
        ????return?pattern.test(name)
        ??}
        ??return?false
        }

        接著判斷當(dāng)前組件是否符合緩存條件,組件名與include不匹配或與exclude匹配都會直接退出并返回 VNode,不走緩存機(jī)制。

        const?{?cache,?keys?}?=?this
        const?key:??string?=?vnode.key?==?null
        ??//?same?constructor?may?get?registered?as?different?local?components
        ??//?so?cid?alone?is?not?enough?(#3269)
        ????componentOptions.Ctor.cid?+?(componentOptions.tag???`::${componentOptions.tag}`?:?'')
        ??:?vnode.key
        if?(cache[key])?{
        ??vnode.componentInstance?=?cache[key].componentInstance
        ??//?make?current?key?freshest
        ??remove(keys,?key)
        ??keys.push(key)
        }?else?{
        ??cache[key]?=?vnode
        ??keys.push(key)
        ??//?prune?oldest?entry
        ??if?(this.max?&&?keys.length?>?parseInt(this.max))?{
        ????pruneCacheEntry(cache,?keys[0],?keys,?this._vnode)
        ??}
        }
        vnode.data.keepAlive?=?true

        匹配條件通過會進(jìn)入緩存機(jī)制的邏輯,如果命中緩存,從 cache 中獲取緩存的實(shí)例設(shè)置到當(dāng)前的組件上,并調(diào)整 key 的位置將其放到最后。如果沒命中緩存,將當(dāng)前 VNode 緩存起來,并加入當(dāng)前組件的 key。如果緩存組件的數(shù)量超出 max 的值,即緩存空間不足,則調(diào)用 pruneCacheEntry 將最舊的組件從緩存中刪除,即 keys[0] 的組件。之后將組件的 keepAlive 標(biāo)記為 true,表示它是被緩存的組件。

        function?pruneCacheEntry?(
        ??cache:?VNodeCache,
        ??key:?string,
        ??keys:?Array,
        ??current?:?VNode
        )?
        {
        ??const?cached?=?cache[key]
        ??if?(cached?&&?(!current?||?cached.tag?!==?current.tag))?{
        ????cached.componentInstance.$destroy()
        ??}
        ??cache[key]?=?null
        ??remove(keys,?key)
        }

        pruneCacheEntry 負(fù)責(zé)將組件從緩存中刪除,它會調(diào)用組件 $destroy 方法銷毀組件實(shí)例,緩存組件置空,并移除對應(yīng)的 key。

        mounted?()?{
        ??this.$watch('include',?val?=>?{
        ????pruneCache(this,?name?=>?matches(val,?name))
        ??})
        ??this.$watch('exclude',?val?=>?{
        ????pruneCache(this,?name?=>?!matches(val,?name))
        ??})
        }

        function?pruneCache?(keepAliveInstance:?any,?filter:?Function)?{
        ??const?{?cache,?keys,?_vnode?}?=?keepAliveInstance
        ??for?(const?key?in?cache)?{
        ????const?cachedNode:??VNode?=?cache[key]
        ????if?(cachedNode)?{
        ??????const?name:??string?=?getComponentName(cachedNode.componentOptions)
        ??????if?(name?&&?!filter(name))?{
        ????????pruneCacheEntry(cache,?key,?keys,?_vnode)
        ??????}
        ????}
        ??}
        }

        keep-alivemounted 會監(jiān)聽 includeexclude 的變化,屬性發(fā)生改變時(shí)調(diào)整緩存和 keys 的順序,最終調(diào)用的也是 pruneCacheEntry。

        「小結(jié)」cache 用于緩存組件,keys 存儲組件的 key,根據(jù)LRU策略來調(diào)整緩存組件。keep-aliverender 中最后會返回組件的 VNode,因此我們也可以得出一個(gè)結(jié)論,keep-alive 并非真的不會渲染,而是渲染的對象是包裹的子組件。

        組件渲染流程

        ?

        溫馨提示:這部分內(nèi)容需要對 renderpatch 過程有了解

        ?

        渲染過程最主要的兩個(gè)過程就是 renderpatch,在 render 之前還會有模板編譯,render 函數(shù)就是模板編譯后的產(chǎn)物,它負(fù)責(zé)構(gòu)建 VNode 樹,構(gòu)建好的 VNode 會傳遞給 patch,patch 根據(jù) VNode 的關(guān)系生成真實(shí)dom節(jié)點(diǎn)樹。

        這張圖描述了 Vue 視圖渲染的流程:

        VNode構(gòu)建完成后,最終會被轉(zhuǎn)換成真實(shí)dom,而 patch 是必經(jīng)的過程。為了更好的理解組件渲染的過程,假設(shè) keep-alive 包括的組件有A和B兩個(gè)組件,默認(rèn)展示A組件。


        初始化渲染

        組件在 patch 過程是會執(zhí)行 createComponent 來掛載組件的,A組件也不例外。

        //?源碼位置:src/core/vdom/patch.js
        function?createComponent?(vnode,?insertedVnodeQueue,?parentElm,?refElm)?{
        ??let?i?=?vnode.data
        ??if?(isDef(i))?{
        ????const?isReactivated?=?isDef(vnode.componentInstance)?&&?i.keepAlive
        ????if?(isDef(i?=?i.hook)?&&?isDef(i?=?i.init))?{
        ??????i(vnode,?false?/*?hydrating?*/)
        ????}
        ????//?after?calling?the?init?hook,?if?the?vnode?is?a?child?component
        ????//?it?should've?created?a?child?instance?and?mounted?it.?the?child
        ????//?component?also?has?set?the?placeholder?vnode's?elm.
        ????//?in?that?case?we?can?just?return?the?element?and?be?done.
        ????if?(isDef(vnode.componentInstance))?{
        ??????initComponent(vnode,?insertedVnodeQueue)
        ??????insert(parentElm,?vnode.elm,?refElm)
        ??????if?(isTrue(isReactivated))?{
        ????????reactivateComponent(vnode,?insertedVnodeQueue,?parentElm,?refElm)
        ??????}
        ??????return?true
        ????}
        ??}
        }

        isReactivated 標(biāo)識組件是否重新激活。在初始化渲染時(shí),A組件還沒有初始化構(gòu)造完成,componentInstance 還是 undefined。而A組件的 keepAlivetrue,因?yàn)?keep-alive 作為父級包裹組件,會先于A組件掛載,也就是 kepp-alive 會先執(zhí)行 render 的過程,A組件被緩存起來,之后對插槽內(nèi)第一個(gè)組件(A組件)的 keepAlive 賦值為 true,不記得這個(gè)過程請看上面組件實(shí)現(xiàn)的代碼。所以此時(shí)的 isReactivatedfalse。

        接著會調(diào)用 init 函數(shù)進(jìn)行組件初始化,它是組件的一個(gè)鉤子函數(shù):

        //?源碼位置:src/core/vdom/create-component.js
        const?componentVNodeHooks?=?{
        ??init?(vnode:?VNodeWithData,?hydrating:?boolean):??boolean?{
        ????if?(
        ??????vnode.componentInstance?&&
        ??????!vnode.componentInstance._isDestroyed?&&
        ??????vnode.data.keepAlive
        ????)?{
        ??????//?kept-alive?components,?treat?as?a?patch
        ??????const?mountedNode:?any?=?vnode?//?work?around?flow
        ??????componentVNodeHooks.prepatch(mountedNode,?mountedNode)
        ????}?else?{
        ??????const?child?=?vnode.componentInstance?=?createComponentInstanceForVnode(
        ????????vnode,
        ????????activeInstance
        ??????)
        ??????child.$mount(hydrating???vnode.elm?:?undefined,?hydrating)
        ????}
        ??},
        ??//?...
        }

        createComponentInstanceForVnode 內(nèi)會 new Vue 構(gòu)造組件實(shí)例并賦值到 componentInstance,隨后調(diào)用 $mount 掛載組件。

        createComponent,繼續(xù)走下面的邏輯:

        if?(isDef(vnode.componentInstance))?{
        ??initComponent(vnode,?insertedVnodeQueue)
        ??insert(parentElm,?vnode.elm,?refElm)
        ??if?(isTrue(isReactivated))?{
        ????reactivateComponent(vnode,?insertedVnodeQueue,?parentElm,?refElm)
        ??}
        ??return?true
        }

        調(diào)用 initComponentvnode.elm 賦值為真實(shí)dom,然后調(diào)用 insert 將組件的真實(shí)dom插入到父元素中。

        所以在初始化渲染中,keep-alive 將A組件緩存起來,然后正常的渲染A組件。

        緩存渲染

        當(dāng)切換到B組件,再切換回A組件時(shí),A組件命中緩存被重新激活。

        再次經(jīng)歷 patch 過程,keep-alive 是根據(jù)插槽獲取當(dāng)前的組件,那么插槽的內(nèi)容又是如何更新實(shí)現(xiàn)緩存?

        const?isRealElement?=?isDef(oldVnode.nodeType)
        if?(!isRealElement?&&?sameVnode(oldVnode,?vnode))?{
        ??//?patch?existing?root?node
        ??patchVnode(oldVnode,?vnode,?insertedVnodeQueue,?null,?null,?removeOnly)
        }

        非初始化渲染時(shí),patch 會調(diào)用 patchVnode 對比新舊節(jié)點(diǎn)。

        //?源碼位置:src/core/vdom/patch.js
        function?patchVnode?(
        ??oldVnode,
        ??vnode,
        ??insertedVnodeQueue,
        ??ownerArray,
        ??index,
        ??removeOnly
        )?
        {
        ??//?...
        ??let?i
        ??const?data?=?vnode.data
        ??if?(isDef(data)?&&?isDef(i?=?data.hook)?&&?isDef(i?=?i.prepatch))?{
        ????i(oldVnode,?vnode)
        ??}
        ??//?...
        }

        patchVnode 內(nèi)會調(diào)用鉤子函數(shù) prepatch

        //?源碼位置:src/core/vdom/create-component.js
        prepatch?(oldVnode:?MountedComponentVNode,?vnode:?MountedComponentVNode)?{
        ??const?options?=?vnode.componentOptions
        ??const?child?=?vnode.componentInstance?=?oldVnode.componentInstance
        ??updateChildComponent(
        ????child,
        ????options.propsData,?//?updated?props
        ????options.listeners,?//?updated?listeners
        ????vnode,?//?new?parent?vnode
        ????options.children?//?new?children
        ??)
        },

        updateChildComponent 就是更新的關(guān)鍵方法,它里面主要是更新實(shí)例的一些屬性:

        //?源碼位置:src/core/instance/lifecycle.js
        export?function?updateChildComponent?(
        ??vm:?Component,
        ??propsData:??Object,
        ??listeners:??Object,
        ??parentVnode:?MountedComponentVNode,
        ??renderChildren:??Array
        )?
        {
        ??//?...

        ??//?Any?static?slot?children?from?the?parent?may?have?changed?during?parent's
        ??//?update.?Dynamic?scoped?slots?may?also?have?changed.?In?such?cases,?a?forced
        ??//?update?is?necessary?to?ensure?correctness.
        ??const?needsForceUpdate?=?!!(
        ????renderChildren?||???????????????//?has?new?static?slots
        ????vm.$options._renderChildren?||??//?has?old?static?slots
        ????hasDynamicScopedSlot
        ??)
        ??
        ??//?...
        ??
        ??//?resolve?slots?+?force?update?if?has?children
        ??if?(needsForceUpdate)?{
        ????vm.$slots?=?resolveSlots(renderChildren,?parentVnode.context)
        ????vm.$forceUpdate()
        ??}
        }

        Vue.prototype.$forceUpdate?=?function?()?{
        ??const?vm:?Component?=?this
        ??if?(vm._watcher)?{
        ????//?這里最終會執(zhí)行?vm._update(vm._render)
        ????vm._watcher.update()
        ??}
        }

        從注釋中可以看到 needsForceUpdate 是有插槽才會為 truekeep-alive 符合條件。首先調(diào)用 resolveSlots 更新 keep-alive 的插槽,然后調(diào)用 $forceUpdatekeep-alive 重新渲染,再走一遍 render。因?yàn)锳組件在初始化已經(jīng)緩存了,keep-alive 直接返回緩存好的A組件 VNode。VNode 準(zhǔn)備好后,又來到了 patch 階段。

        function?createComponent?(vnode,?insertedVnodeQueue,?parentElm,?refElm)?{
        ??let?i?=?vnode.data
        ??if?(isDef(i))?{
        ????const?isReactivated?=?isDef(vnode.componentInstance)?&&?i.keepAlive
        ????if?(isDef(i?=?i.hook)?&&?isDef(i?=?i.init))?{
        ??????i(vnode,?false?/*?hydrating?*/)
        ????}
        ????//?after?calling?the?init?hook,?if?the?vnode?is?a?child?component
        ????//?it?should've?created?a?child?instance?and?mounted?it.?the?child
        ????//?component?also?has?set?the?placeholder?vnode's?elm.
        ????//?in?that?case?we?can?just?return?the?element?and?be?done.
        ????if?(isDef(vnode.componentInstance))?{
        ??????initComponent(vnode,?insertedVnodeQueue)
        ??????insert(parentElm,?vnode.elm,?refElm)
        ??????if?(isTrue(isReactivated))?{
        ????????reactivateComponent(vnode,?insertedVnodeQueue,?parentElm,?refElm)
        ??????}
        ??????return?true
        ????}
        ??}
        }

        A組件再次經(jīng)歷 createComponent 的過程,調(diào)用 init

        const?componentVNodeHooks?=?{
        ??init?(vnode:?VNodeWithData,?hydrating:?boolean):??boolean?{
        ????if?(
        ??????vnode.componentInstance?&&
        ??????!vnode.componentInstance._isDestroyed?&&
        ??????vnode.data.keepAlive
        ????)?{
        ??????//?kept-alive?components,?treat?as?a?patch
        ??????const?mountedNode:?any?=?vnode?//?work?around?flow
        ??????componentVNodeHooks.prepatch(mountedNode,?mountedNode)
        ????}?else?{
        ??????const?child?=?vnode.componentInstance?=?createComponentInstanceForVnode(
        ????????vnode,
        ????????activeInstance
        ??????)
        ??????child.$mount(hydrating???vnode.elm?:?undefined,?hydrating)
        ????}
        ??},
        }

        這時(shí)將不再走 $mount 的邏輯,只調(diào)用 prepatch 更新實(shí)例屬性。所以在緩存組件被激活時(shí),不會執(zhí)行 createdmounted 的生命周期函數(shù)。

        回到 createComponent,此時(shí)的 isReactivatedtrue,調(diào)用 reactivateComponent:

        function?reactivateComponent?(vnode,?insertedVnodeQueue,?parentElm,?refElm)?{
        ??let?i
        ??//?hack?for?#4339:?a?reactivated?component?with?inner?transition
        ??//?does?not?trigger?because?the?inner?node's?created?hooks?are?not?called
        ??//?again.?It's?not?ideal?to?involve?module-specific?logic?in?here?but
        ??//?there?doesn't?seem?to?be?a?better?way?to?do?it.
        ??let?innerNode?=?vnode
        ??while?(innerNode.componentInstance)?{
        ????innerNode?=?innerNode.componentInstance._vnode
        ????if?(isDef(i?=?innerNode.data)?&&?isDef(i?=?i.transition))?{
        ??????for?(i?=?0;?i?????????cbs.activate[i](emptyNode,?innerNode)
        ??????}
        ??????insertedVnodeQueue.push(innerNode)
        ??????break
        ????}
        ??}
        ??//?unlike?a?newly?created?component,
        ??//?a?reactivated?keep-alive?component?doesn't?insert?itself
        ??insert(parentElm,?vnode.elm,?refElm)
        }

        最后調(diào)用 insert 插入組件的dom節(jié)點(diǎn),至此緩存渲染流程完成。

        「小結(jié)」:組件首次渲染時(shí),keep-alive 會將組件緩存起來。等到緩存渲染時(shí),keep-alive 會更新插槽內(nèi)容,之后 $forceUpdate 重新渲染。這樣在 render 時(shí)就獲取到最新的組件,如果命中緩存則從緩存中返回 VNode。

        總結(jié)

        keep-alive 組件是抽象組件,在對應(yīng)父子關(guān)系時(shí)會跳過抽象組件,它只對包裹的子組件做處理,主要是根據(jù)LRU策略緩存組件 VNode,最后在 render 時(shí)返回子組件的 VNode。緩存渲染過程會更新 keep-alive 插槽,重新再 render 一次,從緩存中讀取之前的組件 VNode 實(shí)現(xiàn)狀態(tài)緩存。


        瀏覽 51
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評論
        圖片
        表情
        推薦
        點(diǎn)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            性色av一区二区三区免费看开蚌 | 亚洲三级电影在线观看 | 久久免费在线观看 | 亚洲人成色777777精品音频 | 午夜福利一区二区三区免费 | 人妻噜噜噜人妻 | 奇米影视7777狠狠狠色一岳坶 | 麻豆精产国品一二三产区风险 | 黄色电影视频网址 | 爱草在线 |