1. 前言
大家好,我是若川。最近組織了源碼共讀活動《1個月,200+人,一起讀了4周源碼》,已經有超50+人提交了筆記,群里已經有超1200人,感興趣的可以點此鏈接掃碼加我微信?ruochuan12
之前寫的《學習源碼整體架構系列》jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
十余篇源碼文章。其中最新的三篇是:
50行代碼串行Promise,koa洋蔥模型原來是這么實現?
Vue 3.2 發布了,那尤雨溪是怎么發布 Vue.js 的?
初學者也能看懂的 Vue3 源碼中那些實用的基礎工具函數
寫相對很難的源碼,耗費了自己的時間和精力,也沒收獲多少閱讀點贊,其實是一件挺受打擊的事情。從閱讀量和讀者受益方面來看,不能促進作者持續輸出文章。所以轉變思路,寫一些相對通俗易懂的文章。其實源碼也不是想象的那么難,至少有很多看得懂。歌德曾說:讀一本好書,就是在和高尚的人談話。同理可得:讀源碼,也算是和作者的一種學習交流的方式。
本文源于一次源碼共讀群里群友的提問,請問@若川,“為什么 data 中的數據可以用 this 直接獲取到啊”,當時我翻閱源碼做出了解答。想著如果下次有人再次問到,我還需要回答一次。當時打算有空寫篇文章告訴讀者自己探究原理,于是就有了這篇文章。
閱讀本文,你將學到:
1.?如何學習調試?vue2?源碼
2.?data?中的數據為什么可以用?this?直接獲取到
3.?methods?中的方法為什么可以用?this?直接獲取到
4.?學習源碼中優秀代碼和思想,投入到自己的項目中
本文不難,用過 Vue
的都看得懂,希望大家動手調試和學會看源碼。
看源碼可以大膽猜測,最后小心求證。
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
里的數據,能獲取到 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
源碼學習。
3. 準備環境調試源碼一探究竟
可以在本地新建一個文件夾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
頁面了。
對于調試還不是很熟悉的讀者,可以看這篇文章《前端容易忽略的 debugger 調試技巧》
調試:在
F12
打開調試,source
面板,在例子中const vm = new Vue({
打上斷點。

刷新頁面后按F11
進入函數,這時斷點就走進了 Vue 構造函數。
3.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
是一個效果。
如果不明白 new
操作符的用處,可以看我之前的文章。面試官問:能否模擬實現JS的new操作符
調試:繼續在
this._init(options);
處打上斷點,按F11
進入函數。
3.2 _init 初始化函數
進入 _init
函數后,這個函數比較長,做了挺多事情,我們猜測跟data
和methods
相關的實現在initState(vm)
函數里。
//?代碼有刪減
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//??初始化狀態initState(vm);initProvide(vm);?//?resolve?provide?after?data/propscallHook(vm,?'created');};
}
調試:接著我們在
initState(vm)
函數這里打算斷點,按F8
可以直接跳轉到這個斷點,然后按F11
接著進入initState
函數。
3.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,初始化?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
。
調試:在
initMethods
這句打上斷點,同時在initData(vm)
處打上斷點,看完initMethods
函數后,可以直接按F8
回到initData(vm)
函數。繼續按F11
,先進入initMethods
函數。
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
函數,主要有一些判斷。
判斷 methods 中的每一項是不是函數,如果不是警告。
判斷 methods 中的每一項是不是和 props 沖突了,如果是,警告。
判斷?methods?中的每一項是不是已經在?new?Vue實例 vm 上存在,而且是方法名是保留的?_?$?(在JS中一般指內部變量標識)開頭,如果是警告。
除去這些判斷,我們可以看出initMethods
函數其實就是遍歷傳入的methods
對象,并且使用bind
綁定函數的this指向為vm
,也就是new Vue
的實例對象。
這就是為什么我們可以通過this
直接訪問到methods
里面的函數的原因。
我們可以把鼠標移上 bind
變量,按alt
鍵,可以看到函數定義的地方,這里是218行
,點擊跳轉到這里看 bind
的實現。
3.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方法面試官問:能否模擬實現JS的bind方法
調試:看完了
initMethods
函數,按F8
回到上文提到的initData(vm)
函數斷點處。
3.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?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 獲取數據
是函數時調用函數,執行獲取到對象。
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循環中是否會被枚舉。
configurable——該屬性是否可被刪除。
set()——該屬性的更新操作所調用的函數。
get()——獲取屬性值時所調用的函數。
詳細舉例見此鏈接
3.6 文中出現的一些函數,最后統一解釋下
3.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
//?是自己的本身擁有的屬性,不是通過原型鏈向上查找的。
3.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
4. 最后用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){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
//?'若川'
5. 總結
本文涉及到的基礎知識主要有如下:
構造函數
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
。有余力的讀者可以探究這一原理。
最后歡迎加我微信 ruochuan12源碼共讀 活動,大家一起學習源碼,共同進步。
最近組建了一個湖南人的前端交流群,如果你是湖南人可以加我微信?ruochuan12?私信?湖南?拉你進群。
推薦閱讀
1個月,200+人,一起讀了4周源碼
我讀源碼的經歷
老姚淺談:怎么學JavaScript?
我在阿里招前端,該怎么幫你(可進面試群)
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動
識別上方二維碼加我微信、拉你進源碼共讀群
今日話題
略。歡迎分享、收藏、點贊、在看我的公眾號文章~