Vue 源码解析

一、FLow类型检测

  • 安装Flow

    • npm install flow-bin
  • 初始化flow项目

    • flow init
  • flow检查项目 flow 文件

  • /*@flow*/
    接下去的代码就需要flow检查了
    

二、Vue目录源码设计

  • 源码GitHub路径

  • Vue.js 的源码都在 src 目录下,其目录结构如下。

    • src
      ├── compiler        # 编译相关 
      ├── core            # 核心代码 
      ├── platforms       # 不同平台的支持
      ├── server          # 服务端渲染
      ├── sfc             # .vue 文件解析
      ├── shared          # 共享代码
      
    • compiler

      • Vue2.0有一个改变,添加了watch dom,watch dom生成实际执行的是render function,平时很少写render function 大部分写template ,将template转成render function,就在complier 中。编译的代码都在complier目录下
    • core

      • component:内部存放了很多组件,比如keep-alive
      • global-api:全局api
      • instance:存放的是渲染的一些函数,事件、初始化、生命周期
      • observer:跟响应式数据相关
      • util:存放工具方法
      • vdom:watch dom 核心代码放到这里
    • platforms

      • web:浏览器程序
        • complier:平台相关的编译
        • runtime:平台运行代码
        • server:平台相关的server render代码
        • util:辅助的工具函数
      • weex:混合app
    • server:跟服务端渲染的相关代码

    • sfc:vue的解析器,将单文件(.vue)编译成一个对象

    • shared:辅助的方法,比如以下常亮和工具方法

    通过模块化方式,这让这些相互引用,再通过编译工具(Rollup)生成独立的js

三、源码构建

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

    • Rollup 和webpack区别:
      • webpack将静态资源全部编译成js,而Rollup适合js库的编译,只处理js,不管理其他文件,更加轻量
    • package.json
      • mian:npm包入口,import vue,从这里查找vue文件路径
      • module:webpack2以上,将module将这个当做默认入口
      • scripts:
        • 构建项目的执行命令有:build、build:ssr、build:weex
          • "build": "node scripts/build.js":编译成web相关的
          • "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer":输出和renderer相关的
          • "build:weex": "npm run build -- weex":输出和weex相关的
  • 构建过程:

    • scripts/build.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
            })
          }
          
        • build(builds):执行构建

    • 分析如何拿到配置文件当前目录下的config.js

      • if (process.env.TARGET) {
          module.exports = genConfig(process.env.TARGET)
        } else {
          exports.getBuild = genConfig
          exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
        }
        
        • 暴露一个方法getAllBuilds,通过Object.keys(builds)拿到builds的所有keys形成数组,map遍历

        • builds是什么:

          • 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)
              }
            }
            
            • builds的可以是不同版本的vuejs编译配置
              • entry:表示入口文件
              • dest:输出目标
              • format:构建出来的文件格式
              • banner:添加注释,版本,创建这之类的
          • genConfig方式,将builds配置转换成Rollup的配置文件格式

          • 最终 let builds = require('./config').getAllBuilds():拿到相关配置文件数组

        • 继续返回build.js

          • // 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
              })
            }
            
            • process.argv[2] 拿到的scripts配置文件的参数
              • 比如:"build:weex": "npm run build -- weex":process.argv[2] 拿到的是weex这个参数
            • filter 方法,根据参数,将不要的配置给过滤掉,如果没有参数的话,将weex给过滤掉,也就是打包web平台
          • 编译的方法build()

            • function build (builds) {
                // 计数器,用于取出配置文件
                let built = 0
                // 配置文件的个数
                const total = builds.length
                // 循环配置文件执行编译
                const next = () => {
                  buildEntry(builds[built]).then(() => {
                    built++
                    if (built < total) {
                      next()
                    }
                  }).catch(logError)
                }
              
                next()
              }
              
              /**
               * Rollup打包构建
               * @param config 配置文件
               * @returns {Promise<RollupOutput | never>}
               */
              function buildEntry (config) {
                // 输出文件配置
                /*
                  const config = {
                    input: opts.entry,
                    external: opts.external,
                    plugins: [
                      flow(),
                      alias(Object.assign({}, aliases, opts.alias))
                    ].concat(opts.plugins || []),
                    output: {
                      file: opts.dest,
                      format: opts.format,
                      banner: opts.banner,
                      name: opts.moduleName || 'Vue'
                    },
                    onwarn: (msg, warn) => {
                      if (!/Circular/.test(msg)) {
                        warn(msg)
                      }
                    }
                  }
                */
                const output = config.output
                // file:输出文件名和路径  banner:添加注释,版本,创建这之类的
                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) {
                      const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
                        toplevel: true,
                        output: {
                          ascii_only: true
                        },
                        compress: {
                          pure_funcs: ['makeMap']
                        }
                      }).code
                      // 将编译好的代码写入对应的file指定的路径并命名指定的文件名
                      return write(file, minified, true)
                    } else {
                      return write(file, code)
                    }
                  })
              }
              
              /**
               * // 将编译好的代码写入对应的file指定的路径并命名指定的文件名
               * @param dest 输出的路径和名称
               * @param code 代码
               * @param zip 是否压缩
               * @returns {Promise<any>}
               */
              function write (dest, code, zip) {
                return new Promise((resolve, reject) => {
                  function report (extra) {
                    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()
                    }
                  })
                })
              }
              
  * Runtime Only 和Runtime + Complier比较

    * Runtime Only:将template模板编译成render函数,编译后是reader函数版本,运行时候不带编译,编译在离线时候做

    * Runtime + Complier:可以不对代码做预编译,不使用vue单文件方式,可以在vue在运行时候,将template编译成render函数

      * ```js
        // 需要编译器的版本(Runtime + Complier)
        new Vue({
          template: '<div>{{ hi }}</div>'
        })
        
        // 这种情况不需要编译器的版本,只需要Runtime Only就行了
        new Vue({
          render (h) {
            return h('div', this.hi)
          }
        })
        ```

    * 我们写的.vue 文件,在真正运行时候,早就编译成了javascript函数了,并且template模板已经编译成了render函数了

  * 将目标编译,是很耗性能的,所以开发阶段建议使用Runtime Only

