深入理解Vue实例生命周期(中)

简介: 每个 Vue 实例在被创建时都会经过一系列的初始化过程。例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。 为了让开发者在Vue实例生命周期的不同阶段有机会插入自己的代码逻辑,vue提供了一种叫做生命周期钩子的函数。

DOM元素挂载


实例初始化完成后,会调用$mount开始DOM元素挂载。这个阶段会触发两个钩子函数:beforeMountmounted


// src/platforms/web/entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}


// src/platforms/web/runtime/index.js
// 覆写$mount方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  const options = this.$options
  if (!options.render) {
    let template = options.template
    // 选项中指定了template
    if (template) {
      if (typeof template === 'string') {
        // 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        // 虽然在文档中并未说明,但template还可以指定一个DOM元素作为模板
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 选项中指定了el
      template = getOuterHTML(el)
    }
    // 将模板解析成render函数
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // ...省略部分代码
  return mount.call(this, el, hydrating)
}


// src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 将对DOM元素的引用保存到$el
  vm.$el = el
  // ...省略部分代码
  // 调用beforeMount前需要执行模板编译逻辑
  callHook(vm, 'beforeMount')
  let updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  if (vm.$vnode == null) {
    // 标记为已挂载
    vm._isMounted = true
    // 触发mounted事件
    callHook(vm, 'mounted')
  }
  return vm
}


beforeMount


我们知道vue需要render函数来生成vnode。但是在实际开发中,基本都是通过templateel来指定模板,很少直接提供一个render函数。因此在触发beforeMount前,vue最重要的一个工作就是将HTML模板编译成render函数。beforeMount钩子函数被调用时,我们尚不能访问DOM元素。


mounted


每个vue实例都会对应一个render watcherrender watcher 会创建vnode(通过_render方法),并对vnode进行diff后,创建或者更新DOM元素(通过_update方法)。对于初次渲染来说,当创建完DOM元素后,把DOM树的根元素插入到body中,然后触发mounted钩子函数。此时,在钩子函数中可以对DOM元素进行操作了。


更新


实例完成初始化和挂载之后,如果由于用户的交互导致实例的状态发生了变化,实例将进入更新阶段。例如在代码中执行 this.msg = 'update msg',vue实例需要更新DOM元素。


实例的更新是异步的。前面提到过,render watcher 会负责调度程序创建vnode、创建更新DOM元素。当数据发生变化后,vue不会立即启动DOM的更新,而是先把实例对应的render watcher添加到一个队列中。然后在下一个事件循环中,统一执行DOM更新,清空队列。也就是调用下面代码中的flushSchedulerQueue函数。


此阶段会触发的钩子是:beforeUpdateupdated


/**
 * 清空所有的队列并执行watcher的更新逻辑
 */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id
  // 队列按照watcher的id升序排序,目的是确保:
  // 1. 组件总是从父向子进行更新
  // 2. 用户创建的watcher先于渲染watcher更新
  // 3. 如果组件在父组件的watcher运行时被销毁,该组件的watcher可以跳过处理
  queue.sort((a, b) => a.id - b.id)
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    // 调用watcher.before,触发beforeUpdate钩子
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    // 更新dom
    watcher.run()
  }
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  resetSchedulerState()
  // 触发activated钩子
  callActivatedHooks(activatedQueue)
  // 触发updated钩子
  callUpdatedHooks(updatedQueue)
}
// 触发updated钩子
function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}


相关文章
|
8天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
8天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
8天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
8天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
7天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
9天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
7天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
JavaScript 测试技术 容器
Vue2+VueRouter2+webpack 构建项目
1). 安装Node环境和npm包管理工具 检测版本 node -v npm -v 图1.png 2). 安装vue-cli(vue脚手架) npm install -g vue-cli --registry=https://registry.
1050 0
|
9天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
14天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发