有了之前对于 Vue 整个渲染流程的分析,我们可以深入研究一下 <transition> 的实现原理。

Vue 渲染机制分析:

使用方法

先来简单看下使用方法。按照 Vue Transition 文档的介绍,<transition> 可以应用在下列元素或者组件中:

在进入具体分析之前,先来看下 Vue 中强制触发渲染流程的方法:

// src/core/instance/lifecycle.js

Vue.prototype.$forceUpdate = function () {
    const vm = this;
    if (vm._watcher) {
        vm._watcher.update();
    }
};

之前介绍过的 Watcher,创建时我们传入了回调函数(第二个参数),调用实例上的 update() 时就会触发这个回调函数,完成从 render 函数到 VNode 再到 DOM 的渲染流程。

// src/platforms/web/runtime/index.js

export function mountComponent(vm, el, hydrating) {
    let updateComponent = () => {
        vm._update(vm._render(), hydrating);
    };
    vm._watcher = new Watcher(vm, updateComponent, noop);
}

生成 VNode 阶段

在这一阶段中,会执行 render 函数得到 VNode。

内置的 <transition> 是一个抽象组件(abstract)。Vue 文档中是没有抽象组件的,应该是 Vue 的内置组件才会用到,比如还有 <keep-alive>

// src/platforms/web/runtime/components/transition.js

export default {
    name: 'transition',
    props: transitionProps,
    abstract: true,
    render(h) {...}
}

获取子 VNode 节点

进入 render 函数,结合前面的渲染分析,我们知道 VNode 的生成顺序,先子节点再父节点。 所以这里 _renderChildren 一定就是子节点数组了。 另外,transition 只支持单子节点,否则会报警告,后续的处理也仅针对第一个子节点进行。

let children = this.$options._renderChildren;
if (!children) {
    return;
}
// 只处理第一个子节点
const rawChild = children[0];

生成 ID

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 切换过程中,前后两个节点都需要渲染,旧节点在 _vnode 上。 之所以还要调用 getRealChild(),是因为子元素有可能还是一个抽象节点,例如 <keep-alive>,还需要进一步获取真实元素。 属性保存在 data.transition 对象中。

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

出现 v-show 指令,标记在 data 上:

if (child.data.directives && child.data.directives.some(d => d.name === 'show')) {
    child.data.show = true;
}

过渡模式

默认情况下,前后两个元素的 transition 过渡效果是同时发生的。 对于需要设置先后顺序的场景,提供了过渡模式

对于 out-in 也就是当前元素先进行过渡,完成之后新元素过渡进入的情况。 在 VNode 的 afterLeave 钩子中触发强制更新。

if (this._leaving) {
    // 处理 keep-alive,其他元素直接 return
    return placeholder(h, rawChild);
}
if (mode === 'out-in') {
    // return placeholder node and queue update when leave finishes
    this._leaving = true;
    mergeVNodeHook(oldData, 'afterLeave', () => {
        this._leaving = false;
        this.$forceUpdate();
    });
    return placeholder(h, rawChild);
}

真实的 DOM 操作都定义在 VNode 上的钩子中,在下一个 patch 阶段执行。

patch 阶段

在这个阶段中,会调用 VNode 上的一些钩子,主要涉及具体的 DOM 操作。

在前面的文章中介绍过,patch 阶段支持如下钩子,在创建/ Diff 更新/删除 VNode 的各个阶段会调用相应的钩子:

// src/core/vdom/patch.js

const hooks = ['create', 'activate', 'update', 'remove', 'destroy'];

其中 transition 在 VNode 的创建和删除阶段定义了如下钩子:

// src/platforms/web/runtime/modules/transition.js

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

进入阶段

前面介绍过,处理条件展示,使用 v-show

function _enter(_, vnode) {
    if (vnode.data.show !== true) {
        enter(vnode);
    }
}

Vue 支持 CSS 动画和 JS 动画两种。先来看看 Vue 最常用的 CSS 动画。 默认情况下 Vue 会监听 CSS Transition 结束事件,动画效果完成后自动调用结束钩子。同时也支持用户显式传入 duration,这时会使用 setTimeout 按照用户的意愿结束动画。

if (expectsCSS) {
    addTransitionClass(el, startClass);
    addTransitionClass(el, activeClass);
    nextFrame(() => {
        addTransitionClass(el, toClass);
        removeTransitionClass(el, startClass);
        if (!cb.cancelled && !userWantsControl) {
            // 显式定义了持续时间
            if (isValidDuration(explicitEnterDuration)) {
                setTimeout(cb, explicitEnterDuration);
            }
            // 监听 transition 结束事件
            else {
                whenTransitionEnds(el, type, cb);
            }
        }
    });
}

监听过渡效果结束

这段代码可谓非常巧妙。考虑到了下面的情况:

  1. transitionEnd 的事件的浏览器兼容性。这个比较简单,检测后加上 Webkit 前缀就行
  2. 可能定义了多个动画属性,持续时间各异。
  3. 规定时间未执行完,需要强制结束。

实际做法如下:

export function whenTransitionEnds(
    el,
    expectedType,
    cb
) {
    const {
        type,
        timeout,
        propCount
    } = getTransitionInfo(el, expectedType);
    if (!type) {
        return cb();
    }

    const event = type === TRANSITION ? transitionEndEvent : animationEndEvent;
    let ended = 0;
    const onEnd = e => {
        if (e.target === el) {
            // 全部属性的动画都执行完成,结束
            if (++ended >= propCount) {
                end();
            }
        }
    };
    const end = () => {
        el.removeEventListener(event, onEnd);
        cb();
    };
    setTimeout(() => {
        // 此时还有属性的动画没有执行完成,强制结束
        if (ended < propCount) {
            end();
        }
    }, timeout + 1);
    el.addEventListener(event, onEnd);
}

JS 钩子

前面介绍了 transition 支持的 CSS 动画。对于需要精确化控制的复杂场景,需要使用 JS 钩子

值得一提的是在 CSS 动画中也可以使用这些钩子。在这种情况下 enterleave 这两个钩子都是可选的,因为进入离开过程中的操作已经由 CSS transition 完成。

如果希望完全控制,需要显式传入 v-bind:css="false"。这时候使用 enterleave 这两个钩子就能进行复杂的操作了。