深刻剖析Vue源码 - 基础的数据代理检测

简单回顾一下这个系列的前两节,前两节花了大量的篇幅介绍了Vue的选项合并,选项合并是Vue实例初始化的开始,Vue为开发者提供了丰富的选项配置,而每一个选项都严格规定了合并的策略。然而这只是初始化中的第一步,这一节咱们将对另外一个重点的概念深刻的分析,他就是数据代理,咱们知道Vue大量利用了代理的思想,而除了响应式系统外,还有哪些场景也须要进行数据代理呢?这是咱们这节分析的重点。javascript

2.1 数据代理的含义

数据代理的另外一个说法是数据劫持,当咱们在访问或者修改对象的某个属性时,数据劫持能够拦截这个行为并进行额外的操做或者修改返回的结果。而咱们知道Vue响应式系统的核心就是数据代理,代理使得数据在访问时进行依赖收集,在修改更新时对依赖进行更新,这是响应式系统的核心思路。而这一切离不开Vue对数据作了拦截代理。然而响应式并非本节讨论的重点,这一节咱们将看看数据代理在其余场景下的应用。在分析以前,咱们须要掌握两种实现数据代理的方法: Object.definePropertyProxyhtml

2.1.1 Object.defineProperty

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

基本用法:java

Object.defineProperty(obj, prop, descriptor)
复制代码

Object.defineProperty()能够用来精确添加或修改对象的属性,只须要在descriptor对象中将属性特性描述清楚,descriptor的属性描述符有两种形式,一种是数据描述符,另外一种是存取描述符,咱们分别看看各自的特色。node

  1. 数据描述符,它拥有四个属性配置
  • configurable:数据是否可删除,可配置
  • enumerable:属性是否可枚举
  • value:属性值,默认为undefined
  • writable:属性是否可读写
  1. 存取描述符,它一样拥有四个属性选项
  • configurable:数据是否可删除,可配置
  • enumerable:属性是否可枚举
  • get:一个给属性提供 getter 的方法,若是没有 getter 则为 undefined
  • set:一个给属性提供 setter 的方法,若是没有 setter 则为 undefined

须要注意的是: 数据描述符的value,writable 和 存取描述符中的get, set属性不能同时存在,不然会抛出异常。 有了Object.defineProperty方法,咱们能够方便的利用存取描述符中的getter/setter来进行数据的监听,这也是响应式构建的雏形。getter方法可让咱们在访问数据时作额外的操做处理,setter方法使得咱们能够在数据更新时修改返回的结果。看看下面的例子,因为设置了数据代理,当咱们访问对象oa属性时,会触发getter执行钩子函数,当修改a属性的值时,会触发setter钩子函数去修改返回的结果。react

var o = {}
var value;
Object.defineProperty(o, 'a', {
    get() {
        console.log('获取值')
        return value
    },
    set(v) {
        console.log('设置值')
        value = qqq
    }
})
o.a = 'sss' 
// 设置值
console.log(o.a)
// 获取值
// 'qqq'

复制代码

前面说到Object.definePropertygetset方法是对对象进行监测并响应变化,那么数组类型是否也能够监测呢,参照监听属性的思路,咱们用数组的下标做为属性,数组的元素做为拦截对象,看看Object.defineProperty是否能够对数组的数据进行监控拦截。webpack

var arr = [1,2,3];
arr.forEach((item, index) => {
    Object.defineProperty(arr, index, {
        get() {
            console.log('数组被getter拦截')
            return item
        },
        set(value) {
            console.log('数组被setter拦截')
            return item = value
        }
    })
})

arr[1] = 4;
console.log(arr)
// 结果
数组被setter拦截
数组被getter拦截
4
复制代码

显然,**已知长度的数组是能够经过索引属性来设置属性的访问器属性的。**可是数组的添加确没法进行拦截,这个也很好理解,不论是经过arr.push()仍是arr[10] = 10添加的数据,数组所添加的索引值并无预先加入数据拦截中,因此天然没法进行拦截处理。这个也是使用Object.defineProperty进行数据代理的弊端。为了解决这个问题,Vue在响应式系统中对数组的方法进行了重写,间接的解决了这个问题,详细细节能够参考后续的响应式系统分析。es6

另外若是须要拦截的对象属性嵌套多层,若是没有递归去调用Object.defineProperty进行拦截,深层次的数据也依然没法监测。web

2.1.2 Proxy

为了解决像数组这类没法进行数据拦截,以及深层次的嵌套问题,es6引入了Proxy的概念,它是真正在语言层面对数据拦截的定义。和Object.defineProperty同样,Proxy能够修改某些操做的默认行为,可是不一样的是,Proxy针对目标对象会建立一个新的实例对象,并将目标对象代理到新的实例对象上,。 本质的区别是后者会建立一个新的对象对原对象作代理,外界对原对象的访问,都必须先经过这层代理进行拦截处理。而拦截的结果是咱们只要经过操做新的实例对象就能间接的操做真正的目标对象了。针对Proxy,下面是基础的写法:算法

