Vue源码解析(一):入口文件
在学习Vue源码之前,首先要做的一件事情,就是去GitHub上将Vue源码clone下来,目前我这里分析的Vue版本是V2.5.21,下面开始分析:
一、源码的目录结构:
Vue的源码都在src目录下,分为6个不同功能的文件
src
├── compiler # 编译相关:包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能。
├── core # 核心代码:包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等
├── platforms # 不同平台的支持: 2个目录代表2个主要入口,分别打包成运行在web上和weex上的Vue.js
├── server # 服务端渲染:所有服务端渲染相关的逻辑,跑在服务端的Node.js,把组件渲染为服务器端的HTML字符串,将它们直接发送到浏览器
├── sfc # .vue 文件解析:把.vue文件内容解析成一个JavaScript的对象
├── shared # 共享代码:被浏览器端和服务端所共享的工具方法
二、源码的构建:
Vue源码是用rollup构建的,在package.json文件下,可以看到构建的脚本如下:
1 2 3 4 5 | "scripts" : { "build" : "node scripts/build.js" , "build:ssr" : "npm run build -- web-runtime-cjs,web-server-renderer" , "build:weex" : "npm run build -- weex" } |
按照脚本的文件路径,可以在scripts/build.js找到对应的构建文件,在scripts/build.js中,可以看到这样一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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配置文件读取配置,再通过process.argv[2]获取命令行参数,对构建配置做过滤,构建出不同用途的 Vue.js。
我们再来看一下最主要的逻辑scripts/config.js文件里面的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | const builds = { // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify 'web-runtime-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' : { 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' : { 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 }, // ... } |
这里的单个配置是遵循 Rollup 的构建规则:
entry:表示构建的入口 JS 文件地址
dest:表示构建后的 JS 文件地址
format:表示构建的格式(cjs:表示构建出来的文件遵循 CommonJS 规范;es:构建出来的文件遵循 ES Module 规范;umd:构建出来的文件遵循 UMD 规范)
三、Runtime Only vs Runtime + Compiler
Runtime Only:
我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。
Runtime + Compiler:
我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板,因为在 Vue.js 2.0 中,最终渲染都是通过 render
函数,如果写 template
属性,则需要编译成 render
函 数,那么这个编译过程会发生运行时,所以需要带有编译器的版本,很显然,这个编译过程对性能会有一定损耗,所以通常我们更推荐使用 Runtime-Only 的 Vue.js。
四、源码入口:
在 web 应用下,我们来分析 Runtime + Compiler 构建出来的 Vue.js,它的入口是 src/platforms/web/entry-runtime-with-compiler.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | /* @flow */ import config from 'core/config' import { warn, cached } from 'core/util/index' import { mark, measure } from 'core/util/perf' import Vue from './runtime/index' import { query } from './util/index' import { compileToFunctions } from './compiler/index' import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat' const idToTemplate = cached(id => { const el = query(id) return el && el.innerHTML }) const 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 <html> or <body> - 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.compile = compileToFunctions export default Vue |
上面这个文件首先import Vue from './runtime/index'导入了vue对象,然后对其prototype做了一些操作,所以我们找到'./runtime/index'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | /* @flow */ 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' // install platform patch function 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) } export default Vue |
从上面这个文件中可以看到,import Vue from 'core/index'导入vue,所以我们再去'core/index'找到vue,
在'core/index'中,initGlobalAPI定义了一些vue的全局的API的方法,可以在'src/core/global-api/index.js'文件里面找到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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' initGlobalAPI(Vue) Object.defineProperty(Vue.prototype, '$isServer' , { get: isServerRendering }) Object.defineProperty(Vue.prototype, '$ssrContext' , { get () { /* istanbul ignore next */ return this .$vnode && this .$vnode.ssrContext } }) // expose FunctionalRenderContext for ssr runtime helper installation Object.defineProperty(Vue, 'FunctionalRenderContext' , { value: FunctionalRenderContext }) Vue.version = '__VERSION__' export default Vue |
从以上文件中,终于看到初始化vue的API的代码!
然后我们去'./instance/index'找到Vue,它实际上就是一个用Function实现的类,我们使用的时候就是通过new Vue实例化的,后面很多xxxMixin的函数调用,它们的功能都是给 Vue 的 prototype 上扩展一些方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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' 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 |
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步