Vue源码学习-基于2.1.7-主线

Vue源码主线

Vue构造函数位于src/core/instance/index.js文件中instance是存放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) {
  // 安全模式处理告诉开发者必须使用 new 操作符调用 Vue。
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }

  // 将参数options 传入进_init()
  this._init(options)
}

// 引入依赖,定义Vue构造函,然后以Vue构造函数为参数调用这5个方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

// 最后导出Vue
export default Vue

以上对应的5个方法其实就是Vue原型prototype上挂载的方法或属性

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

  this._init(options)
}

// 引入依赖,定义Vue构造函,然后以Vue构造函数为参数调用这5个方法

/**
* 初始化相关
* Vue.prototype._init = function (options?: Object) {}
* */
initMixin(Vue)

/*
* 状态相关
* // Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象属性的访问。
* Vue.prototype.$data
*
* // 设置对象的属性。如果对象是响应式的,确保属性被创建后也是响应式的,同时触发视图更新。这个方法主要用于避开 Vue 不能检测属性被添加的限制。
* Vue.prototype.$set = set
*
* // 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制,但是你应该很少会使用它。
* Vue.prototype.$delete = del
*
* // 回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。
* Vue.prototype.$watch = function (){}
* */
stateMixin(Vue)

/**
* 事件相关
* // 监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
* Vue.prototype.$on = function (event: string, fn: Function): Component {}
*
* // 监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器
* Vue.prototype.$once = function (event: string, fn: Function): Component {}
*
* // 移除自定义事件监听器
* Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
*
* // 触发当前实例上的事件。附加参数都会传给监听器回调
* Vue.prototype.$emit = function (event: string): Component {}
* */
eventsMixin(Vue)

/*
* 生命周期相关
* // 挂载 beforeMount和mounted生命钩子
* Vue.prototype._mount = function (el?: Element | void,hydrating?: boolean): Component {}
*
* // 挂载 beforeUpdate和updated生命钩子
* Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
*
* // 更新props、propsData、$listeners、$forceUpdate、$slots、$parent、$children等详细请参见该方法
* Vue.prototype._updateFromParent = function (propsData: ?Object,listeners: ?Object,parentVnode: VNode,renderChildren: ?Array<VNode>) {}
*
* // 迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
* Vue.prototype.$forceUpdate = function () {}
*
* // 完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。
* // 触发 beforeDestroy 和 destroyed 的钩子
* Vue.prototype.$destroy = function () {}
* */
lifecycleMixin(Vue)

/*
* 渲染相关
*
* // 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。
* Vue.prototype.$nextTick = function (fn: Function) {}
*
* // 字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。
* Vue.prototype._render
*
* // toString方法
* Vue.prototype._s = _toString
*
* // 将文本转换为vnode
* Vue.prototype._v = createTextVNode
*
* // number转换
* Vue.prototype._n = toNumber
*
* // 创建一个空节点
* Vue.prototype._e = createEmptyVNode
*
* // 检查两个值是否宽松相等 —— 也就是说,如果它们是纯对象,它们是否具有相同的形状?
* Vue.prototype._q = looseEqual
*
* // 寻找该数组对应值的索引值,如果找到了,返回索引值;否则返回 -1
* Vue.prototype._i = looseIndexOf
*
* // 渲染静态节点
* Vue.prototype._m = function renderStatic (index: number,isInFor?: boolean): VNode | Array<VNode> {}
*
* // 将节点标记为静态(v-once)只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
* Vue.prototype._o = function markOnce () {}
*
* // 过滤器 filters
* Vue.prototype._f = function resolveFilter (id) {}
*
* // 渲染v-for
* Vue.prototype._l = function renderList (){}
*
* // 渲染Slot
* Vue.prototype._t = function (){}
*
* // 调用 v-bind 方法
* Vue.prototype._b = function bindProps (data: any,tag: string,value: any,asProp?: boolean): VNodeData {}
*
* // 检查 v-on 建码
* Vue.prototype._k = function checkKeyCodes (
* */
renderMixin(Vue)

// 最后导出Vue
export default Vue

根据我们之前寻找 Vue 的路线,这只是刚刚开始,我们追溯路线往回走,那么下一个处理 Vue 构造函数的应该是src/core/index.js

// 导入已经在原型上加载了方法和属性后的Vue
import Vue from './instance/index'
// 导入全局API
import { initGlobalAPI } from './global-api/index'
// 导入服务渲染
import { isServerRendering } from 'core/util/env'

/*
* 该方法作用是在Vue构造函数上面挂载静态属性和方法
* // 导出Vue的全部配置
* Vue.config
*
* // 一些工具方法
* Vue.util
*
* // 在一个对象上设置一个属性。 添加新的属性和
* Vue.set = set
*
* // 删除一个属性,并在必要时触发更改。
* Vue.delete = del
*
* // 推迟任务以异步执行它。
* Vue.nextTick = util.nextTick
*
* // 组件可以拥有的资源列表。包含 Vue 实例
* Vue.options = {
    components: {
        KeepAlive
    },
    directives: {},
    filters: {},
    _base: Vue
}
*
* // 安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入
* Vue.use
*
* // 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
* Vue.mixin
*
* Vue.cid = 0
*
* // 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
* Vue.extend
*
* // 注册或获取全局组件。注册还会自动使用给定的id设置组件的名称
* Vue.component = function(){}
*
* // 注册或获取全局指令。
* Vue.directive = function(){}
*
* // 注册或获取全局过滤器。
* Vue.filter = function(){}
*
* // 当前 Vue 实例是否运行于服务器。
* Vue.prototype.$isServer
*
* // 版本号
* Vue.version = '__VERSION__'
* */
initGlobalAPI(Vue)

