Skip to content
On this page

transition

transition 组件,用于执行元素显示或者隐藏过程的过渡动画 transition 组件与平台相关,不同平台实现不同

js
// platforms/web/runtime/index.js
// 扩展到components组件上,与keep-alive类似,在组件初始化会合并入组件选项
extend(Vue.options.components, platformComponents)

定义

js
// platforms/web/runtime/components/transition.js
export default {
  name: 'transition',
  abstract: true, // 同样是抽象组件,与keep-alive一样,实际自身不添加dom
  render(h) {
    let children = this.$slots.default
    if (!children) {
        return
    }
    // 内部的节点不能是纯文本节点
    children = children.filter(isNotTextNode)
    if (children.length > 1) {
      // transition组件只能有一个子节点,多个则报错提示改用transition-group
    }
    
    const mode = this.mode
    // mode 只支持 in-out/out-in
    const rawChild = children[0]
    if (hasParentTransition(this.$vnode)) {
      // this.$vnode是组件占位vnode <transition />
      // 如果外层 <transition /> 是根节点
      // 直接返回子节点vnode
      return rawChild
    }
    const child = getRealChild(rawChild)
    // 如果过渡组件是abstract组件,递归寻找其内部非抽象组件
    if (!child) {
      // 一般child为过渡组件vnode !== false
        return rawChild
    }
    // ...
    // 生成组件id和过渡元素的key
    const id = `__transition-${this._uid}-`
    child.key = child.key == null
          ? child.isComment
            ? id + 'comment'
            : id + child.tag
          : isPrimitive(child.key)
            ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
            : child.key
    
    // 将transition的相关属性赋值到子组件
    const data = (child.data || (child.data = {})).transtion = extractTransition(this)
    const oldRawChild = this._vnode
    const oldChild = getRealChild(oldRawChild)
    // 这里就是找到 渲染前的 内部非抽象元素/组件
    if (oldChild && oldChild.data 
        && isSameChild(child, oldChild) && isAsyncPlaceholder(oldChild)
        && !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)
    ) {
        const oldData = oldChild.data.transition = extend({}, data)
        if (mode === 'out-in') {
          this._leaving = true // 标识正在隐藏
          mergeVNodeHook(oldData, 'afterLeave', () => {
            // 旧元素隐藏之后的回调
            this._leaving = false
            // 重新解析transition子组件更新
            this.$forceUpdate()
          })
          
        } 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 })
        }
    }
    // 最终渲染的还是子节点,本质是slot
    return rawChild
  }
}

// 将transition组件的属性赋值给子组件
function extractTransitionData (comp: Component): Object {
  // comp 是 transition的vm实例
  const data = {}
  const options = comp.$options
  // props
  // 转移transition的props
  for (const key in options.propsData) {
    data[key] = comp[key]
  }
  // events
  // 转移事件处理函数
  const listeners = options._parentListeners
  for (const key in listeners) {
    data[camelize(key)] = listeners[key]
  }
  return data
}

小结:render函数内,只是定义了transition组件内的渲染逻辑,还没有加入过渡动画的逻辑

动画控制

transition组件,通过添加和删除css类名,从而控制元素动画过渡效果 主要是通过扩展在transition的钩子函数实现

动画逻辑实现

js
// platforms/web/modules/transition.js
function _enter (_: any, vnode: VNodeWithData) {
  if (vnode.data.show !== true) {
    // 不是用v-show控制
    // 最终调用enter函数
    enter(vnode)
  }
}
export default inBrowser ? {
  // create和activate钩子会在创建patch函数的时候注入到baseCompileOption选项中
  // patchvnode的时候会触发对应钩子
  create: _enter,
  activate: _enter,
  remove (vnode: VNode, rm: Function) {
    if (vnode.data.show !== true) {
      leave(vnode, rm)
    } else {
      rm()
    }
  }
} : {}

具体enter实现

