更新阶段

更新阶段在数据变化时触发。当响应式数据发生变化,Vue 会重新渲染虚拟 DOM 并更新真实 DOM。这个过程涉及两个钩子:beforeUpdate 和 updated。

beforeUpdate 钩子

beforeUpdate 在数据变化后,DOM 更新之前调用。

执行时机

数据变化
    │
    ▼
触发 setter
    │
    ▼
通知 watcher
    │
    ▼
beforeUpdate  ← 数据已更新,DOM 未更新
    │
    ▼
重新渲染虚拟 DOM
    │
    ▼
更新真实 DOM
    │
    ▼
updated

特点

  • 数据已经更新
  • DOM 还是旧的内容
  • 可以访问更新前的 DOM
  • 适合在 DOM 更新前获取快照

示例

<div id="app">
  <p ref="text">{{ message }}</p>
  <button @click="update">更新</button>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    message: 'Hello'
  },
  methods: {
    update() {
      this.message = 'World'
    }
  },
  beforeUpdate() {
    console.log('beforeUpdate')
    console.log('数据:', this.message)
    console.log('DOM:', this.$refs.text.textContent)
  }
})
</script>

点击按钮后输出:

beforeUpdate
数据: World
DOM: Hello

使用场景

1. 获取更新前的 DOM 状态

beforeUpdate() {
  this.oldScrollTop = this.$refs.container.scrollTop
}

2. 记录变化日志

beforeUpdate() {
  console.log('数据即将更新:', JSON.stringify(this.$data))
}

updated 钩子

updated 在数据变化导致的 DOM 更新完成后调用。

执行时机

beforeUpdate
    │
    ▼
重新渲染虚拟 DOM
    │
    ▼
diff 算法比较
    │
    ▼
更新真实 DOM
    │
    ▼
updated  ← DOM 已更新完成

特点

  • 数据已更新
  • DOM 已更新
  • 可以访问更新后的 DOM
  • 避免在此修改数据(会导致无限循环)

示例

new Vue({
  el: '#app',
  data: {
    message: 'Hello'
  },
  methods: {
    update() {
      this.message = 'World'
    }
  },
  updated() {
    console.log('updated')
    console.log('数据:', this.message)
    console.log('DOM:', this.$refs.text.textContent)
  }
})

点击按钮后输出:

updated
数据: World
DOM: World

使用场景

1. DOM 更新后的操作

updated() {
  this.scrollToBottom()
},
methods: {
  scrollToBottom() {
    const container = this.$refs.messages
    container.scrollTop = container.scrollHeight
  }
}

2. 第三方库同步更新

updated() {
  if (this.chart) {
    this.chart.update()
  }
}

3. 检测元素尺寸变化

updated() {
  const rect = this.$refs.element.getBoundingClientRect()
  if (rect.height !== this.lastHeight) {
    this.lastHeight = rect.height
    this.$emit('resize', rect)
  }
}

避免无限循环

在 updated 中修改数据会导致无限循环:

错误示例

updated() {
  this.counter++
}

这会导致:

数据变化 → beforeUpdate → 更新 DOM → updated → 数据变化 → ...

正确做法

如果需要在 updated 中修改数据,应该添加条件判断:

updated() {
  if (this.needsUpdate) {
    this.needsUpdate = false
    this.doSomething()
  }
}

或者使用 $nextTick:

updated() {
  this.$nextTick(() => {
    if (this.shouldUpdate) {
      this.performUpdate()
    }
  })
}

$nextTick 与 updated 的区别

$nextTick 和 updated 都在 DOM 更新后执行,但有关键区别:

特性updated$nextTick
触发条件任意数据变化手动调用
执行次数每次更新都执行只执行一次
使用场景响应所有更新特定更新后执行

使用 $nextTick 替代 updated

很多时候,使用 $nextTick 比 updated 更合适:

methods: {
  addItem() {
    this.items.push({ id: Date.now(), text: 'New item' })
    
    this.$nextTick(() => {
      this.scrollToBottom()
    })
  }
}

父子组件的更新顺序

父子组件更新时的执行顺序:

父 beforeUpdate
  子 beforeUpdate
  子 updated
父 updated

示例验证

Vue.component('child', {
  template: '<div>{{ value }}</div>',
  props: ['value'],
  beforeUpdate() { console.log('子 beforeUpdate') },
  updated() { console.log('子 updated') }
})

new Vue({
  template: '<child :value="message"></child>',
  data: { message: 'Hello' },
  beforeUpdate() { console.log('父 beforeUpdate') },
  updated() { console.log('父 updated') },
  methods: {
    update() {
      this.message = 'World'
    }
  }
}).$mount()

输出结果:

父 beforeUpdate
子 beforeUpdate
子 updated
父 updated

