vue源码学习——综述

以前也断断续续看过一些关于vue源码的文章,但是都不够完整或者说系统,对源码的理解也一直是囫囵吞枣的状态。最近尤大大在微博分享了两个学习源码的链接,我觉得有必要听从大神的安排来整理一波知识点了。
大神分享的两个链接如下:
Vue技术内幕
Vue技术揭秘
个人学习版本2.5.17,按照上面两篇文章来写,为了加深自己的记忆而已。


源码目录说明

首先自行去Github上下载Vue的源码,Vue的源码与我们之前看的jQuery源码不同,不是一整个js文件,而是利用模块化的思想,作者把功能模块拆分的非常清楚,相关的逻辑放在一个独立的目录下维护,并且把复用的代码也抽成一个独立目录。这样的目录设计让代码的阅读性和可维护性都变强,是非常值得学习和推敲的。如下图:

├── scripts ------------------------------- 构建相关的文件,一般情况下我们不需要动
│   ├── git-hooks ------------------------- 存放git钩子的目录
│   ├── alias.js -------------------------- 别名配置
│   ├── config.js ------------------------- 生成rollup配置的文件
│   ├── build.js -------------------------- 对 config.js 中所有的rollup配置进行构建
│   ├── ci.sh ----------------------------- 持续集成运行的脚本
│   ├── release.sh ------------------------ 用于自动发布新版本的脚本
├── dist ---------------------------------- 构建后文件的输出目录
├── examples ------------------------------ 存放一些使用Vue开发的应用案例
├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/)
├── packages ------------------------------ 存放独立发布的包的目录
├── test ---------------------------------- 包含所有测试文件
├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码
│   ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数
│   ├── core ------------------------------ 存放通用的,与平台无关的代码
│   │   ├── observer ---------------------- 响应系统,包含数据观测的核心代码
│   │   ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码
│   │   ├── instance ---------------------- 包含Vue构造函数设计相关的代码
│   │   ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码
│   │   ├── components -------------------- 包含抽象出来的通用组件
│   ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码
│   ├── platforms ------------------------- 包含平台特有的相关代码,不同平台的不同构建的入口文件也在这里
│   │   ├── web --------------------------- web平台
│   │   │   ├── 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 -------------------------- 混合应用
│   ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│   ├── shared ---------------------------- 包含整个代码库通用的代码
├── package.json -------------------------- 不解释
├── yarn.lock ----------------------------- yarn 锁定文件
├── .editorconfig ------------------------- 针对编辑器的编码风格配置文件
├── .flowconfig --------------------------- flow 的配置文件
├── .babelrc ------------------------------ babel 配置文件
├── .eslintrc ----------------------------- eslint 配置文件
├── .eslintignore ------------------------- eslint 忽略配置
├── .gitignore ---------------------------- git 忽略配置

我们的学习过程可以分为八个部分学习,如下图所示:
vue源码学习——综述_第1张图片
第一章:准备工作

介绍了 Flow、Vue.js 的源码构建方式,以及分析了 Vue.js 的初始化过程。

第二章:数据驱动

详细讲解了模板数据到 DOM 渲染的过程,从 new Vue 开始,分析了 mount、render、update、patch 等流程。

第三章:组件化

分析了组件化的实现原理,并且分析了组件周边的原理实现,包括合并配置、生命周期、组件注册、异步组件。

第四章:深入响应式原理

详细讲解了数据的变化如何驱动视图的变化,分析了响应式对象的创建,依赖收集、派发更新的实现过程,一些特殊情况的处理,并对比了计算属性和侦听属性的实现,最后分析了组件更新的过程。

第五章:编译

从编译的入口函数开始,分析了编译的三个核心流程的实现:parse -> optimize -> codegen。

第六章:扩展

详细讲解了 event、v-model、slot、keep-alive、transition、transition-group 等常用功能的原理实现,该章节作为一个可扩展章节,未来会分析更多 Vue 提供的特性。

第七章:Vue-Router

分析了 Vue-Router 的实现原理,从路由注册开始,分析了路由对象、matcher,并深入分析了整个路径切换的实现过程和细节。

第八章:Vuex

分析了 Vuex 的实现原理,深入分析了它的初始化过程,常用 API 以及插件部分的实现。

