Vue2源码学习笔记之构造函数的扩展

想要了解 Vue 的实例化过程,还是先了解下 Vue 的构造函数以及平台化包装吧。 我们知道 core/instance/index.js 是 Vue 的出生地文件,是一切代码最开始的地方。

目录
  1. 出生地文件
    1. initMixin
    2. stateMixin
    3. eventsMixin
    4. lifecycleMixin
    5. renderMixin
    6. 小结
  2. 核心代码入口
    1. initGlobalAPI
    2. 其他
  3. 参考资料

想要了解 Vue 的实例化过程,还是先了解下 Vue 的构造函数以及平台化包装吧。

出生地文件#

我们知道core/instance/index.js是 Vue 的出生地文件,是一切代码最开始的地方。

查看下它的代码。

// ! 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 is a constructor and should be called with the `new` keyword");
  }
  this._init(options); // ! 实例时,执行初始化方法,初始化所有的设置
}
 
// ! 新增方法和属性到 Vue 的原型对象中 (即 Vue 实例中可以使用到的方法)
initMixin(Vue); // ! 混入初始化方法 _init
stateMixin(Vue); // ! 混入 $data 和 $props 属性,$del $delete $$watch 状态相关方法
eventsMixin(Vue); // ! 混入 $on $once $off $emit 事件相关的方法
lifecycleMixin(Vue); // ! 混入 _update $forceUpdate $destroy 生命周期相关的方法
renderMixin(Vue); // ! 混入大量渲染相关的方法 包括 $nextTick _render 等等
 
export default Vue;

我们发现,Vue 的构造函数非常简单,除了一个非生产环境的警告,就只有一行代码。

this._init(options);

这小小的一个构造函数,是怎么变成一个功能非常强大的前端框架的呢?接下来会慢慢展开说明。

在我们使用new创建 Vue 的实例对象时,会执行构造函数里面的_init方法。但是这个_init方法是从哪里来的呢?

我们注意到在构造函数下面连续调用了五个 函数,这五个函数都在调用时都把 Vue 的构造函数作为参数传入,最后再导出这个构造函数。

下面分别看下这五个函数。

initMixin#

先看第一个函数initMixin的代码。

export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ...
  };
}

这个函数只做了一件事,就是往构造函数的原型对象上添加_init方法。这个方法也就是 Vue 构造函数中调用的this._init方法,在创建 Vue 的实例对象时会调用这个方法。

stateMixin#

再看第二个函数stateMixin的代码。

export function stateMixin(Vue: Class<Component>) {
  const dataDef = {};
  dataDef.get = function () {
    return this._data;
  };
  const propsDef = {};
  propsDef.get = function () {
    return this._props;
  };
  if (process.env.NODE_ENV !== "production") {
    dataDef.set = function () {
      warn(
        "Avoid replacing instance root $data. " +
          "Use nested data properties instead.",
        this
      );
    };
    propsDef.set = function () {
      warn(`$props is readonly.`, this);
    };
  }
  // ! 定义 $data 和 $props 的 getter (响应式只读属性)
  Object.defineProperty(Vue.prototype, "$data", dataDef);
  Object.defineProperty(Vue.prototype, "$props", propsDef);
  // ..
}

先看上面代码的最后两行,在构造函数的原型对象上定义了两个属性$data$props,使用过 Vue 的同学应该很熟悉这两个属性吧,它们分别用来保存dataprops数据,这里使用Object.defineProperty来定义。

再看下面的代码。

// ! 新增实例方法 $set $delete
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
 
// ! 新增实例方法 $watch => 创建 watcher
Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // ...
};

这里又往构造函数的原型对象上增加了$set$delete$watch三个方法,暂时也不管方法的具体代码。

小结:stateMixin函数给Vue的原型对象上增加了两个属性$data$props,以及三个方法$set$delete$watch

eventsMixin#

再看第三个函数eventsMixin的代码。

export function eventsMixin(Vue: Class<Component>) {
  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 {
    /* ... */
  };
}

eventsMixin函数给构造函数的原型对象上增加了四个方法$on$once$once$emit,这些方法和 Vue 的事件处理相关。

lifecycleMixin#

再看第四个函数lifecycleMixin的代码。

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

