Vue源码解析:Vue是如何设计的?

组成与设计.png

项目文件结构

Vue项目中,所有核心的代码都是在src目录下完成,为了更好的了解Vue的底层实现,我们首先来了解一下src目录下代码的组织情况,从全局入手,在脑海里留下简单的印象,方便后续的学习。(注意:当前使用Vue的版本为2.6.12,不同版本的内容可能会有所差异)

.
├── compiler  // 编译模块:将 template 编译成为可以生成 vnode 的 render 函数
│   ├── codeframe.js
│   ├── codegen             // 代码生成文件:根据 ast 树可生成 vnode 的 render代码
│   ├── create-compiler.js  // 创建编译器的工厂函数
│   ├── directives          // 指令解析:v-on, v-bind, v-model
│   ├── error-detector.js   
│   ├── helpers.js          // 编译相关方法,如属性获取等方法
│   ├── index.js            // 入口文件
│   ├── optimizer.js        // 编译优化:将 ast 树进行优化
│   ├── parser                      // html 解析文件:将 template 解析成 ast 树🌲
│   └── to-function.js      // 创建编译器的工厂函数
├── core     // 构造函数核心模块:构建Vue构造函数,添加原型方法,实现完成渲染流程的_init方法
│   ├── components  // 自带的全局组件,如 keep-alive
│   ├── config.js   // 配置相关
│   ├── global-api  // 全局api,如 Vue.use, extend, mixin, component等方法
│   ├── index.js    // 入口文件,在 Vue 上挂载全局方法并导出 Vue
│   ├── instance    // 构造函数起始位置
│   ├── observer    // 响应式原理
│   ├── util        // 一些工具方法,包含 mergeOptions, nextTick 等方法的实现
│   └── vdom        // 虚拟 dom
├── platforms // 平台相关,包含不同平台的不同构建入口,这里主要研究web端
│   ├── weex
│   └── web
│       ├── compiler   // 与平台相关的编译
│       ├── entry-compiler.js // vue-template-compiler 包的入口文件
│       ├── entry-runtime-with-compiler.js // 构建入口,包含编译器
│       ├── entry-runtime.js  // 构建入口,不包含编译器,不支持 template 转换 render
│       ├── entry-server-basic-renderer.js
│       ├── entry-server-renderer.js
│       ├── runtime   // 与平台相关的构建
│       ├── server
│       └── util
│
├── server    // 服务端渲染相关
├── sfc       // 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
└── shared    // 代码库通用代码
    ├── constants.js
    └── util.js

以上是Vue项目中主要文件目录,里面附带一些注释,讲解了比较主要模块的功能及作用。刚开始学习时只做简单了解即可,后面我们会逐步详细学习其中的一些模块,从而从原理级别理解整个Vue项目的设计与实现。

Vue的真面目

要想真正的了解Vue是怎样的,首先我们需要找到Vue是咋哪里被定义的。我们先找到package.json文件下的scripts配置。scripts里存放的都是运行命令的别名形式,通过命令可以轻松找到对应命令执行文件的路径。

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

这里当运行dev命令实际上是运行scripts/config.js文件,让我们找到scripts/config.js文件。

通过运行命令参数我们可以知道process.env.TARGET的值为web-full-dev,因此可以在builds里找到对应的配置文件,如下

const builds = {
  '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
   }
  ...
}

module.exports = genConfig(process.env.TARGET)

通过entry,我们找到web/entry-runtime-with-compiler.js文件:

import Vue from './runtime/index'

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  ...
}
Vue.compile = compileToFunctions

export default Vue

在这里,我们终于找到了Vue相关的文件,这也是Vue的起始入口。接着根据Vue的引入路径,找到./runtime/index文件:

import Vue from 'core/index'
...
Vue.prototype.__patch__ = inBrowser ? patch : noop
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  ...
}
export default Vue

这里还不是Vue真正的起始点,继续查找core/index文件:

