【问题标题】:What is causing this broken animation/transition in a Vue.js component in Chrome?是什么导致 Chrome 中的 Vue.js 组件出现这种损坏的动画/过渡?
【发布时间】:2017-07-29 09:30:47
【问题描述】:

const NotificationBar = {
  name: 'notification-bar',

  template: `
    <div
      :class="{
        'notification-bar': true,
        'notification-bar--error': isError,
        'notification-bar--warning': isWarning,
        'notification-bar--info': isInfo,
        'notification-bar--visible': isVisible,
      }"

      @click="dismiss"
      @transitionend="transitionEnd($event)">
      {{ message }}
    </div>
  `,

  props: {
    message: {
      type: String,
      required: true,
    },

    type: {
      type: String,
      required: true,

      validator(value) {
        const valid = ['error', 'warning', 'info'];
        return valid.includes(value);
      },
    },

    dismissable: {
      type: Boolean,
      default: false,
    },

    timeout: {
      type: Number,
      default: 0,
    },
  },

  data() {
    return {
      isVisible: false,
    };
  },

  computed: {
    isError() {
      return this.type === 'error';
    },

    isWarning() {
      return this.type === 'warning';
    },

    isInfo() {
      return this.type === 'info';
    },
  },

  methods: {
    clear() {
      const event = 'cleared';
      let done;

      if (this.isVisible) {
        this.$once('transitionend', () => {
          done = true;
          this.$emit(event, done);
        });
        this.isVisible = false;
      } else {
        done = false;
        this.$emit(event, done);
      }
    },

    dismiss() {
      const event = 'dismissed';
      let done;

      if (this.dismissable) {
        done = true;
        this.$emit(event, done);
        this.clear();
      } else {
        done = false;
        this.$emit(event, done);
      }
    },

    show() {
      if (!this.isVisible) {
        this.isVisible = true;
        this.$emit('show', this.clear);

        if (this.timeout) {
          setTimeout(() => {
            this.$emit('timeout');
            this.clear();
          }, this.timeout);
        }
      }
    },

    transitionEnd(event) {
      this.$emit('transitionend', event);
    },
  },

  mounted() {
    window.requestAnimationFrame(this.show);
  },
};

const NotificationCenter = {
  name: 'notification-center',

  components: {
    NotificationBar,
  },

  template: `
    <div>
      <notification-bar
        v-for="notification in active"

        :message="notification.message"
        :type="notification.type"
        :dismissable="notification.dismissable"
        :timeout="notification.timeout"

        @cleared="clear">
      </notification-bar>
    </div>
  `,

  props: {
    queue: {
      type: Array,
      required: true,
    },
  },

  data() {
    return {
      active: [],
    };
  },

  computed: {
    hasActiveNotification() {
      return this.active.length > 0;
    },

    hasQueuedNotification() {
      return this.queue.length > 0;
    },
  },

  watch: {
    queue() {
      if (this.hasQueuedNotification && !this.hasActiveNotification) {
        this.setNextActive();
      }
    },
  },

  methods: {
    setNextActive() {
      this.setActive(this.queue.shift());
    },

    setActive(notification) {
      this.active.push(notification);
    },

    removeActive() {
      this.active.pop();
    },

    clear() {
      this.active.pop();

      if (this.hasQueuedNotification) {
        this.$nextTick(this.setNextActive);
      }
    },
  },
};

