Quelles sont les méthodes d'optimisation des performances pour vue ?

青灯夜游
Libérer: 2022-01-10 15:45:20
original
11121 Les gens l'ont consulté

Les méthodes d'optimisation des performances incluent : 1. Utilisez "v-slot:slotName" au lieu de "slot="slotName"" ; 2. Évitez d'utiliser "v-for" et "v-if" en même temps ; utilisez " v-for" pour ajouter une clé et n'utilisez pas d'index comme clé ; 4. Utilisez le rendu retardé, etc.

Quelles sont les méthodes d'optimisation des performances pour vue ?

L'environnement d'exploitation de ce tutoriel : système Windows 7, vue version 2.9.6, ordinateur DELL G3.

Dans notre développement quotidien avec Vue ou d'autres frameworks, nous rencontrerons plus ou moins des problèmes de performances. Bien que Vue nous ait aidé à faire de nombreuses optimisations en interne, il reste encore certains problèmes que nous devons activement éviter. J'ai résumé certains scénarios sujets à des problèmes de performances et des techniques d'optimisation pour ces problèmes dans mon travail quotidien et dans des articles rédigés par divers experts sur Internet. J'en discuterai dans cet article, j'espère que cela vous sera utile.

Utilisez v-slot:slotName au lieu de 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>
Copier après la connexion

会被编译成:

function render() {
  with (this) {
    return _c(&#39;child&#39;, {
      scopedSlots: _u([
        {
          key: &#39;name&#39;,
          fn: function () {
            return [_v(_s(name))]
          },
          proxy: true
        }
      ])
    })
  }
}
Copier après la connexion

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

<child>
  <template slot="name">{{name}}</template>
</child>
Copier après la connexion

会被编译成:

function render() {
  with (this) {
    return _c(
      &#39;child&#39;,
      [
        _c(
          &#39;template&#39;,
          {
            slot: &#39;name&#39;
          },
          [_v(_s(name))]
        )
      ],
    )
  }
}
Copier après la connexion

通过编译后的代码可以发现,旧的写法是将插槽内容作为 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>
Copier après la connexion

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

v-slot est une nouvelle syntaxe dans la version 2.6 pour Pour plus de détails, veuillez consulter : Vue2.6, 2.6 a été publié il y a presque deux ans, mais de nombreuses personnes utilisent encore la syntaxe slot="slotName". Bien que les deux syntaxes puissent produire le même effet, la logique interne est effectivement différente. Examinons les différences entre les deux méthodes. Voyons d'abord dans quoi ces deux syntaxes seront compilées :

En utilisant la nouvelle méthode d'écriture, pour le modèle suivant dans le composant parent :

// 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>
Copier après la connexion

sera compilé en :

<template functional>
  <div class="user-profile">{{ props.name }}</div>
</template>
Copier après la connexion

En utilisant l'ancienne méthode d'écriture , pour Le modèle suivant :

<template>
  <div>
    <UserProfile :user="user1" v-if="visible" />
    <button @click="visible = !visible">toggle</button>
  </div>
</template>
Copier après la connexion

sera compilé en :

<template>
  <div>
    <UserProfile :user="user1" v-show="visible" />
    <button @click="visible = !visible">toggle</button>
  </div>
</template>
Copier après la connexion

Vous pouvez trouver dans le code compilé que

l'ancienne façon d'écrire consiste à restituer le contenu de la machine à sous en tant qu'enfants, qui seront créés dans la fonction de rendu du composant parent et les dépendances du contenu du slot seront collectées par le composant parent (le dépôt du nom est collecté vers l'observateur de rendu du composant parent), et la nouvelle méthode d'écriture place le contenu du slot dans scopedSlots, qui sera appelé dans le fonction de rendu du composant enfant.Les dépendances du contenu du slot seront collectées par le composant enfant.Collection de composants (le dépôt de nom collecte l'observateur de rendu du composant enfant)

, le résultat final est : lorsque nous modifions l'attribut name. , l'ancienne façon d'écrire consiste à appeler la mise à jour du composant parent (appeler l'observateur de rendu du composant parent), puis dans Pendant le processus de mise à jour du composant parent, la mise à jour du composant enfant (prePatch => updateChildComponent) est appelée, tandis que la nouvelle méthode d'écriture consiste à appeler directement la mise à jour du composant enfant (appeler l'observateur de rendu du composant enfant). De cette façon, l'ancienne méthode d'écriture ajoute un processus de mise à jour du composant parent lors de la mise à jour, tandis que la nouvelle méthode d'écriture sera plus efficace et plus performante car elle met directement à jour les composants enfants, il est donc recommandé de toujours utiliser v - syntaxe slot:slotName.

Utiliser les propriétés calculées