var obj = {}
var nobj = new Proxy(obj, {
    get(target, key, receiver) {
        console.log('获取值')
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log('设置值')
        return Reflect.set(target, key, value, receiver)
    }
})

nobj.a = '代理'
console.log(obj)
// 结果
设置值
{a: "代理"}
复制代码

上面的get,setProxy支持的拦截方法,而Proxy 支持的拦截操做有13种之多,具体能够参照ES6-Proxy文档,前面提到,Object.definePropertygettersetter方法并不适合监听拦截数组的变化,那么新引入的Proxy又可否作到呢?咱们看下面的例子。

var arr = [1, 2, 3]
let obj = new Proxy(arr, {
    get: function (target, key, receiver) {
        // console.log("获取数组元素" + key);
        return Reflect.get(target, key, receiver);
    },
    set: function (target, key, receiver) {
        console.log('设置数组');
        return Reflect.set(target, key, receiver);
    }
})
// 1. 改变已存在索引的数据
obj[2] = 3
// result: 设置数组
// 2. push,unshift添加数据
obj.push(4)
// result: 设置数组 * 2 (索引和length属性都会触发setter)
// // 3. 直接经过索引添加数组
obj[5] = 5
// result: 设置数组 * 2
// // 4. 删除数组元素
obj.splice(1, 1)

复制代码

显然Proxy完美的解决了数组的监听检测问题,针对数组添加数据,删除数据的不一样方法,代理都能很好的拦截处理。另外Proxy也很好的解决了深层次嵌套对象的问题,具体读者能够自行举例分析。

2.2 initProxy

数据拦截的思想除了为构建响应式系统准备,它也能够为数据进行筛选过滤,咱们接着往下看初始化的代码,在合并选项后,vue接下来会为vm实例设置一层代理,这层代理能够为vue在模板渲染时进行一层数据筛选,这个过程究竟怎么发生的,咱们看代码的实现。

Vue.prototype._init = function(options) {
    // 选项合并
    ...
    {
        // 对vm实例进行一层代理
        initProxy(vm);
    }
    ...
}
复制代码

initProxy的实现以下:

// 代理函数
var initProxy = function initProxy (vm) {
    
    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

var hasProxy =
      typeof Proxy !== 'undefined' && isNative(Proxy);
复制代码

当浏览器支持Proxy时,vm._renderProxy会代理vm实例,而且代理过程也会随着参数的不一样呈现不一样的效果;当浏览器不支持Proxy时,直接将vm赋值给vm._renderProxy

读到这里,我相信你们会有不少的疑惑。 1. 这层代理的访问时机是什么,也就是说什么场景会触发这层代理 2. 参数options.render._withStripped表明着什么,getHandlerhasHandler又有什么不一样。 3. 如何理解为模板数据的访问进行数据筛选过滤。到底有什么数据须要过滤。 4. 只有在支持原生proxy环境下才会创建这层代理,那么在旧的浏览器,非法的数据又将如何展现。

带着这些疑惑,咱们接着往下分析。

2.2.1 触发代理

源码中vm._renderProxy的使用出如今Vue实例的_render方法中,Vue.prototype._render是将渲染函数转换成Virtual DOM的方法,这部分是关于实例的挂载和模板引擎的解析,笔者并不会在这一章节中深刻分析,咱们只须要先有一个认知,**Vue内部在js和真实DOM节点中设立了一个中间层,这个中间层就是Virtual DOM,遵循js -> virtual -> 真实dom的转换过程,而Vue.prototype._render是前半段的转换,**当咱们调用render函数时,代理的vm._renderProxy对象便会访问到。

Vue.prototype._render = function () {
    ···
    // 调用vm._renderProxy
    vnode = render.call(vm._renderProxy, vm.$createElement);
}
复制代码

那么代理的处理函数又是什么?咱们回过头看看代理选项handlers的实现。 handers函数会根据 options.render._withStripped的不一样执行不一样的代理函数,当使用相似webpack这样的打包工具时,一般会使用vue-loader插件进行模板的编译,这个时候options.render是存在的,而且_withStripped的属性也会设置为true(关于编译版本和运行时版本的区别能够参考后面章节),因此此时代理的选项是hasHandler,在其余场景下,代理的选项是getHandlergetHandler,hasHandler的逻辑类似,咱们只分析使用vue-loader场景下hasHandler的逻辑。另外的逻辑,读者能够自行分析。

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
}
复制代码

