欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Vue中雙向綁定原理及簡單實(shí)現(xiàn)

 更新時(shí)間:2023年05月06日 16:42:20   作者:彩虹修狗  
這篇文章主要為大家詳細(xì)介紹了Vue中雙向綁定原理及簡單實(shí)現(xiàn),文中的示例代碼講解詳細(xì),對(duì)我們深入了解Vue有一定的幫助,需要的可以參考一下

監(jiān)聽器

vue實(shí)現(xiàn)雙向綁定時(shí),首先要實(shí)現(xiàn)目標(biāo)data的監(jiān)聽(通過 Object.defineProperty 來實(shí)現(xiàn)

(1)遍歷整個(gè)data,對(duì)data下面所有的key進(jìn)行Object.defineProperty來監(jiān)聽,然后獲取監(jiān)聽key的get,set事件。

(2)對(duì)每一個(gè)key進(jìn)行Object.defineProperty監(jiān)聽的時(shí)候,都單獨(dú)的創(chuàng)建一個(gè)Dep類用于收集和觸發(fā)依賴,后續(xù)的更新函數(shù)的觸發(fā)和收集都會(huì)存儲(chǔ)在每一個(gè)key對(duì)應(yīng)的Dep實(shí)例中。

// 數(shù)據(jù)監(jiān)聽,監(jiān)聽對(duì)象的所有g(shù)et和set
function observe(data) {
  // 如果data不存在或data不是object
  if (!data || typeof data !== 'object') {
    return;
  }
  // 遍歷data,獲得當(dāng)前的key
  for (const key in data) {
    // 監(jiān)聽data下的當(dāng)前key的屬性變動(dòng)
    defineReactive(data, key, data[key]); // 一次只處理data和一個(gè)key的監(jiān)聽關(guān)系
  }
}
// 通過Object.defineProperty去監(jiān)聽data下的key
function defineReactive(data, key, value) {
  // 處理key對(duì)應(yīng)的value是一個(gè)對(duì)象的情況
  observe(value)
  // 創(chuàng)建Dep() 實(shí)例 用于存儲(chǔ)key的依賴,以及觸發(fā)key的依賴
  let dep = new Dep()
  Object.defineProperty(data, key, {
    // 返回value值
    get: function () {
      // get的時(shí)候進(jìn)行依賴收集
      if(Dep.target) { 
				// Dep.target默認(rèn)為null,不會(huì)收集依賴,但是創(chuàng)建watcher實(shí)例的時(shí)候
        // 會(huì)為Dep.target賦值,并指向當(dāng)前的watcher實(shí)例,同時(shí)會(huì)為watcher掛載上update函數(shù)
        // 然后get的時(shí)候會(huì)出現(xiàn)觸發(fā)依賴收集,因?yàn)檫@時(shí)候Dep.target指向當(dāng)前的watcher實(shí)例,就可以將這個(gè)watcher實(shí)例收集起來
        dep.addSub(Dep.target)
      }
      return value
    },
    set: function (newValue) {
      // get的時(shí)候觸發(fā)依賴
      val = newValue;
      // 依賴觸發(fā),遍歷依賴?yán)锩娴膚atcher并調(diào)用update函數(shù)
      dep.notify(newValue)
    }
  }
)}
// 創(chuàng)建Dep類,用來存儲(chǔ)依賴
class Dep{
    constructor() {
        this.subs = [] // 定義一個(gè)subs數(shù)組用來存放依賴
    }
    addSub(sub) {
        // get的時(shí)候存儲(chǔ)依賴
        // 注:更新函數(shù)會(huì)被掛載到單獨(dú)創(chuàng)建的watcher實(shí)例,存儲(chǔ)依賴的時(shí)候,實(shí)際上存儲(chǔ)的是創(chuàng)建的watcher實(shí)例
        this.subs.push(sub)
    }
    notify(newValue) {
        // set的時(shí)候觸發(fā)依賴
        // 注: 遍歷依賴,開始執(zhí)行里面watcher實(shí)例的更新函數(shù)
        this.subs.forEach(item => {
            item.update(newValue)
        })
    }
    target = null
}

訂閱器

創(chuàng)建訂閱器,訂閱器作用于觀察器后面,實(shí)際上訂閱器會(huì)被掛載到被監(jiān)聽的key的依賴上。(細(xì)品)

會(huì)緩存上一個(gè)值,以及提供update方法(set的時(shí)候執(zhí)行)

(1) 創(chuàng)建實(shí)例的時(shí)候會(huì)暫存監(jiān)聽的節(jié)點(diǎn)和監(jiān)聽的數(shù)據(jù)對(duì)象等信息,并創(chuàng)建update函數(shù)(用于處理數(shù)據(jù)更新等回調(diào)(2) 初始化的時(shí)候會(huì)調(diào)用訂閱器實(shí)例的get方法,并設(shè)置Dep構(gòu)造函數(shù)的target屬性指向當(dāng)前的訂閱器實(shí)例(注意這個(gè)是一個(gè)實(shí)例,即說明已經(jīng))

class Watcher{
    constructor(vm, prop, callback) {
        this.vm = vm  // 監(jiān)聽的節(jié)點(diǎn)和監(jiān)聽的數(shù)據(jù)對(duì)象
        this.prop = prop // 監(jiān)聽的數(shù)據(jù)對(duì)象的某個(gè)參數(shù)key,如 name、age 等
        this.callback = callback // 存儲(chǔ)回調(diào)
        this.value = this.get() // 觸發(fā)數(shù)據(jù)get操作,開始依賴收集
    }
    get() {
        Dep.target = this // 將訂閱器賦值給Dep的target
        // 調(diào)用watcher前需要調(diào)用observe,所以這里的data已經(jīng)被監(jiān)聽了,get的時(shí)候開始依賴收集因?yàn)镈ep.target = this,所以target非空,Watcher會(huì)被插入到data的key下的依賴集合中
        const value = this.vm.data[this.prop] 
        Dep.target = null // 關(guān)閉依賴收集,防止每次都收集依賴
        return value // 獲取當(dāng)前的值
    }
    // 注意調(diào)用update的時(shí)候是在監(jiān)聽器的notify里面進(jìn)行的,說明已經(jīng)set操作完了,值已經(jīng)變了,但是watcher的value值在get的時(shí)候先緩存了上一次value值
    update(newValue) { // 更新函數(shù)
        const value = newValue // 獲取當(dāng)前的值
        const oldValue = this.value // 獲取之前緩存的值
        if(value != oldValue) { // 如果當(dāng)前的值和之前緩存的值不同則觸發(fā)更新函數(shù),并重置value
            this.value = value
            this.callback(value); // 觸發(fā)更新回調(diào)函數(shù)
        }
    }
}

雙向綁定構(gòu)造函數(shù)

基于監(jiān)聽器及訂閱器,我們已經(jīng)可以完成簡單的數(shù)據(jù)更新

具體使用時(shí)會(huì)先創(chuàng)建一個(gè)雙向綁定構(gòu)造函數(shù)實(shí)例,然后將傳入的參數(shù)緩存起來。

然后調(diào)用實(shí)例的init方法,init方法中會(huì)對(duì)傳入的參數(shù)(包括需要監(jiān)聽的data),對(duì)需要監(jiān)聽的data進(jìn)行observe數(shù)據(jù)監(jiān)聽,所以這時(shí)候傳入的data已經(jīng)被我們監(jiān)聽了,我們已經(jīng)是可以獲取到get,set事件的。

然后我們會(huì)顯示的單獨(dú)創(chuàng)建一個(gè)訂閱器實(shí)例

訂閱器的構(gòu)造方法中同樣會(huì)緩存?zhèn)魅氲膮?shù),然后調(diào)用訂閱器的get方法,訂閱器的get方法,會(huì)設(shè)置公共的Dep類,給Dep類的target參數(shù)指向自身,即Dep.target = 訂閱器

