Vue:从父母那里收到道具后,子组件不会改变

Posted

技术标签:

【中文标题】Vue:从父母那里收到道具后,子组件不会改变【英文标题】:Vue: child component won't change after receiving props from parents 【发布时间】:2020-02-15 15:37:48 【问题描述】:

所以我与vue的父子组件通信有问题。问题是,在我导航到一个组件后,它应该调用一个 ajax 从服务器获取数据。父组件收到数据后,应该通过 props 将数据发送给所有子组件,但是 props 数据没有显示出来。只有在我在编辑器上更改代码后,子组件才开始显示道具数据。 所以,这是我的父组件的代码

<template>
  <div id="single-product-container">
    <product-header :name="singleProductName" :details="singleProductDetail" />
    <product-spec :spec="singleProductSpec" />
  </div>
</template>

<script>
import SingleProductHeader from '@/pages/SingleProductPage/single-product-header'
import SingleProductSpec from '@/pages/SingleProductPage/single-product-spec'
import singleProductApi from '@/api/product.api'

export default 
  data () 
    return 
      singleProductData: null,
      singleProductDetail: [],
      singleProductName: '',
      singleProductSpec: null
    
  ,
  methods: 
    getAllSingleProductDetail () 
      const productName = this.$route.params.product
      const location = this.location || 'jakarta'
      let vehicleType = null
      const path = this.$route.fullPath
      let self = this
      if (path.includes('motorcycle')) 
        vehicleType = 'motorcycle'
       else if (path.includes('car')) 
        vehicleType = 'car'
      
      singleProductApi.getSingleProductRequest(location, productName, vehicleType)
        .then(singleProductResponse => 
          console.log(singleProductResponse)
          let specObj = singleProductResponse.specification
          self.singleProductDetail = singleProductResponse.detail
          self.singleProductName = singleProductResponse.product_name
          self.singleProductSpec = specObj
          self.singleProductData = singleProductResponse
        )
        .catch(error => 
          throw error
        )
    
  ,
  mounted () 
    document.title = this.$route.params.product
  ,
  created () 
     this.getAllSingleProductDetail()
  ,
  components: 
    'product-header': SingleProductHeader,
    'product-spec': SingleProductSpec
  

</script>

这是我的单一产品规格组件,它不会加载道具数据:

<template>
  <div id="product-spec">
    <div class="product-spec-title">
      Spesifikasi
    </div>
    <div class="produk-laris-wrapper">
      <div class="tab-navigation-wrapper tab-navigation-default">
        <div class="tab-navigation tab-default" v-bind:class=" 'active-default': mesinActive" v-on:click="openSpaceTab(event, 'mesin')">
          <p class="tab-text tab-text-default">Mesin</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class=" 'active-default': rangkaActive" v-on:click="openSpaceTab(event, 'rangka')">
          <p class="tab-text tab-text-default">Rangka & Kaki</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class=" 'active-default': dimensiActive" v-on:click="openSpaceTab(event, 'dimensi')">
          <p class="tab-text tab-text-default">Dimensi & Berat</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class=" 'active-default': kapasitasActive" v-on:click="openSpaceTab(event, 'kapasitas')">
          <p class="tab-text tab-text-default">Kapasitas</p>
        </div>
        <div class="tab-navigation tab-default" v-bind:class=" 'active-default': kelistrikanActive" v-on:click="openSpaceTab(event, 'kelistrikan')">
          <p class="tab-text tab-text-default">Kelistrikan</p>
        </div>
      </div>
      <div id="tab-1" class="spec-tab-panel" v-bind:style=" display: mesinTab ">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in mesinData" :key="name">
              <td>  name  </td>
              <td>  value  </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-2" class="spec-tab-panel" v-bind:style=" display: rangkaTab ">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in rangkaData" :key="name">
              <td>  name  </td>
              <td>  value  </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-3" class="spec-tab-panel" v-bind:style=" display: dimensiTab ">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in dimensiData" :key="name">
              <td>  name  </td>
              <td>  value  </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-4" class="spec-tab-panel" v-bind:style=" display: kapasitasTab ">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in kapasitasData" :key="name">
              <td>  name  </td>
              <td>  value  </td>
            </tr>
          </tbody>
        </table>
      </div>
      <div id="tab-5" class="spec-tab-panel" v-bind:style=" display: kelistrikanTab ">
        <table class="spec-table">
          <tbody>
            <tr class="spec-row" v-for="(value, name) in kelistrikanData" :key="name">
              <td>  name  </td>
              <td>  value  </td>
            </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</template>

