「试着读读 Vue 源代码」初始化前后做了哪些事情 ❓

3,910 阅读10分钟

说明

  • 首先这篇文章是读 vue.js 源代码的梳理性文章,文章分块梳理,记录着自己的一些理解及大致过程;更重要的一点是希望在 vue.js 3.0 发布前深入的了解其原理。

  • 如果你从未看过或者接触过 vue.js 源代码,建议你参考以下列出的 vue.js 解析的相关文章,因为这些文章更细致的讲解了这个工程,本文只是以一些 demo 演示某一功能点或 API 实现,力求简要梳理过程。

  • 如果搞清楚了工程目录及入口,建议直接去看代码,这样比较高效 ( 遇到难以理解对应着回来看看别人的讲解,加以理解即可 )

  • 文章所涉及到的代码,基本都是缩减版,具体还请参阅 vue.js - 2.5.17

  • 如有任何疏漏和错误之处欢迎指正、交流

初始化前

调用关系

JavaScript 本身是一种直译式脚本语言,在找到入口后,主要需要理清其调用关系? 找出 Vue 构造函数的在哪定义了?按照这个逻辑,跟着程序一步一步走即可。

首先src/platforms/web/entry-runtime-with-compiler.js

这个文件最开始,引入一些方法与配置,并导入了 Vue 进而程序去执行 ./runtime/index 文件


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';

以下代码省略, 将在分析初始化时展开...

接着 src/platforms/web/runtime/index.js

这个文件也是引入一些方法与配置,并导入了 Vue , 程序继续走到 core/index


import Vue from 'core/index';
import config from 'core/config';
import { extend, noop } from 'shared/util';
import { mountComponent } from 'core/instance/lifecycle';
import { devtools, inBrowser, isChrome } from 'core/util/index';
import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index';

import { patch } from './patch';
import platformDirectives from './directives/index';
import platformComponents from './components/index';

以下代码省略, 将在分析初始化时展开...

来到核心代码 src/core/index.js

该文件仍然也是从外部文件导入了 Vue , 程序来到 ./instance/index


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';

以下代码省略, 将在分析初始化时展开...

最后 src/core/instance/index.js


import { initMixin } from './init';
...

/**
 * Vue构造函数
 *
 * @param {*} options 选项参数
 */
function Vue(options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue是一个构造函数,应该用“new”关键字调用');
  }
  this._init(options);
}

export default Vue;

以下代码省略, 将在分析初始化时展开...

综上:

  • src/core/instance/index.js ( 定义 Vue 构造函数 ) =>
  • src/core/index.js ( 在 Vue 构造函数上添加全局的 API ) =>
  • web/runtime/index.js ( 安装特定于平台的 utils & 运行时指令和组件 & 定义公用的挂载方法 & 配置 devtools 全局钩子 ) =>
  • web/entry-runtime-with-compiler.js ( 重写 mount 函数,给运行时版的mount 函数增加编译模板的能力 )

初始化前做的事情

根据上述调用关系一步一步走,首先看到最初定义 Vue 构造函数的文件到底做了哪些事情

初始化前 - 定义 Vue 构造函数并执行五个函数

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';

function Vue(options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue是一个构造函数,应该用“new”关键字调用');
  }
  this._init(options);
}

initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

export default Vue;

initMixin

该方法就做了一件事,在 Vue.prototype 添加 _init 方法。

export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function(options?: Object) {
    // 代码省略,在初始化会细致分析
  };
}

stateMixin

import {
  set,
  del,
  observe,
  defineReactive,
  toggleObserving
} from '../observer/index';
...

