Heim > Web-Frontend > Front-End-Fragen und Antworten > Welche Methoden zur Leistungsoptimierung gibt es für Vue?

Welche Methoden zur Leistungsoptimierung gibt es für Vue?

青灯夜游
Freigeben: 2022-01-10 15:45:20
Original
11195 Leute haben es durchsucht

Zu den Methoden zur Leistungsoptimierung gehören: 1. Verwenden Sie „v-slot:slotName“ anstelle von „slot="slotName"; 2. Vermeiden Sie die gleichzeitige Verwendung von „v-for“ und „v-if“. Verwenden Sie „v-for“, um den Schlüssel hinzuzufügen, und verwenden Sie nicht den Index als Schlüssel. 4. Verwenden Sie verzögertes Rendern usw.

Welche Methoden zur Leistungsoptimierung gibt es für Vue?

Die Betriebsumgebung dieses Tutorials: Windows 7-System, Vue-Version 2.9.6, DELL G3-Computer.

Bei unserer täglichen Entwicklung mit Vue oder anderen Frameworks werden wir mehr oder weniger auf einige Leistungsprobleme stoßen. Obwohl Vue uns intern bei vielen Optimierungen geholfen hat, gibt es immer noch einige Probleme, die wir aktiv vermeiden müssen. Ich habe einige Szenarien, die für Leistungsprobleme anfällig sind, und Optimierungstechniken für diese Probleme in meiner täglichen Arbeit und in Artikeln, die von verschiedenen Experten im Internet verfasst wurden, zusammengefasst. Ich hoffe, dass sie für Sie hilfreich sein werden.

Verwenden Sie v-slot:slotName anstelle von slot="slotName"v-slot:slotName,而不是slot="slotName"

v-slot是 2.6 新增的语法,具体可查看:Vue2.6,2.6 发布已经是快两年前的事情了,但是现在仍然有不少人仍然在使用slot="slotName"这个语法。虽然这两个语法都能达到相同的效果,但是内部的逻辑确实不一样的,下面来看下这两种方式有什么不同之处。

我们先来看下这两种语法分别会被编译成什么:

使用新的写法,对于父组件中的以下模板:

<child>
  <template v-slot:name>{{name}}</template>
</child>
Nach dem Login kopieren

会被编译成:

function render() {
  with (this) {
    return _c(&#39;child&#39;, {
      scopedSlots: _u([
        {
          key: &#39;name&#39;,
          fn: function () {
            return [_v(_s(name))]
          },
          proxy: true
        }
      ])
    })
  }
}
Nach dem Login kopieren

使用旧的写法,对于以下模板:

<child>
  <template slot="name">{{name}}</template>
</child>
Nach dem Login kopieren

会被编译成:

function render() {
  with (this) {
    return _c(
      &#39;child&#39;,
      [
        _c(
          &#39;template&#39;,
          {
            slot: &#39;name&#39;
          },
          [_v(_s(name))]
        )
      ],
    )
  }
}
Nach dem Login kopieren

通过编译后的代码可以发现,旧的写法是将插槽内容作为 children 渲染的,会在父组件的渲染函数中创建,插槽内容的依赖会被父组件收集(name 的 dep 收集到父组件的渲染 watcher),而新的写法将插槽内容放在了 scopedSlots 中,会在子组件的渲染函数中调用,插槽内容的依赖会被子组件收集(name 的 dep 收集到子组件的渲染 watcher),最终导致的结果就是:当我们修改 name 这个属性时,旧的写法是调用父组件的更新(调用父组件的渲染 watcher),然后在父组件更新过程中调用子组件更新(prePatch => updateChildComponent),而新的写法则是直接调用子组件的更新(调用子组件的渲染 watcher)。

这样一来,旧的写法在更新时就多了一个父组件更新的过程,而新的写法由于直接更新子组件,就会更加高效,性能更好,所以推荐始终使用v-slot:slotName语法。

使用计算属性

这一点已经被提及很多次了,计算属性最大的一个特点就是它是可以被缓存的,这个缓存指的是只要它的依赖的不发生改变,它就不会被重新求值,再次访问时会直接拿到缓存的值,在做一些复杂的计算时,可以极大提升性能。可以看以下代码:

<template>
  <div>{{superCount}}</div>
