vue2 生命周期 源码探究

vue2 生命周期所经历的阶段

组件生命周期经历的几个阶段:

  1. 数据初始化(beforeCreate、created) 为Vue实例上初始化一些属性,事件以及响应式数据

  2. 模板编译 created ==>> beforeMount 之间进行, 将模板编译成渲染函数

  3. 挂载阶段(beforeMount、mounted) 将模板渲染到真实的DOM中,挂载指定的DOM上

  4. 卸载阶段(beforeDestory、destoryed)将实例自身从父组件中删除,并取消依赖追踪及事件监听器

    数据更新时 beforeUpdate updated

    activated deactivated

    errorCaptured 在捕获一个来自后代组件的错误时被调用。

在这里插入图片描述

数据初始化

beforeCreated: 此时 data 和 el 均未初始值 undefined。 beforeCreated 是在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。此时的 options 中的 data 和 this 还未进行绑定。

beforeCreated ===>> created期间: 实现 注入和响应式的初始化 initInject initState initProvide

created: 实例创建并完成 数据观测(data observer)、data、 methods、 watch、event事件回调 ,options中的属性都绑定到了this实例对象上,并对属性都设置了响应式处理, created 是最早能获取数据的 hook。

initMixin :

src\core\instance\init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ... 设置Vue元素的uid唯一值,设置Vue对象的_isVue避免被观察
      
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        // 根组件走这里   选项合并  将全局配置选项合并到根组件的局部配置上
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
  
    // 初始化this._renderProxy属性,_self指向自己this
    vm._self = vm
      
    initLifecycle(vm)  // 初始化实例属性
    initEvents(vm)     // 初始化事件
    //  初始化插槽  获取 this.$slots  定义 this._c 即createElement 方法   平时使用 h 函数
    initRender(vm)
    // 执行 beforeCreate 生命周期钩子函数
    callHook(vm, 'beforeCreate')
      
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
      
    // 执行 created 生命周期钩子函数
    callHook(vm, 'created')

   // 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount,
   // 反之,没有 el 则必须手动调用 $mount
    if (vm.$options.el) {
      // 调用 $mount 方法,进入挂载阶段
      vm.$mount(vm.$options.el)
    }
  }
}

由此得出数据初始化的顺序: beforeCreate ==> inject ==>> provide ==>> created

provide / inject 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

initLifecycle: 挂载默认属性
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // 找到父节点然后将当前元素和父节点进行关联
  let parent = options.parent
  //找到第一个非抽象父节点
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 为子组件时自行添加到本组件的 $chidren 数组中
    parent.$children.push(vm)
  }
    
//  $开头属性都是提供用户使用的,内部属性都是以下划线_;
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

在 initLifecycle 中 初始化实例属性,对当前的 vue 的 parent 元素进行绑定, 并设置 $root 根属性

initEvents: 初始化 v-on 注册事件
export function initEvents (vm: Component) {
  vm._events = Object.create(null)  // 用于存储事件
  vm._hasHookEvent = false // 是否存在 hook 事件
  // 初始化父组件的监听事件
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)  // 将父组件向子组件注册的事件注册到子组件实例中的_events对象里
  }
}

let target: any
function add (event, fn) {
  target.$on(event, fn)  // 监听绑定事件
}

function remove (event, fn) {
  target.$off(event, fn)  // 解绑事件
}

function createOnceHandler (event, fn) {
  const _target = target
  // 创建只执行一次的 once 事件
  return function onceHandler () {
    const res = fn.apply(null, arguments)
    if (res !== null) {
      _target.$off(event, onceHandler)
    }
  }
}

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm  // 更新监听器
  target = undefined
}

初始化事件 实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。

