雙向數據綁定原理

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

當且僅當該屬性的 configurabletrue 時,該屬性描述符才能夠被改變洋机,同時該屬性也能從對應的對象上被刪除坠宴。默認為 false

  • enumerable

當且僅當該屬性的 enumerabletrue 時绷旗,該屬性允許被循環(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)
  }
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末何乎,一起剝皮案震驚了整個濱河市句惯,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌支救,老刑警劉巖抢野,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異各墨,居然都是意外死亡指孤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門贬堵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恃轩,“玉大人,你說我怎么就攤上這事黎做〔骢耍” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵蒸殿,是天一觀的道長筷厘。 經常有香客問我,道長宏所,這世上最難降的妖魔是什么酥艳? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮爬骤,結果婚禮上充石,老公的妹妹穿的比我還像新娘。我一直安慰自己霞玄,他們只是感情好赫冬,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布浓镜。 她就那樣靜靜地躺著,像睡著了一般劲厌。 火紅的嫁衣襯著肌膚如雪膛薛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天补鼻,我揣著相機與錄音哄啄,去河邊找鬼。 笑死风范,一個胖子當著我的面吹牛咨跌,可吹牛的內容都是我干的。 我是一名探鬼主播硼婿,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼锌半,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寇漫?” 一聲冷哼從身側響起刊殉,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎州胳,沒想到半個月后记焊,有當地人在樹林里發(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡栓撞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年遍膜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓤湘。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡瓢颅,死狀恐怖,靈堂內的尸體忽然破棺而出弛说,到底是詐尸還是另有隱情挽懦,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布剃浇,位于F島的核電站巾兆,受9級特大地震影響猎物,放射性物質發(fā)生泄漏虎囚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一蔫磨、第九天 我趴在偏房一處隱蔽的房頂上張望淘讥。 院中可真熱鬧,春花似錦堤如、人聲如沸蒲列。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蝗岖。三九已至侥猩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抵赢,已是汗流浹背欺劳。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留铅鲤,地道東北人划提。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像邢享,于是被迫代替她去往敵國和親鹏往。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內容