乐趣区

关于vue.js:Vue-tansition源码解析

1. 模板中 transition 标签会依据内建的 transition 组件选项创立组件实例。组件渲染成页面时调用 render 函数,获取 transition 标签内的标签对应的节点(插槽),并将 transition 组件标签上的数据合并到该节点 VNode 上。

如果先人(包含以后 transition 标签)都是根标签,且并存在它是 transition 标签内的标签(插槽)时,间接跳过以后的 transition。

mode 值为 out-in(以后元素先进行过渡,实现之后新元素过渡进入),当 transition 标签内组件切换时,间接返回空文本节点,来替换旧组件节点,旧节点移除时实现 leave 动画后,强制更新 transition 组件,新组件开始插入,并开始 enter 动画。

mode 值为 in-out(新元素先进行过渡,实现之后以后元素过渡来到),当 transition 标签内组件切换时,为旧组件节点增加 delayLeave 钩子(钩子用来开始节点的 leave 动画),当新节点实现 enter 动画后,再执行旧节点的 performLeave,开始旧节点的 leave 动画,最初从 document 中移除旧节点。

// 内建的 transition 组件选项
var transitionProps = {
  name: String,
  appear: Boolean,
  css: Boolean,
  mode: String,
  type: String,
  enterClass: String,
  leaveClass: String,
  enterToClass: String,
  leaveToClass: String,
  enterActiveClass: String,
  leaveActiveClass: String,
  appearClass: String,
  appearActiveClass: String,
  appearToClass: String,
  duration: [Number, String, Object]
};
/* transition 组件选项 */
var Transition = {
  name: 'transition',
  props: transitionProps,
  abstract: true,

  render: function render (h) {// 获取 Vnode 节点,h:createElement
    var this$1 = this;

    var children = this.$slots.default;
    if (!children) {return}

    // filter out text nodes (possible whitespaces)
    children = children.filter(isNotTextNode);// 过滤文本节点
    /* istanbul ignore if */
    if (!children.length) {return}

    // warn multiple elements
    if (process.env.NODE_ENV !== 'production' && children.length > 1) {
      warn(
        '<transition> can only be used on a single element. Use' +
        '<transition-group> for lists.',
        this.$parent
      );
    }

    var mode = this.mode;

    // warn invalid mode
    if (process.env.NODE_ENV !== 'production' &&
      mode && mode !== 'in-out' && mode !== 'out-in'
    ) {
      warn(
        'invalid <transition> mode:' + mode,
        this.$parent
      );
    }

    var rawChild = children[0];

    // if this is a component root node and the component's
    // parent container node also has transition, skip.
    // 如果先人都是根标签, 且正好是 transition 组件标签内的标签(插槽)时,间接跳过以后的 transition
    if (hasParentTransition(this.$vnode)) {return rawChild}

    // apply transition data to child
    // use getRealChild() to ignore abstract components e.g. keep-alive
    var child = getRealChild(rawChild);
    /* istanbul ignore if */
    if (!child) {// 如果没有原生标签节点
      return rawChild
    }

    if (this._leaving) {return placeholder(h, rawChild)
    }

    // ensure a key that is unique to the vnode type and to this transition
    // component instance. This key will be used to remove pending leaving nodes
    // during entering.
    var 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;

    var data = (child.data || (child.data = {})).transition = extractTransitionData(this);// 将 transition 标签上的数据增加到子标签节点 data.transition 上
    var oldRawChild = this._vnode;
    var oldChild = getRealChild(oldRawChild);

    // mark v-show
    // so that the transition module can hand over the control to the directive
    if (child.data.directives && child.data.directives.some(isVShowDirective)) {child.data.show = true;}

    if (
      oldChild &&
      oldChild.data &&
      !isSameChild(child, oldChild) &&
      !isAsyncPlaceholder(oldChild) &&
      // #6687 component root is a comment node
      !(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)// 不是组件,或者不是只含正文标签的组件
    ) {
      // replace old child transition data with fresh one
      // important for dynamic transitions!
      var oldData = oldChild.data.transition = extend({}, data);
      // handle transition mode
      if (mode === 'out-in') {// 以后元素先进行过渡,实现之后新元素过渡进入
        // return placeholder node and queue update when leave finishes
        this._leaving = true;
        mergeVNodeHook(oldData, 'afterLeave', function () {// 增加 afterLeave 回调函数,旧节点动画实现后,再更新组件,从新渲染
          this$1._leaving = false;
          this$1.$forceUpdate();});
        return placeholder(h, rawChild)// 移除旧节点,替换为空的文本节点
      } else if (mode === 'in-out') {// 新元素先进行过渡,实现之后以后元素过渡来到
        if (isAsyncPlaceholder(child)) {return oldRawChild}
        var delayedLeave;
        var performLeave = function () { delayedLeave(); };
        mergeVNodeHook(data, 'afterEnter', performLeave);// 在 enter 动画实现之后开始调用 performLeave,开始 leave 动画
        mergeVNodeHook(data, 'enterCancelled', performLeave);// 在 enter 动画过程中勾销后开始调用 performLeave,开始 leave 动画
        mergeVNodeHook(oldData, 'delayLeave', function (leave) {delayedLeave = leave;});// 更改旧节点的 leave 动画的执行机会,提早从 document 移除的工夫
      }
    }

    return rawChild
  }
};

