从源码看vue(v2.7.10)中的transition组件的原理

transition组件大家肯定都用过,这个组件是Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果,现在我们从源码看看vue是怎么实现的。

只有css

// app.vue
<template>
  <div>
    <button v-on:click="show = !show">Toggle</button>
    <transition name="fade">
      <p v-if="show">hello</p>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      show: true,
    };
  },
};
</script>
<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.65s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
</style>
...
var Transition = {
    name: 'transition',
    props: transitionProps,
    abstract: true,
    render: function (h) {
      var _this = 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 (children.length > 1) {
        warn$2('<transition> can only be used on a single element. Use ' +
          '<transition-group> for lists.', this.$parent);
      }
      var mode = this.mode;
      // warn invalid mode
      if (mode && mode !== 'in-out' && mode !== 'out-in') {
        warn$2('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.
      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-".concat(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));
      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 () {
            _this._leaving = false;
            _this.$forceUpdate();
          });
          return placeholder(h, rawChild);
        }
        else if (mode === 'in-out') {
          if (isAsyncPlaceholder(child)) {
            return oldRawChild;
          }
          var delayedLeave_1;
          var performLeave = function () {
            delayedLeave_1();
          };
          mergeVNodeHook(data, 'afterEnter', performLeave);
          mergeVNodeHook(data, 'enterCancelled', performLeave);
          mergeVNodeHook(oldData, 'delayLeave', function (leave) {
            delayedLeave_1 = leave;
          });
        }
      }
      return rawChild;
    }
  };

transition也是vue的一个内置组件,渲染函数如上所述,最后会返回rawChild,这个是$slots.default中的第一个vnode。这就意味着如果你在里面写入超过一个元素,也就只有第一个有动画效果。当我们点击toggle的时候,hello会消失,此时会触发removeVnodes([oldVnode])方法:

function removeVnodes(vnodes, startIdx, endIdx) {
  for (; startIdx <= endIdx; ++startIdx) {
    var ch = vnodes[startIdx];
    if (isDef(ch)) {
      if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch);
        invokeDestroyHook(ch);
      }
      else {
        // Text node
        removeNode(ch.elm);
      }
    }
  }
}

该方法主要执行removeAndInvokeRemoveHook(ch):

function removeAndInvokeRemoveHook(vnode, rm) {
 if (isDef(rm) || isDef(vnode.data)) {
    var i_3;
    var listeners = cbs.remove.length + 1;
    if (isDef(rm)) {
      // we have a recursively passed down rm callback
      // increase the listeners count
      rm.listeners += listeners;
    }
    else {
      // directly removing
      rm = createRmCb(vnode.elm, listeners);
    }
    // recursively invoke hooks on child component root node
    if (isDef((i_3 = vnode.componentInstance)) &&
      isDef((i_3 = i_3._vnode)) &&
      isDef(i_3.data)) {
      removeAndInvokeRemoveHook(i_3, rm);
    }
    for (i_3 = 0; i_3 < cbs.remove.length; ++i_3) {
      cbs.remove[i_3](vnode, rm);
    }
    if (isDef((i_3 = vnode.data.hook)) && isDef((i_3 = i_3.remove))) {
      i_3(vnode, rm);
    }
    else {
      rm();
    }
  }
  else {
    removeNode(vnode.elm);
  }
}

主要执行cbs.remove[i_3](vnode, rm)的leave(vnode, rm)方法:

