寫在開頭
學習完了ES 6基礎,推薦閱讀:ECMAScript 6 全套學習目錄 整理 完結
現在開始逐步深入Vue 技術棧,想了想,技術棧專欄的主要內容包括:
1、Vue源碼分析
2、手把手教 保姆級 擼代碼
3、無懼面試,學以致用,繼承創新
4、談談前端發展與學習心得
5、手寫源碼技術棧,附上詳細註釋
6、從源碼中學習設計模式,一舉兩得
7、編程思想的提升及代碼質量的提高
8、通過分析源碼學習架構,看看優秀的框架
9、項目實戰開發
10、面試準備,完善個人簡歷
暫時想到的就這麼多,把這列舉的10點做好了,我覺得也OK了,歡迎一起學習,覺得不錯的話,可以關注博主,專欄會不斷更新,可以關注一下,傳送門~
學習目錄
爲了方便自己查閱與最後整合,還是打算整個目錄,關於Vue技術棧前面的幾篇優秀的文章:
正文
Vue 2的響應式原理
提到Vue2的響應式原理,或許你就會想到Object.defineProperty()
,但Object.defineProperty()嚴格來說的話,並不是來做響應式的。
什麼是defineProperty( )
推薦閱讀:Vue 中 數據劫持 Object.defineProperty()
- defineProperty其實是定義對象的屬性,或者你可以認爲是對象的屬性標籤
defineProperty其實並不是核心的爲一個對象做數據雙向綁定,而是去給對象做屬性標籤,只不過屬性裏的get和set實現了響應式
屬性名 | 默認值 |
---|---|
value | undefined |
get | undefined |
set | undefined |
writalbe | true |
enumerable | true |
configurable | true |
下面我們來詳細瞭解一下:
var obj={
a:1,
b:2
}
//參數說明:1.對象 2.對象的某個屬性 3.對於屬性的配置
Object.defineProperty(obj,'a',{
writable:false
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
打開瀏覽器,按F12,將以上代碼粘貼過去,查看控制檯內容:
上述,打印的就是我們obj對象中a屬性的一系列標籤,權限方面可以看到默認的話爲true
那麼,我們剛剛設置了 writalbe爲false,即設置了a屬性不可寫,進行簡單測試一下:
發現我們無法對a屬性進行value的修改,因爲將writalbe設置了爲false
當然,我們可以設置其他權限標籤,例如:
var obj={
a:1,
b:2
}
//參數說明:1.對象 2.對象的某個屬性 3.對於屬性的配置
Object.defineProperty(obj,'a',{
writable:false,
enumerable:false,
configurable:false
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
因此,承接上文所述,defineProperty並不是來做響應式的,而是給對象中某個屬性設置權限操作,是否可寫,是否可以for in,是否可delete
get和set的使用
Vue中實現雙向綁定,其實就是與get和set有很大關係
舉個栗子,請看如下代碼:
var obj={
a:1,
b:2
}
//參數說明:1.對象 2.對象的某個屬性 3.對於屬性的配置
Object.defineProperty(obj,'a',{
get:function(){
console.log('a is be get!');
},
set:function(){
console.log('a is be set!');
}
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
我們在控制檯,簡單測試一下:
問題來了,細心的夥伴,應該發現了上圖的問題,當我們get的時候,我們返回的是一個undefined,而且我們set一個值之後,也是獲取不到新值,依舊是undefined,如下:
原因呢,其實就是我們的get函數是有返回值的,如果你不return的話,就會默認返回undefined,不管你怎麼set都沒用,那麼如何解決這個問題呢,請看下面代碼:
var obj={
a:1,
b:2
}
//藉助外部變量存儲值
let _value=obj.a;
//參數說明:1.對象 2.對象的某個屬性 3.對於屬性的配置
Object.defineProperty(obj,'a',{
get:function(){
console.log('a is be get!');
return _value;
},
set:function(newVal){
console.log('a is be set!');
_value=newVal;
return _value;
}
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));
可以看到,我們必須藉助一個外部變量,也就是中轉站一樣,才能達到我們的get和set效果,這也是vue2 中不太優雅的地方
然後,查看控制檯,解決了上述問題
Vue中從改變一個數據到發生改變的過程
手寫 Vue 2 中響應式原理
基於上述流程圖,我們可以手寫一個簡單版的Vue2.0實現雙向綁定的例子:
這裏我就只實現邏輯,不具體去弄視圖渲染了
文件名:2.js
//Vue響應式手寫實現
function vue(){
this.$data={a:1};
this.el=document.getElementById('app');
this.virtualdom="";
this.observer(this.$data)
this.render();
}
//註冊get和set監聽
vue.prototype.observer=function(obj){
var value; //藉助外部變量
var self=this; //緩存this
/*下面代碼 a可能是data裏的某個對象,不是屬性
因此在vue2.0中需要for in循環找到屬性*/
//Object.defineProperty(obj,'a')
for(var key in obj){
value=obj[key];
//判斷是否爲對象
if(typeof value === 'object'){
this.observer(value);
}else{
Object.defineProperty(this.$data,key,{
get:function(){
//進行依賴收集
return value;
},
set:function(newVal){
value=newVal;
//視圖渲染
self.render();
}
})
}
}
}
//更新渲染部分
vue.prototype.render=function(){
this.virtualdom="i am "+this.$data.a;
this.el.innerHTML=this.virtualdom;
}
文件名:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>手寫Vue響應式原理</title>
</head>
<body>
<div id='app'></div>
<script type="text/javascript" src="./2.js"></script>
<script type="text/javascript">
var vm = new vue();
//設置set定時器
setTimeout(function(){
console.log('2秒後將值改爲123');
console.log(vm.$data);
vm.$data.a=123;
},2000)
</script>
</body>
</html>
查看頁面,就會有如下效果:
那麼,以後面試如果遇到手寫響應式原理,把上述js代碼寫上去就ok了
源碼分析:響應式原理中的依賴收集
手寫的代碼裏面對於依賴收集這一塊我們進行了省略,下面我們從源碼的角度去看依賴收集到底是什麼玩意:
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
//依賴收集
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
//進行依賴收集
dep.depend();
/*採用依賴收集的原因:*/
//1.data裏面的數據並不是所有地方都要用到
//2.如果我們直接更新整個視圖,會造成資源浪費
//3.將依賴於某個變量的組件收集起來
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
對依賴收集的總結
在初次渲染時,會觸發一次get函數,爲了提高效率,節省資源,採用依賴收集,這裏以之前手寫的爲例,get部分,我們就會對this.$data
裏的每一個屬性(即key值)進行收集,看在哪些組件裏進行了調用,以此提高效率。
而在set部分,就會更新我們收集到的依賴
Object.defineProperty(this.$data,key,{
get:function(){
//進行依賴收集
return value;
},
set:function(newVal){
value=newVal;
//視圖渲染
self.render();
}
})
額外注意——關於數組的監聽(探索設計模式)
從前文我們可以瞭解到,defineProperty定義的ger和set是對象的屬性,那麼數組該怎麼辦呢?
對於數組呢,在Vue中,你是沒有辦法像C/C++、Java等語言那樣直接通過操作下標來觸發更新,只能通過push、pop等方法來觸發數據更新
var arr=[1,2,3];
arr.push(4);
arr.pop();
arr.shift();
這裏 特別重要!
關於數組這一塊裏面巧妙運用到了一個設計模式——裝飾者模式
//裝飾者模式
//先取出原型
var arraypro=Array.prototype;
//拷貝一份,原因:避免影響到了原來的原型鏈
var arrob=Object.create(arraypro);
//定義一個需要裝飾的方法的數組,這裏只例舉以下三個
var arr=['push','pop','shift'];
//設置重寫方法(裝飾者模式)
arr.forEach(function(methods,index){
arrob[method]=function(){
//先調用原來的方法
var res=arraypro[method].apply(this,arguments);
//觸發視圖更新
dep.notify();
}
})
//接下來將數組的prototype替換到data上的prototype(此處省略)
//這樣的話,例如我們push方法,既能push又能觸發視圖更新了
對於設計模式呢,其實並不是很難,常說難懂,很難學,可能你學設計模式,你看了書,看到的可能就是簡單事例,只是一個用法,沒有訓練思維,正確的做法是:
- 提高我們的思維,提高代碼質量
- 先學透,記住一些定義和一些具體使用,然後去看,去探索
- 非常好的一種方式就是結合源碼,例如上文我們從Vue數組的監聽原理裏面剖析出來了裝飾者模式
- 學以致用
Vue 3的響應式原理
對於2.0響應式原理,我們暫告一段落,接下來,我們討論Vue 3中的技巧,衆所周知,Vue 3將defineProperty
替換成了proxy
什麼是proxy
用於定義基本操作的自定義行爲
和defineProperty類似,功能幾乎一樣,只不過用法上有所不同
和上文一樣,我們依舊寫一個響應式,不過下面的代碼是有問題的,讀者可以先思考一下。
var obj={
a:1,
b:2
}
//無需藉助外部變量
new Proxy(obj,{
get(target,key,receiver){
console.log(target,key,receiver);
return target[key];
},
set(target,key,value,receiver){
return Reflect.set(target,key,value);
//return target[key]=value;
/*上面註釋的代碼和上一行意思相同*/
}
})
我們在控制檯跑一下上述代碼,發現它並沒有輸出console.log的內容,因此是有問題的
正確代碼如下:
var obj={
a:1,
b:2
}
//無需藉助外部變量
//對於vue 2,提高效率,無需for in 遍歷找屬性
//不會污染原對象,會返回一個新的代理對象,原對象依舊是原對象
//也是軟件工程裏的重要知識,儘量不要"污染"原對象,不用給原對象做任何操作
//只需對代理對象進行操作
var objChildren=new Proxy(obj,{
get(target,key,receiver){
console.log(target,key,receiver);
return target[key];
},
set(target,key,value,receiver){
return Reflect.set(target,key,value);
//return target[key]=value;
/*上面註釋的代碼和上一行意思相同*/
}
})
總結:爲什麼Vue 3中使用proxy
- defineProperty只能監聽某個屬性,不能對全對象進行監聽
- 可以省去for in遍歷找對象中的屬性,提高效率,省去很多代碼
- 可以監聽數組,不用再去單獨的對數組進行特異性操作
- 不會污染原對象,會返回一個新的代理對象,原對象依舊是原對象
- 只需對代理對象進行操作
手寫 Vue 3 中響應式原理
下面代碼,是在上文手寫 Vue 2 響應式原理基礎上修改的,通過對比,可以發現,我們省去了好多代碼,不需要進行for in循環比較複雜、耗時間的操作了
//Vue響應式手寫實現
function vue(){
this.$data={a:1};
this.el=document.getElementById('app');
this.virtualdom="";
this.observer(this.$data)
this.render();
}
//註冊get和set監聽
vue.prototype.observer=function(obj){
var self=this;
this.$data=new Proxy(this.$data,{
get(target,key){
return target[key];
},
set(target,key,value){
target[key]=value;
self.render();
}
})
}
//更新渲染部分
vue.prototype.render=function(){
this.virtualdom="i am "+this.$data.a;
//this.el.innerHTML=this.virtualdom;
this.el.innerHTML=this.virtualdom;
}
查看頁面,就會有如下效果:
proxy這麼好用,還能做什麼呢?(再遇設計模式)
我們學習知識並不只是爲了應付面試那種程度,對於面試應該作爲我們的最低要求,接下來,我們接着去深度研究proxy還能幹什麼呢?
在 Vue 3 基本上已經不兼容IE8了,這裏簡單提及一下
- 類型驗證
這裏我們就自定義一個實例:創建一個成人的對象,擁有name和age兩個屬性
要求:name必須是中文,age必須是數字,並且大於18
如果用純原生js做驗證的話,可想有多難去驗證上述需求,或許你想到的是在構造函數裏面去實現,但也不會簡單,那麼我們看看proxy怎麼實現的:
//類型驗證
//外部定義一個驗證器對象
var validator={
name:function(value){
var reg=/^[\u4E00-\u9FA5]+$/;
if(typeof value=='string'&®.test(value)){
return true;
}
return false;
},
age:function(value){
if(typeof value=='number'&&value>=18){
return true;
}
return false;
}
}
function person(name,age){
this.name=name;
this.age=age;
return new Proxy(this,{
get(target,key){
return target[key];
},
set(target,key,value){
if(validator[key](value)){
return Reflect.set(target,key,value);
}else{
throw new Error(key+' is not right!');
}
}
})
}
這裏 特別重要!
關於類型驗證這一塊裏面又巧妙運用到了一個設計模式——策略模式
關於設計模式這一塊,此專欄不會細講,但會在探索源碼時發現了好的實例,會提出來一下。
上述用到了一個正則表達式,關於這個可能面試會問到,這是之前ES 6 裏的內容,大家可以看看這篇簡單易懂的文章:
推薦閱讀:ES6 面試題:你能說出瀏覽器上到此支持多少箇中文字嗎?
- 私有變量
關於私有變量這一塊,我們就拿 vue-router 源碼
來進行分析:
//vue-router源碼分析
Object.defineProperty(this,'$router',{//Router的實例
get(){
return this._root._router;
}
});
Object.defineProperty(this,'$route',{
get(){
return{
//當前路由所在的狀態
current:this._root._router.history.current
}
}
})
通過查看源碼,提出疑問:爲什麼要爲$router
寫get方法呢,而且沒做什麼操作,只是一個return?
原因:這樣可以使得$router
不可修改。避免程序員通過set修改了路由,導致路由失效的情況。這裏就體現了數據安全思想,前端程序員或許考慮的沒有Java程序員多,甚至沒有爲變量想過某個變量設置不可修改。由於工作的需要,我們也要努力提升自己的代碼質量!讓自己的職業生涯更加輝煌!
virtual dom 和 diff算法
關於diff算法和虛擬dom,也是面試常見的問題,平常容易忽視,這裏我也就深入研究了一下:
虛擬dom
所謂虛擬dom,如字面意思,它是虛擬的,只在概念裏面存在,並不真的存在,在vue中是ast語法樹,關於這個語法樹本文就不詳細介紹了,有興趣的讀者可以深入研究一下。
下面代碼,是一個簡單vue template模板,那麼解析成虛擬dom是怎樣的呢?
<template>
<div id='dd'>
<p>{{msg}}</p>
<p>abc</p>
<p>123</p>
</div>
</template>
解析成虛擬dom:
diff <div>
props:{
id:dd
},
children:[
diff <p>
props:
children:[
],
text:xxx,
]
上述代碼就是概念上的介紹,如果懂一點算法知識的應該就明白了,就是不斷地嵌套,但爲了讓更多夥伴讀懂學會虛擬dom,下面來手寫一個對象的形式:
<template>
<div id='dd'>
<p><span></span></p>
<p>abc</p>
<p>123</p>
</div>
</template>
var virtual=
{
dom:'div',
props:{
id:dd
},
children:[
{
dom:'p',
children:[
dom:'span',
children:[]
]
},
{
dom:'p',
children:[
]
},
{
dom:'p',
children:[
]
}
]
}
上述代碼應該就很清晰了,簡單來說,就是將最上面的dom結構,解析成下面用js解析成的對象,每一個對象都有一個基礎的結構:
- dom元素標籤
- props記錄掛載了哪些屬性
- children記錄有哪些子元素(子元素擁有和父元素相同的結構)
diff算法的比對機制
下面部分採用了僞代碼形式介紹diff算法的比對機制,已經給出了詳細的註釋說明:
//diff算法匹配機制
patchVnode(oldVnode,vnode){
//先拿到真實的dom
const el=vnode.el=oldVnode.el;
//分別拿出舊節點和新節點的子元素
let i,oldCh=oldVnode.children,ch=vnode.children;
//如果新舊節點相同,直接return
if(oldVnode==vnode) return;
/*分四種情況討論*/
//1.只有文字節點不同的情況
if(oldVnode.text!==null&&vnode.text!==null&&oldVnode.text!==vnode.text){
api.setTextContent(el,vnode.text);
}else{
updateEle();
//2.如果新舊節點的子元素都存在,那麼發生的是子元素變動
if(oldCh&&ch&&oldCh!==ch){
updateChildren();
//3.如果只有新節點有子元素,那麼發生的是新增子元素
}else if(ch){
createEl(vnode);
//4.如果只有舊節點有子元素,那麼發生的是新節點刪除了子元素
}else if(oldCh){
api.removeChildren(el);
}
}
}
總結
學如逆水行舟,不進則退