源碼揭秘為什么 Vue2 this 能夠直接獲取到 data 和 methods
1. 示例:this 能夠直接獲取到 data 和 methods
舉例:
const vm = new Vue({
data: {
name: '我是若川',
},
methods: {
sayName(){
console.log(this.name);
}
},
});
console.log(vm.name); // 我是若川
console.log(vm.sayName()); // 我是若川
這樣是可以輸出我是若川的。好奇的人就會(huì)思考為啥 this 就能直接訪問到呢。
那么為什么 this.xxx 能獲取到data里的數(shù)據(jù),能獲取到 methods 方法。
我們自己構(gòu)造寫的函數(shù),如何做到類似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
如果是你,你會(huì)怎么去實(shí)現(xiàn)呢。帶著問題,我們來調(diào)試 Vue2源碼學(xué)習(xí)。
2. 準(zhǔn)備環(huán)境調(diào)試源碼一探究竟
可以在本地新建一個(gè)文件夾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啟動(dòng)服務(wù)。
npm i -g http-server cd examples http-server . // 如果碰到端口被占用,也可以指定端口 http-server -p 8081 .
這樣就能在http://localhost:8080/打開剛寫的index.html頁面了。
調(diào)試:在 F12 打開調(diào)試,source 面板,在例子中const vm = new Vue({打上斷點(diǎn)。

刷新頁面后按F11進(jìn)入函數(shù),這時(shí)斷點(diǎn)就走進(jìn)了 Vue 構(gòu)造函數(shù)。
2.1 Vue 構(gòu)造函數(shù)
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 關(guān)鍵詞調(diào)用構(gòu)造函數(shù)。
一般而言,我們平時(shí)應(yīng)該不會(huì)考慮寫這個(gè)。
當(dāng)然看源碼庫也可以自己函數(shù)內(nèi)部調(diào)用 new 。但 vue 一般一個(gè)項(xiàng)目只需要 new Vue() 一次,所以沒必要。
而 jQuery 源碼的就是內(nèi)部 new ,對(duì)于使用者來說就是無new構(gòu)造。
jQuery = function( selector, context ) {
// 返回new之后的對(duì)象
return new jQuery.fn.init( selector, context );
};
因?yàn)槭褂?jQuery 經(jīng)常要調(diào)用。
其實(shí) jQuery 也是可以 new 的。和不用 new 是一個(gè)效果。
調(diào)試:繼續(xù)在this._init(options);處打上斷點(diǎn),按F11進(jìn)入函數(shù)。
2.2 _init 初始化函數(shù)
進(jìn)入 _init 函數(shù)后,這個(gè)函數(shù)比較長,做了挺多事情,我們猜測跟data和methods相關(guān)的實(shí)現(xiàn)在initState(vm)函數(shù)里。
// 代碼有刪減
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
// 初始化狀態(tài)
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
};
}
調(diào)試:接著我們?cè)?code>initState(vm)函數(shù)這里打算斷點(diǎn),按F8可以直接跳轉(zhuǎn)到這個(gè)斷點(diǎn),然后按F11接著進(jìn)入
initState函數(shù)。
2.3 initState 初始化狀態(tài)
從函數(shù)名來看,這個(gè)函數(shù)主要實(shí)現(xiàn)功能是:
- 初始化
props- 初始化
methods- 監(jiān)測數(shù)據(jù)
- 初始化
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);
}
}
我們重點(diǎn)來看初始化 methods,之后再看初始化 data。
調(diào)試:在
initMethods這句打上斷點(diǎn),同時(shí)在initData(vm)處打上斷點(diǎn),看完initMethods函數(shù)后,可以直接按F8回到initData(vm)函數(shù)。繼續(xù)按F11,先進(jìn)入initMethods函數(shù)。
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函數(shù),主要有一些判斷。
- 判斷
methods中的每一項(xiàng)是不是函數(shù),如果不是警告。- 判斷
methods中的每一項(xiàng)是不是和 props 沖突了,如果是,警告。- 判斷
methods中的每一項(xiàng)是不是已經(jīng)在 new Vue實(shí)例 vm 上存在,而且是方法名是保留的 _ $ (在JS中一般指內(nèi)部變量標(biāo)識(shí))開頭,如果是警告。
除去這些判斷,我們可以看出initMethods函數(shù)其實(shí)就是遍歷傳入的methods對(duì)象,并且使用bind綁定函數(shù)的this指向?yàn)関m,也就是new Vue的實(shí)例對(duì)象。
這就是為什么我們可以通過this直接訪問到methods里面的函數(shù)的原因。
我們可以把鼠標(biāo)移上 bind 變量,按alt鍵,可以看到函數(shù)定義的地方,這里是218行,點(diǎn)擊跳轉(zhuǎn)到這里看 bind 的實(shí)現(xiàn)。
2.4.1 bind 返回一個(gè)函數(shù),修改 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函數(shù)。同時(shí)兼容寫法,對(duì)參數(shù)多少做出了判斷,使用call和apply實(shí)現(xiàn),據(jù)說是因?yàn)樾阅軉栴}。
如果對(duì)于call、apply、bind的用法和實(shí)現(xiàn)不熟悉,能否模擬實(shí)現(xiàn)JS的call和apply方法
調(diào)試:看完了initMethods函數(shù),按F8回到上文提到的initData(vm)函數(shù)斷點(diǎn)處。
2.5 initData 初始化 data
initData函數(shù)也是一些判斷。主要做了如下事情:- 先給
_data賦值,以備后用。- 最終獲取到的
data不是對(duì)象給出警告。- 遍歷
data,其中每一項(xiàng):- 如果和
methods沖突了,報(bào)警告。- 如果和
props沖突了,報(bào)警告。- 不是內(nèi)部私有的保留屬性,做一層代理,代理到
_data上。- 最后監(jiān)測
data,使之成為響應(yīng)式的數(shù)據(jù)。
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 獲取數(shù)據(jù)
是函數(shù)時(shí)調(diào)用函數(shù),執(zhí)行獲取到對(duì)象。
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 代理
其實(shí)就是用 Object.defineProperty 定義對(duì)象
這里用處是: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 定義對(duì)象屬性
Object.defineProperty算是一個(gè)非常重要的API。還有一個(gè)定義多個(gè)屬性的API:Object.defineProperties(obj, props) (ES5)
Object.defineProperty涉及到比較重要的知識(shí)點(diǎn),面試也???。value——當(dāng)試圖獲取屬性時(shí)所返回的值。writable——該屬性是否可寫。enumerable——該屬性在for in循環(huán)中是否會(huì)被枚舉。configurable——該屬性是否可被刪除。set()—該屬性的更新操作所調(diào)用的函數(shù)。get()—獲取屬性值時(shí)所調(diào)用的函數(shù)。
2.6 文中出現(xiàn)的一些函數(shù),最后統(tǒng)一解釋下
2.6.1 hasOwn 是否是對(duì)象本身擁有的屬性
調(diào)試模式下,按alt鍵,把鼠標(biāo)移到方法名上,可以看到函數(shù)定義的地方。點(diǎn)擊可以跳轉(zhuǎn)。
/**
* 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 是否是內(nèi)部私有保留的字符串$ 和 _ 開頭
/**
* 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余行代碼實(shí)現(xiàn)簡化版
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);
// 未實(shí)現(xiàn)前: undefined
// '若川'
console.log(p.sayName());
// 未實(shí)現(xiàn)前:Uncaught TypeError: p.sayName is not a function
// '若川'
4. 總結(jié)
本文涉及到的基礎(chǔ)知識(shí)主要有如下:
- 構(gòu)造函數(shù)
this指向call、bind、applyObject.defineProperty
本文源于解答源碼共讀群友的疑惑,通過詳細(xì)的描述了如何調(diào)試 Vue 源碼,來探尋答案。
解答文章開頭提問:
通過this直接訪問到methods里面的函數(shù)的原因是:因?yàn)?code>methods里的方法通過 bind 指定了this為 new Vue的實(shí)例(vm)。
通過 this 直接訪問到 data 里面的數(shù)據(jù)的原因是:data里的屬性最終會(huì)存儲(chǔ)到new Vue的實(shí)例(vm)上的 _data對(duì)象中,訪問 this.xxx,是訪問Object.defineProperty代理后的 this._data.xxx。
Vue的這種設(shè)計(jì),好處在于便于獲取。也有不方便的地方,就是props、methods 和 data三者容易產(chǎn)生沖突。
文章整體難度不大,但非常建議讀者朋友們自己動(dòng)手調(diào)試下。調(diào)試后,你可能會(huì)發(fā)現(xiàn):原來 Vue 源碼,也沒有想象中的那么難,也能看懂一部分。
啟發(fā):我們工作使用常用的技術(shù)和框架或庫時(shí),保持好奇心,多思考內(nèi)部原理。能夠做到知其然,知其所以然。就能遠(yuǎn)超很多人。
你可能會(huì)思考,為什么模板語法中,可以省略this關(guān)鍵詞寫法呢,內(nèi)部模板編譯時(shí)其實(shí)是用了with。有余力的讀者可以探究這一原理。
到此這篇關(guān)于源碼揭秘為什么 Vue2 this 能夠直接獲取到 data 和 methods的文章就介紹到這了,更多相關(guān)Vue2 this 直接獲取到 data 和 methods內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue+mui實(shí)現(xiàn)圖片的本地緩存示例代碼
這篇文章主要介紹了Vue+mui實(shí)現(xiàn)圖片的本地緩存的實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-05-05
vue跨域處理方式(vue項(xiàng)目中baseUrl設(shè)置問題)
這篇文章主要介紹了vue跨域處理方式(vue項(xiàng)目中baseUrl設(shè)置問題),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05
vue 實(shí)現(xiàn)Web端的定位功能 獲取經(jīng)緯度
這篇文章主要介紹了vue 實(shí)現(xiàn)Web端的定位功能獲取經(jīng)緯度,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08
vue圓環(huán)百分比進(jìn)度條組件功能的實(shí)現(xiàn)
在一些頁面設(shè)置進(jìn)度條效果給人一種很好的體驗(yàn)效果,今天小編教大家vue圓環(huán)百分比進(jìn)度條組件功能的實(shí)現(xiàn)代碼,代碼超級(jí)簡單啊,感興趣的朋友快來看下吧2021-05-05
vue報(bào)錯(cuò)Not?allowed?to?load?local?resource的解決辦法
這篇文章主要給大家介紹了關(guān)于vue報(bào)錯(cuò)Not?allowed?to?load?local?resource的解決辦法,這個(gè)錯(cuò)誤是因?yàn)閂ue不能加載本地資源的原因,需要的朋友可以參考下2023-07-07
vue實(shí)現(xiàn)三級(jí)聯(lián)動(dòng)動(dòng)態(tài)菜單
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)三級(jí)聯(lián)動(dòng)動(dòng)態(tài)菜單,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04
vue創(chuàng)建項(xiàng)目卡住不動(dòng),vue?create?project卡住不動(dòng)的解決
這篇文章主要介紹了vue創(chuàng)建項(xiàng)目卡住不動(dòng),vue?create?project卡住不動(dòng)的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10