function leave(vnode, rm) {
    var el = vnode.elm;
    // call enter callback now
    if (isDef(el._enterCb)) {
      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, type = data.type, leaveClass = data.leaveClass, leaveToClass = data.leaveToClass, leaveActiveClass = data.leaveActiveClass, beforeLeave = data.beforeLeave, leave = data.leave, afterLeave = data.afterLeave, leaveCancelled = data.leaveCancelled, delayLeave = data.delayLeave, duration = data.duration;
    var expectsCSS = css !== false && !isIE9;
    var userWantsControl = getHookArgumentsLength(leave);
    var explicitLeaveDuration = toNumber(isObject(duration) ? duration.leave : duration);
    if (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);
      }
      // @ts-expect-error
      if (cb.cancelled) {
        if (expectsCSS) {
          removeTransitionClass(el, leaveClass);
        }
        leaveCancelled && leaveCancelled(el);
      }
      else {
        rm();
        afterLeave && afterLeave(el);
      }
      el._leaveCb = null;
    }));
    if (delayLeave) {
      delayLeave(performLeave);
    }
    else {
      performLeave();
    }
    function performLeave() {
      // the delayed leave may have already been cancelled
      // @ts-expect-error
      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);
      if (expectsCSS) {
        addTransitionClass(el, leaveClass);
        addTransitionClass(el, leaveActiveClass);
        nextFrame(function () {
          removeTransitionClass(el, leaveClass);
          // @ts-expect-error
          if (!cb.cancelled) {
            addTransitionClass(el, leaveToClass);
            if (!userWantsControl) {
              if (isValidDuration(explicitLeaveDuration)) {
                setTimeout(cb, explicitLeaveDuration);
              }
              else {
                whenTransitionEnds(el, type, cb);
              }
            }
          }
        });
      }
      leave && leave(el, cb);
      if (!expectsCSS && !userWantsControl) {
        cb();
      }
    }
  }

该方法首先执行resolveTransition(vnode.data.transition)会根据名称拼凑动画离开和进入的类。
在这里插入图片描述
然后执行performLeave方法:

function performLeave() {
   // the delayed leave may have already been cancelled
   // @ts-expect-error
   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);
   if (expectsCSS) {
     addTransitionClass(el, leaveClass);
     addTransitionClass(el, leaveActiveClass);
     nextFrame(function () {
       removeTransitionClass(el, leaveClass);
       // @ts-expect-error
       if (!cb.cancelled) {
         addTransitionClass(el, leaveToClass);
         if (!userWantsControl) {
           if (isValidDuration(explicitLeaveDuration)) {
             setTimeout(cb, explicitLeaveDuration);
           }
           else {
             whenTransitionEnds(el, type, cb);
           }
         }
       }
     });
   }
   leave && leave(el, cb);
   if (!expectsCSS && !userWantsControl) {
     cb();
   }
 }
}

首先执行addTransitionClass(el, leaveClass)新增离开时的类,然后执行addTransitionClass(el, leaveActiveClass)新增离开中的类,随后执行nextFrame方法。该方法是requestAnimationFrame函数,首先执行removeTransitionClass(el, leaveClass)移除离开时的类,再执行addTransitionClass(el, leaveToClass)添加离开完成的类。然后执行whenTransitionEnds(el, type, cb)方法:

function whenTransitionEnds(el, expectedType, cb) {
    var _a = getTransitionInfo(el, expectedType), type = _a.type, timeout = _a.timeout, propCount = _a.propCount;
    if (!type)
      return cb();
    var event = type === TRANSITION ? transitionEndEvent : animationEndEvent;
    var ended = 0;
    var end = function () {
      el.removeEventListener(event, onEnd);
      cb();
    };
    var onEnd = function (e) {
      if (e.target === el) {
        if (++ended >= propCount) {
          end();
        }
      }
    };
    setTimeout(function () {
      if (ended < propCount) {
        end();
      }
    }, timeout + 1);
    el.addEventListener(event, onEnd);
  }

首先执行getTransitionInfo去获取当前的transition时间,然后监听transitionEnd事件。等待动画执行完毕移除监听事件,然后执行cb()方法,该方法是:

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);
   }
   // @ts-expect-error
   if (cb.cancelled) {
     if (expectsCSS) {
       removeTransitionClass(el, leaveClass);
     }
     leaveCancelled && leaveCancelled(el);
   }
   else {
     rm();
     afterLeave && afterLeave(el);
   }
   el._leaveCb = null;
 }));

执行removeTransitionClass(el, leaveToClass)和removeTransitionClass(el, leaveActiveClass)移除执行完的类后执行rm()方法的removeNode(childElm)方法去移除子元素。到此动画执行完毕。

function remove() {
    if (--remove.listeners === 0) {
      removeNode(childElm);
    }
  }

function removeNode(el) {
   var parent = nodeOps.parentNode(el);
    // element may have already been removed due to v-html / v-text
    if (isDef(parent)) {
      nodeOps.removeChild(parent, el);
    }
  }

小结

在移除元素的时候会执行removeAndInvokeRemoveHook方法去给要隐藏的元素添加离开的类并在下一帧中移除移除离开起始的类,随后获取元素设置的transition时间,并启用setTimeout去等待动画执行完成。执行完后会执行一个回调函数去移除当前dom上的类,并通过父元素移除当前子元素。

