• 2.2 initProxy
    • 2.2.1 触发代理
    • 2.2.2 数据过滤

    2.2 initProxy

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

    1. Vue.prototype._init = function(options) {
    2. // 选项合并
    3. ...
    4. {
    5. // 对vm实例进行一层代理
    6. initProxy(vm);
    7. }
    8. ...
    9. }

    initProxy的实现如下:

    1. // 代理函数
    2. var initProxy = function initProxy (vm) {
    3. if (hasProxy) {
    4. var options = vm.$options;
    5. var handlers = options.render && options.render._withStripped
    6. ? getHandler
    7. : hasHandler;
    8. // 代理vm实例到vm属性_renderProxy
    9. vm._renderProxy = new Proxy(vm, handlers);
    10. } else {
    11. vm._renderProxy = vm;
    12. }
    13. };

    首先是判断浏览器是否支持原生的proxy

    1. var hasProxy =
    2. 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对象便会访问到。

    1. Vue.prototype._render = function () {
    2. ···
    3. // 调用vm._renderProxy
    4. vnode = render.call(vm._renderProxy, vm.$createElement);
    5. }

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

    1. var hasHandler = {
    2. // key in obj或者with作用域时,会触发has的钩子
    3. has: function has (target, key) {
    4. ···
    5. }
    6. };

    hasHandler函数定义了has的钩子,前面介绍过,proxy的钩子有13个之多,而has是其中一个,它用来拦截propKey in proxy的操作,返回一个布尔值。而除了拦截 in 操作符外,has钩子同样可以用来拦截with语句下的作用对象。例如:

    1. var obj = {
    2. a: 1
    3. }
    4. var nObj = new Proxy(obj, {
    5. has(target, key) {
    6. console.log(target) // { a: 1 }
    7. console.log(key) // a
    8. return true
    9. }
    10. })
    11. with(nObj) {
    12. a = 2
    13. }

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

    1. var vm = new Vue({
    2. el: '#app'
    3. })
    4. console.log(vm.$options.render)
    5. //输出, 模板渲染使用with语句
    6. ƒ anonymous() {
    7. with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(message)+_s(_test))])}
    8. }

    2.2.2 数据过滤

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

    1. var hasHandler = {
    2. has: function has (target, key) {
    3. var has = key in target;
    4. // isAllowed用来判断模板上出现的变量是否合法。
    5. var isAllowed = allowedGlobals(key) ||
    6. (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
    7. // _和$开头的变量不允许出现在定义的数据中,因为他是vue内部保留属性的开头。
    8. // 1. warnReservedPrefix: 警告不能以$ _开头的变量
    9. // 2. warnNonPresent: 警告模板出现的变量在vue实例中未定义
    10. if (!has && !isAllowed) {
    11. if (key in target.$data) { warnReservedPrefix(target, key); }
    12. else { warnNonPresent(target, key); }
    13. }
    14. return has || !isAllowed
    15. }
    16. };
    17. // 模板中允许出现的非vue实例定义的变量
    18. var allowedGlobals = makeMap(
    19. 'Infinity,undefined,NaN,isFinite,isNaN,' +
    20. 'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
    21. 'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,' +
    22. 'require' // for Webpack/Browserify
    23. );

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

    我们可以看看两个报错信息的源代码(是不是很熟悉):

    1. // 模板使用未定义的变量
    2. var warnNonPresent = function (target, key) {
    3. warn(
    4. "Property or method \"" + key + "\" is not defined on the instance but " +
    5. 'referenced during render. Make sure that this property is reactive, ' +
    6. 'either in the data option, or for class-based components, by ' +
    7. 'initializing the property. ' +
    8. 'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
    9. target
    10. );
    11. };
    12. // 使用$,_开头的变量
    13. var warnReservedPrefix = function (target, key) {
    14. warn(
    15. "Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " +
    16. 'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
    17. 'prevent conflicts with Vue internals' +
    18. 'See: https://vuejs.org/v2/api/#data',
    19. target
    20. );
    21. };

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

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

    2.2 initProxy - 图1

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

    2.2 initProxy - 图2

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

    1. function initData(vm) {
    2. vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
    3. if (!isReserved(key)) {
    4. // 数据代理,用户可直接通过vm实例返回data数据
    5. proxy(vm, "_data", key);
    6. }
    7. }
    8. function isReserved (str) {
    9. var c = (str + '').charCodeAt(0);
    10. // 首字符是$, _的字符串
    11. return c === 0x24 || c === 0x5F
    12. }

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

    1. function proxy (target, sourceKey, key) {
    2. sharedPropertyDefinition.get = function proxyGetter () {
    3. // 当访问this[key]时,会代理访问this._data[key]的值
    4. return this[sourceKey][key]
    5. };
    6. sharedPropertyDefinition.set = function proxySetter (val) {
    7. this[sourceKey][key] = val;
    8. };
    9. Object.defineProperty(target, key, sharedPropertyDefinition);
    10. }