有了这个导航很方便了哈,除了根据上面两个链接学习,自己也可以多找对应知识的知识点博客看,加深学习印象,各个知识点的学习也不至于零散。


1、准备工作

1.1 Flow

Flow 是 facebook 出品的 JavaScript 静态类型检查工具。Vue.js 的源码利用了 Flow 做了静态类型检查,所以了解 Flow 有助于我们阅读源码。如何将Flow应用在程序中,带有flow类型检查的js文件并不能直接运行,需要使用babel进行转码,具体使用方法还可参考Flow官网。

Flow使用原因

JavaScript 是动态类型语言,它过于灵活的副作用是很容易就写出非常隐蔽的隐患代码,在编译期甚至看上去都不会报错,但在运行阶段就可能出现各种奇怪的 bug。

类型检查是当前动态类型语言的发展趋势,所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语言相近的体验。

项目越复杂就越需要通过工具的手段来保证项目的维护性和增强代码的可读性。 Vue.js 在做 2.0 重构的时候,在 ES2015 的基础上,除了 ESLint 保证代码风格之外,也引入了 Flow 做静态类型检查。之所以选择 Flow,主要是因为 Babel 和 ESLint 都有对应的 Flow 插件以支持语法,可以完全沿用现有的构建配置,非常小成本的改动就可以拥有静态类型检查的能力。

Flow工作方式

通常类型检查分成2种方式:
* 类型推断: 通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型。
* 类型注释: 事先注释好我们期待的类型,Flow会基于这些注释来判断。

类型推断

它不需要任何代码修改即可进行类型检查,最小化开发者的工作量。它不会强制你改变开发习惯,因为它会自动推断出变量的类型。这就是所谓的类型判断,Flow最重要的特性之一。

通过一个简单的例子说明一下:

/*@flow*/

function split(str) {
    return str.split(' ');
}

split(11);   //报错:str.split() is not a function.

因为split函数期待的参数是字符串,而我们输入了数字。

类型注释

考虑如下代码:

/*@flow*/

function add(x, y) {
    return x + y;
}

add('Hello', 11);   // "Hello11"

Flow检查上述代码检查不出错误,因为从语法层面考虑,+既可以用在字符串上,也可以用在数字 上,我们并没有明确指出 add() 的参数必须为数字。

在这种情况下,我们可以借助类型注释来指明期望的类型。类型注释是冒号 : 开头,可以在函数参数,返回值,变量声明中使用。

例如,在上段代码中添加类型注释:

/*@flow*/

function add(x: number, y: number): number {
    return x + y;
}

add(1, 11);          //no errors
add('Hello', 11);    //error

现在Flow就能检查出错误,因为函数参数的期待类型为数字,而我们提供了字符串。

上面的例子是针对函数的类型注释。接下来我们看看Flow能支持的一些常见的类型注释。

/*1、数组
数组类型的数据格式是Array,T表示数组中每项的数据类型。在上述代码中,arr是每项均为数字的数组。如果我们给这个数组添加了一个字符串,Flow能检查出错误。
*/
/*@flow*/

let arr: Array = [1, 2, 3];

arr.push('Hello');  //error

/*
2、类和对象
类的类型注释如下所示,可以对类自身的属性做类型检查,也可以对构造函数的参数做类型检查。
对象的注释类型类似于类,需要指定对象属性的类型。
*/
/*@flow*/

class Bar {
    x: string;             //x是字符串
    y: string | number;    //y可以是字符串或者数字
    z: boolean;

    constructor(x: string, y: string | number){
        this.x = x;
        this.y = y;
        this.z = false;
    }
}

let bar: Bar = new Bar('hello', 4);  //这里对对象实例bar作了类型注释

let obj: { a: string, b: number, c: Array, d: Bar } = {
    a: 'hello',
    b: 11,
    c: ['hello', 'world'],
    d: new Bar('hello', 3)
}

/*
3、Null
若想任意类型T可以为null或者undefined,只需类似如下写成 ?T 的格式即可。
*/
/*@flow*/

let foo: ?string = null;   //此时,foo可以是字符串,也可以为null

目前只列举了Flow的一些常见的类型注释。如果想了解所有的类型注释,自行查看官方文档。

Flow在Vue.js源码中的运用

有时候我们想引用第三方库,或者自定义一些类型,但Flow并不认识,因此检查的时候会报错。为了解决这个问题,Flow提出了一个libdef的概念,可以用来识别这些第三方库或者是自定义类型,而Vue.js也利用了这一特性。