2.transition 的 create、activete、reomve 办法将会增加到节点钩子函数汇合中,将在节点 patch 过程中的不同周期调用,在创立 dom 后、插入到 document 之前调用_enter 办法,在 dom 从 document 移除之前调用 leave 办法。

var transition = inBrowser ? {
  create: _enter,// 创立 dom 后调用
  activate: _enter,// 创立组件后,且是 transition 组件根标签节点时
  remove: function remove$$1 (vnode, rm) {// 从文档中移除前调用
    /* istanbul ignore else */
    if (vnode.data.show !== true) {leave(vnode, rm);
    } else {rm();// 从文档中移除
    }
  }
} : {};

3. 在 enter 办法中,创立 dom、插入到 document 之前,增加 enter 和 enterActive 类名、调用对应的钩子。插入到文档后移除 enter 类名,增加 enterTo 类名。动画实现后,移除 enterTo 和 enterActive 类名。


function enter (vnode, toggleDisplay) {
  var el = vnode.elm;
  ...
  var css = data.css;// 是否通过 css 实现过渡
  var type = data.type;// 判断是 transition 还是 animation
  var enterClass = data.enterClass;
  var enterToClass = data.enterToClass;
  var enterActiveClass = data.enterActiveClass;
  var appearClass = data.appearClass;
  var appearToClass = data.appearToClass;
  var appearActiveClass = data.appearActiveClass;
  var beforeEnter = data.beforeEnter;// 钩子函数
  var enter = data.enter;// 钩子函数
  var afterEnter = data.afterEnter;// 钩子函数
  var enterCancelled = data.enterCancelled;// 钩子函数
  var beforeAppear = data.beforeAppear;// 钩子函数
  var appear = data.appear;// 钩子函数(节点在初始渲染的过渡)
  var afterAppear = data.afterAppear;// 钩子函数
  var appearCancelled = data.appearCancelled;// 钩子函数
  var duration = data.duration;// 进入的持续时间

  // activeInstance will always be the <transition> component managing this
  // transition. One edge case to check is when the <transition> is placed
  // as the root node of a child component. In that case we need to check
  // <transition>'s parent for appear check.
  var context = activeInstance;
  var transitionNode = activeInstance.$vnode;
  while (transitionNode && transitionNode.parent) {// 向上查找不是组件根标签的先人节点
    context = transitionNode.context;
    transitionNode = transitionNode.parent;
  }

  var isAppear = !context._isMounted || !vnode.isRootInsert;

  if (isAppear && !appear && appear !== '') {return}
// 类名
  var startClass = isAppear && appearClass
    ? appearClass
    : enterClass;
  var activeClass = isAppear && appearActiveClass
    ? appearActiveClass
    : enterActiveClass;
  var toClass = isAppear && appearToClass
    ? appearToClass
    : enterToClass;
// 钩子函数
  var beforeEnterHook = isAppear
    ? (beforeAppear || beforeEnter)
    : beforeEnter;
  var enterHook = isAppear
    ? (typeof appear === 'function' ? appear : enter)
    : enter;
  var afterEnterHook = isAppear
    ? (afterAppear || afterEnter)
    : afterEnter;
  var enterCancelledHook = isAppear
    ? (appearCancelled || enterCancelled)
    : enterCancelled;

  var explicitEnterDuration = toNumber(isObject(duration)
      ? duration.enter
      : duration
  );

  if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {checkDuration(explicitEnterDuration, 'enter', vnode);
  }

  var expectsCSS = css !== false && !isIE9;
  var userWantsControl = getHookArgumentsLength(enterHook);

  var cb = el._enterCb = once(function () {// 动画完结后的回调,第一次调用后生效
    if (expectsCSS) {removeTransitionClass(el, toClass);// 移除 toClass 类名
      removeTransitionClass(el, activeClass);// 移除 active 类名
    }
    if (cb.cancelled) {if (expectsCSS) {removeTransitionClass(el, startClass);
      }
      enterCancelledHook && enterCancelledHook(el);// 调用 enter 过程勾销的钩子
    } else {afterEnterHook && afterEnterHook(el);// 调用 afterEnter 钩子
    }
    el._enterCb = null;
  });

  if (!vnode.data.show) {
    // remove pending leave element on enter by injecting an insert hook
    mergeVNodeHook(vnode, 'insert', function () {
      var parent = el.parentNode;
      var pendingNode = parent && parent._pending && parent._pending[vnode.key];
      if (pendingNode &&
        pendingNode.tag === vnode.tag &&
        pendingNode.elm._leaveCb
      ) {pendingNode.elm._leaveCb();
      }
      enterHook && enterHook(el, cb);
    });
  }

  // start enter transition
  beforeEnterHook && beforeEnterHook(el);// 在插入文档之前调用 beforeEnter 钩子
  if (expectsCSS) {addTransitionClass(el, startClass);// 增加 enter 类名,在插入到文档后移除
    addTransitionClass(el, activeClass);// 增加 active 类名在
    nextFrame(function () {// 下一帧
      removeTransitionClass(el, startClass);// 移除 enter 类名
      if (!cb.cancelled) {// 当进入 leave 状态,cb 将会被勾销
        addTransitionClass(el, toClass);// 插入到文档后增加 enterTo 类名
        if (!userWantsControl) {if (isValidDuration(explicitEnterDuration)) {setTimeout(cb, explicitEnterDuration);
          } else {whenTransitionEnds(el, type, cb);// 动画完结后调用 cb
          }
        }
      }
    });
  }
  ...
}

