准备工作
-
下载
vue源码,可以先将vue项目fork到自己的github仓库,然后在clone自己仓库的vue,这样在解读源码的时候可以随时添加注释,并将注释提交到自己的仓库。 -
源码代码主要结构说明:
dist:打包生成的文件examples:实例代码目录src:源码文件目录compiler:编译器相关代码,把template模板转化成render函数core:核心代码components:定义vue自带的keep-alive组件global-api:定义vue中的静态方法,包括mixin、extend、use等instance:创建vue实例成员,包括构造函数、初始化和生命周期函数。observer:响应式实现util:工具方法vdom:虚拟DOM实现,重写了Snabbdom,增加了组件的机制。
platforms:平台相关处理web:web平台weex:基于vue的移动端框架
server:服务器端渲染sfc:单文件组件,将单文件组件转换成js模块
-
Vue2.x使用了Flow来进行代码静态类型检查。 -
Vue使用rollup进行打包,相对于webpack来说,rollup打包不会生成冗余代码。rollup命令行参数说明:-
-w:开启监视模式 -
-c:指定配置文件 -
--sourcemap:开启sourceMap功能,之后能在浏览器中的源码中看到源码对应的src文件夹

-
--environment:指定运行环境参数,TARGET指定打包生成的版本。"rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
-
-
Vue构建版本之间的差别

术语说明:
完整版(Full): 同时包含编译器和运行时的版本
编译器: 将模板字符串编译成javascript渲染函数的代码
运行时(Runtime-only): 用来创建Vue实例、渲染并处理虚拟DOM等的代码。基本上就是除去编译器的其它一切。运行时版本体积小,运行效率更高。
UMD: 可以通过<script>标签直接用在浏览器中。
CommonJS:CommonJS版本用来配合老的打包工具比如Browserify或webpack 1。这些打包工具的默认文件(pkg.main)是只包含运行时的CommonJS版本
ES Module: 会提供两个ES Modules (ESM)构建文件:- 为打包工具提供的
ESM:为诸如webpack 2或Rollup提供的现代打包工具。ESM格式被设计为可以被静态分析(编译时解析模块依赖),所以打包工具可以利用这一点来进行tree-shaking并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件pkg.module是只有运行时的ES Module构建(vue.runtime.esm.js)。 - 为浏览器提供的
ESM (2.6+):用于在现代浏览器中通过<script type="module">直接导入。
- 为打包工具提供的
-
寻找入口文件(以
dev命令为例)-
根据
package.json中的命令行找到配置文件

