Skip to content

生命周期钩子

1. Vue 组件生命周期的各个阶段是什么?每个阶段的典型应用场景是什么?

答案:Vue 组件的生命周期主要分为以下几个阶段:

  1. 创建阶段:

    • beforeCreate:实例初始化之后,数据观测和事件配置之前调用。
    • created:实例创建完成后调用,此时已完成数据观测、属性和方法的运算,但尚未开始 DOM 渲染。
  2. 挂载阶段:

    • beforeMount:在挂载开始之前被调用,相关的 render 函数首次被调用。
    • mounted:实例被挂载后调用,此时 el 被新创建的 vm.$el 替换了。
  3. 更新阶段:

    • beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
    • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  4. 销毁阶段:

    • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
    • destroyed:实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

典型应用场景:

  • created:进行 Ajax 请求异步数据的获取、初始化数据
  • mounted:访问操作 DOM、进行 DOM 相关的初始化
  • beforeUpdate:在更新之前访问现有的 DOM,如手动移除已添加的事件监听器
  • updated:当数据更改导致虚拟 DOM 重新渲染和打补丁时,可以执行依赖于 DOM 的操作
  • beforeDestroy:清除定时器、解绑全局事件、销毁插件对象等

2. 为什么 Vue 的生命周期钩子不推荐使用箭头函数?

答案:Vue 的生命周期钩子不推荐使用箭头函数,主要是因为箭头函数绑定了父级作用域的上下文,这意味着在生命周期钩子中使用 this 将无法正确指向 Vue 实例。

例如:

javascript
export default {
  created: () => {
    console.log(this.a) // 这里的 this 不是 Vue 实例
  },
  data() {
    return {
      a: 1
    }
  }
}

在这个例子中,created 钩子中的 this 将不是 Vue 实例,而是父级作用域的上下文,可能是 undefinedwindow

正确的做法是使用普通函数:

javascript
export default {
  created() {
    console.log(this.a) // 这里的 this 是 Vue 实例
  },
  data() {
    return {
      a: 1
    }
  }
}

3. 在哪个生命周期钩子中可以访问到 DOM 元素?

答案:在 mounted 钩子中可以访问到 DOM 元素。这是因为在 mounted 钩子被调用时,Vue 已经将模板渲染到了真实的 DOM 中。

例如:

javascript
export default {
  mounted() {
    console.log(this.$el) // 可以访问到挂载的 DOM 元素
    this.$refs.myElement // 可以访问到有 ref="myElement" 的 DOM 元素
  }
}

需要注意的是,虽然 mounted 保证了当前组件已经挂载,但不保证所有子组件都已经挂载。如果你需要等待整个视图都渲染完毕,可以在 mounted 中使用 Vue.nextTick

javascript
export default {
  mounted() {
    this.$nextTick(() => {
      // 整个视图都已经渲染完毕
    })
  }
}

4. 如何在子组件中监听到父组件的生命周期?

答案:在 Vue 中,可以通过在子组件中使用 $emit 来触发一个事件,并在父组件中监听这个事件来实现对父组件生命周期的监听。

具体步骤如下:

  1. 在父组件中,为子组件绑定一个事件监听器:
html
<template>
  <child @hook:mounted="onChildMounted"/>
</template>

<script>
export default {
  methods: {
    onChildMounted() {
      console.log('Child has been mounted')
    }
  }
}
</script>
  1. 这样,当子组件的 mounted 钩子触发时,父组件的 onChildMounted 方法就会被调用。

这种方法可以用于监听任何生命周期钩子,只需要将 mounted 替换为其他钩子名称即可。

在 Vue 3 中,我们可以使用 onMounted 等组合式 API 来实现类似的功能:

javascript
import { onMounted } from 'vue'

export default {
  setup(props, { emit }) {
    onMounted(() => {
      emit('mounted')
    })
  }
}

5. 在 created 和 mounted 钩子中请求数据有什么区别?

答案:在 createdmounted 钩子中请求数据主要有以下区别:

  1. 执行时机:

    • created 在组件实例创建完成后立即调用,此时还没有挂载 DOM,无法操作 DOM。
    • mounted 在组件挂载到 DOM 后调用,此时可以操作 DOM。
  2. 数据响应:

    • created 中请求数据,数据将在组件渲染之前获取,可以避免组件渲染时的空数据状态。
    • mounted 中请求数据,组件可能会先以空数据状态渲染,然后在数据获取后再次渲染。
  3. 服务器端渲染(SSR):

    • 在服务器端渲染时,只会调用到 created 钩子,不会调用 mounted 钩子。
  4. 性能:

    • created 中请求数据可能会稍微快一些,因为它在 DOM 渲染之前执行。

通常情况下,如果数据获取不依赖于 DOM,推荐在 created 钩子中请求数据。如果数据获取是依赖于 DOM 的,那么应该在 mounted 钩子中进行。