// 在Vue.prototype上挂载 $isServer
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

// 挂载版本属性
Vue.version = '__VERSION__'

export default Vue

下一个就是web-runtime.js 文件了,web-runtime.js文件主要做了三件事儿:

1、覆盖 Vue.config 的属性,将其设置为平台特有的一些方法
2、Vue.options.directives 和 Vue.options.components 安装平台特有的指令和组件
3、在 Vue.prototype 上定义 patch 和 $mount

代码如下

一些引用
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { devtools, inBrowser, isEdge } from 'core/util/index'
import { patch } from 'web/runtime/patch'
import platformDirectives from 'web/runtime/directives/index'
import platformComponents from 'web/runtime/components/index'
import {
  query,
  isUnknownElement,
  isReservedTag,
  getTagNamespace,
  mustUseProp
} from 'web/util/index'

/*
* install platform specific utils
*
* 覆盖Vue.config的值
*
* isUnknownElement 获取元素
* isReservedTag 获取标签
* getTagNamespace 获取标签名称
* mustUseProp 获取标签定义
* */
Vue.config.isUnknownElement = isUnknownElement
Vue.config.isReservedTag = isReservedTag
Vue.config.getTagNamespace = getTagNamespace
Vue.config.mustUseProp = mustUseProp


/*
* 安装平台运行时指令和组件
* install platform runtime directives & components
*
* 包含 Vue 实例可用指令的哈希表。
* Vue.options.directives(componentUpdated、inserted、bind、unbind、update)
*
* 包含 Vue 实例可用组件
* Vue.options.components(KeepAlive、Transition、TransitionGroup)
*
* Vue.options变化为
*
* Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: {},
    _base: Vue
}
* */
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

/*
* 安装补丁功能
* install platform patch function
* */
Vue.prototype.__patch__ = inBrowser ? patch : noop

/*
* 手动地挂载一个未挂载的实例。
* wrap mount
* */
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 根据浏览器环境决定用不用 query(el)获取元素
  el = el && inBrowser ? query(el) : undefined
  // 然后将 el 作为参数传递给 this._mount()
  return this._mount(el, hydrating)
}

/*
* 配置是否允许 vue-devtools 检查代码。开发版本默认为 true,生产版本默认为 false。生产版本设为 true 可以启用检查。
* devtools global hook
* */
/* istanbul ignore next */
setTimeout(() => {
  if (config.devtools) {
    if (devtools) {
      devtools.emit('init', Vue)
    } else if (
      process.env.NODE_ENV !== 'production' &&
      inBrowser && !isEdge && /Chrome\/\d+/.test(window.navigator.userAgent)
    ) {
      console.log(
        'Download the Vue Devtools for a better development experience:\n' +
        'https://github.com/vuejs/vue-devtools'
      )
    }
  }
}, 0)

export default Vue

最后一个处理 Vue 的文件就是入口文件web-runtime-with-compiler.js 了,该文件做了两件事

1、缓存来自web-runtime.js文件的 $mount 函数

const mount = Vue.prototype.$mount

然后覆盖覆盖了 Vue.prototype.$mount

2、在 Vue 上挂载 compile,compileToFunctions 函数的作用,就是将模板 template 编译为render函数。

Vue.compile = compileToFunctions

具体代码如下

// 一些引入
import Vue from './web-runtime'
import { warn, cached } from 'core/util/index'
import { query } from 'web/util/index'
import { shouldDecodeNewlines } from 'web/util/compat'
import { compileToFunctions } from 'web/compiler/index'
 

// 缓存Template模板
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

// 缓存来自 web-runtime.js 文件的 $mount 函数 =》获取到对应元素的内容
const mount = Vue.prototype.$mount

// 覆盖了 Vue.prototype.$mount
Vue.prototype.$mount = function (
  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(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options

  // 解析 模板/el 并转换为渲染函数
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    // 如果有传入 template模板
    if (template) {
      // 判断是否 template 是否是字符串
      if (typeof template === 'string') {
        // 如果模板template第一个字符是 # 就返回错误
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      }
      // 如果不是字符串就获取template里面的内容 :如<template></template>
      else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    }
    // 如果没有传入template模板,就把 el节点 转换成模板字符串
    else if (el) {
      template = getOuterHTML(el)
    }
    // Vue 上挂载 compile
    // compileToFunctions 函数的作用,就是将模板 template 编译为render函数。
    if (template) {
      const { render, staticRenderFns } = compileToFunctions(template, {
        warn,
        shouldDecodeNewlines,
        delimiters: options.delimiters
      }, this)

      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }

  return mount.call(this, el, hydrating)
}

/**
 * 获取元素的外部HTML,注意
 * IE中的SVG元素
 * 输出外部HTML
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): 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

至此,我们还原了 Vue 构造函数,总结一下:

1、Vue.prototype 下的属性和方法的挂载主要是在 src/core/instance 目录中的代码处理的
2、Vue 下的静态属性和方法的挂载主要是在 src/core/global-api 目录下的代码处理的
3、web-runtime.js 主要是添加web平台特有的配置、组件和指令
4、web-runtime-with-compiler.js 给Vue的 $mount 方法添加 compiler 编译器,支持 template