Vue源码阅读(31):从源码的角度解读官网中的生命周期图

 我的开源库:

这篇博客旨在从源码的角度全面的概括 Vue 的运行流程,让大家对 Vue 的总体知识体系有所了解,也是对前面写的 30 篇文章的一个小总结,首先抛出 Vue 官网中的生命周期图,如下所示:

这张图是 Vue 官网中的生命周期图,相信大家对这张图都很熟悉了,接下来,我们从源码的角度对图中的内容进行一步步的解析。

1,new Vue() ==> beforeCreate

1-1,执行 new Vue()

function Vue (options) {
  // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
  this._init(options)
}

执行 new Vue(),Vue 构造函数内部会执行原型上的 _init() 方法,对当前的 Vue 实例进行初始化。

1-2,进入 _init() 方法

_init() 方法是 Vue 原型对象中的方法,通过 Vue 构造函数创建的实例能够通过原型链访问到这个方法,该方法定义在 src/core/instance/init.js 文件中。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
    const vm: Component = this
    // 赋值唯一的 id
    vm._uid = uid++

    // 下面这个 if else 分支需要注意一下。
    // 在 Vue 中,有两个时机会创建 Vue 实例,一个是 main.js 中手动执行的 new Vue({}),还有一个是当我们
    // 在模板中使用组件时,每使用一个组件,就会创建与之相对应的 Vue 实例。也就是说 Vue 的实例有两种,一种是
    // 手动调用的 new Vue,还有一种是组件的 Vue 实例。组件的 Vue 实例会进入下面的 if 分支,而手动调用的
    // new Vue 会进入下面的 else 分支。
    //
    // 合并 options,options 用于保存当前 Vue 组件能够使用的各种资源和配置,例如:组件、指令、过滤器等等
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // options 中保存的是当前组件能够使用资源和配置,这些都是当前组件私有的。
      // 但还有一些全局的资源,例如:使用 Vue.component、Vue.filter 等注册的资源,
      // 这些资源都是保存到 Vue.options 中,因为是全局的资源,所以当前的组件也要能访问到,
      // 所以在这里,将这个保存全局资源的 options 和当前组件的 options 进行合并,并保存到 vm.$options
      vm.$options = mergeOptions(
        // resolveConstructorOptions 函数的返回值是 Vue 的 options
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // 初始化与生命周期有关的内容
    initLifecycle(vm)
    // 初始化与事件有关的属性以及处理父组件绑定到当前组件的方法。
    initEvents(vm)
    // 初始化与插槽和渲染有关的内容
    initRender(vm)
    // 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
    // 执行 beforeCreate 生命周期函数
    callHook(vm, 'beforeCreate')

    ......
    ......
  }
}

从进入 _init() 方法到触发 beforeCreate 生命周期函数,Vue 一共做了四件事,非别是:options 的合并、初始化与生命周期有关的内容、初始化父组件在当前组件实例上注册的事件、初始化插槽和渲染有关的内容。

options 的合并可以看我的这篇文章

初始化父组件在当前组件实例上注册的事件可以看我的这篇文章

插槽可以看我的这篇文章

2,beforeCreate ==> created

 执行完 beforeCreate 生命周期函数之后,接下来就开始 Vue 实例 状态、方法、计算属性和侦听器的初始化工作,源码如下所示:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    ......
    ......

    // 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
    // 执行 beforeCreate 生命周期函数
    callHook(vm, 'beforeCreate')
    // 解析初始化当前组件的 inject
    initInjections(vm) // resolve injections before data/props
    // 初始化 state,包括 props、methods、data、computed、watch
    initState(vm)
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
    // 在 created 回调函数中,可以访问到实例中的数据
    // 执行 created 回调函数
    callHook(vm, 'created')

    ......
    ......
  }
}

在 beforeCreate 和 created 生命周期函数之间执行了三个方法,非别是:initInjections()、initState()、initProvide(),initInjections() 和 initProvide() 用于处理 provide / inject,initState() 用于初始化当前实例的 props、methods、data、computed、watch。这两块内容会在后续的博客中专门写两篇文章进行解析。

3,判断有没有配置 el 选项,有的话执行 vm.$mount(vm.$options.el)

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    ......
    ......

    // 执行 created 回调函数
    callHook(vm, 'created')

    // 如果配置中有 el 的话,则自动执行挂载操作
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

created 生命周期函数执行完之后,开始判断有没有配置 el 选项,如果配置了的话,就开始进行挂载操作,如果没有配置的话,则需要用户手动调用 vm.$mount(el) 触发执行挂载操作。

4,模板字符串编译成 render 函数

在这一步,首先判断选项中有没有配置自定义的 render 函数,如果没有配置的话,则需要框架将模板字符串编译成 render 函数。在进行编译之前,首先要做的是先获取到模板字符串,如果在配置选项中配置了 template 的话,则该选项的值就是模板字符串,如果配置选项没有配置 template 的话,则将 el 节点的 outerHTML 作为模板字符串。

获取到模板字符串之后,则调用 compileToFunctions() 方法将模板字符串编译成 render 函数,编译出的 render 函数保存到 vm.$options 对象中。