export function stateMixin(Vue: Class<Component>) {
  // 在使用object.defineproperty时,flow在直接声明定义对象方面存在一些问题,因此我们必须在这里以程序的方式构建对象。
  const dataDef = {};
  dataDef.get = function() {
    return this._data;
  };
  const propsDef = {};
  propsDef.get = function() {
    return this._props;
  };

  // 在非生产环境下 设置 $data $props 为只读属性
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function(newData: Object) {
      warn('避免替换实例根$data。 而是使用嵌套数据属性。', this);
    };
    propsDef.set = function() {
      warn(`$props 是只读的。`, this);
    };
  }
  // 在Vue原型上定义两个属性,并分别代理了 _data _props 的实例属性
  Object.defineProperty(Vue.prototype, '$data', dataDef);
  Object.defineProperty(Vue.prototype, '$props', propsDef);

  // 在 vue 原型上添加 实例方法 / 数据相关: $set/$delete/$watch
  Vue.prototype.$set = set; // 向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新
  Vue.prototype.$delete = del; // 删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。
  Vue.prototype.$watch = function( // 观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    // 代码省略,在初始化会细致分析
  };

  ...
}

eventsMixin

Vue.prototype 添加实例方法 / 事件相关:$on/$once/$off/$emit

export function eventsMixin(Vue: Class<Component>) {
  // 作用:监听当前实例上的自定义事件。事件可以由vm.$emit触发。回调函数会接收所有传入事件触发函数的额外参数。
  Vue.prototype.$on = function(
    event: string | Array<string>,
    fn: Function
  ): Component {
    // ...
  };

  // 作用:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器
  Vue.prototype.$once = function(event: string, fn: Function): Component {
    // ...
  };

  // 作用:移除自定义事件监听器。
  Vue.prototype.$off = function(
    event?: string | Array<string>,
    fn?: Function
  ): Component {
    // ...
  };

  // 作用:触发当前实例上的事件。附加参数都会传给监听器回调。
  Vue.prototype.$emit = function(event: string): Component {
    // ...
  };
}

lifecycleMixin

Vue.prototype 添加实例方法 / 生命周期相关:_update/$forceUpdate/$destroy


export function lifecycleMixin(Vue: Class<Component>) {
  Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
    // ...
  }

  // 作用:迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
  Vue.prototype.$forceUpdate = function() {
    // ...
  }

  // 作用:完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。
  Vue.prototype.$destroy = function() {
    // ...
  }

initRender

Vue.prototype 添加实例方法:$nextTick/_render/_o/_n等。

export function installRenderHelpers(target: any) {
  target._o = markOnce;
  target._n = toNumber;
  target._s = toString;
  target._l = renderList;
  target._t = renderSlot;
  target._q = looseEqual;
  target._i = looseIndexOf;
  target._m = renderStatic;
  target._f = resolveFilter;
  target._k = checkKeyCodes;
  target._b = bindObjectProps;
  target._v = createTextVNode;
  target._e = createEmptyVNode;
  target._u = resolveScopedSlots;
  target._g = bindObjectListeners;
}
import {
  warn,
  nextTick,
  emptyObject,
  handleError,
  defineReactive
} from '../util/index';
import { installRenderHelpers } from './render-helpers/index';

export function renderMixin(Vue: Class<Component>) {
  installRenderHelpers(Vue.prototype); // 安装运行时方便助手

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

  Vue.prototype._render = function(): VNode {
    // ...
  };
}

断点调试

综上所述该文件主要做了两件事:定义 Vue 构造函数包装 Vue.prototype

初始化前 - 在 Vue 构造函数上添加全局的 API

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';

initGlobalAPI(Vue); // 在 Vue 构造函数上添加全局的API

// 在 Vue.prototype 上添加 $isServer 只读属性,该属性代理了 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
});

// 在 Vue.prototype 上添加 $ssrContext 只读属性,该属性代理了 $vnode.ssrContext
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get() {
    return this.$vnode && this.$vnode.ssrContext;
  }
});

// 为 ssr 运行时助手安装公开 FunctionalRenderContext
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
});

Vue.version = '__VERSION__'; // 在 Vue 上添加静态属性 version

export default Vue;

initGlobalAPI

初始化全局 API

