列表渲染和 Vue 的 v-for 指令

Avatar of Hassan Djirdeh
Hassan Djirdeh

DigitalOcean 为您的旅程各个阶段提供云产品。 立即开始使用 $200 免费信用额度!

列表渲染是前端网页开发中最常用的实践之一。 动态列表渲染通常用于以简洁友好的格式向用户呈现一系列类似的组合信息。 在我们使用的几乎所有 Web 应用程序中,我们都可以在应用程序的许多区域看到内容的列表

在本文中,我们将了解 Vue 的 v-for 指令在生成动态列表中的作用,并通过一些示例来了解在这样做时为什么要使用 key 属性。

由于我们将从编写代码开始全面解释,因此本文假设您对 Vue(和/或其他 JavaScript 框架)的知识很少或没有。

案例研究:Twitter

我们将使用 Twitter 作为本文的案例研究。

登录后,在 Twitter 的主索引路由中,我们会看到类似于以下视图的内容

A screenshot of the Twitter homepage displaying a list of tweets from the author's timeline.

在主页上,我们已经习惯于看到趋势列表、推文列表、潜在关注者列表等。 这些列表中显示的内容取决于多种因素,例如我们的 Twitter 历史记录、我们关注的人、我们的喜欢等。 因此,我们可以说所有这些数据都是动态的。

尽管这些数据是动态获取的,但显示这些数据的方式保持不变。 这部分是由于使用可重用 Web 组件

例如,我们可以看到推文列表是一系列单个 tweet-component 项。 我们可以将 tweet-component 视为一个外壳,它接收某种数据(例如用户名、句柄、推文和头像等),并且只以一致的标记显示这些数据。

A diagram that dissects the elements of a tweet, including the username, handle, avatar, tweet body and like count.

假设我们想要基于从服务器获取的大量数据源渲染组件列表(例如 tweet-component 项列表)。 在 Vue 中,首先想到的用来完成此操作的方法是 v-for 指令。

v-for 指令

v-for 指令用于根据数据源渲染项目列表。 该指令可以在模板元素上使用,并且需要类似于以下的特定语法

A visual showing a Vue for directive of v-for equals item in items, where item is the alias and items is the data collection.

让我们看一个实践中的示例。 首先,假设我们已经获得了一组推文数据

const tweets = [
  {
    id: 1,
    name: 'James',
    handle: '@jokerjames',
    img: 'https://semantic-ui.com.cn/images/avatar2/large/matthew.png',
    tweet: "If you don't succeed, dust yourself off and try again.",
    likes: 10,
  },
  { 
    id: 2,
    name: 'Fatima',
    handle: '@fantasticfatima',
    img: 'https://semantic-ui.com.cn/images/avatar2/large/molly.png',
    tweet: 'Better late than never but never late is better.',
    likes: 12,
  },
  {
    id: 3,
    name: 'Xin',
    handle: '@xeroxin',
    img: 'https://semantic-ui.com.cn/images/avatar2/large/elyse.png',
    tweet: 'Beauty in the struggle, ugliness in the success.',
    likes: 18,
  }
]

tweetstweet 对象的集合,每个 tweet 包含该特定推文的详细信息,例如唯一标识符、帐户的名称/句柄、推文消息等。 现在让我们尝试使用 v-for 指令根据这些数据渲染推文组件列表。

首先,我们将创建 Vue 实例,它是 Vue 应用程序的核心。 我们将实例挂载/附加到 id 为 app 的 DOM 元素,并将 tweets 集合分配为实例数据对象的一部分。

new Vue({
  el: '#app',
  data: {
    tweets
  }
});

现在我们将创建 tweet-component,我们的 v-for 指令将使用它来渲染列表。 我们将使用全局 Vue.component 构造函数来创建一个名为 tweet-component 的组件

Vue.component('tweet-component', {
  template: `  
    <div class="tweet">
      <div class="box">
        <article class="media">
          <div class="media-left">
            <figure class="image is-64x64">
              <img :src="tweet.img" alt="Image">
            </figure>
          </div>
          <div class="media-content">
            <div class="content">
              <p>
                <strong>{{tweet.name}}</strong> <small>{{tweet.handle}}</small>
                <br>
                {{tweet.tweet}}
              </p>
            </div>
              <div class="level-left">
                <a class="level-item">
                  <span class="icon is-small"><i class="fas fa-heart"></i></span>
                  <span class="likes">{{tweet.likes}}</span>
                </a>
              </div>
          </div>
        </article>
      </div>
    </div>  
  `,
  props: {
    tweet: Object
  }
});

