Apakah kaedah pengoptimuman prestasi untuk vue?

青灯夜游
Lepaskan: 2022-01-10 15:45:20
asal
11123 orang telah melayarinya

Kaedah pengoptimuman prestasi termasuk: 1. Gunakan "v-slot:slotName" dan bukannya "slot="slotName"" 2. Elakkan menggunakan "v-for" dan "v-if" pada masa yang sama ; 3. , Sentiasa tambahkan kunci pada "v-for", dan jangan gunakan indeks sebagai kunci 4. Gunakan rendering tertunda, dsb.

Apakah kaedah pengoptimuman prestasi untuk vue?

Persekitaran pengendalian tutorial ini: sistem Windows 7, vue versi 2.9.6, komputer DELL G3.

Dalam pembangunan harian kami menggunakan Vue atau rangka kerja lain, kami akan menghadapi beberapa masalah prestasi Walaupun Vue telah membantu kami melakukan banyak pengoptimuman secara dalaman, masih terdapat beberapa masalah yang perlu kami ambil inisiatif. untuk dielakkan. Saya telah merumuskan beberapa senario yang terdedah kepada masalah prestasi dan teknik pengoptimuman untuk masalah ini dalam kerja harian saya dan dalam artikel yang ditulis oleh pelbagai pakar di Internet saya akan membincangkannya dalam artikel ini.

Gunakan v-slot:slotName bukannya slot="slotName"

v-slot ialah sintaks baharu dalam 2.6 Untuk butiran, sila semak: Vue2.6, 2.6 has been dikeluarkan Hampir dua tahun yang lalu, tetapi ramai orang masih menggunakan sintaks slot="slotName". Walaupun kedua-dua sintaks boleh mencapai kesan yang sama, logik dalaman sememangnya berbeza. Mari kita lihat perbezaan antara kedua-dua kaedah.

Mari kita lihat dahulu apakah kedua-dua sintaks ini akan disusun:

Menggunakan kaedah penulisan baharu, untuk templat berikut dalam komponen induk:

<child>
  <template v-slot:name>{{name}}</template>
</child>
Salin selepas log masuk

Akan disusun menjadi:

function render() {
  with (this) {
    return _c(&#39;child&#39;, {
      scopedSlots: _u([
        {
          key: &#39;name&#39;,
          fn: function () {
            return [_v(_s(name))]
          },
          proxy: true
        }
      ])
    })
  }
}
Salin selepas log masuk

Menggunakan kaedah penulisan lama, untuk templat berikut:

<child>
  <template slot="name">{{name}}</template>
</child>
Salin selepas log masuk

akan disusun menjadi:

function render() {
  with (this) {
    return _c(
      &#39;child&#39;,
      [
        _c(
          &#39;template&#39;,
          {
            slot: &#39;name&#39;
          },
          [_v(_s(name))]
        )
      ],
    )
  }
}
Salin selepas log masuk

Selepas penyusunan Ia boleh didapati dalam kod bahawa cara penulisan lama adalah untuk menjadikan kandungan slot sebagai kanak-kanak, yang akan dibuat dalam fungsi pemaparan komponen induk, dan kebergantungan kandungan slot akan dikumpulkan oleh komponen induk (dep nama dikumpul ke dalam pemaparan pemerhati komponen induk), dan kaedah penulisan baharu meletakkan kandungan slot dalam scopedSlots, yang akan dipanggil dalam fungsi pemaparan subkomponen dan kebergantungan slot kandungan akan dikumpul oleh subkomponen (dep nama dikumpul ke dalam pemerhati rendering subkomponen) , hasil akhir ialah: apabila kami mengubah suai atribut nama, cara penulisan lama ialah memanggil kemas kini komponen induk (panggil pemerhati rendering komponen induk), dan kemudian panggil kemas kini komponen anak (prePatch => semasa proses kemas kini komponen induk) updateChildComponent), dan kaedah penulisan baharu adalah untuk memanggil terus kemas kini subkomponen (panggil pemerhati rendering daripada subkomponen).

Dengan cara ini, kaedah penulisan lama menambah proses kemas kini komponen induk semasa mengemas kini, manakala kaedah penulisan baharu akan lebih cekap dan berprestasi lebih baik kerana ia mengemas kini komponen anak secara langsung, jadi disyorkan untuk sentiasa menggunakan v-slot:slotNameTatabahasa.

Gunakan sifat terkira

Ini telah disebut berkali-kali Salah satu ciri terbesar sifat terkira ialah ia boleh dicache. Cache ini merujuk kepada Selagi kebergantungannya tidak berubah, ia tidak akan dinilai semula, dan nilai cache akan diperoleh terus apabila diakses semula, yang boleh meningkatkan prestasi dengan banyak apabila melakukan beberapa pengiraan yang rumit. Anda boleh melihat kod berikut:

<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>
Salin selepas log masuk

Dalam contoh ini, atribut superCount diakses dalam dicipta, dipasang dan templat Di antara ketiga-tiga akses ini, ia sebenarnya hanya kali pertama, iaitu created . penilaian superCount, memandangkan atribut count tidak berubah, dua kali lagi mengembalikan nilai cache secara langsung.

