Vue2源码系列-开篇

202 阅读6分钟

前言

大概两年前有学习过vue源码,当时学的比较粗糙,学习到的东西也比较少,差不多都快忘完了。最近打算再次捡起来,同时希望通过博客的方式加深理解和记忆,更希望能遇到一起交流的小伙伴~

Flow

本次分析的 2.6 版本是使用 flow 作为类型检查工具的,其主要的作用是对 JS 变量进行类型注释和类型推断,弥补JS作为一门弱类型语言的不足。感兴趣的朋友可以在官网上进行学习和了解。在 vue 中,有专门的 flow 文件夹对不同的变量类型进行定义,比如我们后面在源码中常见的 VNodeData

// flow/vnode.js
declare interface VNodeData {
  key?: string | number;
  slot?: string;
  ref?: string;
  is?: string;
  pre?: boolean;
  tag?: string;
  staticClass?: string;
  class?: any;
  staticStyle?: { [key: string]: any };
  style?: string | Array<Object> | Object;
  normalizedStyle?: Object;
  props?: { [key: string]: any };
  attrs?: { [key: string]: string };
  domProps?: { [key: string]: any };
  hook?: { [key: string]: Function };
  on?: ?{ [key: string]: Function | Array<Function> };
  nativeOn?: { [key: string]: Function | Array<Function> };
  transition?: Object;
  show?: boolean; // marker for v-show
  inlineTemplate?: {
    render: Function;
    staticRenderFns: Array<Function>;
  };
  directives?: Array<VNodeDirective>;
  keepAlive?: boolean;
  scopedSlots?: { [key: string]: Function };
  model?: {
    value: any;
    callback: Function;
  };
};

构建入口

我们可以先看看 package.json 中的构建脚本命令

// package.json
{
  "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
  "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
  "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
  "dev:test": "karma start test/unit/karma.dev.config.js",
  "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 ",
  "dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
  "dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
  "dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
  "build": "node scripts/build.js",
  "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
  "build:weex": "npm run build -- weex",
  "test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",
  "test:unit": "karma start test/unit/karma.unit.config.js",
  "test:cover": "karma start test/unit/karma.cover.config.js",
  "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.js",
  "test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.js",
  "test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.js",
  "test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
  "test:types": "tsc -p ./types/test/tsconfig.json",
  "lint": "eslint src scripts test",
  "flow": "flow check",
  "sauce": "karma start test/unit/karma.sauce.config.js",
  "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
  "release": "bash scripts/release.sh",
  "release:weex": "bash scripts/release-weex.sh",
  "release:note": "node scripts/gen-release-note.js",
  "commit": "git-cz"
}

太多了有木有,根本学不过来。我们主要简单地了解下vue使用的打包工具不是 webpack,而是 rolluprollup 作为一个轻量级的打包工具,可以很好的处理JS代码打包,使其成为很多JS库打包首选。还有一个知识点就是,vue可以打包成多种不同版本,包含 weex平台运行版本,SSR服务端渲染版本,esm模块化版本,带编译的版本等等。

接下来我们大概看看vue的打包过程,是如何实现的

// scripts/build.js

// 首先从 `config.js` 获取所有的打包版本配置
let builds = require('./config').getAllBuilds()

// 接着对命令行参数进行解析处理,通过命令行参数匹配过滤得到需要打包的版本
// filter builds via command line arg
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
  })
}

关于版本配置我们也可以简单看看

// scripts/config.js

