Vue(2.6.11)源碼閱讀——擴展

本文目錄:
1. event
2. v-model
3. slot
4. keep-alive
5. transition
6. transition-group

[event]

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

先通過正則把節(jié)點上的元素都解析出來脓钾,并對事件是原生事件還是自定義事件加以區(qū)分。然后把所有的事件用 vm._events 存儲起來. on 就是往_events 里push腿短,off就是對_events的元素進行刪除仅淑,once就是兩者結(jié)合一下称勋。

[v-model]

v-model其實是一種語法糖,其本質(zhì)利用了父子組件的通信完成的漓糙。

[slot]

普通插槽是在父組件編譯和渲染階段生成 vnodes铣缠,所以數(shù)據(jù)的作用域是父組件實例,子組件渲染的時候直接拿到這些渲染好的 vnodes昆禽。而對于作用域插槽蝗蛙,父組件在編譯和渲染階段并不會直接生成 vnodes,而是在父節(jié)點 vnode 的 data 中保留一個 scopedSlots 對象醉鳖,存儲著不同名稱的插槽以及它們對應的渲染函數(shù)捡硅,只有在編譯和渲染子組件階段才會執(zhí)行這個渲染函數(shù)生成 vnodes,由于是在子組件環(huán)境執(zhí)行的盗棵,所以對應的數(shù)據(jù)作用域是子組件實例壮韭。

[keep-alive]

/* @flow */

import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'

type VNodeCache = { [key: string]: ?VNode };

function getComponentName (opts: ?VNodeComponentOptions): ?string {
  return opts && (opts.Ctor.options.name || opts.tag)
}

function matches (pattern: string | RegExp | Array<string>, 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)
  }
  /* istanbul ignore next */
  return false
}

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)
      }
    }
  }
}

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

const patternTypes: Array<Function> = [String, RegExp, Array]

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])
  }
}

可以看到這個組件設(shè)置了一個內(nèi)置屬性abstract。
雖然文檔上沒有提及纹因,但是可以在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
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

也就是說如果這個組件是抽象組件喷屋,那么這個組件不會建立子組件。

<keep-alive> 直接實現(xiàn)了 render 函數(shù)瞭恰,而不是我們常規(guī)模板的方式屯曹,執(zhí)行 <keep-alive> 組件渲染的時候,就會執(zhí)行到這個 render 函數(shù)惊畏,接下來我們分析一下它的實現(xiàn):

  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])
  }
export function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
  if (Array.isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      const c = children[i]
      if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
        return c
      }
    }
  }
}

可以看到keep-alive組件值對獲取到第一個子組件生效恶耽,因此它的使用場景一般是路由和動態(tài)組件。并且緩存的組件也接受include和exclude的限制颜启。
另外可以看到這里之前緩存過的話就用之前緩存過的實例偷俭,否則僅僅就是做了緩存而已。

另外缰盏,之前提到了涌萤,keep-alive是沒有確立父子關(guān)系的,那么能更新的原因是因為在diff之前執(zhí)行了prepatch函數(shù)口猜,由于keep-alive本身是支持插槽的形葬,那么它就會執(zhí)行forceupdate方法,這就是他能渲染包裹的組件的原因暮的。

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)
    }
  },

  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
    )
  },

  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },

  destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}
  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
      }
    }
  }

從上面的代碼可以看到如果是keep-alive組件,那么不會執(zhí)行mount方法了淌实,直接通過createComponent 去渲染冻辩,同時通過reactivateComponent執(zhí)行keep-alive的生命周期回調(diào)函數(shù)actived和deactived.

transition && transition-group

自動嗅探目標元素是否應用了 CSS 過渡或動畫猖腕,如果是,在恰當?shù)臅r機添加/刪除 CSS 類名恨闪。

如果過渡組件提供了 JavaScript 鉤子函數(shù)倘感,這些鉤子函數(shù)將在恰當?shù)臅r機被調(diào)用。

如果沒有找到 JavaScript 鉤子并且也沒有檢測到 CSS 過渡/動畫咙咽,DOM 操作 (插入/刪除) 在下一幀中立即執(zhí)行老玛。

