vue的响应式原理
Vue 的響應(yīng)式是通過(guò) Object.defineProperty 對(duì)數(shù)據(jù)進(jìn)行劫持,并結(jié)合發(fā)布訂閱者模式實(shí)現(xiàn)。 Vue 利用 Object.defineProperty 創(chuàng)建一個(gè) observe 來(lái)劫持監(jiān)聽(tīng)所有的屬性,把這些屬性全部轉(zhuǎn)為 getter 和 setter。Vue 中每個(gè)組件實(shí)例都會(huì)對(duì)應(yīng)一個(gè) watcher 實(shí)例,它會(huì)在組件渲染的過(guò)程中把使用過(guò)的數(shù)據(jù)屬性通過(guò) getter 收集為依賴。之后當(dāng)依賴項(xiàng)的 setter 觸發(fā)時(shí),會(huì)通知 watcher,從而使它關(guān)聯(lián)的組件重新渲染。
一、reduce
1.數(shù)值的累加
作用:將****前一項(xiàng)*和*后一項(xiàng)****的值進(jìn)行運(yùn)算,返回累積的結(jié)果
格式:數(shù)組.reduce(function(prev,next){…})
其中,prev表示前一項(xiàng),next表示后一項(xiàng)。
運(yùn)算規(guī)則:
默認(rèn)情況下,會(huì)把數(shù)組的第一個(gè)元素作為prev的初始值。
每循環(huán)一次,把累積的結(jié)果賦給prev,next就變?yōu)橄乱粋€(gè)數(shù)組元素
var arr3 = [10,22,23,25,50];const total = arr3.reduce(function(pre,next){console.log(pre+"----"+next);return pre+next;})console.log(total);實(shí)際上,reduce方法還有第二個(gè)參數(shù),****如果傳遞了第二個(gè)參數(shù),就作為prev的初始值****。同時(shí)next就是數(shù)組的第一個(gè)元素。
<script>const total = arr3.reduce(function(pre,next){console.log(pre+"----"+next);return pre+next;},100)console.log(total);</script>2.鏈?zhǔn)将@取對(duì)象的值
const person={name:"尼古拉斯趙四",info:{address:{location:"東北鐵嶺",work:"二人轉(zhuǎn)"}}}const arrs=["info","address","location"];const result=arrs.reduce((newobj,k)=>{console.log(newobj)return newobj[k]},person)console.log(result); //如果不是一個(gè)數(shù)組而是一個(gè)字符串呢 const str="info.address.location"; //console.log(str.split("."))const result2 = str.split(".").reduce((newobj,k)=>{return newobj[k]; },person) console.log(result2);二、Object.defineProperty
Object.defineProperty()的作用就是直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)已經(jīng)存在的屬性
Object.defineProperty(obj, prop, desc)一般通過(guò)為對(duì)象的屬性賦值的情況下,對(duì)象的屬性可以修改也可以刪除,但是通過(guò)Object.defineProperty()定義屬性,通過(guò)描述符的設(shè)置可以進(jìn)行更精準(zhǔn)的控制對(duì)象屬性。
屬性描述符
通過(guò)Object.defineProperty()為對(duì)象定義屬性,有兩種形式,且不能混合使用,分別為數(shù)據(jù)描述符,存取描述符,下面分別描述下兩者的區(qū)別:
數(shù)據(jù)描述符 --特有的兩個(gè)屬性(value,writable)
let Person = {} Object.defineProperty(Person, 'name', {value: 'Lucky',writable: true // 是否可以改變 })存取描述符 --是由一對(duì) getter、setter 函數(shù)功能來(lái)描述的屬性
get:一個(gè)給屬性提供getter的方法,如果沒(méi)有g(shù)etter則為undefined。該方法返回值被用作屬性值。默認(rèn)為undefined。
set:一個(gè)給屬性提供setter的方法,如果沒(méi)有setter則為undefined。該方法將接受唯一參數(shù),并將該參數(shù)的新值分配給該屬性。默認(rèn)值為undefined。
數(shù)據(jù)描述符和存取描述均具有以下描述符
**注意:**configurable: false 時(shí),不能刪除當(dāng)前屬性,且不能重新配置當(dāng)前屬性的描述符(但是可以把writable的狀態(tài)由true改為false,但是無(wú)法由false改為true),但是在writable: true的情況下,可以改變value的值。
其他屬性:Object.seal()、Object.freeze()不再說(shuō)。
configurable: true時(shí),可以刪除當(dāng)前屬性,可以配置當(dāng)前屬性所有描述符。
var fun={name:"kelly",age:'123'}var funage='哈哈';Object.defineProperty(fun,'address',{get(){return funage;},set(val){console.log('觸發(fā)fun')funage=val; }})// fun.name="張三";// console.log(fun);// fun.age=12;// console.log(fun.age)fun.address="中山西路"console.log(fun.address)三、發(fā)布-訂閱者模式
1.vue響應(yīng)原理:
vue.js采用數(shù)據(jù)劫持結(jié)合發(fā)布-訂閱者模式,通過(guò)Object.defineProperty()來(lái)劫持data中各個(gè)屬性的setter、getter,在數(shù)據(jù)變動(dòng)時(shí),發(fā)布消息給訂閱者,觸發(fā)響應(yīng)的監(jiān)聽(tīng)回調(diào)。
(setter和getter是對(duì)象的存儲(chǔ)器屬性,是一個(gè)函數(shù)(屬性),用來(lái)獲取和設(shè)置值)
2、發(fā)布-訂閱者模式的作用:
處理一對(duì)多的場(chǎng)景,應(yīng)用于不同情況下的不同函數(shù)調(diào)用
優(yōu)點(diǎn):低耦合性,易于代碼維護(hù);
缺點(diǎn):若訂閱的消息未發(fā)生,需消耗一定的時(shí)間和內(nèi)存。
發(fā)布訂閱者模式:
其定義對(duì)象間一種一對(duì)多的依賴關(guān)系,當(dāng)一個(gè)對(duì)象的狀態(tài)發(fā)生改變時(shí),所有依賴于它的對(duì)象都將得到通知
舉個(gè)栗子
這里面以微信公眾號(hào)為例:
- 假如用戶大腳訂閱了 胡吃海喝這個(gè)公眾號(hào),那么當(dāng)公眾號(hào)胡吃海喝推送消息的時(shí)候,用戶大腳就會(huì)收到相關(guān)的推送,點(diǎn)開(kāi)可以查看推送的消息內(nèi)容。
- 但是公眾號(hào)胡吃海喝并不關(guān)心訂閱的它的是男人、女人還是未成年,它只負(fù)責(zé)發(fā)布自己的主體,只要是訂閱公眾號(hào)的用戶均會(huì)收到該消息。
- 作為用戶大腳,不需要時(shí)刻打開(kāi)手機(jī)查看公眾號(hào)胡吃海喝是否有推動(dòng)消息,因?yàn)樵诠娞?hào)推送消息的那一刻,大腳就會(huì)收到相關(guān)推送。
- 當(dāng)然了,用戶大腳如果不想繼續(xù)關(guān)注公眾號(hào)胡吃海喝,那么可以取消關(guān)注,取關(guān)以后,公眾號(hào)胡吃海喝再推送消息,大腳就無(wú)法收到了。
還有一個(gè)生活中的栗子,就是買(mǎi)房子的情景。
vue的發(fā)布訂閱模式可以用下圖簡(jiǎn)單描述:
接下來(lái)用代碼實(shí)現(xiàn)簡(jiǎn)單的發(fā)布訂閱者
四、簡(jiǎn)單實(shí)現(xiàn)雙向數(shù)據(jù)綁定
1.創(chuàng)建一個(gè)js文件,vue.js
class Vue {constructor(options) {this.$data = options.data// 調(diào)用數(shù)據(jù)劫持的方法Observe(this.$data)// 屬性代理Object.keys(this.$data).forEach((key) => {Object.defineProperty(this, key, {enumerable: true,configurable: true,get() {return this.$data[key]},set(newValue) {this.$data[key] = newValue},})})// 調(diào)用模板編譯的函數(shù)Compile(options.el, this)} }// 定義一個(gè)數(shù)據(jù)劫持的方法 function Observe(obj) {// 遞歸終止條件if (!obj || typeof obj !== 'object') returnconst dep = new Dep()// 通過(guò) Object.keys(obj) 獲取到當(dāng)前 obj 上的每個(gè)屬性Object.keys(obj).forEach((key) => {// 當(dāng)前被循環(huán)的 key 所對(duì)應(yīng)的屬性值let value = obj[key]// 把 value 這個(gè)子節(jié)點(diǎn),進(jìn)行遞歸Observe(value)// 需要為當(dāng)前的 key 所對(duì)應(yīng)的屬性,添加 getter 和 setterObject.defineProperty(obj, key, {enumerable: true,configurable: true,get() {// 只要執(zhí)行了下面這一行,那么剛才 new 的 Watcher 實(shí)例,// 就被放到了 dep.subs 這個(gè)數(shù)組中了Dep.target && dep.addSub(Dep.target)return value},set(newVal) {value = newValObserve(value)// 通知每一個(gè)訂閱者更新自己的文本dep.notify()},})}) }// 對(duì) HTML 結(jié)構(gòu)進(jìn)行模板編譯的方法 function Compile(el, vm) {// 獲取 el 對(duì)應(yīng)的 DOM 元素vm.$el = document.querySelector(el)// 創(chuàng)建文檔碎片,提高 DOM 操作的性能const fragment = document.createDocumentFragment()while ((childNode = vm.$el.firstChild)) {fragment.appendChild(childNode)}// 進(jìn)行模板編譯replace(fragment)vm.$el.appendChild(fragment)// 負(fù)責(zé)對(duì) DOM 模板進(jìn)行編譯的方法function replace(node) {// 定義匹配插值表達(dá)式的正則const regMustache = /\{\{\s*(\S+)\s*\}\}/// 證明當(dāng)前的 node 節(jié)點(diǎn)是一個(gè)文本子節(jié)點(diǎn),需要進(jìn)行正則的替換if (node.nodeType === 3) {// 注意:文本子節(jié)點(diǎn),也是一個(gè) DOM 對(duì)象,如果要獲取文本子節(jié)點(diǎn)的字符串內(nèi)容,需要調(diào)用 textContent 屬性獲取const text = node.textContent// 進(jìn)行字符串的正則匹配與提取const execResult = regMustache.exec(text)console.log(execResult)if (execResult) {const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)node.textContent = text.replace(regMustache, value)// 在這個(gè)時(shí)候,創(chuàng)建 Watcher 類的實(shí)例new Watcher(vm, execResult[1], (newValue) => {node.textContent = text.replace(regMustache, newValue)})}// 終止遞歸的條件return}// 判斷當(dāng)前的 node 節(jié)點(diǎn)是否為 input 輸入框if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {// 得到當(dāng)前元素的所有屬性節(jié)點(diǎn)const attrs = Array.from(node.attributes)const findResult = attrs.find((x) => x.name === 'v-model')if (findResult) {// 獲取到當(dāng)前 v-model 屬性的值 const expStr = findResult.valueconst value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)node.value = value// 創(chuàng)建 Watcher 的實(shí)例new Watcher(vm, expStr, (newValue) => {node.value = newValue})// 監(jiān)聽(tīng)文本框的 input 輸入事件,拿到文本框最新的值,把最新的值,更新到 vm 上即可node.addEventListener('input', (e) => {const keyArr = expStr.split('.')const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)const leafKey = keyArr[keyArr.length - 1]obj[leafKey] = e.target.value})}}// 證明不是文本節(jié)點(diǎn),可能是一個(gè)DOM元素,需要進(jìn)行遞歸處理node.childNodes.forEach((child) => replace(child))} }// 依賴收集的類/收集 watcher 訂閱者的類 class Dep {constructor() {// 所有的 watcher 都要存到這個(gè)數(shù)組中this.subs = []}// 向 subs 數(shù)組中,添加 watcher 的方法addSub(watcher) {this.subs.push(watcher)}// 負(fù)責(zé)通知每個(gè) watcher 的方法notify() {this.subs.forEach((watcher) => watcher.update())} }// 訂閱者的類 class Watcher {// cb 回調(diào)函數(shù)中,記錄著當(dāng)前 Watcher 如何更新自己的文本內(nèi)容// 但是,只知道如何更新自己還不行,還必須拿到最新的數(shù)據(jù),// 需要在 new Watcher 期間,把 vm 也傳遞進(jìn)來(lái)(因?yàn)?vm 中保存著最新的數(shù)據(jù))// 必須在 new Watcher 期間,指定 watcher 對(duì)應(yīng)的數(shù)據(jù)的名字constructor(vm, key, cb) {this.vm = vmthis.key = keythis.cb = cb// 把創(chuàng)建的 Watcher 實(shí)例存到 Dep 實(shí)例的 subs 數(shù)組中 Dep.target = thiskey.split('.').reduce((newObj, k) => newObj[k], vm)Dep.target = null}// watcher 的實(shí)例,需要有 update 函數(shù),從而讓發(fā)布者能夠通知我們進(jìn)行更新!update() {const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)this.cb(value)} }2.在頁(yè)面中引入vue.js 然后實(shí)例化一個(gè)實(shí)例
<script src="./vue.js"></script></head><body><div id="app"><p>姓名:{{name}}</p><p>年齡:{{age}}</p><p>工作:{{info.work}}</p><p>住址:{{info.address}}</p><input type="text" v-model="phone" /><p>雙向綁定的iPhone:{{phone}}</p></div><script>const vm=new Vue({el:"#app",data:{name:"海綿寶寶",age:3,info:{work:"高級(jí)廚師",address:"太平洋比奇堡"},phone:'123'}})</script>總結(jié)