那么这两个触发条件是否跟_render过程有直接的关系呢?答案是确定的。vnode = render.call(vm._renderProxy, vm.$createElement);的主体是render函数,而这个render函数就是包装成with的执行语句,**在执行with语句的过程当中,该做用域下变量的访问都会触发has钩子,这也是模板渲染时之全部会触发代理拦截的缘由。**咱们经过代码来观察render函数的原形。

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))])}
}
复制代码

2.2.2 数据过滤

咱们已经大体知道了Proxy代理的访问时机,那么设置这层代理的做用又在哪里呢?首先思考一个问题,咱们经过data选项去设置实例数据,那么这些数据能够随着我的的习惯任意命名吗?显然不是的,若是你使用js的关键字(像Object,Array,NaN)去命名,这是不被容许的。另外一方面,Vue源码内部使用了以$,_做为开头的内部变量,因此以$,_开头的变量名也是不被容许的,这就构成了数据过滤监测的前提。接下来咱们具体看hasHandler的细节实现。

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内部保留属性的开头。
        // 1. warnReservedPrefix: 警告不能以$ _开头的变量
        // 2. 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
);
复制代码

首先allowedGlobals定义了javascript保留的关键字,这些关键字是不容许做为用户变量存在的。(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data)的逻辑对以$,_开头,或者是不是data中未定义的变量作判断过滤。这里对未定义变量的场景多解释几句,前面说到,代理的对象vm.renderProxy是在执行_render函数中访问的,而在使用了template模板的状况下,render函数是对模板的解析结果,换言之,之因此会触发数据代理拦截是由于模板中使用了变量,例如<div>{{message}}}</div>。而若是咱们在模板中使用了未定义的变量,这个过程就被proxy拦截,并定义为不合法的变量使用。

咱们能够看看两个报错信息的源代码(是否是很熟悉):

// 模板使用未定义的变量
var warnNonPresent = function (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
    );
};

// 使用$,_开头的变量
var warnReservedPrefix = function (target, key) {
    warn(
    "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " +
    'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
    'prevent conflicts with Vue internals' +
    'See: https://vuejs.org/v2/api/#data',
    target
    );
};
复制代码

分析到这里,前面的疑惑只剩下最后一个问题。只有在浏览器支持proxy的状况下,才会执行initProxy设置代理,那么在不支持的状况下,数据过滤就失效了,此时非法的数据定义还能正常运行吗?咱们先对比下面两个结论。

// 模板中使用_开头的变量,且在data选项中有定义
<div id="app">{{_test}}</div>
new Vue({
    el: '#app',
    data: {
        _test: 'proxy'
    }
})
复制代码
  1. 支持proxy浏览器的结果

  1. 不支持proxy浏览器的结果

显然,在没有通过代理的状况下,使用_开头的变量依旧会 报错,可是它变成了js语言层面的错误,表示该变量没有被声明。可是这个报错没法在Vue这一层知道错误的详细信息,而这就是能使用Proxy的好处。接着咱们会思考,既然已经在data选项中定义了_test变量,为何访问时仍是找不到变量的定义呢? 原来在初始化数据阶段,Vue已经为数据进行了一层筛选的代理。具体看initData对数据的代理,其余实现细节不在本节讨论范围内。

function initData(vm) {
    vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
    if (!isReserved(key)) {
        // 数据代理,用户可直接经过vm实例返回data数据
        proxy(vm, "_data", key);
    }
}

function isReserved (str) {
    var c = (str + '').charCodeAt(0);
    // 首字符是$, _的字符串
    return c === 0x24 || c === 0x5F
  }
复制代码

vm._data能够拿到最终data选项合并的结果,isReserved会过滤以$,_开头的变量,proxy会为实例数据的访问作代理,当咱们访问this.message时,实际上访问的是this._data.message,而有了isReserved的筛选,即便this._data._test存在,咱们依旧没法在访问this._test时拿到_test变量。这就解释了为何会有变量没有被声明的语法错误,而proxy的实现,又是基于上述提到的Object.defineProperty来实现的。

function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
        // 当访问this[key]时,会代理访问this._data[key]的值
        return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
        this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码

2.3 小结

这一节内容,详细的介绍了数据代理在Vue的实现思路和另外一个应用场景,数据代理是一种设计模式,也是一种编程思想,Object.definePropertyProxy均可以实现数据代理,可是他们各有优劣,前者兼容性较好,可是却没法对数组或者嵌套的对象进行代理监测,而Proxy基本能够解决全部的问题,可是对兼容性要求很高。Vue中的响应式系统是以Object.defineProperty实现的,可是这并不表明没有Proxy的应用。initProxy就是其中的例子,这层代理会在模板渲染时对一些非法或者没有定义的变量进行筛选判断,和没有数据代理相比,非法的数据定义错误会提早到应用层捕获,这也有利于开发者对错误的排查。


相关文章
相关标签/搜索