6. 如何在组件销毁时取消未完成的异步任务?

答案:在组件销毁时取消未完成的异步任务是一个很好的实践,可以防止内存泄漏和不必要的网络请求。可以在 beforeDestroydestroyed 生命周期钩子中进行清理工作。

以下是几种常见的取消异步任务的方法:

  1. 取消 Axios 请求:
javascript
export default {
  data() {
    return {
      cancelToken: null
    }
  },
  methods: {
    fetchData() {
      this.cancelToken = axios.CancelToken.source()
      axios.get('/api/data', {
        cancelToken: this.cancelToken.token
      })
    }
  },
  beforeDestroy() {
    if (this.cancelToken) {
      this.cancelToken.cancel('Component destroyed')
    }
  }
}
  1. 清除定时器:
javascript
export default {
  data() {
    return {
      timer: null
    }
  },
  created() {
    this.timer = setInterval(() => {
      // 某些操作
    }, 1000)
  },
  beforeDestroy() {
    clearInterval(this.timer)
  }
}
  1. 取消 Promise:
javascript
export default {
  data() {
    return {
      promiseAbortController: null
    }
  },
  methods: {
    async fetchData() {
      this.promiseAbortController = new AbortController()
      try {
        const response = await fetch('/api/data', {
          signal: this.promiseAbortController.signal
        })
        // 处理响应
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Fetch aborted')
        } else {
          console.error('Fetch error:', err)
        }
      }
    }
  },
  beforeDestroy() {
    if (this.promiseAbortController) {
      this.promiseAbortController.abort()
    }
  }
}

通过这些方法,可以确保在组件销毁时,相关的异步任务也被适当地清理。

7. 如何理解 Vue 的响应式系统?它是在哪个生命周期阶段初始化的?

答案:Vue 的响应式系统是其核心特性之一,它使得数据和视图能够保持同步。

Vue 的响应式系统主要通过以下方式工作:

  1. 对象属性拦截:使用 Object.defineProperty(Vue 2)或 Proxy(Vue 3)来拦截对象属性的 get 和 set 操作。
  2. 依赖收集:在 getter 中收集依赖(即使用该数据的组件或计算属性)。
  3. 变更通知:在 setter 中通知所有依赖进行更新。

响应式系统的初始化发生在 beforeCreatecreated 生命周期钩子之间。具体来说:

  1. beforeCreate 钩子调用之前,Vue 会初始化实例的生命周期、事件系统等,但数据观测(data observer)和 event/watcher 的配置还未完成。
  2. beforeCreatecreated 之间,Vue 会进行数据观测、属性和方法的运算、watch/event 事件回调的配置等。这个过程中,响应式系统被初始化。
  3. created 钩子被调用时,响应式系统已经完全设置好了。

这就是为什么在 created 钩子中我们可以访问响应式数据,而在 beforeCreate 中则不行。

理解这一点对于正确使用 Vue 的生命周期钩子非常重要,特别是在处理数据初始化和异步操作时。

8. 为什么不建议在 created 和 mounted 钩子中同时获取数据?

答案:虽然在 createdmounted 钩子中都可以获取数据,但通常不建议在这两个钩子中同时获取数据,原因如下:

  1. 重复请求:在两个钩子中都获取数据会导致重复的网络请求,这是不必要的,会增加服务器负载和浪费带宽。

  2. 性能问题:重复的数据获取可能会导致性能下降,特别是在数据量大或网络较慢的情况下。

  3. 状态管理复杂:在两个不同的生命周期阶段获取数据可能会导致状态管理变得复杂,难以追踪数据的来源和更新时机。

  4. 可能的竞态条件:如果两个请求的响应时间不同,可能会导致数据不一致或覆盖的问题。

通常的最佳实践是选择其中一个钩子来获取数据:

  • 如果数据获取不依赖于 DOM,推荐在 created 钩子中进行,这样可以更快地获取到数据。
  • 如果数据获取依赖于 DOM(例如,需要从 DOM 中获取某些信息),那么应该在 mounted 钩子中进行。

例如:

javascript
export default {
  data() {
    return {
      fetchedData: null
    }
  },
  created() {
    // 在这里获取数据
    this.fetchData()
  },
  methods: {
    fetchData() {
      // 异步获取数据的逻辑
      axios.get('/api/data')
        .then(response => {
          this.fetchedData = response.data
        })
    }
  }
}

这样可以确保数据只被获取一次,并且在适当的时机进行。

9. 如何在 Vue 3 的 setup 函数中模拟生命周期钩子?

  1. onBeforeMount - 对应 beforeMount
  2. onMounted - 对应 mounted
  3. onBeforeUpdate - 对应 beforeUpdate
  4. onUpdated - 对应 updated
  5. onBeforeUnmount - 对应 beforeDestroy
  6. onUnmounted - 对应 destroyed
  7. onErrorCaptured - 对应 errorCaptured

