1.數組的reduce方法
應用場景: 下次操作的初始值,依賴于上次操作的返回值
- 數組的累加計算
const arr = [3, 8, 9 ,12, 89, 56, 43]
// 普通程序員的實現(xiàn)邏輯
let total = 0;
arr.forEach(item => {
total += item;
})
console.log(total)
// reduce方法實現(xiàn)
// arr.reduce(函數隧膏, 初始值)
// arr.reduce((上次計算的結果, 當前循環(huán)的item) => {}, 0)
const total = arr.reduce((oldValue, item) => {
return oldValue + item
}, 0)
console.log(total)
- 鏈式獲取對象屬性的值
const obj = {
name: 'zs',
info: {
address: {
location: '北京順義'
}
}
}
const attrs = ['info', 'address', 'location']
// 第一次reduce
初始值是 obj 這個對象
當前的 item 項是 info
第一次 reduce 的結果是 obj.info 屬性對應的對象
// 第二次reduce
初始值是 obj.info 這個對象
當前的 item 項是 address
第二次reduce的結果是 obj.info.address 屬性對應的對象
// 第三次reduce
初始值是 obj.info.address 這個對象
當前的 item 項是 location
第三次reduce的結果是 obj.info.address.location 屬性的值
const val = attrs.reduce((newObj, k) => {
return newObj[k]
}, obj)
console.log(val)
2.發(fā)布訂閱模式
1. Dep類
- 負責進行依賴收集
- 首先有個數組專門來存放所有的訂閱信息
- 其次挤巡,還要提供一個向數組中追加訂閱信息的方法
- 然后渣慕,還要提供一個循環(huán)弄砍,循環(huán)觸發(fā)數組中的每個訂閱信息
2. Watcher類
- 負責訂閱一些事件
// 收集依賴/收集訂閱者
class Dep {
constructor() {
// 這個 subs 數組批狱,用來存放所有訂閱者的信息
this.subs = []
}
// 向 subs 數組中谓娃,添加訂閱者信息
addSub(watcher) {
this.subs.push(watcher)
}
// 發(fā)布通知(訂閱)的方法
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 訂閱者的類
class Watcher {
constructor(cb) {
// 這里的作用就是cb回調函數,根據得到的最新數據來更新自己的DOM結構的
this.cb = cb
}
update() {
this.cb()
}
}
const w1 = new Watcher(() => {
console.log('我是第一個訂閱者')
})
const w2 = new Watcher(() => {
console.log('我是第二個訂閱者')
})
// 將w1 和 w2這兩個觀察者放入 Dep 的 subs 數組中
const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
// 只要我們?yōu)?Vue 中 data 數據重新賦值了替劈,這個賦值操作寄雀,會被 Vue 監(jiān)聽到
// 然后 Vue 要把數據的變化,通知到每個訂閱者
// 接下來抬纸,訂閱者(DOM元素)要根據最新的數據咙俩,更新自己的內容
dep.notify()
這里 Vue 要做的事情就是要把 data 的變化通知到每一個訂閱者,在這里每一個訂閱者就是DOM元素湿故,當 Vue 發(fā)現(xiàn)數據變化的時候會通知到每個訂閱者拿到最新的數據阿趁,這里通過 dep.notify
方法來執(zhí)行watcher中的 update
方法,update
方法中的回調函數來實現(xiàn) DOM 元素數據的更新
3.使用 Object.defineProperty() 進行數據劫持
- 通過
get()
劫持取值操作 - 通過
set()
劫持賦值操作
Object.defineProperty
語法坛猪,在 MDN 上是這么定義的:
Object.defineProperty(obj, prop, descriptor)
(1)參數
-
obj
要在其上定義屬性的對象脖阵。
-
prop
要定義或修改的屬性的名稱。
-
descriptor
將被定義或修改的屬性描述符墅茉。
(2)返回值
被傳遞給函數的對象命黔。
(3)屬性描述符
Object.defineProperty()
為對象定義屬性,分 數據描述符 和 存取描述符 就斤,兩種形式不能混用悍募。
數據描述符和存取描述符均具有以下可選鍵值:
configurable
當且僅當該屬性的 configurable
為 true
時,該屬性描述符才能夠被改變洋机,同時該屬性也能從對應的對象上被刪除坠宴。默認為 false。
enumerable
當且僅當該屬性的 enumerable
為 true
時绷旗,該屬性允許被循環(huán)喜鼓。默認為 false。
Object.defineProperty(obj, 'name', {
enumerable: true, // 當前屬性衔肢,允許被循環(huán)
configurable: true // 當前屬性允許被配置 delete
})
存取描述符具有以下可選鍵值:
get
一個給屬性提供 getter
的方法庄岖,如果沒有 getter
則為 undefined
。當訪問該屬性時角骤,該方法會被執(zhí)行隅忿,方法執(zhí)行時沒有參數傳入,但是會傳入this
對象(由于繼承關系邦尊,這里的this
并不一定是定義該屬性的對象)硼控。默認為 undefined
。
set
一個給屬性提供 setter
的方法胳赌,如果沒有 setter
則為 undefined
牢撼。當屬性值修改時,觸發(fā)執(zhí)行該方法疑苫。該方法將接受唯一參數熏版,即該屬性新的參數值纷责。默認為 undefined
。
const obj = {
name: 'zs',
age: '23',
}
Object.defineProperty(obj, 'name', {
get() {
return '我不是zs'
}
set(newVal) {
console.log('我不要你給的值', newVal)
dep.notify()
}
})
console.log(obj.name) // 我不是張三
// 這里如果沒有`defineProperty`對屬性進行get操作撼短,那么打印結果應該是zs再膳,但是通過get操作,這里的結果應該是:我不是zs曲横,說明get方法可以攔截這個屬性取值操作(getter)
obj.name = ls // 執(zhí)行后結果為:我不要你給的值 ls
//說明set方法可以攔截這個屬性的賦值操作(setter)
4.模擬Vue實現(xiàn)簡單的雙向數據綁定
- 原理圖:
- html部分:
<div id="app">
<h3>姓名是: {{name}}</h3>
<h3>年齡是:{{age}}</h3>
<h3>info.a的值是:{{info.a}}</h3>
<div>name的值是:<input type="text" v-model="name" /></div>
<div>info.a的值是:<input type="text" v-model="info.a" /></div>
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'zs',
age: 20,
info: {
a: 'a1',
b: 'b1'
}
}
})
</script>
- vue.js內容:
class Vue {
// options指向的就是傳進來的對象
constructor(options) {
this.$data = options.data
// 調用數據劫持的方法
Observe(this.$data)
// 屬性代理
// 我們希望只通過vm就能獲取到data中第一層屬性的值
// 這里就比如我們在生命周期中獲取data中屬性 name 的值可以直接使用 this.name 就是因為我們做了屬性代理
// 即:獲取 vm.name -> 自動去找 vm.$data.name vm在這里只是做了一個代理
Object.keys(this.$data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return this.$data[key]
// 這里只要有訪問vm獲取data值的時候喂柒,它本身并沒有,直接去找 $data 獲取對應的值
},
set(newVal) {
this.$data[key] = newVal
}
})
})
// 調用模板編譯的函數
Compile(options.el, this)
}
}
// 定義一個數據劫持的方法
function Observe(obj) {
// 這是遞歸的終止條件
if(!obj || typeof obj !== 'object') return
const dep = new Dep()
// 通過 Object.keys 獲取到 obj 上的每一個屬性
Object.keys(obj).forEach(key => {
// 當前被循環(huán)的 key 所對應的屬性值
let value = obj[key]
// 判斷 value 是否是一個對象禾嫉,如果是對象那么繼續(xù)遞歸灾杰,如果不是,那么在開頭就會被遞歸終止條件終止了
// 把 value 這個子節(jié)點進行遞歸
Observe(value)
// 需要為當前的 key 所對應的屬性添加 getter 和 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// getter攔截取值后我們應該返回攔截屬性所對應的值
get() {
// Dep.target 此時還沒有為null熙参,還是指向 Watcher 實例
//只要執(zhí)行了下面這一行艳吠,那么剛才 new 的 Watcher 實例
// 就被放入了 dep.subs 這個數組中
// target 所指向的 Watcher 實例,加到數組中
Dep.target && dep.addSub(Dep.target)
return value
},
// setter攔截賦值孽椰,應該把攔截屬性當前值修改為新的值
set(newVal) {
value = newVal
// 為新賦值的對象添加 getter 和 setter
Observe(value)
// 通知每一個訂閱者更新自己的文本
dep.notify()
}
})
})
}
// 對HTML結構進行模板編譯的方法
function Compile(el, vm) {
// 獲取到的 dom 元素直接掛載到 vm 的 $el 上
vm.$el = document.querySelector(el)
// 創(chuàng)建文檔碎片昭娩,提高 DOM 操作性能
// 如果我們頁面中有很多的插值表達式,那么我們要頻繁的去更新 dom 元素的內容黍匾,這個時候會觸發(fā)頁面的重繪和重排栏渺。浪費我們的內存
// 內容發(fā)生變化會觸發(fā)重繪,定位和位置發(fā)生變化會觸發(fā)重排
// 這時候我們就要創(chuàng)建一個文檔碎片锐涯,所謂文檔碎片就是一塊內存磕诊,把頁面的每個 dom 節(jié)點都存進去
// 這時候頁面中就沒有這個 dom 節(jié)點了,我們這時候直接在內存中操作 dom 元素
// 由于文檔碎片不在頁面上全庸,所以我們這時候隨意修改也不會觸發(fā)重繪和重排
const fragment = document.createDocumentFragment() // 創(chuàng)建文檔碎片
while(childNode = vm.$el.firstChild) {
fragment.appendChild(childNode) // 把所有節(jié)點都放入文檔碎片中,這時候頁面中就沒有 dom 節(jié)點了
}
// 再把文檔碎片中的節(jié)點放回到頁面中
// 在這里進行模板編譯
// 因為在這一行之前頁面中還沒有dom節(jié)點融痛,我可以在這個節(jié)點的時候dom元素還在文檔碎片中放著呢
// 此時我們可以操作文檔碎片中的每個子節(jié)點進行編譯壶笼,編譯完成后在append回去就不會觸發(fā)重繪和重排)
Replace(fragment)
vm.$el.appendChild(fragment)
// 負責對 dom 節(jié)點進行編譯的方法
function Replace(node) {
// 對插值表達式進行正則
const regMustache = /\{\{\s*(\S+)\s*\}\}/
// 證明當前的node節(jié)點是一個文本子節(jié)點,需要進行正則的替換
if(node.nodeType === 3) {
// 注意:文本子節(jié)點也是一個 dom 對象
// 如果要獲取文本子節(jié)點的字符串內容雁刷,需要調用 textContent 屬性獲取
const text = node.textContent
// 進行字符串的正則匹配與提取
const execResult = regMustache.exec(text)
if(execResult) {
const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
node.textContent = text.replace(regMustache, value) // 這里的replace方法是字符串本身的方法
// 在這個時候創(chuàng)建 watcher 類的實例
// 為什么要在這里調用Watcher類覆劈?
// 當執(zhí)行到上面這行代碼的時候,你是第一次知道怎么來更新自己
// 這個時候你應該立即把怎么更新自己的代碼存到cb這個回調函數中
// 因為cb回調函數就是來記錄怎么更新自己的
// 怎么存到cb中沛励?這時候需要new一個實例才能存到cb中
new Watcher(vm, execResult[1], (newVal) =>{
// 根據最新的value值來更新自己的文本內容
node.textContent = text.replace(regMustache, newVal)
})
}
// 終止遞歸的條件
return
}
// 實現(xiàn)文本框數據綁定
// 如果是一個 dom 節(jié)點责语,就要判斷你身上有沒有 v-model 這個屬性
// 如果存在我就認為你是一個文本框,并且要給你提供一個值
// 判斷當前的 node 節(jié)點是否為 input 輸入框
if(node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
const attrs = Array.from(node.attributes)
const findResult = attrs.find(x => x.name === 'v-model')
if(findResult) {
// 獲取到當前 v-model 屬性的值 v-model="name" v-model="info.a"
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
node.value = value
// 創(chuàng)建 Watcher 的實例
new Watcher(vm, expStr, (newValue) => {
node.value = newValue
})
// 監(jiān)聽文本框的 input 輸入事件目派,拿到文本框最新的值坤候,把最新的值更新到 vm 上即可
node.addEventListener('input', (e) => {
const keyArr = expStr.split('.')
const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
obj[keyArr[keyArr.length - 1]] = e.target.value
})
}
}
// 走到這一步證明不是文本子節(jié)點,需要進行遞歸處理
node.childNodes.forEach(child => Replace(child))
}
}
// 我們只用 Object.defineProperty 我們只能實現(xiàn)在頁面打開的一瞬間實現(xiàn)數據編譯
// 但是后面頁面數據發(fā)生變化的時候是沒有辦法重現(xiàn)渲染頁面的
// 這時候就需要用到發(fā)布訂閱模式來實現(xiàn)數據的實時更新
// 因為加了發(fā)布訂閱就相當于每個dom訂閱了數據更新的一個行為企蹭,只要數據更新就會自動進行發(fā)布
// 依賴收集的類/收集 watcher 訂閱者的類
class Dep {
constructor() {
// 今后白筹,所有的 watcher 都要存在這個數組中
this.subs = []
}
// 向 subs 數組中添加 watcher 的方法
addSub(watcher) {
this.subs.push(watcher)
}
// 負責同志每一個 watcher 的方法
notify() {
this.subs.forEach((watcher) => watcher.update())
}
}
// 訂閱者的類
class Watcher {
// cb 回調函數中智末,記錄著當前 watcher 如何更新自己的文本內容
// 但是,只知道如何更新自己還不行徒河,還必須拿到最新的數據
// 因此系馆,還需要在 new Watcher 期間,把vm也傳遞進來(因為vm中存著最新的數據)
// 除此之外顽照,還需要知道在 vm 身上眾多的數據中由蘑,哪個數據才是當前自己所需要的數據
// 因此必須在 new Watcher 期間,指定watcher對應的數據的名字
constructor(vm, key, cb) {
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
// 當我們執(zhí)行這一步的操作的時候可以拿到對應key的值代兵,但是我們的目的不是為了拿到key的值
// 因為這一步觸發(fā)了 getter 方法尼酿,到這一步會暫緩下面的代碼執(zhí)行,跳到 getter 函數中奢人,這就是我們的目的(具體看上面getter中操作)
// 我們這里的真正目的是為了將 new Watcher 每次調用的觀察者存入 Dep 數組中谓媒,要不然下次無法通知到它
key.split('.').reduce((newObj, k) => newObj[k], vm)
Dep.target = null
}
// watcher 實例需要有 update 函數,從而讓發(fā)布者能夠通知我們進行更新
update() {
const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
this.cb(value)
}
}