函数式

<template>
  <div>
    <button @click="show = !show">Toggle</button>
    <transition
      @before-enter="beforeEnter"
      @enter="enter"
      @leave="leave"
      @css="false"
    >
      <p v-if="show">Demo</p>
    </transition>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      show: true,
    };
  },
  methods: {
    beforeEnter: function (el) {
      console.log(1234);
      el.style.opacity = 0;
      el.style.transformOrigin = 'left';
    },
    enter: function (el, done) {
      Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 });
      Velocity(el, { fontSize: '1em' }, { complete: done });
    },
    leave: function (el, done) {
      console.log(7758);
      Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 });
      Velocity(el, { rotateZ: '100deg' }, { loop: 2 });
      Velocity(
        el,
        {
          rotateZ: '45deg',
          translateY: '30px',
          translateX: '30px',
          opacity: 0,
        },
        { complete: done }
      );
    },
  },
};
</script>

可以看到我们定义了三个函数,一个是进入前和进入中、离开中三个函数。然后我们再来看看此时transition的渲染函数:

var Transition = {
    name: 'transition',
    props: transitionProps,
    abstract: true,
    render: function (h) {
      var _this = 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 (children.length > 1) {
        warn$2('<transition> can only be used on a single element. Use ' +
          '<transition-group> for lists.', this.$parent);
      }
      var mode = this.mode;
      // warn invalid mode
      ...
      var rawChild = children[0];
      // if this is a component root node and the component's
      // parent container node also has transition, skip.
      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-".concat(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));
      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;
      }
     ...
      return rawChild;
    }
  };

我们重点看var data = ((child.data || (child.data = {})).transition =
extractTransitionData(this)),这个是获取transition的_parentListeners并赋给p元素的vnode:
在这里插入图片描述
最后返回p的vnode。我们先来看离开时是啥时候触发的。离开执行的是 removeVnodes([oldVnode], 0, 0),该函数主要执行 removeAndInvokeRemoveHook(ch)的cbs.remove[i_3](vnode, rm)方法:

var transition = inBrowser
? {
   ...
   remove: function (vnode, rm) {
     /* istanbul ignore else */
     if (vnode.data.show !== true) {
       // @ts-expect-error
       leave(vnode, rm);
     }
     else {
       rm();
     }
   }
 }
 : {};

可以看出执行的是leave(vnode, rm)方法:

function leave(vnode, rm) {
    var el = vnode.elm;
    // call enter callback now
    if (isDef(el._enterCb)) {
      el._enterCb.cancelled = true;
      el._enterCb();
    }
    // 是否有transition
    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, type = data.type, leaveClass = data.leaveClass, leaveToClass = data.leaveToClass, leaveActiveClass = data.leaveActiveClass, beforeLeave = data.beforeLeave, leave = data.leave, afterLeave = data.afterLeave, leaveCancelled = data.leaveCancelled, delayLeave = data.delayLeave, duration = data.duration;
    var expectsCSS = css !== false && !isIE9;
    var userWantsControl = getHookArgumentsLength(leave);
    var explicitLeaveDuration = toNumber(isObject(duration) ? duration.leave : duration);
    if (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);
      }
      // @ts-expect-error
      if (cb.cancelled) {
        if (expectsCSS) {
          removeTransitionClass(el, leaveClass);
        }
        leaveCancelled && leaveCancelled(el);
      }
      else {
        rm();
        afterLeave && afterLeave(el);
      }
      el._leaveCb = null;
    }));
    if (delayLeave) {
      delayLeave(performLeave);
    }
    else {
      performLeave();
    }
    function performLeave() {
      // the delayed leave may have already been cancelled
      // @ts-expect-error
      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);
      if (expectsCSS) {
        addTransitionClass(el, leaveClass);
        addTransitionClass(el, leaveActiveClass);
        nextFrame(function () {
          removeTransitionClass(el, leaveClass);
          // @ts-expect-error
          if (!cb.cancelled) {
            addTransitionClass(el, leaveToClass);
            if (!userWantsControl) {
              if (isValidDuration(explicitLeaveDuration)) {
                setTimeout(cb, explicitLeaveDuration);
              }
              else {
                whenTransitionEnds(el, type, cb);
              }
            }
          }
        });
      }
      leave && leave(el, cb);
      if (!expectsCSS && !userWantsControl) {
        cb();
      }
    }
  }