Gunakan komponen berfungsi

Untuk sesetengah komponen, jika kita hanya menggunakannya untuk memaparkan beberapa data dan tidak perlu mengurus status, memantau data, dsb., maka kita boleh menggunakan komponen gaya fungsi. Komponen berfungsi adalah tanpa kewarganegaraan dan tanpa instance Mereka tidak perlu memulakan keadaan semasa pemulaan, membuat kejadian atau menangani kitaran hayat Berbanding dengan komponen stateful, ia lebih ringan dan mempunyai prestasi yang lebih baik. Untuk penggunaan khusus komponen berfungsi, sila rujuk dokumentasi rasmi: Komponen Fungsian

Kami boleh menulis demo ringkas untuk mengesahkan pengoptimuman ini:

// 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>
Salin selepas log masuk

Profil Pengguna Komponen ini hanya memaparkan nama prop, dan kemudian memanggilnya 500 kali dalam App.vue, dan mengira masa yang diambil dari beforeMount untuk dipasang, iaitu masa yang diambil untuk memulakan 500 subkomponen (UserProfile).

Selepas banyak percubaan, saya mendapati bahawa penggunaan masa adalah sekitar 30ms, jadi sekarang kita menukarnya kepada UserProfile menjadi komponen berfungsi:

<template functional>
  <div class="user-profile">{{ props.name }}</div>
</template>
Salin selepas log masuk

Pada masa ini, selepas beberapa kali Selepas mencuba, masa permulaan ialah 10-15ms, yang cukup untuk menunjukkan bahawa komponen berfungsi mempunyai prestasi yang lebih baik daripada komponen stateful.

Gunakan v-show dan v-if dalam kombinasi dengan adegan

Berikut ialah dua templat menggunakan v-show dan v-if

<template>
  <div>
    <UserProfile :user="user1" v-if="visible" />
    <button @click="visible = !visible">toggle</button>
  </div>
</template>
Salin selepas log masuk
<template>
  <div>
    <UserProfile :user="user1" v-show="visible" />
    <button @click="visible = !visible">toggle</button>
  </div>
</template>
Salin selepas log masuk

Kedua-duanya digunakan untuk mengawal paparan/menyembunyikan komponen atau DOM tertentu Sebelum membincangkan perbezaan prestasi mereka, mari kita analisa dahulu perbezaan antara kedua-duanya. Antaranya, templat v-if akan disusun menjadi:

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;)]
        )
      ],
    )
  }
}
Salin selepas log masuk

可以看到,v-if 的部分被转换成了一个三元表达式,visible 为 true 时,创建一个 UserProfile 的 vnode,否则创建一个空 vnode,在 patch 的时候,新旧节点不一样,就会移除旧的节点或创建新的节点,这样的话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;)]
        )
      ],
    )
  }
}
Salin selepas log masuk

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

el.style.display = value ? el.__vOriginalDisplay : &#39;none&#39;
Salin selepas log masuk

它其实是通过切换元素的 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>
Salin selepas log masuk

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

<template>
  <div>
    <keep-alive>
      <component :is="currentComponent" />
    </keep-alive>
  </div>
</template>
Salin selepas log masuk

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

<template>
  <div>
    <keep-alive>
      <UserProfileA v-if="visible" />
      <UserProfileB v-else />
    </keep-alive>
  </div>
</template>
Salin selepas log masuk

这样的话,初始化时不会渲染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>
Salin selepas log masuk

会被编译成:

// 简化版
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()
    }),
  )
}
Salin selepas log masuk

可以看到,这里是先遍历(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>
复制代码
Salin selepas log masuk

这样只会在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;)
Salin selepas log masuk

这里有一个 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;
    }
  }
]
Salin selepas log masuk

执行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;
    }
  }
]
Salin selepas log masuk

此时执行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 {
    //...
  }
}
Salin selepas log masuk

通过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)))
  )
}
Salin selepas log masuk

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;
    }
  }
]
Salin selepas log masuk

执行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;
    }
  }
]
Salin selepas log masuk

这里和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>
Salin selepas log masuk

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

参考黄轶老师揭秘 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>
Salin selepas log masuk

其实原理很简单,主要是维护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>
Salin selepas log masuk

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

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

//...
data() {
  return {
    heavyData: Object.freeze(heavyData)
  }
}
//...
Salin selepas log masuk

改完之后再试下,初始化数据的时间变成了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)
  }
  // ...省略其他逻辑
}
Salin selepas log masuk

这个判断条件中有一个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>
Salin selepas log masuk

会被编译成:

function render() {
  with (this) {
    return _m(0)
  }
}
Salin selepas log masuk

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

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;
  })`
}
Salin selepas log masuk

这里就是生成代码的关键逻辑,这里会把渲染函数保存在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
}
Salin selepas log masuk

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

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

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

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

【相关推荐:vue.js教程

Atas ialah kandungan terperinci Apakah kaedah pengoptimuman prestasi untuk vue?. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Label berkaitan:
sumber:php.cn
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan
Tentang kita Penafian Sitemap
Laman web PHP Cina:Latihan PHP dalam talian kebajikan awam,Bantu pelajar PHP berkembang dengan cepat!