在Vue.js的主目录下有.flowconfig文件,它是Flow的配置文件,其中的 [libs] 部分用来描述包含指定库定义的目录,这里 [libs] 配置的是 flow ,表示指定的库定义都在 flow 文件夹内。我们打开这个目录,会发现:

flow
├── compiler.js        # 编译相关
├── component.js       # 组件数据结构
├── global-api.js      # Global API 结构
├── modules.js         # 第三方库定义
├── options.js         # 选项相关
├── ssr.js             # 服务端渲染相关
├── vnode.js           # 虚拟 node 相关
├── weex.js            # 混合应用相关

Vue.js有很多自定义类型的定义,在阅读源码的时候,如果遇到某个类型并想了解它的完整的数据结构的时候,可以在flow中翻阅这些数据结构的定义。类似Flow 的工具还有TypeScript ,也可自行了解一下。

1.2 Vue.js源码构建

Vue.js源码是基于Rollup 构建的,它的构建相关配置都在scripts目录下。

构建脚本

通常一个基于NPM托管的项目都会有一个package.json 文件,它是对项目的描述文件,它的内容实际上是一个标准的JSON对象。

我们通常会配置package.json中的 scripts 字段作为NPM的执行脚本,这里去掉测试相关以及weex相关的脚本配置:‘

"scripts": {
      // 构建完整版 umd 模块的 Vue
      //-w 就是watch,-c 就是指定配置文件为 scripts/config.js 
    "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 ",
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- vue.runtime.common.js,vue-server-renderer",
    "build:weex": "npm run build -- weex",
    "lint": "eslint src build test",
    "flow": "flow check",
    "release": "bash scripts/release.sh",
    "release:note": "node scripts/gen-release-note.js",
    "commit": "git-cz"
  },

有关 dev 的命令都是开发阶段的命令,构建阶段使用 build 相关命令,当在命令行运行 npm run build 的时候,实际上就会执行 node scripts/build.js ,接下来我们看看它实际上是怎么构建的。

构建过程

打开 scripts/build.js 文件,可看源码:

const fs = require('fs')
const path = require('path')
const zlib = require('zlib')  //zlib模块提供了用Gzip和Deflate/Inflate实现的压缩功能。
const rollup = require('rollup')
const uglify = require('uglify-js')  //UglifyJs 是一个js 解释器、最小化器、压缩器、美化器工具集
//以同步的方法检测目录是否存在。
if (!fs.existsSync('dist')) {
  fs.mkdirSync('dist')   //不存在则创建
}
//从配置文件读取配置
let builds = require('./config').getAllBuilds()

// 通过命令行参数对构建配置做过滤,这样就可以构建出不同用途的Vue.js了
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

build(builds)

function build (builds) {
  let built = 0
  const total = builds.length
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }

  next()
}
//构建函数
function buildEntry (config) {
  const output = config.output
  const { file, banner } = output
  const isProd = /min\.js$/.test(file)  //正则表达式,检测file是否是已压缩文件
  return rollup.rollup(config)
    .then(bundle => bundle.generate(output))
    .then(({ code }) => {
      if (isProd) {
        var minified = (banner ? banner + '\n' : '') + uglify.minify(code, {
          output: {
            ascii_only: true
          },
          compress: {
            pure_funcs: ['makeMap']
          }
        }).code
        return write(file, minified, true)  //压缩代码
      } else {
        return write(file, code)   //不压缩代码
      }
    })
}
//输出代码处理函数
function write (dest, code, zip) {
  return new Promise((resolve, reject) => {
    function report (extra) {
      //process.cwd():获取node.js进程当前工作的绝对路径
      //path.relative(argS,argE)方法用于获取从argS进入argE的相对路径,当两个参数都为绝对路径,且不同盘时,返回参数areE
      console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ''))
      resolve()
    }

    fs.writeFile(dest, code, err => {
      if (err) return reject(err)
      if (zip) {
        zlib.gzip(code, (err, zipped) => {
          if (err) return reject(err)
          report(' (gzipped: ' + getSize(zipped) + ')')
        })
      } else {
        report()
      }
    })
  })
}
//获取代码大小
function getSize (code) {
  return (code.length / 1024).toFixed(2) + 'kb'
}
//输出台打印错误
function logError (e) {
  console.log(e)
}
//控制终端上字符串的显示效果
function blue (str) {
  return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
}
不同构建输出的区别

