Vue(v2.6.14)源码解毒(二):初始化和挂载

457 阅读4分钟

初始化流程

new Vue

我们在使用 Vue 的时候,首页就是先 new Vue(...) ;在上一章中通过分析构建流程,我们得出入口文件 src/platforms/web/entry-runtime-with-compiler.js ,通过入口文件,我们一步一步找到 Vue 构造函数定义所在:

// src/platforms/web/entry-runtime-with-compiler.js
// ...
import Vue from './runtime/index'
// ...
// src/platforms/web/runtime/index.js
import Vue from 'core/index'
// ...
// src/core/index.js
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 初始化全局 API
initGlobalAPI(Vue)
// ...
// src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
// Vue 构造函数
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')
  }
  // 调用 Vue.prototype_init 方法,该方法是在 initMixin 中定义的
  this._init(options)
}
// 定义 Vue.prototype_init 方法
initMixin(Vue)
/**
 * 定义:
 *   Vue.prototype.$data
 *   Vue.prototype.$props
 *   Vue.prototype.$set
 *   Vue.prototype.$delete
 *   Vue.prototype.$watch
 */
stateMixin(Vue)
/**
 * 定义 事件相关的 方法:
 *   Vue.prototype.$on
 *   Vue.prototype.$once
 *   Vue.prototype.$off
 *   Vue.prototype.$emit
 */
eventsMixin(Vue)
/**
 * 定义:
 *   Vue.prototype._update
 *   Vue.prototype.$forceUpdate
 *   Vue.prototype.$destroy
 */
lifecycleMixin(Vue)
/**
 * 定义:
 *   Vue.prototype.$nextTick
 *   Vue.prototype._render
 */
renderMixin(Vue)
export default Vue

_init

// src/core/instance/init.js
export function initMixin (VueClass<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vmComponent = this
    // 每个实例都保存一个 _uid
    vm._uid = uid++
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }
    // a flag to avoid this being observed
    vm._isVue = true
    // 处理组件配置项
    if (options && options._isComponent) {
      // 每个子组件初始化时走这里,这里只做了一些性能优化
      // 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
      initInternalComponent(vm, options)
    } else {
      // 合并选项,合并默认选项和自定义选项
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 设置代理,将 vm 实例上的属性代理到 vm._renderProxy
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化实例关系属性,$parent、$children、$refs、$root等
    initLifecycle(vm)
    // 初始化自定义事件,处理父组件传递的事件和回调
    initEvents(vm)
    // 解析组件的插槽信息,得到 vm.$slot,处理渲染函数(_render),得到 vm.$createElement 方法,即 h 函数
    initRender(vm)
    // 调用 beforeCreate 钩子函数
    callHook(vm, 'beforeCreate')
    // 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
    initInjections(vm)
    // 数据响应式核心,处理 props、methods、data、computed、watch
    initState(vm)
    // 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
    initProvide(vm) // resolve provide after data/props
    // 调用 created 钩子函数
    callHook(vm, 'created')
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

上面代码很清晰的看出初始化都做了哪些事情,在初始化的最后,如果有 el 属性,则会自动调用 vm.$mount 进行挂载,否则我们就需要手动调用 $mount。接下里就进入了挂载阶段。

Vue 实例挂载

$mount

入口文件 src/platforms/web/entry-runtime-with-compiler.js

/* @flow */
import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})
/**
 * 编译器的入口
 * 进行预编译,最终将模版编译成 render 函数
 */
// 缓存原型上的方法
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方法,则解析 template 和 el,并转换为 render 函数
   *   优先级:render > template > el
   */
  if (!options.render) {
    let template = options.template
    // template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // { template: '#app' },以 id 为 ‘app’ 的节点,作为挂载节点
          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 是一个正常的元素,获取其 innerHtml 作为模版
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // el
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      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)
      // 将两个渲染函数放到 this.$options 上
      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')
      }
    }
  }
  // 调用原型上方法挂载
  return mount.call(this, el, hydrating)
}
/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (elElement): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}
Vue.compile = compileToFunctions
export default Vue

从上面代码可以看出,不管定义 render 方法还是 eltemplate 属性,最终的目的就是得到 render 渲染函数。然后保存在 options 上。

编译模板,得到 render 渲染函数,通过调用 compileToFunctions 方法,这个到编译器的时候再一块看。

最后调用原型上的 $mount ,定义在 src/platform/web/runtime/index.js

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

实际调用 mountComponent ,定义在 src/core/instance/lifecycle.js

mountComponent

// src/core/instance/lifecycle.js
export function mountComponent (
  vmComponent,
  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')
  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 {
    // 执行 vm._render() 函数,得到 虚拟 DOM,并将 vnode 传递给 _update 方法,接下来就该到 patch 阶段了
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
 
 // vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

mountComponent 内定义了 updateComponent 方法,然后实例化一个Watcher,同时将 updateComponent 作为参数传入,在 Watcher 的回调函数中被调用。Watcher 在这里主要是初始化和数据变化时,执行回调函数。

最后设置 vm._isMounted = true ,表示实例已挂载。

updateComponent 的调用会执行 vm._updatevm._rendervm._render 获取虚拟DOM,vm._update 更新视图。

上面代码出现了三个生命周期钩子 beforeMountbeforeUpdatemounted ;也就是说,在执行 vm._render()  之前,执行了 beforeMount 钩子函数;在执行完 vm._update()  把虚拟DOM转换真实 DOM 后,执行 mounted 钩子函数;后续若数据变化时,通过 _isMounted 标记,表示已挂载则执行 beforeUpdate 钩子函数。

这里值得注意的是,在 mounted 钩子执行前有个判断,只有在父虚拟 Node 为 null 的时候执行。只有 new Vue 才会走到这里,如果是组件的话,它的父虚拟 Node 是存在的。组件的 mounted 在别的地方。

相关链接

Vue(v2.6.14)源码解毒(预):手写一个简易版Vue

Vue(v2.6.14)源码解毒(一):准备工作

Vue(v2.6.14)源码解毒(二):初始化和挂载

Vue(v2.6.14)源码解毒(三):响应式原理

Vue(v2.6.14)源码解毒(四):更新策略

Vue(v2.6.14)源码解毒(五):render和VNode

Vue(v2.6.14)源码解毒(六):update和patch

Vue(v2.6.14)源码解毒(七):模板编译

如果觉得还凑合的话,给个赞吧!!!也可以来我的个人博客逛逛 www.mingme.net/