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