Vue生命周期源码解析

vue-source-code-explain

Posted on 2018-05-29

vue 目录结构

  • compiler:模板编译器
    • codegen - 根据AST生成render函数
    • directives - 生成render函数之前需要处理的指令
    • parser - 模板解析
  • core:核心代码
    • components - 全局组件, 目前只有keep-alive组件
    • global-api - Vue全局API,如 Vue.use, Vue.extend, Vue.mixin
    • instance - Vue实例相关,包括实例方法,生命周期,事件等
    • observer - 响应式数据
    • util - 工具方法
    • vdom - 虚拟dom
  • platforms:平台相关代码
  • server:服务端渲染
  • sfc:转换单文件组件(*.vue)
  • shared:全局共享的一些方法和常量

new Vue 说起

当我们 new Vue() 一个实例时,实际上会实例化一个对象。

// src/core/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

之后调用this._init 方法,此方法中主要执行下面的代码

// src/core/instance/init.js

// vm为当前Vue的实例
initLifecycle(vm) // 初始化生命周期相关属性,包括$root, $refs, _watcher,_inactive,_isMounted,_isDestroyed等
initEvents(vm) // 初始化事件相关属性,包括_events,_hasHookEvent
initRender(vm) // 初始化渲染,添加了虚拟dom, solt等属性和方法
callHook(vm, 'beforeCreate') // 执行生命周期函数beforeCreate
initInjections(vm) // 初始化inject(接收指定的要添加在这个实例上的属性)
initState(vm) // 初始化数据,props、methods、data、computed、watch
initProvide(vm) // 初始化provier(用于提供给后代组件的数据/方法)
callHook(vm, 'created') // 执行生命周期函数created

// 如果有 el 属性,则调用 vm.$mount 方法挂载 vm
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

在created函数调用完成后,此时Vue实例已经完成了如下操作:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见

挂载

上述代码执行完毕后,在_init函数中执行下面的代码

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

判断是否存在 el属性,存在则执行$mount方法,此方法存在于多个文件中。这里以 platforms/web/entry-runtime-with-compiler.js进行分析 。

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

// 定义 mount 存储原型上的$mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el) // 查询传入的节点是否存在
  
   // 绑定的节点不能是body或者html标签
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  
  // 获取组件属性
  const options = this.$options

  // 是否存在render函数,存在则直接执行mount.call(this, el, hydrating),如果不存在,则获取template 或者 el节点,template可以是#id、模版字符串、dom元素,如果没有template,则获取el以及其子内容作为模版
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } 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 = getOuterHTML(el)
    }
    
    // 经过上面的判断,template 可以是 字符串 或者 el节点,通过 compileToFunctions 将 template 转换成 render 函数
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      
      // 
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      
      // 实例的 options 属性添加 render 函数和 staticRenderFns 函数
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  
  // 调用之前定义的原型上的 $mount 方法挂载
  return mount.call(this, el, hydrating)
} 

上面挂载最后都会调用mount.call(this, el, hydrating)

// platforms/web/runtime/index.js

// el: 挂载的元素  
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

$mount 方法会去调用 mountComponent 方法

// src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el // 创建vm.$el替换el
  
  // 不存在render函数, 创建空的虚拟节点
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    ...
  }
  // 调用 beforeMount 生命周期函数
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // 开发环境
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      
      // vm._render() 会生成虚拟节点
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    // updateComponent 赋值
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // new Watch, 会执行 updateComponent方法, 此方法会执行 vm._render() 去生成虚拟节点,vm._update会去更新DOM, 且监听数据的更新
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // vm 的父虚拟节点不存在, 则设置_isMounted为true,同时执行生命周期函数mounted
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted') // // 调用 mounted 生命周期函数
  }
  return vm
}

在整个挂载的流程主要经历下面几个步骤

  • 获取 Vue 实例绑定的 template
  • 通过 compileToFunctions 生成 render 函数绑定到 Vue 实例
  • 执行 beforeMount 生命周期函数
  • 创建 Watch 生成虚拟节点, 并监听实例中的数据变化去更新DOM
  • 执行 mounted 生命周期函数

更新

当数据发生变化,会调用 vm._update(vm._render(), hydrating)

// core/instance/lifecycle.js

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

调用beforeUpdate钩子之后对虚拟dom重新渲染和补丁

由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子。 当这个钩子被调用时,组件DOM已经更新,所以你现在可以执行依赖于DOM的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或watcher取而代之

// vue/src/core/observer/scheduler.js
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') // 调用updated钩子
    }
  }
}

function flushSchedulerQueue () {
  flushing = true
  let watcher, id
  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // 调用组件的updated和activated生命周期
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true // 此参数用于判断watcher的ID是否存在
   	...
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

// src/core/observer/watcher.js
  update () {
    // lazy 懒加载
    // sync 组件数据双向改变
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this) // 排队watcher
    }
  }

队列执行完Watcher数组的update方法后调用了updated钩子,然后再是调用mounted钩子

销毁

当vue实例被销毁前调用

// vue/src/core/instance/lifecycle.js
Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy') // 调用beforeDestroy钩子
  }
}

当vue实例被销毁后调用

Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy') //调用beforeDestroy钩子
    vm._isBeingDestroyed = true

    // 清除相关配置
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // 清除watcher
    if (vm._watcher) {
      vm._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--
    }
    // call the last hook...
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // 关闭所有实例的listeners
    vm.$off()
    // 移除vue实例的引用
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 释放循环引用
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}