Vue2.x源码学习笔记(十)——实例方法(生命周期篇)

176 阅读2分钟

/src/core/instance/lifecycle.js 的lifecycleMixin函数中往Vue的原型上挂载了$forceUpdate和$destroy方法。

vm.$forceUpdate

Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }
//Watcher类中有这样处理:
if (isRenderWatcher) {
   vm._watcher = this
}

vm._watcher存放了当前实例的渲染watcher。调用渲染watcher的update方法,迫使组件重新渲染。它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

vm.$destory

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

为了防止$destroy被反复执行,所以当vm._isBeingDestroyed为true时表示实例已经开始销毁,直接return。

先调用beforeDestroy生命周期函数,将_isBeingDestroyed置为true,表示开始销毁实例。如果父组件没有被销毁,那么就将自己从父组件的children列表中移除,解除与父组件的关系。将当前实例的_watcher和_watchers中的watcher都通过teardown()移除监听。

_isDestroyed置为true表示实例已经销毁,调用 __patch__,销毁节点。调用destroyed生命周期函数。调用$off移除所有事件。

vm.$nextTick

Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn, this)
}

nextTick方法请看第Vue2.x源码学习笔记(七)

vm.$mount

function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

第一个参数el,它表示挂载的元素,可以是字符串,也可以是DOM对象,如果是字符串在浏览器环境下会调用query方法转换成DOM对象的。query方法中调用原生的document.querySelector(el)去获取DOM元素。如果没获取到则会创建一个div作为元素返回。

第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。

最终会去调用mountComponent进行挂载。

Runtime+Compiler版本的$mount

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  if (el === document.body || el === document.documentElement) {
   //warn
    )
    return this
  }
  const options = this.$options
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          if (process.env.NODE_ENV !== 'production' && !template) {
            //warn
          }
        }
      } 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)
    }
    if (template) {
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

如果是Runtime+Compiler版本时会重写原来的$mount方法。el不能是body和html元素,如果options中没有render函数则会进行模板编译的过程。

如果options中提供的template是‘#id’,则会通过idToTemplate转为元素字符串。如果template是一个元素,则会通过innerHTML去获取元素字符串。如果没有template则会通过el去获取元素字符串。

然后通过compileToFunctions对元素字符串进行编译,编译生成render函数添加到options中。然后调用原来的$mount方法。最终去调用mountComponent方法。

mountComponent

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') {
      //warn
    }
  }
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

如果options中没有render函数则会创建一个空的注释vnode节点并且抛出警告。调用beforeMount生命周期函数。然后定义一个updateComponent函数,updateComponent函数中会执行vm._update方法,将通过vm._render()生成的虚拟dom渲染成真实dom。

然后将updateComponent函数在new Watcher时作为参数传入。这个watcher是一个渲染watcher,会存放在实例的_watcher上。每当依赖项发生更新时就会派发更新然后调用updateComponent函数更新时图,期间会调用beforeUpdate生命周期函数,当挂载完毕后会调用mounted生命周期函数。