Vue 源码初探(一)

目录结构

├── .circleci ----------------------------- 存放持续集成工具circleci的配置文件
├── .github ------------------------------- 存放README.md中关联的md文档 
├── benchmarks ---------------------------- 性能测试、评估,跑分demo,比如大数据量的table或者渲染大量SVG
├── dist ---------------------------------- 各个平台构建后文件的输出目录(UMD、CommonJS、ES 生产和开发包)
├── examples ------------------------------ 例子代码
├── flow ---------------------------------- 静态类型声明 [Flow](https://flow.org/)
├── packages ------------------------------ 包含服务端渲染和模板编译器两种不同的npm包,是提供给不同使用场景使用的
├── scripts ------------------------------- 构建相关的文件
│   ├── git-hooks ------------------------- 存放git钩子的目录
│   ├── alias.js -------------------------- 别名配置
│   ├── build.js -------------------------- 对 config.js 中所有的rollup配置进行构建
│   ├── config.js ------------------------- 生成rollup配置的文件
│   ├── feature-flags.js ------------------ 部分属性的标志
│   ├── release.sh ------------------------ 用于自动发布新版本的脚本
├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码
│   ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数
│   │   ├── codegen ----------------------- 把AST转换为Render函数
│   │   ├── directives -------------------- 通用生成Render函数之前需要处理的指令
│   │   ├── parser ------------------------ 解析模版成AST
│   ├── core ------------------------------ 存放通用的,与平台无关的代码
│   │   ├── components -------------------- 包含抽象出来的通用组件,主要是Keep-Alive
│   │   ├── global-api -------------------- 包含给Vue构造函数挂载全局方法或属性的代码
│   │   ├── instance ---------------------- 实例化相关内容,生命周期、事件等
│   │   ├── observer ---------------------- 响应系统,包含数据观测的核心代码(双向数据绑定)
│   │   ├── util -------------------------- 工具方法
│   │   ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码
│   ├── platforms ------------------------- 包含平台特有的相关代码,不同平台的不同构建的入口文件也在这里
│   │   ├── web --------------------------- web平台
│   │   │   ├── compiler ------------------ web端编译相关代码,将 template 编译为 render 函数
│   │   │   ├── runtime ------------------- web端运行时相关代码,用于创建Vue实例等
│   │   │   ├── server -------------------- 服务端渲染
│   │   │   ├── util ---------------------- 相关工具类
│   │   │   ├── entry-runtime.js ---------- 运行时构建的入口,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意
│   │   │   ├── entry-runtime-with-compiler.js -- 独立构建版本的入口,它在 entry-runtime 的基础上添加了模板(template)到render函数的编译器
│   │   │   ├── entry-compiler.js --------- vue-template-compiler 包的入口文件
│   │   │   ├── entry-server-renderer.js -- vue-server-renderer 包的入口文件
│   │   │   ├── entry-server-basic-renderer.js -- 输出 packages/vue-server-renderer/basic.js 文件
│   │   ├── weex -------------------------- 混合应用
│   ├── server ---------------------------- 包含服务端渲染(ssr)的相关代码
│   ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│   ├── shared ---------------------------- 全局共享的方法和常量
├── test ---------------------------------- 包含所有测试文件
├── types --------------------------------- 支持TypeScript,TypeScript类型声明文件
├── .babelrc ------------------------------ babel 配置文件
├── .editorconfig ------------------------- 针对编辑器的编码风格配置文件
├── .eslintignore ------------------------- eslint 忽略配置
├── .eslintrc ----------------------------- eslint 配置文件
├── .flowconfig --------------------------- flow 的配置文件
├── .gitignore ---------------------------- git 忽略配置
├── .BACKERS.md --------------------------- 赞助者信息文件
├── LICENSE ------------------------------- 项目开源协议
├── package.json -------------------------- 依赖
├── README.md ----------------------------- 说明文档
├── yarn.lock ----------------------------- yarn 锁定文件

对于目录结构并不需要在看到描述后就完完全全知道所有文件的作用,刚开始知道个大概,在源码解析的过程中慢慢去理解体会。

Vue 不同版本的构建

在 dist 输出文件夹的 REMEDE.md 中,可以看到作者给出的一个表格

UMD CommonJS ES Module
Full vue.js vue.common.js vue.esm.js
Runtime-only vue.runtime.js vue.runtime.common.js vue.runtime.esm.js
Full (production)、 vue.min.js
Runtime-only (production) vue.runtime.min.js

其中行上按照输出的模块形式分为 UMD、CommonJS、ES Module 三种,而列上按照环境和版本分别分成 Full、Runtime-only、Full (production)、Runtime-only (production)四种。作者按照这些分类在 dist 文件夹下构建了多个不同的文件以供使用。下面简单解释一下这些分类。

  • UMD: UMD 版本可以通过 <script> 标签直接用在浏览器中。

jsDelivr CDN 的 https://cdn.jsdelivr.net/npm/vue 默认文件就是运行时 + 编译器的 UMD 版本 (vue.js)。

  • CommonJS:CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1。

这些打包工具的默认文件 (pkg.main) 是只包含运行时的 CommonJS 版本 (vue.runtime.common.js) —— 对应 package.json 的 main 。

  • ES Module: 为打包工具提供的 ESM:为诸如 webpack 2 或 Rollup提供的现代打包工具。

ESM格式被设计为可以被静态分析, 所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件 (pkg.module) 是只有运行时的 ES Module 构建 (vue.runtime.esm.js) —— 对应 package.json 的 module 。

  • ES Module(2.6+):为浏览器提供的 ESM (2.6+):用于在现代浏览器中通过 <script type="module"> 直接导入。

如果你需要在客户端编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器

  • Full: 完整版 = 运行时版 + Compiler(编译器)

  • Runtime-only:运行时版

  • Full (production)、Runtime-only (production): 生产环境的完整版和运行时版

为什么要分 运行时版 与 完整版?完整版比运行时版多了一个 Compiler,Compiler在目录结构中解释为 将 template 编译为 render 函数,这个过程不一定要在代码运行的时候去做,实际上可以在构建的时候完成,这样真正运行的代码就免去了这样一个步骤,提升了性能。同时,将 Compiler 抽离为单独的包,还减小了库的体积。完整版是允许在代码运行的时候去现场编译模板,在不配合构建工具的情况下可以直接使用。我们更多时候配合构建工具使用运行时版本。

同样,我们在 scripts/config.js(Rollup配置文件)中也能看出这些分类的区别。

不同模块的运行时和完整版入口文件是一样的。运行时的入口文件名字为 entry-runtime.js,完整版的入口文件名字为 entry-runtime-with-compiler.js(web 是 alias 中配置的别名: src/platforms/web)。但是输出的格式(format)是不同的,分别是 cjs、es 以及 umd。

对应 package.json 文件中的启动命令也不同。

"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",
    // 构建运行时 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",
    // 构建 Compiler 包
    "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
}