接下来我们看看上段代码引入的配置文件,在 scripts/config.js 中:

const aliases = require('./alias')  //引入别名配置的文件scripts/alias.js
//resolve函数:通过获取别名配置,获取路径的真实路径
const resolve = p => {  
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}
/*
1、按照输出的模块形式分类,Vue有三种不同的构建输出,分别是:UMD、CommonJS以及 ES Module 。
2、三种构建配置的入口是相同的,即 web/entry-runtime.js 文件,但是输出的格式(format)是不同的,分别是cjs、es以及umd。
3、每种模块形式又分别输出了 运行时版 以及 完整版,完整版比运行时版本多了一个 compiler, compiler的作用是:编译器代码的存放目录,将 template 编译为 render 函数。
4、umd模块格式的输出较cjs 与 es 版本的输出相比,同样也分为运行时版 以及 完整版,并且还分为 生产环境 与 开发环境。
*/
const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs': {  //运行时版cjs模块
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.js'),
    format: 'cjs',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs': {  //完整版cjs模块
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.js'),
    format: 'cjs',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only (ES Modules). Used by bundlers that support ES Modules,
  // e.g. Rollup & Webpack 2
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler CommonJS build (ES Modules)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {    //umd模块 运行时版开发环境
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {    //umd模块 运行时版生产环境
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {    //umd模块 完整版开发环境
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {    //umd模块 完整版生产环境
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  //服务端渲染插件以及weex的打包配置
  ......
}

//生成配置的方法
//最终,genConfig 函数返回一个 config 对象,这个config对象就是Rollup的配置对象
function genConfig (name) {
  const opts = builds[name]
  const config = {
    input: opts.entry,
    external: opts.external,
    plugins: [
      replace({
        __WEEX__: !!opts.weex,
        __WEEX_VERSION__: weexVersion,
        __VERSION__: version
      }),
      flow(),
      buble(),
      alias(Object.assign({}, aliases, opts.alias))
    ].concat(opts.plugins || []),
    output: {
      file: opts.dest,
      format: opts.format,
      banner: opts.banner,
      name: opts.moduleName || 'Vue'
    }
  }
    //config对象处于生产环境还是开发环境
  if (opts.env) {
    config.plugins.push(replace({
      'process.env.NODE_ENV': JSON.stringify(opts.env)
    }))
  }
//设置config对象的属性,不可枚举
  Object.defineProperty(config, '_name', {
    enumerable: false,
    value: name
  })

  return config
}
//当我们运行 npm run dev 的时候 process.env.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)
}

相比于知道 Vue 的不同构建输出,我们更关心的是:不同的构建输出有什么区别,为什么要输出这么多不同的版本,有什么作用?

运行时版 + Compiler = 完整版。也就是说完整版比运行时版多了一个 Compiler(将字符串模板编译为 render 函数),大家想一想:将字符串模板编译为 render 函数的这个过程,是不是一定要在代码运行的时候再去做?当然不是,实际上这个过程在构建的时候就可以完成,这样真正运行的代码就免去了这样一个步骤,提升了性能。同时,将 Compiler 抽离为单独的包,还减小了库的体积。

那么为什么需要完整版呢?说白了就是允许你在代码运行的时候去现场编译模板,在不配合构建工具的情况下可以直接使用,但是更多的时候推荐你配合构建工具使用运行时版本。

除了运行时版与完整版之外,为什么还要输出不同形式的模块的包?比如 cjs、es 和 umd?其中:
* umd 是使得你可以直接使用

1.3 Vue.js的初始化

Vue构造函数的原型

我们分析是基于dev脚本的(即:npm run dev),也就是完整版的umd模块的Vue( web-full-dev )。从前面的分析可知,它的入口文件是 src/platforms/web/entry-runtime-with-compiler.js ,打开这个文件,可以看到:

import Vue from './runtime/index'

这说明,这个文件并不是Vue构造函数的“出生地”,我们应该看看runtime 目录下的 index.js文件,可以看到:

import Vue from 'core/index'

同理,根据scripts/alias.js 中的配置,获取core指向的路径src/core ,打开 src/core/index.js ,可以看到:

import Vue from './instance/index'

继续打开 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构造函数,使用了安全模式来提醒要使用 new 操作符调用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构造函数作为参数,分别传递给了导入进来的五个方法
//它们的功能都是给 Vue 的 prototype 上扩展一些方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
//最后导出Vue。
export default Vue

那么这五个方法又做了什么呢?在Vue.prototype上添加了一系列属性和方法(Vue构造函数整理-原型),可以先放下,理清整个框架结构之后,再分析细节。

Vue构造函数的静态属性和方法

到目前为止,core/instance/index.js 文件,也就是 Vue 的出生文件的代码我们就看完了,按照之前我们寻找 Vue 构造函数时的文件路径回溯,下一个我们要看的文件应该就是 core/index.js 文件,这个文件将 Vue 从 core/instance/index.js 文件中导入了进来,我们打开 core/index.js 文件,下面是其全部的代码:

// 从 Vue 的出生文件导入 Vue
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 构造函数作为参数,传递给 initGlobalAPI 方法,该方法来自 ./global-api/index.js 文件
//在 Vue 上添加一些全局的API
initGlobalAPI(Vue)

// 在 Vue.prototype 上添加 $isServer 属性,该属性代理了来自 core/util/env.js 文件的 isServerRendering 方法
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

// 在 Vue.prototype 上添加 $ssrContext 属性
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

/*
*在 Vue 构造函数上定义了 FunctionalRenderContext 静态属性,
*并且 FunctionalRenderContext 属性的值为来自 core/vdom/create-functional-component.js 文件的 FunctionalRenderContext,
*之所以在 Vue 构造函数上暴露该属性,是为了在 ssr 中使用它。
* */
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

// Vue.version 存储了当前 Vue 的版本号
/*
* 打开 scripts/config.js 文件,找到 genConfig 方法,其中有这么一句话:__VERSION__: version。这句话被写在了 rollup 的 replace 插件中,
* 也就是说,__VERSION__ 最终将被 version 的值替换,而 version 的值就是 Vue 的版本号。
* */
Vue.version = '__VERSION__'

// 导出 Vue
export default Vue

分析函数 initGlobalAPI :

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  /*
  * 在 Vue 构造函数上添加 config 属性,这个属性的添加方式类似我们前面看过的 $data 以及 $props,
  * 也是一个只读的属性,并且当你试图设置其值时,在非生产环境下会给你一个友好的提示。
  * */
  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

  Vue.options = Object.create(null)   //空的对象
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = 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
//将 builtInComponents 的属性混合到 Vue.options.components 中
  extend(Vue.options.components, builtInComponents)

  /*
  * 截止现在为止,Vue.options已经是:
  * Vue.options = {
      components: {
        KeepAlive
      },
      directives: Object.create(null),
      filters: Object.create(null),
      _base: Vue
    }
  * */

  initUse(Vue)    //在 Vue 构造函数上添加 use 方法,即 Vue.use 这个全局API,用来安装 Vue 插件。
  initMixin(Vue)
  initExtend(Vue)    //initExtend 方法在 Vue 上添加了 Vue.cid 静态属性,和 Vue.extend 静态方法。
  initAssetRegisters(Vue)

  /*
  * 经过 initAssetRegisters 方法,Vue 又多了三个静态方法全局注册组件,指令和过滤器:
  * Vue.component
    Vue.directive
    Vue.filter
  * */
}

对于 core/index.js 文件的作用我们也大概清楚了,在这个文件里,它首先将核心的 Vue,也就是在 core/instance/index.js 文件中的 Vue,也可以说是原型被包装(添加属性和方法)后的 Vue 导入,然后使用 initGlobalAPI 方法给 Vue 添加静态方法和属性,除此之外,在这个文件里,也对原型进行了修改,为其添加了两个属性: isServer i s S e r v e r 和 ssrContext,最后添加了 Vue.version 属性并导出了 Vue。Vue 构造函数整理-全局API。

Vue的平台化包装

我们在介绍 Vue 项目目录结构的时候说过:core 目录存放的是与平台无关的代码,所以无论是 core/instance/index.js 文件还是 core/index.js 文件,它们都在包装核心的 Vue,且这些包装是与平台无关的。但是,Vue 是一个 Multi-platform 的项目(web和weex),不同平台可能会内置不同的组件、指令,或者一些平台特有的功能等等,那么这就需要对 Vue 根据不同的平台进行平台化地包装,这就是接下来我们要看的文件,这个文件也出现在我们寻找 Vue 构造函数的路线上,它就是:platforms/web/runtime/index.js 文件。

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, isChrome } 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'
/*
*从 core/config.js 文件导出的 config 对象:
* Vue.config = {
    optionMergeStrategies: Object.create(null),
    silent: false,
    productionTip: process.env.NODE_ENV !== 'production',
    devtools: process.env.NODE_ENV !== 'production',
    performance: false,
    errorHandler: null,
    warnHandler: null,
    ignoredElements: [],
    keyCodes: Object.create(null),
    isReservedTag: no,
    isReservedAttr: no,
    isUnknownElement: no,
    getTagNamespace: noop,
    parsePlatformTagName: identity,
    mustUseProp: no,
    _lifecycleHooks: LIFECYCLE_HOOKS
  }
* */
// install platform specific utils
//覆盖默认导出的 config 对象的属性
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.options = {
    components: {
      KeepAlive,
      Transition,
      TransitionGroup
    },
    directives: {
      model,
      show
    },
    filters: Object.create(null),
    _base: Vue
  }