/* @flow */
import config from '../config';
import { initUse } from './use';
import { initMixin } from './mixin';
import { initExtend } from './extend';
import { initAssetRegisters } from './assets';
import { set, del } from '../observer/index';
import { ASSET_TYPES } from 'shared/constants';
import builtInComponents from '../components/index';

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index';

// 全局API以静态属性和方法的形式被添加到 Vue 构造函数
export function initGlobalAPI(Vue: GlobalAPI) {
  const configDef = {};
  configDef.get = () => config;
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn('不要替换 Vue.config 对象,请设置单独的字段代替。');
    };
  }
  Object.defineProperty(Vue, 'config', configDef); // 在 Vue 上添加 config 只读属性,该属性代理了 config

  // 暴露 util 的方法。注意:这些不被认为是公共API的一部分——除非您意识到了风险,否则请避免依赖它们。
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  };

  // 在 Vue 上添加 set/delete/nextTick/options 属性
  Vue.set = set;
  Vue.delete = del;
  Vue.nextTick = nextTick;
  Vue.options = Object.create(null);

  // 在 Vue.options 添加 components, directives, filters 属性
  // ASSET_TYPES = [ 'component', 'directive', 'filter' ]
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null);
  });

  // 这用于标识“基本”构造函数,以便在Weex的多实例场景中扩展所有纯对象组件。
  Vue.options._base = Vue;

  // 将 builtInComponents 的属性混入到 Vue.options.components 中
  extend(Vue.options.components, builtInComponents); // extend() 将属性混合到目标对象中

  /*
    包装之后 Vue.options 结果如下:
      Vue.options = {
        components: {
          KeepAlive
        },
        directives: Object.create(null),
        filters: Object.create(null),
        _base: Vue
      }
  */

  // 在 Vue 构造函数上添加 use 静态方法,全局API Vue.use
  initUse(Vue);
  // 在 Vue 构造函数上添加 mixins 静态方法,全局API Vue.mixins
  initMixin(Vue);
  // 在 Vue 构造函数上添加 Vue.cid 静态属性 extend 静态方法,全局API Vue.extend
  initExtend(Vue);
  // 在 Vue 构造函数上添加 三个 静态方法,分别用来全局注册组件,指令和过滤器
  initAssetRegisters(Vue);
}

接下来就其中细节部分分别展开讨论

来自 ../components/indexbuiltInComponents

实际只是导出了包含内置组件(keep-alive)属性的对象

import KeepAlive from './keep-alive';

export default {
  KeepAlive
};

keep-alive 内容如下:

export default {
  name: 'keep-alive',

  abstract: true, // 是否是抽象组件

  props: {
    // ...
  },

  created() {
    // ...
  },

  destroyed() {
    // ...
  },

  mounted() {
    // ...
  },

  render() {
    // ...
  }
};

initUse

export function initUse(Vue: GlobalAPI) {
  // 作用:安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。
  //      如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
  Vue.use = function(plugin: Function | Object) {
    // ...
  };
}

initMixin

export function initMixin(Vue: GlobalAPI) {
  // 作用:全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
  //      插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。
  Vue.mixin = function(mixin: Object) {
    // ...
  };
}

initExtend

