挂载阶段


alias: vue-mounted-phase keywords: Vue2, beforeMount, mounted, 挂载阶段 description: Vue.js 挂载阶段生命周期钩子详解,掌握 DOM 操作的最佳时机。

挂载阶段

挂载阶段是 Vue 实例生命周期的第二个阶段。在这个阶段,Vue 将模板编译成渲染函数,生成虚拟 DOM,并最终渲染成真实 DOM。

挂载阶段包含两个钩子:beforeMount 和 mounted。

beforeMount 钩子

beforeMount 在挂载开始之前调用,此时模板已经编译完成,但还没有渲染到页面。

执行时机

created
    │
    ▼
编译模板为渲染函数
    │
    ▼
beforeMount  ← 模板已编译,DOM 未渲染
    │
    ▼
创建虚拟 DOM
    │
    ▼
渲染真实 DOM
    │
    ▼
mounted

特点

  • 可以访问 data、methods
  • 可以访问 this.$el(但内容是模板字符串)
  • DOM 还未渲染到页面
  • 很少使用此钩子

示例

<div id="app">
  <p>{{ message }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue'
  },
  beforeMount() {
    console.log('beforeMount')
    console.log(this.$el.textContent)
    console.log(this.$el.outerHTML)
  }
})
</script>

输出结果:

beforeMount
{{ message }}
<div id="app"><p>{{ message }}</p></div>

此时 $el 的内容还是模板字符串,没有被渲染。

使用场景

beforeMount 使用场景很少,偶尔用于:

1. 最后修改模板

beforeMount() {
  this.$options.template = this.$options.template.replace(
    /{{\s*(\w+)\s*}}/g,
    '[[ $1 ]]'
  )
}

2. 服务端渲染判断

beforeMount() {
  if (typeof window !== 'undefined') {
    this.initClientOnly()
  }
}

mounted 钩子

mounted 在实例挂载完成后调用,此时 DOM 已经渲染完成。

执行时机

beforeMount
    │
    ▼
创建虚拟 DOM
    │
    ▼
渲染真实 DOM
    │
    ▼
mounted  ← DOM 已渲染完成

特点

  • 可以访问 this.$el
  • DOM 已经渲染完成
  • 可以操作 DOM
  • 可以使用第三方库(需要 DOM 的)
  • 子组件可能还未完成挂载

示例

<div id="app">
  <p ref="message">{{ message }}</p>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue'
  },
  mounted() {
    console.log('mounted')
    console.log(this.$el.textContent)
    console.log(this.$refs.message.textContent)
  }
})
</script>

输出结果:

mounted
Hello Vue
Hello Vue

使用场景

mounted 是非常常用的钩子:

1. 操作 DOM

mounted() {
  const canvas = this.$refs.canvas
  const ctx = canvas.getContext('2d')
  ctx.fillStyle = 'red'
  ctx.fillRect(0, 0, 100, 100)
}

2. 初始化第三方库

mounted() {
  this.editor = new CodeMirror(this.$refs.editor, {
    mode: 'javascript',
    theme: 'monokai'
  })
},
beforeDestroy() {
  this.editor.toTextArea()
}

3. 添加事件监听

mounted() {
  window.addEventListener('resize', this.handleResize)
  document.addEventListener('click', this.handleClick)
},
beforeDestroy() {
  window.removeEventListener('resize', this.handleResize)
  document.removeEventListener('click', this.handleClick)
}

4. 初始化图表

mounted() {
  this.chart = new Chart(this.$refs.chart, {
    type: 'bar',
    data: this.chartData,
    options: this.chartOptions
  })
},
watch: {
  chartData(newData) {
    this.chart.data = newData
    this.chart.update()
  }
},
beforeDestroy() {
  this.chart.destroy()
}

$nextTick 的使用

Vue 的 DOM 更新是异步的。在 mounted 中,如果需要等待 DOM 更新完成,可以使用 $nextTick。

为什么需要 $nextTick

mounted() {
  this.message = 'Updated'
  console.log(this.$el.textContent)
  
  this.$nextTick(() => {
    console.log(this.$el.textContent)
  })
}

示例:等待子组件渲染

mounted() {
  this.$nextTick(() => {
    this.initPlugin()
  })
}

示例:获取元素尺寸