所以真正執(zhí)行動畫的是我們寫的 CSS 或者是 JavaScript 鉤子函數(shù),而 Vue 的 <transition> 只是幫我們很好地管理了這些 CSS 的添加/刪除钧敞,以及鉤子函數(shù)的執(zhí)行時機蜡豹。

總結(jié)

1. event

先通過正則把節(jié)點上的元素都解析出來,并對事件是原生事件還是自定義事件加以區(qū)分溉苛。然后把所有的事件用 vm._events 存儲起來. on 就是往_events 里push镜廉,off就是對_events的元素進行刪除,once就是兩者結(jié)合一下愚战。

2. v-model

v-model其實是一種語法糖娇唯,其本質(zhì)利用了父子組件的通信完成的。

3. slot

普通插槽是在父組件編譯和渲染階段生成 vnodes寂玲,所以數(shù)據(jù)的作用域是父組件實例塔插,子組件渲染的時候直接拿到這些渲染好的 vnodes。而對于作用域插槽拓哟,父組件在編譯和渲染階段并不會直接生成 vnodes想许,而是在父節(jié)點 vnode 的 data 中保留一個 scopedSlots 對象,存儲著不同名稱的插槽以及它們對應的渲染函數(shù)彰檬,只有在編譯和渲染子組件階段才會執(zhí)行這個渲染函數(shù)生成 vnodes伸刃,由于是在子組件環(huán)境執(zhí)行的,所以對應的數(shù)據(jù)作用域是子組件實例逢倍。

4. keep-alive

vue內(nèi)部對keep-alive做了特殊處理捧颅,在執(zhí)行prepatch階段會重新渲染緩存的組件,沒有重新生成一個vue實例较雕,因此也沒有標準組件的生命周期碉哑。

transition && transition-group

自動嗅探目標元素是否應用了 CSS 過渡或動畫,如果是亮蒋,在恰當?shù)臅r機添加/刪除 CSS 類名扣典。

如果過渡組件提供了 JavaScript 鉤子函數(shù),這些鉤子函數(shù)將在恰當?shù)臅r機被調(diào)用慎玖。

如果沒有找到 JavaScript 鉤子并且也沒有檢測到 CSS 過渡/動畫贮尖,DOM 操作 (插入/刪除) 在下一幀中立即執(zhí)行。

所以真正執(zhí)行動畫的是我們寫的 CSS 或者是 JavaScript 鉤子函數(shù)趁怔,而 Vue 的 <transition> 只是幫我們很好地管理了這些 CSS 的添加/刪除湿硝,以及鉤子函數(shù)的執(zhí)行時機薪前。
transition && transition-group

返回目錄

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市关斜,隨后出現(xiàn)的幾起案子示括,更是在濱河造成了極大的恐慌,老刑警劉巖痢畜,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件垛膝,死亡現(xiàn)場離奇詭異,居然都是意外死亡丁稀,警方通過查閱死者的電腦和手機吼拥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來二驰,“玉大人扔罪,你說我怎么就攤上這事⊥叭福” “怎么了矿酵?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長矗积。 經(jīng)常有香客問我全肮,道長,這世上最難降的妖魔是什么棘捣? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任辜腺,我火速辦了婚禮,結(jié)果婚禮上乍恐,老公的妹妹穿的比我還像新娘评疗。我一直安慰自己,他們只是感情好茵烈,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布百匆。 她就那樣靜靜地躺著,像睡著了一般呜投。 火紅的嫁衣襯著肌膚如雪加匈。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天仑荐,我揣著相機與錄音雕拼,去河邊找鬼。 笑死粘招,一個胖子當著我的面吹牛啥寇,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼示姿,長吁一口氣:“原來是場噩夢啊……” “哼甜橱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起栈戳,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎难裆,沒想到半個月后子檀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡乃戈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年褂痰,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片症虑。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡缩歪,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谍憔,到底是詐尸還是另有隱情匪蝙,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布习贫,位于F島的核電站逛球,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏苫昌。R本人自食惡果不足惜颤绕,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望祟身。 院中可真熱鬧奥务,春花似錦、人聲如沸袜硫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽父款。三九已至溢谤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間憨攒,已是汗流浹背世杀。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工是整, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留淹接,地道東北人格嘁。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓要销,卻偏偏與公主長得像脊髓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子仰泻,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容