export function initExtend(Vue: GlobalAPI) {

  // 每个实例构造函数,包括Vue,都有一个惟一的cid。这使我们能够为原型继承创建包装的“子构造函数”并缓存它们。
  Vue.cid = 0
  let cid = 1

  // 作用:使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。
  Vue.extend = function (extendOptions: Object): Function {
    // ...
  }

initAssetRegisters

export function initAssetRegisters(Vue: GlobalAPI) {

  // 创建 asset 注册方法
  // ASSET_TYPES = [ 'component', 'directive', 'filter' ]
  ASSET_TYPES.forEach(type => {
    Vue[type] = function(
      id: string,
      definition: Function | Object
    ): Function | Object | void {
    // ...
  }

  // Vue.component( id, [definition] ) 注册或获取全局组件。注册还会自动使用给定的id设置组件的名称
  // Vue.directive( id, [definition] ) 注册或获取全局指令。
  // Vue.filter( id, [definition] )    注册或获取全局过滤器。
}

断点调试

综上所述该文件主要做了一件事:包装 Vue 构造函数

初始化前 - 安装特定于平台的 utils & 运行时指令和组件 & 定义公用的挂载方法 & 配置 devtools 全局钩子

/* @flow */
import Vue from 'core/index';
import config from 'core/config';
import { extend, noop } from 'shared/util';
import { mountComponent } from 'core/instance/lifecycle';
import { devtools, inBrowser, isChrome } from 'core/util/index';
import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index';

import { patch } from './patch';
import platformDirectives from './directives/index';
import platformComponents from './components/index';

/********* 安装特定于平台的utils **********/

Vue.config.mustUseProp = mustUseProp; // 检查属性是否必须使用属性绑定,例如,值与平台相关。
Vue.config.isReservedTag = isReservedTag; // 检查是否是保留标签,以便不能将其注册为组件。这是平台相关的,可能会被覆盖。
Vue.config.isReservedAttr = isReservedAttr; // 检查是否是保留属性,使其不能用作组件 prop。这是平台相关的,可能会被覆盖。
Vue.config.getTagNamespace = getTagNamespace; // 获取元素的名称空间
Vue.config.isUnknownElement = isUnknownElement; // 检查标记是否为未知元素。平台相关的。

/********* 安装特定于平台的utils **********/

/********* 安装平台运行时指令和组件 **********/

extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
/*
    对 Vue.options.directives/components 合并包装之后:
      Vue.options = {
        components: {
          KeepAlive,
          Transition,
          TransitionGroup
        },
        directives: {
          model,
          show
        },
        filters: Object.create(null),
        _base: Vue
      }
*/

/********* 安装平台运行时指令和组件 **********/

Vue.prototype.__patch__ = inBrowser ? patch : noop; // 安装平台补丁功能

/**
 * 公用的挂载方法
 *
 * @param {String | Element} el 挂载元素
 * @param {Boolean} hydrating 用于 Virtual DOM 的补丁算法
 * @returns {Function} 真正的挂载组件的方法
 */
Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating);
};

/************** 配置 devtools 全局钩子函数 与 开发提示 **************/
if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      if (devtools) {
        devtools.emit('init', Vue);
      } else if (
        process.env.NODE_ENV !== 'production' &&
        process.env.NODE_ENV !== 'test' &&
        isChrome
      ) {
        console[console.info ? 'info' : 'log'](
          '下载Vue Devtools扩展以获得更好的开发体验:\n' +
            'https://github.com/vuejs/vue-devtools'
        );
      }
    }
    if (
      process.env.NODE_ENV !== 'production' &&
      process.env.NODE_ENV !== 'test' &&
      config.productionTip !== false &&
      typeof console !== 'undefined'
    ) {
      console[console.info ? 'info' : 'log'](
        `您正在以开发模式运行Vue。\n` +
          `在部署生产时,请确保打开生产模式。\n` +
          `详情请浏览 https://vuejs.org/guide/deployment.html`
      );
    }
  }, 0);
}
/************** 配置 devtools 全局钩子函数 与 开发提示 **************/

export default Vue;

platformDirectives

import model from './model';
import show from './show';

export default {
  model,
  show
};
  • model实现:

    
      const directive = {
        inserted (el, binding, vnode, oldVnode) {
          // ...
        }
        componentUpdated (el, binding, vnode) {
          // ...
        }
      };
    
      export default directive;
    
  • show实现:

    export default {
      bind(el: any, { value }: VNodeDirective, vnode: VNodeWithData) {
        // ...
      },
    
      update(el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) {
        // ...
      },
    
      unbind(
        el: any,
        binding: VNodeDirective,
        vnode: VNodeWithData,
        oldVnode: VNodeWithData,
        isDestroy: boolean
      ) {
        // ...
      }
    };
    

platformComponents

import Transition from './transition';
import TransitionGroup from './transition-group';