* */

// install platform patch function
//在 Vue.prototype 上添加 __patch__ 方法,
// 如果在浏览器环境运行的话,这个方法的值为 patch 函数,否则是一个空函数 noop。
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)
}

// devtools global hook
/* istanbul ignore next */
if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      if (devtools) {
        devtools.emit('init', Vue)
      } else if (
        process.env.NODE_ENV !== 'production' &&
        process.env.NODE_ENV !== 'test' &&
        isChrome
      ) {
        console[console.info ? 'info' : 'log'](
          'Download the Vue Devtools extension for a better development experience:\n' +
          'https://github.com/vuejs/vue-devtools'
        )
      }
    }
    if (process.env.NODE_ENV !== 'production' &&
      process.env.NODE_ENV !== 'test' &&
      config.productionTip !== false &&
      typeof console !== 'undefined'
    ) {
      console[console.info ? 'info' : 'log'](
        `You are running Vue in development mode.\n` +
        `Make sure to turn on production mode when deploying for production.\n` +
        `See more tips at https://vuejs.org/guide/deployment.html`
      )
    }
  }, 0)
}

export default Vue
/*
该文件的作用是对 Vue 进行平台化地包装:

设置平台化的 Vue.config。
在 Vue.options 上混合了两个指令(directives),分别是 model 和 show。
在 Vue.options 上混合了两个组件(components),分别是 Transition 和 TransitionGroup。
在 Vue.prototype 上添加了两个方法:__patch__ 和 $mount。
*/
with compiler