然后顯示的觸發(fā)data的get操作,因?yàn)間et操作中會(huì)判斷Dep.target 是否存在,如果存在則存入緩存(dep),再把Dep.target 重新指向 null,防止后面反復(fù)get操作

完成上述步驟之后,我們可以發(fā)現(xiàn)目標(biāo)data的key不僅我們可以監(jiān)聽到它的get,set操作,并成功對(duì)齊設(shè)置了依賴,后續(xù)數(shù)據(jù)變動(dòng)的話實(shí)際上就會(huì)觸發(fā)set操作,會(huì)用更新函數(shù)同步視圖即可。

class MyVue {
  constructor(options, prop) {
    this.options = options // 需要雙向綁定的對(duì)象,包括數(shù)據(jù)對(duì)象和對(duì)應(yīng)的節(jié)點(diǎn)
    this.data = options.data // 需要監(jiān)聽的數(shù)據(jù)對(duì)象
    this.el = options.el // 需要更新視圖節(jié)點(diǎn)
    this.prop = prop // 對(duì)應(yīng)的key值
    this.init()
  }
  init() {
    observe(this.data) // 監(jiān)聽data
    document.getElementById(this.el).textContent = this.data[this.prop] // 將數(shù)據(jù)初始化到頁面上
    // 創(chuàng)建watcher實(shí)例,在watcher中對(duì)data進(jìn)行g(shù)et操作,并將watcher本身掛載到data對(duì)應(yīng)的key的依賴下
    new Watcher(this.options, this.prop, value => {
      document.getElementById(this.el).textContent = value
    })
  }
}

初步效果:

編譯器

在上面的例子中我們已經(jīng)完成了如何監(jiān)聽一個(gè)數(shù)據(jù),然后當(dāng)數(shù)據(jù)變動(dòng)的時(shí)候,觸發(fā)函數(shù)依賴去更新頁面。

但這是個(gè)單向的流程,我們只能監(jiān)聽數(shù)據(jù)變動(dòng)操作頁面的元素更改,而且在雙向綁定構(gòu)造函數(shù)中我們顯式的創(chuàng)建了watcher實(shí)例(只為id是app的節(jié)點(diǎn)負(fù)責(zé),且這個(gè)節(jié)點(diǎn)下面如果嵌套其他節(jié)點(diǎn)我們就無法處理了) ,顯然是不合理的,我們應(yīng)該要基于頁面元素去分析針對(duì)對(duì)應(yīng)的內(nèi)容進(jìn)行watcher創(chuàng)建。

// 完全是為一個(gè)節(jié)點(diǎn)服務(wù),我們應(yīng)該要針對(duì)不同元素動(dòng)態(tài)的掛載watcher
init() {
  observe(this.data) // 監(jiān)聽data
  document.getElementById(this.el).textContent = this.data[this.prop] // 將數(shù)據(jù)初始化到頁面上
  // 創(chuàng)建watcher實(shí)例,在watcher中對(duì)data進(jìn)行g(shù)et操作,并將watcher本身掛載到data對(duì)應(yīng)的key的依賴下
  new Watcher(this.options, this.prop, value => {
    document.getElementById(this.el).textContent = value
  })
}

