vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue 生命周期

深入了解Vue之组件的生命周期流程

作者:_只要平凡

每个Vue实例在创建时都要经过一系列初始化, 例如设置数据监听、编译模板、将实例挂载到DOM并在数据变化时更新DOM等,同时, 也会运行一些叫作生命周期钩子的函数,接下来让我们一起来探索Vue实例被创建时都经历了什么,感兴趣的同学跟着小编一起来探讨吧

生命周期流程图

每个Vue实例在创建时都要经过一系列初始化, 例如设置数据监听、编译模板、将实例挂载到DOM并在数据变化时更新DOM等. 同时, 也会运行一些叫作生命周期钩子的函数, 这给了我们在不同阶段添加自定义代码的机会. 接下来让我们一起来探索Vue实例被创建时都经历了什么.

生命周期图示(贴自vue官网)

new Vue()被调用时发生了什么

想要了解new Vue()被调用时发生了什么, 我们需要知道在Vue构造函数中实现了哪些逻辑。具体代码如下:

function Vue (options) {
    if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
        warn('Vue is a constructor and should be called with `new` keyword')
    }
    this._init(options)
}
export default Vue

从上面代码我们可以看到调用了_init函数来执行生命周期的初始化流程,那么this._init是在哪里定义的,内部原理是怎样的呢?

_init方法的定义

import { initMixin } from './init'
function Vue (options) {
    if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
        warn('Vue is a constructor and should be called with `new` keyword')
    }
    this._init(options)
}
initMixin(Vue)
export default Vue

将init.js文件导出的initMixin函数引入后,通过调用initMixin函数向Vue构造函数的原型中挂载一些方法。initMixin方法的实现代码如下:

export function initMixin (Vue) {
    Vue.prototype._init = function (options) {
        //
    }
}

_init方法的内部原理

Vue.prototype._init = function (options) {
    const vm = this
    // uid初始化值为0
    vm._uid = uid++
    let startTag, endTag
    if (__DEV__ && config.performance && mark) {
        startTag = `vue-perf-start:${vm._uid}`
        endTag = `vue-perf-end:${vm._uid}`
        mark(startTag)
    }
    // vue实例标志
    vm._Vue = true
    // 避免observed
    vm.__v_skip = true
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    if (options && options._isComponent) {
      initInternalComponent(vm, options as any)
    } else {
       // 合并构造函数配置和参数配置
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化生命周期相关
    initLifecycle(vm)
    // 初始化事件相关
    initEvents(vm)
    // 初始化render相关
    initRender(vm)
    // 调用beforeCreate钩子
    callHook(vm, 'beforeCreate', undefined, false)
    // 初始化inject
    initInjections(vm)
    // 初始化状态相关
    initState(vm)
    // 初始化provide
    initProvide(vm)
    // 调用created钩子
    callHook(vm, 'created')
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    // 如果有配置el选项 自动调用mount 否则需要手动调用mount
    if (vm.$options.el) {
        vm.$mount(vm.$options.el)
    }
}

从_init方法的源码实现, 我们可以画出new Vue的主要流程图:

接下来我们来看下_init内部几个初始化方法的源码实现:

initLifecycle

function initLifecycle (vm) {
    const options = vm.$options
    let parent = options.parenet
    if (parent && !options.abstract) {
        // 如果为抽象组件 继续往上找
        while (parent.$options.abstract && parent.$parent) {
            parent = parent.$parent
        }
        parent.$children.push(vm)
    }
    vm.$parent = parent
    vm.$root = parent ? parent.$root : vm
    vm.$children = []
    vm.$refs = []
    vm._provided = parent ? parent._provided : Object.create(null)
    vm._watcher = null
    vm._inactive = null
    vm._directInactive = false
    vm._isMounted = false
    vm._isDestroyed = false
    vm._isBeingDestroyed = false
}

initEvents

function initEvents (vm) {
    vm._events = Object.create(null)
    vm._hasHookEvent = false
    const listeners = vm.$options._parentListeners
    if (listeners) {
        updateComponentListeners(vm, listeners)
    }
}

initRender

function initRender(vm) {
  vm._vnode = null
  vm._staticTrees = null
  const options = vm.$options
  const parentVnode = (vm.$vnode = options._parentVnode!)
  const renderContext = parentVnode && (parentVnode.context as Component)
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = parentVnode
    ? normalizeScopedSlots(
        vm.$parent!,
        parentVnode.data!.scopedSlots,
        vm.$slots
      )
    : emptyObject
  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
  if (__DEV__) {
    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
    )
  }
}

initInjections

function initInjections(vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      if (__DEV__) {
        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)
  }
}

function resolveInject(
  inject: any,
  vm: Component
): Record<string, any> | undefined | null {
  if (inject) {
    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
      const provideKey = inject[key].from
      if (provideKey in vm._provided) {
        result[key] = vm._provided[provideKey]
      } else if ('default' in inject[key]) {
        const provideDefault = inject[key].default
        result[key] = isFunction(provideDefault)
          ? provideDefault.call(vm)
          : provideDefault
      } else if (__DEV__) {
        warn(`Injection "${key as string}" not found`, vm)
      }
    }
        return result
  }
}

initState

function initState (vm) {
    const opts = vm.$options
    
    if (opts.props) {
        initProps(vm, opts.props)
    }
    
    // 组合式api
    initSetup(vm)
    
    if (opts.methods) {
        initMethods(vm, opts.methods)
    }
    
    if (ops.data) {
        initData(vm, opts.data)
    } else {
        const ob = observe(vm._data = {})
        ob && ob.vmCount++
    }
    
    if (opts.compouted) {
        initComputed(vm, opts.computed)
    }
    
    // Firefox Object.prototype有watch方法 nativeWatch = {}.watch
    if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
    }
}