lifecycleMixin函数给构造函数的原型对象上增加了三个方法_update$forceUpdate$destroy,这些方法和 Vue 实例对象的生命周期相关。

renderMixin#

再看最后一个函数renderMixin的代码。

export function renderMixin(Vue: Class<Component>) {
  installRenderHelpers(Vue.prototype);
  Vue.prototype.$nextTick = function (fn: Function) {
    /* ... */
  };
  Vue.prototype._render = function (): VNode {
    /* ... */
  };
}

首先使用installRenderHelpers(Vue.prototype)给构造函数的原型对象上增加了一系列渲染工具函数。

渲染工具函数installRenderHelpers的代码如下,这些函数主要用来渲染模板字符串。

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; // ! 创建文本 VNode
  target._e = createEmptyVNode; // ! 创建空的 VNode
  target._u = resolveScopedSlots;
  target._g = bindObjectListeners;
  target._d = bindDynamicKeys;
  target._p = prependModifier;
}

然后又在原型对象上增加了两个方法$nextTick_render$nextTick是异步执行方法,而_render是生成虚拟节点的方法。

小结#

Vue 的构造函数代码虽然非常简单,但是在使用了上面的五个函数给它的原型对象上添加了大量的属性和方法之后,它的功能就变得越来越强大。后面还会继续扩展 Vue 构造函数的功能。

因为是在 Vue 的构造函数的原型对象上添加的属性和方法,所以通过new生成的 Vue 的实例对象也会继承这些属性和方法。

核心代码入口#

在出生地文件中是为 Vue 的构造函数的原型对象添加属性和方法的,即添加为 Vue 的实例对象添加属性和方法。

现在回到上一级核心代码入口 core 的index.js文件中,这里却是直接在 Vue 的构造函数添加静态属性和方法,也就是 Vue 的全局属性和方法。

查看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);
 
// ! 新增属性 $isServer
Object.defineProperty(Vue.prototype, "$isServer", {
  get: isServerRendering,
});
 
// ! 新增属性 $ssrContext
Object.defineProperty(Vue.prototype, "$ssrContext", {
  get() {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext;
  },
});
 
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, "FunctionalRenderContext", {
  value: FunctionalRenderContext,
});
 
Vue.version = "__VERSION__";
 
export default Vue;

这是 Vue 源码中的核心代码的入口文件。

首先从实例文件夹中引入出生地文件中的 Vue 的构造函数,然后再次对这个函数进行扩展,不过这次扩展的是函数的静态属性和方法。

initGlobalAPI#

首先是调用initGlobalAPI函数,并把 Vue 的构造函数作为参数传入。通过函数的名称我们大概可以知道,这是初始化全局 API 的方法。

下面看下initGlobalAPI函数的代码。

export function initGlobalAPI(Vue: GlobalAPI) {
  // config
  const configDef = {};
  configDef.get = () => config;
  if (process.env.NODE_ENV !== "production") {
    configDef.set = () => {
      warn(
        "Do not replace the Vue.config object, set individual fields instead."
      );
    };
  }
  Object.defineProperty(Vue, "config", configDef); // ! 新增属性 config
  // ...
}

首先在 Vue 的构造函数上添加全局属性config,使用Object.defineProperty方法来定义。

这个属性是从哪里来的呢?

import config from "../config";

是从核心代码的配置文件config.js中引入的。

继续看下面的代码。

// ! 添加工具方法 util 不稳定
Vue.util = {
  warn,
  extend,
  mergeOptions,
  defineReactive,
};

在 Vue 的构造函数中添加了一个util属性,这个属性里面包含四个方法,它们来源于core/util/index.js文件。

继续看下面的代码。

// ! 新增方法 set delete nextTick
Vue.set = set;
Vue.delete = del;
Vue.nextTick = nextTick;

又在 Vue 的构造函数中添加了三个全局方法:setdeletenextTick。是不是觉得有点眼熟?

没错,这三个全局方法和原型对象上添加的方法$set$delete$nextTick是一样的。

继续看下面的代码。

// ! 新增方法 observable
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
  observe(obj);
  return obj;
};
 
// ! 新增属性 options, 初始值是没有原型对象的空对象
Vue.options = Object.create(null);

