源码揭秘为什么 Vue2 this 能够直接获取到 data 和
目录
- 1. 示例:this 能够直接获取到 data 和 methods
- 2. 准备环境调试源码一探究竟
- 2.1 Vue 构造函数
- 2.2 _init 初始化函数
- 2.3 initState 初始化状态
- 2.4 initMethods 初始化方法
- 2.4.1 bind 返回一个函数,修改 this 指向
- 2.5 initData 初始化 data
- 2.5.1 getData 获取数据
- 2.5.2 proxy 代理
- 2.5.3 Object.defineProperty 定义对象属性
- 2.6 文中出现的一些函数,最后统一解释下
- 2.6.1 hasOwn 是否是对象本身拥有的属性
- 2.6.2 isReserved 是否是内部私有保留的字符串$ 和 _ 开头
- 3. 最后用60余行代码实现简化版
- 4. 总结
1. 示例:this 能够直接获取到 data 和 methods
举例:
const vm = new Vue({ data: { name: '我是若川', }, methods: { sayName(){ console.log(this.name); } }, }); console.log(vm.name); // 我是若川 console.log(vm.sayName()); // 我是若川
这样是可以输出我是若川的。好奇的人就会思考为啥 this 就能直接访问到呢。
那么为什么 this.xxx
能获取到data
里的数据,能获取到 methods
方法。
我们自己构造写的函数,如何做到类似Vue
的效果呢。
function Person(options){ } const p = new Person({ data: { name: '若川' }, methods: { sayName(){ console.log(this.name); } } }); console.log(p.name); // undefined console.log(p.sayName()); // Uncaught TypeError: p.sayName is not a function
如果是你,你会怎么去实现呢。带着问题,我们来调试 Vue2源码学习。
2. 准备环境调试源码一探究竟
可以在本地新建一个文件夹examples
,新建文件index.html
文件。
在<body></body>
中加上如下js。
<script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script> <script> const vm = new Vue({ data: { name: '我是若川', }, methods: { sayName(){ console.log(this.name); } }, }); console.log(vm.name); console.log(vm.sayName()); </script>
再全局安装npm i -g http-server
启动服务。
npm i -g http-server cd examples http-server . // 如果碰到端口被占用,也可以指定端口 http-server -p 8081 .
这样就能在http://localhost:8080/
打开刚写的index.html
页面了。
调试:在 F12 打开调试,source 面板,在例子中const vm = new Vue({打上断点。
刷新页面后按F11进入函数,这时断点就走进了 Vue 构造函数。
2.1 Vue 构造函数
function Vue (options) { if (!(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); } // 初始化 initMixin(Vue); stateMixin(Vue); eventsMixin(Vue); lifecycleMixin(Vue); renderMixin(Vue);
值得一提的是:if (!(this instanceof Vue)){}
判断是不是用了 new 关键词调用构造函数。
一般而言,我们平时应该不会考虑写这个。
当然看源码库也可以自己函数内部调用 new
。但 vue
一般一个项目只需要 new Vue()
一次,所以没必要。
而 jQuery 源码的就是内部 new ,对于使用者来说就是无new
构造。
jQuery = function( selector, context ) { // 返回new之后的对象 return new jQuery.fn.init( selector, context ); };
因为使用 jQuery
经常要调用。
其实 jQuery
也是可以 new
的。和不用 new 是一个效果。
调试:继续在this._init(options);处打上断点,按F11进入函数。
2.2 _init 初始化函数
进入 _init
函数后,这个函数比较长,做了挺多事情,我们猜测跟data
和methods
相关的实现在initState(vm)
函数里。
// 代码有删减 function initMixin (Vue) { Vue.prototype._init = function (options) { var vm = this; // a uid vm._uid = uid$3++; // a flag to avoid this being observed vm._isVue = true; // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options); } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); } // expose real self vm._self = vm; initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, 'beforeCreate'); initInjections(vm); // resolve injections before data/props // 初始化状态 initState(vm); initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); }; }
调试:接着我们在
initState(vm)
函数这里打算断点,按F8可以直接跳转到这个断点,然后按F11接着进入initState
函数。
2.3 initState 初始化状态
从函数名来看,这个函数主要实现功能是:
- 初始化
props
- 初始化
methods
- 监测数据
- 初始化
computed
- 初始化
watch
function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.props) { initProps(vm, opts.props); } // 有传入 methods,初始化方法 if (opts.methods) { initMethods(vm, opts.methods); } // 有传入 data,初始化 data if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); } if (opts.computed) { initComputed(vm, opts.computed); } if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch); } }
我们重点来看初始化 methods
,之后再看初始化 data
。
调试:在
initMethods
这句打上断点,同时在initData(vm)
处打上断点,看完initMethods
函数后,可以直接按F8回到initData(vm)函数。继续按F11,先进入initMethods
函数。
2.4 initMethods 初始化方法
function initMethods (vm, methods) { var props = vm.$options.props; for (var key in methods) { { if (typeof methods[key] !== 'function') { warn( "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " + "Did you reference the function correctly?", vm ); } if (props && hasOwn(props, key)) { warn( ("Method \"" + key + "\" has already been defined as a prop."), vm ); } if ((key in vm) && isReserved(key)) { warn( "Method \"" + key + "\" conflicts with an existing Vue instance method. " + "Avoid defining component methods that start with _ or $." ); } } vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm); } }
initMethods
函数,主要有一些判断。
- 判断
methods
中的每一项是不是函数,如果不是警告。- 判断
methods
中的每一项是不是和 props 冲突了,如果是,警告。- 判断
methods
中的每一项是不是已经在 new Vue实例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指内部变量标识)开头,如果是警告。
除去这些判断,我们可以看出initMethods
函数其实就是遍历传入的methods
对象,并且使用bind
绑定函数的this指向为vm,也就是new Vue
的实例对象。
这就是为什么我们可以通过this直接访问到methods
里面的函数的原因。
我们可以把鼠标移上 bind
变量,按alt键,可以看到函数定义的地方,这里是218行,点击跳转到这里看 bind 的实现。
2.4.1 bind 返回一个函数,修改 this 指向
function polyfillBind (fn, ctx) { function boundFn (a) { var l = arguments.length; return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx) } boundFn._length = fn.length; return boundFn } function nativeBind (fn, ctx) { return fn.bind(ctx) } var bind = Function.prototype.bind ? nativeBind : polyfillBind;
简单来说就是兼容了老版本不支持 原生的bind
函数。同时兼容写法,对参数多少做出了判断,使用call
和apply
实现,据说是因为性能问题。
如果对于call
、apply
、bind
的用法和实现不熟悉,能否模拟实现JS的call
和apply
方法
调试:看完了initMethods函数,按F8回到上文提到的initData(vm)函数断点处。
2.5 initData 初始化 data
initData
函数也是一些判断。主要做了如下事情:- 先给
_data
赋值,以备后用。- 最终获取到的
data
不是对象给出警告。- 遍历
data
,其中每一项:- 如果和
methods
冲突了,报警告。- 如果和
props
冲突了,报警告。- 不是内部私有的保留属性,做一层代理,代理到
_data
上。- 最后监测
data
,使之成为响应式的数据。
function initData (vm) { var data = vm.$options.data; data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}; if (!isPlainObject(data)) { data = {}; warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ); } // proxy data on instance var keys = Object.keys(data); var props = vm.$options.props; var methods = vm.$options.methods; var i = keys.length; while (i--) { var key = keys[i]; { if (methods && hasOwn(methods, key)) { warn( ("Method \"" + key + "\" has already been defined as a data property."), vm ); } } if (props && hasOwn(props, key)) { warn( "The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.", vm ); } else if (!isReserved(key)) { proxy(vm, "_data", key); } } // observe data observe(data, true /* asRootData */); }
2.5.1 getData 获取数据
是函数时调用函数,执行获取到对象。
function getData (data, vm) { // #7573 disable dep collection when invoking data getters pushTarget(); try { return data.call(vm, vm) } catch (e) { handleError(e, vm, "data()"); return {} } finally { popTarget(); } }
2.5.2 proxy 代理
其实就是用 Object.defineProperty
定义对象
这里用处是:this.xxx
则是访问的 this._data.xxx。
/** * Perform no operation. * Stubbing args to make Flow happy without leaving useless transpiled code * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/). */ function noop (a, b, c) {} 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); }
2.5.3 Object.defineProperty 定义对象属性
Object.defineProperty
算是一个非常重要的API。还有一个定义多个属性的API:Object.defineProperties(obj, props) (ES5)
Object.defineProperty
涉及到比较重要的知识点,面试也常考。value
——当试图获取属性时所返回的值。writable
——该属性是否可写。enumerable
——该属性在for in循环中是否会被枚举。configurable
——该属性是否可被删除。set()
—该属性的更新操作所调用的函数。get()
—获取属性值时所调用的函数。
2.6 文中出现的一些函数,最后统一解释下
2.6.1 hasOwn 是否是对象本身拥有的属性
调试模式下,按alt键,把鼠标移到方法名上,可以看到函数定义的地方。点击可以跳转。 /** * Check whether an object has the property. */ var hasOwnProperty = Object.prototype.hasOwnProperty; function hasOwn (obj, key) { return hasOwnProperty.call(obj, key) } hasOwn({ a: undefined }, 'a') // true hasOwn({}, 'a') // false hasOwn({}, 'hasOwnProperty') // false hasOwn({}, 'toString') // false // 是自己的本身拥有的属性,不是通过原型链向上查找的。
2.6.2 isReserved 是否是内部私有保留的字符串$ 和 _ 开头
/** * Check if a string starts with $ or _ */ function isReserved (str) { var c = (str + '').charCodeAt(0); return c === 0x24 || c === 0x5F } isReserved('_data'); // true isReserved('$options'); // true isReserved('data'); // false isReserved('options'); // false
3. 最后用60余行代码实现简化版
function noop (a, b, c) {} 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); } function initData(vm){ const data = vm._data = vm.$options.data; const keys = Object.keys(data); var i = keys.length; while (i--) { var key = keys[i]; proxy(vm, '_data', key); } } function initMethods(vm, methods){ for (var key in methods) { vm[key] = typeof methods[key] !== 'function' ? noop : methods[key].bind(vm); } } function Person(options){ let vm = this; vm.$options = options; var opts = vm.$options; if(opts.data){ initDa美国高防服务器http://www.558idc.com/usa.htmlta(vm); } if(opts.methods){ initMethods(vm, opts.methods) } } const p = new Person({ data: { name: '若川' }, methods: { sayName(){ console.log(this.name); } } }); console.log(p.name); // 未实现前: undefined // '若川' console.log(p.sayName()); // 未实现前:Uncaught TypeError: p.sayName is not a function // '若川'
4. 总结
本文涉及到的基础知识主要有如下:
- 构造函数
this
指向call
、bind
、apply
Object.defineProperty
本文源于解答源码共读群友的疑惑,通过详细的描述了如何调试 Vue 源码,来探寻答案。
解答文章开头提问:
通过this直接访问到methods
里面的函数的原因是:因为methods
里的方法通过 bind 指定了this为 new Vue
的实例(vm)。
通过 this 直接访问到 data 里面的数据的原因是:data里的属性最终会存储到new Vue的实例(vm)上的 _data对象中,访问 this.xxx
,是访问Object.defineProperty
代理后的 this._data.xxx。
Vue的这种设计,好处在于便于获取。也有不方便的地方,就是props
、methods
和 data三者容易产生冲突。
文章整体难度不大,但非常建议读者朋友们自己动手调试下。调试后,你可能会发现:原来 Vue 源码,也没有想象中的那么难,也能看懂一部分。
启发:我们工作使用常用的技术和框架或库时,保持好奇心,多思考内部原理。能够做到知其然,知其所以然。就能远超很多人。
你可能会思考,为什么模板语法中,可以省略this关键词写法呢,内部模板编译时其实是用了with。有余力的读者可以探究这一原理。
到此这篇关于源码揭秘为什么 Vue2 this 能够直接获取到 data 和 methods
的文章就介绍到这了,更多相关Vue2 this 直接获取到 data 和 methods内容请搜索hwidc以前的文章或继续浏览下面的相关文章希望大家以后多多支持hwidc!