export default {
  Transition,
  TransitionGroup
};
  • Transition实现:

    export const transitionProps = {
      name: String,
      appear: Boolean,
      css: Boolean,
      mode: String,
      type: String,
      enterClass: String,
      leaveClass: String,
      enterToClass: String,
      leaveToClass: String,
      enterActiveClass: String,
      leaveActiveClass: String,
      appearClass: String,
      appearActiveClass: String,
      appearToClass: String,
      duration: [Number, String, Object]
    };
    
    export default {
      name: 'transition',
      props: transitionProps,
      abstract: true,
    
      render(h: Function) {
        // ...
      }
    };
    
  • TransitionGroup实现:

    const props = extend(
      {
        tag: String,
        moveClass: String
      },
      transitionProps
    );
    
    export default {
      props,
    
      beforeMount() {
        // ...
      },
    
      render(h: Function) {
        // ...
      },
    
      updated() {
        // ...
      },
    
      methods: {
        hasMove(el: any, moveClass: string): boolean {
          // ...
        }
      }
    };
    

断点调试

综上所述该文件主要对 Vue.config 进行扩展、 对 Vue.options.directives/components 进行合并包装、添加公用的挂载方法 $mount、配置 devtools 全局钩子函数。

初始化前 - 重写 $mount 函数,给运行时版的 $mount 函数增加编译模板的能力

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 mount = Vue.prototype.$mount; // 缓存运行时版的 $mount 函数

// 重写 $mount 函数,给运行时版的 $mount 函数增加编译模板的能力
Vue.prototype.$mount = function(
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el); // 处理 挂载点

  // 过滤 body html
  if (el === document.body || el === document.documentElement /*html*/) {
    process.env.NODE_ENV !== 'production' &&
      warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      );
    return this;
  }

  /*************** 解析模板/el并转换为render函数 ***************/
  const options = this.$options;
  if (!options.render) {
    let template = options.template; // 获取合适的内容作为模板(template)
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // 把该字符串作为 css 选择符去选中对应的元素,并把该元素的 innerHTML 作为模板
          template = idToTemplate(template);
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(`模板元素未找到或为空: ${options.template}`, this);
          }
        }
      } else if (template.nodeType) {
        // 元素节点
        template = template.innerHTML;
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('无效的模板选项:' + template, this);
        }
        return this;
      }
    } else if (el) {
      template = getOuterHTML(el); // el 选项指定的挂载点将被作为组件模板
    }

    if (template) {
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile');
      }
      /*************** 将模板(template)字符串编译为渲染函数 ***************/
      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        },
        this
      );
      options.render = render;
      options.staticRenderFns = staticRenderFns;
      /*************** 将模板(template)字符串编译为渲染函数 ***************/

      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end');
        measure(`vue ${this._name} compile`, 'compile', 'compile end');
      }
    }
  }
  /*************** 解析模板/el并转换为render函数 ***************/

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

/**
 * 获取元素的outerHTML,并在IE中处理SVG元素。
 */
function getOuterHTML(el: Element): string {
  // IE9-11 中 SVG 标签元素是没有 innerHTML 和 outerHTML 这两个属性
  if (el.outerHTML) {
    return el.outerHTML;
  } else {
    const container = document.createElement('div');
    container.appendChild(el.cloneNode(true)); // 返回调用该方法的节点的一个副本(是否深度克隆)
    return container.innerHTML;
  }
}

/**
 * 根据 ID 获取或替换 HTML 元素的内容
 */
const idToTemplate = cached(id => {
  const el = query(id);
  return el && el.innerHTML;
});

Vue.compile = compileToFunctions;

export default Vue;

总结: 跟着程序执行过程看下来,整个初始化的过程就是对 Vue 构造函数的包装与丰富。

本部分内容旨在梳理初始化的全过程,对其中全局 API 及方法实现并未细化。


承接上文 - 「试着读读Vue源代码」工程目录及本地运行(断点调试)

承接下文 - 「试着读读 Vue 源代码」new Vue()发生了什么 ❓