Vue(2.6.11)ソースコードの読み取り-拡張

14717 ワード

本カタログ:1.event 2. v-model 3. slot 4. keep-alive 5. transition 6. transition-group
[event]
export function eventsMixin (Vue: Class) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array, 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, 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
  }
}

まず,ノード上の要素を正則的に解析し,イベントがオリジナルイベントであるかカスタムイベントであるかを区別する.そしてすべてのイベントをvm.イベントが格納されます.onは行きますイベントでpush,offは正しいeventsの要素を削除し、onceは両者を結合します.
[v-model]
v-modelは実は文法糖であり、その本質は親子コンポーネントの通信を利用して完成した.
[slot]
通常のスロットでは、親コンポーネントのコンパイルとレンダリングのフェーズでvnodesが生成されるため、データの役割ドメインは親コンポーネントインスタンスであり、サブコンポーネントがレンダリングされると、これらのレンダリングされたvnodesが直接取得されます.一方、アクティブドメインスロットの場合、親コンポーネントはコンパイルおよびレンダリングフェーズでvnodesを直接生成するのではなく、親ノードvnodeのdataにscopedSlotsオブジェクトを保持し、異なる名前のスロットと対応するレンダリング関数を格納します.このレンダリング関数は、サブコンポーネント環境で実行されるため、コンパイルおよびレンダリングのサブコンポーネントフェーズでのみ実行されます.したがって、対応するデータ役割ドメインはサブコンポーネントインスタンスである.
[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, 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,
  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 = [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])
  }
}


このコンポーネントには、組み込み属性abstractが設定されていることがわかります.文書には言及されていませんが、srccoreinstancelifecycle.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
}

すなわち、このコンポーネントが抽象コンポーネントである場合、このコンポーネントはサブコンポーネントを構築しません.
通常のテンプレートではなくrender関数を直接実装し、コンポーネントレンダリングを実行すると、このrender関数が実行されます.次に、その実装を分析します.
  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 {
  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コンポーネントの値は、最初のサブコンポーネントが取得された場合に有効であることがわかります.そのため、使用シーンは一般的にルーティングおよびダイナミックコンポーネントです.キャッシュされたコンポーネントもincludeとexcludeの制限を受けます.また,ここで前にキャッシュした場合は前にキャッシュした例を用いるが,そうでなければキャッシュしただけである.
また、keep-aliveは親子関係を確立していないが、diffの前にprepatch関数が実行されているため、keep-alive自体がスロットをサポートしているため、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コンポーネントであればmountメソッドは実行されず、createComponentで直接レンダリングするとともにreactivateComponentでkeep-aliveのライフサイクルコールバック関数activedとdeactivedを実行することがわかります.
transition && transition-group
ターゲット要素にCSS遷移またはアニメーションが適用されているかどうかを自動的に嗅ぎ、そうであれば適切なタイミングでCSSクラス名を追加/削除します.
遷移コンポーネントがJavaScriptフック関数を提供する場合、これらのフック関数は適切なタイミングで呼び出されます.
JavaScriptフックが見つかり、CSS遷移/アニメーションも検出されなかった場合、DOM操作(挿入/削除)は次のフレームで直ちに実行されます.
だから本当にアニメーションを実行するのは私たちが書いたCSSあるいはJavaScriptフック関数で、Vueのは私たちがこれらのCSSの追加/削除、およびフック関数の実行タイミングをよく管理してくれただけです.
まとめ
1. event
まず,ノード上の要素を正則的に解析し,イベントがオリジナルイベントであるかカスタムイベントであるかを区別する.そしてすべてのイベントをvm.イベントが格納されます.onは行きますイベントでpush,offは正しいeventsの要素を削除し、onceは両者を結合します.
2. v-model
v-modelは実は文法糖であり、その本質は親子コンポーネントの通信を利用して完成した.
3. slot
通常のスロットでは、親コンポーネントのコンパイルとレンダリングのフェーズでvnodesが生成されるため、データの役割ドメインは親コンポーネントインスタンスであり、サブコンポーネントがレンダリングされると、これらのレンダリングされたvnodesが直接取得されます.一方、アクティブドメインスロットの場合、親コンポーネントはコンパイルおよびレンダリングフェーズでvnodesを直接生成するのではなく、親ノードvnodeのdataにscopedSlotsオブジェクトを保持し、異なる名前のスロットと対応するレンダリング関数を格納します.このレンダリング関数は、サブコンポーネント環境で実行されるため、コンパイルおよびレンダリングのサブコンポーネントフェーズでのみ実行されます.したがって、対応するデータ役割ドメインはサブコンポーネントインスタンスである.
4. keep-alive
vue内部ではkeep-aliveが特殊に処理され、prepatchを実行する段階でキャッシュされたコンポーネントが再レンダリングされ、vueインスタンスが再生成されないため、標準コンポーネントのライフサイクルもありません.
transition && transition-group
ターゲット要素にCSS遷移またはアニメーションが適用されているかどうかを自動的に嗅ぎ、そうであれば適切なタイミングでCSSクラス名を追加/削除します.
遷移コンポーネントがJavaScriptフック関数を提供する場合、これらのフック関数は適切なタイミングで呼び出されます.
JavaScriptフックが見つかり、CSS遷移/アニメーションも検出されなかった場合、DOM操作(挿入/削除)は次のフレームで直ちに実行されます.
だから本当にアニメーションを実行するのは私たちが書いたCSSあるいはJavaScriptフック関数で、Vueのは私たちがこれらのCSSの追加/削除、およびフック関数の実行タイミングをよく管理してくれただけです.transition && transition-group
ディレクトリに戻る