简单回顾一下这个系列的前两节,前两节花了大篇幅讲了vue在初始化时进行的选项合并。选项配置是vue实例化的第一步,针对不一样类型的选项,vue提供的丰富选项配置策略以保证用户可使用不一样丰富的配置选项。而在这一节中,咱们会分析选项合并后的又两步重要的操做: 数据代理和关联子父组件关系,分别对应的处理过程为initProxy和initLifecycle。这章节的知识点也为后续的响应式系统介绍和模板渲染作铺垫。
在介绍这一章的源码分析以前,咱们须要掌握一下贯穿整个vue数据代理,监控的技术核心:Object.defineProperty 和 Proxyjavascript
官方定义:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
基本用法: Object.defineProperty(obj, prop, descriptor)
咱们能够用来精确添加或修改对象的属性,只须要在descriptor中将属性特性描述清楚,descriptor的属性描述符有两种形式,一种是数据描述符,另外一种是存取描述符。前端
数据描述符vue
存取描述符java
注意: 数据描述符的value,writable 和 存取描述符的get, set属性不能同时存在,不然会抛出异常。
有了Object.defineProperty方法,咱们能够方便的利用存取描述符中的getter/setter来进行数据监听,在get,set钩子中分别作不一样的操做,这是vue双向数据绑定原理的雏形,咱们会在响应式系统的源码分析时具体阐述。node
var o = {} var value; Object.defineProperty(o, 'a', { get() { console.log('获取值') return value }, set(v) { console.log('设置值') value = v } }) o.a = 'sss' // 设置值 console.log(o.a) // 获取值 // 'sss'
然而Object.defineProperty的get和set方法只能观测到对象属性的变化,对于数组类型的变化并不能检测到,这是用Object.defineProperty进行数据监控的缺陷,而vue中对于数组类型的方法作了特殊的处理。
es6的proxy能够完美的解决这一类问题。webpack
Proxy 是es6的语法,和Object.defineProperty同样,也是用于修改某些操做的默认行为,可是和Object.defineProperty不一样的是,Proxy针对目标对象,会建立一个新的实例对象,并将目标对象代理到新的实例对象上, 本质的区别就是多了一层代理,外界对该对象的访问,都必须先经过这层拦截,所以提供了一种机制,能够对外界的访问进行过滤和改写。外界经过操做新的实例对象从而操做真正的目标对象。针对getter和setter的基本用法以下:git
var obj = {} var nobj = new Proxy(obj, { get(target, property) { console.log('获取值') return target[property] }, set(target, key, value) { console.log('设置值') return target[key] } }) nobj.a = 1111 // 经过操做代理对象从而映射到目标对象上 // 设置值 // 获取值 // 1111 console.log(nobj.a)
Proxy 支持的拦截操做有13种之多,具体能够参照Proxy,上面提到,Object.defineProperty的get和set方法并不能监测到数组的变化,而Proxy是否能作到呢?es6
var arr = [1, 2, 3] let obj = new Proxy(arr, { get: function (target, key, receiver) { console.log("获取数组"); return Reflect.get(target, key, receiver); }, set: function (target, key, receiver) { console.log('设置数组'); return Reflect.set(target, key, receiver); } }) obj.push(222) // '获取数组' // '设置数组'
显然proxy能够很容易的监听到数组的变化。github
有了这些理论基础,咱们往下看vue的源码,在初始化合并选项后,vue接下来的操做是为vm实例设置一层代理,代理的目的是为vue在模板渲染时进行一层数据筛选。若是浏览器不支持Proxy,这层代理检验数据则会失效。(检测数据会放到其余地方检测)web
{ // 对vm实例进行一层代理 initProxy(vm); } // 代理函数 var initProxy = function initProxy (vm) { // 浏览器若是支持es6原生的proxy,则会进行实例的代理,这层代理会在模板渲染时对一些非法或者不存在的字符串进行判断,作数据的过滤筛选。 if (hasProxy) { var options = vm.$options; var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; // 代理vm实例到vm属性_renderProxy vm._renderProxy = new Proxy(vm, handlers); } else { vm._renderProxy = vm; } }; 如何判断浏览器支持原生proxy // 是否支持Symbol 和 Reflect var hasSymbol = typeof Symbol !== 'undefined' && isNative(Symbol) && typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys); function isNative (Ctor) { // Proxy自己是构造函数,且Proxy.toString === 'function Proxy() { [native code] }' return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) }
看到这里时,心中会有几点疑惑。
要解决这个疑惑,咱们接着往下看:
Vue.prototype._render = function () { ··· // 调用vm._renderProxy vnode = render.call(vm._renderProxy, vm.$createElement); }
也就是说模板引擎<div>{{message}}</div>
的渲染显示,会经过Proxy这层代理对数据进行过滤,并对非法数据进行报错提醒。
接着上面的问题,vm实例代理时会根据是不是编译的版本决定使用hasHandler或者getHandler,咱们先默认使用的是编译版本,所以咱们单独分析hasHandler的处理函数,getHandler的分析相似。
var hasHandler = { // key in obj或者with做用域时,会触发has的钩子 has: function has (target, key) { ··· } };
hasHandler函数定义了has的钩子,前面介绍过proxy有多达13个钩子,has是其中一个,它用来拦截propKey in proxy的操做,返回一个布尔值。除了拦截 in 操做符外,has钩子一样能够用来拦截with语句下的做用对象。例如
var obj = { a: 1 } var nObj = new Proxy(obj, { has(target, key) { console.log(target) // { a: 1 } console.log(key) // a return true } }) with(nObj) { a = 2 }
而在vue的render函数的内部,本质上也是调用了with语句,当调用with语句时,该做用域下变量的访问都会触发has钩子,这也是模板渲染时会触发代理拦截的缘由。
var vm = new Vue({ el: '#app' }) console.log(vm.$options.render) //输出, 模板渲染使用with语句 ƒ anonymous() { with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message)+_s(_test))])} }
再次思考:咱们知道with语句是不推荐使用的,一个最主要的缘由是性能问题,查找不是变量属性的变量,较慢的速度会影响性能一系列性能问题。
官方给出的解释是: 为了减小编译器代码大小和复杂度,而且也提供了经过vue-loader这类构建工具,不含with的版本。
接着上面的分析,在模板引擎render渲染时,因为with语句的存在,访问变量时会触发has钩子函数,该函数会进行数据的检测,好比模板上的变量是不是实例中所定义,是否包含_, $这类vue内部保留关键字为开头的变量。同时模板上的变量将容许出现javascript的保留变量对象,例如Math, Number, parseFloat等。
var hasHandler = { has: function has (target, key) { var has = key in target; // isAllowed用来判断模板上出现的变量是否合法。 var isAllowed = allowedGlobals(key) || (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)); // _和$开头的变量不容许出如今定义的数据中,由于他是vue内部保留属性的开头。 // warnReservedPrefix警告不能以$ _开头的变量 // warnNonPresent 警告模板出现的变量在vue实例中未定义 if (!has && !isAllowed) { if (key in target.$data) { warnReservedPrefix(target, key); } else { warnNonPresent(target, key); } } return has || !isAllowed } }; // 模板中容许出现的非vue实例定义的变量 var 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 );
分析完initProxy方法后,接下来是initLifecycle的过程。简单归纳,initLifecycle的目的是将当前实例添加到父实例的$children
属性中,并设置自身的$parent
属性指向父实例。这为后续子父组件之间的通讯提供了桥梁。举一个具体的应用场景:
<div id="app"> <component-a></component-a> </div> Vue.component('component-a', { template: '<div>a</div>' }) var vm = new Vue({ el: '#app'}) console.log(vm) // 将实例对象输出
因为vue实例向上没有父实例,因此vm.$parent
为undefined,vm的$children
属性指向子组件componentA 的实例。
子组件componentA的 $parent
属性指向它的父级vm实例,它的$children
属性指向为空
源码解析以下:
function initLifecycle (vm) { var options = vm.$options; // 子组件注册时,会把父组件的实例挂载到自身选项的parent上 var parent = options.parent; // 若是是子组件,而且该组件不是抽象组件时,将该组件的实例添加到父组件的$parent属性上,若是父组件是抽象组件,则一直往上层寻找,直到该父级组件不是抽象组件,并将,将该组件的实例添加到父组件的$parent属性 if (parent && !options.abstract) { while (parent.$options.abstract && parent.$parent) { parent = parent.$parent; } parent.$children.push(vm); } // 将自身的$parent属性指向父实例。 vm.$parent = parent; vm.$root = parent ? parent.$root : vm; vm.$children = []; vm.$refs = {}; vm._watcher = null; vm._inactive = null; vm._directInactive = false; // 该实例是否挂载 vm._isMounted = false; // 该实例是否被销毁 vm._isDestroyed = false; // 该实例是否正在被销毁 vm._isBeingDestroyed = false; }
最后简单讲讲抽象组件,在vue中有不少内置的抽象组件,例如<keep-alive></keep-alive>,<slot><slot>
等,这些抽象组件并不会出如今子父级的路径上,而且它们也不会参与DOM的渲染。
喜欢本系列的朋友欢迎关注公众号 假前端,有源码解析和算法精选哦