Vue 构造函数

在使用 Vue 的时候,要使用 new 操作符进行调用,说明 Vue 应该是一个构造函数。可以从 npm run dev 作为切入点。

在 package.json 文件中

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

根据 scripts/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
},

找到入口文件为 src/platforms/web/entry-runtime-with-compiler.js,在这个文件中可以看到 vue 是从另一个文件中引用的。

import Vue from './runtime/index'

打开 ./runtime/index.js 文件,vue 同样是从另外一个文件中引用的。

import Vue from 'core/index'  // core 是 alias 中配置的别名: src/core

打开 src/core/index.js 文件,

import Vue from './instance/index'

打开 ./instance/index.js 文件, 发现 Vue 构造函数是在这里被定义的。

// 从五个文件导入五个方法(不包括 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)
  ) {
    // 使用了安全模式来提醒要使用 new 操作符来调用 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)

// 导出
export default Vue

其中最主要的是导入的 initMixin、stateMixin、renderMixin、eventsMixin、lifecycleMixin 五个方法,依次看一下。

打开 ./init.js 文件,找到 initMixin 方法,

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
   // 函数体
  }
}

可以看到这个方法的作用就是在 Vue 的原型上添加 _init 方法,当我们执行 new Vue() 的时候,this._init(options) 将被执行。顾名思义,应该是初始化某些东西。

再打开 ./state.js 文件,找到 stateMixin 方法,

export function stateMixin (Vue: Class<Component>) {
  // 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') {
    //如果不是生产环境,就为 $data 和 $props 这两个属性设置 set 只读
    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 (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    // 函数体
  }
}

可以看到,stateMixin 先使用 Object.defineProperty 在 Vue.prototype 上定义了 $data 和 $props 两个属性,又在 Vue.prototype 上定义了 $set、$delete 以及 $watch三个方法。其中,$data 和 $props 这两个属性的定义分别写在了 dataDef 以及 propsDef 这两个对象里,通过 get 方法代理了 _data 和 _props 这两个实例属性。

再打开 ./events.js 文件,找到 eventsMixin 方法,

export function eventsMixin (Vue: Class<Component>) {
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
  Vue.prototype.$once = function (event: string, fn: Function): Component {}
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
  Vue.prototype.$emit = function (event: string): Component {}
}

在 Vue.prototype 上定义了 $on、$once、$off 以及 $emit 四个方法。

再打开 ./lifecycle.js 文件,找到 lifecycleMixin 方法。

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
  Vue.prototype.$forceUpdate = function () {}
  Vue.prototype.$destroy = function () {}
}

在 Vue.prototype 上定义了_update、$forceUpdate 以及 $destroy 三个方法。

最后打开 render.js 文件,找到 renderMixin 方法,

export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)
  Vue.prototype.$nextTick = function (fn: Function) {}
  Vue.prototype._render = function (): VNode {}
}

renderMixin 方法先执行 installRenderHelpers 函数,又在 Vue.prototype 上添加了 $nextTick 和 _render 两个方法。

而 installRenderHelpers 函数在 render-helpers/index.js 文件中,打开后,

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
}

可以发现这个函数的作用是在 Vue.prototype 上添加一系列方法。

总的看来,在 ./instance/index.js 文件中的五个方法其实就是在 Vue.prototype 上挂载一些属性和方法。这也是对 Vue 构造函数的一个初探。

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