import Vue from './instance/index'
initGlobalAPI(Vue)
...
Vue.version = '__VERSION__'
export default Vue

发现仍然不是Vue的起始点,继续查找'./instance/index'文件:

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)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

好了,大功告成!费劲千辛万苦终于找到了Vue的真正定义的位置!可以看出Vue其实就是一个构造函数,而构造函数内部仅仅只是调用了_init方法,看上去非常简单。但是Vue是如何通过这么简单的定义实现那么复杂的功能呢?这里就要涉及到构造函数原型实例的概念了,不了解这些概念的建议参考《javascript高级设计程序》中原型章节来进行学习。下面我们通过下方三个方面来介绍Vue的实现。

  1. 原型方法属性:通过 5 个 init 方法,向Vue的原型上添加方法,
  2. 静态方法属性:在导入Vue构造函数的过程中,向Vue构造函数上添加静态方法,也有向原型上添加方法
  3. 实例化:在实例化的过程中,执行_init方法,完成整个Vue初始化到渲染的逻辑。

Vue的原型方法(通过5个init方法添加)

initMixin

initMixin方法主要实现了_init方法。

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // init 实现内容,由于这里仅做概览,所以具体实现均已省略
    ... 
  }
}

从上面Vue构造函数我们可以知道,这个方法在实例化时有被调用,它主要的作用是实现:选项的合并,数据初始化(如响应式处理),以及触发编译和渲染的流程,所以十分重要。这里也只是先做一个了解,后续的实例化章节将都会从这个方法开始分析。

stateMixin

stateMixin主要实现了data,props的代理功能,即当我们访问$data时,实际访问的是_data。另外在非生产环境下,会对$data,$props进行 set处理,每次设置新的值时都会打印提示,所以实际上$data,$props都是只读属性。

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)
    }
  }
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  Vue.prototype.$watch = function () { ... }
}

除此之外,这里还在Vue原型上挂载了比较常见的三个方法:$set$delete$watch

eventsMixin

nodeEventEmitter类似,eventsMixin实现了四个方法:$on,$off,$once,$emit,用于监听,触发,销毁事件。

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function () { ... }
  Vue.prototype.$once = function () { ... }
  Vue.prototype.$off = function () { ... }
  Vue.prototype.$emit = function () { ... }
}

lifecycleMixin

lifecycleMixin实现了三个方法:_update方法非常重要,它主要负责将vnode生成真实节点。

export function lifecycleMixin (Vue: Class<Component>) {
  // 更新,将 vnode 生成 真实节点
  Vue.prototype._update = function () { ... }
  // 强制刷新
  Vue.prototype.$forceUpdate = function () { ... }
  // 销毁
  Vue.prototype.$destroy = function () { ... }
}

renderMixin

renderMixin主要做了三项工作

export function renderMixin (Vue: Class<Component>) {
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  
  Vue.prototype._render = function (): VNode {
    return vnode
  }
}
  1. installRenderHelpers函数用于添加render相关方法,在编译环节最后生成的代码,都是由这些方法拼接而成的代码,所以也是非常的重要,在这里先混个眼熟。
  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
  1. $nextTick方法,在下一次事件循环触发,涉及到事件循环机制。
  2. _render方法,用于生成vnode

Vue的静态方法属性

通过上面5个init方法我们已经了解了许多原型方法的添加过程,但是在Vue中还有很多全局方法,比如Vue.component,Vue.use等方法,它们都是构造函数的静态属性,下面我们看看这些静态属性是如何添加的。与寻找Vue的起始位置过程恰恰相反,这次我们从Vue的起始文件出发,看看最后导出的Vue是怎样的。

/src/core/index.js文件

这是第一层引入Vue构造函数的文件

import { initGlobalAPI } from './global-api/index'

initGlobalAPI(Vue)

// ... 中间省略

Vue.version = '__VERSION__'

这里我们看一下initGlobalAPI方法,打开core/global-api/index.js文件