4.leave 办法中,dom 从 document 移除之前,增加 leave 和 leaveActive 类名,以及对应的钩子。在下一帧移除 leave 类名,增加 leaveTo 类名。动画实现后移除 leaveTo 和 leaveActive 类名,而后 dom 从 document 中移除。

function leave (vnode, rm) {
  var el = vnode.elm;

  // call enter callback now
  if (isDef(el._enterCb)) {// 终止 enter 状态,针对 afterEnterHook 钩子还没调用的状况,将会触发 enterCancelledHook 钩子函数
    el._enterCb.cancelled = true;
    el._enterCb();}

  var data = resolveTransition(vnode.data.transition);
  if (isUndef(data) || el.nodeType !== 1) {return rm()
  }

  /* istanbul ignore if */
  if (isDef(el._leaveCb)) {return}

  var css = data.css;
  var type = data.type;
  var leaveClass = data.leaveClass;
  var leaveToClass = data.leaveToClass;
  var leaveActiveClass = data.leaveActiveClass;
  var beforeLeave = data.beforeLeave;
  var leave = data.leave;
  var afterLeave = data.afterLeave;
  var leaveCancelled = data.leaveCancelled;
  var delayLeave = data.delayLeave;// mode 为 in-out 时存在
  var duration = data.duration;

  var expectsCSS = css !== false && !isIE9;
  var userWantsControl = getHookArgumentsLength(leave);

  var explicitLeaveDuration = toNumber(isObject(duration)
      ? duration.leave
      : duration
  );

  if (process.env.NODE_ENV !== 'production' && isDef(explicitLeaveDuration)) {checkDuration(explicitLeaveDuration, 'leave', vnode);
  }

  var cb = el._leaveCb = once(function () {if (el.parentNode && el.parentNode._pending) {el.parentNode._pending[vnode.key] = null;
    }
    if (expectsCSS) {removeTransitionClass(el, leaveToClass);
      removeTransitionClass(el, leaveActiveClass);
    }
    if (cb.cancelled) {if (expectsCSS) {removeTransitionClass(el, leaveClass);
      }
      leaveCancelled && leaveCancelled(el);
    } else {rm();// 移除 DOM 元素,会在本次宏工作执行完开始从新渲染
      afterLeave && afterLeave(el);
    }
    el._leaveCb = null;
    
  });

  if (delayLeave) {delayLeave(performLeave);// 提早执行 performLeave(mode 为 in-out 时,在新节点 enter 动画实现时执行 performLeave,开始 leave 动画)} else {performLeave();
  }

  function performLeave () {
    // the delayed leave may have already been cancelled
    if (cb.cancelled) {// 被勾销了
      return
    }
    // record leaving element
    if (!vnode.data.show && el.parentNode) {(el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key)] = vnode;
    }
    beforeLeave && beforeLeave(el);// 调用 beforeLeave 钩子函数
    if (expectsCSS) {addTransitionClass(el, leaveClass);// 增加 leave 类名
      addTransitionClass(el, leaveActiveClass)// 增加 leaveActive 类名
      nextFrame(function () {//
        removeTransitionClass(el, leaveClass);// 在下一帧移除 leave 类名
        if (!cb.cancelled) {addTransitionClass(el, leaveToClass);// 在下一帧增加 leaveToClass 类名
          if (!userWantsControl) {if (isValidDuration(explicitLeaveDuration)) {// 动画实现后或者设定的持续时间实现后移除 leaveTo 和 leaveActive 类名,最初 dom 从 document 移除
              setTimeout(cb, explicitLeaveDuration);// 在用户设定的持续时间后调用回调函数
            } else {whenTransitionEnds(el, type, cb);// 动画完结调用回调函数
            }
          }
        }
      });
    }
    leave && leave(el, cb);
    if (!expectsCSS && !userWantsControl) {cb();
    }
  }
}

5.vue 通过 window.getComputedStyle 获取 css 设置的动画工夫,element.style 读取的只是元素的内联款式,即写在元素的 style 属性上的款式;而 getComputedStyle 读取的款式是最终款式,包含了内联款式、嵌入款式和内部款式。

退出移动版