Vue2.0源碼閱讀筆記(九):內置組件

  Vue2.0中一共有五個內置組件:動態渲染組件的component、用於過渡動畫的transition-group與transition、緩存組件的keep-alive、內容分發插槽的slot。
  component組件配合is屬性在編譯的過程當中被替換成具體的組件,而slot組件已經在上一篇文章中加以描述,所以本章主要闡述剩餘的三個內置組件。
css

1、KeepAlive

  <keep-alive> 包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出如今父組件鏈中。該組件要求同時只有一個子元素被渲染。前端

一、KeepAlive組件

  KeepAlive 組件源碼以下所示:
node

{
  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) {
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}
複製代碼

  KeepAlive 組件的邏輯相對比較簡單,根據傳入的 include 與 exclude 規則來決定是否緩存子組件,根據傳入的 max 參數來決定最多緩存多少組件。
  從 render 函數中能夠看出,若是子組件是緩存對象 cache 的屬性,則直接返回該子組件的VNode,若是不是,則添加到緩存對象上,並將緩存的VNode的 data.keepAlive 屬性置爲 true。
  這裏有兩點須要注意:keepAlive組件的 abstract 屬性爲 true、被緩存的子組件 vnode.data.keepAlive 屬性爲 true。
react

二、abstract 屬性

  當 abstract 屬性爲 true 時,表示該組件爲抽象組件:組件自己不會被渲染成DOM元素、不會出如今父組件鏈中。
  在完成一系列初始化的過程當中,會調用 initLifecycle 方法:
web

function initLifecycle(vm) {
  const options = vm.$options

  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  /* ... */
}
複製代碼

  由上可知,在 options.abstract 爲 true 時,組件實例創建父子關係的時候會被忽略。
算法

三、vnode.data.keepAlive 屬性

  在 patch 的過程當中會調用 createComponent 方法:
數組

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)
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}
複製代碼

  在本系列第七篇文章《組件》中,詳細分析過 createComponent 方法,當時沒考慮 keepAlive 值爲 true的狀況,在這裏重點介紹。
  在首次渲染時 vnode.componentInstance 的值爲空,所以不論 keepAlive 是否爲空,isReactivated 值老是 false。再次渲染時,若 keepAlive 值爲 true 則isReactivated 爲true。
瀏覽器

if (isDef(i = i.hook) && isDef(i = i.init)) {
  i(vnode, false)
}
複製代碼

  鉤子函數 init 在 keepAlive 值爲 false 時的功能是調取組件的構造函數生成組件構造實例。
緩存

init (vnode, hydrating) {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    const mountedNode = vnode
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    /* 省略... */
  }
}
複製代碼

  當 keepAlive 值爲 true 時,會調用 prepatch 方法,該方法不會再執行組件的 mount 過程,而是直接調用 updateChildComponent 方法更新子組件,這也是被 keepAlive 包裹的組件在有緩存的時候就不會再執行組件的 created、mounted 等鉤子函數的緣由。
app

function prepatch (oldVnode, vnode) {
  var options = vnode.componentOptions;
  var child = vnode.componentInstance = oldVnode.componentInstance;
  updateChildComponent(child,options.propsData,options.listeners,
        vnode,options.children);
}
複製代碼

  在 createComponent 函數最後,若是組件再次渲染且 keepAlive 爲 true 時,會調用 reactivateComponent 函數,該函數將緩存的DOM元素直接插入到目標位置。

function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  /* 省略對 transition 動畫不觸發的問題的處理*/
  insert(parentElm, vnode.elm, refElm);
}
複製代碼

2、Transition

  Vue 提供了 transition 的封裝組件,在下列情形中,能夠給任何元素和組件添加進入/離開過渡

一、條件渲染 (使用 v-if)。
二、條件展現 (使用 v-show)。
三、動態組件。
四、組件根節點。

  當插入或刪除包含在 transition 組件中的元素時,Vue 將會作如下處理:

一、自動嗅探目標元素是否應用了 CSS 過渡或動畫,若是是,在恰當的時機添加/刪除 CSS 類名。
二、若是過渡組件提供了 JavaScript 鉤子函數,這些鉤子函數將在恰當的時機被調用。
三、若是沒有找到 JavaScript 鉤子而且也沒有檢測到 CSS 過渡/動畫,DOM 操做 (插入/刪除) 在下一幀中當即執行。