window.vm = new Vue({
  components: {
    NotificationCenter,
  },

  el: '#app',

  template: `
    <div>
      <notification-center
        :queue="notifications">
      </notification-center>

      <label>
        <strong>Type</strong> <br>
        Error <input v-model="type" type="radio" name="type" value="error"> <br>
        Warning <input v-model="type" type="radio" name="type" value="warning"> <br>
        Info <input v-model="type" type="radio" name="type" value="info"> <br>
      </label>

      <label>
        <strong>Message</strong>
        <input v-model="message" type="text">
      </label>

      <label>
        <strong>Dismissable</strong>
        <input v-model="dismissable" type="checkbox">
      </label>

      <label>
        <strong>Timeout</strong>
        <input v-model="timeout" type="number" step="100" min="0">
      </label>

      <button @click="generateNotification">Generate notification</button>
    </div>
  `,

  data: {
    notifications: [],
    
    type: null,
    message: null,
    dismissable: null,
    timeout: null,
  },
  
  methods: {
    generateNotification() {
      const {
        type,
        message,
        dismissable,
        timeout,
      } = this;
      
      this.notifications.push({
        type,
        message,
        dismissable,
        timeout,
      });
      
      this.type = this.message = this.dismissable = this.timeout = null;
    },
  },
});
.notification-bar {
  box-sizing: border-box;
  position: absolute;
  top: -3.2rem;
  right: 0;
  left: 0;
  z-index: 9999;
  width: 100%;
  height: 3.2rem;
  color: #fff;
  font-family: 'Avenir Next', sans-serif;
  font-size: 1.2em;
  line-height: 3.2rem;
  text-align: center;
  transition: top 266ms ease;
}
.notification-bar--error {
  background-color: #f02a4d;
}
.notification-bar--warning {
  background-color: #ffc107;
}
.notification-bar--info {
  background-color: #2196f3;
}
.notification-bar--visible {
  top: 0;
}

