Vue源码解析一——骨架梳理

大家都知道,阅读源码可以帮助自己成长。源码解析的文章也看了不少,但是好记性不如烂笔头,看过的东西过段时间就忘的差不多了,所以还是决定自己动手记一记。

首先看下项目目录,大致知道每个文件夹下面都是干什么的


Vue源码解析一——骨架梳理_第1张图片
Vue.png

当我们阅读一个项目源码的时候,首先看它的package.json文件,这里包含了项目的依赖、执行脚本等,可以帮助我们快速找到项目的入口。

我们来看几个重要字段:

// main和module指定了加载的入口文件,它们都指向运行时版的Vue,
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",

当打包工具遇到该模块时:

  1. 如果已经支持pkg.module字段,会优先使用es6模块规范的版本,这样可以启用tree shaking机制
  2. 否则根据main字段的配置加载,使用已经编译成CommonJS规范的版本。

webpack2+和rollup都已经支持pkg.module, 会根据module字段的配置进行加载

接下来看一下scripts里面部分脚本配置:

"scripts": {
  // 构建完整版umd模块的Vue
  "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
  // 构建运行时cjs模块的Vue
  "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
  // 构建运行时es模块的Vue
  "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
  // 构建web-server-renderer包
  "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
  "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
  "build": "node scripts/build.js",
  "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer"
},

umd让我们可以直接用script标签来引用Vue

cjs形式的模块是为browserify 和 webpack 1 提供的,他们在加载模块的时候不能直接加载ES Module

webpack2+ 以及 Rollup可以直接加载ES Module,es形式的模块是为它们服务的

接下来,我们将基于dev脚本进行分析

当我们执行npm run dev命令时,

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

可以看到配置文件是scripts/config.js,传给配置文件的TARGET变量的值是‘web-full-dev’。

在配置文件的最后,是这样一段代码:

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

因为process.env.TARGET有值,所以执行的是if里面的代码。根据process.env.TARGET === 'web-full-dev', 我们看到这样一段配置:

// Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'), // 入口文件
    dest: resolve('dist/vue.js'), // 最终输出文件
    format: 'umd', // umd模块
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

现在我们知道了入口文件是'web/entry-runtime-with-compiler.js',但是web是指的哪一个目录呢?在scripts下面有一个alias.js文件,里面定义了一些别名:

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

可以看到web是指的'src/platforms/web',所以入口文件的全路径就是src/platforms/web/entry-runtime-with-compiler.js

我们使用Vue的时候,是用new关键字进行调用的,这说明Vue是一个构造函数,接下来我们就从入口文件开始扒一扒Vue构造函数是咋个情况。

寻找Vue构造函数的位置

打开入口文件src/platforms/web/entry-runtime-with-compiler.js,我们看到这样一句代码

import Vue from './runtime/index'

这说明Vue是从别的文件引进来的,接着打开./runtime/index文件,看到

import Vue from 'core/index'

说明这里也不是Vue的出生地,接着寻找。打开core/index,根据别名配置可以知道,core是指的'src/core'目录。Vue依然是引入的

import Vue from './instance/index'

没办法,接着找。在./instance/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构造函数终于找到源头了。最后我们再理一下这个路径

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

——> src/platforms/web/runtime/index.js

——> src/core/index.js

——> src/core/instance/index.js

接下来我们从出生地开始一一来看

Vue构造函数——实例属性和方法

来看一下src/core/instance/index.js文件中的全部代码:

/**
 * 在原型上添加了各种属性和方法
 */
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)
}

initMixin(Vue)
// 在Vue的原型上添加了_init方法。在执行new Vue()的时候,this._init(options)被执行
stateMixin(Vue)
// 在vue的原型上定义了属性: $data、$props,方法:$set、$delete、$watch
eventsMixin(Vue)
// 在原型上添加了四个方法: $on $once $off $emit
lifecycleMixin(Vue)
// 在Vue.prototye上添加了三个方法:_update $forceUpdate $destory
renderMixin(Vue)
// 在原型上添加了方法:$nextTick _render _o _n _s _l _t _q _i _m _f _k _b _v _e _u _g _d _p

export default Vue

该文件主要是定义了Vue构造函数,然后又以Vue为参数,执行了initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin这五个方法。

Vue构造函数首先检查了是不是用new关键字调用的,然后调用了_init方法。

接下来五个方法分别在Vue的原型上添加了各种属性和方法。首先来看initMixin

initMixin

打开'./init'文件,找到initMixin方法,发现它其实只做了一件事:

export function initMixin (Vue: Class) {
  Vue.prototype._init = function (options?: Object) {
    ...
  }
}

就是在Vue.prototype上挂载了_init方法,在执行new Vue()的时候,该方法会执行。

stateMixin

export function stateMixin (Vue: Class) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  if (process.env.NODE_ENV !== 'production') { // 不是生产环境,设置set
    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是只读属性
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

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

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

这个方法首先在Vue.prototype上定义了两个只读属性$data$props。为什么是只读属性呢?因为为属性设置set的时候有一个判断,不能是生产环境。

然后在原型上定义了三个方法:$set, $delete, $watch

eventsMixin

export function eventsMixin (Vue: Class) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array, fn: Function): Component {}

  Vue.prototype.$once = function (event: string, fn: Function): Component {}

  Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {}

  Vue.prototype.$emit = function (event: string): Component {}
}

这里面是在原型上挂载了四个方法,这几个方法平时也都经常用到,肯定很熟悉

lifecycleMixin

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

  Vue.prototype.$forceUpdate = function () {}

  Vue.prototype.$destroy = function () {}
}