一、Transition組件

  Transition 組件的定義在 /src/platforms/web/runtime/components/transition.js 文件中,精簡代碼以下:

export default {
  name: 'transition',
  props: transitionProps,
  abstract: true,

  render (h) {
    let children = this.$slots.default
    if (!children) {return}
    children = children.filter(isNotTextNode)
    if (!children.length) {return}

    /* 省略多個子元素警告 */

    const mode = this.mode

    /* 省略無效模式警告 */

    const rawChild = children[0]
    if (hasParentTransition(this.$vnode)) {return rawChild}
    const child = getRealChild(rawChild)
    if (!child) {return rawChild}
    if (this._leaving){return placeholder(h, rawChild)}

    /* 省略獲取id與key代碼 */

    const data = (child.data || (child.data = {})).transition = extractTransitionData(this)
    const oldRawChild = this._vnode
    const oldChild = getRealChild(oldRawChild)

    if (child.data.directives && child.data.directives.some(isVShowDirective)) {
      child.data.show = true
    }

    /* 省略多元素過渡模式處理代碼 */

    return rawChild
  }
}
複製代碼

  Transition 組件與 KeepAlive 組件同樣是抽象函數,在該組件定義中比較重要的就是 render 函數,其做用就是渲染生成VNode。
  在該渲染函數中有三個功能比較重要:

一、將 Transition 組件上的參數賦值到 child.data.transition 上。
二、若是 Transition 組件上使用 v-show 指令,則將 child.data.show 設爲 true。
三、設置多元素過渡的模式。

二、過渡模式mode

  Vue 提供了兩種過渡模式,默認同時生效。

in-out:新元素先進行過渡,完成以後當前元素過渡離開。
out-in:當前元素先進行過渡,完成以後新元素過渡進入。

  在 render 函數中相關代碼以下所示:

const oldData = oldChild.data.transition = extend({}, data)
if (mode === 'out-in') {
  this._leaving = true
  mergeVNodeHook(oldData, 'afterLeave', () => {
    this._leaving = false
    this.$forceUpdate()
  })
  return placeholder(h, rawChild)
} else if (mode === 'in-out') {
  if (isAsyncPlaceholder(child)) {
    return oldRawChild
  }
  let delayedLeave
  const performLeave = () => { delayedLeave() }
  mergeVNodeHook(data, 'afterEnter', performLeave)
  mergeVNodeHook(data, 'enterCancelled', performLeave)
  mergeVNodeHook(oldData,'delayLeave',leave=>{delayedLeave=leave})
}
複製代碼

  從上述代碼可知:當過渡模式爲 out-in,在切換元素時,當前元素徹底 leave 後纔會加載新元素。當過渡模式爲 in-out,當前元素延時到新元素 enter 後再 leave。

三、過渡邏輯

  過渡相關的邏輯在 /src/platforms/web/runtime/modules/transition.js 文件中實現,Vue會將相關邏輯插入到 patch 的生命週期中去處理。

export default inBrowser ? {
  create: _enter,
  activate: _enter,
  remove (vnode, rm) {
    if (vnode.data.show !== true) {
      leave(vnode, rm)
    } else {
      rm()
    }
  }
} : {}

function _enter (_,vnode) {
  if (vnode.data.show !== true) {
    enter(vnode)
  }
}
複製代碼

  能夠看出過渡的邏輯本質上就是在元素插入時調用 enter 函數,在元素移除時調用 leave 函數。
  由於在使用 v-show 指令時元素始終會被渲染並保留在 DOM 中,只是簡單地切換元素的 CSS 屬性 display。因此會對使用 v-show 指令的狀況進行特殊處理,在下一小結闡述具體處理過程。
  整體來看 enter 函數與 leave 函數幾乎是一個鏡像過程,下面僅分析 enter 函數。

function enter (vnode, toggleDisplay) {
  const el = vnode.elm
  /* 省略... */
  const expectsCSS = css !== false && !isIE9
  const userWantsControl = getHookArgumentsLength(enterHook)
  /* 省略 cb 函數實現 */
  /* 合併 insert 鉤子函數 */
  if (!vnode.data.show) {
    mergeVNodeHook(vnode, 'insert', () => {
      const parent = el.parentNode
      const pendingNode = parent && parent._pending && parent._pending[vnode.key]
      if (pendingNode &&
        pendingNode.tag === vnode.tag &&
        pendingNode.elm._leaveCb
      ) {
        pendingNode.elm._leaveCb()
      }
      enterHook && enterHook(el, cb)
    })
  }
  /* 開始執行過渡動畫 */
  beforeEnterHook && beforeEnterHook(el)
  if (expectsCSS) {
    addTransitionClass(el, startClass)
    addTransitionClass(el, activeClass)
    nextFrame(/* 省略... */)
  }
  /* 省略使用 v-show 指令的狀況 */
  if (!expectsCSS && !userWantsControl) {
    cb()
  }
}
複製代碼

  enter 函數看上去很複雜,其核心代碼是開始執行過渡動畫的部分。首先執行 beforeEnterHook 鉤子函數,若使用 css 過渡類,則接着執行:

addTransitionClass(el, startClass)
addTransitionClass(el, activeClass)
複製代碼

  addTransitionClass 函數做用就是給元素添加樣式,而後執行 nextFrame 函數。

function nextFrame (fn: Function) {
  raf(() => {
    raf(fn)
  })
}

const raf = inBrowser
  ? window.requestAnimationFrame
    ? window.requestAnimationFrame.bind(window)
    : setTimeout
複製代碼

  nextFrame 函數在支持 requestAnimationFrame 方法的瀏覽器中使用該方法,參數 fn 會在下一幀執行。若是不支持則使用 setTimeout 代替。

nextFrame(() => {
  removeTransitionClass(el, startClass)
  if (!cb.cancelled) {
    addTransitionClass(el, toClass)
    if (!userWantsControl) {
      if (isValidDuration(explicitEnterDuration)) {
        setTimeout(cb, explicitEnterDuration)
      } else {
        whenTransitionEnds(el, type, cb)
      }
    }
  }
})
複製代碼

  在下一幀時,首先移除 startClass 樣式,而後判斷過渡是否被取消。若是沒有取消,添加 toClass 樣式,而後根據是否經過 enterHook 鉤子函數控制動畫來決定 cb 函數的執行時機。

const cb = el._enterCb = once(() => {
  if (expectsCSS) {
    removeTransitionClass(el, toClass)
    removeTransitionClass(el, activeClass)
  }
  if (cb.cancelled) {
    if (expectsCSS) {
      removeTransitionClass(el, startClass)
    }
    enterCancelledHook && enterCancelledHook(el)
  } else {
    afterEnterHook && afterEnterHook(el)
  }
  el._enterCb = null
})
複製代碼

  cb 函數首先移除 toClass 與 activeClass 樣式,若是過渡被取消則先移除 startClass 樣式,再執行 enterCancelledHook 鉤子函數。若是過渡沒有被取消,則調用 afterEnterHook 鉤子函數。

四、v-show

  對於在 Transition 組件上使用 v-show 指令的狀況,在 v-show 指令的實現中有特殊處理。相關代碼在 /src/platforms/web/runtime/directives/show.js 文件中。