mounted() {
  this.$nextTick(() => {
    const rect = this.$refs.element.getBoundingClientRect()
    this.width = rect.width
    this.height = rect.height
  })
}

父子组件的挂载顺序

理解父子组件的挂载顺序非常重要:

挂载顺序

父 beforeCreate
父 created
父 beforeMount
  子 beforeCreate
  子 created
  子 beforeMount
  子 mounted
父 mounted

示例验证

Vue.component('child', {
  template: '<div>Child</div>',
  beforeCreate() { console.log('子 beforeCreate') },
  created() { console.log('子 created') },
  beforeMount() { console.log('子 beforeMount') },
  mounted() { console.log('子 mounted') }
})

new Vue({
  template: '<div><child></child></div>',
  beforeCreate() { console.log('父 beforeCreate') },
  created() { console.log('父 created') },
  beforeMount() { console.log('父 beforeMount') },
  mounted() { console.log('父 mounted') }
}).$mount()

输出结果:

父 beforeCreate
父 created
父 beforeMount
子 beforeCreate
子 created
子 beforeMount
子 mounted
父 mounted

等待所有子组件挂载

如果需要在所有子组件挂载完成后执行操作:

mounted() {
  this.$nextTick(() => {
    console.log('所有子组件已挂载')
    this.doSomethingWithChildren()
  })
}

实际案例

集成 ECharts

Vue.component('bar-chart', {
  template: '<div ref="chart" style="width: 100%; height: 300px;"></div>',
  props: {
    data: {
      type: Array,
      required: true
    }
  },
  data() {
    return {
      chart: null
    }
  },
  mounted() {
    this.initChart()
    window.addEventListener('resize', this.handleResize)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize)
    if (this.chart) {
      this.chart.dispose()
    }
  },
  watch: {
    data: {
      deep: true,
      handler() {
        this.updateChart()
      }
    }
  },
  methods: {
    initChart() {
      this.chart = echarts.init(this.$refs.chart)
      this.updateChart()
    },
    updateChart() {
      this.chart.setOption({
        xAxis: {
          type: 'category',
          data: this.data.map(item => item.name)
        },
        yAxis: {
          type: 'value'
        },
        series: [{
          type: 'bar',
          data: this.data.map(item => item.value)
        }]
      })
    },
    handleResize() {
      this.chart.resize()
    }
  }
})

集成 jQuery 插件

Vue.component('datepicker', {
  template: '<input ref="input" type="text">',
  props: {
    value: String,
    options: {
      type: Object,
      default: () => ({})
    }
  },
  mounted() {
    $(this.$refs.input).datepicker({
      ...this.options,
      defaultDate: this.value,
      onSelect: (dateText) => {
        this.$emit('input', dateText)
      }
    })
  },
  beforeDestroy() {
    $(this.$refs.input).datepicker('destroy')
  },
  watch: {
    value(newVal) {
      $(this.$refs.input).datepicker('setDate', newVal)
    }
  }
})

滚动监听

Vue.component('scroll-spy', {
  template: '<div><slot></slot></div>',
  data() {
    return {
      activeSection: null
    }
  },
  mounted() {
    this.sections = this.$el.querySelectorAll('section')
    window.addEventListener('scroll', this.handleScroll)
    this.handleScroll()
  },
  beforeDestroy() {
    window.removeEventListener('scroll', this.handleScroll)
  },
  methods: {
    handleScroll() {
      const scrollPos = window.scrollY + 100
      
      this.sections.forEach((section, index) => {
        const top = section.offsetTop
        const bottom = top + section.offsetHeight
        
        if (scrollPos >= top && scrollPos < bottom) {
          this.activeSection = index
          this.$emit('change', index)
        }
      })
    }
  }
})

服务端渲染注意

在服务端渲染时:

  • beforeMount 不会执行
  • mounted 不会执行
export default {
  mounted() {
    console.log('只在客户端执行')
  }
}

最佳实践

1. DOM 操作放在 mounted

mounted() {
  this.initDOM()
}

2. 及时清理资源

mounted() {
  this.timer = setInterval(this.poll, 5000)
},
beforeDestroy() {
  clearInterval(this.timer)
}

3. 使用 $nextTick 等待更新

mounted() {
  this.$nextTick(() => {
    this.measureElement()
  })
}

4. 避免在 mounted 中直接修改数据导致重渲染

mounted() {
  this.someData = 'new value'
}