有关模板编译的解析可以看我的以下这几篇文章:

Vue源码阅读(11):聊聊模板编译

Vue源码阅读(12):解析器

Vue源码阅读(13):优化器

Vue源码阅读(14):代码生成器

编译成 render 函数并保存到 vm.$options 对象中之后,则开始挂载操作。

对应的源码如下所示:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 根据 el 获取其对应的 DOM 元素
  el = el && query(el)

  // 拿到 new Vue() 传递的配置对象
  const options = this.$options
  // 判断配置对象中有没有写 render 函数,如果没有定义 render 的话,接下来会根据提供的 template 或者 el
  // 生成 render 函数,并赋值给 options
  // 也就是说:最终 Vue 只认 render 函数,如果用户定义了 render 函数的话,那就直接使用,如果没有定义的话,Vue 会为其生成
  if (!options.render) {
    // 判断配置对象中有没有 template
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 如果 template 选项没有配置的话,使用 innerHTML 属性获取 el 节点的字符串形式
      template = getOuterHTML(el)
    }

    // 到这里,我们获取到了 template
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      // compileToFunctions 是一个函数,作用是将:template 模板字符串编译成 render 函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 在确保 options 中有 render 函数之后,就开始执行 runtime 中挂载的 $mount 进行渲染
  // 也就是说:(1)entry-runtime-with-compiler 中的 $mount 负责编译的工作,最终的处理结果就是 options 中一定会有用于渲染的 render 函数
  //          (2)而 runtime 中的 $mount 函数则负责根据生成的 render 函数进行页面的渲染
  return mount.call(this, el, hydrating)
}

5,进行挂载操作

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 将 el 设值到 vm 中的 $el
  vm.$el = el
  
  // 触发执行 beforeMount 生命周期函数(挂载之前)
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
    // vm._render() 函数的执行结果是一个 VNode
    // vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
    vm._update(vm._render(), hydrating)
  }

  // 这里的 Watcher 实例是一个渲染 Watcher,组件级别的
  vm._watcher = new Watcher(vm, updateComponent, noop)

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

updateComponent 函数的作用是将组件最新的 vnode 渲染(挂载)到页面上。在这里将 updateComponent 函数作为一个参数创建 Watcher 实例,在 Watcher 类中会调用执行 updateComponent 函数,进行页面的渲染。这里所创建的 Watcher 实例是当前 Vue 实例的渲染 Watcher,这个 Watcher 实例的作用是监控当前组件中状态的变化,如果组件中的状态发生变化的话,则会触发这个 render Watcher 中的 update() 方法,update() 方法会进而触发执行传递进去的 updateComponent 函数,进行组件的重新渲染。

vm._render() 的作用是使用组件的 render 函数结合组件当前最新的状态生成最新的 vnode,vnode 是 patch diff 算法的原材料。

vm._update() 的作用是使用最新的 vnode 执行 patch diff 算法,进行组件的渲染。

至此,组件的首先渲染就完成了。

相关的博客如下所示:

Vue源码阅读(8):以组件的渲染为例看依赖收集和变化侦测

Vue源码阅读(15):虚拟DOM

Vue源码阅读(16):VNode

Vue源码阅读(17):patch() 方法、diff 算法

6,组件的状态发生变更,引起组件的重新渲染

当组件的状态发生变化的时候,会触发数据的 setter 函数,在 setter 函数中会触发数据对应 Dep 实例的 notify() 方法。这个 Dep 实例和数据是一一对应关系,保存着该数据被依赖的所有 Watcher 实例。

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  // 在此进行依赖收集
  get: function reactiveGetter () {
    ......
    ......
  },
  // 在此进行派发更新
  set: function reactiveSetter (newVal) {
    // 拿到旧的 value
    const value = getter ? getter.call(obj) : val
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    // 如果存在用户自定义的 setter 的话,用这个用户自定义的 setter 赋值这个 value
    if (setter) {
      setter.call(obj, newVal)
    } else {
      // 否则就直接将 newVal 赋值给 val
      val = newVal
    }
    // 将新设置值中的 keys 也转换成响应式的
    childOb = !shallow && observe(newVal)
    // 触发依赖的更新
    dep.notify()
  }
})

Dep 实例的 notify() 方法会触发保存所有 Watcher 实例的 update() 方法。

export default class Dep {
  // 用于收集依赖的数组
  subs: Array<Watcher>;

  constructor () {
    // 将当前的 uid 当做 id 赋值给 id
    this.id = uid++
    // 初始化保存依赖的数组 subs
    this.subs = []
  }

  // 触发 subs 数组中依赖的更新操作
  notify () {
    // 数组的 slice 函数具有拷贝的作用
    const subs = this.subs.slice()
    // 遍历 subs 数组中的依赖项
    for (let i = 0, l = subs.length; i < l; i++) {
      // 执行依赖项的 update 函数,触发执行依赖
      subs[i].update()
    }
  }
}

Watcher 中的 update() 方法会间接的触发执行 updateComponent() 方法,updateComponent() 方法会触发执行 Vue 实例的 _update() 方法。