四、从入口开始 import 开始

  • 我们研究的是Runtime + Complier 版本

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

  • 从入口文件,按照Vue的路径,一路找到了 src/core/instance/index.js

    • 路径:src/platforms/web/entry-runtime-with-compiler.js ➡️ src/platforms/web/runtime/index.js ➡️ src/core/index.js ➡️ src/core/instance/index.js

    • 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'
        
        // ES5 实现的 Class
        function Vue (options) {
          if (process.env.NODE_ENV !== 'production' &&
            // 这个就是强制实行new Vue,直接调用会提示下面的警告的
            !(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
        
        
        • initMixin方法:向原型上挂载_init 方法
        • stateMixin方法:向原型挂载set、delete、$watch等方法
        • 为什么Vue使用ES5实现而不是ES6实现:因为,哪些Mixin方式,实际是向Vue原型上挂在方法,如果用ES6就做不到将挂载的方法拆分出来管理,这种做法有利于代码管理,按照模块的组织关系把Vue的原型拆分到不同的文件中
      • src/core/index.js

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

推荐阅读更多精彩内容

  • 大家都知道,阅读源码可以帮助自己成长。源码解析的文章也看了不少,但是好记性不如烂笔头,看过的东西过段时间就忘的差不...
    snow_in阅读 4,678评论 0 7
  • 前言 大家看源码是不是都有一种感觉,就是很多东西都是看的时候当时记住了,过段时间不去使用或者回顾又忘记了,所以现在...
    flowsands阅读 4,209评论 0 55
  • 我是一个无比平凡的女孩,生于乾隆年间
    雾可可阅读 354评论 0 1
  • 雨把这个节气塞得满满当当 风得了病,刚刚 轻轻柔柔,忽然怒了 把树折断,把庄稼按倒 上房掀着瓦片 一片狼藉,是它的...
    2b4c67af34a7阅读 179评论 2 1
  • 甘德礼(幸福成长宣言)持续原创分享第27天。 今天看到焦点少年的《小田有话说》,他解析的这首焦点诗真的很棒!在此推...
    华南帝虎阅读 384评论 0 2