Vue 是什么

2022/11/4 Vue

上面一篇看了 Vue full-dev 环境的入口是 entry-runtime-with-compiler.ts, 这篇我们就从这个入口来看一下,Vue 是什么。

# entry-runtime-with-compiler

// entry-runtime-with-compiler.ts
import Vue from './runtime-with-compiler';
import * as vca from 'v3';
import { extend } from 'shared/util';

extend(Vue, vca);

import { effect } from 'v3/reactivity/effect';
Vue.effect = effect;

export default Vue;

这里可以看到,Vue 是从 runtime-with-compiler.ts 里引入的,引入了 v3 目录下的所有内容,以及引入了v3 里的 effect,通过 extend 方法处理了 Vue 和 v3,我们这里研究一下 extend 和 effect, 看看这两步是 在做什么。

// 将属性混合到目标对象中。
export function extend(
  to: Record<PropertyKey, any>,
  _from?: Record<PropertyKey, any>,
): Record<PropertyKey, any> {
  for (const key in _from) {
    to[key] = _from[key];
  }
  return to;
}

我们都知道 Record 这个高级类型,他会将一个类型的所有属性值都映射到另一个类型上并创造一个新的类型。

extend 方法就是遍历第二个参数的所有属性,把它赋值到第一个参数上,然后返回第一个参数

而 effect 则是一个内部测试的方法,没有在 Vue2 中暴露出来

/**
 * @internal Vue2 中没有暴露该方法,仅作为内部测试
 */
export function effect(fn: () => any, scheduler?: (cb: any) => void) {
  const watcher = new Watcher(currentInstance, fn, noop, {
    sync: true,
  });
  if (scheduler) {
    watcher.update = () => {
      scheduler(() => watcher.run());
    };
  }
}

上面的的代码可以看出来,在entry-runtime-with-compiler.ts文件中,只是引入了 Vue,并把 v3 的属性赋值给了 Vue,并返回了 Vue。所以我们要探寻 Vue,还需要继续往下找

# runtime-with-compiler

在上面,我们看到 Vue 是从 runtime-with-compiler.ts里引入的,我们来看看这个文件

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';
import type { Component } from 'types/component';
import type { GlobalAPI } from 'types/global-api';

// 这样多次调用idToTemplate(template),不会多次执行query方法
const idToTemplate = cached((id) => {
  const el = query(id);
  return el && el.innerHTML;
});

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
  // query方法,实际上是对el参数做了一个转化,el可能是string 或者 element。如果是string,将返回document.querySelector(el)
  el = el && query(el);

  // 测试环境添加提醒 Vue 不能添加在 body 和 html 上
  if (el === document.body || el === document.documentElement) {
    __DEV__ && warn(`Do not mount Vue to <html> or <body> - mount to normal elements instead.`);
    return this;
  }

  const options = this.$options;
  // 解析模板或者 el,并转换为 render 函数
  if (!options.render) {
    // render函数不存在
    let template = options.template;
    if (template) {
      // 如果存在template配置项:
      // template可能是"#xx",那么根据id获取element内容
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template);
          /* istanbul ignore if */
          if (__DEV__ && !template) {
            warn(`Template element not found or is empty: ${options.template}`, this);
          }
        }
      } else if (template.nodeType) {
        // 如果template存在nodeType,那么获取template.innerHTML 内容
        template = template.innerHTML;
      } else {
        // 如果存在template,但是不存在相关属性,就报错提醒
        if (__DEV__) {
          warn('invalid template option:' + template, this);
        }
        return this;
      }
    } else if (el) {
      // 如果 template 配置项不存在 template,但是存在 el
      // 那么根据 el 获取对应的 element 内容
      // @ts-expect-error
      template = getOuterHTML(el);
    }

    // 拿到了 template,调用 compileToFunctions 方法,返回 render 方法
    if (template) {
      /* istanbul ignore if */
      if (__DEV__ && config.performance && mark) {
        mark('compile');
      }

      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          outputSourceRange: __DEV__,
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments,
        },
        this,
      );
      options.render = render;
      options.staticRenderFns = staticRenderFns;

      /* istanbul ignore if */
      if (__DEV__ && config.performance && mark) {
        mark('compile end');
        measure(`vue ${this._name} compile`, 'compile', 'compile end');
      }
    }
  }
  // 调用 mount 方法,并执行 mount 的逻辑
  return mount.call(this, el, hydrating);
};

/**
 * 获取元素的outerHtml
 */
function getOuterHTML(el: Element): string {
  // 如果元素的outerHTML存在
  if (el.outerHTML) {
    // 返回该元素及本身的所有HTML代码
    return el.outerHTML;
  } else {
    // 否则 先创建一个 div
    const container = document.createElement('div');
    // 向 div 中添加这个el
    container.appendChild(el.cloneNode(true));
    // 返回container的innerHTML就是该元素即本身的HTML代码
    return container.innerHTML;
  }
}