由上面可以看出,enter参数是过渡元素/组件的vnode

js
// platforms/web/runtime/modules/transition.js
function enter(vnode, toggleDisplay/*这是个函数*/) {
  const el = vnode.elm // el是通过vnode.elm获取,所以就算是组件也可以过渡
  //...
  // 解析出之前赋值给vnode 的transition数据
  const data = resolveTransition(vnode.data.transition)
  if (isUndef(data)) { return }
  //...
  // 解析data各项
  const {
    css,
    type,
    enterClass,
    enterToClass,
    enterActiveClass,
    appearClass,
    appearToClass,
    appearActiveClass,
    beforeEnter,
    enter,
    afterEnter,
    enterCancelled,
    beforeAppear,
    appear,
    afterAppear,
    appearCancelled,
    duration
  } = data
  // context 是transition的vm
  let context = activeInstance
  let transitionNode = activeInstance.$vnode // <transition />
  while (transition && transition.parent) {
    // transition在根节点
    transitionNode = transitionNode.parent
    context = transitionNode.context
  }
  // 一通操作下来,transitionNode就是 <transition> ,context就是transition范围最外层的vm
  const isAppear = !context._isMounted || !vnode.isRootInsert
  if (isAppear && !appear && appear !== '') {
    // 如果不设置appear属性,首次就不执行过渡效果
   return 
  }
  const startClass = isAppear && appearClass ? appearClass : enterClass
  // ... 同样的方法得到activeClass、toClass还有那些钩子函数
  const expectCSS = css !== false && isIE9
  // 这是用于移除过渡类名的回调函数
  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
  })
  // ...
  // 过渡逻辑核心
  beforeEnterHook && beforeEnterHook(el)
  if (expectsCSS) {
    // 添加 v-enter、v-enter-active
    addTransitionClass(el, startClass)
    addTransitionClass(el, activeClass)
    // 就是window.requestNextFrame
    nextFrame(() => {
      // 移除v-enter
      removeTransitionClass(el, startClass)
      if (!cb.cancelled) {
        // 添加v-enter-to
        addTransitionClass(el, toClass)
        if (!userWantsControl) {
          if (isValidDuration(explicitEnterDuration)) {
            setTimeout(cb, explicitEnterDuration)
          } else {
            // 动画结束时候的移除类名操作,注意这里的cb就是上面定义的 cb回调函数
            whenTransitionEnds(el, type, cb)
          }
        }
      }
    })
  }
  
  if (vnode.data.show) {
    // v-show处理
    toggleDisplay && toggleDisplay()
    enterHook && enterHook(el, cb)
  }

}

whenTransitionEnds 实现

js
// platforms/web/runtime/transition-util.js
export function whenTransitionEnds (
  el: Element,
  expectedType: ?string,
  cb: Function
) {
  const { type, timeout, propCount } = getTransitionInfo(el, expectedType)
  // 其实就是获取到过渡属性的各项信息 window.getComputedStyle
  // 比如过渡类系(transition/animation),过渡css属性个数,过渡时间等
  if (!type) return cb()
  // 动画结束事件
  const event = type === TRANSITION ? transitionEndEvent : animationEndEvent
  let ended = 0
  const end = () => {
    // 当所有css元素属性过渡完,移除监听器
    el.removeEventListener(event, onEnd)
    // 同时cb回调,移除active类名和to类名
    cb()
  }
  const onEnd = e => {
    if (e.target === el) {
      // 当全部过渡元素css属性完成过渡
      if (++ended >= propCount) {
        end()
      }
    }
  }
  // 定时器,就算过渡元素属性个数还没完全过渡完,也强制执行
  setTimeout(() => {
    if (ended < propCount) {
      end()
    }
  }, timeout + 1)
  // 监听过渡动画结束事件,执行onEnd
  el.addEventListener(event, onEnd)
}

隐藏的动画(leave)执行逻辑也和enter类似