scrapy使用代理报错keyerror: proxy_Vue数据代理检测(源码)

f26ddf98fcf560efddf28ee37fc4a2ca.png

阅读源码通常是枯燥无味的,类似 Vue 这种框架级的,代码量更是巨大;且各个实现之间关联性很大,跟踪源码非常跳跃,看完后总是稀里糊涂。今天,从一个常见的错误说起,与使用场景相结合,带着目的去查看源码。

从一个告警说起

Vue 工程中,在 data 对象中,使用 _& 开头命名变量,且将该变量应用到模板中,会收到如下警告(开发模式下):

[Vue warn]: Property myName must be accessed with $data._myName because properties starting with "$" or "" are not proxied in the Vue instance to prevent conflicts with Vue internals.
const app = new Vue({
  el: '#test',
  data () {
    return {
      _myName: 'ligang'
    }
  }
})

注意:上面的限定词(模板中使用!)

  1. 在 data 中声明变量,并不会报错(如,上述 _myName
  2. 在非模板中使用,不会报错,但会返回 undefined
created () {
    console.log(this._myName)           // undefined
    console.log(this.$data._myName) // ligang
}
  1. 在模板中使用,会报错
<div id="#test">
  <span>{{_myName}}</span>       <!-- 报错 -->
  <span>{{$data._myName}}</span> <!-- 可以正常渲染 -->
</div>
查阅 Vue 源码之前,先思考,为什么要这样设计?以及如何才能达到上述的效果?

为什么这样设计

_$ 开头的属性 不会 被 Vue 实例代理,因为它们可能和 Vue 内置的属性、API 方法冲突。你可以使用例如 vm.$data._property 的方式访问这些属性。 -- Vue官网

如何达到效果

47844f0bed03a69a2420df1ea603cda1.png

通过数据代理(劫持) 实现!访问或者修改对象的某个属性时,拦截这个行为并进行额外的操作或者修改返回的结果(在访问时进行依赖收集,在修改更新时对依赖进行更新),这也是 Vue 响应式系统的核心

  • Object.defineProperty():利用存取描述符中的getter/setter来进行数据的监听
对于数组索引的新增等, Object.defineProperty() 不具备代理的能力, Vue在响应式系统中对数组的方法进行了重写,间接的解决了这个问题。--
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

var methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
];

methodsToPatch.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];

    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    ob.dep.notify();
    return result
  });
});
  • Proxy:针对目标对象会创建一个新的实例对象,并将目标对象代理到新的实例对象上(通过操作新的实例对象就能间接的操作真正的目标对象了

第一条线路:初始化(数据&代理)

Vue 对 vm 实例设置代理,为 vue 在模板渲染前做数据筛选。

Vue.prototype._init:L4991

Vue.prototype._init = function (options) {
  initProxy(vm);
  initState(vm);
}
  1. initProxy:L2108
initProxy = function initProxy (vm) {
  if (hasProxy) {   // 是否支 持Proxy,typeof Proxy !== 'undefined' && isNative(Proxy)
    var options = vm.$options;
    var handlers = options.render && options.render._withStripped
    ? getHandler
    : hasHandler;
    vm._renderProxy = new Proxy(vm, handlers);
  } else {
    vm._renderProxy = vm;
  }
};
  • 当浏览器支持 Proxy 时,vm._renderProxy 会代理 vm 实例
  • 当浏览器不支持 Proxy 时,直接将 vm 赋值给 vm._renderProxy
getHandler & hasHandler:
  • getHandler:_withStripped 为 true 的情况,单元测试中存在,其他地方暂未发现(欢迎大家补充);
  • hasHandler: has (target, key) 重点关注
  • 属性查询: foo in proxy
  • 继承属性查询: foo in Object.create(proxy)
  • with 检查:with(proxy) { (foo) }
  • Reflect.has()
  1. initState:L5000
function initState (vm) { 
  initData(vm); 
}

initData(isReserved):L4733

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
}

基于上述提到的 Object.defineProperty 来实现的。

function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

this._myName 实际访问的是 this._data._myName ,以 $, _ 开头,没有被代理,所以无法通过 this._myName 访问到。

为什么 this.$data._myName 可以访问: L4925
Object.defineProperty(Vue.prototype,'$data', 
dataDef dataDef.get=function(){returnthis._data };

第二条线路:模板渲染(触发代理)

触发数据代理拦截是因为模板中使用了变量{{_myName}}}。而如果我们在模板中使用了未定义的变量,这个过程就被. proxy 拦截,并定义为不合法的变量使用

模板 ==> AST ==> render函数 ==> vnode对象(virtual dom) ==> 真实Dom

由于模板使用了变量 vm._renderProxy 被调用(接上述)。Vue.prototype._render:L3527

Vue.prototype._render = function () {
    vnode = render.call(vm._renderProxy, vm.$createElement);
}

通过 render 函数,生成 with 包裹的执行语句。

console.log(app.$options.render)

//输出, 模板渲染使用with语句
ƒ anonymous() {
    with(this){return _c('div',{attrs:{"id":"test"}},[_c('span',[_v(_s(_myName))])])}
}

在执行 with 语句的过程中,该作用域下变量的访问都会触发上述 has 钩子,这也是模板渲染时之所有会触发代理拦截的原因!

hasHandler L2085

var hasHandler = {
  has: function has (target, key) {
    var has = key in target;
    var isAllowed = allowedGlobals(key) ||
        (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
    if (!has && !isAllowed) {
      if (key in target.$data) { warnReservedPrefix(target, key); }
      else { warnNonPresent(target, key); }
    }
    return has || !isAllowed
  }
};
  1. 模板中允许出现的非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.以$/_开头,或者是否是data中未定义的变量做判断过滤

(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))

注意,这里并没有 $ 了啊,这要具体看 initData L4733

3. 错误提示

  • warnReservedPrefix:开头处报的错误
  • warnNonPresent:未定义

不支持 proxy 的情况

数据过滤就失效,直接跑错 ReferenceError: _myName is not defined js 语法错误。Vue 层面无法做拦截,报告详细的错误信息。

补充

上述遗漏了关于直接使用 render 函数的情况。

render(createElement){
    return createElement('div', {'class': {
      info: true
    }}, [
      `<span>${this._myName}</span>`
    ])
  }

由于提供了相关 render 函数,所以不会生成上述 with 语句 function () {with(){}},因此不会被 hasHandler 拦截,也不会报错!直接返回undefined(初始化数据仍然会被拦截)。

参考地址

  • https://cn.vuejs.org/v2/api/#data
  • https://github.com/vuejs/vue/blob/v2.6.11/dist/vue.js
  • https://ocean1509.github.io/In-depth-analysis-of-Vue/src/%E5%9F%BA%E7%A1%80%E7%9A%84%E6%95%B0%E6%8D%AE%E4%BB%A3%E7%90%86%E6%A3%80%E6%B5%8B.html
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值