vue目前是前端使用頻率較高的一套前端mvvm框架之一,提供了數據的響應式、watch
、computed
等極爲方便的功能及api,那麼,vue究竟是如何實現這些功能的呢?在探究vue源碼以前,必須瞭解如下幾點javascript的基本內容,經過了解這些內容,你能夠更加輕鬆的閱讀vue源碼。javascript
Flow就是JavaScript的靜態類型檢查工具,由Facebook團隊於2014年的Scale Conference上首次提出。該庫的目標在於檢查JavaScript中的類型錯誤,開發者一般不須要修改代碼便可使用,故使用成本很低。同時,它也提供額外語法支持,使得開發者能更大程度地發揮Flow的做用。總結一句話:將javascript從弱類型語言變成了強類型語言。前端
Flow支持原始數據類型,其中void對應js中的undefined,基本有以下幾種:vue
boolean
number
string
null
void
複製代碼
在定義變量的同時,只須要在關鍵的地方聲明想要的類型,基本使用以下:java
let str:number = 1; let str1:string = 'a'; // 從新賦值 str = 'd' // error str1 = 3 // error 複製代碼
Flow支持複雜類型檢測,基本有以下幾種:git
Object
Array
Function
自定義Class
複製代碼
基本使用以下示例代碼:github
// Object 定義 let o:Object = { key: 123 } //聲明瞭Object的key let o2:{key:string} = { key: '111' } // Array 定義 //基於基本相似的數組,數組內都是相同類型 let numberArr:number[] = [12,3,4,5,2]; //另外一個寫法 let numberAr2r:Array<number> = [12,3,2,3]; let stringArr:string[] = ['12','a','cc']; let booleanArr:boolean[] = [true,true,false]; let nullArr:null[] = [null,null,null]; let voidArr:void[] = [ , , undefined,void(0)]; //數組內包含各個不一樣的類型數據 //第4個原素沒有聲明,則能夠是任意類型 let arr:[number,string,boolean] = [1,'a',true,function(){},]; 複製代碼
Function定義寫法以下,vue源碼中出現頻率最多的:web
/** * 聲明帶類型的函數 * 這裏是聲明一個函數fn,規定了本身須要的參數類型和返回值類型。 */ function fn(arg:number,arg2:string):Object{ return { arg, arg2 } } /** * vue源碼片斷 * src/core/instance/lifecycle.js */ export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { // 省略 } 複製代碼
自定義的class,聲明一個自定義類,而後用法如同基本類型,基本代碼以下:json
/** * vue源碼片斷 * src/core/observer/index.js */ export class Observer { value: any; dep: Dep; vmCount: number; constructor (value: any) { // 省略 } } 複製代碼
直接使用flow.js,javascript是沒法在瀏覽器端運行的,必須藉助babel插件,vue源碼中使用的是babel-preset-flow-vue這個插件,而且在babelrc進行配置,片斷代碼以下:api
// package.json 文件
// 省略
"devDependencies": {
// 省略
"babel-preset-flow-vue": "^1.0.0"
}
// 省略
// babelrc 文件
{
"presets": ["es2015", "flow-vue"],
"plugins": ["transform-vue-jsx", "syntax-dynamic-import"],
"ignore": [
"dist/*.js",
"packages/**/*.js"
]
}
複製代碼
這裏只對對象的建立、對象上的屬性操做相關、getter/setter方法、對象標籤等進行再分析,對於原型鏈以及原型繼承原理不是本文的重要內容。數組
通常建立對象有如下三種寫法,基本代碼以下:
// 第一種 最簡單的寫法 let obj = { a: 1 } obj.a // 1 typeof obj.toString // 'function' // 第二種 let obj2 = Object.create({ a: 1 }) obj2.a // 1 typeof obj2.toString // 'function' // 第三種 let obj3 = Object.create(null) typeof obj3.toString // 'undefined' 複製代碼
圖解基本以下:
Object.create能夠理解爲繼承一個對象,它是ES5的一個新特性,對於舊版瀏覽器須要作兼容,基本代碼以下(vue使用ie9+瀏覽器,因此不須要作兼容處理):
if (!Object.create) { Object.create = function (o) { function F() {} //定義了一個隱式的構造函數 F.prototype = o; return new F(); //其實仍是經過new來實現的 }; } 複製代碼
其中,在vue源碼中會看見使用Object.create(null)
來建立一個空對象,其好處不用考慮會和原型鏈上的屬性重名問題,vue代碼片斷以下:
// src/core/global-api/index.js // 再Vue上定義靜態屬性options而且賦值位空對象,ASSET_TYPES是在vue上定義的'component','directive','filter'等屬性 Vue.options = Object.create(null) ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) }) 複製代碼
其實在建立對象的同時,對象上會默認設置當前對象的枚舉類型值,若是不設置,默認全部枚舉類型均爲false,那麼如何定義對象而且設置枚舉類型值呢?主要使用到的是ES5的新特性Object.defineProperty
。
Object.defineProperty(obj,prop,descriptor)
中的descriptor
有以下幾種參數:
注意:在 descriptor 中不能同時設置訪問器 (get 和 set) 和 value。
完整示例代碼以下:
Object.defineProperty(obj,prop, configurable: true, enumerable: true, writable: true, value: '', get: function() { }, set: function() { } ) 複製代碼
經過使用Object.getOwnPropertyDescriptor
來查看對象上屬性的枚舉類型值,具體使用相關示例代碼以下:
// 若是不設置枚舉類型,默認都是false let obj = {} Object.defineProperty(obj, 'name', { value : "wqzwh" }) Object.getOwnPropertyDescriptor(obj, 'name') // {value: "wqzwh", writable: false, enumerable: false, configurable: false} let obj2 = {} Object.defineProperty(obj2, 'name', { enumerable: true, writable: true, value : "wqzwh" }) Object.getOwnPropertyDescriptor(obj2, 'name') // {value: "wqzwh", writable: true, enumerable: true, configurable: false} 複製代碼
經過Object.keys()
來獲取對象的key,必須將enumerable
設置爲true才能獲取,不然返回是空數組,代碼以下:
let obj = {} Object.defineProperty(obj, 'name', { enumerable: true, value : "wqzwh" }) Object.keys(obj) // ['name'] 複製代碼
經過propertyIsEnumerable
能夠判判定義的對象是否可枚舉,代碼以下:
let obj = {} Object.defineProperty(obj, 'name', { value : "wqzwh" }) obj.propertyIsEnumerable('name') // false let obj = {} Object.defineProperty(obj, 'name', { enumerable: true, value : "wqzwh" }) obj.propertyIsEnumerable('name') // true 複製代碼
經過hasOwnProperty
來檢測一個對象是否含有特定的自身屬性;和 in 運算符不一樣,該方法會忽略掉那些從原型鏈上繼承到的屬性。代碼以下:
// 使用Object.defineProperty建立對象屬性 let obj = {} Object.defineProperty(obj, 'name', { value : "wqzwh", enumerable: true }) let obj2 = Object.create(obj) obj2.age = 20 for (key in obj2) { console.log(key); // age, name } for (key in obj2) { if (obj2.hasOwnProperty(key)) { console.log(key); // age } } // 普通建立屬性 let obj = {} obj.name = 'wqzwh' let obj2 = Object.create(obj) obj2.age = 20 for (key in obj2) { console.log(key); // age, name } for (key in obj2) { if (obj2.hasOwnProperty(key)) { console.log(key); // age } } 複製代碼
注意:若是繼承的對象屬性是經過
Object.defineProperty
建立的,而且enumerable
未設置成true
,那麼for in
依然不能枚舉出原型上的屬性。(感謝 @SunGuoQiang123 同窗指出錯誤問題,已經作了更改)
經過get/set
方法來檢測屬性變化,基本代碼以下:
function foo() {} Object.defineProperty(foo.prototype, 'z', { get: function(){ return 1 } } ) let obj = new foo(); console.log(obj.z) // 1 obj.z = 10 console.log(obj.z) // 1 複製代碼
這個是z
屬性是foo.prototype
上的屬性而且有get
方法,對於第二次經過obj.z = 10
並不會在obj
自己建立z
屬性,而是直接原型觸發上的get
方法。
圖解基本以下:
若是在建立當前對象上定義z
屬性,而且設置writable
和configurable
爲true
,那麼就能夠改變z
屬性的值,而且刪除z
屬性後再次訪問obj.z
仍然是1,測試代碼以下:
function foo() {} Object.defineProperty(foo.prototype, 'z', { get: function(){ return 1 } } ) let obj = new foo(); console.log(obj.z) // 1 Object.defineProperty(obj, 'z', { value: 100, writable: true, configurable: true } ) console.log(obj.z) // 100 obj.z = 300 console.log(obj.z) // 300 delete obj.z console.log(obj.z) // 1 複製代碼
圖解基本以下:
Object.defineProperty
中的configurable
、enumerable
、writable
、value
、get
、set
幾個參數相互之間的關係到底如何呢?能夠用一張圖來清晰說明:
其實建立對象的同時都會附帶一個__proto__
的原型標籤,除了使用Object.create(null)
創建對象之外,代碼以下:
let obj = {x: 1, y: 2} obj.__proto__.z = 3 console.log(obj.z) // 3 複製代碼
Object.preventExtensions
方法用於鎖住對象屬性,使其不可以拓展,也就是不能增長新的屬性,可是屬性的值仍然能夠更改,也能夠把屬性刪除,Object.isExtensible
用於判斷對象是否能夠被拓展,基本代碼以下:
let obj = {x : 1, y : 2}; Object.isExtensible(obj); // true Object.preventExtensions(obj); Object.isExtensible(obj); // false obj.z = 1; obj.z; // undefined, add new property failed Object.getOwnPropertyDescriptor(obj, 'x'); // Object {value: 1, writable: true, enumerable: true, configurable: true} 複製代碼
Object.seal
方法用於把對象密封,也就是讓對象既不能夠拓展也不能夠刪除屬性(把每一個屬性的configurable設爲false),單數屬性值仍然能夠修改,Object.isSealed
因爲判斷對象是否被密封,基本代碼以下:
let obj = {x : 1, y : 2}; Object.seal(obj); Object.getOwnPropertyDescriptor(obj, 'x'); // Object {value: 1, writable: true, enumerable: true, configurable: false} Object.isSealed(obj); // true 複製代碼
Object.freeze
徹底凍結對象,在seal的基礎上,屬性值也不能夠修改(每一個屬性的wirtable也被設爲false),Object.isFrozen
判斷對象是否被凍結,基本代碼以下:
let obj = {x : 1, y : 2}; Object.freeze(obj); Object.getOwnPropertyDescriptor(obj, 'x'); // Object {value: 1, writable: false, enumerable: true, configurable: false} Object.isFrozen(obj); // true 複製代碼
在介紹這個命題以前,先看一段vue源碼中的model的指令,打開platforms/web/runtime/directives/model.js
,片斷代碼以下:
/* istanbul ignore if */ if (isIE9) { // http://www.matts411.com/post/internet-explorer-9-oninput/ document.addEventListener('selectionchange', () => { const el = document.activeElement if (el && el.vmodel) { trigger(el, 'input') } }) } // 省略 function trigger (el, type) { const e = document.createEvent('HTMLEvents') e.initEvent(type, true, true) el.dispatchEvent(e) } 複製代碼
其中document.activeElement
是當前得到焦點的元素,可使用document.hasFocus()
方法來查看當前元素是否獲取焦點。
對於標準瀏覽器,其提供了可供元素觸發的方法:element.dispatchEvent(). 不過,在使用該方法以前,咱們還須要作其餘兩件事,及建立和初始化。所以,總結說來就是:
document.createEvent() event.initEvent() element.dispatchEvent() 複製代碼
createEvent()
方法返回新建立的Event
對象,支持一個參數,表示事件類型,具體見下表:
參數 事件接口 初始化方法
HTMLEvents HTMLEvent initEvent()
MouseEvents MouseEvent initMouseEvent()
UIEvents UIEvent initUIEvent()
複製代碼
initEvent()
方法用於初始化經過DocumentEvent
接口建立的Event
的值。支持三個參數:initEvent(eventName, canBubble, preventDefault)
. 分別表示事件名稱,是否能夠冒泡,是否阻止事件的默認操做。
dispatchEvent()
就是觸發執行了,上文vue源碼中的el.dispatchEvent(e)
, 參數e表示事件對象,是createEvent()
方法返回的建立的Event
對象。
那麼這個東東具體該怎麼使用呢?例如自定一個click
方法,代碼以下:
// 建立事件. let event = document.createEvent('HTMLEvents'); // 初始化一個點擊事件,能夠冒泡,沒法被取消 event.initEvent('click', true, false); let elm = document.getElementById('wq') // 設置事件監聽. elm.addEventListener('click', (e) => { console.log(e) }, false); // 觸發事件監聽 elm.dispatchEvent(event); 複製代碼
接受兩個參數,第一個是函數(接受三個參數:數組當前項的值、當前項在數組中的索引、數組對象自己
),第二個參數是執行第一個函數參數的做用域對象,也就是上面說的函數中this所指向的值,若是不設置默認是undefined。
這兩種方法都不會改變原數組
示例代碼以下:
let arr = [ 1, 2, 3, 4, 5, 6 ]; console.log( arr.some( function( item, index, array ){ console.log( 'item=' + item + ',index='+index+',array='+array ); return item > 3; })); console.log( arr.every( function( item, index, array ){ console.log( 'item=' + item + ',index='+index+',array='+array ); return item > 3; })); 複製代碼
some方法是碰到一個返回true的值時候就返回了,並無繼續往下運行,而every也同樣,第一個值就是一個false,因此後面也沒有進行下去的必要了,就直接返回結果了。
該方法返回一個矩形對象,其中四個屬性:left、top、right、bottom
,分別表示元素各邊與頁面上邊和左邊的距離,x、y
表示左上角定點的座標位置。
經過這個方法計算得出的left、top、right、bottom、x、y
會隨着視口區域內滾動操做而發生變化,若是你須要得到相對於整個網頁左上角定位的屬性值,那麼只要給top、left屬性值加上當前的滾動位置。
爲了跨瀏覽器兼容,請使用 window.pageXOffset 和 window.pageYOffset 代替 window.scrollX 和 window.scrollY。不能訪問這些屬性的腳本可使用下面的代碼:
// For scrollX (((t = document.documentElement) || (t = document.body.parentNode)) && typeof t.scrollLeft == 'number' ? t : document.body).scrollLeft // For scrollY (((t = document.documentElement) || (t = document.body.parentNode)) && typeof t.scrollTop == 'number' ? t : document.body).scrollTop 複製代碼
在IE中,默認座標從(2,2)開始計算,致使最終距離比其餘瀏覽器多出兩個像素,代碼以下:
document.documentElement.clientTop; // 非IE爲0,IE爲2 document.documentElement.clientLeft; // 非IE爲0,IE爲2 // 因此爲了保持全部瀏覽器一致,須要作以下操做 functiongGetRect (element) { let rect = element.getBoundingClientRect(); let top = document.documentElement.clientTop; let left= document.documentElement.clientLeft; return{ top: rect.top - top, bottom: rect.bottom - top, left: rect.left - left, right: rect.right - left } } 複製代碼
vue中片斷源碼以下:
if (process.env.NODE_ENV !== 'production') { const perf = inBrowser && window.performance /* istanbul ignore if */ if ( perf && perf.mark && perf.measure && perf.clearMarks && perf.clearMeasures ) { mark = tag => perf.mark(tag) measure = (name, startTag, endTag) => { perf.measure(name, startTag, endTag) perf.clearMarks(startTag) perf.clearMarks(endTag) perf.clearMeasures(name) } } } 複製代碼
performance.mark
方法在瀏覽器的性能條目緩衝區中建立一個具備給定名稱的緩衝區,performance.measure
在瀏覽器的兩個指定標記(分別稱爲起始標記和結束標記)之間的性能條目緩衝區中建立一個命名,測試代碼以下:
let _uid = 0 const perf = window.performance function testPerf() { _uid++ let startTag = `test-mark-start:${_uid}` let endTag = `test-mark-end:${_uid}` // 執行mark函數作標記 perf.mark(startTag) for(let i = 0; i < 100000; i++) { } // 執行mark函數作標記 perf.mark(endTag) perf.measure(`test mark init`, startTag, endTag) } 複製代碼
測試結果能夠在谷歌瀏覽器中的Performance
中監測到,效果圖以下:
瀏覽器中performance
處理模型基本以下(更多具體參數說明):
get
方法用於攔截某個屬性的讀取操做,能夠接受三個參數,依次爲目標對象、屬性名和 proxy 實例自己(嚴格地說,是操做行爲所針對的對象),其中最後一個參數可選。
攔截對象屬性的讀取,好比proxy.foo和proxy['foo']
基本使用以下:
let person = { name: "張三" }; let proxy = new Proxy(person, { get: (target, property) => { if (property in target) { return target[property]; } else { throw new ReferenceError("Property \"" + property + "\" does not exist."); } } }); proxy.name // "張三" proxy.age // 拋出一個錯誤 複製代碼
若是一個屬性不可配置(configurable)且不可寫(writable),則 Proxy 不能修改該屬性,不然經過 Proxy 對象訪問該屬性會報錯。示例代碼以下:
const target = Object.defineProperties({}, { foo: { value: 123, writable: false, configurable: false }, }); const handler = { get(target, propKey) { return 'abc'; } }; const proxy = new Proxy(target, handler); proxy.foo // TypeError: Invariant check failed 複製代碼
此方法能夠接受兩個參數,分別是目標對象、需查詢的屬性名,主要攔截以下幾種操做:
若是原對象不可配置或者禁止擴展,這時has攔截會報錯。基本示例代碼以下:
let obj = { a: 10 }; Object.preventExtensions(obj); let p = new Proxy(obj, { has: function(target, prop) { return false; } }); 'a' in p // TypeError is thrown 複製代碼
has
攔截只對in
運算符生效,對for...in
循環不生效。基本示例代碼以下:
let stu1 = {name: '張三', score: 59}; let stu2 = {name: '李四', score: 99}; let handler = { has(target, prop) { if (prop === 'score' && target[prop] < 60) { console.log(`${target.name} 不及格`); return false; } return prop in target; } } let oproxy1 = new Proxy(stu1, handler); let oproxy2 = new Proxy(stu2, handler); 'score' in oproxy1 // 張三 不及格 // false 'score' in oproxy2 // true for (let a in oproxy1) { console.log(oproxy1[a]); } // 張三 // 59 for (let b in oproxy2) { console.log(oproxy2[b]); } // 李四 // 99 複製代碼
使用with
關鍵字的目的是爲了簡化屢次編寫訪問同一對象的工做,基本寫法以下:
let qs = location.search.substring(1); let hostName = location.hostname; let url = location.href; with (location){ let qs = search.substring(1); let hostName = hostname; let url = href; } 複製代碼
使用with
關鍵字會致使代碼性能下降,使用let
定義變量相比使用var
定義變量能提升一部分性能,示例代碼以下:
// 不使用with function func() { console.time("func"); let obj = { a: [1, 2, 3] }; for (let i = 0; i < 100000; i++) { let v = obj.a[0]; } console.timeEnd("func");// 1.310302734375ms } func(); // 使用with而且使用let定義變量 function funcWith() { console.time("funcWith"); const obj = { a: [1, 2, 3] }; with (obj) { let a = obj.a for (let i = 0; i < 100000; i++) { let v = a[0]; } } console.timeEnd("funcWith");// 14.533935546875ms } funcWith(); // 使用with function funcWith() { console.time("funcWith"); var obj = { a: [1, 2, 3] }; with (obj) { for (var i = 0; i < 100000; i++) { var v = a[0]; } } console.timeEnd("funcWith");// 52.078857421875ms } funcWith(); 複製代碼
js引擎在代碼執行以前有一個編譯階段,在不使用with
關鍵字的時候,js引擎知道a是obj上的一個屬性,它就能夠靜態分析代碼來加強標識符的解析,從而優化了代碼,所以代碼執行的效率就提升了。使用了with
關鍵字後,js引擎沒法分辨出a變量是局部變量仍是obj的一個屬性,所以,js引擎在遇到with關鍵字後,它就會對這段代碼放棄優化,因此執行效率就下降了。
使用has
方法攔截with
關鍵字,示例代碼以下:
let stu1 = {name: '張三', score: 59}; let handler = { has(target, prop) { if (prop === 'score' && target[prop] < 60) { console.log(`${target.name} 不及格`); return false; } return prop in target; } } let oproxy1 = new Proxy(stu1, handler); function test() { let score with(oproxy1) { return score } } test() // 張三 不及格 複製代碼
在使用with
關鍵字時候,主要是由於js引擎在解析代碼塊中變量的做用域形成的性能損失,那麼咱們能夠經過定義局部變量來提升其性能。修改示例代碼以下:
// 修改後 function funcWith() { console.time("funcWith"); const obj = { a: [1, 2, 3] }; with (obj) { let a = obj.a for (let i = 0; i < 100000; i++) { let v = a[0]; } } console.timeEnd("funcWith");// 1.7109375ms } funcWith(); 複製代碼
可是在實際使用的時候在with
代碼塊中定義局部變量不是很可行,那麼刪除頻繁查找做用域的功能應該能夠提升代碼部分性能,經測試運行時間幾乎相同,修改代碼以下:
function func() { console.time("func"); let obj = { a: [1, 2, 3] }; let v = obj.a[0]; console.timeEnd("func");// 0.01904296875ms } func(); // 修改後 function funcWith() { console.time("funcWith"); const obj = { a: [1, 2, 3] }; with (obj) { let v = a[0]; } console.timeEnd("funcWith");// 0.028076171875ms } funcWith(); 複製代碼
配上has
函數後執行效果如何呢,片斷代碼以下:
// 第一段代碼其實has方法沒用,只是爲了對比使用 console.time("測試"); let stu1 = {name: '張三', score: 59}; let handler = { has(target, prop) { if (prop === 'score' && target[prop] < 60) { console.log(`${target.name} 不及格`); return false; } return prop in target; } } let oproxy1 = new Proxy(stu1, handler); function test(oproxy1) { return { render: () => { return oproxy1.score } } } console.log(test(oproxy1).render()) // 張三 不及格 console.timeEnd("測試"); // 0.719970703125ms console.time("測試"); let stu1 = {name: '張三', score: 59}; let handler = { has(target, prop) { if (prop === 'score' && target[prop] < 60) { console.log(`${target.name} 不及格`); return false; } return prop in target; } } let oproxy1 = new Proxy(stu1, handler); function test(oproxy1) { let score return { render: () => { with(oproxy1) { return score } } } } console.log(test(oproxy1).render()) // 張三 不及格 console.timeEnd("測試"); // 0.760009765625ms 複製代碼
vue中使用with
關鍵字的片斷代碼以下,主要經過proxy
來攔截AST
語言樹中涉及到的變量以及方法,而且判斷是否AST
語言樹中是否存在爲定義的變量及方法,至於爲何vue
會使用with
關鍵字,具體能夠點擊查看
export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } } 複製代碼
打開platforms/web/entry-runtime-width-compile.js
,查看getOuterHTML
方法,片斷代碼以下:
function getOuterHTML (el: Element): string { if (el.outerHTML) { return el.outerHTML } else { const container = document.createElement('div') container.appendChild(el.cloneNode(true)) return container.innerHTML } } 複製代碼
因爲在IE9-11中SVG
標籤元素是沒有innerHTML
和outerHTML
這兩個屬性,因此會有else
以後的語句
這裏針對proxy
和Object.defineProperty
在vue
源碼中使用作一次補充說明下。vue
中的定義的data
實際上是經過Object.defineProperty
來進行監聽變化的,若是定義的data
單純是對象,按照Object.defineProperty
api介紹是合理的,可是若是是數組呢?這個是如何實現的呢?
注意:
Object.defineProperty
有必定的缺陷:只能針對obj
中的屬性進行數據劫持,若是對象層級過深,那麼須要深度遍歷整個對象;對於數組不能監聽到數據的變化
這裏想說明的是Object.defineProperty
沒法監聽數組的變化,帶着這個疑問查看源碼,先查看src/core/instance/state.js
中的initData
方法,片斷代碼以下:
export function proxy (target: Object, sourceKey: string, key: string) { 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: Component) { // 省略 while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && 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 */) } 複製代碼
這裏重要的是proxy
和observe
,那麼問題來了,爲何proxy
已經監聽了,爲何還須要observe
再次監聽呢,繼續打開src/core/observer/index.js
,片斷代碼以下:
export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } 複製代碼
這裏就判斷了value
的類型,若是value是對象那麼直接return
,若是是數組,那麼會繼續執行ob = new Observer(value)
,其實就是再次監聽。而後根據方法最終找到了,打開src/core/observer/array.js
核心代碼以下:
const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // notify change ob.dep.notify() return result }) }) 複製代碼
這裏爲何會將Array.prototype
賦值給arrayProto
,而且從新定義一個變量arrayMethods
繼承arrayProto
,我的以爲這是一個小技巧,這樣methodsToPatch
方法中的def
(src/core/util/lang.js
文件中的方法,其實就是Object.defineProperty
)的第一個參數就是個對象了,而且將數組的幾個方法所有使用Object.defineProperty
再包裝一次,這樣就能尊崇Object.defineProperty
api規範了。
話題轉回來,其實若是是數組,那麼vue
中須要經過vm.$set
才能及時更新試圖,通過測試發現調用vm.$set
改變數組,實際上是觸發了數組的splice
方法,而splice
方法又被監聽了,因此才能實現最開始的疑問數組也能被監聽,測試代碼以下:
<div> {{arr}} </div> let vm = new Vue({ el: '#app', data() { return { arr: [1, 2] } } }) // 只能經過vm.$set來更新試圖 vm.$set(vm.arr, 0, 31) 複製代碼
這種實現感受存在性能問題,就是數組須要遍歷而且調用
Object.defineProperty
方法。
再說回proxy
,其實這個也有get
和set
方法,proxy
實際上是優越Object.defineProperty
,由於它能夠攔截數組類型的數據,測試代碼以下:
// 由於proxy確定能攔截對象,因此這裏只用數組來作測試 const handler = { get (target, key) { console.log('----get-----') return target[key]; }, set (target, key, value) { console.log('----set-----') target[key] = value; return true; } }; const target = [1,2]; const arr = new Proxy(target, handler); arr[0] = 3 // '----set-----' 複製代碼
所以我以爲,vue徹底可使用
proxy
來替代Object.defineProperty
,性能也能獲得必定的提高。
以上是我對proxy
和Object.defineProperty
作的一個補充,若是有什麼不對的地方,但願可以指出來。
以上主要是在閱讀源碼時,發現不是很明白的api
以及一些方法,每一個人能夠根據本身的實際狀況選擇性閱讀,以上就是所有內容,若是有什麼不對的地方,歡迎提issues