vue2從數(shù)據(jù)變化到視圖變化發(fā)布訂閱模式詳解
引言
發(fā)布訂閱者模式是最常見(jiàn)的模式之一,它是一種一對(duì)多的對(duì)應(yīng)關(guān)系,當(dāng)一個(gè)對(duì)象發(fā)生變化時(shí)會(huì)通知依賴他的對(duì)象,接受到通知的對(duì)象會(huì)根據(jù)情況執(zhí)行自己的行為。
假設(shè)有財(cái)經(jīng)報(bào)紙送報(bào)員financialDep,有報(bào)紙閱讀愛(ài)好者a,b,c,那么a,b,c想訂報(bào)紙就告訴financialDep,financialDep依次記錄a,b,c這三個(gè)人的家庭地址,次日,送報(bào)員一大早把報(bào)紙送到a,b,c家門口的郵箱中,a,b,c收到報(bào)紙后都會(huì)認(rèn)認(rèn)真真的打開(kāi)閱讀。隨著時(shí)間的推移,會(huì)有以下幾種場(chǎng)景:
- 有新的訂閱者加入: 有一天d也想訂報(bào)紙了,那么找到financialDep,financialDep把d的家庭地址記錄到a,b,c的后面,次日,為a,b,c,d分別送報(bào)紙。
- 有訂閱者退出了:有一天a要去旅游了,提前給送報(bào)員financialDep打電話取消了訂閱,如果不取消的話,積攢的報(bào)紙就會(huì)溢出小郵箱。
- 有新的報(bào)社開(kāi)業(yè):有一天鎮(zhèn)子又開(kāi)了家體育類的報(bào)館,送報(bào)員是sportDep,b和d也是球類愛(ài)好者,于是在sportDep那里做了登記,sportDep的記錄中就有了b和d。
從上面的例子中可以看出,剛開(kāi)始送報(bào)員financialDep的記錄中有a,b和c,先是d加進(jìn)來(lái)后來(lái)是a離開(kāi),最終financialDep的記錄中有b,c和d。體育類報(bào)館開(kāi)張的時(shí)候,b和d也訂閱了報(bào)紙,sportDep的記錄中就有了b和d。我們發(fā)現(xiàn),c只訂閱了財(cái)經(jīng)類報(bào)刊,而b和d既訂閱了財(cái)經(jīng)類的報(bào)紙也定了財(cái)經(jīng)類的報(bào)刊。
一、發(fā)布訂閱者模式的特點(diǎn)
從以上例子可以發(fā)現(xiàn)特點(diǎn):
- 發(fā)布者可以支持訂閱者的加入
- 發(fā)布者可以支持訂閱者的刪除
- 一個(gè)發(fā)布者可以有多個(gè)訂閱者,一個(gè)訂閱者也可以訂閱多個(gè)發(fā)布者的消息那可能會(huì)有疑問(wèn),有沒(méi)有可能會(huì)有發(fā)布者的刪除,答案是會(huì),但是此時(shí),發(fā)布者已消失,訂閱者再也不會(huì)收到消息,也就不會(huì)與當(dāng)前發(fā)布者相關(guān)的消息誘發(fā)的行為。好比體育類報(bào)館關(guān)停了(發(fā)布者刪除)那么b和d在也不會(huì)收到體育類報(bào)紙(消息),也就不會(huì)再閱讀體育類報(bào)紙(行為)。
二、vue中的發(fā)布訂閱者模式
以上的例子基本就是vue中發(fā)布訂閱者的大體概況,vue
中的發(fā)布者是啥時(shí)候定義的?
在new Vue
實(shí)例化的過(guò)程中會(huì)執(zhí)行this._init
的初始化方法,_init
方法中有方法initState
:
export function initState (vm: Component) { // ... if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } //... }
首先看initData
對(duì)于data
的初始化:
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } // observe data observe(data, true /* asRootData */) }
這里首先獲取data
,如果data
是函數(shù)又會(huì)執(zhí)行getData
方法。然后,獲取methods
和props
中的key
值,如果已經(jīng)定義過(guò)則在開(kāi)發(fā)環(huán)境進(jìn)行控制臺(tái)警告。其中,proxy
的目的是讓訪問(wèn)this[key]
相當(dāng)于訪問(wèn)this._data[key]
。最后,對(duì)數(shù)據(jù)進(jìn)行響應(yīng)式處理 observe(data, true /* asRootData */)
:
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
如果不是對(duì)象或者當(dāng)前值是VNode
的實(shí)例直接返回。如果當(dāng)前當(dāng)前值上有屬性__ob__
并且value.__ob__
是Observer
的實(shí)例,那么說(shuō)明該值已經(jīng)被響應(yīng)式處理過(guò),直接將value.__ob__
賦值給ob
并在最后返回即可。如果滿足else if
中的條件,則可執(zhí)行ob = new Observer(value)
:
/** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */ export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } } /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
Observer
是構(gòu)造函數(shù),通過(guò)對(duì)value
是否是數(shù)組的判斷,分別執(zhí)行observeArray
和walk
,observeArray
會(huì)對(duì)數(shù)組中的元素執(zhí)行observe(items[i])
,即通過(guò)遞歸的方式對(duì)value
樹(shù)進(jìn)行深度遍歷,遞歸的最后都會(huì)執(zhí)行到walk
方法。再看walk
中的defineReactive(obj, keys[i])
方法:
/** * Define a reactive property on an Object. */ export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) dep.notify() } }) }
這里就是vue
響應(yīng)式原理、watcher
訂閱者收集、數(shù)據(jù)變化時(shí)發(fā)布者dep
通知subs
中訂閱者watcher
進(jìn)行相應(yīng)操作的主要流程,new Dep()
實(shí)例化、Object.defineProperty
方法、dep.depend()
訂閱者收集和dep.notify()
是主要的功能。先看發(fā)布者Dep
的實(shí)例化:
1、dep
import type Watcher from './watcher' import { remove } from '../util/index' import config from '../config' let uid = 0 /** * A dep is an observable that can have multiple * directives subscribing to it. */ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { if (Dep.target) { Dep.target.addDep(this) } } notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } // The current target watcher being evaluated. // This is globally unique because only one watcher // can be evaluated at a time. Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) { targetStack.push(target) Dep.target = target } export function popTarget () { targetStack.pop() Dep.target = targetStack[targetStack.length - 1] }
這里的dep
就相當(dāng)于財(cái)經(jīng)或者體育報(bào)館,其中定義了屬性id
和subs
,subs
相當(dāng)于送報(bào)員financialDep手中的筆記本,用來(lái)是用來(lái)記錄訂閱者的數(shù)組。發(fā)布者的消息如何發(fā)給訂閱者,就需要借助Object.defineProperty
:
2、Object.defineProperty
對(duì)于一個(gè)對(duì)象的屬性進(jìn)行訪問(wèn)或者設(shè)置的時(shí)候可以為其設(shè)置get
和set
方法,在其中進(jìn)行相應(yīng)的操作,這也是vue
響應(yīng)式原理的本質(zhì),也是IE低版本瀏覽器不支持vue
框架的原因,因?yàn)镮E低版本瀏覽器不支持Object.defineProperty
方法。
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 當(dāng)訪問(wèn)屬性的時(shí)候,進(jìn)行訂閱者的收集 }, set: function reactiveSetter (newVal) { // 當(dāng)修改屬性的時(shí)候,收到發(fā)布者消息的時(shí)候進(jìn)行相應(yīng)的操作 } })
在vue中訂閱者有computer watcher
計(jì)算屬性、watch watcher
偵聽(tīng)器和render watcher
渲染watcher
。這里先介紹渲染watcher
:
3、watcher
let uid = 0 /** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop 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() } /** * Evaluate the getter, and re-collect dependencies. */ get () { pushTarget(this) let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } popTarget() this.cleanupDeps() } return value } // watcher還有很多其他自定義方法,用的時(shí)候再列舉 }
Watcher
實(shí)例化的最后會(huì)執(zhí)行this.value = this.lazy ? undefined : this.get()
方法,默認(rèn)this.lazy=false
,滿足條件執(zhí)行Watcher
實(shí)例的回調(diào)this.get()
方法。 pushTarget(this)
定義在dep.js文件中,為全局targetStack
中推入當(dāng)前訂閱者,是一種棧的組織方式。Dep.target = target
表示當(dāng)前訂閱者是正在計(jì)算中的訂閱者,全局同一時(shí)間點(diǎn)有且只有一個(gè)。 然后執(zhí)行value = this.getter.call(vm, vm)
,這里的this.getter
就是
updateComponent = () => { vm._update(vm._render(), hydrating) }
進(jìn)行當(dāng)前vue
實(shí)例的渲染,在渲染過(guò)程中會(huì)創(chuàng)建vNode
,進(jìn)而訪問(wèn)數(shù)據(jù)data
中的屬性,進(jìn)入到get方法中
,觸發(fā)dep.depend()
。
4、dep.depend
dep.depend()
是在訪問(wèn)obj[key]
的時(shí)候進(jìn)行執(zhí)行的,在渲染過(guò)程中Dep.target
就是渲染watcher
,條件滿足,執(zhí)行Dep.target.addDep(this)
,即執(zhí)行watcher
中的
/** * Add a dependency to this directive. */ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } }
newDepIds
和depIds
分別表示當(dāng)前訂閱者依賴的當(dāng)前發(fā)布者和舊發(fā)布者id
的Set
集合,newDeps
表示當(dāng)前發(fā)布者實(shí)例的數(shù)組列表。首次渲染時(shí)this.newDepIds
中不包含id
,this.newDepIds
添加了發(fā)布者的id
,this.newDeps
中添加了dep
實(shí)例。同時(shí),this.depIds
中不包含id
,繼而執(zhí)行到dep.addSub(this)
。
addSub (sub: Watcher) { this.subs.push(sub) }
這個(gè)動(dòng)作就表示訂閱者watcher
訂閱了發(fā)布者dep
發(fā)布的消息,當(dāng)前發(fā)布者的subs
數(shù)組中訂閱者數(shù)量+1
,等下次數(shù)據(jù)變化時(shí)發(fā)布者就通過(guò)dep.notify()
的方式進(jìn)行消息通知。
5、dep.notify
notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) { // subs aren't sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } }
const subs = this.subs.slice()
對(duì)訂閱者進(jìn)行淺拷貝,subs.sort((a, b) => a.id - b.id)
按照訂閱者的id
進(jìn)行排序,最后循環(huán)訂閱者,訂閱者觸發(fā)update
方法:
/** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }
this.dirty
表示計(jì)算屬性,這里是false
,this.sync
表示同步,這里是false
,最后會(huì)走到queueWatcher(this)
:
/** * Push a watcher into the watcher queue. * Jobs with duplicate IDs will be skipped unless it's * pushed when the queue is being flushed. */ export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } } }
這里未刷新?tīng)顟B(tài)flushing === false
時(shí)會(huì)在隊(duì)列queue
中推入訂閱者watcher
,如果沒(méi)有在等待狀態(tài)waiting===false
時(shí)執(zhí)行nextTick
將flushSchedulerQueue
的執(zhí)行推入異步隊(duì)列中,等待所有的同步操作執(zhí)行完畢再去按照次序執(zhí)行異步的flushSchedulerQueue
。需要了解nextTick
原理請(qǐng)移步:http://www.dbjr.com.cn/article/261842.htm
/** * Flush both queues and run the watchers. */ function flushSchedulerQueue () { currentFlushTimestamp = getNow() flushing = true let watcher, id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component's user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component's watcher run, // its watchers can be skipped. queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== 'production' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( 'You may have an infinite update loop ' + ( watcher.user ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ), watcher.vm ) break } } } // keep copies of post queues before resetting state const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() // call component updated and activated hooks callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit('flush') } } function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { callHook(vm, 'updated') } } }
這里主要做了四件事:
- 對(duì)隊(duì)列
queue
進(jìn)行排序 - 遍歷執(zhí)行
watcher
的run方法
resetSchedulerState
進(jìn)行重置,清空queue
,并且waiting = flushing = false
進(jìn)行狀態(tài)重置callUpdatedHooks
執(zhí)行callHook(vm, 'updated')
生命周期鉤子函數(shù) 這里的run
是在Watcher
的時(shí)候定義的:
/** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { try { this.cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { this.cb.call(this.vm, value, oldValue) } } } }
active
默認(rèn)為true
,執(zhí)行到const value = this.get()
就開(kāi)始了數(shù)據(jù)變化后的渲染的操作,好比訂閱者收到報(bào)紙后認(rèn)真讀報(bào)一樣。get
方法中,value = this.getter.call(vm, vm)
渲染執(zhí)行完以后,會(huì)通過(guò)popTarget
把targetStack
棧頂?shù)脑匾瞥⑶彝ㄟ^(guò)Dep.target = targetStack[targetStack.length - 1]
修改當(dāng)前執(zhí)行的元素。最后執(zhí)行this.cleanupDeps
:
6、訂閱者取消訂閱
/** * Clean up for dependency collection. */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 }
首先通過(guò)while
的方式循環(huán)舊的this.deps
發(fā)布者的數(shù)組,如果當(dāng)前訂閱者所依賴的發(fā)布者this.newDepIds
中沒(méi)有包含舊的發(fā)布者,那么,就讓發(fā)布者在this.subs
中移除訂閱者,這樣就不會(huì)讓發(fā)布者dep
進(jìn)行額外的通知,這種額外的通知可能會(huì)引起未訂閱者的行為(可能消耗內(nèi)存資源或引起不必要的計(jì)算)。后面的邏輯就是讓新舊發(fā)布者id
和dep
進(jìn)行交換,方便下次發(fā)布者發(fā)布消息后的清除操作。
小結(jié)
vue
中的發(fā)布訂閱者是在借助Object.defineProperty
將數(shù)據(jù)變成響應(yīng)式的過(guò)程中定義了dep
,在get
過(guò)程中dep
對(duì)于訂閱者的加入進(jìn)行處理,在set
修改數(shù)據(jù)的過(guò)程中dep
通知訂閱者進(jìn)行相應(yīng)的操作。
以上就是vue2從數(shù)據(jù)變化到視圖變化發(fā)布訂閱模式詳解的詳細(xì)內(nèi)容,更多關(guān)于vue2數(shù)據(jù)視圖變化發(fā)布訂閱模式的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue awesome swiper異步加載數(shù)據(jù)出現(xiàn)的bug問(wèn)題
這篇文章主要介紹了vue awesome swiper異步加載數(shù)據(jù)出現(xiàn)的bug問(wèn)題,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-07-07解決VUE中document.body.scrollTop為0的問(wèn)題
今天小編就為大家分享一篇解決VUE中document.body.scrollTop為0的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09Vue淺析講解動(dòng)態(tài)組件與緩存組件及異步組件的使用
這篇文章主要介紹了Vue開(kāi)發(fā)中的動(dòng)態(tài)組件與緩存組件及異步組件的使用教程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09Vue router-view和router-link的實(shí)現(xiàn)原理
這篇文章主要介紹了Vue router-view和router-link的實(shí)現(xiàn)原理,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03