本来文章的名字叫作《源码解析》,不事后来想一想,仍是用“源码学习”来的合适一点,在没有完全掌握源码中的每个字母以前,“解析”就有点标题党了。建议在看这篇文章以前,最好打开2.1.7的源码对照着看,这样可能更容易理解。另外本人水平有限,文中有错误或不妥的地方望你们多多指正共同成长。html
补充:Vue 2.2 刚刚发布,做为一个系列文章的第一篇,本篇文章主要从Vue代码的组织,Vue构造函数的还原,原型的设计,以及参数选项的处理和已经被写烂了的数据绑定与如何使用 Virtual DOM 更新视图入手。从总体的大方向观察框架,这么看来 V2.1.7
对于理解 V2.2
的代码不会有太大的影响。该系列文章的后续文章,都会从最新的源码入手,并对改动的地方作相应的提示。前端
好久以前写过一篇文章:JavaScript实现MVVM之我就是想监测一个普通对象的变化,文章开头提到了我写博客的风格,仍是那句话,只写努力让小白,甚至是小学生都能看明白的文章。这难免会致使对于某些同窗来讲这篇文章有些墨迹,因此你们根据本身的喜爱,能够详细的看,也能够跳跃着看。vue
要看一个项目的源码,不要一上来就看,先去了解一下项目自己的元数据和依赖,除此以外最好也了解一下 PR 规则,Issue Reporting 规则等等。特别是“前端”开源项目,咱们在看源码以前第一个想到的应该是:package.json
文件。node
在 package.json
文件中,咱们最应该关注的就是 scripts
字段和 devDependencies
以及 dependencies
字段,经过 scripts
字段咱们能够知道项目中定义的脚本命令,经过 devDependencies
和 dependencies
字段咱们能够了解项目的依赖状况。webpack
了解了这些以后,若是有依赖咱们就 npm install
安装依赖就ok了。git
除了 package.json
以外,咱们还要阅读项目的贡献规则文档,了解如何开始,一个好的开源项目确定会包含这部份内容的,Vue也不例外:https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md,在这个文档里说明了一些行为准则,PR指南,Issue Reporting 指南,Development Setup 以及 项目结构。经过阅读这些内容,咱们能够了解项目如何开始,如何开发以及目录的说明,下面是对重要目录和文件的简单介绍,这些内容你均可以去本身阅读获取:github
├── build --------------------------------- 构建相关的文件,通常状况下咱们不须要动 ├── dist ---------------------------------- 构建后文件的输出目录 ├── examples ------------------------------ 存放一些使用Vue开发的应用案例 ├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/) ├── package.json -------------------------- 不解释 ├── test ---------------------------------- 包含全部测试文件 ├── src ----------------------------------- 这个是咱们最应该关注的目录,包含了源码 │ ├── entries --------------------------- 包含了不一样的构建或包的入口文件 │ │ ├── web-runtime.js ---------------- 运行时构建的入口,输出 dist/vue.common.js 文件,不包含模板(template)到render函数的编译器,因此不支持 `template` 选项,咱们使用vue默认导出的就是这个运行时的版本。你们使用的时候要注意 │ │ ├── web-runtime-with-compiler.js -- 独立构建版本的入口,输出 dist/vue.js,它包含模板(template)到render函数的编译器 │ │ ├── web-compiler.js --------------- vue-template-compiler 包的入口文件 │ │ ├── web-server-renderer.js -------- vue-server-renderer 包的入口文件 │ ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数 │ │ ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码 │ │ ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码 │ │ ├── optimizer.js ------------------ 分析静态树,优化vdom渲染 │ ├── core ------------------------------ 存放通用的,平台无关的代码 │ │ ├── observer ---------------------- 反应系统,包含数据观测的核心代码 │ │ ├── vdom -------------------------- 包含虚拟DOM建立(creation)和打补丁(patching)的代码 │ │ ├── instance ---------------------- 包含Vue构造函数设计相关的代码 │ │ ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码 │ │ ├── components -------------------- 包含抽象出来的通用组件 │ ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码 │ ├── platforms ------------------------- 包含平台特有的相关代码 │ ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包 │ ├── shared ---------------------------- 包含整个代码库通用的代码
大概了解了重要目录和文件以后,咱们就能够查看 Development Setup 中的经常使用命令部分,来了解如何开始这个项目了,咱们能够看到这样的介绍:web
# watch and auto re-build dist/vue.js $ npm run dev # watch and auto re-run unit tests in Chrome $ npm run dev:test
如今,咱们只须要运行 npm run dev
便可监测文件变化并自动从新构建输出 dist/vue.js,而后运行 npm run dev:test
来测试。不过为了方便,我会在 examples
目录新建一个例子,而后引用 dist/vue.js 这样,咱们能够直接拿这个例子一边改Vue源码一边看本身写的代码想怎么玩怎么玩。算法
在真正步入源码世界以前,我想简单说一说看源码的技巧:npm
当你看一个项目代码的时候,最好是能找到一条主线,先把大致流程结构摸清楚,再深刻到细节,逐项击破,拿Vue举个栗子:假如你已经知道Vue中数据状态改变后会采用virtual DOM的方式更新DOM,这个时候,若是你不了解virtual DOM,那么听我一句“暂且不要去研究内部具体实现,由于这会是你丧失主线”,而你仅仅须要知道virtual DOM分为三个步骤:
1、createElement(): 用 JavaScript对象(虚拟树) 描述 真实DOM对象(真实树)
2、diff(oldNode, newNode) : 对比新旧两个虚拟树的区别,收集差别
3、patch() : 将差别应用到真实DOM树
有的时候 第二步 可能与 第三步 合并成一步(Vue 中的patch就是这样),除此以外,还好比 src/compiler/codegen
内的代码,可能你不知道他写了什么,直接去看它会让你很痛苦,可是你只须要知道 codegen 是用来将抽象语法树(AST)生成render函数的就OK了,也就是生成相似下面这样的代码:
function anonymous() { with(this){return _c('p',{attrs:{"id":"app"}},[_v("\n "+_s(a)+"\n "),_c('my-com')])} }
当咱们知道了一个东西存在,且知道它存在的目的,那么咱们就很容易抓住这条主线,这个系列的第一篇文章就是围绕大致主线展开的。了解大致以后,咱们就知道了每部份内容都是作什么的,好比 codegen 是生成相似上面贴出的代码所示的函数的,那么再去看codegen下的代码时,目的性就会更强,就更容易理解。
balabala一大堆,开始来干货吧。咱们要作的第一件事就是搞清楚 Vue 构造函数究竟是什么样子的。
咱们知道,咱们要使用 new
操做符来调用 Vue
,那么也就是说 Vue
应该是一个构造函数,因此咱们第一件要作的事儿就是把构造函数先扒的一清二楚,如何寻找 Vue
构造函数呢?固然是从 entry 开始啦,还记的咱们运行 npm run dev
命令后,会输出 dist/vue.js
吗,那么咱们就去看看 npm run dev
干了什么:
"dev": "TARGET=web-full-dev rollup -w -c build/config.js",
首先将 TARGET 得值设置为 ‘web-full-dev’,而后,而后,而后若是你不了解 rollup 就应该简单去看一下啦……,简单的说就是一个JavaScript模块打包器,你能够把它简单的理解为和 webpack 同样,只不过它有他的优点,好比 Tree-shaking (webpack2也有),但一样,在某些场景它也有他的劣势。。。废话很少说,其中 -w
就是watch,-c
就是指定配置文件为 build/config.js
,咱们打开这个配置文件看一看:
// 引入依赖,定义 banner ... // builds 对象 const builds = { ... // Runtime+compiler development build (Browser) 'web-full-dev': { entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'), dest: path.resolve(__dirname, '../dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner }, ... } // 生成配置的方法 function genConfig(opts){ ... } if (process.env.TARGET) { module.exports = genConfig(builds[process.env.TARGET]) } else { exports.getBuild = name => genConfig(builds[name]) exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name])) }
上面的代码是简化过的,当咱们运行 npm run dev
的时候 process.env.TARGET
的值等于 ‘web-full-dev’,因此
module.exports = genConfig(builds[process.env.TARGET])
这句代码至关于:
module.exports = genConfig({ entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'), dest: path.resolve(__dirname, '../dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner })
最终,genConfig 函数返回一个 config 对象,这个config对象就是Rollup的配置对象。那么咱们就不难看到,入口文件是:
src/entries/web-runtime-with-compiler.js
咱们打开这个文件,不要忘了咱们的主题,咱们在寻找Vue构造函数,因此当咱们看到这个文件的第一行代码是:
import Vue from './web-runtime'
这个时候,你就应该知道,这个文件暂时与你无缘,你应该打开 web-runtime.js
文件,不过当你打开这个文件时,你发现第一行是这样的:
import Vue from 'core/index'
依照此思路,最终咱们寻找到Vue构造函数的位置应该是在 src/core/instance/index.js
文件中,其实咱们猜也猜获得,上面介绍目录的时候说过:instance 是存放Vue构造函数设计相关代码的目录。总结一下,咱们寻找的过程是这样的:
咱们回头看一看 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' 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
引入依赖,定义 Vue 构造函数,而后以Vue构造函数为参数,调用了五个方法,最后导出 Vue。这五个方法分别来自五个文件:init.js
state.js
render.js
events.js
以及 lifecycle.js
。
打开这五个文件,找到相应的方法,你会发现,这些方法的做用,就是在 Vue 的原型 prototype 上挂载方法或属性,经历了这五个方法后的Vue会变成这样:
// initMixin(Vue) src/core/instance/init.js ************************************************** Vue.prototype._init = function (options?: Object) {} // stateMixin(Vue) src/core/instance/state.js ************************************************** Vue.prototype.$data Vue.prototype.$set = set Vue.prototype.$delete = del Vue.prototype.$watch = function(){} // renderMixin(Vue) src/core/instance/render.js ************************************************** Vue.prototype.$nextTick = function (fn: Function) {} Vue.prototype._render = function (): VNode {} Vue.prototype._s = _toString Vue.prototype._v = createTextVNode Vue.prototype._n = toNumber Vue.prototype._e = createEmptyVNode Vue.prototype._q = looseEqual Vue.prototype._i = looseIndexOf Vue.prototype._m = function(){} Vue.prototype._o = function(){} Vue.prototype._f = function resolveFilter (id) {} Vue.prototype._l = function(){} Vue.prototype._t = function(){} Vue.prototype._b = function(){} Vue.prototype._k = function(){} // eventsMixin(Vue) src/core/instance/events.js ************************************************** Vue.prototype.$on = function (event: string, fn: Function): Component {} Vue.prototype.$once = function (event: string, fn: Function): Component {} Vue.prototype.$off = function (event?: string, fn?: Function): Component {} Vue.prototype.$emit = function (event: string): Component {} // lifecycleMixin(Vue) src/core/instance/lifecycle.js ************************************************** Vue.prototype._mount = function(){} Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {} Vue.prototype._updateFromParent = function(){} Vue.prototype.$forceUpdate = function () {} Vue.prototype.$destroy = function () {}
这样就结束了吗?并无,根据咱们以前寻找 Vue 的路线,这只是刚刚开始,咱们追溯路线往回走,那么下一个处理 Vue 构造函数的应该是 src/core/index.js
文件,咱们打开它:
import Vue from './instance/index' import { initGlobalAPI } from './global-api/index' import { isServerRendering } from 'core/util/env' initGlobalAPI(Vue) Object.defineProperty(Vue.prototype, '$isServer', { get: isServerRendering }) Vue.version = '__VERSION__' export default Vue
这个文件也很简单,从 instance/index 中导入已经在原型上挂载了方法和属性后的 Vue,而后导入 initGlobalAPI
和 isServerRendering
,以后将Vue做为参数传给 initGlobalAPI
,最后又在 Vue.prototype
上挂载了 $isServer
,在 Vue
上挂载了 version
属性。
initGlobalAPI
的做用是在 Vue
构造函数上挂载静态属性和方法,Vue
在通过 initGlobalAPI
以后,会变成这样:
// src/core/index.js / src/core/global-api/index.js Vue.config Vue.util = util Vue.set = set Vue.delete = del Vue.nextTick = util.nextTick Vue.options = { components: { KeepAlive }, directives: {}, filters: {}, _base: Vue } Vue.use Vue.mixin Vue.cid = 0 Vue.extend Vue.component = function(){} Vue.directive = function(){} Vue.filter = function(){} Vue.prototype.$isServer Vue.version = '__VERSION__'
其中,稍微复杂一点的就是 Vue.options
,你们稍微分析分析就会知道他的确长成那个样子。下一个就是 web-runtime.js
文件了,web-runtime.js
文件主要作了三件事儿:
一、覆盖
Vue.config
的属性,将其设置为平台特有的一些方法
二、Vue.options.directives
和Vue.options.components
安装平台特有的指令和组件
三、在Vue.prototype
上定义__patch__
和$mount
通过 web-runtime.js
文件以后,Vue
变成下面这个样子:
// 安装平台特定的utils Vue.config.isUnknownElement = isUnknownElement Vue.config.isReservedTag = isReservedTag Vue.config.getTagNamespace = getTagNamespace Vue.config.mustUseProp = mustUseProp // 安装平台特定的 指令 和 组件 Vue.options = { components: { KeepAlive, Transition, TransitionGroup }, directives: { model, show }, filters: {}, _base: Vue } Vue.prototype.__patch__ Vue.prototype.$mount
这里你们要注意的是 Vue.options
的变化。另外这里的 $mount
方法很简单:
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return this._mount(el, hydrating) }
首先根据是不是浏览器环境决定要不要 query(el)
获取元素,而后将 el
做为参数传递给 this._mount()
。
最后一个处理 Vue 的文件就是入口文件 web-runtime-with-compiler.js
了,该文件作了两件事:
一、缓存来自 web-runtime.js
文件的 $mount
函数
const mount = Vue.prototype.$mount
而后覆盖覆盖了 Vue.prototype.$mount
二、在 Vue 上挂载 compile
Vue.compile = compileToFunctions
compileToFunctions 函数的做用,就是将模板 template
编译为render函数。
至此,咱们算是还原了 Vue 构造函数,总结一下:
一、
Vue.prototype
下的属性和方法的挂载主要是在src/core/instance
目录中的代码处理的二、
Vue
下的静态属性和方法的挂载主要是在src/core/global-api
目录下的代码处理的三、
web-runtime.js
主要是添加web平台特有的配置、组件和指令,web-runtime-with-compiler.js
给Vue的$mount
方法添加compiler
编译器,支持template
。
在了解了 Vue
构造函数的设计以后,接下来,咱们一个贯穿始终的例子就要登场了,掌声有请:
let v = new Vue({ el: '#app', data: { a: 1, b: [1, 2, 3] } })
好吧,我认可这段代码你家没满月的孩子都会写了。这段代码就是咱们贯穿始终的例子,它就是这篇文章的主线,在后续的讲解中,都会以这段代码为例,当讲到必要的地方,会为其添加选项,好比讲计算属性的时候固然要加上一个 computed
属性了。不过在最开始,我只传递了两个选项 el
以及 data
,“咱们看看接下来会发生什么,让咱们拭目以待“ —- NBA球星在接受采访时最喜欢说这句话。
当咱们按照例子那样编码使用Vue的时候,Vue都作了什么?
想要知道Vue都干了什么,咱们就要找到 Vue 初始化程序,查看 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) }
咱们发现,_init()
方法就是Vue调用的第一个方法,而后将咱们的参数 options
透传了过去。在调用 _init()
以前,还作了一个安全模式的处理,告诉开发者必须使用 new
操做符调用 Vue。根据以前咱们的整理,_init()
方法应该是在 src/core/instance/init.js
文件中定义的,咱们打开这个文件查看 _init()
方法:
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ // a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) callHook(vm, 'beforeCreate') initState(vm) callHook(vm, 'created') initRender(vm) }
_init()
方法在一开始的时候,在 this
对象上定义了两个属性:_uid
和 _isVue
,而后判断有没有定义 options._isComponent
,在使用 Vue 开发项目的时候,咱们是不会使用 _isComponent
选项的,这个选项是 Vue 内部使用的,按照本节开头的例子,这里会走 else
分支,也就是这段代码:
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
这样 Vue
第一步所作的事情就来了:使用策略对象合并参数选项
能够发现,Vue使用 mergeOptions
来处理咱们调用Vue时传入的参数选项(options),而后将返回值赋值给 this.$options
(vm === this),传给 mergeOptions
方法三个参数,咱们分别来看一看,首先是:resolveConstructorOptions(vm.constructor)
,咱们查看一下这个方法:
export function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { const superOptions = Ctor.super.options const cachedSuperOptions = Ctor.superOptions const extendOptions = Ctor.extendOptions if (superOptions !== cachedSuperOptions) { // super option changed Ctor.superOptions = superOptions extendOptions.render = options.render extendOptions.staticRenderFns = options.staticRenderFns extendOptions._scopeId = options._scopeId options = Ctor.options = mergeOptions(superOptions, extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options }
这个方法接收一个参数 Ctor
,经过传入的 vm.constructor
咱们能够知道,其实就是 Vue
构造函数自己。因此下面这句代码:
let options = Ctor.options
至关于:
let options = Vue.options
你们还记得 Vue.options
吗?在寻找Vue构造函数一节里,咱们整理了 Vue.options
应该长成下面这个样子:
Vue.options = { components: { KeepAlive, Transition, TransitionGroup }, directives: { model, show }, filters: {}, _base: Vue }
以后判断是否认义了 Vue.super
,这个是用来处理继承的,咱们后续再讲,在本例中,resolveConstructorOptions
方法直接返回了 Vue.options
。也就是说,传递给 mergeOptions
方法的第一个参数就是 Vue.options
。
传给 mergeOptions
方法的第二个参数是咱们调用Vue构造函数时的参数选项,第三个参数是 vm
也就是 this
对象,按照本节开头的例子那样使用 Vue,最终运行的代码应该以下:
vm.$options = mergeOptions( // Vue.options { components: { KeepAlive, Transition, TransitionGroup }, directives: { model, show }, filters: {}, _base: Vue }, // 调用Vue构造函数时传入的参数选项 options { el: '#app', data: { a: 1, b: [1, 2, 3] } }, // this vm )
了解了这些,咱们就能够看看 mergeOptions
到底作了些什么了,根据引用寻找到 mergeOptions
应该是在 src/core/util/options.js
文件中定义的。这个文件第一次看可能会头大,下面是我处理后的简略展现,你们看上去应该更容易理解了:
// 一、引用依赖 import Vue from '../instance/index' 其余引用... // 二、合并父子选项值为最终值的策略对象,此时 strats 是一个空对象,由于 config.optionMergeStrategies = Object.create(null) const strats = config.optionMergeStrategies // 三、在 strats 对象上定义与参数选项名称相同的方法 strats.el = strats.propsData = function (parent, child, vm, key){} strats.data = function (parentVal, childVal, vm) config._lifecycleHooks.forEach(hook => { strats[hook] = mergeHook }) config._assetTypes.forEach(function (type) { strats[type + 's'] = mergeAssets }) strats.watch = function (parentVal, childVal) strats.props = strats.methods = strats.computed = function (parentVal: ?Object, childVal: ?Object) // 默认的合并策略,若是有 `childVal` 则返回 `childVal` 没有则返回 `parentVal` const defaultStrat = function (parentVal: any, childVal: any): any { return childVal === undefined ? parentVal : childVal } // 四、mergeOptions 中根据参数选项调用同名的策略方法进行合并处理 export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { // 其余代码 ... const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options }
上面的代码中,我省略了一些工具函数,例如 mergeHook
和 mergeAssets
等等,惟一须要注意的是这段代码:
config._lifecycleHooks.forEach(hook => { strats[hook] = mergeHook }) config._assetTypes.forEach(function (type) { strats[type + 's'] = mergeAssets })
config
对象引用自 src/core/config.js
文件,最终的结果就是在 strats
下添加了相应的生命周期选项的合并策略函数为 mergeHook
,添加指令(directives)、组件(components)、过滤器(filters)等选项的合并策略函数为 mergeAssets
。
这样看来就清晰多了,拿咱们贯穿本文的例子来讲:
let v = new Vue({ el: '#app', data: { a: 1, b: [1, 2, 3] } })
其中 el
选项会使用 defaultStrat
默认策略函数处理,data
选项则会使用 strats.data
策略函数处理,而且根据 strats.data
中的逻辑,strats.data
方法最终会返回一个函数:mergedInstanceDataFn
。
这里就不详细的讲解每个策略函数的内容了,后续都会讲到,这里咱们仍是抓住主线理清思路为主,只须要知道Vue在处理选项的时候,使用了一个策略对象对父子选项进行合并。并将最终的值赋值给实例下的 $options
属性即:this.$options
,那么咱们继续查看 _init()
方法在合并完选项以后,又作了什么:
合并完选项以后,Vue 第二部作的事情就来了:初始化工做与Vue实例对象的设计
前面讲了 Vue 构造函数的设计,而且整理了 Vue原型属性与方法 和 Vue静态属性与方法,而 Vue 实例对象就是经过构造函数创造出来的,让咱们来看一看 Vue 实例对象是如何设计的,下面的代码是 _init()
方法合并完选项以后的代码:
/* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) callHook(vm, 'beforeCreate') initState(vm) callHook(vm, 'created') initRender(vm)
根据上面的代码,在生产环境下会为实例添加两个属性,而且属性值都为实例自己:
vm._renderProxy = vm vm._self = vm
而后,调用了四个 init*
方法分别为:initLifecycle
、initEvents
、initState
、initRender
,且在 initState
先后分别回调了生命周期钩子 beforeCreate
和 created
,而 initRender
是在 created
钩子执行以后执行的,看到这里,也就明白了为何 created 的时候不能操做DOM了。由于这个时候尚未渲染真正的DOM元素到文档中。created
仅仅表明数据状态的初始化完成。
根据四个 init*
方法的引用关系打开对应的文件查看对应的方法,咱们发现,这些方法是在处理Vue实例对象,以及作一些初始化的工做,相似整理Vue构造函数同样,我一样针对Vue实例作了属性和方法的整理,以下:
// 在 Vue.prototype._init 中添加的属性 ********************************************************** this._uid = uid++ this._isVue = true this.$options = { components, directives, filters, _base, el, data: mergedInstanceDataFn() } this._renderProxy = this this._self = this // 在 initLifecycle 中添加的属性 ********************************************************** this.$parent = parent this.$root = parent ? parent.$root : this this.$children = [] this.$refs = {} this._watcher = null this._inactive = false this._isMounted = false this._isDestroyed = false this._isBeingDestroyed = false // 在 initEvents 中添加的属性 ********************************************************** this._events = {} this._updateListeners = function(){} // 在 initState 中添加的属性 ********************************************************** this._watchers = [] // initData this._data // 在 initRender 中添加的属性 ********************************************************** this.$vnode = null // the placeholder node in parent tree this._vnode = null // the root of the child tree this._staticTrees = null this.$slots this.$scopedSlots this._c this.$createElement
以上就是一个Vue实例所包含的属性和方法,除此以外要注意的是,在 initEvents
中除了添加属性以外,若是有 vm.$options._parentListeners
还要调用 vm._updateListeners()
方法,在 initState
中又调用了一些其余init方法,以下:
export function initState (vm: Component) { vm._watchers = [] initProps(vm) initMethods(vm) initData(vm) initComputed(vm) initWatch(vm) }
最后在 initRender
中若是有 vm.$options.el
还要调用 vm.$mount(vm.$options.el)
,以下:
if (vm.$options.el) { vm.$mount(vm.$options.el) }
这就是为何若是不传递 el
选项就须要手动 mount 的缘由了。
那么咱们依照咱们本节开头的的例子,以及初始化的前后顺序来逐一看一看都发生了什么。咱们将 initState
中的 init*
方法展开来看,执行顺序应该是这样的(从上到下的顺序执行):
initLifecycle(vm) initEvents(vm) callHook(vm, 'beforeCreate') initProps(vm) initMethods(vm) initData(vm) initComputed(vm) initWatch(vm) callHook(vm, 'created') initRender(vm)
首先是 initLifecycle
,这个函数的做用就是在实例上添加一些属性,而后是 initEvents
,因为 vm.$options._parentListeners
的值为 undefined
因此也仅仅是在实例上添加属性, vm._updateListeners(listeners)
并不会执行,因为咱们只传递了 el
和 data
,因此 initProps
、initMethods
、initComputed
、initWatch
这四个方法什么都不会作,只有 initData
会执行。最后是 initRender
,除了在实例上添加一些属性外,因为咱们传递了 el
选项,因此会执行 vm.$mount(vm.$options.el)
。
综上所述:按照咱们的例子那样写,初始化工做只包含两个主要内容即:initData
和 initRender
。
Vue的数据响应系统包含三个部分:Observer
、Dep
、Watcher
。关于数据响应系统的内容真的已经被文章讲烂了,因此我就简单的说一下,力求你们能理解就ok,咱们仍是先看一下 initData
中的代码:
function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? data.call(vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props let i = keys.length while (i--) { if (props && hasOwn(props, keys[i])) { process.env.NODE_ENV !== 'production' && warn( `The data property "${keys[i]}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else { proxy(vm, keys[i]) } } // observe data observe(data) data.__ob__ && data.__ob__.vmCount++ }
首先,先拿到 data 数据:let data = vm.$options.data
,你们还记得此时 vm.$options.data
的值应该是经过 mergeOptions
合并处理后的 mergedInstanceDataFn
函数吗?因此在获得 data 后,它又判断了 data 的数据类型是否是 ‘function’,最终的结果是:data 仍是咱们传入的数据选项的 data,即:
data: { a: 1, b: [1, 2, 3] }
而后在实例对象上定义 _data
属性,该属性与 data
是相同的引用。
而后是一个 while
循环,循环的目的是在实例对象上对数据进行代理,这样咱们就能经过 this.a
来访问 data.a
了,代码的处理是在 proxy
函数中,该函数很是简单,仅仅是在实例对象上设置与 data
属性同名的访问器属性,而后使用 _data
作数据劫持,以下:
function proxy (vm: Component, key: string) { if (!isReserved(key)) { Object.defineProperty(vm, key, { configurable: true, enumerable: true, get: function proxyGetter () { return vm._data[key] }, set: function proxySetter (val) { vm._data[key] = val } }) } }
作完数据的代理,就正式进入响应系统,
observe(data)
咱们说过,数据响应系统主要包含三部分:Observer
、Dep
、Watcher
,代码分别存放在:observer/index.js
、observer/dep.js
以及 observer/watcher.js
文件中,这回咱们换一种方式,咱们先不看其源码,你们先跟着个人思路来思考,最后回头再去看代码,你会有一种:”奥,不过如此“的感受。
假如,咱们有以下代码:
var data = { a: 1, b: { c: 2 } } observer(data) new Watch('a', () => { alert(9) }) new Watch('a', () => { alert(90) }) new Watch('b.c', () => { alert(80) })
这段代码目的是,首先定义一个数据对象 data
,而后经过 observer 对其进行观测,以后定义了三个观察者,当数据有变化时,执行相应的方法,这个功能使用Vue的实现原来要如何去实现?其实就是在问 observer
怎么写?Watch
构造函数又怎么写?接下来咱们逐一实现。
首先,observer 的做用是:将数据对象data的属性转换为访问器属性:
class Observer { constructor (data) { this.walk(data) } walk (data) { // 遍历 data 对象属性,调用 defineReactive 方法 let keys = Object.keys(data) for(let i = 0; i < keys.length; i++){ defineReactive(data, keys[i], data[keys[i]]) } } } // defineReactive方法仅仅将data的属性转换为访问器属性 function defineReactive (data, key, val) { // 递归观测子属性 observer(val) Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { return val }, set: function (newVal) { if(val === newVal){ return } // 对新值进行观测 observer(newVal) } }) } // observer 方法首先判断data是否是纯JavaScript对象,若是是,调用 Observer 类进行观测 function observer (data) { if(Object.prototype.toString.call(data) !== '[object Object]') { return } new Observer(data) }
上面的代码中,咱们定义了 observer 方法,该方法检测了数据data是否是纯JavaScript对象,若是是就调用 Observer
类,并将 data
做为参数透传。在 Observer
类中,咱们使用 walk
方法对数据data的属性循环调用 defineReactive
方法,defineReactive
方法很简单,仅仅是将数据data的属性转为访问器属性,并对数据进行递归观测,不然只能观测数据data的直属子属性。这样咱们的第一步工做就完成了,当咱们修改或者获取data属性值的时候,经过 get
和 set
即能获取到通知。
咱们继续往下看,来看一下 Watch
:
new Watch('a', () => { alert(9) })
如今的问题是,Watch
要怎么和 observer
关联???????咱们看看 Watch
它知道些什么,经过上面调用 Watch
的方式,传递给 Watch
两个参数,一个是 ‘a’ 咱们能够称其为表达式,另一个是回调函数。因此咱们目前只能写出这样的代码:
class Watch { constructor (exp, fn) { this.exp = exp this.fn = fn } }
那么要怎么关联呢,你们看下面的代码会发生什么:
class Watch { constructor (exp, fn) { this.exp = exp this.fn = fn data[exp] } }
多了一句 data[exp]
,这句话是在干什么?是否是在获取 data
下某个属性的值,好比 exp 为 ‘a’ 的话,那么 data[exp]
就至关于在获取 data.a
的值,那这会放生什么?你们不要忘了,此时数据 data
下的属性已是访问器属性了,因此这么作的结果会直接触发对应属性的 get
函数,这样咱们就成功的和 observer
产生了关联,但这样还不够,咱们仍是没有达到目的,不过咱们已经无限接近了,咱们继续思考看一下可不能够这样:
既然在
Watch
中对表达式求值,可以触发observer
的get
,那么可不能够在get
中收集Watch
中函数呢?
答案是能够的,不过这个时候咱们就须要 Dep
出场了,它是一个依赖收集器。咱们的思路是:data
下的每个属性都有一个惟一的 Dep
对象,在 get
中收集仅针对该属性的依赖,而后在 set
方法中触发全部收集的依赖,这样就搞定了,看以下代码:
class Dep { constructor () { this.subs = [] } addSub () { this.subs.push(Dep.target) } notify () { for(let i = 0; i < this.subs.length; i++){ this.subs[i].fn() } } } Dep.target = null function pushTarget(watch){ Dep.target = watch } class Watch { constructor (exp, fn) { this.exp = exp this.fn = fn pushTarget(this) data[exp] } }
上面的代码中,咱们在 Watch
中增长了 pushTarget(this)
,能够发现,这句代码的做用是将 Dep.target
的值设置为该Watch对象。在 pushTarget
以后咱们才对表达式进行求值,接着,咱们修改 defineReactive
代码以下
function defineReactive (data, key, val) { observer(val) let dep = new Dep() // 新增 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.addSub() // 新增 return val }, set: function (newVal) { if(val === newVal){ return } observer(newVal) dep.notify() // 新增 } }) }
如标注,新增了三句代码,咱们知道,Watch
中对表达式求值会触发 get
方法,咱们在 get
方法中调用了 dep.addSub
,也就执行了这句代码:this.subs.push(Dep.target)
,因为在这句代码执行以前,Dep.target
的值已经被设置为一个 Watch
对象了,因此最终结果就是收集了一个 Watch
对象,而后在 set
方法中咱们调用了 dep.notify
,因此当data属性值变化的时候,就会经过 dep.notify
循环调用全部收集的Watch对象中的回调函数:
notify () { for(let i = 0; i < this.subs.length; i++){ this.subs[i].fn() } }
这样 observer
、Dep
、Watch
三者就联系成为一个有机的总体,实现了咱们最初的目标,完整的代码能够戳这里:observer-dep-watch。这里还给你们挖了个坑,由于咱们没有处理对数组的观测,因为比较复杂而且这又不是咱们讨论的重点,若是你们想了解能够戳个人这篇文章:JavaScript实现MVVM之我就是想监测一个普通对象的变化,另外,在 Watch 中对表达式求值的时候也只作了直接子属性的求值,因此若是 exp 的值为 ‘a.b’ 的时候,就不能够用了,Vue的作法是使用 .
分割表达式字符串为数组,而后遍历一下对其进行求值,你们能够查看其源码。以下:
/** * Parse simple path. */ const bailRE = /[^\w.$]/ export function parsePath (path: string): any { if (bailRE.test(path)) { return } else { const segments = path.split('.') return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } } }
Vue 的求值代码是在 src/core/util/lang.js
文件中 parsePath
函数中实现的。总结一下Vue的依赖收集过程应该是这样的:
实际上,Vue并无直接在 get
中调用 addSub
,而是调用的 dep.depend
,目的是将当前的 dep 对象收集到 watch 对象中,若是要完整的流程,应该是这样的:(你们注意数据的每个字段都拥有本身的 dep
对象和 get
方法。)
这样 Vue 就创建了一套数据响应系统,以前咱们说过,按照咱们的例子那样写,初始化工做只包含两个主要内容即:initData
和 initRender
。如今 initData
咱们分析完了,接下来看一看 initRender
在 initRender
方法中,由于咱们的例子中传递了 el
选项,因此下面的代码会执行:
if (vm.$options.el) { vm.$mount(vm.$options.el) }
这里,调用了 $mount
方法,在还原Vue构造函数的时候,咱们整理过全部的方法,其中 $mount
方法在两个地方出现过:
一、在 web-runtime.js
文件中:
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return this._mount(el, hydrating) }
它的做用是经过 el
获取相应的DOM元素,而后调用 lifecycle.js
文件中的 _mount
方法。
二、在 web-runtime-with-compiler.js
文件中:
// 缓存了来自 web-runtime.js 的 $mount 方法 const mount = Vue.prototype.$mount // 重写 $mount 方法 Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // 根据 el 获取相应的DOM元素 el = el && query(el) // 不容许你将 el 挂载到 html 标签或者 body 标签 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 // 若是咱们没有写 render 选项,那么就尝试将 template 或者 el 转化为 render 函数 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) { const { render, staticRenderFns } = compileToFunctions(template, { warn, shouldDecodeNewlines, delimiters: options.delimiters }, this) options.render = render options.staticRenderFns = staticRenderFns } } // 调用已经缓存下来的 web-runtime.js 文件中的 $mount 方法 return mount.call(this, el, hydrating) }
分析一下可知 web-runtime-with-compiler.js
的逻辑以下:
一、缓存来自 web-runtime.js
文件的 $mount
方法
二、判断有没有传递 render
选项,若是有直接调用来自 web-runtime.js
文件的 $mount 方法
三、若是没有传递 render
选项,那么查看有没有 template
选项,若是有就使用 compileToFunctions
函数根据其内容编译成 render
函数
四、若是没有 template
选项,那么查看有没有 el
选项,若是有就使用 compileToFunctions
函数将其内容(template = getOuterHTML(el))编译成 render
函数
五、将编译成的 render
函数挂载到 this.$options
属性下,并调用缓存下来的 web-runtime.js
文件中的 $mount 方法
简单的用一张图表示 mount
方法的调用关系,从上至下调用:
不过无论怎样,咱们发现这些步骤的最终目的是生成 render
函数,而后再调用 lifecycle.js
文件中的 _mount
方法,咱们看看这个方法作了什么事情,查看 _mount
方法的代码,这是简化过得:
Vue.prototype._mount = function ( el?: Element | void, hydrating?: boolean ): Component { const vm: Component = this // 在Vue实例对象上添加 $el 属性,指向挂载点元素 vm.$el = el // 触发 beforeMount 生命周期钩子 callHook(vm, 'beforeMount') vm._watcher = new Watcher(vm, () => { vm._update(vm._render(), hydrating) }, noop) // 若是是第一次mount则触发 mounted 生命周期钩子 if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
上面的代码很简单,该注释的都注释了,惟一须要看的就是这段代码:
vm._watcher = new Watcher(vm, () => { vm._update(vm._render(), hydrating) }, noop)
看上去很眼熟有没有?咱们平时使用Vue都是这样使用 watch的:
this.$watch('a', (newVal, oldVal) => { }) // 或者 this.$watch(function(){ return this.a + this.b }, (newVal, oldVal) => { })
第一个参数是 表达式或者函数,第二个参数是回调函数,第三个参数是可选的选项。原理是 Watch
内部对表达式求值或者对函数求值从而触发数据的 get
方法收集依赖。但是 _mount
方法中使用 Watcher
的时候第一个参数 vm
是什么鬼。咱们不妨去看看源码中 $watch
函数是如何实现的,根据以前还原Vue构造函数中所整理的内容可知:$warch
方法是在 src/core/instance/state.js
文件中的 stateMixin
方法中定义的,源码以下:
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: Function, options?: Object ): Function { const vm: Component = this options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { cb.call(vm, watcher.value) } return function unwatchFn () { watcher.teardown() } }
咱们能够发现,$warch
实际上是对 Watcher
的一个封装,内部的 Watcher
的第一个参数实际上也是 vm
即:Vue实例对象,这一点咱们能够在 Watcher
的源码中获得验证,代开 observer/watcher.js
文件查看:
export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: Object = {} ) { } }
能够发现真正的 Watcher
第一个参数实际上就是 vm
。第二个参数是表达式或者函数,而后以此类推,因此如今再来看 _mount
中的这段代码:
vm._watcher = new Watcher(vm, () => { vm._update(vm._render(), hydrating) }, noop)
忽略第一个参数 vm
,也就说,Watcher
内部应该对第二个参数求值,也就是运行这个函数:
() => { vm._update(vm._render(), hydrating) }
因此 vm._render()
函数被第一个执行,该函数在 src/core/instance/render.js
中,该方法中的代码不少,下面是简化过的:
Vue.prototype._render = function (): VNode { const vm: Component = this // 解构出 $options 中的 render 函数 const { render, staticRenderFns, _parentVnode } = vm.$options ... let vnode try { // 运行 render 函数 vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { ... } // set parent vnode.parent = _parentVnode return vnode }
_render
方法首先从 vm.$options
中解构出 render
函数,你们应该记得:vm.$options.render
方法是在 web-runtime-with-compiler.js
文件中经过 compileToFunctions
方法将 template
或 el
编译而来的。解构出 render
函数后,接下来便执行了该方法:
vnode = render.call(vm._renderProxy, vm.$createElement)
其中使用 call
指定了 render
函数的做用域环境为 vm._renderProxy
,这个属性在咱们整理实例对象的时候知道,他是在 Vue.prototype._init
方法中被添加的,即:vm._renderProxy = vm
,其实就是Vue实例对象自己,而后传递了一个参数:vm.$createElement
。那么 render
函数究竟是干什么的呢?让咱们根据上面那句代码猜一猜,咱们已经知道 render
函数是从 template
或 el
编译而来的,若是没错的话应该是返回一个虚拟DOM对象。咱们不妨使用 console.log
打印一下 render
函数,当咱们的模板这样编写时:
<ul id="app"> <li>{{a}}</li> </ul>
打印的 render
函数以下:
咱们修改模板为:
<ul id="app"> <li v-for="i in b">{{a}}</li> </ul>
打印出来的 render
函数以下:
其实了解Vue2.x版本的同窗都知道,Vue提供了 render
选项,做为 template
的代替方案,同时为JavaScript提供了彻底编程的能力,下面两种编写模板的方式实际是等价的:
// 方案一: new Vue({ el: '#app', data: { a: 1 }, template: '<ul><li>{{a}}</li><li>{{a}}</li></ul>' }) // 方案二: new Vue({ el: '#app', render: function (createElement) { createElement('ul', [ createElement('li', this.a), createElement('li', this.a) ]) } })
如今咱们再来看咱们打印的 render
函数:
function anonymous() { with(this){ return _c('ul', { attrs: {"id": "app"} },[ _c('li', [_v(_s(a))]) ]) } }
是否是与咱们本身写 render
函数很像?由于 render 函数的做用域被绑定到了Vue实例,即:render.call(vm._renderProxy, vm.$createElement)
,因此上面代码中 _c
、_v
、_s
以及变量 a
至关于Vue实例下的方法和变量。你们还记得诸如 _c
、_v
、_s
这样的方法在哪里定义的吗?咱们在整理Vue构造函数的时候知道,他们在 src/core/instance/render.js
文件中的 renderMixin
方法中定义,除了这些以外还有诸如:_l
、 _m
、 _o
等等。其中 _l
就在咱们使用 v-for
指令的时候出现了。因此如今你们知道为何这些方法都被定义在 render.js
文件中了吧,由于他们就是为了构造出 render
函数而存在的。
如今咱们已经知道了 render
函数的长相,也知道了 render
函数的做用域是Vue实例自己即:this
(或vm
)。那么当咱们执行 render
函数时,其中的变量如:a
,就至关于:this.a
,咱们知道这是在求值,因此 _mount
中的这段代码:
vm._watcher = new Watcher(vm, () => { vm._update(vm._render(), hydrating) }, noop)
当 vm._render
执行的时候,所依赖的变量就会被求值,并被收集为依赖。按照Vue中 watcher.js
的逻辑,当依赖的变量有变化时不只仅回调函数被执行,实际上还要从新求值,即还要执行一遍:
() => { vm._update(vm._render(), hydrating) }
这实际上就作到了 re-render
,由于 vm._update
就是文章开头所说的虚拟DOM中的最后一步:patch
vm_render
方法最终返回一个 vnode
对象,即虚拟DOM,而后做为 vm_update
的第一个参数传递了过去,咱们看一下 vm_update
的逻辑,在 src/core/instance/lifecycle.js
文件中有这么一段代码:
if (!prevVnode) { // initial render vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) }
若是尚未 prevVnode
说明是首次渲染,直接建立真实DOM。若是已经有了 prevVnode
说明不是首次渲染,那么就采用 patch
算法进行必要的DOM操做。这就是Vue更新DOM的逻辑。只不过咱们没有将 virtual DOM 内部的实现。
如今咱们来好好理理思路,当咱们写以下代码时:
new Vue({ el: '#app', data: { a: 1, b: [1, 2, 3] } })
Vue 所作的事:
一、构建数据响应系统,使用
Observer
将数据data转换为访问器属性;将el
编译为render
函数,render
函数返回值为虚拟DOM二、在
_mount
中对_update
求值,而_update
又会对render
求值,render
内部又会对依赖的变量求值,收集为被求值的变量的依赖,当变量改变时,_update
又会从新执行一遍,从而作到re-render
。
用一张详细一点的图表示就是这样的:
到此,咱们从大致流程,挑着重点的走了一遍Vue,可是还有不少细节咱们没有说起,好比:
一、将模板转为 render
函数的时候,实际是先生成的抽象语法树(AST),再将抽象语法树转成的 render
函数,并且这一整套的代码咱们也没有说起,由于他在复杂了,其实这部份内容就是在完正则。
二、咱们也没有详细的讲 Virtual DOM 的实现原理,网上已经有文章讲了,你们能够搜一搜
三、咱们的例子中仅仅传递了 el
,data
选项,你们知道 Vue 支持的选项不少,好比咱们都没有讲到,但都是举一反三的,好比你搞清楚了 data
选项再去看 computed
选项或者 props
选项就会很容易,好比你知道了 Watcher
的工做机制再去看 watch
选项就会很容易。
本篇文章做为Vue源码的启蒙文章,也许还有不少缺陷,全当抛砖引玉了。