Vue—创建到销毁全过程

2022/03/05 Vue 共 5035 字,约 15 分钟

初始化

new Vue()

这个阶段做的第一件事就是 new 创建一个 Vue 实例对象

new Vue({
  el: '#app',
  render: h => h(App)
})

源码地址:src/core/instance/index.js

function Vue (options) {
  ...
  this._init(options)
}
initMixin(Vue)

_init()

源码地址:src/core/instance/init.js

主要流程:

  • 合并配置,把一些内置组件 <component /><keep-alive /><transition />directivefilter 等合并到 Vue.optoins
  • 调用一些初始化函数
  • 触发生命周期钩子,beforeCreatecreated
  • 最后调用 $mount 挂载,进入下一阶段
export function initMixin(Vue: typeof Component) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Record<string, any>) {
    // 保存当前实例
    const vm: Component = this

    ...

    // merge options
    if (options && options._isComponent) {
      // 把子组件依赖父组件的 props, listeners 挂载到 options 上,并指定组件的 $options
      initInternalComponent(vm, options as any)
    } else {
      // 把传进来的 options 和当前构造函数和父级的 options 进行合并,并挂载到原型上
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    ...
    // expose real self
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$options, $children, $refs, $root, _watcher ...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染:render, mixin
    callHook(vm, 'beforeCreate')
    initInjections(vm) // 初始化 inject
    initState(vm) // 初始化组件数据:props、data、methods、watch、computed
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') // 调用声明周期钩子函数

    ...

    if (vm.$options.el) {
      // 如果传了 el 就会调用 $mount 进入模板编译和挂载阶段
      // 如果没有传就需要手动执行 $mount 才会进入下一阶段
      vm.$mount(vm.$options.el)
    }
  }
}

模板编译阶段

$mount

源码地址:dist/vue.js - 11927

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  ...

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          ...
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (__DEV__) {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // @ts-expect-error
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (__DEV__ && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          outputSourceRange: __DEV__,
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        },
        this
      )
      options.render = render
      options.staticRenderFns = staticRenderFns

      ...
    }
  }
  return mount.call(this, el, hydrating)
}

$mount 主要就是判断要不要编译,使用哪一个模板编译

挂载阶段

这个阶段主要做的事有两件

  • 根据 render 返回的虚拟 DOM 创建真实的 DOM 节点,插入到视图中,完成渲染
  • 对模板中数据或状态做响应式处理

mountComponent()

这里主要做的就是

  • 调用钩子函数 beforeMount
  • 调用 _update() 方法对新老虚拟 DOM 进行 patch 以及 new Watcher 对模板数据做响应式处理
  • 再调钩子函数 mounted

源码地址:src/core/instance/lifecycle.js - 139

export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 判断有没有渲染函数 render
  if (!vm.$options.render) {
    // 如果没有,默认创建一个注释节点
    vm.$options.render = createEmptyVNode
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  updateComponent = () => {
    // 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Digg)到真实 DOM,这里是首次渲染
    vm._update(vm._render(), hydrating)
  }

  const watcherOptions: WatcherOptions = {
    // 触发更新时,会在更新之前调用
    before() {
      // 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不执行
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }

  // 为当前组件实例设置观察者,监控 updateComponent 函数得到的数据
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  hydrating = false

  // 没有旧 vnode,说明是首次渲染
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Watcher

_update()

源码地址:src/core/instance/lifecycle.js - 62

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el // 当前组件根节点
  const prevVnode = vm._vnode // 旧的 vnode
  const restoreActiveInstance = setActiveInstance(vm)
  vm._vnode = vnode // 更新旧的 vnode
  // 如果是首次渲染
  if (!prevVnode) {
    // 对 vnode 进行 patch 创建真实的 DOM,挂载到 vm.$el 上
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 修改的时候,进行新旧 vnode 对比
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // 删除旧根节点的引用
  if (prevEl) {
    prevEl.__vue__ = null
  }
  // 更新当前根节点的引用
  if (vm.$el) {
    vm.$el.__vue__ = vm
  }
  // 更新父级的引用
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
    vm.$parent.$el = vm.$el
  }
}

销毁阶段

$destroy()

  • 调用生命周期钩子函数 beforeDestroy
  • 从父组件中删除当前组件
  • 移除当前组件内的所有观察者(依赖追踪),删除数据对象的引用,删除虚拟 DOM
  • 调用生命周期钩子函数 destroyed
  • 关闭所有事件监听,删除当前跟组件的引用,删除父级的引用

源码地址:src/core/instance/lifecycle.js - 100

Vue.prototype.$destroy = function () {
  const vm: Component = this
  // 如果实例正在被销毁的过程中,直接跳过
  if (vm._isBeingDestroyed) {
    return
  }
  callHook(vm, 'beforeDestroy')
  // 更新销毁状态
  vm._isBeingDestroyed = true
  // remove self from parent
  const parent = vm.$parent
  // 如果父级存在,并且父级没有在被销毁,并且不是抽象组件而是真实组件(<keep-alive>就是抽象组件,它的 abstract 为 true)
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    // 从父组件中删除
    remove(parent.$children, vm)
  }
  // teardown scope. this includes both the render watcher and other
  // watchers created
  vm._scope.stop()
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // 删除实例的虚拟 DOM
  vm.__patch__(vm._vnode, null)
  // fire destroyed hook
  callHook(vm, 'destroyed')
  // turn off all instance listeners.
  vm.$off()
  // 删除当前跟组件的引用
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // 删除父级的引用
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

什么时候调用 $destroy()

  • 新的 vnode 不存在,旧的 vnode 存在时,触发卸载旧 vnode 对应组件的 destroy
  • 新的 vnode 根节点被修改时,调用旧 vnode 对应组件的 destroy
  • 新旧 vnode 对比结束后,调用旧 vnode 对应组件的 destroy