前言
Vue 作为国内炙手可热的前端框架,重要性不言而喻。尤其渐进式对前端新手都是开发体验非常友好的,因此将它的任何细节和特性单拎出来重点讲解,我认为都是不过分的。
在我们平时写 vue2 的时候,经常使用 this.xxx
来访问 data 里面定义的数据,那么问题来了?按照我们以往的常识,这不太像是 JavaScript 自身的 this 特性吧?所以本次,我们来揭开它这层薄薄的面纱。
如果读者已经了解了该功能的实现原理,建议可以跳过文章的前半部分,不用浪费阅读时间。直接看下面 我的探究方法 段落,去了解一些不同的想法。
现象到本质
这里我们有一个数据 message ,有一个方法 showMessage 用来打印 message。问题在于:这里的 this 如果要访问 message 按理来说,应该是 取不到的,因为this指向methods? 而 vue 神奇的通过 this 直接拿到,难道有什么魔法嘛?
let vm = new Vue({ el:"#app", data:{ message:'Deno' }, methods:{ showMessage(){ debugger; console.log(this.message) } }})
上述代码,我们先来 debugger 一下,看看这个地方的 this 到底有什么神奇之处?
呃,似乎看不出来有什么奇怪的地方。this不是methods,而是vm实例。而this 确实里面有 message 属性?而 _data 里面也有一份。从我们浅薄的概念里,_data 里这份不奇怪,奇怪的是 this 这份哪来的?接着往下翻,魔法就暴露出来了
发现没?调试工具告诉我们,this 身上的 message 是被劫持走的,所以真正的逻辑在这里,我们点击 FunctionLocation 进入到函数定义的源码部分:
var sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop};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); }
从源码里,有经验的同学大概一眼可以看出端倪。但如果暂时没有看出来,也没关系。在这里打上断点,一探究竟!
可以很清晰的看到,这里最终就是使用了 _data 里面的值,它只是做了一层转发,最终使用了 Object.defineProperty(target, key, sharedPropertyDefinition);
跟我们 vue2 的响应式原理其实是一样的。所以当我们获取 this 身上部分 data 里面定义的值,就可以直接拿到啦!是不是非常简单?
追本溯源
data
前面我很容易的就定位到了,根本原因。但是,前面的推理,其实是有 bug 的。细心的同学会嘀咕一件事:你怎么老在说 _data ,我代码里面明明写的是 data,这里是怎么在 vue 里面变成 _data 的呢?没讲清楚呀!
是的,这就是我们现在要解释的问题了。为了回答清楚这个问题,还是需要从 new 一个 vue 实例来说起了。
中间的过程,我想本文并不涉及太多,如果大家关注的话,可以看 若川 的引文。我这里直接调试到了最终这个逻辑的关键部分:
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; ......
最关键的代码就是 data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
我们可以看到,data 最后还是会赋值给 _data。
顺便我们可以了解一个知识点:vue 中 data 可能是一个函数。
getData 这里会执行一下这个函数,返回一个新的 data 对象。那为什么需要这么做呢?因为 vue 组件可能会有多个实例。如果共用一个 data 会导致内部混乱。所以每次返回一个新的 data 。这样每个组件都有自己的 data 。而根组件往往只有一个,所以我前面 new Vue 的时候 data 写不写成函数形式,都无妨。不过为了养成良好的习惯,这里建议还是写成函数形式。
methods
前面介绍完了 data ,接下来我们看看 methods 。如果大家有仔细调试前面的 data 源码部分,应该在中间很容易发现这样一段代码:
function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } if (opts.methods) { initMethods(vm, opts.methods); }//这里在初始化方法 if (opts.data) { initData(vm);//这里初始化数据 } else { ......
很明显,methods 方法的处理就在 initMethods 函数中了。我们进去看看......
function initMethods (vm, methods) { var props = vm.$options.props; for (var key in methods) { ...... vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm); ...... } }
解释下这段逻辑:循环拿到 methods 里面的每一个方法名字,如果是个函数就执行 bind 函数。如果不是,则绑定一个空的 noop 函数(这里也能知道,如果 methods 写了无效参数,也会给一个兜底的函数)。
function noop (a, b, c) {}
/* istanbul ignore next */ function polyfillBind (fn, ctx) { function boundFn (a) { var l = arguments.length; return l ? l > 1 ? fn.apply(ctx, arguments)//大于1个参数,那么直接可以apply传数组了 : fn.call(ctx, a) : fn.call(ctx) } boundFn._length = fn.length;//这是我比较佩服的地方,vue 很严谨还特意留了一份长度,函数的len一般都是参数。(普通形参才算,es6的默认参数用法出现,则包括它自身的后续都不算长度)。这里vue将length的值也修正了。但是注意,是修正给了下划线length。自身的length是不能修改的。 return boundFn } function nativeBind (fn, ctx) { return fn.bind(ctx)//绑定实例 vm 成为方法的第一个参数 } var bind = Function.prototype.bind ? nativeBind : polyfillBind;
在这里,我们很容易发现 vue 做的还是比较好的。它考虑到了如果不存在 bind 情况,那么就需要自己实现一个。
注释里也都写了,我想直接用一句话就可以概括实现的核心思想:bind 相当于一个未执行的函数里面存放着call/apply等待被执行,传入的第一个参数都是改变 this。 基于此逻辑实现的 bind 都是成立的。
至此,我们总算是解释完了标题里提到的这两点。
疑问扩展
为什么 methods 可以用这种逻辑赋值的方式给实例?而 data 却要用 Object.defineProperty 他两用一个逻辑不行吗?
data 不能学 methods 这样的处理逻辑:
methods 之所以这样赋值,由于函数本身是对象,赋值的是一个引用关系。而非真正的值拷贝。如果 data 使用这样的方式,如果里面储存的是原始值。那么就会导致产生副本了。这是一个糟糕的 bug !
methods 不能学 data Object.defineProperty 处理:
由于methods还需要处理this指向重新bind,这种是不太合适的。参考网上 Object.defineProperty 缺陷类文章。主要出于性能方面考量。能不用尽量不用。
我的探究方法
先学门外汉
当我们对框架里某个特性特别感兴趣的时候,大家的第一反应往往是看着漫长的源码,望而生畏,最后不了了之。
这样很容易放弃,我比较推荐的办法是:先理清这个特性如何使用,尽可能详细的分析出它的行为。一定要充分细致的了解,这样才能够调试的时候,迅速抓住问题的“根”。
学会借力
经常看到有同学看到问题。还没去网上查,自己先琢磨1-2小时,结果一看,人家早就写了文章清清楚楚的解答了,顿时心里是不是落差挺大的。比较推荐的办法是,遇到问题,百度或者 Google 并不丢人。不要吝啬使用自己的搜索引擎。如果这个办法行不通,大胆的群里找大佬们问。总之,面子不值钱,技术值钱哈(笑~)。
探索的路上并不孤独
有些问题很难描述给别人听,或者不知道怎么搜索答案。我想大家都会感到一种孤独或无助感,没有关系。试着去分解问题。开始源码调试前,务必先搞清楚自己是否对某个 API 已经熟练使用了。
在 debugger 的时候,可以像本文中这样,先从问题的点出发,反过来再去寻找哪里引发的。这种方式我承认确实反直觉,但是某些情况下真的很好用。
当 debugger 到了看不懂的地方,这个时候像同伴们发起提问,我想会更有效率一些。因为大家知道你卡的点在哪里了,而不是看到你漫长的文字描述问题。大佬们一般看代码比看文字要省力。
相信大家如果能理解我说的办法,那么找问题肯定是事半功倍的。如果有更好的方法论,也欢迎你跟我分享,我们共同进步!
原文:https://juejin.cn/post/7099387400207482917