export default {
  bind (el, { value }, vnode) {
    /* ... */
    const transition = vnode.data && vnode.data.transition
    const originalDisplay = el.__vOriginalDisplay =
      el.style.display === 'none' ? '' : el.style.display
    if (value && transition) {
      vnode.data.show = true
      enter(vnode, () => {
        el.style.display = originalDisplay
      })
    }
    /* ... */
  },

  update (el, { value, oldValue }, vnode) {
    /* ... */
    const transition = vnode.data && vnode.data.transition
    if (transition) {
      vnode.data.show = true
      if (value) {
        enter(vnode, () => {
          el.style.display = el.__vOriginalDisplay
        })
      } else {
        leave(vnode, () => {
          el.style.display = 'none'
        })
      }
    }
  },
  /* 省略... */
複製代碼

  能夠看到在 v-show 指令的實現中,若在 Transition 組件上使用則調用 enter 與 leave 函數,與 patch 生命週期調用這兩個函數不一樣的會額外的傳入第二個參數。
  在 enter 與 leave 函數也有對應的處理,以保證在DOM元素沒有新增和移除的狀況下實現過渡效果。

if (vnode.data.show) {
  toggleDisplay && toggleDisplay()
  enterHook && enterHook(el, cb)
}
複製代碼

3、TransitionGroup

  Vue 使用 <transition-group> 組件完成列表過渡效果,該組件有如下幾個特色:

一、該組件不是抽象組件,會以一個真實元素呈現,默認是 <span>,能夠經過 tag參數 指定。
二、過渡模式不可用。
三、內部元素老是須要提供惟一的 key 屬性值。
四、CSS 過渡的類將會應用在內部的元素中,而不是這個組/容器自己。

一、TransitionGroup組件

  TransitionGroup 組件定義在 /src/platforms/web/runtime/components/transition-group.js 文件中,精簡代碼以下:

const props = extend({
  tag,
  moveClass
}, transitionProps)

delete props.mode

export default {
  props,
  beforeMount () { /* 省略具體實現 */ },
  render (h) { /* 省略具體實現 */ },
  updated () { /* 省略具體實現 */ },
  methods: {
    hasMove (el, moveClass){ /* 省略具體實現 */ }
  }
}
複製代碼

  TransitionGroup 組件有兩種過渡效果:基本過渡效果、平滑過渡效果,後者經過 v-move 特性來實現。
  在源碼實現中,基本過渡效果由組件的 render 函數完成,當數據發生變化時的平滑過渡效果由 updated 生命週期鉤子函數完成。

二、基本過渡實現

  TransitionGroup 組件 render 方法的完整代碼以下所示:

render (h) {
  const tag = this.tag || this.$vnode.data.tag || 'span'
  const map = Object.create(null)
  const prevChildren = this.prevChildren = this.children
  const rawChildren = this.$slots.default || []
  const children = this.children = []
  const transitionData = extractTransitionData(this)

  for (let i = 0; i < rawChildren.length; i++) {
    const c = rawChildren[i]
    if (c.tag) {
      if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
        children.push(c)
        map[c.key] = c
        ;(c.data || (c.data = {})).transition = transitionData
      } else if (process.env.NODE_ENV !== 'production') {
        const opts = c.componentOptions
        const name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag
        warn(`<transition-group> children must be keyed: <${name}>`)
      }
    }
  }

  if (prevChildren) {
    const kept = []
    const removed = []
    for (let i = 0; i < prevChildren.length; i++) {
      const c = prevChildren[i]
      c.data.transition = transitionData
      c.data.pos = c.elm.getBoundingClientRect()
      if (map[c.key]) {
        kept.push(c)
      } else {
        removed.push(c)
      }
    }
    this.kept = h(tag, null, kept)
    this.removed = removed
  }

  return h(tag, null, children)
}
複製代碼

  render 函數的本質功能就是生成VNode,其中該函數的參數 h 爲用來生成VNode的 createElement 函數。
  函數中首先聲明的幾個變量具體含義以下所示:

一、tag:TransitionGroup 組件最終渲染的元素類型,默認是 span。
二、map:存儲原始子節點 key 與值的對象。
三、prevChildren:存儲上一次的子節點數組。
四、rawChildren:原始子節點數組。
五、children:當前子節點數組。
五、transitionData:TransitionGroup 組件上提取的過渡參數。

  緊接着的 for 循環是處理原始子節點的,由於 TransitionGroup 組件要求全部子節點都顯式提供 key 值,若是沒有提供 key 值在開發環境下會報錯。

if (c.key != null && String(c.key).indexOf('__vlist') !== 0)
複製代碼

  判斷是否顯式提供 key 值的條件語句之因此這樣寫,是由於在 for 循環的渲染過程當中,在沒有提供 key 值的狀況下,會自動加上 __vlist 爲開頭的字符串做爲 key 值。
  這個 for 循環還有一個重要的功能是將組件過渡參數賦值給子組件的 data.transition 屬性,在上一節講述 Transition 組件時有講過,在元素進入和移除時會根據這個屬性來顯示相應的過渡效果。
  最後處理改變前的子節點,調用了原生 DOM 的 getBoundingClientRect 方法獲取到原生 DOM 的位置信息,記錄到 vnode.data.pos 中。而後將存在的節點放入 kept 中,將刪除的節點放入 removed 中。最後返回由 createElement 函數生成的VNode。
  TransitionGroup 組件的 render 方法因爲將過渡信息下沉到子節點上,是能夠實現基本的子節點添加刪除的過渡效果的。因爲插入和刪除操做與須要移動的元素沒有過渡效果控制的關聯,因此並無平滑過渡的效果。