Vue.compile = compileToFunctions;

export default Vue as GlobalAPI;

这个文件,主要是把 Vue 引入进来,然后针对 +complier 的情况,做一些 mount 的处理,Vue 只认 识render函数,所以如果有render函数,那么就直接调用mount.call。反之,vue 会将template做为参数,运行时调用compileToFunctions方法,转化为render函数,再去调用mount.call方法。

mount.call方法,调用的是runtime/index.ts里的$mount 方法,而 Vue 也是从 runtime/index.ts 引入的,我们也来看一下这个文件

# runtime/index.ts

一层一层剥洋葱,剥到这一层了,我们在来看看这个文件都是做了啥

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 } from 'core/util/index';

import {
  query,
  mustUseProp, // 标记dom元素必须与哪些属性对应
  isReservedTag, // 标记Vue的预留标签(html标签+svg标签)
  isReservedAttr, // 标记Vue标签的预留属性
  getTagNamespace, // 标记标签的命名空间
  isUnknownElement, // 标记哪些tag为未知元素
} from 'web/util/index';

import { patch } from './patch';
import platformDirectives from './directives/index';
import platformComponents from './components/index';
import type { Component } from 'types/component';

// 安装平台特定的工具
Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;

// 安装平台运行时的指令和组件
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);

// 安装浏览器端的 patch
Vue.prototype.__patch__ = inBrowser ? patch : noop;

// 公用的 $mount 方法
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
  el = el && inBrowser ? query(el) : undefined;
  // 执行 mountComponent 方法
  return mountComponent(this, el, hydrating);
};

// devtools 全局钩子
/* istanbul ignore next */
if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      if (devtools) {
        devtools.emit('init', Vue);
      } else if (__DEV__ && process.env.NODE_ENV !== 'test') {
        // @ts-expect-error
        console[console.info ? 'info' : 'log'](
          'Download the Vue Devtools extension for a better development experience:\n' +
            'https://github.com/vuejs/vue-devtools',
        );
      }
    }
    if (
      __DEV__ &&
      process.env.NODE_ENV !== 'test' &&
      config.productionTip !== false &&
      typeof console !== 'undefined'
    ) {
      // @ts-expect-error
      console[console.info ? 'info' : 'log'](
        `You are running Vue in development mode.\n` +
          `Make sure to turn on production mode when deploying for production.\n` +
          `See more tips at https://vuejs.org/guide/deployment.html`,
      );
    }
  }, 0);
}

export default Vue;

这个文件主要给 Vue 添加了一些扩展,比如新增了一些工具,添加了一些运行时的指令和组件,还有给 Vue 的原型上添加了一个 $mount 方法,就是上文中调用的方法

但是这个文件也是引入的 Vue,我们想要知道 Vue 是啥,还得继续剥洋葱

# core/index.ts

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';
import { version } from 'v3';

// 初始化全局 API
initGlobalAPI(Vue);

// 给 Vue 的原型上添加 '$isServer' 属性
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering,
});

// 给 Vue 的原型上添加 '$ssrContext' 属性
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get() {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext;
  },
});

// 公开 FunctionalRenderContext 用于 ssr 运行时 helper 的安装
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext,
});

Vue.version = version;

export default Vue;

在该文件中,引入了 Vue,然后初始化了全局 API,添加了服务端渲染的一些属性。

继续剥洋葱,去 core/instance/index.ts 里继续寻找~

# core/instance/index.ts

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';
import type { GlobalAPI } from 'types/global-api';

// Vue 是一个函数类,只能通过 new 关键字来初始化
function Vue(options) {
  // 开发环境 不通过 new 初始化 给出报错提示
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  // 初始化options
  this._init(options);
}

//@ts-expect-error Vue has function type
initMixin(Vue); // 给 Vue 构造函数的 prototype 里添加 _init 方法
//@ts-expect-error Vue has function type
stateMixin(Vue); // 给 Vue 构造函数的 prototype 里添加一些数据实例方法
//@ts-expect-error Vue has function type
eventsMixin(Vue); // 给 Vue 构造函数的 prototype 里添加一些事件实例方法
//@ts-expect-error Vue has function type
lifecycleMixin(Vue); // 给 Vue 构造函数的 prototype 里添加生命周期实例方法
//@ts-expect-error Vue has function type
renderMixin(Vue); // 给 Vue 构造函数的 prototype 里添加渲染的实例方法

export default Vue as unknown as GlobalAPI;

哇,终于剥开了这洋葱的外皮了,找到内心了。

Vue 是一个用函数类,我们只能通过 new Vue 去初始化它。

我们知道了 Vue 是啥了,下一步就可以从 Vue 初始化来继续看代码了