深刻剖析Vue源码 - 数据代理,关联子父组件

简单回顾一下这个系列的前两节,前两节花了大篇幅讲了vue在初始化时进行的选项合并。选项配置是vue实例化的第一步,针对不一样类型的选项,vue提供的丰富选项配置策略以保证用户可使用不一样丰富的配置选项。而在这一节中,咱们会分析选项合并后的又两步重要的操做: 数据代理和关联子父组件关系,分别对应的处理过程为initProxy和initLifecycle。这章节的知识点也为后续的响应式系统介绍和模板渲染作铺垫。

2.1 Object.defineProperty和Proxy

在介绍这一章的源码分析以前,咱们须要掌握一下贯穿整个vue数据代理,监控的技术核心:Object.defineProperty 和 Proxyjavascript

2.1.1 Object.defineProperty
官方定义:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
基本用法: Object.defineProperty(obj, prop, descriptor)

咱们能够用来精确添加或修改对象的属性,只须要在descriptor中将属性特性描述清楚,descriptor的属性描述符有两种形式,一种是数据描述符,另外一种是存取描述符。前端

数据描述符vue

  • configurable:数据是否可删除
  • enumerable:属性是否可枚举
  • value:属性值,默认为undefined
  • writable:属性是否可读写

存取描述符java

  • configurable:数据可改变
  • enumerable:可枚举
  • get:一个给属性提供 getter 的方法,若是没有 getter 则为 undefined。
  • set:一个给属性提供 setter 的方法,若是没有 setter 则为 undefined。

注意: 数据描述符的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

2.1.2 Proxy

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

2.2 initProxy

有了这些理论基础,咱们往下看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())
}

看到这里时,心中会有几点疑惑。

  • 何时会触发这层代理进行数据检测?
  • getHandler 和 hasHandler的场景分别是什么?

要解决这个疑惑,咱们接着往下看:

  • 1.在组件的更新渲染时会调用vm实例的render方法(具体模板引擎如何工做,咱们放到相关专题在分析),咱们观察到,vm实例的render方法在调用时会触发这一层的代理。
Vue.prototype._render = function () {
    ···
    // 调用vm._renderProxy
    vnode = render.call(vm._renderProxy, vm.$createElement);
}

也就是说模板引擎<div>{{message}}</div>的渲染显示,会经过Proxy这层代理对数据进行过滤,并对非法数据进行报错提醒。

  • 2.handers函数会根据options.render 和 options.render._withStripped执行不一样的代理函数getHandler,hasHandler。当使用相似webpack这样的打包工具时,咱们将使用vue-loader进行模板编译,这个时候options.render 是存在的,而且_withStripped的属性也会设置为true,关于编译版本和运行版本的区别不在这一章节展开。先大体了解使用场景便可。
2.2.1 代理场景

接着上面的问题,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的版本。

2.2.2 代理检测过程

接着上面的分析,在模板引擎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
);

2.3 initLifecycle

分析完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的渲染。

喜欢本系列的朋友欢迎关注公众号 假前端,有源码解析和算法精选哦

clipboard.png

相关文章
相关标签/搜索