前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue数据代理检测(源码)

Vue数据代理检测(源码)

作者头像
奋飛
发布2020-05-28 17:04:13
2.9K0
发布2020-05-28 17:04:13
举报
文章被收录于专栏:Super 前端Super 前端

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

从一个告警说起

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

[Vue warn]: Property myName must be accessed with data.myNamebecausepropertiesstartingwith"data._myName because properties starting with "data.m​yNamebecausepropertiesstartingwith"" 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官网

如何达到效果

在这里插入图片描述
在这里插入图片描述

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

  • Object.defineProperty():利用存取描述符中的getter/setter来进行数据的监听 对于数组索引的新增等,Object.defineProperty() 不具备代理的能力,Vue在响应式系统中对数组的方法进行了重写,间接的解决了这个问题。-- https://github.com/vuejs/vue/blob/v2.6.11/dist/vue.js#L860 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 () { return this._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
);
  1. $/_开头,或者是否是data中未定义的变量做判断过滤
 (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))

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

  1. 错误提示
  • 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
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-02-13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从一个告警说起
  • 为什么这样设计
  • 如何达到效果
    • 第一条线路:初始化(数据&代理)
      • 第二条线路:模板渲染(触发代理)
        • hasHandler L2085
          • 不支持 proxy 的情况
          • 补充
          • 参考地址
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档