此外,Vue 3 还引入了两个新的钩子:

  1. onRenderTracked
  2. onRenderTriggered

使用示例:

javascript
import { onMounted, onBeforeUnmount } from 'vue'

export default {
  setup() {
    onMounted(() => {
      console.log('Component is mounted')
    })

    onBeforeUnmount(() => {
      console.log('Component is about to be unmounted')
    })

    // ...
  }
}

这种方式使得生命周期钩子可以被多次使用,并且可以和其他组合式函数一起使用,提高了代码的组织性和复用性。

10. Vue 3 的 setup 函数在什么时候被调用?它的作用是什么?

答案setup 函数是 Vue 3 组合式 API 的入口点。它在组件被创建之前,props 被解析之后执行,所以它取代了 Vue 2 中的 beforeCreatecreated 生命周期钩子。

setup 函数的主要作用包括:

  1. 定义响应式状态(reactive state)
  2. 定义生命周期钩子
  3. 定义方法
  4. 提供模板中使用的数据和函数

setup 函数接收两个参数:

  • props:组件的 props
  • context:一���包含了一些有用属性的对象(如 attrs, slots, emit)

示例:

javascript
import { ref, onMounted } from 'vue'

export default {
  props: ['initialCount'],
  setup(props, context) {
    const count = ref(props.initialCount)
    
    function increment() {
      count.value++
    }

    onMounted(() => {
      console.log('Component mounted')
    })

    return {
      count,
      increment
    }
  }
}

在这个例子中,setup 函数定义了响应式状态 count,一个方法 increment,以及一个生命周期钩子 onMounted。最后,它返回一个对象,这个对象中的属性可以在模板中使用。

11. 如何在 Vue 3 的 setup 函数中访问 this

答案:在 Vue 3 的 setup 函数中,我们不能直接访问 this。这是因为 setup 函数在组件实例创建之前被调用,此时组件实例还不存在。

然而,setup 函数接收两个参数,可以用来替代 this 的大部分功能:

  1. props:包含组件的 props
  2. context:一个对象,包含了一些有用的属性

context 对象包含以下属性:

  • attrs:非响应式对象,包含除了 props 以外的所有属性
  • slots:组件的插槽
  • emit:触发事件的方法
  • expose:暴露公共属性的函数(Vue 3.2+)

示例:

javascript
export default {
  props: ['message'],
  setup(props, context) {
    console.log(props.message)
    
    function handleClick() {
      context.emit('custom-event', 'Hello from child')
    }

    return {
      handleClick
    }
  }
}

在这个例子中,我们通过 props 访问了组件的 props,并使用 context.emit 触发了一个自定义事件。

如果确实需要访问组件实例,可以使用 getCurrentInstance API,但这通常不推荐,因为它可能导致代码难以维护:

javascript
import { getCurrentInstance } from 'vue'

export default {
  setup() {
    const instance = getCurrentInstance()
    console.log(instance)
    // 但要注意,这里的 instance 不等同于 Vue 2 中的 this
  }
}

总的来说,Vue 3 的设计鼓励我们使用 setup 函数和组合式 API,而不是依赖 this。这种方式可以带来更好的类型推断和更清晰的代码结构。

12. Vue 3 中的 reactiveref 有什么区别?

答案reactiveref 都是 Vue 3 中用于创建响应式数据的 API,但它们有一些重要的区别:

  1. 数据类型:

    • reactive 用于创建响应式对象
    • ref 可以用于任何类型的数据,包括基本类型(如 number, string, boolean)
  2. 访问和修改:

    • reactive 对象可以直接访问和修改其属性
    • ref 创建的响应式数据需要通过 .value 来访问和修改(在模板中使用时除外)
  3. 解构:

    • reactive 对象解构后会失去响应性
    • ref 可以安全地解构,因为它是一个包含 .value 的对象
  4. 模板使用:

    • 在模板中使用 reactive 对象时,可以直接访问其属性
    • 在模板中使用 ref 时,Vue 会自动解包,不需要使用 .value

示例:

javascript
import { reactive, ref } from 'vue'

export default {
  setup() {
    const state = reactive({
      count: 0,
      message: 'Hello'
    })

    const count = ref(0)

    function incrementReactive() {
      state.count++ // 直接修改
    }

    function incrementRef() {
      count.value++ // 通过 .value 修改
    }

    return {
      state,
      count,
      incrementReactive,
      incrementRef
    }
  }
}

在模板中使用:

html
<template>
  <div>
    <p>Reactive count: {{ state.count }}</p>
    <p>Ref count: {{ count }}</p> <!-- 不需要 .value -->
    <button @click="incrementReactive">Increment Reactive</button>
    <button @click="incrementRef">Increment Ref</button>
  </div>
</template>

总的来说,ref 更适合处理单个值的响应式状态,而 reactive 更适合处理复杂的对象状态。在实际应用中,我们经常会同时使用这两种 API。