三、平滑過渡實現

  當數據改變時會調用 updated 生命週期鉤子,TransitionGroup 組件當子節點添加與刪除的平滑過渡效果在該鉤子函數中實現。

updated () {
  const children = this.prevChildren
  const moveClass = this.moveClass || ((this.name || 'v') + '-move')
  if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
    return
  }

  children.forEach(callPendingCbs)
  children.forEach(recordPosition)
  children.forEach(applyTranslation)

  this._reflow = document.body.offsetHeight

  children.forEach((c) => {
    if (c.data.moved) {
      const el = c.elm
      const s = el.style
      addTransitionClass(el, moveClass)
      s.transform = s.WebkitTransform = s.transitionDuration = ''
      el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
        if (e && e.target !== el) {
          return
        }
        if (!e || /transform$/.test(e.propertyName)) {
          el.removeEventListener(transitionEndEvent, cb)
          el._moveCb = null
          removeTransitionClass(el, moveClass)
        }
      })
    }
  })
}
複製代碼

  updated 函數首先使用 hasMove 方法判斷子節點是否認義了 move 相關的動畫樣式,接着對子節點進行預處理:

一、callPendingCbs:在前一個過渡動畫沒執行完又再次執行到該方法的時候,會提早執行 _moveCb 和 _enterCb。
二、recordPosition:記錄節點的新位置,賦值給 data.newPos 屬性。
三、applyTranslation:先計算節點新位置和舊位置的差值,把須要移動的節點的位置又偏移到以前的舊位置,目的是爲了作 move 緩動作準備。

  接着經過讀取 document.body.offsetHeight 強制觸發瀏覽器重繪。
  而後遍歷子節點,先給子節點添加 moveClass,接着把子節點的 style.transform 設置爲空,因爲以前使用 applyTranslation 方法將子節點偏移到舊位置,此時會按照設置的過渡效果偏移到當前位置,進而實現平滑過渡的效果。
  最後監聽 transitionEndEvent 過渡結束的事件,作一些清理的操做。

四、子元素更新算法不穩定的處理

  Vue 中虛擬 DOM 的子元素更新算法是不穩定的,它不能保證被移除元素的相對位置。Vue 在 beforeMount 生命週期鉤子函數中對這種狀況進行了處理。

beforeMount () {
  const update = this._update
  this._update = (vnode, hydrating) => {
    const restoreActiveInstance = setActiveInstance(this)
    this.__patch__(this._vnode, this.kept, false, true)
    this._vnode = this.kept
    restoreActiveInstance()
    update.call(this, vnode, hydrating)
  }
}
複製代碼

  在 beforeMount 函數中,首先重寫了 _update 方法,_update 方法自己的做用是根據VNode生成真實DOM的。重寫後的 _update 方法主要有兩步:首先移除須要移除的 vnode,同時觸發它們的 leaving 過渡;而後須要把插入和移動的節點達到它們的最終態,同時還要保證移除的節點保留在應該的位置。
  Vue 經過這兩步處理來解決子元素更新算法是不穩定的問題,做者在 TransitionGroup 組件實現的文件中也有詳細的註釋說明。

4、總結

  KeepAlive 組件不渲染真實DOM節點,會將緩存的子組件放入 cache 數組中,並將被緩存子組件的 data.keepAlive 屬性置爲 true。若是須要再次渲染被緩存的子組件,則直接返回該子組件的VNode,而組件的 created、mounted 等鉤子函數不會再執行。
   Transition 組件的 render 函數會將組件上的參數賦值到 child.data.transition 上,而後在 patch 的過程當中會調用 enter 與 leave 函數完成相關過渡效果。在使用 v-show 指令時,DOM元素並無新增和刪除,Vue 對這種狀況進行了特別處理,保證在DOM元素沒有新增和移除的狀況下實現過渡效果。
  TransitionGroup 組件的基本過渡效果跟 Transition 組件實現效果同樣。修改列表數據的時候,若是是添加或者刪除數據,則會觸發相應元素自己的過渡動畫。平滑過渡效果本質上就是先將元素移動到舊位置,而後再根據定義的過渡效果將其移動到新位置。

歡迎關注公衆號:前端桃花源,互相交流學習!

相關文章
相關標籤/搜索