从源码看Vue生命周期

使用Vue开发对于Vue生命周期的理解自然少不了,以前在面试时候也被问到过,当时也就只能回答出生命周期的几个钩子函数beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed。还有当使用<keep-alive>进行组件缓存的时候会有activateddeactivated生命周期。

先看官网图:


lifecycle.png

本文就我根据资料阅读Vue生命周期源码来了解生命周期中都做了哪些事情。

初始化流程

// src/core/instance/index.js :8
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)
}

当我们调用new Vue()的时候,会直接调用Vue实例上的_init方法。

// src/core/instance/init.js :15
Vue.prototype._init = function (options?: Object) {
  //...
  initLifecycle(vm) // 初始化生命周期相关属性
  initEvents(vm) // 初始化事件
  initRender(vm) //初始化渲染函数
  callHook(vm, 'beforeCreate')
}

在_init方法中,先对实例上的属性进行一些处理,比如合并组件options,初始化生命周期相关属性(绑定父组件,根组件等),初始化事件(父组件添加的事件),初始化渲染函数(给实例添加$attrs$listeners属性),然后调用beforeCreate生命周期钩子函数。

beforeCreate被调用完成之后做了以下几件事

// src/core/instance/init.js :15
Vue.prototype._init = function (options?: Object) {
  //...
  callHook(vm, 'beforeCreate')
  initInjections(vm) // 初始化inject
  initState(vm)  //初始化state
  initProvide(vm) // 初始化provide
  callHook(vm, 'created') //调用created生命周期
}
  1. 初始化inject
  2. 初始化state
    • 初始化props
    • 初始化methods
    • 初始化data
    • 初始化computed
    • 初始化watch
  3. 初始化provide

在initState方法中:

//src/core/instance/state.js :48
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) //初始化props
  if (opts.methods) initMethods(vm, opts.methods) //初始化methods
  if (opts.data) {
    initData(vm)  //初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed) //初始化computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch) //初始化watch
  }
}

由方法执行顺序可知,在data中可以使用props,不会报错,反过来则不行。
然后执行created钩子函数
created执行完成之后,会去调用vm.$mount()方法,开始挂载组件到dom上。

//src/core/instance/init.js :68
//...
callHook(vm,"created")
//...
if (vm.$options.el) {
      vm.$mount(vm.$options.el)
}

感觉$mount方法比较重要,先看看$mount方法。

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

其中核心是mountComponent方法

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')
  //...
}

这一步主要是判断当前实例是否含有render函数,如果有render函数那么就准备开始执行beforeMount钩子函数,否则直接提示报错“Failed to mount component: template or render function not defined”。
如果使用了runtime-with-compile版本(没有render函数)详情见官网运行时 + 编译器 vs. 只包含运行时 在实例化Vue时,将传入的template通过一系列编译生成render函数。

  • 编译这个template,生成AST抽象语法树。
  • 优化这个AST,标记静态节点。(渲染过程中不会变得那些节点,优化性能)。
  • 根据AST,生成render函数。

对应代码如下:

const ast = parse(template.trim(),options)
if(options.optimize !== false){
  optimize(ast,options)
}
const code = generate(ast,options)

总之,在有了render函数之后,就可以进行渲染步骤了,执行beforeMount钩子函数。

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)
      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 = () => {
      vm._update(vm._render(), hydrating)
    }
  }
//...

定义一个渲染组件的函数updateComponent

updateComponent =  () => {
  vm._update(vm._render,hydrating)
}  

vm._render就是调用render函数生成一个vnode,而vm._update方法则会对这个vnode进行patch操作,帮我们把vnode通过cleateElm函数创建新节点,并且渲染到dom中。

//src/core/instance/lifecycle.js :59
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  //...
     vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  //...
}

看完updateComponent方法里面的具体实现之后
接下来就是要执行,updateComponent方法了。
执行是由Watcher类负责执行的。

  //src/core/instance/lifecycle.js :197
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

为什么要用Watcher来执行呢?因为在执行过程中,需要去观测这个函数依赖了哪些响应式的数据,将来在数据更新的时候,我们需要再重新执行updateComponent函数。

如果是更新后调用updateComponent函数,updateComponent内部的path就不再是初始化的时候创建节点,而是通过diff算法将差异的地方找到,以最小化的代价更新到真实dom上。

Watcher中有一个before方法,逻辑是当vm._isMounted,也就是第一次挂载完成之后,再次更新视图之前,会先调用beforeUpdate钩子函数。

注意:如果在render过程中有子组件的话,此时子组件也会有一系列的初始化过程,也会走之前所说的所有过程,因此这是一个递归构建过程。