<script>
export default 
  props: 
    location: String,
    spec: Object
  ,
  data () 
    return 
      mesinActive: true,
      rangkaActive: false,
      dimensiActive: false,
      kapasitasActive: false,
      kelistrikanActive: false,
      mesinTab: 'block',
      rangkaTab: 'none',
      dimensiTab: 'none',
      kapasitasTab: 'none',
      kelistrikanTab: 'none',
      mesinData: ,
      rangkaData: ,
      dimensiData: ,
      kapasitasData: ,
      kelistrikanData: 
    
  ,
  methods: 
    openSpaceTab (evt, tab) 
      if (tab === 'mesin') 
        this.mesinActive = true
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'block'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
       else if (tab === 'rangka') 
        this.mesinActive = false
        this.rangkaActive = true
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'block'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
       else if (tab === 'dimensi') 
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = true
        this.kapasitasActive = false
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'block'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'none'
       else if (tab === 'kapasitas') 
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = true
        this.kelistrikanActive = false
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'block'
        this.kelistrikanTab = 'none'
       else if (tab === 'kelistrikan') 
        this.mesinActive = false
        this.rangkaActive = false
        this.dimensiActive = false
        this.kapasitasActive = false
        this.kelistrikanActive = true
        this.mesinTab = 'none'
        this.rangkaTab = 'none'
        this.dimensiTab = 'none'
        this.kapasitasTab = 'none'
        this.kelistrikanTab = 'block'
      
    
  ,
  created () 
    this.mesinData = this.spec.mesin
    this.rangkaData = this.spec.rangka
    this.dimensiData = this.spec.dimensi
    this.kapasitasData = this.spec.kapasitas
    this.kelistrikanData = this.spec.kelistrikan
  

</script>

正如我所说,我的单一产品规格组件的唯一问题不是它不会加载道具数据。问题是,当我在文本编辑器中更改代码时,它只加载道具数据(这很奇怪,我知道)。当我开始调试时,我开始意识到这一点,当我在 single-product-spec 组件中更改我的代码时,props 数据开始开始加载。如果我不更改我的单一产品规格组件代码,无论我等待多长时间,道具数据都不会加载。

【问题讨论】:

使用 created() 钩子而不是 mount(),因为在渲染 dom 后调用了mounted。更多info 你是不是粘贴错了?您的最后一个示例以&lt;/template&gt; 结束,mesinTab 也未在您的代码中定义。 在使用 ES6 lambda 语法时也不需要使用 let self = this 引用 嗨@Flame,我故意粘贴我的代码不完整,有一个处理mesinTab的方法,但它太长而且多余,也与我面临的问题无关。 我们无法判断代码示例何时显示明显错误 【参考方案1】:

好的,让我们按顺序逐步介绍发生的事情:

    父组件已创建,触发created 钩子并从服务器启动数据加载。 父组件渲染,创建子组件。 spec 的 prop 值将是 null,因为数据尚未加载,singleProductSpec 仍然是 null。 运行single-product-speccreated 挂钩。由于this.specnull,我想这会引发错误,尽管问题中没有提到错误。 在未来某个时间点,数据加载完成,更新singleProductSpec 的值。它是父组件的渲染依赖,因此该组件将被添加到渲染队列中。 父组件将重新渲染。 singleProductSpec 的新值将作为 spec 属性传递给 single-product-specsingle-product-spec 的新实例不会被创建,它只会重新使用它第一次渲染时创建的实例。

到那时,其他任何事情都不会发生。 single-product-speccreated 钩子不会重新运行,因为它不是刚刚创建的。

当您编辑子组件的源代码时,它将触发该组件的热重载。这种更改的确切效果会有所不同,但通常会导致重新创建该子项,而无需重新创建父项。由于父级已经从服务器加载了数据,因此新创建的子级将传递完整的spec 值。这允许在 created 挂钩中读取它。

有很多方法可以解决这个问题。

首先,在数据准备好之前,我们可以避免创建single-product-spec

<product-spec v-if="singleProductSpec" :spec="singleProductSpec" />

这将简单地避免在初始渲染期间创建组件,以便在运行孩子的created 挂钩时它可以访问您想要的数据。这可能是您应该使用的方法。

第二种方法是使用key。键用于在重新渲染时配对组件,以便 Vue 知道哪个旧组件与哪个新组件匹配。如果 key 发生变化,那么 Vue 将丢弃旧的子组件并创建一个新的子组件。创建新组件时,它将运行created 挂钩。这可能不是您的方案的最佳方法,因为尚不清楚子组件在​​传递 specnull 时应该做什么。

第三种方法是在子组件中使用watch。这将监视spec 的值何时更改并将相关值复制到组件的本地数据属性。虽然在某些情况下像这样使用 watch 是合适的,但它通常表明组件设计中存在潜在的弱点。

但是,您的代码中还有其他问题...

    首先不清楚为什么要将道具中的值复制到本地数据中。你可以直接使用道具。如果您这样做只是为了给它们提供更短的名称,那么只需使用计算属性即可。像这样复制它们的唯一合法原因是,如果属性值可以在子级中更改,并且 prop 仅用于传递初始值。即使在那种情况下,您也不会使用created 钩子,您只需在data 函数中执行它。见https://vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow。 对于 5 个选项卡,您将所有内容复制了 5 次。这应该使用对象数组来实现,每个对象都包含选项卡的所有相关详细信息。 属性mesinActivemesinTab 都表示相同的基础数据。你不应该在data 中同时拥有两者。至少应该是一个计算属性,尽管我个人可能会完全摆脱mesinTab。而是使用 CSS 类来应用相关的样式,并使用 mesinActive 来决定应用哪些类(就像你在其他地方一样)。显然,这同样适用于其他 xActive/xTab 属性。 您的选项卡是一种单选形式。使用 5 个布尔值来表示一个选择不是合适的数据结构。执行此操作的正确方法是拥有一个标识当前选项卡的属性。具体细节可能会有所不同,它可能包含选项卡索引、代表选项卡数据的对象或代表选项卡的 id。 您不需要将let self = this 与箭头函数一起使用。 this 值保留在周围范围内。