而且watcher的get的依賴收集操作,是直接讀data下的參數(shù),意味著如果data.a.b這種情況是無法使用的。

// 無法處理復(fù)雜數(shù)據(jù)類型,這種只能處理data對(duì)象下的數(shù)據(jù),如果是data.a.b就會(huì)異常
const value = this.vm.data[this.prop] 

所以我們需要有一個(gè)編譯器,去自動(dòng)讀取頁面的目標(biāo)節(jié)點(diǎn)下的所有節(jié)點(diǎn),對(duì)每一個(gè)節(jié)點(diǎn)進(jìn)行分析,比如a節(jié)點(diǎn)是個(gè)什么類型的節(jié)點(diǎn)。

如果是文本節(jié)點(diǎn)的話,內(nèi)容里面是否包含{{}}語法,如果包含了{(lán){}}里面的內(nèi)容,那么這個(gè)內(nèi)容是否已經(jīng)在我們data中聲明,聲明的話,我們就需要針對(duì)這個(gè)內(nèi)容給它加上這個(gè)節(jié)點(diǎn)的依賴,告訴它如果你的值要改變那么你的回調(diào)函數(shù)中需要更新這個(gè)節(jié)點(diǎn)下的值。

如果是input標(biāo)簽等元素節(jié)點(diǎn)的話,你需要分析這個(gè)節(jié)點(diǎn)有什么屬性,是否有v-開頭的屬性,當(dāng)然你也可以隨便自定義個(gè)什么東西,如果解析到這種元素節(jié)點(diǎn),你首先要做的是判斷這個(gè)節(jié)點(diǎn)下面有沒有子字節(jié),如果有就開始遞歸調(diào)用一直去解析其子節(jié)點(diǎn)。

如果沒有子節(jié)點(diǎn)就要去分析這個(gè)是什么元素,上面綁了什么屬性。

比如v-html那么在對(duì)這個(gè)節(jié)點(diǎn)創(chuàng)建watcher的時(shí)候,就不能像文本節(jié)點(diǎn)那樣node.textcontent = xxx,而應(yīng)該設(shè)置其html。

同樣的最常見的v-model,我們用在對(duì)應(yīng)的元素節(jié)點(diǎn)上時(shí),我們?cè)跒檫@個(gè)節(jié)點(diǎn)創(chuàng)建watcher的時(shí)候,還得為這個(gè)節(jié)點(diǎn)設(shè)置oninput事件,因?yàn)楫?dāng)其內(nèi)容改變的時(shí)候,我們要用這個(gè)回調(diào)同步的修改我們目標(biāo)data下的值。

下面直接開始上代碼我們需要暴露一個(gè)編譯器類

(1)Compile類

在構(gòu)造函數(shù)中暫存我們雙向綁定構(gòu)造函數(shù)實(shí)例,同時(shí)讀取當(dāng)前目標(biāo)節(jié)點(diǎn)的內(nèi)容,然后將目標(biāo)節(jié)點(diǎn)中的值全部移動(dòng)到我們創(chuàng)建的文檔碎片中,這一步是為了一次性渲染完減少資源損耗,獲取到目標(biāo)節(jié)點(diǎn)下所有的節(jié)點(diǎn)之后,我們要對(duì)這個(gè)節(jié)點(diǎn)進(jìn)行分析主要是為每一個(gè)節(jié)點(diǎn)去建立映射關(guān)系為其設(shè)置對(duì)應(yīng)的watcher,如果是有v-model這種事件還得給這個(gè)節(jié)點(diǎn)加上如oninput或者change這種事件。最后把處理完的節(jié)點(diǎn)重新放回頁面即可。

class Compile {
  // 對(duì)數(shù)據(jù)進(jìn)行解析
  constructor(el, vm) { // el當(dāng)前目標(biāo)元素根節(jié)點(diǎn),vm雙向綁定構(gòu)造函數(shù)實(shí)例
    this.el = this.isElementNode(el) ? el : document.getElementById(el) // 獲取目標(biāo)節(jié)點(diǎn)
    this.vm = vm // 暫存構(gòu)造函數(shù)實(shí)例
    // 獲取文檔碎片節(jié)點(diǎn),暫存在內(nèi)容中
    const fragmentNode = this.node2Fragment(this.el)
    // 模板編譯,主要是為每一個(gè)節(jié)點(diǎn)去建立映射關(guān)系為其設(shè)置對(duì)應(yīng)的watcher
    this.compile(fragmentNode)
    // 將處理后端
    this.el.appendChild(fragmentNode)
  }
  ....
}

(2)node2Fragment函數(shù)

這個(gè)是讀取目標(biāo)節(jié)點(diǎn)的函數(shù),會(huì)創(chuàng)建一個(gè)新的空白文檔碎片,把原來目標(biāo)節(jié)點(diǎn)下的子節(jié)點(diǎn)一個(gè)一個(gè)放進(jìn)去

// 解析元素節(jié)點(diǎn)
  node2Fragment(Node) {
    // 創(chuàng)建空白文檔
    const f = document.createDocumentFragment()
    // 每次先看看Node.firstChild是否存在
    while((Node.firstChild)) {
      // 把子元素加到空白文檔中,加過去之后意味著目標(biāo)節(jié)點(diǎn)第一個(gè)節(jié)點(diǎn)遷移到空白文檔中,那么目標(biāo)節(jié)點(diǎn)的元素就會(huì)越來越少,全部變成空白文檔下的子元素
      f.appendChild(Node.firstChild)
    }
    return f
  }

(3)compile函數(shù)