实际案例

聊天窗口自动滚动

Vue.component('chat-window', {
  template: `
    <div class="chat-container" ref="container">
      <div v-for="msg in messages" :key="msg.id" class="message">
        {{ msg.text }}
      </div>
    </div>
  `,
  props: {
    messages: Array
  },
  data() {
    return {
      userScrolled: false
    }
  },
  mounted() {
    this.$refs.container.addEventListener('scroll', this.handleScroll)
  },
  beforeDestroy() {
    this.$refs.container.removeEventListener('scroll', this.handleScroll)
  },
  updated() {
    if (!this.userScrolled) {
      this.scrollToBottom()
    }
  },
  methods: {
    scrollToBottom() {
      const container = this.$refs.container
      container.scrollTop = container.scrollHeight
    },
    handleScroll() {
      const container = this.$refs.container
      const isAtBottom = container.scrollHeight - container.scrollTop <= container.clientHeight + 50
      this.userScrolled = !isAtBottom
    }
  }
})

虚拟列表高度计算

Vue.component('virtual-list', {
  template: `
    <div class="virtual-list" ref="container" @scroll="handleScroll">
      <div class="content" :style="{ height: totalHeight + 'px' }">
        <div v-for="item in visibleItems" :key="item.id" ref="items">
          <slot :item="item"></slot>
        </div>
      </div>
    </div>
  `,
  props: {
    items: Array,
    itemHeight: Number
  },
  data() {
    return {
      startIndex: 0,
      visibleCount: 10
    }
  },
  computed: {
    totalHeight() {
      return this.items.length * this.itemHeight
    },
    visibleItems() {
      return this.items.slice(
        this.startIndex,
        this.startIndex + this.visibleCount
      )
    }
  },
  updated() {
    this.$nextTick(() => {
      this.updateItemPositions()
    })
  },
  methods: {
    handleScroll() {
      const scrollTop = this.$refs.container.scrollTop
      this.startIndex = Math.floor(scrollTop / this.itemHeight)
    },
    updateItemPositions() {
      const items = this.$refs.items
      if (!items) return
      
      items.forEach((item, index) => {
        item.style.position = 'absolute'
        item.style.top = (this.startIndex + index) * this.itemHeight + 'px'
      })
    }
  }
})

表格排序指示器

Vue.component('sortable-table', {
  template: `
    <table>
      <thead>
        <tr>
          <th v-for="col in columns" :key="col.key" @click="sortBy(col.key)">
            {{ col.label }}
            <span v-if="sortKey === col.key" class="sort-indicator">
              {{ sortOrder > 0 ? '↑' : '↓' }}
            </span>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="row in sortedData" :key="row.id">
          <td v-for="col in columns" :key="col.key">{{ row[col.key] }}</td>
        </tr>
      </tbody>
    </table>
  `,
  props: {
    columns: Array,
    data: Array
  },
  data() {
    return {
      sortKey: null,
      sortOrder: 1
    }
  },
  computed: {
    sortedData() {
      if (!this.sortKey) return this.data
      
      return [...this.data].sort((a, b) => {
        const aVal = a[this.sortKey]
        const bVal = b[this.sortKey]
        return (aVal > bVal ? 1 : -1) * this.sortOrder
      })
    }
  },
  updated() {
    this.highlightSortedColumn()
  },
  methods: {
    sortBy(key) {
      if (this.sortKey === key) {
        this.sortOrder *= -1
      } else {
        this.sortKey = key
        this.sortOrder = 1
      }
    },
    highlightSortedColumn() {
      const headers = this.$el.querySelectorAll('th')
      headers.forEach((th, index) => {
        if (this.columns[index].key === this.sortKey) {
          th.classList.add('sorted')
        } else {
          th.classList.remove('sorted')
        }
      })
    }
  }
})

性能优化

减少不必要的更新

使用计算属性缓存避免重复计算:

computed: {
  filteredItems() {
    return this.items.filter(item => item.active)
  }
}

使用 v-once

不需要更新的内容使用 v-once:

<div v-once>
  {{ staticContent }}
</div>

使用 Object.freeze

冻结不需要响应式的数据:

created() {
  this.largeList = Object.freeze(largeDataArray)
}

最佳实践

1. 避免在 updated 中修改数据

updated() {
  this.counter++
}

2. 使用 $nextTick 进行精确控制

this.items.push(newItem)
this.$nextTick(() => {
  this.scrollToNew()
})

3. 添加条件判断避免重复执行

updated() {
  if (this.pendingScroll) {
    this.pendingScroll = false
    this.scrollToBottom()
  }
}

4. 考虑使用 watch 替代 updated

watch: {
  items() {
    this.$nextTick(() => {
      this.scrollToBottom()
    })
  }
}