在 _update() 方法中会判断当前的 Vue 实例有没有挂载过,如果当前的组件已经被挂载过的话,则触发执行 beforeUpdate 生命周期函数

接下来执行 __patch__ 方法进行 diff 算法重新渲染组件。

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  if (vm._isMounted) {
    // 如果当前的 vm 已经挂载了的话,说明当前是第(2)中情况,所以需要执行 beforeUpdate 回调函数
    callHook(vm, 'beforeUpdate')
  }
  const prevEl = vm.$el
  // 上一次渲染时的 VNode
  // 第一次渲染时,为空
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  if (!prevVnode) {
    // 首次渲染
    vm.$el = vm.__patch__(
      vm.$el, vnode, hydrating, false /* removeOnly */,
      vm.$options._parentElm,
      vm.$options._refElm
    )
    // no need for the ref nodes after initial patch
    // this prevents keeping a detached DOM tree in memory (#5851)
    vm.$options._parentElm = vm.$options._refElm = null
  } else {
    // 依赖的数据更新时,页面需要重新渲染
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}

接下来需要说说什么时候执行 updated 生命周期函数,这需要说一下另外一个知识点,就是当触发执行 Watcher 实例的 update 方法时,并不会立即触发 Watcher 实例的 run 方法,而是先将当前的 Watcher 实例缓存到一个数组中,这个数组用于缓存在当前的事件循环中,需要组件重新渲染的所有 Watcher 实例,当下轮事件循环开始时,统一执行缓存的所有 Watcher 实例的 run 方法进行组件的重新更新,这样做,是为了优化性能,具体解析可以看我的这篇文章 —— Vue源码阅读(27):vm.$nextTick、Vue.nextTick 的源码解析

缓存 Watcher 实例和执行所有缓存 Watcher 的 run 方法的代码逻辑在 src/core/observer/scheduler.js 文件中,相关源码如下所示:

const queue: Array<Watcher> = []

function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // 刷新前对队列排序。
  // 这可确保:
  //  1. 组件从父级更新到子级(因为父级组件总是在子组件之前创建)。
  //  2. 组件的自定义 watcher 在组件的渲染 watcher 之前运行(因为自定义 watcher 在渲染 watcher 之前创建)。
  //  3. 如果在父组件的渲染 watcher 运行期间,子组件被销毁,该子组件的 watcher 会被跳过。
  queue.sort((a, b) => a.id - b.id)

  // 不要使用变量固定缓存当前状态 watcher 队列的长度,,因为新的 watcher 有可能随时被 push 到队列中
  // 遍历触发执行队列中的 watcher 实例
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    // 将 map 中,当前 watcher 的 id 置空
    has[id] = null
    // 核心:执行 watcher 实例的 run 方法
    watcher.run()
  }

  // 重置调度程序的状态
  resetSchedulerState()

  // 触发执行 updated 生命周期函数
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

可以发现,在 flushSchedulerQueue() 函数中,循环执行完缓存 Watcher 的 run 方法之后,组件的更新就完成了,接下来会执行 callUpdatedHooks 方法,在 callUpdatedHooks 方法中触发执行 updated 生命周期函数

7,销毁组件实例

当执行 Vue 实例的 $destroy 原型方法时,会进行组件销毁的逻辑。

vm.$destroy 的官方文档点击这里

对应的源码如下所示:

// Vue 实例销毁的方法
// beforeDestroy 和 destroyed 的生命周期函数都是在这里触发执行的
Vue.prototype.$destroy = function () {
  const vm: Component = this
  // _isBeingDestroyed 变量用于防止实例被重复执行 $destroy 方法
  // 如果 vm._isBeingDestroyed 值为 true 的话,则直接 return
  if (vm._isBeingDestroyed) {
    return
  }
  // 触发执行 beforeDestroy 生命周期函数
  callHook(vm, 'beforeDestroy')
  // 将 _isBeingDestroyed 属性设置为 true
  vm._isBeingDestroyed = true
  // 解除当前 Vue 实例和父 Vue 实例的关系
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }

  // 销毁当前实例的 render Watcher
  if (vm._watcher) {
    // teardown() 方法用于将 watcher 实例从依赖的所有数据的 dep 实例中移除
    vm._watcher.teardown()
  }
  // Vue 实例中除了 render Watcher,还有计算属性 Watcher 和侦听器 Watcher,
  // 这些 Watcher 实例都保存在 vm._watchers 数组中,
  // 遍历执行所有 Watcher 实例的 teardown 方法即可。
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // 将 _isDestroyed 设置为 true,表明 vm 已经被销毁了
  vm._isDestroyed = true
  // 执行 vm 的 __patch__ (重新渲染)方法,__patch__ 方法的第二个参数是 null,
  // 这会解除绑定的指令,移除组件的 DOM
  vm.__patch__(vm._vnode, null)
  // 执行 destroyed 生命周期函数
  callHook(vm, 'destroyed')
  // turn off all instance listeners.
  // 移除 vm 上绑定的事件
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值