先判断是否有transition对象,如果有的话会执行如果有 beforeLeave(el)方法,然后判断是否需要css,我们已经表示不需要,我们自己去处理。然后执行leave(el, cb)函数触发我们设置的回调。随后执行完cbs.remove[i_3](vnode, rm)会去执行removeNode方法移除函数。
下面我们看看beforeEnter和enter的触发时机,它们执行的阶段是在createElm阶段,在执行insert(parentElm, vnode.elm, refElm)方法前会执行invokeCreateHooks方法去执行插入前的一个方法:

function invokeCreateHooks(vnode, insertedVnodeQueue) {
   for (var i_2 = 0; i_2 < cbs.create.length; ++i_2) {
     cbs.create[i_2](emptyNode, vnode);
   }
   i = vnode.data.hook; // Reuse variable
   if (isDef(i)) {
     if (isDef(i.create))
       i.create(emptyNode, vnode);
     if (isDef(i.insert))
       insertedVnodeQueue.push(vnode);
   }
 }

通过遍历执行cbs.create[i_2](emptyNode, vnode)方法中的_enter(_, vnode)方法,去判断如果有vnode.data.beforeEnter就会执行回调。

function enter(vnode, toggleDisplay) {
    var el = vnode.elm;
    // call leave callback now
    if (isDef(el._leaveCb)) {
      el._leaveCb.cancelled = true;
      el._leaveCb();
    }
    var data = resolveTransition(vnode.data.transition);
    if (isUndef(data)) {
      return;
    }
    /* istanbul ignore if */
    if (isDef(el._enterCb) || el.nodeType !== 1) {
      return;
    }
    var css = data.css, type = data.type, enterClass = data.enterClass, enterToClass = data.enterToClass, enterActiveClass = data.enterActiveClass, appearClass = data.appearClass, appearToClass = data.appearToClass, appearActiveClass = data.appearActiveClass, beforeEnter = data.beforeEnter, enter = data.enter, afterEnter = data.afterEnter, enterCancelled = data.enterCancelled, beforeAppear = data.beforeAppear, appear = data.appear, afterAppear = data.afterAppear, appearCancelled = data.appearCancelled, 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 ? (isFunction(appear) ? appear : enter) : enter;
    var afterEnterHook = isAppear ? afterAppear || afterEnter : afterEnter;
    var enterCancelledHook = isAppear
      ? appearCancelled || enterCancelled
      : enterCancelled;
    var explicitEnterDuration = toNumber(isObject(duration) ? duration.enter : duration);
    if (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);
        removeTransitionClass(el, activeClass);
      }
      // @ts-expect-error
      if (cb.cancelled) {
        if (expectsCSS) {
          removeTransitionClass(el, startClass);
        }
        enterCancelledHook && enterCancelledHook(el);
      }
      else {
        afterEnterHook && afterEnterHook(el);
      }
      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);
    if (expectsCSS) {
      addTransitionClass(el, startClass);
      addTransitionClass(el, activeClass);
      nextFrame(function () {
        removeTransitionClass(el, startClass);
        // @ts-expect-error
        if (!cb.cancelled) {
          addTransitionClass(el, toClass);
          if (!userWantsControl) {
            if (isValidDuration(explicitEnterDuration)) {
              setTimeout(cb, explicitEnterDuration);
            }
            else {
              whenTransitionEnds(el, type, cb);
            }
          }
        }
      });
    }
    if (vnode.data.show) {
      toggleDisplay && toggleDisplay();
      enterHook && enterHook(el, cb);
    }
    if (!expectsCSS && !userWantsControl) {
      cb();
    }
  }

该函数主要做了两件事,首先就是给当前vnode绑定了insert事件,然后触发beforeEnterHook方法。insert方法中我们可以看到会执行enterHook,那么insert方法什么时候触发呢?我们在前面的文章也分析过,insert的触发是在没有parentVnode的时候才会触发,此时会执行queue[i_6].data.hook.insert(queue[i_6])去最终执行enterHook方法。

小结

函数式的transition方法的beforeLeave和leave是在有 removeVnodes阶段执行的,enterHook是在最后insert时执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Young soul2

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值