专栏首页Super 前端Vue数据代理检测(源码)

Vue数据代理检测(源码)

阅读源码通常是枯燥无味的,类似 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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 动态执行脚本

    提到动态执行脚本,大家想到的肯定是 eval 或 new Function(),在 nodejs 中有专属的 vm 模块,可以完成相应的 sandbox 作用。

    奋飛
  • JavaScript设计模式--享元模式

    享元(flyweight)模式是一种用于性能优化的模式,核心是运用共享技术来有效支持大量细刻度的对象。 在JavaScript中,浏览器特别是移动端的浏览器分...

    奋飛
  • 前端模块系统

    这是最原始的 JavaScript 文件加载方式,如果把每一个文件看做是一个模块,那么他们的接口通常是暴露在全局作用域下,也就是定义在 window 对象中,不...

    奋飛
  • 谈谈基于SQL Server 的Exception Handling[中篇]

    三、TRY CATCH & Return 在上面一节中,我通过RAISERROR重写了创建User的Stored procedure,实际上上面的Stored ...

    蒋金楠
  • 如何用Web3.jsAPI在页面中进行转账

    本文介绍如何使用Web3.js API 在页面中进行转账,是我翻译的文档Web3.js 0.2x 中文版 及 区块链全栈-以太坊DAPP开发实战 中Demo的文...

    Tiny熊
  • @ModelAttribute注解使用1 注释方法2 注释一个方法的参数

    被@ModelAttribute注释的方法会在此controller每个方法执行前被执行,因此对于一个controller映射多个URL的用法来说,要谨慎使用。

    JavaEdge
  • 报道称实体安全密钥是谷歌员工避免网络钓鱼的秘密

    据外媒CNET报道,事实证明,谷歌员工避免网络钓鱼的关键是一个真正的密钥。 Krebs on Security上周报道称,该公司于2017年初开始使用基于物理U...

    周俊辉
  • 偶遇FFMpeg(四)-FFmpeg PC端推流

    之前在Android集成FFmpeg。主要还是基于命令行的方式进行操作。刚刚好最近又在研究推流相关的东西。看了一些博文。和做了一些实践。 就希望通过本文记录袭...

    deep_sadness
  • 《Redis篇:》《Redis的安全问题--->设置密码,不当黄金矿工》

    创建数据卷映射Redis容器的目录 添加配置文件:redis.conf,并编写上述配置

  • python 脚本实现查看文件内容

    py3study

扫码关注云+社区

领取腾讯云代金券