这里新增observable全局方法,是 Vue 2.6+ 版本新增加的方法。同时,初始化选项options为空对象。

继续看最后的代码。

// ! 设置 Vue.options.components  Vue.options.directives Vue.options.filters 为空对象
ASSET_TYPES.forEach((type) => {
  Vue.options[type + "s"] = Object.create(null);
});
 
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
Vue.options._base = Vue; // ! 新增属性 _base -> Vue 构造函数
 
// ! 添加内置组件 <keep-alive/>
extend(Vue.options.components, builtInComponents);
 
initUse(Vue); // ! 新增方法 Vue.use
initMixin(Vue); // ! 新增方法  Vue.mixin
initExtend(Vue); // ! 新增方法  Vue.extend
initAssetRegisters(Vue); // ! 新增方法 Vue.component Vue.directive Vue.filter

其中,ASSET_TYPES是一个字符串数组。

export const ASSET_TYPES = ["component", "directive", "filter"];

先设置options中的componentsdirectivesfilters为空对象。

然后设置options中的_base为 Vue 的构造函数Vue,即它本身。

再往components中添加内置组件,目前的内置组件只有<keep-alive />,这个组件是用来缓存组件的。

最后调用四个函数,把 Vue 的构造函数作为参数传入,继续扩展构造函数。

下面一起来看看这四个函数吧。

initUse

先看第一个函数initUse的代码。

export function initUse(Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    /* ... */
  };
}

initUse给构造函数Vue添加全局方法use,这个方法是用来安装插件的。

initMixin

再看第二个函数initMixin的代码。

export function initMixin(Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin); // ! 合并 mixin 的配置
    return this;
  };
}

initMixin给构造函数Vue添加全局方法mixin,这个方法是用来复用组件的配置的。

initExtend

再看第三个函数initExtend的代码。

export function initExtend(Vue: GlobalAPI) {
  Vue.extend = function (extendOptions: Object): Function {
    // ...
  };
}

initExtend给构造函数Vue添加全局方法extend,这个方法用来生成 Vue 的子类,即含有选项的构造函数。

initAssetRegisters

最后看第四个函数initAssetRegisters的代码。

export function initAssetRegisters(Vue: GlobalAPI) {
  ASSET_TYPES.forEach((type) => {
    // ...
    if (type === "component" && isPlainObject(definition)) {
      /* ... */
    }
    if (type === "directive" && typeof definition === "function") {
      /* ... */
    }
  });
}

initAssetRegisters给构造函数Vue添加三个全局方法componentdirectivefilter,这三个都是管理资源型文件的方法,分别用来注册组件、指令和筛选器。

其他#

initGlobalAPI学习完成后,再次回到core/index.js文件中,继续看后面的代码。

// ! 新增属性 $isServer
Object.defineProperty(Vue.prototype, "$isServer", {
  get: isServerRendering,
});
 
// ! 新增属性 $ssrContext
Object.defineProperty(Vue.prototype, "$ssrContext", {
  get() {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext;
  },
});
 
// ! 新增属性 $FunctionalRenderContext
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, "FunctionalRenderContext", {
  value: FunctionalRenderContext,
});
 
Vue.version = "__VERSION__";
 
export default Vue;

首先,给 Vue 的构造函数的原型对象添加了两个属性$isServer$ssrContext,这两个属性是和服务端渲染相关的,暂时不管它们。

然后给 Vue 的构造函数添加了一个全局属性 的FunctionalRenderContext,这个属性也和服务端渲染有关。

接下来再给构造函数Vue本身添加了一个全局属性version,它是一个字符串类型的常量__VERSION__

最后再次把 Vue 的构造函数导出,现在 Vue 的构造函数和它的原型对象上都已经挂满了各种属性和方法了。

这些就是 Vue 的构造函数的扩展,一个简单的构造函数Vue,在上面扩展各种各样属性和方法后,功能变得越来越强大。虽然现在还有详细分析其中代码具体的逻辑,但是却能让我们更加清楚的了解vue的结构。

另外再次说明下,core 文件下的代码是 Vue 源码中最核心的代码,这里的代码在每个平台都会用到。

那么,平台上的代码会有哪些不同呢?

参考资料#