這個(gè)是解析的核心函數(shù),用來解析每一個(gè)節(jié)點(diǎn),判斷這個(gè)節(jié)點(diǎn)是什么類型,然后需要如何為其設(shè)置watcher。

可以看到這個(gè)函數(shù)讀取了我們傳過來的文檔碎片,如果一個(gè)一個(gè)遍歷,判斷你這個(gè)節(jié)點(diǎn)是否是含有子節(jié)點(diǎn)的,如果有子節(jié)點(diǎn)那就繼續(xù)遞歸遍歷,如果沒有就判斷你是什么類型的節(jié)點(diǎn)是文本節(jié)點(diǎn)還是元素節(jié)點(diǎn),文本節(jié)點(diǎn)調(diào)用文本編譯函數(shù),元素節(jié)點(diǎn)調(diào)用元素編譯函數(shù)。

// 編譯模板
compile(fragmentNode) {
  // 獲取模板的子節(jié)點(diǎn)
  const childNodes = fragmentNode.childNodes;
  // 遍歷所有節(jié)點(diǎn)
  [...childNodes].forEach( child => {
    // 判斷子節(jié)點(diǎn)中是否還有節(jié)點(diǎn) 遞歸調(diào)用
    if (child.childNodes && child.childNodes.length) {
      this.compile(child);
    }
      // 如果是元素節(jié)點(diǎn)類型
    else if(this.isElementNode(child)) {
      // 使用元素編譯
      this.elementCompile(child)
    } 
      // 如果是文本節(jié)點(diǎn)類型
    else {
      // 使用文本編譯
      this.textCompile(child)
    }
  })
}

1.文本編譯函數(shù)

文本編譯函數(shù),針對(duì)文本類型節(jié)點(diǎn)編譯的函數(shù),主要是負(fù)責(zé)那些{{}}語法的處理,在這里會(huì)匹配這個(gè)節(jié)點(diǎn)里面有沒有{{}}語法,如果有就調(diào)用compileFunc下的text函數(shù),給這個(gè)節(jié)點(diǎn)單獨(dú)添加watcher,使得你數(shù)據(jù)更新的時(shí)候可以用watcher里面的回調(diào)去修改頁面的對(duì)應(yīng)節(jié)點(diǎn)的數(shù)據(jù)。

// 文本編譯函數(shù)
textCompile(node) {
  // 獲取節(jié)點(diǎn)文字內(nèi)容
  const content = node.textContent;
  // 匹配插值語法,如果有{{}}語法則為其設(shè)置watcher
  if(/{{.+?}}/.test(content)) {
    compileFunc['text'](node, content, this.vm)
  }
}

下面是compileFunc關(guān)于text模塊的代碼,首先匹配出所有的{{}}內(nèi)的字符,生成一個(gè)數(shù)組,對(duì)這個(gè)數(shù)組去重,因?yàn)橐粋€(gè)text節(jié)點(diǎn)里面是一個(gè)字符串,而一個(gè)字符串可能不只有一個(gè){{}}所以需要找到所有的{{}}內(nèi)的字符,因?yàn)槊恳粋€(gè)字符都相當(dāng)于一個(gè)被監(jiān)聽的key,他們都有自己的回調(diào)都會(huì)對(duì)這個(gè)節(jié)點(diǎn)進(jìn)行操作(ps當(dāng)然這個(gè)操作有點(diǎn)問題后面再說)

找到所有的字符之后就開始循環(huán),為其單獨(dú)設(shè)置回調(diào)函數(shù)即

this.updater.textUpdater(node, value, propList[index], content)

回調(diào)函數(shù)的作用是去設(shè)置這個(gè)節(jié)點(diǎn)下的textcontent的value,然后為這個(gè){{}}內(nèi)的字符創(chuàng)建一個(gè)watcher,值得注意的是因?yàn)閧{}}里面可能有a.b.c這種情況我們還得返回一個(gè)函數(shù),在調(diào)用時(shí)會(huì)取到data下對(duì)應(yīng)的值,只有才能給對(duì)象里面對(duì)象的可以設(shè)置回調(diào)