添加了三个生命周期相关的实例方法:

  • _update:
  • $forceUpdate: 迫使Vue实例重新渲染,包括其下的子组件
  • $destory: 完全销毁一个实例, 触发生命周期beforeDestroy和destroyed

renderMixin

export function renderMixin (Vue: Class) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

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

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

首先是以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
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

也是在原型上挂载了各种方法, 用于构造render函数。

之后又在原型上挂载了两个实例方法$nextTick_render

至此我们大致了解了instance/index.js里面的内容,就是包装了Vue.prototyp,在其上挂载了各种属性和方法。

Vue构造函数——挂载全局API

接下来来看/src/core/index文件

/** 
* 添加全局API,在原型上添加了两个属性$isServer和$ssrContext,加了version版本属性
*/
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'

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

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

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的版本号
Vue.version = '__VERSION__'

export default Vue

首先是导入了构造函数Vue和其他三个变量,接下来就是以Vue构造函数为参数调用了initGlobalAPI方法,该方法来自./global-api/index。我们先把下面的内容看完再回过头来分析该方法。

接下来是在Vue.prototype上面挂载了两个只读属性$isServer$ssrContext。之后又在Vue构造函数上添加了FunctionalRenderContext属性,根据注释知道该属性是在ssr中用到的。

最后在Vue构造函数上添加了静态属性version,其值是__VERSION__,这是个什么鬼?打开/scripts/config.js,可以看到这么一句代码:

__VERSION__: version

而version的值在文件最上面可以看到:

process.env.VERSION || require('../package.json').version

所以最终的值就是Vue的版本。

我们再回过头来看一下initGlobalAPI函数,从函数名可以猜出它应该是定义全局API的,其实也就是这样。

先看前部分代码

// 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) // 只读属性

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  // 上面意思就是轻易不要用,有风险
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

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

  // 2.6 explicit observable API
  Vue.observable = (obj: T): T => {
    observe(obj)
    return obj
  }

先是定义了只读属性config。接着定义了util属性,并且在util上挂载了四个方法。只不过util以及它下面的方法不被视为公共API的一部分,要避免使用,除非你可以控制风险。

接着就是在Vue上添加了四个属性:set、delete、nextTick、observable.

然后定义了一个空对象options

Vue.options = Object.create(null)

之后通过循环填充属性:

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

ASSET_TYPES的值通过查找对应文件后知道为['component', 'directive', 'filter'],所以循环之后options对象变为:

Vue.options = {
  components: Object.create(null),
  directives: Object.create(null),
  filters: 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

这是在options上添加了_base属性

接下来是这句代码

// 将builtInComponents的属性混合到Vue.options.components中
  extend(Vue.options.components, builtInComponents)

extend 来自于 shared/util.js 文件,代码也很简单

/**
 * Mix properties into target object.
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

builtInComponents 来自于 core/components/index.js 文件

import KeepAlive from './keep-alive'

export default {
  KeepAlive
}

现在为止,Vue.options变成

Vue.options = {
    components: {
        KeepAlive
    },
    directives: Object.create(null),
    filters: Object.create(null),
    _base: Vue
  }

在函数的最后,调用了四个方法:

  // 在Vue构造函数上添加use方法,Vue.use()用来安装Vue插件
  initUse(Vue)
  // 添加全局API:Vue.mixin()
  initMixin(Vue)
  // 添加Vue.cid静态属性 和 Vue.extend 静态方法
  initExtend(Vue)
  // 添加静态方法:Vue.component Vue.directive Vue.filter
  // 全局注册组件、指令、过滤器
  initAssetRegisters(Vue)

我们先大致了解这几个方法的作用,至于具体实现以后再详细分析。

第二个阶段大体就了解完了,就是挂载静态属性和方法。

Vue平台化包装

接下来来看platforms/web/runtime/index.js文件,我们之前看的两个文件是在core目录下的,是Vue的核心文件,与平台无关的。platforms下面的就是针对特定平台对Vue进行包装。主要分两个平台:web和weex, 我们看的是web平台下的内容。

首先是安装特定平台的工具函数

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

Vue.config我们之前见过,它代理的是/src/core/config.js文件抛出的内容,现在是重写了其中部分属性。

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

这是安装平台运行时的指令和组件。extend的作用我们都已经知道了。来看一下platformDirectives和platformComponents的内容。

platformDirectives:

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

export default {
  model,
  show
}

platformComponents:

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

export default {
  Transition,
  TransitionGroup
}

Vue.options之前已经有过包装,经过这两句代码之后变成:

Vue.options = {
    components: {
        KeepAlive,
        Transition,
        TransitionGroup
    },
    directives: {
        model,
        show
    },
    filters: Object.create(null),
    _base: Vue
}

继续看下面的代码

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

这是添加了两个实例方法:__patch__$mount

看完之后我们就知道了该文件的作用

  1. 设置平台化的Vue.config
  2. 在Vue.options上混合了两个指令:modelshow
  3. 在Vue.options上混合了两个组件:TransitionTransitionGroup
  4. 在Vue.prototye上添加了两个方法:__patch__$mount

compiler

到目前为止,运行时版本的Vue已经构造完了。但是我们的入口是entry-runtime-with-compiler.js文件,从文件名可以看出来这里是多了一个compiler。我们来看看这个文件吧

// 获取拥有指定ID属性的元素的innerHTML
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function ( // 重写了$mount方法
  el?: string | Element,
  hydrating?: boolean
): Component {}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

// 添加compile全局API
Vue.compile = compileToFunctions

export default Vue

这个文件主要是重写了Vue.prototype.$mount方法,添加了Vue.compile全局API

以上,我们从Vue构造函数入手,大致梳理了项目的脉络。理清楚了大体流程,之后再慢慢探索细节。

你可能感兴趣的:(Vue源码解析一——骨架梳理)