浅曦Vue源码-2-vue.js 执行说明

508 阅读5分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

一、背景

这一部分我考虑过很久,觉得还是要加上这一部分,这一部分是必修的,考虑到不是人人都像我这种摸鱼专家,有时间学习源码,直接就讲源码大家会蒙圈的,这里先按照js的一个执行顺序,先把这个代码的执行顺序捋一捋,这样方便后面理解,这部分属于磨刀不费砍柴工。

二、test.html 中的玄机

在前文,浅羲Vue源码-准备 中最后有一个 test.html 的文件,注意看它的 script 标签顺序:

<script src="./dist/vue.js"></script>
<script>
  debugger
  const sub = {
    template: `
      <div style="color: red;background: #5cb85c;display: inline-block">{{ someKey + foo }}</div>`,
    props: {
      someKey: {
        type: String,
        default: 'hhhhhhh'
      }
    },
  inject: ['foo']
  };

  new Vue({
    el: '#app',
    data: {
      msg: 'hello vue'
    },
  hahaha: 'hahahahahha',
  provide: {
      foo: 'bar'
  },
  components: {
      someCom: sub
  }
  })
</script>

2.1 加载并执行 vue.js

这一点大家都清楚,浏览器解析 HTML 的时候,遇到 script 标签会等 script 加载 src 对应的资源,而这里 src 对应的是一个 js 文件,加载完还不要紧,要紧的是浏览器检测到这个资源是 js 的时候,就会执行它,注意是执行它,不执行他就是一段文本,只有执行了才是变量、函数...

接着我们看看 dist/vue.js 的大致结构:

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = global || self, global.Vue = factory());
}(this, (function () { 'use strict';

  var emptyObject = Object.freeze({});

  // These helpers produce better VM code in JS engines due to their
  // explicitness and function inlining.
  function isUndef (v) {
    return v === undefined || v === null
  }
  
  // ....
  function Vue (options) {
    if (
      !(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
   }

  initMixin(Vue);
  stateMixin(Vue);
  // .....
  
  Vue.compile = compileToFunctions;

  return Vue;
})));

2.2 Vue 这个变量的注册

从上面的 vue.js 的代码结构可以看得出来,首先是个自执行函数(IIFE),这个自执行函数定义了两个形参:globalfactory,而后自执行和函数接收了两个实参,第一个是 this,另一个是个函数,很显然 global 对应 thisfactory 对应这个匿名函数;

然后自执行函数中判断 exportsdefine 这两个对象,分别以 CMDAMD 的方式注册 Vue,当然这里是不有 exportsdefine 的,自然就是最后这一段:

(global = global || self, global.Vue = factory());

global.Vue = factory(),那么 global 是谁呢?global 是 前面的 this 啊,this 是谁,this 在全局作用域中就是 window 对象,factory 就是这个下面匿名函数,下称工厂函数:

 (function () { 'use strict';

  var emptyObject = Object.freeze({});
  // ....
  function Vue (options) {
    if (
      !(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
   }
 // ....
  return Vue;
}))

所以 global.Vue = factory() 的执行后等效于 window.Vue = funciton Vue() {}; 如此一来,就把 Vuewindow 对象属性的形式注册到全局作用域了。

三、关于工厂函数中 initGlobalAPI() 细节

在工厂函数从上到下执行的时候,除了注册 Vue 这个类,还完成了一些 Vue 的全局配置,其中一部分是在 initGlobalAPI 这个方法中完成的。

这部分其实也是 Vue 的面向对象设计的妙处,这里不表架构设计,但是得先有个印象,这些全局的配置是要复用的,我们所熟知的 Vue全局组件(keep-alive)全局指令全局过滤器 都是在这里初始化的。

而后面所谓的复用,则是创建组件时通过创建 Vue 的子类,而创建子类时是要继承 Vue 的这部分全局配置,以实现全局的能力,有没有人清楚这个设计模式呢,请评论区留意言吖~

// vue factory 函数片段
function initGlobalAPI (Vue) {
   // details of initGlobalAPI
}

initGlobalAPI(Vue);

3.1 Vue.options

这里有一个 Vue.options 是在 initGlobalAPI 初始化的,这个 options 就是 Vue 全局 options,其中包含了全局的components、directives、filters,这个 Vue.options 在后面经常以 Ctor.options 的形式出现,这个先提一下,具体初始化过程如下:

// vue factory 函数片段
function initGlobalAPI (Vue) {
    // ASSET_TYPES = [
    //  'component',
    //  'directive',
    //  'filter'
    // ];

    // builtInComponents = {
    //   KeepAlive: KeepAlive
    // };

    Vue.options = Object.create(null);
    ASSET_TYPES.forEach(function (type) {
      Vue.options[type + 's'] = Object.create(null);
    });

    extend(Vue.options.components, builtInComponents);
   
    Vue.options._base = Vue; 
}

initGlobalAPI(Vue);

经过上面过程,Vue.options 初始化完成应该是这样子的了:

Vue.options = {
  components: {
    keepAlive: keepAlive,
  },
  directives: {},
  filters: {},
  _base: Vue
}

_base, 这个后面也常用到,Ctor.options._base 其实就是在说 Vue 了。

3.2 部分全局方法注册

// vue factory 函数片段
function initGlobalAPI (Vue) {

    Vue.set = set;
    Vue.delete = del;
    Vue.nextTick = nextTick;

    // 2.6 explicit observable API
    Vue.observable = function (obj) {
      observe(obj);
      return obj
    };

    // Vue.options ....

    initUse(Vue); // 注册 Vue.use 方法
    initMixin$1(Vue); // 注册 Vue.mixin 方法
    initExtend(Vue); // 注册 Vue.extend 方法
    initAssetRegisters(Vue); // 注册 Vue.component / Vue.directive / Vue.filter 方法
}

initGlobalAPI(Vue);

经过上面的这一系列操作,有了 Vue.options(下文中常以 Ctor.options 或 vm.constructor.options 出现,你发现我已经提醒好几次,说明这个真的很重要),有了 Vue.component(注册全局组件) / Vue.directive(注册全局指令) / Vue.filter(全局过滤器) 方法

四、总结

以下有需要注意的点,也有谨记的内容

4.1 你仍然没开始看源码

我们现在还在看 vue.js 这个经过 rollup 编译输出的产物,注意这个不是源码。为什么说这个,因为这个vue.js 只有一份,代码从上到下执行很符合预期,而源码是 ESModule,各种 import / export,很多内容不在一个模块,这就给源码阅读增加了不少麻烦。

我在这里,提前交代一些全局的内容,比如 Vue.options 马上就要用到了,后面 new Vue(options) 的时候,会各种 mergeOptions,你会感到迷惑,为啥要 mergeOptions?谁又和谁 merge 了?因为你不知道 options是个什么东西。

另外,请谨记当我们的代码:new Vue() 执行的时候,vue.js 中的所有内容都已经执行过了,我们才会有 Vue 这个类。

4.2 梳理

  1. test.html 中的 vue.jsscript 标签要写在我们 demo 代码的前面,确保其先加载和执行;
  2. 在我们的 demo 中,Vue 通过 window.Vue 的方式挂载在全局作用域;
  3. 在工厂函数中有个 initGlobalAPI 会初始化 Vue.options 和一些全局的方法;