这里需要注意一些有趣的事情。

  1. tweet-component 期望一个 tweet 对象道具,如道具验证要求(props: {tweet: Object})所示。 如果该组件以非对象tweet 道具进行渲染,Vue 将发出警告。
  2. 我们使用 Mustache 语法:{{ }} 将推文对象道具的属性绑定到组件模板上。
  3. 组件标记采用 Bulma 的 Box 元素,因为它与推文非常相似。

在 HTML 模板中,我们需要创建 Vue 应用程序将被挂载到的标记(即 id 为 app 的元素)。 在此标记内,我们将使用 v-for 指令来渲染推文列表。 由于 tweets 是我们将迭代的数据集合,因此 tweet 将是我们在指令中使用的适当别名。 在每个渲染的 tweet-component 中,我们还将传递迭代的 tweet 对象作为道具,以便在组件中访问它。

<div id="app" class="columns">
  <div class="column">
    <tweet-component v-for="tweet in tweets" :tweet="tweet"/>
  </div>
</div>

无论向集合中添加多少个推文对象,或者它们随着时间的推移如何变化,我们的设置将始终以我们期望的相同标记渲染集合中的所有推文。

借助一些自定义 CSS,我们的应用程序看起来像这样

查看 CodePen 上 Hassan Dj (@itslit) 的 简单 Twitter 提要 #1

虽然一切按预期工作,但我们可能会在浏览器控制台中看到 Vue 的提示

[Vue tip]: <tweet-component v-for="tweet in tweets">: component lists rendered with v-for should have explicit keys...

在通过 CodePen 运行代码时,您可能无法在浏览器控制台中看到警告。

为什么 Vue 告诉我们在列表中指定显式键,而一切按预期工作?

key

为渲染的 v-for 列表中每个迭代元素指定一个 key 属性是常见的做法。 这是因为 Vue 使用 key 属性为每个节点的标识创建唯一绑定

让我们进一步解释一下,如果我们的列表有任何动态 UI 变化(例如,列表项的顺序被随机排列),Vue 将选择改变每个元素中的数据,而不是相应地移动 DOM 元素。 在大多数情况下,这不会出现问题。 但是,在某些情况下,如果我们的 v-for 列表依赖于 DOM 状态和/或子组件状态,这可能会导致一些意外的行为。

让我们来看一个例子。 如果我们的简单推文组件现在包含一个输入字段,允许用户直接回复推文消息,会怎么样? 我们将忽略如何提交此回复,只关注新的输入字段本身

A mockup of the tweet we are trying to create that looks exactly like the one diagramed earlier. This one has an additional component that includes a text input for submitting a reply.

我们将此新输入字段包含到 tweet-component 的模板中

Vue.component('tweet-component', {
  template: `
    <div class="tweet">
      <div class="box">
        // ...
      </div>
      <div class="control has-icons-left has-icons-right">
        <input class="input is-small" placeholder="Tweet your reply..." />
        <span class="icon is-small is-left">
          <i class="fas fa-envelope"></i>
        </span>
      </div>
    </div>
  `,
  props: {
    tweet: Object
  }
});

假设我们想要在我们的应用程序中引入另一个新功能。 此功能将允许用户随机排列推文列表。

要做到这一点,我们可以首先在 HTML 模板中包含一个“Shuffle!”按钮

<div id="app" class="columns">
  <div class="column">
    <button class="is-primary button" @click="shuffle">Shuffle!</button>
    <tweet-component  v-for="tweet in tweets" :tweet="tweet"/>
  </div>
</div>

我们在按钮元素上附加了一个点击事件监听器,以在触发时调用 shuffle 方法。 在我们的 Vue 实例中,我们将创建负责随机排列实例中 tweets 集合的 shuffle 方法。 我们将使用 lodash 的 _shuffle 方法来实现这一点

new Vue({
  el: '#app',
  data: {
    tweets
  },
  methods: {
    shuffle() {
      this.tweets = _.shuffle(this.tweets)
    }
  }
});

让我们试一试! 如果我们点击 shuffle 几次,我们会注意到我们的推文元素在每次点击后都会随机排列。