initRender: 初始化渲染
export function initRender (vm: Component) {
  vm._vnode = null 
  vm._staticTrees = null 
  // 合并后的 option 赋值
  // 找到父组件节点 parentVnode  和渲染内容 renderContext
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode 
  const renderContext = parentVnode && parentVnode.context

  // 初始化插槽
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject

  // 将createElement fn绑定到这个实例,这样就能在里面获得合适的渲染环境。
  /**
   *  生成 VNode
   * 定义 _c 是 createElement 的 柯里化方法
   * @param {*} a  标签名
   * @param {*} b  属性的 JSON 字符串
   * @param {*} c  子节点数组
   * @param {*} d  节点规范化类型
   * @returns VNode 或者  VNode 数组
   */

  //  createElement 用来生成虚拟DOM
  // 将createElement()挂载到 vm 的 _c 和 $createElement 两个属性上
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  const parentData = parentVnode && parentVnode.data

  // 把父组件的 $attrs 和 $listeners 用 defineReactive()设置为响应式
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

a t t r s : 多级组件传值时,调用目标组件绑定 attrs: 多级组件传值时,调用目标组件绑定 attrs:多级组件传值时,调用目标组件绑定attrs,可直接获取根组件所传递参数(不包含classstyle和事件属性),而不用每一级组件逐层传递。

l i s t e n e r s : 通过 ‘ v − o n = " listeners: 通过`v-on=" listeners:通过von="listeners"`传入内部组件。

  • $listeners是组件的内置属性,它的值是父组件(不含.native修饰器的) v-on事件监听器
  • 组件可以通 过在自己的子组件上使用v-on=”$listeners”,进一步把值传给自己的子组件
callHook触发钩子函数

遍历数组,执行钩子函数

export function callHook (vm: Component, hook: string) {
 
  pushTarget()
 
  // handlers 这里其实是个数组
 
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      // 在这个函数里面执行call, 钩子函数
      // 遍历数组,把每一个钩子函数都执行一遍 
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    // 这里触发hook
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。

在beforeCreate 之前完成了 属性、 事件、渲染 的初始化准备。

此后

beforeCreate ===>> created 之间 依次执了initInjections、initState、initProvide,完成了依赖注入和响应式的初始化。

initInject:依赖注入
export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}



export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      if (key === '__ob__') continue
      // 获取 key 的 from 属性作为 provideKey
      const provideKey = inject[key].from
      let source = vm
      
       // 遍历所有的祖代组件,直到 根组件,找到组件中配置的 provide 选项,从而找到对应 key 的值,
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      //  如果上一个循环未找到,则采用 inject[key].default,
      // 如果没有设置 default 值,则抛出错误
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }

inject 祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。

initState: 响应式
//响应式原理入口
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 处理 props 对象,为 props 对象的每个属性设置响应式,
  //  并将 props 代理到 vm 实例上  支持 this.propKey 的访问方式
  if (opts.props) initProps(vm, opts.props)
  // 处理 methos 对象,校验每个属性的值是否为函数、和 props 属性比对进行判重处理,
  //  最后得到 vm[key] = methods[key]
  // 将 methods 中的所有方法赋值到 vue 实例上, 支持通过 this.methodkey 访问定义的方法
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    //  1.判重处理  data 中的属性 不能和props、methods中的属性重复
    //  2.代理, 将 data 中的属性代理到 vue 实例上 支持通过 this.key 的方式访问
    //  3.通过 observe 实现了数据的响应式
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }

  // computed  通过 watcher 实现 对每个 computedKey 实例化 watcher  默认懒执行
  // 将 computedKey 代理到 vue 实例上  支持通过 this.computedKey 的方式访问 computed.key
  //  computed 缓存实现原理 
  if (opts.computed) initComputed(vm, opts.computed)

   /**
   * 三件事:
   *   1、处理 watch 对象
   *   2、为 每个 watch.key 创建 watcher 实例,key 和 watcher 实例可能是 一对多 的关系
   *   3、如果设置了 immediate,则立即执行 回调函数
   */
  // 核心实例化一个 watcher 并返回一个 unwatch  用于接触监听
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }

  /**
   * 其实到这里也能看出,computed 和 watch 在本质是没有区别的,都是通过 watcher 去实现的响应式
   * 非要说有区别,那也只是在使用方式上的区别,简单来说:
   *   1、watch:适用于当数据变化时执行异步或者开销较大的操作时使用,即需要长时间等待的操作可以放在 watch 中
   *   2、computed:其中可以使用异步方法,但是没有任何意义。所以 computed 更适合做一些同步计算
   */

}


//====================================================
function initData (vm: Component) {
  // 得到 data 对象
  let data = vm.$options.data
  // 保证后续处理的 data 是一个对象
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    // 判重处理,data 中的属性不能和 props、methods 中的属性重复
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe中  通过defineReactive()设置响应式, 实现了数据的响应式处理 
  observe(data, true /* asRootData */)
}

由 initState 可得知 数据初始化的顺序为

beforeCreate ==> inject ==>> props ==>> methods ==>> data ==>> computed ==>> watch ==>> provide ==>> created

在 initData 中实现了数据的响应式

initProvide:初始化 provide
/**
 * 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上 
 */

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

inject选项接收到注入的值有可能被以上这些数据所使用到,所以在初始化完inject后需要先初始化这些数据,然后才能再初始化provide,所以在调用initInjections函数对inject初始化完之后需要先调用initState函数对数据进行初始化,最后再调用initProvide函数对provide进行初始化。

小结

在created的之前initInjections,initProvide,initState(这里面初始化了一些常用的属性,其中initData里面体现了init reactivity),所以beforeCreate之前相当于在为后面的生命周期进行做准备,所以在created之后就会完成一些常用属性的初始化,以及data数据的响应式绑定。也就是说在created之后就可以在vue中用this.data调用data里面的数据了。

模板编译

created ==>> beforeMount期间: 实现了模板编译

initMixin created之后则是进入了模板编译阶段

// 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount,
// 反之,没有 el 则必须手动调用 $mount
    if (vm.$options.el) {
      // 调用 $mount 方法,进入挂载阶段
      vm.$mount(vm.$options.el)
    }
runtime-compile $mount函数

src\platforms\web\entry-runtime-with-compiler.js

//  初始化最后一步的$mount  做备份
const mount = Vue.prototype.$mount
// 覆写 $mount
Vue.prototype.$mount = function (
  // el挂载点 
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 得到挂载点
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      // 不能挂载到html或者body元素上 
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }


  // 配置选项
  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') {
        // id选择器
        if (template.charAt(0) === '#') {
          // 通过idToTemplate  获得el的innerHTML
          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) {
      //获取outerHTML 
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
        
        
      // compileToFunctions 的实现 返回 render() 
      // 将模板编译成 ast 语法树
      // 将 AST 进行一个优化,其实主要优化是打上了静态节点的标记
      // 将编译好的 AST 对象,转化为字符串。,这个字符串将在渲染函数中用来生成 Vnode
      const { render, staticRenderFns } = compileToFunctions(template, {
        // 标记元素在HTML  模板字符串中的开始和结束索引位置
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        // 定界符  {{}}
        delimiters: options.delimiters,
        // 编译时是否保留注释内容
        comments: options.comments
      }, this)
      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')
      }
    }
  }

  // 存在render直接执行备份的$mount方法
  return mount.call(this, el, hydrating)
}

runtime compile 它缓存了原型的$mount方法,再重写了这个方法,在里面加入了编译生成render的部分。runtime-compile版本,会判断是否有tempalte,如果有就用compileToFunctions函数将其编译成render函数,如果没有template就将el挂载的外部html编译成render函数。

compile版本的$mount函数中对 tempalt 进行判断,再通过 compileToFunctions对tempalte 进行处理,将其编译成render函数。

runtime-compiler:template ==>>ast(abstract syntax tree:抽象语法数) ==>> render函数 ==>> vdom(virtual 虚拟dom) ==>> 真实DOM

原型的 $mount (runtime only)

src\platforms\web\runtime\index.js

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


mountComponent: 将template 编译成 render()

src\core\instance\lifecycle.js

// $mount 调用
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
        )
      }
    }
  }
  // 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 = () => {
      // 执行_update 进入更新阶段  首先执行 _render 将组件变成 VNode
      vm._update(vm._render(), hydrating)
    }
  }


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

  // 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
}

runtime-compile main.js 配置

import Vue from "vue/dist/vue.esm.js";   //修改引入的源文件
console.log(`Vue`, Vue);
import App from "./App.vue";
import router from "./router";
import store from "./store";
 
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
 
Vue.config.productionTip = false;
Vue.use(ElementUI);
 
new Vue({
  router,
  store,
  components: { App },   //组件的注册
  template: "<App/>",    //组件的使用
}).$mount("#app");

runtime-only main.js 配置

import Vue from "vue/dist/vue.esm.js";   //修改引入的源文件
console.log(`Vue`, Vue);
import App from "./App.vue";
import router from "./router";
import store from "./store";
 
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
 
Vue.config.productionTip = false;
Vue.use(ElementUI);
 
new Vue({
  router,
  store,
  render: (h) => h(App), 
}).$mount("#app");
runtime compiler和runtime only的区别
  1. main.js 的不同 :在complier的main.js文件中,Vue实例内部需要先注册组件,然后设置模板;而在only的main.js文件中,直接用render函数代替了这两个过程。

  2. runtime-compiler:template ==>>ast(abstract syntax tree:抽象语法数) ==>> render函数 ==>> vdom(virtual 虚拟dom) ==>> 真实DOM
    runtime-only:render函数 ==> vdom(virtual 虚拟dom) ==> 真实DOM, 不能解析template 模板,只能通过 vue-template-compiler 将 template 转化为 render函数。(npm安装vue-template-compiler,)
    因此
    runtime-only 性能更高,因为不需要 ,template ==>> ast(abstract syntax tree:抽象语法数)这两步

挂载阶段:

beforeMount:在模板编译阶段 template通过compileToFunctions()生成了 render() , 但是还未生成虚拟DOM

beforeMount ==>> mounted 期间:做好渲染准备了之后,执行 vm._render生成虚拟DOM,然后再通过vm._update渲染成真实的DOM

mounted: 虚拟Dom已经被挂载到真实Dom上,此时我们可以获取Dom节点,$ref在此时也是可以访问的。

beforeMount:

src\core\instance\lifecycle.js mountComponent() 中

// $mount 调用
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
        )
      }
    }
  }
  // beforeMount
  callHook(vm, 'beforeMount')
  ...
}
mounted:

src\core\instance\lifecycle.js mountComponent 后半段

// 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 = () => {
      // 执行_update 进入更新阶段  
      // 首先执行 _render 生成 VNode,最终调用 vm._update 更新 成真实DOM
      vm._update(vm._render(), hydrating)
    }
  }

  // 实例化 watcher 通过 updateComponent 回调 将
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  // vm.$vnode 为空,则为根实例,即渲染完毕,开始进入 mounted 阶段
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm

实例化 一个 watcher 在初始化时调用 updateComponent() ,然后观察到vm中数据发生变化的时候再执行updateComponent()更新DOM。

updateComponent 中通过 vm._render 方法先生成虚拟DOM,最终调用 vm._update 更新 成真实DOM

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

render() 是在created ===>> beforeMoun 之间 由template通过 compileToFunctions() 生成的,而 render 调用的参数 vm.createElement 是在 beforeCreate 初始化准备工作 initRender() 把 createElement 挂载在vm上的 _c c r e a t e E l e m e n t 属性上的,而 v m . c 是在模板编辑成的 r e n d e r 中使用, v m . createElement 属性上的,而 vm._c 是在模板编辑成的 render 中使用,vm. createElement属性上的,而vm.c是在模板编辑成的render中使用,vm.createElement是在用户手写的 render 中使用,所以真正创建虚拟DOM的其实是 createElement 函数创建了一个VNode Tree

vm._update 触发的时机
  1. DOM的初次渲染

  2. 数据发生变化的时候更新DOM

    vm._update 的作用是把 虚拟DOM转换成真实的DOM

小结

mounted之后就代表着渲染的完成,已经真实的DOM已经挂载到html上面,此时可以进行DOM的相关操作,在beforeMount和mounted函数之间,vue调用了render,将模板字符串转化成虚拟DOM

更新阶段:

beforeUpdate: 在数据发生改变后,DOM 被更新之前被调用。此时数据已更新,页面未更新 date 与 view暂未同步

beforeUpdate ==>> updated期间: 虚拟DOM 重新渲染并更新应用(date ==>> view) 的更新

updated: 数据与页面保持同步

Watcher:通过 watcher 实现数据的更新从而触发页面的更新

渲染watcher 在 vm.$mount() 执行时创建

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

watcher 的三种类型:
class Watcher {
  constructor (vm,expOrFn,cb,options,isRenderWatcher) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      ...
      this.lazy = !!options.lazy;  //主要用于computed watcher
      this.before = options.before
    } else {
       this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;   //expOrFn 对应上面的 updateComponent 方法
    } else {
      this.getter = parsePath(expOrFn);  // user watcher 的 expOrFn 是个表达式
    }
    
    //如果this.lazy为false,就立即执行this.get()
    //所以在创建watcher的时候就会执行updateComponent方法
    this.value = this.lazy? undefined: this.get();  

  }
  
 /**
   * Evaluate the getter, and re-collect dependencies.
   *
   * 在触发更新时重新收集依赖
   * 
   * * 执行 this.getter,并重新收集依赖
   * this.getter 是实例化 watcher 时传递的第二个参数,一个函数或者字符串,比如:updateComponent 或者 parsePath 返回的读取 this.xx 属性值的函数
   * 为什么要重新收集依赖?
   *   因为触发更新说明有响应式数据被更新了,但是被更新的数据虽然已经经过 observe 观察了,但是却没有进行依赖收集,
   *   所以,在更新页面时,会重新执行一次 render 函数,执行期间会触发读取操作,这时候进行依赖收集
   * 
   *   执行回调函数, 触发 updateComponent 执行,进行组件更新 进入 patch 阶段
   *   更新组件是先执行 render 生成 VNode  期间触发读取操作进行依赖收集
   * 
   */
  get () {
    // 对新值重新进行依赖收集
    // 打开 Dep.target,Dep.target = this  
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行回调函数,比如 updateComponent,触发组件更新 进入 patch 阶段
      // 触发读取操作  被 getter 拦截 进行依赖收集
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }

      // 关闭 Dep.target,Dep.target = null
      popTarget()
      this.cleanupDeps()
    }
    // 返回执行结果
    return value
  }
    
  /**
   * 收集依赖
   * 两件事:
   *   1、添加 dep 给自己(watcher)
   *   2、添加自己(watcher)到 dep
   */
  addDep (dep: Dep) {

    // 判重,如果 dep 已经存在则不重复添加
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      // 将 dep 放入 watcher 中
      this.newDeps.push(dep)
      // 避免在 dep 中重复添加 watcher,this.depIds 的设置在 cleanupDeps 方法中
      if (!this.depIds.has(id)) {
        // 将 watcher 自己添加到 dep 中
        dep.addSub(this)
      }
    }
  }

    
  // 数据发生变化时会触发 updae() 方法 从而实现数据的更新
  update () {
    if (this.lazy) {
      // 将 dirty 置为 true,在组件更新之后  当响应式数据再次被更新时  执行 computed getter
      // 重新执行 computed 回调函数 重新计算值 然后缓存到  watcher.value 中
      this.dirty = true  
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)   //渲染watcher会走这里的逻辑,其实最终都会执行this.run(),只是这里用队列进行优化
    }
  }

 /**
   * 由 刷新队列函数 flushSchedulerQueue 调用,完成如下几件事:
   *   1、执行实例化 watcher 传递的第二个参数,updateComponent 或者 获取 this.xx 的一个函数(parsePath 返回的函数)
   *   2、更新旧值为新值
   *   3、执行实例化 watcher 时传递的第三个参数,比如用户 watcher 的回调函数
   */
  run () {
    if (this.active) {
      // 调用 this.get 方法  在触发更新时重新收集依赖
      const value = this.get()
      if (
        value !== this.value ||
       
        isObject(value) ||
        this.deep
        // 深度观测
      ) {
        // 更新旧值为新值
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }
    
   /**
   * 懒执行的 watcher 会调用该方法
   *   比如:computed,在获取 vm.computedProperty 的值时会调用该方法
   * 然后执行 this.get,即 watcher 的回调函数,得到返回值
   * this.dirty 被置为 false,作用是页面在本次渲染中只会一次 computed.key 的回调函数,
   * 这也是大家常说的 computed 和 methods 区别之一是 computed 有缓存的原理所在
   * 而页面更新后会 this.dirty 会被重新置为 true,这一步是在 this.update 方法中完成的
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false       //this.dirty置为false,用于缓存。
  }
}

可以看到在实例化渲染函数观察者对象时,会将传入的 before 函数添加到观察者对象上。在数据更新时会执行 update 方法,在没有添加强制要求时,默认执行 queueWatcher 函数完成数据更新。

export function queueWatcher (watcher: Watcher) {
  ...
  flushSchedulerQueue()
  ...
}

function flushSchedulerQueue () {
  ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    ...
  }
  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 && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}

渲染 watcher

在数据发生变化时会调用 watch.run() 方法 从而触发 updateComponent 引发页面的更新

Watcher.prototype.run = function run () {
  if (this.active) {
    var value = this.get();  // 执行updateComponent方法 
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value;
      this.value = value;
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};

在渲染 watcher 创建的时候,就立即执行取值函数,完成响应式数据的依赖收集。渲染 watcher 的 update() 最终会执行 updateComponent 方法

作为优化,维护一个 watcher 队列,每次执行 watcher.update() 就尝试往队列里面添加 watcher(queueWatcher(this)),如果当前 watcher 已经存在于队列中,就不再添加。最后在nextTick中一次性执行这些 watcher 的 run 方法。

computed watcher

有个属性dirty,用于标记是否执行取值函数。

function defineComputed (target,key,userDef) {  // target:vm, key: newMsg
 
 Object.defineProperty(target, key, {
      enumerable: true,
      configurable: true,
      get: function computedGetter () {  //当视图对newMsg进行取值的时候会执行这里
        var watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
          if (watcher.dirty) {   //这里要对照Watcher的构造函数来看,默认watcher.dirty = watcher.lazy,首次执行为true
            watcher.evaluate();  //会执行watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend();
          }
          return watcher.value
        }
      },
      set: userDef.set || noop
  });
}

Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();    //执行watcher的取值函数,返回取值函数执行的结果,并将watcher添加到msg的订阅数组
  this.dirty = false;  //this.dirty置为false,用于缓存。
};

1、初始化watcher时,watcher.dirty = watcher.lazy,值为true。页面第一次访问newMsg时就会执行watcher.evaluate()

2、取值完成后,watcher.dirty = false。下一次页面再取值就会直接返回之前计算得到的值 watcher.value 。

3、如果watcher订阅的 msg 发生变化,就会通知执行watcher的 watcher.update()。lazy属性为true的watcher执行update方法是watcher.dirty = true,这样页面取值newMsg就会重新执行取值函数,返回新的值。这样就实现了computed的缓存功能。

user watcher

user watcher的核心方法就是vm.$watch:

Vue.prototype.$watch = function (expOrFn,cb,options) {

    //核心就是这里
    //expOrFn  ---> msg
    //cb  ---> 用户自己定义的回调函数,function(oldValue,newValue){console.log(oldValue,newValue)}
    
    var watcher = new Watcher(vm, expOrFn, cb, options);
  };
}


// watcher的构造函数中
if (typeof expOrFn === 'function') {
  this.getter = expOrFn;
} else {
  this.getter = parsePath(expOrFn);
}

创建user watcher时,会根据这个表达式完成取值操作,添加watcher到订阅数组。

expOrFn: 'msg'   -----> vm.msg
expOrFn: 'obj.a'  -----> vm.obj ----->vm.obj.a

deep:true时,会递归遍历当前属性对应的值,将watcher添加到所有属性上,每一次修改某一个属性都会执行watcher.update()

Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  // 执行回调函数,比如 updateComponent,触发组件更新 进入 patch 阶段
  // 触发读取操作  被 getter 拦截 进行依赖收集
  value = this.getter.call(vm, vm);
  
  if (this.deep) {
     traverse(value);  //递归遍历取值,每次取值都添加该watcher到取值属性的订阅数组。
  }
  popTarget();
  return value
};
小结

数据更新是通过观察者对象的实例方法 run 完成的,从上代码可以看到:在数据更新前会调用实例对象上的 before 方法,从而执行 beforeUpdate 生命周期钩子函数;在数据更新完成后,通过执行 callUpdatedHooks 函数完成 updated 生命周期函数的调用。

销毁阶段:

vue会将自身从父组件中删除,取消实例上所有依赖的追踪并且移除所有的事件监听器

beforeDestory: 进入销毁阶段,实例身上的数据仍可使用

beforeDestory ==>> destoryed: 卸载 watchers、 data、 将组件从已经渲染的DOM中卸载

destoryed: 组件完全销毁,取消实例上所有依赖的追踪并且移除所有的事件监听器

// $destory 完全销毁一个实例, 触发 beforeDestory、destoryed 钩子
//  清理它与其他实例的连接,接触她的全部指令及事件监听器
Vue.prototype.$destroy = function () {
    var vm = this;
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy');
    vm._isBeingDestroyed = true;
    // 将自身从父组件中删除
    var parent = vm.$parent;
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm);
    }
    // 卸载 wather
    if (vm._watcher) {
      vm._watcher.teardown();
    }
    var 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;
    // 执行一次null的pathc将虚拟dom已经渲染的dom卸载
    vm.__patch__(vm._vnode, null);
    // fire destroyed hook
    callHook(vm, 'destroyed');
    // 执行destory钩子函数,执行off卸载,绑定事件,解除与el的绑定
    vm.$off();
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null;
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null;
    }
  };


小结

在 beforeDestory 进入实例的销毁阶段,但此时还能获取实例生的数据、事件,之后进入卸载阶段,首先 将自身从父组件上移除开始卸载 watcher、组件上的 data 数据 、卸载子组件的DOM ,在 destoryed 时子实例已被销毁,数据不可使用,移除事件监听器,父组件赋值为null。

生命周期相关问题:

1. 父子组件生命周期

挂载阶段: 父beforeCreate => 父created => 父beforeMount => 子beforeCreate => 子created => 子beforeMount => 子mounted => 父mounted

子组件更新阶段: 父beforeUpdate => 子beforeUpdate => 子updated => 父updated

父组件更新阶段: 父beforeUpdate => 父updated

销毁阶段: 父beforeDestroy => 子beforeDestroy => 子destroyed => 父destroyed

2. 路由跳转时生命周期执行顺序

A 页面跳转至 B 页面 期间执行的生命周期

B.beforeMount => A.beforeDestory => A.destoryed => B.mounted

3. 页面刷新时触发的生命周期

刷新页面并不触发组件的销毁, 只触发页面的重新挂载: beforeMount => mounted

4. 关闭页面时触发生命周期

直接点击浏览器标签关闭页面, 不执行任何生命周期钩子, 如果要在页面关闭前做点事情(例如保存数据),
可以给页面绑定beforeunload或unload事件, 在事件中编写逻辑

5. 父组件中监听子组件的生命周期

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,常规的写法可能如下:

// Parent.vue
<Child @mounted="doSomething"/>

// Child.vue
mounted() {
  this.$emit("mounted");
}

此外,还有一种特别简单的方式,子组件不需要任何处理,只需要在父组件引用的时候通过 @hook 来监听即可,@hook 也可以监听其它的生命周期事件,代码如下:

<Child @hook:mounted="doSomething" />   执行完生命周期函数之后,触发该事件的执行
<Child @hook:updated="doSomething" />

参考

  1. 生命周期源码解读 :https://juejin.cn/post/6999897364597047326 、
    https://juejin.cn/post/6844904048215212045#heading-8

  2. initInject的实现 :https://juejin.cn/post/6877785132640501774

  3. 响应式原理、watcher的实现:https://juejin.cn/post/6918270595771662349#heading-3

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值