function initProps(vm, propsOptions) {
  const propsData = vm.$options.propsData || {}
  const props = (vm._props = shallowReactive({}))
  // 用数组保存props的key 方便便利props
  const keys: string[] = (vm.$options._propKeys = [])
  const isRoot = !vm.$parent
  if (!isRoot) {
    toggleObserving(false)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    if (__DEV__) {
      const hyphenatedKey = hyphenate(key)
      if (
        isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)
      ) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
              `overwritten whenever the parent component re-renders. ` +
              `Instead, use a data or computed property based on the prop's ` +
              `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

function initData(vm) {
  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) {
    data = {}
    __DEV__ &&
      warn(
        'data functions should return an object:\n' +
          'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      )
  }
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (__DEV__) {
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    if (props && hasOwn(props, key)) {
      __DEV__ &&
        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 data
  const ob = observe(data)
  ob && ob.vmCount++
}

function initMethods(vm, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (__DEV__) {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[
            key
          ]}" in the component definition. ` +
            `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(`Method "${key}" has already been defined as a prop.`, vm)
      }
      if (key in vm && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
            `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher(
  vm: Component,
  expOrFn: string | (() => any),
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

initProvides

function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    resolveProvided(currentInstance)[key as string] = value
  }
}

export function resolveProvided(vm: Component): Record<string, any> {
  const existing = vm._provided
  const parentProvides = vm.$parent && vm.$parent._provided
  if (parentProvides === existing) {
    return (vm._provided = Object.create(parentProvides))
  } else {
    return existing
  }
}

mount实现

到这里, 初始化基本完成. 从前面_init方法的实现, 我们可以看到初始化之后会执行mount, 代码如下:

// 如果有配置el选项 自动调用mount 否则需要手动调用mount
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

接下来我们来看下vm.$mount的具体实现:

function query (el) {
    if (typeof el === 'string') {
        const selected = document.querySelector(el)
        if (!selected) {
            __DEV__ && warn('Cannot find element: ' + el)
            return docuemnt.createElement('div')
        }
        return selected
    } else {
    return el
}
function cached (fn) {
    // 模板缓存
    const cache = Object.create(null)
    return function (str) {
        const hit = cache[str]
        return hit || (cache[str] = fn(str))
    }
}
const idToTemplate = cached(id => {
    const el = query(id)
    return el && el.innerHTML
})
function getOuterHTML (el) {
    if (el.outerHTML) {
        return el.outerHTML
    } else {
        const container = document.createElement('div')
        container.appendChild(el.cloneNode(true))
        return container.innerHTML
    }
}
Vue.prototype.$mount = function (el, hydrating) {
    el = el && inBrower ? query(el) : undefined
    return mountComponent(this, el, hydrating)
}
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el, hydrating) {
    el = el && query(el)
    if (el === document.body || el === document.documentElement) {
        __DEV__ &&
            warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`)
        return this
    }
    const options = this.$options
    // 如果options没有render 使用template属性
    if (!options.render) {
        let template = options.template
        // template属性优先使用
        if (template) {
            if (typeof template === 'string') {
                if (template.charAt(0) === '#') {
                    template = idToTemplate(template)
                    if (__DEV__ && !template) {
                    warn(
                      `Template element not found or is empty: ${options.template}`,
                      this
                    )
                  }
                }
            } else if (template.nodeType) {
                template = template.innerHTML
            } else {
                if (__DEV__) {
                  warn('invalid template option:' + template, this)
                }
                return this
            }
        } else if (el) { // template属性不存在 再使用el属性
            template = getOuterHTML(el)
        }
        if (template) {
            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
          if (__DEV__ && config.performance && mark) {
            mark('compile end')
            measure(`vue ${this._name} compile`, 'compile', 'compile end')
          }
        }
    }
    return mount.call(this, el, hydrating)
}

从$mount源码中我们可以知道mount的核心是mountComponent函数, 下面我们来看下mountComponent的实现:

function mountComponent (vm, el, hydrating) {
    vm.$el = el
    if (!vm.$options.render) {
        vm.$options.render = createEmptyNode
        if (__DEV__) {
            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')
    let updateComponent
    if (__DEV__ && 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)
    }
  }
  const watcherOptions: WatcherOptions = {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }

  if (__DEV__) {
    watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
    watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
  }
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  hydrating = false
  const preWatchers = vm._preWatchers
  if (preWatchers) {
    for (let i = 0; i < preWatchers.length; i++) {
      preWatchers[i].run()
    }
  }
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

mountComponent代码实现中会实例化一个Watcher对象, 这里主要有两个作用:

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

我们接着来看下vm._update的实现:

Vue.prototype._update = function (vnode, hydrating) {
    const vm = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    if (!prevVnode) {
      // 初始化渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
    } else {
      // 组件更新时
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    let wrapper = vm
    while (
      wrapper &&
      wrapper.$vnode &&
      wrapper.$parent &&
      wrapper.$vnode === wrapper.$parent._vnode
    ) {
      wrapper.$parent.$el = wrapper.$el
      wrapper = wrapper.$parent
    }
  }

我们可以看到组件初始化渲染时会执行vm.__patch__方法, vm.__patch__的具体实现本章就不细说了, 后面在虚拟DOM章节中再一起学习下, 这里就大概说下__patch__方法主要做了什么:

最后

以上就是vue组件生命周期主要流程, 从源码实现中, 我们可以知道父子组件初始化生命周期钩子执行顺序:

beforeCreate(父) -> created(父) -> beforeMount(父) -> beforeCreate(子) -> created(子) -> beforeMount(子) -> mounted(子) -> mounted(父)

以上就是深入了解Vue之组件的生命周期流程的详细内容,更多关于Vue 生命周期的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文