結(jié)合上面我們對(duì)watcher的設(shè)計(jì)可以知道,如果我們知道了目標(biāo)data和需要監(jiān)聽的data下的key,我們?cè)趧?chuàng)建watcher實(shí)例的時(shí)候就可以將這個(gè)回調(diào)傳入這個(gè)key的依賴中,因?yàn)槲覀冊(cè)诨卣{(diào)中通過閉包的方法我們提前傳入了包括對(duì)應(yīng)元素節(jié)點(diǎn),文案內(nèi)容等信息,所以我們調(diào)用這個(gè)函數(shù)的時(shí)候可以精準(zhǔn)的修改到對(duì)應(yīng)節(jié)點(diǎn)。

const compileFunc = {
    text(node, content, vm) {
        const textMatch = /(?<={{)(.+?)(?=})/g;
        let propList = content.match(textMatch)
        propList = [...new Set(propList)]
        for (let index = 0; index < propList.length; index++) {
            let func = value => {
                this.updater.textUpdater(node, value, propList[index], content)
            }
            new Watcher(vm, this.propFunc(propList[index], vm), func)
        }
    },
    // 處理取對(duì)象下的值的情況,因?yàn)閧{}}里面會(huì)有{{a.b.c}}這種情況需要通過一個(gè)函數(shù)的形式在watcher中進(jìn)行g(shù)et操作
    propFunc(value, vm) {
        return function() {value.split(".").reduce((p, c) => {
            return p[c];
        }, vm.data)}
    },
    // 實(shí)際操作dom方法
    updater: {
        textUpdater(node, value, prop, content) {
            node.textContent = content.replaceAll(`{{${prop}}}`, value);
        },
    }
}

2.元素編譯函數(shù)(僅做實(shí)例只實(shí)現(xiàn)v-model)

元素編譯函數(shù)主要是針對(duì)元素類型節(jié)點(diǎn)的,不同于文本類型,元素類型主要是處理如v-model這種屬性,會(huì)讀取這個(gè)元素節(jié)點(diǎn)下的屬性,判斷是否v-開頭的

// 判斷是否v-開頭的屬性
isMyVueAttributes(name) {
    return name.startsWith('v-')
}

如果有那就針對(duì)這個(gè)節(jié)點(diǎn)的這個(gè)屬性去調(diào)用compileFunc下的對(duì)應(yīng)屬性函數(shù),如v-model,就調(diào)用model函數(shù)。

// 元素編譯函數(shù)
elementCompile(node) {
    // 獲得目標(biāo)的節(jié)點(diǎn)的屬性
    const attributes = node.attributes;
    [...attributes].forEach(item => {
        // 解構(gòu)出參數(shù)的name和value, name相當(dāng)于v指令如v-model等,value則是對(duì)應(yīng)的值 name, age等
        const {name, value} = item
        if(!this.isMyVueAttributes(name)) return
        const attributesName = name.split('-')[1]
        compileFunc[attributesName](node, value, this.vm)
    })
}

可以看看compileFunc下的model函數(shù)干了啥,首先定義一個(gè)回調(diào)函數(shù),函數(shù)的核心是modelUpdater,類似于textUpdater,做的事情是data的key改變時(shí)同步更新頁面,即數(shù)據(jù)更新視圖,并將這個(gè)作為回調(diào)傳入對(duì)應(yīng)的watcher中,這時(shí)data的key就可以通過調(diào)用依賴的方式更新視圖了。

同時(shí)我們也看到我們給這個(gè)節(jié)點(diǎn)加了oninput事件,因?yàn)檫@個(gè)是雙向綁定,視圖一樣要改變數(shù)據(jù),所以當(dāng)輸入了內(nèi)容之后通過oninput事件同步修改頁面,這就是雙向綁定

model(node, prop, vm) {
    let func = value => {
        this.updater.modelUpdater(node, value)
    }
    new Watcher(vm, this.propFunc(prop, vm), func)
    node.oninput =(item)=>{
        // 這里還沒辦法處理字符串轉(zhuǎn)對(duì)應(yīng)對(duì)象
        vm.data[prop] = item.srcElement.value
    }
},
...
modelUpdater(node, value) {
    node.value = value;
},

基于上面的實(shí)現(xiàn)我們已經(jīng)完成了監(jiān)聽器,訂閱器,雙向綁定構(gòu)造函數(shù)以及編譯器,但是還有一點(diǎn)問題,我們可以看到雙向綁定構(gòu)造函數(shù)我們的init()方法是這樣子寫的,只針對(duì)一個(gè)節(jié)點(diǎn)創(chuàng)建watcher

init() {
  observe(this.data) // 監(jiān)聽data
  document.getElementById(this.el).textContent = this.data[this.prop] // 將數(shù)據(jù)初始化到頁面上
  // 創(chuàng)建watcher實(shí)例,在watcher中對(duì)data進(jìn)行g(shù)et操作,并將watcher本身掛載到data對(duì)應(yīng)的key的依賴下
  new Watcher(this.options, this.prop, value => {
    document.getElementById(this.el).textContent = value
  })
}

又因?yàn)槲覀冊(cè)趯懢幾g器的時(shí)候其實(shí)已經(jīng)將watcher整合進(jìn)compile了,所以之后我們其實(shí)只需要?jiǎng)?chuàng)建一個(gè)complie實(shí)例即可監(jiān)聽下面所有的節(jié)點(diǎn)。

init() {
    observe(this.data) // 監(jiān)聽data
    new Compile(this.el, this.options)
}

總結(jié)

如何實(shí)現(xiàn)一個(gè)雙向綁定:

(1)監(jiān)聽器:對(duì)你的目標(biāo)data下的所有key進(jìn)行一個(gè)監(jiān)聽,并且提供一個(gè)Dep類用于依賴的收集及執(zhí)行,在進(jìn)行監(jiān)聽的時(shí)候需要對(duì)每一個(gè)key實(shí)例化一個(gè)Dep,對(duì)這個(gè)key進(jìn)行g(shù)et操作時(shí)需要判斷Dep類上target是否有內(nèi)容,如果有那就將這個(gè)內(nèi)容塞入實(shí)例化的Dep中這就是依賴收集,等到set的時(shí)候再將實(shí)例化的Dep中收集的依賴全部執(zhí)行一遍這就是依賴執(zhí)行。

(2)訂閱器:負(fù)責(zé)給監(jiān)聽器設(shè)置依賴,當(dāng)我們實(shí)現(xiàn)一個(gè)依賴函數(shù)時(shí)比如我們更新了數(shù)據(jù)應(yīng)該要修改頁面某個(gè)節(jié)點(diǎn)值時(shí),這個(gè)修改的函數(shù)會(huì)連同data,key一同傳入訂閱器的構(gòu)造函數(shù)中,在訂閱器的構(gòu)造方法會(huì)先緩存執(zhí)行參數(shù),然后里面我們會(huì)調(diào)用一個(gè)get方法這個(gè)方法會(huì)將Dep的target指向watcher實(shí)例本身,然后get里面自己讀取一次對(duì)應(yīng)data下的key,這一步會(huì)觸發(fā)監(jiān)聽器的get操作,因?yàn)楸O(jiān)聽器的get操作會(huì)判斷Dep類上target是否有內(nèi)容,因?yàn)檫@時(shí)候Dep的target指向watcher實(shí)例本身,所以watcher實(shí)例被加入key的實(shí)例化Dep的依賴中,完成了依賴的收集,最后再把Dep的target重新指向null,防止反復(fù)收集。