在看完 runtime/index.js 文件之后,其实 运行时 版本的 Vue 构造函数就已经“成型了”。我们可以打开 entry-runtime.js 这个入口文件,这个文件只有两行代码:

import Vue from './runtime/index'

export default Vue

完整版的 Vue,入口文件是 entry-runtime-with-compiler.js,我们知道完整版和运行时版的区别就在于 compiler,这个文件的作用:就是在运行时版的基础上添加 compiler,打开 entry-runtime-with-compiler.js 文件:

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

// 导入 运行时 的 Vue
import Vue from './runtime/index'
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

// 根据 id 获取元素的 innerHTML
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

// 使用 mount 变量缓存 Vue.prototype.$mount 方法
const mount = Vue.prototype.$mount
// 重写 Vue.prototype.$mount 方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to  or  - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

/**
 * 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
  }
}

// 在 Vue 上添加一个全局API `Vue.compile` 其值为上面导入进来的 compileToFunctions
Vue.compile = compileToFunctions

export default Vue
/*
* 这个文件运行下来,对 Vue 的影响有两个,
* 第一个影响是它重写了 Vue.prototype.$mount 方法;第二个影响是添加了 Vue.compile 全局API
* */

总结

梳理了完整的 Vue 构造函数,包括原型的设计和全局API的设计。细节函数的原理讲解也都在附录里有,其实两篇原文,一片着重脉络梳理,一篇着重知识点讲透彻,二者结合,真的非常容易理解了。

你可能感兴趣的:(vue源码学习,Vue)