为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!
1. 前言
大家好,我是若川。最近組織了源碼共讀活動《1個月,200+人,一起讀了4周源碼》,已經(jīng)有超50+人提交了筆記,群里已經(jīng)有超1200人,感興趣的可以點此鏈接掃碼加我微信?ruochuan12
之前寫的《學(xué)習(xí)源碼整體架構(gòu)系列》jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4十余篇源碼文章。其中最新的三篇是:
50行代碼串行Promise,koa洋蔥模型原來是這么實現(xiàn)?
Vue 3.2 發(fā)布了,那尤雨溪是怎么發(fā)布 Vue.js 的?
初學(xué)者也能看懂的 Vue3 源碼中那些實用的基礎(chǔ)工具函數(shù)
寫相對很難的源碼,耗費了自己的時間和精力,也沒收獲多少閱讀點贊,其實是一件挺受打擊的事情。從閱讀量和讀者受益方面來看,不能促進作者持續(xù)輸出文章。所以轉(zhuǎn)變思路,寫一些相對通俗易懂的文章。其實源碼也不是想象的那么難,至少有很多看得懂。歌德曾說:讀一本好書,就是在和高尚的人談話。同理可得:讀源碼,也算是和作者的一種學(xué)習(xí)交流的方式。
本文源于一次源碼共讀群里群友的提問,請問@若川,“為什么 data 中的數(shù)據(jù)可以用 this 直接獲取到啊”,當時我翻閱源碼做出了解答。想著如果下次有人再次問到,我還需要回答一次。當時打算有空寫篇文章告訴讀者自己探究原理,于是就有了這篇文章。
閱讀本文,你將學(xué)到:
1.?如何學(xué)習(xí)調(diào)試?vue2?源碼 2.?data?中的數(shù)據(jù)為什么可以用?this?直接獲取到 3.?methods?中的方法為什么可以用?this?直接獲取到 4.?學(xué)習(xí)源碼中優(yōu)秀代碼和思想,投入到自己的項目中本文不難,用過 Vue 的都看得懂,希望大家動手調(diào)試和學(xué)會看源碼。
看源碼可以大膽猜測,最后小心求證。
2. 示例:this 能夠直接獲取到 data 和 methods
眾所周知,這樣是可以輸出我是若川的。好奇的人就會思考為啥 this 就能直接訪問到呢。
const?vm?=?new?Vue({data:?{name:?'我是若川',},methods:?{sayName(){console.log(this.name);}}, }); console.log(vm.name);?//?我是若川 console.log(vm.sayName());?//?我是若川那么為什么 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如果是你,你會怎么去實現(xiàn)呢。帶著問題,我們來調(diào)試 Vue2源碼學(xué)習(xí)。
3. 準備環(huán)境調(diào)試源碼一探究竟
可以在本地新建一個文件夾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啟動服務(wù)。
npm?i?-g?http-server cd?examples http-server?. //?如果碰到端口被占用,也可以指定端口 http-server?-p?8081?.這樣就能在http://localhost:8080/打開剛寫的index.html頁面了。
對于調(diào)試還不是很熟悉的讀者,可以看這篇文章《前端容易忽略的 debugger 調(diào)試技巧》
調(diào)試:在 F12 打開調(diào)試,source 面板,在例子中const vm = new Vue({打上斷點。
debugger刷新頁面后按F11進入函數(shù),這時斷點就走進了 Vue 構(gòu)造函數(shù)。
3.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ù)。一般而言,我們平時應(yīng)該不會考慮寫這個。
當然看源碼庫也可以自己函數(shù)內(nèi)部調(diào)用 new 。但 vue 一般一個項目只需要 new Vue() 一次,所以沒必要。
而 jQuery 源碼的就是內(nèi)部 new ,對于使用者來說就是無new構(gòu)造。
jQuery?=?function(?selector,?context?)?{//?返回new之后的對象return?new?jQuery.fn.init(?selector,?context?); };因為使用 jQuery 經(jīng)常要調(diào)用。其實 jQuery 也是可以 new 的。和不用 new 是一個效果。
如果不明白 new 操作符的用處,可以看我之前的文章。面試官問:能否模擬實現(xiàn)JS的new操作符
調(diào)試:繼續(xù)在this._init(options);處打上斷點,按F11進入函數(shù)。
3.2 _init 初始化函數(shù)
進入 _init 函數(shù)后,這個函數(shù)比較長,做了挺多事情,我們猜測跟data和methods相關(guān)的實現(xiàn)在initState(vm)函數(shù)里。
//?代碼有刪減 function?initMixin?(Vue)?{Vue.prototype._init?=?function?(options)?{var?vm?=?this;//?a?uidvm._uid?=?uid$3++;//?a?flag?to?avoid?this?being?observedvm._isVue?=?true;//?merge?optionsif?(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?selfvm._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/propscallHook(vm,?'created');}; }調(diào)試:接著我們在initState(vm)函數(shù)這里打算斷點,按F8可以直接跳轉(zhuǎn)到這個斷點,然后按F11接著進入initState函數(shù)。
3.3 initState 初始化狀態(tài)
從函數(shù)名來看,這個函數(shù)主要實現(xiàn)功能是:
初始化?props 初始化?methods 監(jiān)測數(shù)據(jù) 初始化?computed 初始化?watchfunction?initState?(vm)?{vm._watchers?=?[];var?opts?=?vm.$options;if?(opts.props)?{?initProps(vm,?opts.props);?}//?有傳入?methods,初始化方法if?(opts.methods)?{?initMethods(vm,?opts.methods);?}//?有傳入?data,初始化?dataif?(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。
調(diào)試:在 initMethods 這句打上斷點,同時在initData(vm)處打上斷點,看完initMethods函數(shù)后,可以直接按F8回到initData(vm)函數(shù)。繼續(xù)按F11,先進入initMethods函數(shù)。
3.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 中的每一項是不是函數(shù),如果不是警告。 判斷 methods 中的每一項是不是和 props 沖突了,如果是,警告。 判斷?methods?中的每一項是不是已經(jīng)在?new?Vue實例 vm 上存在,而且是方法名是保留的?_?$?(在JS中一般指內(nèi)部變量標識)開頭,如果是警告。除去這些判斷,我們可以看出initMethods函數(shù)其實就是遍歷傳入的methods對象,并且使用bind綁定函數(shù)的this指向為vm,也就是new Vue的實例對象。
這就是為什么我們可以通過this直接訪問到methods里面的函數(shù)的原因。
我們可以把鼠標移上 bind 變量,按alt鍵,可以看到函數(shù)定義的地方,這里是218行,點擊跳轉(zhuǎn)到這里看 bind 的實現(xiàn)。
3.4.1 bind 返回一個函數(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ù)多少做出了判斷,使用call和apply實現(xiàn),據(jù)說是因為性能問題。
如果對于call、apply、bind的用法和實現(xiàn)不熟悉,可以查看我在面試官問系列面試官問:能否模擬實現(xiàn)JS的call和apply方法面試官問:能否模擬實現(xiàn)JS的bind方法
調(diào)試:看完了initMethods函數(shù),按F8回到上文提到的initData(vm)函數(shù)斷點處。
3.5 initData 初始化 data
initData 函數(shù)也是一些判斷。主要做了如下事情:
先給?_data 賦值,以備后用。 最終獲取到的 data 不是對象給出警告。 遍歷 data ,其中每一項: 如果和 methods 沖突了,報警告。 如果和 props 沖突了,報警告。 不是內(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?instancevar?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?dataobserve(data,?true?/*?asRootData?*/); }3.5.1 getData 獲取數(shù)據(jù)
是函數(shù)時調(diào)用函數(shù),執(zhí)行獲取到對象。
function?getData?(data,?vm)?{//?#7573?disable?dep?collection?when?invoking?data?getterspushTarget();try?{return?data.call(vm,?vm)}?catch?(e)?{handleError(e,?vm,?"data()");return?{}}?finally?{popTarget();} }3.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); }3.5.3 Object.defineProperty 定義對象屬性
Object.defineProperty 算是一個非常重要的API。還有一個定義多個屬性的API:Object.defineProperties(obj, props) (ES5)
Object.defineProperty 涉及到比較重要的知識點,面試也常考。
value——當試圖獲取屬性時所返回的值。 writable——該屬性是否可寫。 enumerable——該屬性在for?in循環(huán)中是否會被枚舉。 configurable——該屬性是否可被刪除。 set()——該屬性的更新操作所調(diào)用的函數(shù)。 get()——獲取屬性值時所調(diào)用的函數(shù)。詳細舉例見此鏈接
3.6 文中出現(xiàn)的一些函數(shù),最后統(tǒng)一解釋下
3.6.1 hasOwn 是否是對象本身擁有的屬性
調(diào)試模式下,按alt鍵,把鼠標移到方法名上,可以看到函數(shù)定義的地方。點擊可以跳轉(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 //?是自己的本身擁有的屬性,不是通過原型鏈向上查找的。3.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');?//?false4. 最后用60余行代碼實現(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); //?未實現(xiàn)前:undefined //?'若川' console.log(p.sayName()); //?未實現(xiàn)前:Uncaught TypeError: p.sayName is not a function //?'若川'5. 總結(jié)
本文涉及到的基礎(chǔ)知識主要有如下:
構(gòu)造函數(shù) this?指向 call、bind、apply Object.defineProperty 等等基礎(chǔ)知識。本文源于解答源碼共讀群友的疑惑,通過詳細的描述了如何調(diào)試 Vue 源碼,來探尋答案。
解答文章開頭提問:
通過this直接訪問到methods里面的函數(shù)的原因是:因為methods里的方法通過 bind 指定了this為 new Vue的實例(vm)。
通過 this 直接訪問到 data 里面的數(shù)據(jù)的原因是:data里的屬性最終會存儲到new Vue的實例(vm)上的 _data對象中,訪問 this.xxx,是訪問Object.defineProperty代理后的 this._data.xxx。
Vue的這種設(shè)計,好處在于便于獲取。也有不方便的地方,就是props、methods 和 data三者容易產(chǎn)生沖突。
文章整體難度不大,但非常建議讀者朋友們自己動手調(diào)試下。調(diào)試后,你可能會發(fā)現(xiàn):原來 Vue 源碼,也沒有想象中的那么難,也能看懂一部分。
啟發(fā):我們工作使用常用的技術(shù)和框架或庫時,保持好奇心,多思考內(nèi)部原理。能夠做到知其然,知其所以然。就能遠超很多人。
你可能會思考,為什么模板語法中,可以省略this關(guān)鍵詞寫法呢,內(nèi)部模板編譯時其實是用了with。有余力的讀者可以探究這一原理。
最后歡迎加我微信 ruochuan12源碼共讀 活動,大家一起學(xué)習(xí)源碼,共同進步。
最近組建了一個湖南人的前端交流群,如果你是湖南人可以加我微信?ruochuan12?私信?湖南?拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我讀源碼的經(jīng)歷
老姚淺談:怎么學(xué)JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業(yè)于江西高校。現(xiàn)在是一名前端開發(fā)“工程師”。寫有《學(xué)習(xí)源碼整體架構(gòu)系列
從2014年起,每年都會寫一篇年度總結(jié),已經(jīng)寫了7篇,點擊查看年度總結(jié)。
同時,最近組織了源碼共讀活動
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~
總結(jié)
以上是生活随笔為你收集整理的为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 前端学习(3275):js中this的使
- 下一篇: 前端学习(3180):ant-desig