</template>
<script>
  export default {
    data() {
      return {
        count: 1
      }
    },
    computed: {
      superCount() {
        let superCount = this.count
        // 假设这里有个复杂的计算
        for (let i = 0; i < 10000; i++) {
          superCount++
        }
        return superCount
      }
    }
  }
</script>
Nach dem Login kopieren

这个例子中,在 created、mounted 以及模板中都访问了 superCount 属性,这三次访问中,实际上只有第一次即created

v-slot ist eine neue Syntax in 2.6 For Weitere Informationen finden Sie unter: Vue2.6, 2.6 wurde vor fast zwei Jahren veröffentlicht, aber viele Leute verwenden immer noch die slot="slotName"-Syntax. Obwohl beide Syntaxen den gleichen Effekt erzielen können, ist die interne Logik tatsächlich unterschiedlich. Schauen wir uns die Unterschiede zwischen den beiden Methoden an. Werfen wir zunächst einen Blick darauf, wie diese beiden Syntaxen kompiliert werden:

Mit der neuen Schreibmethode wird für die folgende Vorlage in der übergeordneten Komponente:

// UserProfile.vue
<template>
  <div class="user-profile">{{ name }}</div>
</template>
 
<script>
  export default {
    props: [&#39;name&#39;],
    data() {
      return {}
    },
    methods: {}
  }
</script>
<style scoped></style>
 
// App.vue
<template>
  <div id="app">
    <UserProfile v-for="item in list" :key="item" : />
  </div>
</template>
 
<script>
  import UserProfile from &#39;./components/UserProfile&#39;
 
  export default {
    name: &#39;App&#39;,
    components: { UserProfile },
    data() {
      return {
        list: Array(500)
          .fill(null)
          .map((_, idx) => &#39;Test&#39; + idx)
      }
    },
    beforeMount() {
      this.start = Date.now()
    },
    mounted() {
      console.log(&#39;用时:&#39;, Date.now() - this.start)
    }
  }
</script>
 
<style></style>
Nach dem Login kopieren

kompiliert in:

<template functional>
  <div class="user-profile">{{ props.name }}</div>
</template>
Nach dem Login kopieren

Mit der alten Schreibmethode , für Die folgende Vorlage:

<template>
  <div>
    <UserProfile :user="user1" v-if="visible" />
    <button @click="visible = !visible">toggle</button>
  </div>
</template>
Nach dem Login kopieren

wird kompiliert in:

<template>
  <div>
    <UserProfile :user="user1" v-show="visible" />
    <button @click="visible = !visible">toggle</button>
  </div>
</template>
Nach dem Login kopieren

Sie können dem kompilierten Code entnehmen, dass

die alte Schreibweise darin besteht, den Slot-Inhalt als untergeordnete Elemente zu rendern, die in der Rendering-Funktion des erstellt werden Die übergeordnete Komponente und die Abhängigkeiten des Slot-Inhalts werden von der übergeordneten Komponente erfasst (die Abhängigkeit des Namens wird vom Rendering-Watcher der übergeordneten Komponente erfasst), und die neue Schreibmethode platziert den Slot-Inhalt in ScopedSlots, die in aufgerufen werden Die Rendering-Funktion der untergeordneten Komponente wird von der untergeordneten Komponentensammlung erfasst (die Dep des Namens erfasst den Rendering-Watcher der untergeordneten Komponente). Das Endergebnis ist: wenn wir das Namensattribut ändern Die alte Schreibweise besteht darin, die Aktualisierung der übergeordneten Komponente aufzurufen (den Rendering-Watcher der übergeordneten Komponente aufzurufen) und dann während des Aktualisierungsprozesses der übergeordneten Komponente die Aktualisierung der untergeordneten Komponente (prePatch => updateChildComponent) aufzurufen Die neue Schreibmethode besteht darin, die Aktualisierung der untergeordneten Komponente direkt aufzurufen (den Rendering-Watcher der untergeordneten Komponente aufzurufen).

Auf diese Weise fügt die alte Schreibmethode beim Aktualisieren einen Aktualisierungsprozess für übergeordnete Komponenten hinzu, während die neue Schreibmethode effizienter ist und eine bessere Leistung erbringt, da sie die untergeordneten Komponenten direkt aktualisiert. Daher wird empfohlen, immer v zu verwenden - Slot:SlotName-Syntax.

Berechnete Eigenschaften verwenden

Eines der größten Merkmale berechneter Eigenschaften ist, dass sie zwischengespeichert werden können, solange sich ihre Abhängigkeiten nicht ändern neu ausgewertet werden, und der zwischengespeicherte Wert wird beim erneuten Zugriff direkt abgerufen, was die Leistung bei einigen komplexen Berechnungen erheblich verbessern kann. Sie können den folgenden Code sehen: 🎜
function render() {
  with (this) {
    return _c(
      &#39;div&#39;,
      [
        visible
          ? _c(&#39;UserProfile&#39;, {
              attrs: {
                user: user1
              }
            })
          : _e(),
        _c(
          &#39;button&#39;,
          {
            on: {
              click: function ($event) {
                visible = !visible
              }
            }
          },
          [_v(&#39;toggle&#39;)]
        )
      ],
    )
  }
}
Nach dem Login kopieren
Nach dem Login kopieren
🎜In diesem Beispiel wird auf das Attribut superCount in erstellt, gemountet und in einer Vorlage zugegriffen. Von diesen drei Zugriffen wird auf superCount tatsächlich nur beim ersten Mal zugegriffen, nämlich erstellt</code > Auswertung: Da sich das Zählattribut nicht geändert hat, wird der zwischengespeicherte Wert in den anderen beiden Fällen direkt zurückgegeben. 🎜🎜🎜Funktionskomponenten verwenden🎜🎜🎜Wenn wir einige Komponenten nur zum Anzeigen einiger Daten verwenden und keinen Status verwalten, Daten überwachen usw. müssen, können wir Funktionskomponenten verwenden. Funktionskomponenten sind zustandslos und instanzlos. Sie müssen den Status während der Initialisierung nicht initialisieren, keine Instanzen erstellen oder sich mit Lebenszyklen befassen. Sie sind leichter und weisen eine bessere Leistung auf. Informationen zur spezifischen Verwendung von Funktionskomponenten finden Sie in der offiziellen Dokumentation: Funktionskomponenten 🎜🎜Wir können eine einfache Demo schreiben, um diese Optimierung zu überprüfen: 🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">function render() { with (this) { return _c( &amp;#39;div&amp;#39;, [ _c(&amp;#39;UserProfile&amp;#39;, { directives: [ { name: &amp;#39;show&amp;#39;, rawName: &amp;#39;v-show&amp;#39;, value: visible, expression: &amp;#39;visible&amp;#39; } ], attrs: { user: user1 } }), _c( &amp;#39;button&amp;#39;, { on: { click: function ($event) { visible = !visible } } }, [_v(&amp;#39;toggle&amp;#39;)] ) ], ) } }</pre><div class="contentsignin">Nach dem Login kopieren</div></div><div class="contentsignin">Nach dem Login kopieren</div></div>🎜UserProfile Diese Komponente rendert nur den Namen der Requisiten und ruft ihn dann in App.vue auf Zählen Sie 500 Mal die Zeit von beforeMount bis zur Bereitstellung. Dies ist die Zeit, die zum Initialisieren von 500 Unterkomponenten (UserProfile) benötigt wird. 🎜🎜Nach vielen Versuchen habe ich festgestellt, dass die Initialisierungszeit etwa 30 ms betrug, also ändern wir es jetzt in UserProfile und ändern es in eine funktionale Komponente: 🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">el.style.display = value ? el.__vOriginalDisplay : &amp;#39;none&amp;#39;</pre><div class="contentsignin">Nach dem Login kopieren</div></div><div class="contentsignin">Nach dem Login kopieren</div></div>🎜Nach vielen weiteren Versuchen zu diesem Zeitpunkt betrug die Initialisierungszeit etwa 10 ms -15 ms, was ausreicht, um zu zeigen, dass funktionale Komponenten eine bessere Leistung haben als zustandsbehaftete Komponenten. 🎜🎜🎜Verwenden Sie V-Show und V-If in Kombination mit der Szene.🎜🎜🎜Das Folgende sind zwei Vorlagen, die V-Show und V-If verwenden. Bevor wir ihre Leistungsunterschiede diskutieren, analysieren wir zunächst die Unterschiede zwischen den beiden. Darunter wird die V-IF-Vorlage wie folgt kompiliert: 🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">function render() { with (this) { return _c( &amp;#39;div&amp;#39;, [ visible ? _c(&amp;#39;UserProfile&amp;#39;, { attrs: { user: user1 } }) : _e(), _c( &amp;#39;button&amp;#39;, { on: { click: function ($event) { visible = !visible } } }, [_v(&amp;#39;toggle&amp;#39;)] ) ], ) } }</pre><div class="contentsignin">Nach dem Login kopieren</div></div><div class="contentsignin">Nach dem Login kopieren</div></div><p>可以看到,v-if 的部分被转换成了一个三元表达式,visible 为 true 时,创建一个 UserProfile 的 vnode,否则创建一个空 vnode,在 patch 的时候,新旧节点不一样,就会移除旧的节点或创建新的节点,这样的话<code>UserProfile也会跟着创建 / 销毁。如果UserProfile组件里有很多 DOM,或者要执行很多初始化 / 销毁逻辑,那么随着 visible 的切换,势必会浪费掉很多性能。这个时候就可以用 v-show 进行优化,我们来看下 v-show 编译后的代码:

function render() {
  with (this) {
    return _c(
      &#39;div&#39;,
      [
        _c(&#39;UserProfile&#39;, {
          directives: [
            {
              name: &#39;show&#39;,
              rawName: &#39;v-show&#39;,
              value: visible,
              expression: &#39;visible&#39;
            }
          ],
          attrs: {
            user: user1
          }
        }),
        _c(
          &#39;button&#39;,
          {
            on: {
              click: function ($event) {
                visible = !visible
              }
            }
          },
          [_v(&#39;toggle&#39;)]
        )
      ],
    )
  }
}
Nach dem Login kopieren
Nach dem Login kopieren

v-show被编译成了directives,实际上,v-show 是一个 Vue 内部的指令,在这个指令的代码中,主要执行了以下逻辑:

el.style.display = value ? el.__vOriginalDisplay : &#39;none&#39;
Nach dem Login kopieren
Nach dem Login kopieren

它其实是通过切换元素的 display 属性来控制的,和 v-if 相比,不需要在 patch 阶段创建 / 移除节点,只是根据v-show上绑定的值来控制 DOM 元素的style.display属性,在频繁切换的场景下就可以节省很多性能。

但是并不是说v-show可以在任何情况下都替换v-if,如果初始值是false时,v-if并不会创建隐藏的节点,但是v-show会创建,并通过设置style.display=&#39;none&#39;来隐藏,虽然外表看上去这个 DOM 都是被隐藏的,但是v-show已经完整的走了一遍创建的流程,造成了性能的浪费。

所以,v-if的优势体现在初始化时,v-show体现在更新时,当然并不是要求你绝对按照这个方式来,比如某些组件初始化时会请求数据,而你想先隐藏组件,然后在显示时能立刻看到数据,这时候就可以用v-show,又或者你想每次显示这个组件时都是最新的数据,那么你就可以用v-if,所以我们要结合具体业务场景去选一个合适的方式。

使用 keep-alive

在动态组件的场景下:

<template>
  <div>
    <component :is="currentComponent" />
  </div>
</template>
Nach dem Login kopieren

这个时候有多个组件来回切换,currentComponent每变一次,相关的组件就会销毁 / 创建一次,如果这些组件比较复杂的话,就会造成一定的性能压力,其实我们可以使用 keep-alive 将这些组件缓存起来:

<template>
  <div>
    <keep-alive>
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>
Nach dem Login kopieren

keep-alive的作用就是将它包裹的组件在第一次渲染后就缓存起来,下次需要时就直接从缓存里面取,避免了不必要的性能浪费,在讨论上个问题时,说的是v-show初始时性能压力大,因为它要创建所有的组件,其实可以用keep-alive优化下:

<template>
  <div>
    <keep-alive>
      <UserProfileA v-if="visible" />
      <UserProfileB v-else />
    </keep-alive>
  </div>
</template>
Nach dem Login kopieren

这样的话,初始化时不会渲染UserProfileB组件,当切换visible时,才会渲染UserProfileB组件,同时被keep-alive缓存下来,频繁切换时,由于是直接从缓存中取,所以会节省很多性能,所以这种方式在初始化和更新时都有较好的性能。

但是keep-alive并不是没有缺点,组件被缓存时会占用内存,属于空间和时间上的取舍,在实际开发中要根据场景选择合适的方式。

避免 v-for 和 v-if 同时使用

这一点是 Vue 官方的风格指南中明确指出的一点:Vue 风格指南

如以下模板:

<ul>
  <li v-for="user in users" v-if="user.isActive" :key="user.id">
    {{ user.name }}
  </li>
</ul>
Nach dem Login kopieren

会被编译成:

// 简化版
function render() {
  return _c(
    &#39;ul&#39;,
    this.users.map((user) => {
      return user.isActive
        ? _c(
            &#39;li&#39;,
            {
              key: user.id
            },
            [_v(_s(user.name))]
          )
        : _e()
    }),
  )
}
Nach dem Login kopieren

可以看到,这里是先遍历(v-for),再判断(v-if),这里有个问题就是:如果你有一万条数据,其中只有 100 条是isActive状态的,你只希望显示这 100 条,但是实际在渲染时,每一次渲染,这一万条数据都会被遍历一遍。比如你在这个组件内的其他地方改变了某个响应式数据时,会触发重新渲染,调用渲染函数,调用渲染函数时,就会执行到上面的代码,从而将这一万条数据遍历一遍,即使你的users没有发生任何改变。

为了避免这个问题,在此场景下你可以用计算属性代替:

<template>
  <div>
    <ul>
      <li v-for="user in activeUsers" :key="user.id">{{ user.name }}</li>
    </ul>
  </div>
</template>
 
<script>
  export default {
    // ...
    computed: {
      activeUsers() {
        return this.users.filter((user) => user.isActive)
      }
    }
  }
</script>
复制代码
Nach dem Login kopieren

这样只会在users发生改变时才会执行这段遍历的逻辑,和之前相比,避免了不必要的性能浪费。

始终为 v-for 添加 key,并且不要将 index 作为的 key

这一点是 Vue 风格指南中明确指出的一点,同时也是面试时常问的一点,很多人都习惯的将 index 作为 key,这样其实是不太好的,index 作为 key 时,将会让 diff 算法产生错误的判断,从而带来一些性能问题,你可以看下 ssh 大佬的文章,深入分析下,为什么 Vue 中不要用 index 作为 key。在这里我也通过一个例子来简单说明下当 index 作为 key 时是如何影响性能的。

看下这个例子:

const Item = {
  name: &#39;Item&#39;,
  props: [&#39;message&#39;, &#39;color&#39;],
  render(h) {
    debugger
    console.log(&#39;执行了Item的render&#39;)
    return h(&#39;div&#39;, { style: { color: this.color } }, [this.message])
  }
}
 
new Vue({
  name: &#39;Parent&#39;,
  template: `
  <div @click="reverse" class="list">
    <Item
      v-for="(item,index) in list"
      :key="item.id"
      :message="item.message"
      :color="item.color"
    />
  </div>`,
  components: { Item },
  data() {
    return {
      list: [
        { id: &#39;a&#39;, color: &#39;#f00&#39;, message: &#39;a&#39; },
        { id: &#39;b&#39;, color: &#39;#0f0&#39;, message: &#39;b&#39; }
      ]
    }
  },
  methods: {
    reverse() {
      this.list.reverse()
    }
  }
}).$mount(&#39;#app&#39;)
Nach dem Login kopieren

这里有一个 list,会渲染出来a b,点击后会执行reverse方法将这个 list 颠倒下顺序,你可以将这个例子复制下来,在自己的电脑上看下效果。

我们先来分析用id作为 key 时,点击时会发生什么,

由于 list 发生了改变,会触发Parent组件的重新渲染,拿到新的vnode,和旧的vnode去执行patch,我们主要关心的就是patch过程中的updateChildren逻辑,updateChildren就是对新旧两个children执行diff算法,使尽可能地对节点进行复用,对于我们这个例子而言,此时旧的children是:

;[
  {
    tag: &#39;Item&#39;,
    key: &#39;a&#39;,
    propsData: {
      color: &#39;#f00&#39;,
      message: &#39;红色&#39;
    }
  },
  {
    tag: &#39;Item&#39;,
    key: &#39;b&#39;,
    propsData: {
      color: &#39;#0f0&#39;,
      message: &#39;绿色&#39;
    }
  }
]
Nach dem Login kopieren

执行reverse后的新的children是:

;[
  {
    tag: &#39;Item&#39;,
    key: &#39;b&#39;,
    propsData: {
      color: &#39;#0f0&#39;,
      message: &#39;绿色&#39;
    }
  },
  {
    tag: &#39;Item&#39;,
    key: &#39;a&#39;,
    propsData: {
      color: &#39;#f00&#39;,
      message: &#39;红色&#39;
    }
  }
]
Nach dem Login kopieren

此时执行updateChildrenupdateChildren会对新旧两组 children 节点的循环进行对比:

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  if (isUndef(oldStartVnode)) {
    oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
  } else if (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]
  } else if (sameVnode(oldStartVnode, newStartVnode)) {
    // 对新旧节点执行patchVnode
    // 移动指针
  } else if (sameVnode(oldEndVnode, newEndVnode)) {
    // 对新旧节点执行patchVnode
    // 移动指针
  } else if (sameVnode(oldStartVnode, newEndVnode)) {
    // 对新旧节点执行patchVnode
    // 移动oldStartVnode节点
    // 移动指针
  } else if (sameVnode(oldEndVnode, newStartVnode)) {
    // 对新旧节点执行patchVnode
    // 移动oldEndVnode节点
    // 移动指针
  } else {
    //...
  }
}
Nach dem Login kopieren

通过sameVnode判断两个节点是相同节点的话,就会执行相应的逻辑:

function sameVnode(a, b) {
  return (
    a.key === b.key &&
    ((a.tag === b.tag &&
      a.isComment === b.isComment &&
      isDef(a.data) === isDef(b.data) &&
      sameInputType(a, b)) ||
      (isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)))
  )
}
Nach dem Login kopieren

sameVnode主要就是通过 key 去判断,由于我们颠倒了 list 的顺序,所以第一轮对比中:sameVnode(oldStartVnode, newEndVnode)成立,即旧的首节点和新的尾节点是同一个节点,此时会执行patchVnode逻辑,patchVnode中会执行prePatchprePatch中会更新 props,此时我们的两个节点的propsData是相同的,都为{color: &#39;#0f0&#39;,message: &#39;绿色&#39;},这样的话Item组件的 props 就不会更新,Item也不会重新渲染。再回到updateChildren中,会继续执行"移动oldStartVnode节点"的操作,将 DOM 元素。移动到正确位置,其他节点对比也是同样的流程。

可以发现,在整个流程中,只是移动了节点,并没有触发 Item 组件的重新渲染,这样实现了节点的复用。

我们再来看下使用index作为 key 的情况,使用index时,旧的children是:

;[
  {
    tag: &#39;Item&#39;,
    key: 0,
    propsData: {
      color: &#39;#f00&#39;,
      message: &#39;红色&#39;
    }
  },
  {
    tag: &#39;Item&#39;,
    key: 1,
    propsData: {
      color: &#39;#0f0&#39;,
      message: &#39;绿色&#39;
    }
  }
]
Nach dem Login kopieren

执行reverse后的新的children是:

;[
  {
    tag: &#39;Item&#39;,
    key: 0,
    propsData: {
      color: &#39;#0f0&#39;,
      message: &#39;绿色&#39;
    }
  },
  {
    tag: &#39;Item&#39;,
    key: 1,
    propsData: {
      color: &#39;#f00&#39;,
      message: &#39;红色&#39;
    }
  }
]
Nach dem Login kopieren

这里和id作为 key 时的节点就有所不同了,虽然我们把 list 顺序颠倒了,但是 key 的顺序却没变,在updateChildrensameVnode(oldStartVnode, newStartVnode)将会成立,即旧的首节点和新的首节点相同,此时执行patchVnode -> prePatch -> 更新props,这个时候旧的 propsData 是{color: &#39;#f00&#39;,message: &#39;红色&#39;},新的 propsData 是{color: &#39;#0f0&#39;,message: &#39;绿色&#39;},更新过后,Item 的 props 将会发生改变,会触发 Item 组件的重新渲染

这就是 index 作为 key 和 id 作为 key 时的区别,id 作为 key 时,仅仅是移动了节点,并没有触发 Item 的重新渲染。index 作为 key 时,触发了 Item 的重新渲染,可想而知,当 Item 是一个复杂的组件时,必然会引起性能问题。

上面的流程比较复杂,涉及的也比较多,可以拆开写好几篇文章,有些地方我只是简略的说了一下,如果你不是很明白的话,你可以把上面的例子复制下来,在自己的电脑上调式,我在 Item 的渲染函数中加了打印日志和 debugger,你可以分别用 id 和 index 作为 key 尝试下,你会发现 id 作为 key 时,Item 的渲染函数没有执行,但是 index 作为 key 时,Item 的渲染函数执行了,这就是这两种方式的区别。

延迟渲染

延迟渲染就是分批渲染,假设我们某个页面里有一些组件在初始化时需要执行复杂的逻辑:

<template>
  <p>
    <!-- Heavy组件初始化时需要执行很复杂的逻辑,执行大量计算 -->
    <Heavy1 />
    <Heavy2 />
    <Heavy3 />
    <Heavy4 />
  </p>
</template>
Nach dem Login kopieren

这将会占用很长时间,导致帧数下降、卡顿,其实可以使用分批渲染的方式来进行优化,就是先渲染一部分,再渲染另一部分:

参考黄轶老师揭秘 Vue.js 九个性能优化技巧中的代码:

<template>
  <p>
    <Heavy v-if="defer(1)" />
    <Heavy v-if="defer(2)" />
    <Heavy v-if="defer(3)" />
    <Heavy v-if="defer(4)" />
  </p>
</template>

<script>
export default {
  data() {
    return {
      displayPriority: 0
    }
  },
  mounted() {
    this.runDisplayPriority()
  },
  methods: {
    runDisplayPriority() {
      const step = () => {
        requestAnimationFrame(() => {
          this.displayPriority++
          if (this.displayPriority < 10) {
            step()
          }
        })
      }
      step()
    },
    defer(priority) {
      return this.displayPriority >= priority
    }
  }
}
</script>
Nach dem Login kopieren

其实原理很简单,主要是维护displayPriority变量,通过requestAnimationFrame在每一帧渲染时自增,然后我们就可以在组件上通过v-if="defer(n)"使displayPriority增加到某一值时再渲染,这样就可以避免 js 执行时间过长导致的卡顿问题了。

使用非响应式数据

在 Vue 组件初始化数据时,会递归遍历在 data 中定义的每一条数据,通过Object.defineProperty将数据改成响应式,这就意味着如果 data 中的数据量很大的话,在初始化时将会使用很长的时间去执行Object.defineProperty, 也就会带来性能问题,这个时候我们可以强制使数据变为非响应式,从而节省时间,看下这个例子:

<template>
  <p>
    <ul>
      <li v-for="item in heavyData" :key="item.id">{{ item.name }}</li>
    </ul>
  </p>
</template>

<script>
// 一万条数据
const heavyData = Array(10000)
  .fill(null)
  .map((_, idx) => ({ name: &#39;test&#39;, message: &#39;test&#39;, id: idx }))

export default {
  data() {
    return {
      heavyData: heavyData
    }
  },
  beforeCreate() {
    this.start = Date.now()
  },
  created() {
    console.log(Date.now() - this.start)
  }
}
</script>
Nach dem Login kopieren

heavyData中有一万条数据,这里统计了下从beforeCreatecreated经历的时间,对于这个例子而言,这个时间基本上就是初始化数据的时间。

我在我个人的电脑上多次测试,这个时间一直在40-50ms,然后我们通过Object.freeze()方法,将heavyData变为非响应式的再试下:

//...
data() {
  return {
    heavyData: Object.freeze(heavyData)
  }
}
//...
Nach dem Login kopieren

改完之后再试下,初始化数据的时间变成了0-1ms,快了有40ms,这40ms都是递归遍历heavyData执行Object.defineProperty的时间。

那么,为什么Object.freeze()会有这样的效果呢?对某一对象使用Object.freeze()后,将不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。

而 Vue 在将数据改造成响应式之前有个判断:

export function observe(value, asRootData) {
  // ...省略其他逻辑
  if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  // ...省略其他逻辑
}
Nach dem Login kopieren

这个判断条件中有一个Object.isExtensible(value),这个方法是判断一个对象是否是可扩展的,由于我们使用了Object.freeze(),这里肯定就返回了false,所以就跳过了下面的过程,自然就省了很多时间。

实际上,不止初始化数据时有影响,你可以用上面的例子统计下从createdmounted所用的时间,在我的电脑上不使用Object.freeze()时,这个时间是60-70ms,使用Object.freeze()后降到了40-50ms,这是因为在渲染函数中读取heavyData中的数据时,会执行到通过Object.defineProperty定义的getter方法,Vue 在这里做了一些收集依赖的处理,肯定就会占用一些时间,由于使用了Object.freeze()后的数据是非响应式的,没有了收集依赖的过程,自然也就节省了性能。

由于访问响应式数据会走到自定义 getter 中并收集依赖,所以平时使用时要避免频繁访问响应式数据,比如在遍历之前先将这个数据存在局部变量中,尤其是在计算属性、渲染函数中使用,关于这一点更具体的说明,你可以看黄奕老师的这篇文章:Local variables

但是这样做也不是没有任何问题的,这样会导致heavyData下的数据都不是响应式数据,你对这些数据使用computedwatch等都不会产生效果,不过通常来说这种大量的数据都是展示用的,如果你有特殊的需求,你可以只对这种数据的某一层使用Object.freeze(),同时配合使用上文中的延迟渲染、函数式组件等,可以极大提升性能。

模板编译和渲染函数、JSX 的性能差异

Vue 项目不仅可以使用 SFC 的方式开发,也可以使用渲染函数或 JSX 开发,很多人认为仅仅是只是开发方式不同,却不知这些开发方式之间也有性能差异,甚至差异很大,这一节我就找些例子来说明下,希望你以后在选择开发方式时有更多衡量的标准。

其实 Vue2 模板编译中的性能优化不多,Vue3 中有很多,Vue3 通过编译和运行时结合的方式提升了很大的性能,但是由于本篇文章讲的是 Vue2 的性能优化,并且 Vue2 现在还是有很多人在使用,所以我就挑 Vue2 模板编译中的一点来说下。

静态节点

下面这个模板:

<p>你好! <span>Hello</span></p>
Nach dem Login kopieren

会被编译成:

function render() {
  with (this) {
    return _m(0)
  }
}
Nach dem Login kopieren

可以看到和普通的渲染函数是有些不一样的,下面我们来看下为什么会编译成这样的代码。

Vue 的编译会经过optimize过程,这个过程中会标记静态节点,具体内容可以看黄奕老师写的这个文档:Vue2 编译 - optimize 标记静态节点。

codegen阶段判断到静态节点的标记会走到genStatic的分支:

function genStatic(el, state) {
  el.staticProcessed = true
  const originalPreState = state.pre
  if (el.pre) {
    state.pre = el.pre
  }
  state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`)
  state.pre = originalPreState
  return `_m(${state.staticRenderFns.length - 1}${
    el.staticInFor ? &#39;,true&#39; : &#39;&#39;
  })`
}
Nach dem Login kopieren

这里就是生成代码的关键逻辑,这里会把渲染函数保存在staticRenderFns里,然后拿到当前值的下标生成_m函数,这就是为什么我们会得到_m(0)

这个_m其实是renderStatic的缩写:

export function renderStatic(index, isInFor) {
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  if (tree && !isInFor) {
    return tree
  }
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this
  )
  markStatic(tree, `__static__${index}`, false)
  return tree
}

function markStatic(tree, key) {
  if (Array.isArray(tree)) {
    for (let i = 0; i < tree.length; i++) {
      if (tree[i] && typeof tree[i] !== &#39;string&#39;) {
        markStaticNode(tree[i], `${key}_${i}`, isOnce)
      }
    }
  } else {
    markStaticNode(tree, key, isOnce)
  }
}

function markStaticNode(node, key, isOnce) {
  node.isStatic = true
  node.key = key
  node.isOnce = isOnce
}
Nach dem Login kopieren

renderStatic的内部实现比较简单,先是获取到组件实例的_staticTrees,如果没有就创建一个,然后尝试从_staticTrees上获取之前缓存的节点,获取到的话就直接返回,否则就从staticRenderFns上获取到对应的渲染函数执行并将结果缓存到_staticTrees上,这样下次再进入这个函数时就会直接从缓存上返回结果。

拿到节点后还会通过markStatic将节点打上isStatic等标记,标记为isStatic的节点会直接跳过patchVnode阶段,因为静态节点是不会变的,所以也没必要 patch,跳过 patch 可以节省性能。

通过编译和运行时结合的方式,可以帮助我们很好的提升应用性能,这是渲染函数 / JSX 很难达到的,当然不是说不能用 JSX,相比于模板,JSX 更加灵活,两者有各自的使用场景。在这里写这些是希望能给你提供一些技术选型的标准。

Vue2 的编译优化除了静态节点,还有插槽,createElement 等。

【相关推荐:vue.js教程

Das obige ist der detaillierte Inhalt vonWelche Methoden zur Leistungsoptimierung gibt es für Vue?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage