关于前端:vue源码分析基础的数据代理检测

32次阅读

共计 8952 个字符,预计需要花费 23 分钟才能阅读完成。

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

2.1 数据代理的含意

数据代理的另一个说法是数据劫持,当咱们在拜访或者批改对象的某个属性时,数据劫持能够拦挡这个行为并进行额定的操作或者批改返回的后果。而咱们晓得 Vue 响应式零碎的外围就是数据代理,代理使得数据在拜访时进行依赖收集,在批改更新时对依赖进行更新,这是响应式零碎的外围思路。而这所有离不开 Vue 对数据做了拦挡代理。然而响应式并不是本节探讨的重点,这一节咱们将看看数据代理在其余场景下的利用。在剖析之前,咱们须要把握两种实现数据代理的办法:Object.definePropertyProxy

2.1.1 Object.defineProperty

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

根本用法:

Object.defineProperty(obj, prop, descriptor)

Object.defineProperty()能够用来准确增加或批改对象的属性,只须要在 descriptor 对象中将属性个性形容分明,descriptor的属性描述符有两种模式,一种是数据描述符,另一种是存取描述符,咱们别离看看各自的特点。

  1. 数据描述符,它领有四个属性配置
  2. configurable:数据是否可删除,可配置
  3. enumerable:属性是否可枚举
  4. value:属性值, 默认为undefined
  5. writable:属性是否可读写
  6. 存取描述符,它同样领有四个属性选项
  7. configurable:数据是否可删除,可配置
  8. enumerable:属性是否可枚举
  9. get: 一个给属性提供 getter 的办法,如果没有 getter 则为 undefined
  10. set: 一个给属性提供 setter 的办法,如果没有 setter 则为 undefined

须要留神的是: 数据描述符的 value,writable 和 存取描述符中的get, set 属性不能同时存在,否则会抛出异样。 有了 Object.defineProperty 办法,咱们能够不便的利用存取描述符中的 getter/setter 来进行数据的监听, 这也是响应式构建的雏形。getter办法能够让咱们在拜访数据时做额定的操作解决,setter办法使得咱们能够在数据更新时批改返回的后果。看看上面的例子, 因为设置了数据代理,当咱们拜访对象 oa属性时,会触发 getter 执行钩子函数,当批改 a 属性的值时,会触发 setter 钩子函数去批改返回的后果。

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 是否能够对数组的数据进行监控拦挡。

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在响应式零碎中对数组的办法进行了重写,间接的解决了这个问题,具体细节能够参考后续的响应式系统分析。

另外如果须要拦挡的对象属性嵌套多层,如果没有递归去调用 Object.defineProperty 进行拦挡,深层次的数据也仍然无奈监测。

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就是其中的例子,这层代理会在模板渲染时对一些非法或者没有定义的变量进行筛选判断,和没有数据代理相比,非法的数据定义谬误会提前到应用层捕捉,这也有利于开发者对谬误的排查。

正文完
 0