当有子组件时生命周期执行过程:

父 beforeCreate
父 created
父 beforeMount之后--->render
子 beforeCreate
子 created
子 beforeMount之后--->render
子 mounted
父 mounted

  //src/core/instance/lifecycle.js :208
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }

最终mounted生命周期钩子函数触发

更新流程

当一个响应式属性被更新,触发了Watcher的回调函数,也就是updateComponent 方法

updateComponent = () => {
   vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

在更新之前会先进行判断,是否是更新vm._isMounted如果是更新那么就会直接执行beforeUpdate生命周期钩子。

Vue 异步执行 DOM 更新

由于Vue组件的异步更新机制,当响应式数据发生变化,根据数据劫持此时会调用dep.notify(src/core/observer/index.js : 191),之后会在Dep中去遍历所有watcher进行更新subs[i]update()(src/core/observer/dep.js : 47),之后在Watcher中会发现,在调用update方法后,会调用Watcher中的update方法()会发现它执行了一个叫queueWatcher的方法,具体可以看下代码。

// src/core/observer/scheduler.js :164
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

从代码中可以看到,queueWatcher方法主要做了几件事

  1. 将watcher存到一个队列queue中,
  2. 在nextTick方法中执行flushSchedulerQueue方法
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  queue.sort((a, b) => a.id - b.id)
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
  }

flushSchedulerQueue方法中主要干的事情就是,遍历执行存放watcher的queue,并且判断如果当前watcher有before方法,那么就先执行watcher.before,此时就会触发之前的callHook(vm, 'beforeUpdate')方法,触发beforeUpdate生命周期钩子函数(src/core/instance/lifecycle.js)。而nextTick方法就是将flushSchedulerQueue方法存到一个数组callbacks

// src/core/util/next-tick.js :87
export function nextTick (cb?: Function, ctx?: Object) {
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) { 
    pending = true
    timerFunc()
  }
}

最终会执行timerFunc方法

//src/core/instance/util/next-tick.js :33
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

这段代码就是nextTick的核心代码了,Vue响应式更新为什么是异步的也在这里能找到答案,首先判断运行环境是否支持Promise,如果不支持就判断是否支持MutationObserver,如果不支持,就去判断是否支持setImmediate,如果还不支持就用setTimeout大法。最终最终会将callbacks数组中的方法在异步方法中执行,到此,Vue响应式异步更新也差不多梳理了一遍。
beforeUpdate生命周期执行完之后,就开始一系列的patchdiff流程后组件渲染完毕之后就会调用updated生命周期钩子函数。

//src/core/observer/scheduler.js :130
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')
    }
  }
}

注意:这里watcher中的updated是倒序调用的,所以当同一个属性在父-子组件中都有使用,收集依赖是按照父->子收集,但是触发updated钩子函数却是子->父
updated生命周期钩子函数执行完成之后渲染更新流程也就到此结束。

销毁流程

在更新流程中,如果发现有组件在下一轮渲染中消失,比如v-for对应的数组中少了数据,或者v-if控制的组件由true变为false,那么就会调用removeVnodes进入组件的销毁流程。

removeVnodes会调用vnodedestroy生命周期,而destroy内部则会调用vm.destroy.(keep-alive包裹的子组件除外)

这时就会调用callHook(vm,‘beforeDestroy’)

//src/core/instance/lifecycle.js :97
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')
    vm._isBeingDestroyed = true
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }

    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    vm._isDestroyed = true
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')
    vm.$off()
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

beforDestroy生命周期钩子函数调用完之后,就会进行一系列的清理逻辑,比如断开父子关系,关闭watcher,移除引用的data等,之后会调用callHook(vm, 'destroyed')
destroyed生命周期钩子函数执行完成。
注意这里的销毁不是指将组件从试图中移除,使用dom获取方法还能获取到,这里指的销毁是切断组件生命,使其失去活力,Vue响应式等其他生命活动,销毁后的组件全程不会参与而已。
到此Vue整个生命周期也就结束了,实际上官网的生命周期图就已经完美的诠释了一切,这里只是从源码中又多窥探到一些东西而已,源码中其实还掺有复杂的逻辑,我在看的时候选择性的抛开,只看它主线上的逻辑。不过也还有看不懂的地方,记录如有错误,还望不吝指出。

参考:实例生命周期钩子
Vue 的生命周期之间到底做了什么事清?(源码详解)

写在最后:文中内容大多为自己平时从各种途径学习总结,文中参考文章大多收录在我的个人博客里,欢迎阅览http://www.tianleilei.cn

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350