正确实现single-product-spec 的代码应该会崩溃到几乎没有。您应该能够摆脱大约 80% 的代码。如果您只使用适当的数据结构来保存所有数据,我希望 openSpaceTab 方法是单行的。

更新:

根据要求,考虑到我回答的“其他问题”部分的第 1-4 点,这是对您的组件的重写。

const ProductSpecTitle = 
  template: `
    <div>
      <div class="product-spec-title">
        Spesifikasi
      </div>
      <div class="produk-laris-wrapper">    
        <div class="tab-navigation-wrapper tab-navigation-default">
          <div
            v-for="tab of tabs"
            :key="tab.id"
            class="tab-navigation tab-default"
            :class=" 'active-default': tab.active "
            @click="openSpaceTab(tab.id)"
          >
            <p class="tab-text tab-text-default"> tab.text </p>
          </div>
        </div>
        <div
          v-for="tab in tabs"
          class="spec-tab-panel"
          :class=" 'spec-tab-panel-active': tab.active "
        >
          <table class="spec-table">
            <tbody>
              <tr
                v-for="(value, name) in tab.data"
                :key="name"
                class="spec-row"
              >
                <td>  name  </td>
                <td>  value  </td>
              </tr>
            </tbody>
          </table>
        </div>
      </div>
    </div>
  `,
  
  props: 
    spec: Object
  ,
  
  data () 
    return 
      selectedTab: 'mesin'
    
  ,
  
  computed: 
    tabs () 
      const tabs = [
         id: 'mesin', text: 'Mesin' ,
         id: 'rangka', text: 'Rangka & Kaki' ,
         id: 'dimensi', text: 'Dimensi & Berat' ,
         id: 'kapasitas', text: 'Kapasitas' ,
         id: 'kelistrikan', text: 'Kelistrikan' 
      ]
      
      for (const tab of tabs) 
        tab.active = tab.id === this.selectedTab
        tab.data = this.spec[tab.id]
      
      
      return tabs
    
  ,

  methods: 
    openSpaceTab (tab) 
      this.selectedTab = tab
    
  


new Vue(
  el: '#app',
  
  components: 
    ProductSpecTitle
  ,
  
  data () 
    return 
      spec: 
        mesin:  a: 1, b: 2 ,
        rangka:  c: 3, d: 4 ,
        dimensi:  e: 5, f: 6 ,
        kapasitas:  g: 7, h: 8 ,
        kelistrikan:  i: 9, j: 10 
      
    
  
)
.tab-navigation-wrapper 
  display: flex;
  margin-top: 10px;


.tab-navigation 
  border: 1px solid #000;
  cursor: pointer;


.tab-text 
  margin: 10px;


.active-default 
  background: #ccf;


.spec-tab-panel 
  display: none;


.spec-tab-panel-active 
  display: block;
  margin-top: 10px;


.spec-table 
  border-collapse: collapse;


.spec-table td 
  border: 1px solid #000;
  padding: 5px;
<script src="https://unpkg.com/vue@2.6.10/dist/vue.js"></script>

<div id="app">
  <product-spec-title :spec="spec"></product-spec-title>
</div>

【讨论】:

2.所以你是说我应该在组件数据中创建一个对象数组,并通过循环该对象数组来制作选项卡? 4. 我仍然不明白如何根据打开的选项卡显示选项卡数据。我如何知道此选项卡已打开,而其他选项卡未打开?除了,我在模板中做了一个很长的 if-else。当我只需要在模板上创建一个选项卡时,我如何在 xTab 打开/为真时显示 xData?如果您有关于第 2、3 和 4 点的示例或参考指南,我将不胜感激。 @BeingShame 我已经更新了我的答案,并举例说明了如何重写您的组件以考虑这些要点。然而,这只是可以做到的一种方式。它不应该被解释为“一种真正的方式”。 嗨,非常感谢您的回答。真是让我大开眼界!作为初级前端,我无处可问,所以你的回答对我很有帮助,谢谢:) 这个解释完美!好工作!谢谢!

以上是关于Vue:从父母那里收到道具后,子组件不会改变的主要内容,如果未能解决你的问题,请参考以下文章

将父母道具传递给Vue,js中的插槽

反应孩子正在通过道具改变父母状态......我不清楚原因

Vue.js“超出最大调用堆栈大小”错误。将数据从父母传递给孩子失败

React,如何从父母那里访问孩子的状态?无需更新父母的状态

反应:孩子点击改变父母的状态以重新渲染

反应父母对孩子道具传递而不重新渲染