(3)編譯器:用于動(dòng)態(tài)的為每個(gè)節(jié)點(diǎn)添加watcher的類,會(huì)先創(chuàng)建一個(gè)空白的文檔碎片,然后將目標(biāo)節(jié)點(diǎn)下的所有節(jié)點(diǎn)移動(dòng)到空白碎片文檔中,這一步是為了對(duì)節(jié)點(diǎn)進(jìn)行分析,分析完之后一次性加入到頁面中防止頻繁改動(dòng)。

然后就會(huì)調(diào)用編譯函數(shù),遍歷所有節(jié)點(diǎn),如果有子節(jié)點(diǎn)就遞歸遍歷,如果是元素節(jié)點(diǎn)就調(diào)用元素解析器,如果是文本節(jié)點(diǎn)就調(diào)用文本解析器,對(duì)每一個(gè)節(jié)點(diǎn)設(shè)置相關(guān)key的watcher實(shí)現(xiàn)數(shù)據(jù)更新視圖,如果這個(gè)節(jié)點(diǎn)需要更新數(shù)據(jù)的話還得給他設(shè)置回調(diào)函數(shù),使得其可以在回調(diào)函數(shù)中更新數(shù)據(jù)。這就是雙向綁定的原理。

(4)雙向綁定構(gòu)造函數(shù)

緩存我們傳入的目標(biāo)節(jié)點(diǎn)和data,在init方法中調(diào)用監(jiān)聽器,并且實(shí)例化編譯器對(duì)每一個(gè)節(jié)點(diǎn)掛載對(duì)應(yīng)的watcher和回調(diào)函數(shù)即可。

效果