// 可以很清楚地看到不同版本的入口文件 `entry` 和打包路径 `dest`
const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  },
  'web-runtime-cjs-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.prod.js'),
    format: 'cjs',
    env: 'production',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.dev.js'),
    format: 'cjs',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  'web-full-cjs-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.prod.js'),
    format: 'cjs',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // Runtime+compiler ES modules build (for bundlers)
  '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+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.js'),
    format: 'es',
    transpile: false,
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.min.js'),
    format: 'es',
    transpile: false,
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    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': {
    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': {
    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': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Web compiler (CommonJS).
  'web-compiler': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
  },
  // Web compiler (UMD for in-browser use).
  'web-compiler-browser': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/browser.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'VueTemplateCompiler',
    plugins: [node(), cjs()]
  },
  // Web server renderer (CommonJS).
  'web-server-renderer-dev': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.dev.js'),
    format: 'cjs',
    env: 'development',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-prod': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.prod.js'),
    format: 'cjs',
    env: 'production',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-basic': {
    entry: resolve('web/entry-server-basic-renderer.js'),
    dest: resolve('packages/vue-server-renderer/basic.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'renderVueComponentToString',
    plugins: [node(), cjs()]
  },
  'web-server-renderer-webpack-server-plugin': {
    entry: resolve('server/webpack-plugin/server.js'),
    dest: resolve('packages/vue-server-renderer/server-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-webpack-client-plugin': {
    entry: resolve('server/webpack-plugin/client.js'),
    dest: resolve('packages/vue-server-renderer/client-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  // Weex runtime factory
  'weex-factory': {
    weex: true,
    entry: resolve('weex/entry-runtime-factory.js'),
    dest: resolve('packages/weex-vue-framework/factory.js'),
    format: 'cjs',
    plugins: [weexFactoryPlugin]
  },
  // Weex runtime framework (CommonJS).
  'weex-framework': {
    weex: true,
    entry: resolve('weex/entry-framework.js'),
    dest: resolve('packages/weex-vue-framework/index.js'),
    format: 'cjs'
  },
  // Weex compiler (CommonJS). Used by Weex's Webpack loader.
  'weex-compiler': {
    weex: true,
    entry: resolve('weex/entry-compiler.js'),
    dest: resolve('packages/weex-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
  }
}

接着我们继续分析下获取配置文件后的打包流程。

// scripts/build.js

build(builds)

// 对不同的打包版本进行遍历调用buildEntry
function build (builds) {
  let built = 0
  const total = builds.length
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }

  next()
}

// 在buildEntry中可以看到rollup开始打包工作,具体rollup的打包配置及打包原理这边不作学习
function buildEntry (config) {
  const output = config.output
  const { file, banner } = output
  const isProd = /(min|prod)\.js$/.test(file)
  return rollup.rollup(config)
    .then(bundle => bundle.generate(output))
    .then(({ output: [{ code }] }) => {
      if (isProd) {
        // 打包后的生产代码插入banner,就是在文件头部包含版本号,日期的那段注释,可以看看
        // scripts/config.js
        // const banner =
        //   '/*!\n' +
        //   ` * Vue.js v${version}\n` +
        //   ` * (c) 2014-${new Date().getFullYear()} Evan You\n` +
        //   ' * Released under the MIT License.\n' +
        //   ' */'

        const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
          toplevel: true,
          output: {
            ascii_only: true
          },
          compress: {
            pure_funcs: ['makeMap']
          }
        }).code
        return write(file, minified, true)
      } else {
        return write(file, code)
      }
    })
}

打包的流程我们就分析到这,主要是了解下 vue 打包不同版本的实现。

目录结构

vue的核心源码在src目录下,我们先来看看源码的主要目录结构

src
├── compiler        # 编译相关 
├── core            # 核心代码 (组件化,全局API,实例,VDOM,数据监听)
├── platforms       # 不同平台的支持(包括web/weex)
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码

准备工作

在学习源码之前,我们先准备好我们本地的调试环境吧,首先通过 vue create xxx 新建vue项目,同时通过 git clone 在项目中下载 vue 项目源码, 然后再修改 main.js

// src/main.js

// 在入口文件中我们修改 Vue 的来源,可以引用 node_modules 中的 vue/dist 目录,也可以像我一样引用自己 clone 下来的 vue 库
import Vue from '../vue/dist/vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

修改引用文件为我自己 clone 下来的 vue 库的时候,遇到一些坑

  • 首先就是我的编辑器 vscode 打开 vue 源码的时候会报 ts 校验的错误,通过修改 vscode 的配置文件处理好了
"typescript.validate.enable": false,
"javascript.validate.enable": false
  • 其次就是一直报找不到模块 eslint-plugin-flowtype 的错误,按理说这个错误是在 vue/.eslintrc.js 中的报的,但是后面我在外面的 vue 项目中也安装上了 eslint-plugin-flowtype 才解决的报错,怀疑是它找的是外层项目的 node_modules,具体原因也没去了解

  • 在解决报错的过程中,我也为 vscode 中安装了一些支持 flow 的插件,同时屏蔽了项目 eslint 对于 vue 源码项目的检查

// .eslintignore
vue

现在项目的目录结构是

xxx
├── node_modules        
├── public            
├── src      
├── vue       # vue为我们自己 clone 下来的源码
├── .eslintignore             
├── package.js         
├── ...       

至此,我们可以通过 npm install 安装好依赖,再开启运行项目 npm run serve,运行 vue 源码,npm run dev 来进行源码调试了