export function initGlobalAPI (Vue: GlobalAPI) {
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

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

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

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

  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

这里挂载了很多静态方法,Vue中大多数的全局方法都在这个位置添加的,这里我们着重分析一下options

  import builtInComponents from '../components/index'  

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

  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)  

可以看出,在Vue构造函数上添加了一个options属性(注意!这里是静态属性,为构造函数所有,区别于在实例化传入的options)。随后又通过遍历ASSET_TYPES,在options上添加了components,directives,filters方法。另外还添加了_base,指向当前构造函数。最后通过extend方法将builtInComponents合并到options.components当中。这里的builtInComponents实际上就是Vue自带的组件,即keep-alive组件。所以最终Vue.options的内容如下:

// Vue.options 内容
{
    components: {
    KeepAlive
  },
  filters: {},
  directives: {},
  _base: Vue
}

这里之所以额外提起,是因为在后续选项合并时,会使用此处的options进行合并。

/src/platforms/web/runtime/index.js文件

这里是第二层引入Vue的文件,主要给Vue处理平台相关的一些方法

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,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

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

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

Vue.prototype.__patch__ = inBrowser ? patch : noop
Vue.prototype.$mount = function () { ... }

export default Vue

这里首先给Vue.config添加了一系列方法,注意,这些方法之所以在这里添加而不是在core/index.js文件里添加,是因为这里的方法都与平台相关,不同的平台的方法实现也会不一样。

extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

这两个extend实际上进一步扩充了Vue.options方法,扩充后的内容如下

// Vue.options 内容
{
    components: {
    KeepAlive,
    // 新增 platformComponents
    Transition,
    // 新增 platformComponents
    TransitionGroup
  },
  filters: {},
  directives: {
    // 新增 platformDirectives
    model,
    // 新增 platformDirectives
    show
  },
  _base: Vue
}

这也是为什么我们可以不用注册也能全局使用v-model,v-show的原因了,因为Vue已经帮我们全局注册了。

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

这是最后一层引入Vue

import Vue from './runtime/index'
...

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
    
    ...
    
  return mount.call(this, el, hydrating)
}

Vue.compile = compileToFunctions

export default Vue

这里主要是重新实现了$mount方法,但是为什么原先在runtime/index.js文件里实现了$mount方法,这里又要重新实现一遍呢?因为runtime/index.js里的$mount与编译是无关的,无法处理template模板代码,而这里重写的$mount实际上还是调用了runtime/index.js里的$mount,但是在此之前,增加了从templaterender的编译过程。

实例化过程

前面已经将Vue的各种方法属性挂载完毕,现在则是需要进行实例化了,也就是调用之前提到的_init方法。打开/src/core/instance/init.js文件,代码如下:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...

    // 1. 合并options
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
  
    ...

    // 2. 初始化数据
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    // 3. 挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

在初始化的过程中,主要分为三个阶段:

阶段一:合并选项,将 Vue.options和传入的options进行合并

阶段二:初始化数据,并对数据进行响应式处理

阶段三:编译代码,得到render函数,将vnode生成真实节点,并挂载到界面

由于这部分比较核心,且难以理解,这里仅做了解,后续会逐一进行分析。

Vue的整体设计

通过上面的分析,我们已经对Vue原型方法Vue静态方法属性Vue实例化过程有了大致的了解,下面我们用张图总结下整体的内容,也就是Vue整体的设计思路。

image.png

总结下来就是:

  1. 构建一个具有完备功能的构造函数,因此在上面添加各个模块需要的方法属性。包括原型方法属性和静态方法属性。
  2. 进行实例化,在实例化过程中进行各种处理,其中包括:选项合并,数据响应式处理,编译,虚拟DOM更新等等。

这里的描述比较笼统,旨在从整体上来对Vue进行一个了解。在接下来的章节我们会详细分析实例化的整个过程,从而由点及面的了解Vue。下一章节我们将开始Vue核心代码的正式学习。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容