label {
  display: block;
  margin: 2rem 0;
  font-size: 1.4rem;
}
label:first-of-type {
  margin-top: 5rem;
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <style>
      html {
        font-size: 62.5%;
      }
      
      body {
        font-family: sans-serif;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
  </body>
</html>

重现问题的步骤

  1. 在 Chrome 中通过 sn-p 运行。
  2. 生成通知。 (将type设置为任何内容,将message设置为任何内容,设置dismissable,点击Generate notification
  3. 注意通知栏

预期行为

通知栏平滑地动画到可见视口中。您可以通过在 Firefox 中执行上述步骤来观察这一点。

这是一个 GIF,展示了 Chrome 中的正确行为。点击生成通知后,您可以看到该栏平滑过渡。

以下是 Chrome 正常运行时的时间线截图:

这是 Chrome 正常运行时的调用树:

实际行为

通知栏在大多数情况下都无法平滑地动画到可见视口中。在 Chrome Devtools 中捕获时间线显示在显示通知栏时没有运行动画。当栏在屏幕外进行动画处理时,动画始终正确运行。动画在 Firefox 中始终正确运行。

这是一个 GIF,展示了 Chrome 中的错误行为。点击生成通知后,您会看到该栏突然出现。

以下是 Chrome 行为不正常时的时间线截图:

这是 Chrome 行为不正确时的调用树:

其他信息

代码在做什么的概述

  1. NotificationCenter 接受 queue 属性。这是一个对象数组,其中数组表示通知队列,对象表示单个通知。

  2. 一旦queue 更改,观察程序就会运行检查队列中是否有通知以及是否没有活动通知。如果是这种情况,则将下一个通知设置为活动通知。

  3. NotificationCenter 的模板有一个指令循环遍历active 中的项目并呈现NotificationBar。在上一步中,设置了一个新的活动通知,因此将创建一个新的通知栏并将其挂载到 DOM。

  4. 一旦NotificationBar 被挂载到DOM 上,它的show 方法就会在window.requestAnimationFrame 内部运行。

【问题讨论】:

  • 我在运行 Chrome v 56.0.2924.87 的 Mac 上获得流畅的动画
  • @RoyJ:你是否始终如一地理解它?
  • 是的,我已经多次运行 sn-p(全屏)。每次,横幅都会从顶部顺利滑入。
  • @RoyJ:感谢您的确认。我进一步缩小了问题的范围。只有在页面加载后发送第一个通知时,Chrome 中的通知栏动画才会流畅。随后的通知动画流畅。你能说这也是你的情况吗?
  • 对我来说,它第一次运行与后续运行相同。

标签: javascript google-chrome vue.js css-transitions requestanimationframe


【解决方案1】:

问题在于这一行:

this.type = this.message = this.dismissable = this.timeout = null;

如果您删除它,它会正常工作。当它被执行时,道具变为 NULL,并且您验证了道具不应该为空。

const NotificationBar = {
  name: 'notification-bar',

  template: `
    <div
      :class="{
        'notification-bar': true,
        'notification-bar--error': isError,
        'notification-bar--warning': isWarning,
        'notification-bar--info': isInfo,
        'notification-bar--visible': isVisible,
      }"

      @click="dismiss"
      @transitionend="transitionEnd($event)">
      {{ message }}
    </div>
  `,

  props: {
    message: {
      type: String,
      required: true,
    },

    type: {
      type: String,
      required: true,

      validator(value) {
        const valid = ['error', 'warning', 'info'];
        return valid.includes(value);
      },
    },

    dismissable: {
      type: Boolean,
      default: false,
    },

    timeout: {
      type: Number,
      default: 0,
    },
  },

  data() {
    return {
      isVisible: false,
    };
  },

  computed: {
    isError() {
      return this.type === 'error';
    },

    isWarning() {
      return this.type === 'warning';
    },

    isInfo() {
      return this.type === 'info';
    },
  },

  methods: {
    clear() {
      const event = 'cleared';
      let done;

      if (this.isVisible) {
        this.$once('transitionend', () => {
          done = true;
          this.$emit(event, done);
        });
        this.isVisible = false;
      } else {
        done = false;
        this.$emit(event, done);
      }
    },

    dismiss() {
      const event = 'dismissed';
      let done;

      if (this.dismissable) {
        done = true;
        this.$emit(event, done);
        this.clear();
      } else {
        done = false;
        this.$emit(event, done);
      }
    },

    show() {
      if (!this.isVisible) {
        this.isVisible = true;
        this.$emit('show', this.clear);

        if (this.timeout) {
          setTimeout(() => {
            this.$emit('timeout');
            this.clear();
          }, this.timeout);
        }
      }
    },

    transitionEnd(event) {
      this.$emit('transitionend', event);
    },
  },

  mounted() {
    window.requestAnimationFrame(this.show);
  },
};

const NotificationCenter = {
  name: 'notification-center',

  components: {
    NotificationBar,
  },

  template: `
    <div>
      <notification-bar
        v-for="notification in active"

        :message="notification.message"
        :type="notification.type"
        :dismissable="notification.dismissable"
        :timeout="notification.timeout"

        @cleared="clear">
      </notification-bar>
    </div>
  `,

  props: {
    queue: {
      type: Array,
      required: true,
    },
  },

  data() {
    return {
      active: [],
    };
  },

  computed: {
    hasActiveNotification() {
      return this.active.length > 0;
    },

    hasQueuedNotification() {
      return this.queue.length > 0;
    },
  },

  watch: {
    queue() {
      if (this.hasQueuedNotification && !this.hasActiveNotification) {
        this.setNextActive();
      }
    },
  },

  methods: {
    setNextActive() {
      this.setActive(this.queue.shift());
    },

    setActive(notification) {
      this.active.push(notification);
    },

    removeActive() {
      this.active.pop();
    },

    clear() {
      this.active.pop();

      if (this.hasQueuedNotification) {
        this.$nextTick(this.setNextActive);
      }
    },
  },
};

window.vm = new Vue({
  components: {
    NotificationCenter,
  },

  el: '#app',

  template: `
    <div>
      <notification-center
        :queue="notifications">
      </notification-center>

      <label>
        <strong>Type</strong> <br>
        Error <input v-model="type" type="radio" name="type" value="error"> <br>
        Warning <input v-model="type" type="radio" name="type" value="warning"> <br>
        Info <input v-model="type" type="radio" name="type" value="info"> <br>
      </label>

      <label>
        <strong>Message</strong>
        <input v-model="message" type="text">
      </label>

      <label>
        <strong>Dismissable</strong>
        <input v-model="dismissable" type="checkbox">
      </label>

      <label>
        <strong>Timeout</strong>
        <input v-model="timeout" type="number" step="100" min="0">
      </label>

      <button @click="generateNotification">Generate notification</button>
    </div>
  `,

  data: {
    notifications: [],
    
    type: null,
    message: null,
    dismissable: null,
    timeout: null,
  },
  
  methods: {
    generateNotification() {
      const {
        type,
        message,
        dismissable,
        timeout,
      } = this;
      
      this.notifications.push({
        type,
        message,
        dismissable,
        timeout,
      });
      
      //this.type = this.message = this.dismissable = this.timeout = null;
    },
  },
});
.notification-bar {
  box-sizing: border-box;
  position: absolute;
  top: -3.2rem;
  right: 0;
  left: 0;
  z-index: 9999;
  width: 100%;
  height: 3.2rem;
  color: #fff;
  font-family: 'Avenir Next', sans-serif;
  font-size: 1.2em;
  line-height: 3.2rem;
  text-align: center;
  transition: top 266ms ease;
}
.notification-bar--error {
  background-color: #f02a4d;
}
.notification-bar--warning {
  background-color: #ffc107;
}
.notification-bar--info {
  background-color: #2196f3;
}
.notification-bar--visible {
  top: 0;
}

label {
  display: block;
  margin: 2rem 0;
  font-size: 1.4rem;
}
label:first-of-type {
  margin-top: 5rem;
}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <style>
      html {
        font-size: 62.5%;
      }
      
      body {
        font-family: sans-serif;
      }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
  </body>
</html>

编辑

您必须在setActive 函数中进行一些验证,就像您将空项目推入活动状态一样,您的某些验证失败了。

  setActive(notification) {
    if(notification.message){
    this.active.push(notification);
    }
  },

检查此fiddle

【讨论】:

  • 您指向的行仅清除输入字段和复选框。来自这些输入的数据已经被推送到 ViewModel 上的 notifications 属性中,并向下传递给 NotificationCenter 组件,该组件将其向下传递给 NotificationBar 组件。不影响NotificationBar的道具验证或问题中描述的实际问题。
  • @wing 我在该行中收到此错误:Vue warn]: Invalid prop: type check failed for prop "message". Expected String, got Null. (found in component &lt;notification-bar&gt;),如果此行没有正常工作。检查更新的答案。
  • 您是否首先向通知发送消息?我不打算为未在通知中发送消息的开发人员添加保护措施——这就是为什么已经有 prop 验证来检查消息是否为字符串,如果不是则抛出。无论您遇到什么问题,它都与我所面临的问题无关——当通知栏可见时,Chrome 中的动画/过渡会损坏。
  • @wing 我会说它在 Mac Chrome 版本 56.0.2924.87(64 位)中对我有用
  • okai,感谢您抽出宝贵时间,真的很困惑这个看似幻影的错误 - 我想我可能需要制作它的 GIF,以防人们实际看到错误但认为这是正常行为。
【解决方案2】:

在与 Vue 的贡献者 LinusBorg 讨论后,在 Vue forum 上,我们找到了导致此问题的可能原因:

[...] 这个问题很可能是 Vue 对 DOM 进行了异步修补,所以在调用 mounted() 时,组件的元素存在,但并不保证它们在 DOM 中。

所以现在,根据不同浏览器如何处理普通任务、微任务和动画帧的优先级,可能只是在 Chrome 中,当您通过 @987654325 更改类时,元素尚未在 DOM 中@

那样的话,动画效果自然不会出现。

我建议改用this.$nextTick()(保证元素已经在DOM中),或者干脆使用Vue为此提供的工具,即&lt;transition&gt;组件。

– LinusBorg,https://forum.vuejs.org/t/what-is-causing-this-broken-animation-transition-in-a-vue-js-component-in-chrome/7742/7

最初尝试使用 this.$nextTick,但在 Firefox 和 Chrome 中都失败了。

最终我能够使用&lt;transition&gt; 组件来实现这一切。

const NotificationBar = {
  name: 'notification-bar',
  
  template: `
    <transition
      name="visible"
      mode="out-in"

      @after-enter="show">
      <div
        :class="{
          'notification-bar': true,
          'notification-bar--error': isError,
          'notification-bar--warning': isWarning,
          'notification-bar--info': isInfo,
          'notification-bar--visible': isVisible,
        }"

        :key="id"

        @click="dismiss">
        {{ message }}
      </div>
    </transition>
  `,

  props: {
    message: {
      type: String,
      required: true,
    },

    type: {
      type: String,
      required: true,

      validator(value) {
        const valid = ['error', 'warning', 'info'];
        return valid.includes(value);
      },
    },

    id: {
      type: [Number, String],
      required: true,
    },

    dismissable: {
      type: Boolean,
      default: false,
    },

    timeout: {
      type: Number,
      default: 0,
    },
  },

  data() {
    return {
      isVisible: false,
    };
  },

  computed: {
    isError() {
      return this.type === 'error';
    },

    isWarning() {
      return this.type === 'warning';
    },

    isInfo() {
      return this.type === 'info';
    },
  },

  methods: {
    clear() {
      const event = 'clear';
      let done;

      if (this.isVisible) {
        done = true;
        this.$emit(event, done);
        this.isVisible = false;
      } else {
        done = false;
        this.$emit(event, done);
      }
    },

    dismiss() {
      const event = 'dismissed';
      let done;

      if (this.dismissable) {
        done = true;
        this.$emit(event, done);
        this.clear();
      } else {
        done = false;
        this.$emit(event, done);
      }
    },

    show() {
      if (!this.isVisible) {
        this.isVisible = true;
        this.$emit('show', this.clear);

        if (this.timeout) {
          setTimeout(() => {
            this.$emit('timeout');
            this.clear();
          }, this.timeout);
        }
      }
    },
  },
};

const NotificationCenter = {
  name: 'notification-center',
  
  template: `
    <div>
      <notification-bar
        v-if="hasQueuedNotification"

        :message="activeNotification.message"
        :type="activeNotification.type"
        :dismissable="activeNotification.dismissable"
        :timeout="activeNotification.timeout"
        :id="activeNotification.id"

        @clear="clear">
      </notification-bar>
    </div>
  `,

  components: {
    NotificationBar,
  },

  props: {
    queue: {
      type: Array,
      required: true,
    },
  },

  computed: {
    hasQueuedNotification() {
      return this.queue.length > 0;
    },

    activeNotification() {
      return this.queue[0];
    },
  },

  methods: {
    clear() {
      this.queue.shift();
    },
  },
};

window.vm = new Vue({
  components: {
    NotificationCenter,
  },

  el: '#app',

  template: `
    <div>
      <notification-center
        :queue="notifications">
      </notification-center>

      <label>
        <strong>Type</strong> <br>
        Error <input v-model="type" type="radio" name="type" value="error"> <br>
        Warning <input v-model="type" type="radio" name="type" value="warning"> <br>
        Info <input v-model="type" type="radio" name="type" value="info"> <br>
      </label>

      <label>
        <strong>Message</strong>
        <input v-model="message" type="text">
      </label>

      <label>
        <strong>Dismissable</strong>
        <input v-model="dismissable" type="checkbox">
      </label>

      <label>
        <strong>Timeout</strong>
        <input v-model="timeout" type="number" step="100" min="0">
      </label>

      <button @click="generateNotification">Generate notification</button>
    </div>
  `,

  data: {
    notifications: [],

    type: null,
    message: null,
    dismissable: null,
    timeout: null,

    dismissIndex: null,
    dismissMessage: null,
  },

  methods: {
    generateNotification() {
      const {
        type,
        message,
        dismissable,
        timeout,
      } = this;

      const id = Date.now();

      this.notifications.push({
        type,
        message,
        dismissable,
        timeout,
        id,
      });

      this.type = this.message = this.dismissable = this.timeout = null;
    },
  },
});
.notification-bar {
  box-sizing: border-box;
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  z-index: 9999;
  width: 100%;
  height: 3.2rem;
  color: #fff;
  font-family: 'Avenir Next', sans-serif;
  font-size: 1.2em;
  line-height: 3.2rem;
  text-align: center;
}
.notification-bar--error {
  background-color: #f02a4d;
}
.notification-bar--warning {
  background-color: #ffc107;
}
.notification-bar--info {
  background-color: #2196f3;
}
.notification-bar.visible-enter, .notification-bar.visible-leave-to {
  top: -3.2rem;
}
.notification-bar.visible-enter-to, .notification-bar.visible-leave {
  top: 0;
}
.notification-bar.visible-enter-active, .notification-bar.visible-leave-active {
  transition: top 266ms ease;
}

/* ================================================================== */
/*                                                                    */
/* ================================================================== */
html {
  font-size: 62.5%;
}

body {
  margin: 0;
  border: 1px solid black;
  font-family: sans-serif;
}

label {
  display: block;
  margin: 2rem 0;
  font-size: 1.4rem;
}

label:first-of-type {
  margin-top: 5rem;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title></title>
</head>
<body>
  <div id="app"></div>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
</body>
</html>

【讨论】:

    猜你喜欢
    • 2020-01-18
    • 2020-04-15
    • 2012-11-11
    • 2023-03-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-28
    相关资源
    最近更新 更多