2.2 initProxy

有了这些理论基础,我们往下看vue的源码,在初始化合并选项后,vue接下来的操作是为vm实例设置一层代理,代理的目的是为vue在模板渲染时进行一层数据筛选。如果浏览器不支持Proxy,这层代理检验数据则会失效。(检测数据会放到其他地方检测)

  1. {
  2. // 对vm实例进行一层代理
  3. initProxy(vm);
  4. }
  5. // 代理函数
  6. var initProxy = function initProxy (vm) {
  7. // 浏览器如果支持es6原生的proxy,则会进行实例的代理,这层代理会在模板渲染时对一些非法或者不存在的字符串进行判断,做数据的过滤筛选。
  8. if (hasProxy) {
  9. var options = vm.$options;
  10. var handlers = options.render && options.render._withStripped
  11. ? getHandler
  12. : hasHandler;
  13. // 代理vm实例到vm属性_renderProxy
  14. vm._renderProxy = new Proxy(vm, handlers);
  15. } else {
  16. vm._renderProxy = vm;
  17. }
  18. };
  19. 如何判断浏览器支持原生proxy
  20. // 是否支持Symbol 和 Reflect
  21. var hasSymbol =
  22. typeof Symbol !== 'undefined' && isNative(Symbol) &&
  23. typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys);
  24. function isNative (Ctor) {
  25. // Proxy本身是构造函数,且Proxy.toString === 'function Proxy() { [native code] }'
  26. return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
  27. }

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

  • 什么时候会触发这层代理进行数据检测?
  • getHandler 和 hasHandler的场景分别是什么?要解决这个疑惑,我们接着往下看:

  • 1.在组件的更新渲染时会调用vm实例的render方法(具体模板引擎如何工作,我们放到相关专题在分析),我们观察到,vm实例的render方法在调用时会触发这一层的代理。

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

也就是说模板引擎<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的分析类似。

  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. }

而在vue的render函数的内部,本质上也是调用了with语句,当调用with语句时,该作用域下变量的访问都会触发has钩子,这也是模板渲染时会触发代理拦截的原因。

  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. }

再次思考:我们知道with语句是不推荐使用的,一个最主要的原因是性能问题,查找不是变量属性的变量,较慢的速度会影响性能一系列性能问题。

官方给出的解释是: 为了减少编译器代码大小和复杂度,并且也提供了通过vue-loader这类构建工具,不含with的版本。

2.2.2 代理检测过程

接着上面的分析,在模板引擎render渲染时,由于with语句的存在,访问变量时会触发has钩子函数,该函数会进行数据的检测,比如模板上的变量是否是实例中所定义,是否包含_, $这类vue内部保留关键字为开头的变量。同时模板上的变量将允许出现javascript的保留变量对象,例如Math, Number, parseFloat等。

  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. // warnReservedPrefix警告不能以$ _开头的变量
  9. // 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. );