完整代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>VUE雙向綁定原理實(shí)現(xiàn)</title>
</head>
<body>
    <div id="app">{{name}}{{name}}<div>{{child.name}}</div><input v-model="name"></input></div>
    <div id="app1" v-model="a.b.c"></div>
    <script>
        // 監(jiān)聽器
        // 數(shù)據(jù)監(jiān)聽,監(jiān)聽對(duì)象的所有g(shù)et和set
        function observe(data) {
            // 如果data不存在或data不是object
            if (!data || typeof data !== 'object') {
                return;
            }
            // 遍歷data,獲得當(dāng)前的key
            for (const key in data) {
                // 監(jiān)聽data下的當(dāng)前key的屬性變動(dòng)
                defineReactive(data, key, data[key]); // 一次只處理data和一個(gè)key的監(jiān)聽關(guān)系
            }
        }
        // 通過Object.defineProperty去監(jiān)聽data下的key
        function defineReactive(data, key, value) {
            // 處理key對(duì)應(yīng)的value是一個(gè)對(duì)象的情況
            observe(value)
            // 創(chuàng)建Dep() 實(shí)例 用于存儲(chǔ)key的依賴,以及觸發(fā)key的依賴
            let dep = new Dep()
            Object.defineProperty(data, key, {
                // 返回value值
                get: function () {
                    // get的時(shí)候進(jìn)行依賴收集
                    if(Dep.target) { 
                        // Dep.target默認(rèn)為null,不會(huì)收集依賴,但是創(chuàng)建watcher實(shí)例的時(shí)候
                        // 會(huì)為Dep.target賦值并指向當(dāng)前的watcher實(shí)例,同時(shí)會(huì)為watcher掛載上update函數(shù)
                        // 然后get的時(shí)候會(huì)出現(xiàn)觸發(fā)依賴收集,因?yàn)檫@時(shí)候Dep.target指向當(dāng)前的watcher實(shí)例,就可以將這個(gè)watcher實(shí)例收集起來
                        dep.addSub(Dep.target)
                    }
                    return value
                },
                set: function (newValue) {
                    // get的時(shí)候觸發(fā)依賴
                    val = newValue;
                    // 依賴觸發(fā),遍歷依賴?yán)锩娴膚atcher并調(diào)用update函數(shù)
                    dep.notify(newValue)
                }
            }
        )}
        // 創(chuàng)建Dep類,用來存儲(chǔ)依賴
        class Dep{
            constructor() {
                this.subs = [] // 定義一個(gè)subs數(shù)組用來存放依賴
            }
            addSub(sub) {
                // get的時(shí)候存儲(chǔ)依賴
                // 注:更新函數(shù)會(huì)被掛載到單獨(dú)創(chuàng)建的watcher實(shí)例,存儲(chǔ)依賴的時(shí)候,實(shí)際上存儲(chǔ)的是創(chuàng)建的watcher實(shí)例
                this.subs.push(sub)
            }
            notify(newValue) {
                // set的時(shí)候觸發(fā)依賴
                // 注: 遍歷依賴,開始執(zhí)行里面watcher實(shí)例的更新函數(shù)
                this.subs.forEach(item => {
                    item.update(newValue)
                })
            }
            target = null
        }
        // 訂閱器
        class Watcher{
            constructor(vm, prop, callback) {
                this.vm = vm  // 監(jiān)聽的節(jié)點(diǎn)和監(jiān)聽的數(shù)據(jù)對(duì)象
                this.prop = prop // 監(jiān)聽的數(shù)據(jù)對(duì)象的某個(gè)參數(shù)key,如 name、age 等
                this.callback = callback // 存儲(chǔ)回調(diào)
                this.value = this.get() // 觸發(fā)數(shù)據(jù)get操作,開始依賴收集
            }
            get() {
                Dep.target = this // 將訂閱器賦值給Dep的target
                // 調(diào)用watcher前需要調(diào)用observe,所以這里的data已經(jīng)被監(jiān)聽了,get的時(shí)候開始依賴收集因?yàn)镈ep.target = this,所以target非空,Watcher會(huì)被插入到data的key下的依賴集合中
                const value = this.prop() // 由于要處理取對(duì)象下面的對(duì)象的值的情況,這里需要返回一個(gè)函數(shù)在get()的時(shí)候再顯式的收集依賴
                Dep.target = null // 關(guān)閉依賴收集,防止每次都收集依賴
                return value // 獲取當(dāng)前的值
            }
            // 注意調(diào)用update的時(shí)候是在監(jiān)聽器的notify里面進(jìn)行的,說明已經(jīng)set操作完了,值已經(jīng)變了,但是watcher的value值在get的時(shí)候先緩存了上一次value值
            update(newValue) { // 更新函數(shù)
                const value = newValue // 獲取當(dāng)前的值
                const oldValue = this.value // 獲取之前緩存的值
                if(value != oldValue) { // 如果當(dāng)前的值和之前緩存的值不同則觸發(fā)更新函數(shù),并重置value
                    this.value = value
                    this.callback(value); // 觸發(fā)更新回調(diào)函數(shù)
                }
            }
        }
        // 編譯器
        class Compile {
            // 對(duì)數(shù)據(jù)進(jìn)行解析
            constructor(el, vm) { // el當(dāng)前目標(biāo)元素根節(jié)點(diǎn),vm雙向綁定構(gòu)造函數(shù)實(shí)例
                this.el = this.isElementNode(el) ? el : document.getElementById(el) // 獲取目標(biāo)節(jié)點(diǎn)
                this.vm = vm // 暫存構(gòu)造函數(shù)實(shí)例
                // 獲取文檔碎片節(jié)點(diǎn),暫存在內(nèi)容中
                const fragmentNode = this.node2Fragment(this.el)
                // 模板編譯,主要是為每一個(gè)節(jié)點(diǎn)去建立映射關(guān)系為其設(shè)置對(duì)應(yīng)的watcher,把上面創(chuàng)建的節(jié)點(diǎn)傳入解析器去編譯分析
                this.compile(fragmentNode)
                // 將處理后端
                this.el.appendChild(fragmentNode)
            }
            // 解析元素節(jié)點(diǎn)
            node2Fragment(Node) {
                // 創(chuàng)建空白文檔
                const f = document.createDocumentFragment()
                // 每次先看看Node.firstChild是否存在
                while((Node.firstChild)) {
                    // 把子元素加到空白文檔中,加過去之后意味著目標(biāo)節(jié)點(diǎn)第一個(gè)節(jié)點(diǎn)遷移到空白文檔中,那么目標(biāo)節(jié)點(diǎn)的元素就會(huì)越來越少,全部變成空白文檔下的子元素
                    f.appendChild(Node.firstChild)
                }
                return f
            }
            // 編譯模板
            compile(fragmentNode) {
                // 獲取模板的子節(jié)點(diǎn)
                const childNodes = fragmentNode.childNodes;
                // 遍歷所有節(jié)點(diǎn)
                [...childNodes].forEach( child => {
                    // 判斷子節(jié)點(diǎn)中是否還有節(jié)點(diǎn) 遞歸調(diào)用
                    if (child.childNodes && child.childNodes.length) {
                        this.compile(child);
                    }
                    // 如果是元素節(jié)點(diǎn)類型
                    else if(this.isElementNode(child)) {
                        // 使用元素編譯
                        this.elementCompile(child)
                    } 
                    // 如果是文本節(jié)點(diǎn)類型
                    else {
                        // 使用文本編譯
                        this.textCompile(child)
                    }
                })
            }
            // 元素編譯函數(shù)
            elementCompile(node) {
                // 獲得目標(biāo)的節(jié)點(diǎn)的屬性
                const attributes = node.attributes;
                [...attributes].forEach(item => {
                    // 解構(gòu)出參數(shù)的name和value, name相當(dāng)于v指令如v-model等,value則是對(duì)應(yīng)的值 name, age等
                    const {name, value} = item
                    if(!this.isMyVueAttributes(name)) return
                    const attributesName = name.split('-')[1]
                    compileFunc[attributesName](node, value, this.vm)
                })
            }
            // 文本編譯函數(shù)
            textCompile(node) {
                // 獲取節(jié)點(diǎn)文字內(nèi)容
                const content = node.textContent;
                // 匹配插值語法,如果有{{}}語法則為其設(shè)置watcher
                if(/{{.+?}}/.test(content)) {
                    compileFunc['text'](node, content, this.vm)
                }
            }
            // 判斷是否node節(jié)點(diǎn)
            isElementNode(Node) {
               return Node.nodeType == 1
            }
            // 判斷是否v-開頭的屬性
            isMyVueAttributes(name) {
                return name.startsWith('v-')
            }
        }
        const compileFunc = {
            text(node, content, vm) {
                const textMatch = /(?<={{)(.+?)(?=})/g;
                let propList = content.match(textMatch)
                propList = [...new Set(propList)]
                for (let index = 0; index < propList.length; index++) {
                    let func = value => {
                        this.updater.textUpdater(node, value, propList[index], content)
                    }
                    new Watcher(vm, this.propFunc(propList[index], vm), func)
                }
            },
            model(node, prop, vm) {
                let func = value => {
                    this.updater.modelUpdater(node, value)
                }
                new Watcher(vm, this.propFunc(prop, vm), func)
                node.oninput =(item)=>{
                    // 這里還沒辦法處理字符串轉(zhuǎn)對(duì)應(yīng)對(duì)象
                    vm.data[prop] = item.srcElement.value
                }
            },
            // 處理取對(duì)象下的值的情況,因?yàn)閧{}}里面會(huì)有{{a.b.c}}這種情況需要通過一個(gè)函數(shù)的形式在watcher中進(jìn)行g(shù)et操作
            propFunc(value, vm) {
                return function() {value.split(".").reduce((p, c) => {
                    return p[c];
                }, vm.data)}
            },
            // 實(shí)際操作dom方法
            updater: {
                textUpdater(node, value, prop, content) {
                    node.textContent = content.replaceAll(`{{${prop}}}`, value);
                },
                modelUpdater(node, value) {
                    node.value = value;
                },
            }
        }
        // 雙向綁定構(gòu)造函數(shù)
        class MyVue {
            constructor(options, prop) {
                this.options = options // 需要雙向綁定的對(duì)象,包括數(shù)據(jù)對(duì)象和對(duì)應(yīng)的節(jié)點(diǎn)
                this.data = options.data // 需要監(jiān)聽的數(shù)據(jù)對(duì)象
                this.el = options.el // 需要更新視圖節(jié)點(diǎn)
                this.prop = prop // 對(duì)應(yīng)的key值
                this.init()
            }
            init() {
                observe(this.data) // 監(jiān)聽data
                new Compile(this.el, this.options)
            }
        }
        const vm = new MyVue({
            el: "app",
            data: { name: 'xiaoshan',child: {name:123} }
        });
        // // 創(chuàng)建一個(gè)對(duì)象,對(duì)這個(gè)對(duì)象數(shù)據(jù)監(jiān)聽
        // var user = { name: 'xiaoshan', age: 17, son: {name:'test'} }
        // // 數(shù)據(jù)監(jiān)聽函數(shù)
        // observe(user)
        // user.name = user.name + '小山'
        // user.son.name = user.son.name  + '小小山'
    </script>
</body>
</html>

代碼缺陷

上面的代碼沒有做頁面初始化顯示,只是一個(gè)demo

只做了文本節(jié)點(diǎn)和元素節(jié)點(diǎn)v-model的解析

v-model的時(shí)候還不支持a.b.c因?yàn)樵趏ninput事件中set還有點(diǎn)問題

