## Vue 的初始化 #### 用于初始化的最终选项 $options 在 [Vue的思路之以一个例子为线索](/note/Vue的思路之以一个例子为线索) 一节中,我们写了一个很简单的例子,这个例子如下: ```js var vm = new Vue({ el: '#app', data: { test: 1 } }) ``` 我们以这个例子为线索开始了对 `Vue` 代码的讲解,我们知道了在实例化 `Vue` 实例的时候,`Vue.prototype._init` 方法被第一个执行,这个方法定义在 `src/core/instance/init.js` 文件中,在分析 `_init` 方法的时候我们遇到了下面的代码: ```js vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) ``` 正是因为上面的代码,使得我们花了大篇章来讲解其内部实现和运作,也就是 [Vue的思路之选项的规范化](/note/Vue的思路之选项的规范化) 和 [Vue的思路之选项的合并](/note/Vue的思路之选项的合并) 这两节所介绍的内容。现在我们已经知道了 `mergeOptions` 函数是如何对父子选项进行合并处理的,也知道了它的作用。 我们打开 `core/util/options.js` 文件,找到 `mergeOptions` 函数,看其最后一句代码: ```js return options ``` 这说明 `mergeOptions` 函数最终将合并处理后的选项返回,并以该返回值作为 `vm.$options` 的值。`vm.$options` 在 `Vue` 的官方文档中是可以找到的,它作为实例属性暴露给开发者,那么现在你应该知道 `vm.$options` 到底是什么了。并且你看文档的时候你应该更能够理解其作用,比如官方文档是这样介绍 `$options` 实例属性的: > 用于当前 Vue 实例的初始化选项。需要在选项中包含自定义属性时会有用处 并且给了一个例子,如下: ```js new Vue({ customOption: 'foo', created: function () { console.log(this.$options.customOption) // => 'foo' } }) ``` 上面的例子中,在创建 `Vue` 实例的时候传递了一个自定义选项:`customOption`,在之后的代码中我们可以通过 `this.$options.customOption` 进行访问。那原理其实就是使用 `mergeOptions` 函数对自定义选项进行合并处理,由于没有指定 `customOption` 选项的合并策略,所以将会使用默认的策略函数 `defaultStrat`。最终效果就是你初始化的值是什么,得到的就是什么。 另外,`Vue` 也提供了 `Vue.config.optionMergeStrategies` 全局配置,大家也可以在官方文档中找到,我们知道这个对象其实就是选项合并中的策略对象,所以我们可以通过他指定某一个选项的合并策略,常用于指定自定义选项的合并策略,比如我们给 `customOption` 选项指定一个合并策略,只需要在 `Vue.config.optionMergeStrategies` 上添加与选项同名的策略函数即可: ```js Vue.config.optionMergeStrategies.customOption = function (parentVal, childVal) { return parentVal ? (parentVal + childVal) : childVal } ``` 如上代码中,我们添加了自定义选项 `customOption` 的合并策略,其策略为:如果没有 `parentVal` 则直接返回 `childVal`,否则返回两者的和。 所以如下代码: ```js // 创建子类 const Sub = Vue.extend({ customOption: 1 }) // 以子类创建实例 const v = new Sub({ customOption: 2, created () { console.log(this.$options.customOption) // 3 } }) ``` 最终,在实例的 `created` 方法中将打印为数字 `3`。上面的例子很简单,没有什么实际作用,但这为我们提供了自定义选项的机会,这其实是非常有用的。 现在我们需要回到正题上了,还是拿我们例子,如下: ```js var vm = new Vue({ el: '#app', data: { test: 1 } }) ``` 这个时候 `mergeOptions` 函数将会把 `Vue.options` 作为 父选项,把我们传递的实例选项作为子选项进行合并,合并的结果我们可以通过打印 `$options` 属性得知。其实我们前面已经分析过了,`el` 选项将使用默认合并策略合并,最终的值就是字符串 `'#app'`,而 `data` 选项将变成一个函数,且这个函数的执行结果就是合并后的数据,那么在这里,合并后的数据就是 `{test: 1}` 这个对象。 下面是 `vm.$options` 的截图:  我们发现 `el` 确实还是原来的值,而 `data` 也确实变成了一个函数,并且这个函数就是我们之前遇到过的 `mergedInstanceDataFn`,除此之外我们还能看到其他合并后的选项,其中 `components`、`directives`、`filters` 以及 `_base` 我们知道是存在与 `Vue.options` 中的,至于 `render` 和 `staticRenderFns` 这两个选项是在将模板编译成渲染函数时添加上去的,我们后面会遇到。另外 `_parentElm` 和 `_refElm` 这两个选项是在为虚拟DOM创建组件实例时添加的,我们后面也会讲到,这里大家不需要关心,免得失去重点。最后还有一个 `inject` 选项,我们知道无论是 `Vue.options` 中还是实例选项中都没有 `inject`,那么这个 `inject` 是哪来的呢?大家还记不记得在 [Vue的思路之选项的规范化](/note/Vue的思路之选项的规范化) 一节中,在对 `inject` 选项进行规范化的时候,即使我们的选项没有写 `inject` 选项,其内部也会将其初始化为一个空对象,也就是在 `normalizeInject` 函数中的第二句代码: ```js const normalized = options.inject = {} ``` #### 渲染函数的作用域代理 ok,现在我们已经足够了解 `vm.$options` 这个属性了,它才是用来做一系列初始化工作的最终选项,那么接下来我们就继续看 `_init` 方法中的代码,继续了解 `Vue` 的初始化工作。 `_init` 方法中,在经过 `mergeOptions` 合并处理选项之后,要执行的是下面这段代码: ```js /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } ``` 这段代码是一个判断分支,如果是非生产环境的话则执行 `initProxy(vm)` 函数,如果在生产环境则直接在实例上添加 `_renderProxy` 实例属性,该属性的值就是当前实例。 现在有一个问题需要大家思考一下,现在我们还没有看 `initProxy` 函数的具体内容,那么你能猜到 `initProxy` 函数的主要作用是什么吗?我可以直接告诉大家,这个函数的主要作用,听清楚是主要作用其实还是在实例对象 `vm` 上添加 `_renderProxy` 属性。为什么呢?因为生产环境和非生产环境下要保持功能一直。在上面的代码中生产环境下直接执行这句: ```js vm._renderProxy = vm ``` 那么可想而知,在非生产环境下也应该执行这句代码,但实际上却调用了 `initProxy` 函数,所以 `initProxy` 函数的作用之一必然也是在实例对象 `vm` 上添加 `_renderProxy` 属性,那么接下来我们就看看 `initProxy` 的内容,验证一下我们的判断,打开 `core/instance/proxy.js` 文件: ```js /* not type checking this file because flow doesn't play well with Proxy */ import config from 'core/config' import { warn, makeMap } from '../util/index' // 声明 initProxy 变量 let initProxy if (process.env.NODE_ENV !== 'production') { // ... 其他代码 // 在这里初始化 initProxy initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler vm._renderProxy = new Proxy(vm, handlers) } else { vm._renderProxy = vm } } } // 导出 export { initProxy } ``` 上面的代码是简化后的,可以发现在文件的开头声明了 `initProxy` 变量,但并未初始化,所以目前 `initProxy` 还是 `undefined`,随后,在文件的结尾将 `initProxy` 导出,那么 `initProxy` 到底是什么呢?实际上变量 `initProxy` 的初始化赋值是在 `if` 语句块内进行的,这个 `if` 语句块进行环境判断,如果是非生产环境的话,那么才会对 `initProxy` 变量赋值,也就是说在生产环境下我们导出的 `initProxy` 实际上就是 `undefined`。只有在非生产环境下导出的 `initProxy` 才会有值,其值就是这个函数: ```js initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use const options = vm.$options const handlers = options.render && options.render._withStripped ? getHandler : hasHandler vm._renderProxy = new Proxy(vm, handlers) } else { vm._renderProxy = vm } } ``` 这个函数接收一个参数,实际就是 `Vue` 实例对象,我们先从宏观角度来看一下这个函数的作用是什么,可以发现,这个函数由 `if...else` 语句块组成,但无论走 `if` 还是 `else`,其最终的效果都是在 `vm` 对象上添加了 `_renderProxy` 属性。如果 `hasProxy` 为真则走 `if` 分支,对于 `hasProxy` 顾名思义,这是用来判断宿主环境是否支持 `js` 原生的 `Proxy` 特性的,如果发现 `Proxy` 存在,则执行: ```js vm._renderProxy = new Proxy(vm, handlers) ``` 如果不存在,那么和生产环境一样,直接赋值就可以了: ```js vm._renderProxy = vm ``` 所以我们发现 `initProxy` 的作用实际上就是对实例对象 `vm` 的代理,通过原生的 `Proxy` 实现。 另外 `hasProxy` 变量的定义也在当前文件中,代码如下: ```js const hasProxy = typeof Proxy !== 'undefined' && Proxy.toString().match(/native code/) ``` 上面的代码相信大家都能看得懂,所以就不做过多解释,接下来我们就看看它是如何做代理的,并且有什么作用。 查看 `initProxy` 函数的 `if` 语句块,内容如下: ```js initProxy = function initProxy (vm) { if (hasProxy) { // determine which proxy handler to use // options 就是 vm.$options 的引用 const options = vm.$options // handlers 可能是 getHandler 也可能是 hasHandler const handlers = options.render && options.render._withStripped ? getHandler : hasHandler // 代理 vm 对象 vm._renderProxy = new Proxy(vm, handlers) } else { // ... } } ``` 可以发现,如果 `Proxy` 存在,那么将会使用 `Proxy` 对 `vm` 做一层代理,代理对象赋值给 `vm._renderProxy`,所以今后对 `vm._renderProxy` 的访问,如果有代理那么就会被拦截。代理对象配置参数是 `handlers`,可以发现 `handlers` 即可能是 `getHandler` 又可能是 `hasHandler`,至于到底使用哪个,是由判断条件决定的: ```js options.render && options.render._withStripped ``` 如果上面的条件为真,则使用 `getHandler`,否则使用 `hasHandler`,判断条件要求 `options.render` 和 `options.render._withStripped` 必须都为真才行,我现在明确告诉大家,这个是用来写测试用的,所以一般情况下这个条件都会为假,也就是使用 `hasHandler` 作为代理配置。 `hasHandler` 这个变量就定义在当前文件,如下: ```js const hasHandler = { has (target, key) { // has 变量是真实经过 in 运算符得来的结果 const has = key in target // 如果 key 在 allowedGlobals 之内,或者 key 以下划线 _ 开头,则为真 const isAllowed = allowedGlobals(key) || key.charAt(0) === '_' // 如果 has 和 isAllowed 都为假,使用 warnNonPresent 函数打印错误 if (!has && !isAllowed) { warnNonPresent(target, key) } return has || !isAllowed } } ``` 这里我假设大家都对 `Proxy` 的使用已经没有任何问题了,我们知道 `has` 可以拦截一下操作: > * 属性查询: foo in proxy * 继承属性查询: foo in Object.create(proxy) * with 检查: with(proxy) { (foo); } * Reflect.has() 其中关键在就在可以拦截 `with` 语句块里对变量的访问,后面我们会讲到。`has` 函数内出现了两个函数,分别是 `allowedGlobals` 以及 `warnNonPresent`,这两个函数也是定义在当前文件中,首先我们看一下 `allowedGlobals`: ```js const allowedGlobals = makeMap( 'Infinity,undefined,NaN,isFinite,isNaN,' + 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' + 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' + 'require' // for Webpack/Browserify ) ``` 可以看到 `allowedGlobals` 实际上是通过 `makeMap` 生成的函数,所以 `allowedGlobals` 函数的作用是判断给定的 `key` 是否出现在上面字符串中定义的关键字中的。这些关键字都是在 `js` 中可以全局访问的。 `warnNonPresent` 函数如下: ```js const warnNonPresent = (target, key) => { warn( `Property or method "${key}" is not defined on the instance but ` + 'referenced during render. Make sure that this property is reactive, ' + 'either in the data option, or for class-based components, by ' + 'initializing the property. ' + 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', target ) } ``` 这个函数就是通过 `warn` 打印一段警告信息,警告信息提示你“在渲染的时候引用了 `key`,但是在实例上并没有定义 `key` 这个属性或方法”。其实我们很容易就可以看到这个信息,比如下面的代码: ```js const vm = new Vue({ el: '#app', template: '