-
找到配置文件中最后的导出语句,然后向前解析找到最后导出的内容
// 判断环境变量是否有 TARGET // 如果有的话 使用 genConfig() 生成 rollup 配置文件 if (process.env.TARGET) { // 如果有 TARGET 则打包生成指定的目标版本 module.exports = genConfig(process.env.TARGET) } else { // 如果没有则打包生成所有版本 exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }这里关键是是
genConfig方法,查看方法,定位到builds,builds存储了不同TARGET对应的配置。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 }, }resolve是用来解析模块路径中可能存在的alias别名,并返回一个绝对路径。const resolve = p => { // 根据路径中的前半部分去alias中找别名 const base = p.split('/')[0] if (aliases[base]) { return path.resolve(aliases[base], p.slice(base.length + 1)) } else { return path.resolve(__dirname, '../', p) } }alias别名配置module.exports = { vue: resolve('src/platforms/web/entry-runtime-with-compiler'), compiler: resolve('src/compiler'), core: resolve('src/core'), shared: resolve('src/shared'), web: resolve('src/platforms/web'), weex: resolve('src/platforms/weex'), server: resolve('src/server'), sfc: resolve('src/sfc') } -
根据配置找到入口文件,从入口文件开始解读。
-
-
按照上面的方法找到入口文件
entry-runtime-with-compiler.js,通过阅读入口文件了解到该文件的主要功能是重写Vue的$mount方法,用来渲染DOM。通过源码的阅读可以知道以下几处注意事项:-
Vue实例创建时传入的el不能是body或html// el 不能是 body 或者 html 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 } -
创建
Vue实例时如果同时传入了template和render,则会忽略template

-
通过调试确定
$mount是在什么时候使用的。编译时开启sourceMap,直接在浏览器中定位到入口文件,并打下断点,刷新浏览器,运行到断点处,可以看到右侧Call Stack调用栈中当前正在执行Vue.$mount,下面一行则是调用Vue.$mount的运行环境,即在Vue.init中,一直向下可以追溯到Vue实例的创建的执行环境。在调用栈中可以查看函数的调用过程。

-
-
根据导入的
Vue查看runtime/index.js-
为
vue.config定义一些属性。// 判断是否是关键属性(表单元素的 input/checked/selected/muted) // 如果是这些属性,设置el.props属性(属性不设置到标签上) Vue.config.mustUseProp = mustUseProp Vue.config.isReservedTag = isReservedTag Vue.config.isReservedAttr = isReservedAttr Vue.config.getTagNamespace = getTagNamespace Vue.config.isUnknownElement = isUnknownElement -
定义平台环境上的指令
(v-show、v-modal)和组件(Transition,TransitionGroup)// install platform runtime directives & components extend(Vue.options.directives, platformDirectives) extend(Vue.options.components, platformComponents) -
定义
patch方法Vue.prototype.__patch__ = inBrowser ? patch : noop -
定义原型上的
$mount方法// 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的init钩子,并对运行环境的不足做出提示。if (inBrowser) { setTimeout(() => { if (config.devtools) { if (devtools) { devtools.emit('init', Vue) } else if ( process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' ) { 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) }
-
-
Vue的初始化:通过runtime/index.js中依赖的追溯,找到core/index.js,这个文件中定义了Vue的初始化的过程。-
初始化
Vue的静态成员。initGlobalAPI(Vue)-
初始化
Vue.config对象// config 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.' ) } } // 初始化 Vue.config 对象 Object.defineProperty(Vue, 'config', configDef) -
暴露
util方法,但不将其作为Vue API的一部分,尽量不要依赖他们// exposed util methods. // NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk. // 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们 Vue.util = { warn, extend, mergeOptions, defineReactive } -
定义静态
set、del和nextTick方法// 静态方法 set/delete/nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTickset为对象设置为响应式的属性,实现原理解析:function set (target: Array<any> | Object, key: any, val: any): any { }-
确保
target是对象或数组if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${ (target: any)}`) } -
target是数组时,确保key是有效索引,并扩展数组长度,使数组包含key索引,通过splice方法设置值。这里的splice方法是经过处理的方法,在方法内部会调用dep.notify()发送通知。// 判断 target 是否是数组,key 是否是合法的索引 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // 扩展数组长度 // 通过 splice 对key位置的元素进行替换 // splice 在 array.js 进行了响应化的处理 target.splice(key, 1, val) return val } -
key是target对象中的属性,直接赋值。// 如果 key 在对象中已经存在直接赋值 if (key in target && !(key in Object.prototype)) { target[key] = val return val } -
如果
target是vue实例或$data,直接返回// 获取 target 中的 observer 对象 const ob = (target: any).__ob__ // 如果 target 是 vue 实例或者 $data 直接返回 if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } -
target不是响应式的对象,直接赋值
-
-
-

本文深入探讨Vue2.x的响应式原理,从源码角度解析如何实现数据变化到视图更新的响应式过程。涉及内容包括源码结构、构建版本差异、术语解释以及关键方法如、的实现细节。通过对、和等核心概念的解读,揭示Vue实例创建、数据响应化、依赖收集、数组响应式处理以及计算属性、侦听器和用户自定义的处理方式。
最低0.47元/天 解锁文章
1123

被折叠的 条评论
为什么被折叠?



