Vue构造函数.md 7.1 KB

Vue 构造函数

了解 Vue 这个项目 一节中,我们在最后提到这套文章将会以 npm run dev 为切入点:

"dev": "rollup -w -c build/config.js --environment TARGET:web-full-dev",

当我们执行 npm run dev 时,根据 build/config.js 文件中的配置:

// Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  }

可知,入口文件为 web/entry-runtime-with-compiler.js,最终输出 dist/vue.js,它是一个 umd 模块,接下来我们就以入口文件为起点,找到 Vue 构造函数并将 Vue 构造函数的真面目扒的一清二楚。

但现在有一个问题 web/entry-runtime-with-compiler.js 中这个 web 指的是哪一个目录?这其实是一个别名配置,打开 build/alias.js 文件:

const path = require('path')

module.exports = {
  vue: path.resolve(__dirname, '../src/platforms/web/entry-runtime-with-compiler'),
  compiler: path.resolve(__dirname, '../src/compiler'),
  core: path.resolve(__dirname, '../src/core'),
  shared: path.resolve(__dirname, '../src/shared'),
  web: path.resolve(__dirname, '../src/platforms/web'),
  weex: path.resolve(__dirname, '../src/platforms/weex'),
  server: path.resolve(__dirname, '../src/server'),
  entries: path.resolve(__dirname, '../src/entries'),
  sfc: path.resolve(__dirname, '../src/sfc')
}

其中有这么一句:

web: path.resolve(__dirname, '../src/platforms/web')

所以 web 指向的应该是 src/platforms/web,除了 web 之外,alias.js 文件中还配置了其他的别名,大家在找对应目录的时候,可以来这里查阅,后面就不做这种目录寻找的说明了。

接下来我们就进入正题,打开 src/platforms/web/entry-runtime-with-compiler.js 文件,你可以看到这样一句话:

import Vue from './runtime/index'

这说明:这个文件并不是 Vue 构造函数的“出生地”,这个文件中的 Vue 是从 ./runtime/index 导入进来的,于是我们就打开当前目录的 runtime 目录下的 index.js 看一下,你同样能够发现这样一句话:

import Vue from 'core/index'

同样的道理,这说明 runtime/index.js 文件也不是 Vue 的“出生地”,你应该继续顺藤摸瓜打开 core/index.js 文件,在 build/alias.js 的配置中,core 指向的是 src/core,打开 src/core/index.js 你能看到这样一句:

import Vue from './instance/index'

按照之前的逻辑,继续打开 ./instance/index.js 文件:

// 从五个文件导入五个方法(不包括 warn)
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 作为参数传递给导入的五个方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

// 导出 Vue
export default Vue

可以看到,这个文件才是 Vue 构造函数真正的“出生地”,上面的代码是 ./instance/index.js 文件中全部的代码,还是比较简短易看的,首先分别从 ./init.js./state.js./render.js./events.js./lifecycle.js 这五个文件中导出五个方法,分别是:initMixinstateMixinrenderMixineventsMixin 以及 lifecycleMixin,然后定义了 Vue 构造函数,其中使用了安全模式来提醒你要使用 new 操作符来调用 Vue,接着将 Vue 构造函数作为参数,分别传递给了导入进来的这五个方法,最后导出 Vue

那么这五个方法又做了什么呢?以 initMixin 为例,打开 ./init.js 文件,找到 initMixin 方法,如下:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ... _init 方法的函数体,此处省略
  }
}

这个方法的作用就是在 Vue 的原型上添加了 _init 方法,这个 _init 方法看上去应该是内部初始化的一个方法,其实在 instance/index.js 文件中我们是见过这个方法的,如下:

// 定义 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 的构造函数里有这么一句:this._init(options),这说明,当我们执行 new Vue() 的时候,this._init(options) 将被执行。

再打开 ./state.js 文件,找到 stateMixin 方法,这个方法的一开始,是这样一段代码:

  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 (newData: Object) {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

我们先看最后两句,使用 Object.definePropertyVue.prototype 上定义了两个属性,就是大家熟悉的:$data$props,这两个属性的定义分别写在了 dataDef 以及 propsDef 这两个对象里,也就是这两句代码上面的代码,首先是 get

const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }

可以看到,$data 属性实际上代理的是 _data 这个实例属性,而 $props 代理的是 _props 这个实例属性。然后有一个是否为生产环境的判断,如果不是生产环境的话,就为 $data$props 这两个属性设置一下 set,实际上就是提示你一下:别他娘的想修改我,老子无敌。

也就是说,$data$props 是两个只读的属性,所以,现在让你使用 js 实现一个只读的属性,你应该知道要怎么做了。

接下来 stateMixin 又在 Vue.prototype 上定义了三个方法:

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
  	// ...
  }

这三个方法分别是:$set$delete 以及 $watch,实际上这些东西你都见过的,在这里: