您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關為什么Vue2 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源碼學習。
可以在本地新建一個文件夾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 構造函數。
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進入函數。
進入 _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
函數。
從函數名來看,這個函數主要實現功能是:
初始化
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
函數。
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 的實現。
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)函數斷點處。
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 */); }
是函數時調用函數,執行獲取到對象。
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(); } }
其實就是用 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); }
Object.defineProperty
算是一個非常重要的API。還有一個定義多個屬性的API:Object.defineProperties(obj, props) (ES5)
Object.defineProperty
涉及到比較重要的知識點,面試也常考。
value
——當試圖獲取屬性時所返回的值。
writable
——該屬性是否可寫。
enumerable
——該屬性在for in循環中是否會被枚舉。
configurable
——該屬性是否可被刪除。
set()
—該屬性的更新操作所調用的函數。
get()
—獲取屬性值時所調用的函數。
調試模式下,按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 // 是自己的本身擁有的屬性,不是通過原型鏈向上查找的。
/** * 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
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){ initData(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 // '若川'
本文涉及到的基礎知識主要有如下:
構造函數
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 源碼,也沒有想象中的那么難,也能看懂一部分。
關于為什么Vue2 this能夠直接獲取到data和methods就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。