🎜Cela a été mentionné à plusieurs reprises. L'une des principales caractéristiques des propriétés calculées est qu'elles peuvent être mises en cache. Ce cache signifie que tant que ses dépendances ne changent pas, elles ne changeront pas. sera réévaluée et la valeur mise en cache sera obtenue directement lors d'un nouvel accès, ce qui peut considérablement améliorer les performances lors de certains calculs complexes. Vous pouvez voir le code suivant : 🎜
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;)]
        )
      ],
    )
  }
}
Copier après la connexion
Copier après la connexion
🎜Dans cet exemple, l'attribut superCount est accédé en création, monté et modèle. Parmi ces trois accès, superCount n'est en fait accédé que dans un premier temps, qui est créé</code. > Évaluation, puisque l'attribut count n'a pas changé, la valeur mise en cache est renvoyée directement dans les deux autres fois. 🎜🎜🎜Utiliser des composants fonctionnels🎜🎜🎜Pour certains composants, si nous les utilisons uniquement pour afficher certaines données et n'avons pas besoin de gérer l'état, de surveiller les données, etc., alors nous pouvons utiliser des composants fonctionnels. Les composants fonctionnels sont sans état et sans instance. Ils n'ont pas besoin d'initialiser l'état lors de l'initialisation, de créer des instances ou de gérer des cycles de vie. Par rapport aux composants avec état, ils sont plus légers et offrent de meilleures performances. Pour une utilisation spécifique des composants fonctionnels, veuillez vous référer à la documentation officielle : Composants fonctionnels 🎜🎜Nous pouvons écrire une démo simple pour vérifier cette optimisation : 🎜<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">Copier après la connexion</div></div><div class="contentsignin">Copier après la connexion</div></div>🎜UserProfile Ce composant restitue uniquement le nom des accessoires puis l'appelle dans App.vue 500 fois, comptez le temps nécessaire entre beforeMount et le montage, qui est le temps nécessaire pour initialiser 500 sous-composants (UserProfile). 🎜🎜Après de nombreuses tentatives, j'ai constaté que le temps d'initialisation était d'environ 30 ms, alors maintenant nous le changeons en UserProfile et le changeons en composant fonctionnel : 🎜<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">Copier après la connexion</div></div><div class="contentsignin">Copier après la connexion</div></div>🎜Après de nombreuses autres tentatives à ce moment, le temps d'initialisation est d'environ 10 ms -15 ms, ce qui suffit pour montrer que les composants fonctionnels ont de meilleures performances que les composants avec état. 🎜🎜🎜Utilisez v-show et v-if en combinaison avec la scène🎜🎜🎜Voici deux modèles utilisant v-show et v-if🎜<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;">&lt;template&gt; &lt;div&gt; &lt;component :is=&quot;currentComponent&quot; /&gt; &lt;/div&gt; &lt;/template&gt;</pre><div class="contentsignin">Copier après la connexion</div></div><div class="contentsignin">Copier après la connexion</div></div><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;">&lt;template&gt; &lt;div&gt; &lt;keep-alive&gt; &lt;component :is=&quot;currentComponent&quot; /&gt; &lt;/keep-alive&gt; &lt;/div&gt; &lt;/template&gt;</pre><div class="contentsignin">Copier après la connexion</div></div><div class="contentsignin">Copier après la connexion</div></div>🎜Les deux sont utilisés pour contrôler l'affichage de certains composants ou DOM/Hide, avant de discuter de leurs différences de performances, analysons d’abord les différences entre les deux. Parmi eux, le modèle v-if sera compilé en : 🎜<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">Copier après la connexion</div></div><div class="contentsignin">Copier après la connexion</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;)]
        )
      ],
    )
  }
}
Copier après la connexion
Copier après la connexion

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

el.style.display = value ? el.__vOriginalDisplay : &#39;none&#39;
Copier après la connexion
Copier après la connexion

它其实是通过切换元素的 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>
Copier après la connexion
Copier après la connexion

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

<template>
  <div>
    <keep-alive>
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>
Copier après la connexion
Copier après la connexion

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

<template>
  <div>
    <keep-alive>
      <UserProfileA v-if="visible" />
      <UserProfileB v-else />
    </keep-alive>
  </div>
</template>
Copier après la connexion

这样的话,初始化时不会渲染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>
Copier après la connexion

会被编译成:

// 简化版
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()
    }),
  )
}
Copier après la connexion

可以看到,这里是先遍历(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>
复制代码
Copier après la connexion

这样只会在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;)
Copier après la connexion

这里有一个 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;
    }
  }
]
Copier après la connexion

执行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;
    }
  }
]
Copier après la connexion

此时执行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 {
    //...
  }
}
Copier après la connexion

通过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)))
  )
}
Copier après la connexion

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;
    }
  }
]
Copier après la connexion

执行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;
    }
  }
]
Copier après la connexion

这里和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>
Copier après la connexion

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

参考黄轶老师揭秘 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>
Copier après la connexion

其实原理很简单,主要是维护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>
Copier après la connexion

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

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

//...
data() {
  return {
    heavyData: Object.freeze(heavyData)
  }
}
//...
Copier après la connexion

改完之后再试下,初始化数据的时间变成了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)
  }
  // ...省略其他逻辑
}
Copier après la connexion

这个判断条件中有一个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>
Copier après la connexion

会被编译成:

function render() {
  with (this) {
    return _m(0)
  }
}
Copier après la connexion

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

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;
  })`
}
Copier après la connexion

这里就是生成代码的关键逻辑,这里会把渲染函数保存在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
}
Copier après la connexion

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

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

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

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

【相关推荐:vue.js教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!