對(duì)于文本節(jié)點(diǎn)時(shí)應(yīng)該以{{}}為一個(gè)單獨(dú)節(jié)點(diǎn)。不然替換的時(shí)候太麻煩了

到此這篇關(guān)于詳解Vue中雙向綁定原理及簡單實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Vue雙向綁定內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Vue的過濾器你真了解嗎

    Vue的過濾器你真了解嗎

    這篇文章主要為大家詳細(xì)介紹了Vue的過濾器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助
    2022-02-02
  • Vue.js 表單控件操作小結(jié)

    Vue.js 表單控件操作小結(jié)

    這篇文章給大家介紹了Vue.js 表單控件操作的相關(guān)知識(shí),本文通過實(shí)例演示了input和textarea元素中使用v-model的方法,本文給大家介紹的非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友參考下吧
    2018-03-03
  • Nuxt頁面級(jí)緩存的實(shí)現(xiàn)

    Nuxt頁面級(jí)緩存的實(shí)現(xiàn)

    這篇文章主要介紹了Nuxt頁面級(jí)緩存的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • vue?axios中的get請(qǐng)求方式

    vue?axios中的get請(qǐng)求方式

    這篇文章主要介紹了vue?axios中的get請(qǐng)求方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-07-07
  • vue和小程序項(xiàng)目中使用iconfont的方法

    vue和小程序項(xiàng)目中使用iconfont的方法

    這篇文章主要介紹了vue中和小程序中使用iconfont的方法,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-05-05
  • Vue中使用Scss實(shí)現(xiàn)配置、切換主題方式

    Vue中使用Scss實(shí)現(xiàn)配置、切換主題方式

    這篇文章主要介紹了Vue中使用Scss實(shí)現(xiàn)配置、切換主題方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-03-03
  • 在 Vue 中編寫 SVG 圖標(biāo)組件的方法

    在 Vue 中編寫 SVG 圖標(biāo)組件的方法

    這篇文章主要介紹了在 Vue 中編寫 SVG 圖標(biāo)組件的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-02-02
  • vue-quill-editor的使用及個(gè)性化定制操作

    vue-quill-editor的使用及個(gè)性化定制操作

    這篇文章主要介紹了vue-quill-editor的使用及個(gè)性化定制操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2020-08-08
  • vue 根據(jù)選擇的月份動(dòng)態(tài)展示日期對(duì)應(yīng)的星期幾

    vue 根據(jù)選擇的月份動(dòng)態(tài)展示日期對(duì)應(yīng)的星期幾

    這篇文章主要介紹了vue 如何根據(jù)選擇的月份動(dòng)態(tài)展示日期對(duì)應(yīng)的星期幾,幫助大家更好的利用vue框架處理日期需求,感興趣的朋友可以了解下
    2021-02-02
  • 詳解vue配置后臺(tái)接口方式

    詳解vue配置后臺(tái)接口方式

    這篇文章主要介紹了vue配置后臺(tái)接口方式,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-03-03

最新評(píng)論