查看 CodePen 上 Hassan Dj (@itslit) 的 简单 Twitter 提要 #2

但是,如果我们在每个组件的输入中输入一些信息,然后点击 shuffle,我们会注意到一些奇怪的事情正在发生

An animated screenshot of a Shuffle button being clicked. The order of the tweets is shuffled, but the text fields for submitting replies are not.

由于我们没有选择使用 key 属性,因此 Vue 没有为每个推文节点创建唯一绑定。 因此,当我们打算重新排列推文时,Vue 采取了一种更有效的方法,即仅更改(或修补)每个元素中的数据。 由于临时 DOM 状态(即输入的文本)保持原样,因此我们遇到了这种意外的不匹配。

这是一个图表,它向我们展示了修补到每个元素中的数据以及保持原样的 DOM 状态

A diagram of a tweet outlining the patched items that are included in the shuffle and the text field that is a DOM element and remains in place.

为了避免这种情况,我们必须为列表中渲染的每个 tweet-component 分配一个唯一的 key。 我们将使用 tweetid 作为唯一标识符,因为我们可以安全地说推文的 id 不应该等于其他推文的 id。 由于我们使用的是动态值,因此我们将使用 v-bind 指令将我们的 key 绑定到 tweet.id

<div id="app" class="columns">
  <div class="column">
    <button class="is-primary button" @click="shuffle">Shuffle!</button>
    <tweet-component  v-for="tweet in tweets" :tweet="tweet" :key="tweet.id" />
  </div>
</div>

现在,Vue 识别了每个推文节点的标识,因此当我们打算随机排列列表时,它将重新排列组件。

An animated screenshot of the tweets shuffling correctly when the Shuffle button is clicked. Now, all elements of the tweet are included in the shuffle, including the text field.

由于每个推文组件现在都相应地移动,因此我们可以更进一步,使用 Vue 的 transition-group显示元素是如何重新排列的。

要做到这一点,我们将添加 transition-group 元素作为 v-for 列表的包装器。 我们将指定一个名为 tweets 的过渡名称,并声明过渡组应作为 div 元素进行渲染。

<div id="app" class="columns">
  <div class="column">
    <button class="is-primary button" @click="shuffle">Shuffle!</button>
    <transition-group name="tweets" tag="div">
      <tweet-component  v-for="tweet in tweets" :tweet="tweet" :key="tweet.id" />
    </transition-group>
  </div>
</div>

基于过渡的名称,Vue 将自动识别是否指定了任何 CSS 过渡/动画。 由于我们打算对列表中项目的移动调用过渡,因此 Vue 将查找类似于 tweets-move 的指定 CSS 过渡(其中 tweets 是我们赋予过渡组的名称)。 因此,我们将手动引入一个 .tweets-move 类,它具有指定的类型和过渡时间

#app .tweets-move {
  transition: transform 1s;
}

这只是对应用列表过渡的一个非常简短的介绍。 请务必查看 Vue 文档,了解可以应用的所有不同类型的过渡的详细信息!

当调用 shuffle 时,我们的 tweet-component 元素现在将适当地过渡到各个位置。 试一试! 在输入字段中输入一些信息,然后点击“Shuffle!”几次。

查看 CodePen 上 Hassan Dj(@itslit)的 Simple Twitter Feed #3

很酷,对吧?如果没有 key 属性,**transition-group 元素就无法用于创建列表过渡**,因为元素是 *就地修补* 的,而不是重新排序的。

是否应该始终使用 key 属性?**建议使用**。Vue 文档 Vue 文档 指定,只有在以下情况下才能省略 key 属性:

  • 我们有意为了性能原因,使用默认的元素就地修补方式。
  • DOM 内容非常简单。

结论

就是这样!希望这篇简短的文章能让您了解 v-for 指令有多么有用,并更深入地理解为什么经常使用 key 属性。如果您有任何问题或想法,请告诉我!


如果您喜欢我的写作风格,并且可能对学习如何使用 Vue.js 构建应用程序感兴趣,您可能会喜欢我参与出版的书籍 **Fullstack Vue: The Complete Guide to Vue.js**!这本书涵盖了 Vue 的众多方面,包括但不限于路由、简单状态管理、表单处理、Vuex、服务器持久化和测试。如果您感兴趣,可以在我们的网站 **https://fullstack.io/vue** 上获取更多信息。