最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

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

監(jiān)聽器

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

(1)遍歷整個(gè)data,對(duì)data下面所有的key進(jìn)行Object.defineProperty來(lái)監(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)系
  }
}
// 通過(guò)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í)例收集起來(lái)
        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類,用來(lái)存儲(chǔ)依賴
class Dep{
    constructor() {
        this.subs = [] // 定義一個(gè)subs數(shù)組用來(lái)存放依賴
    }
    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í)例,即說(shuō)明已經(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)行的,說(shuō)明已經(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)可以完成簡(jiǎn)單的數(shù)據(jù)更新

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

然后調(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ù)初始化到頁(yè)面上
    // 創(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ù)依賴去更新頁(yè)面。

但這是個(gè)單向的流程,我們只能監(jiān)聽數(shù)據(jù)變動(dòng)操作頁(yè)面的元素更改,而且在雙向綁定構(gòu)造函數(shù)中我們顯式的創(chuàng)建了watcher實(shí)例(只為id是app的節(jié)點(diǎn)負(fù)責(zé),且這個(gè)節(jié)點(diǎn)下面如果嵌套其他節(jié)點(diǎn)我們就無(wú)法處理了) ,顯然是不合理的,我們應(yīng)該要基于頁(yè)面元素去分析針對(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ù)初始化到頁(yè)面上
  // 創(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這種情況是無(wú)法使用的。

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

所以我們需要有一個(gè)編譯器,去自動(dòng)讀取頁(yè)面的目標(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)容里面是否包含{{}}語(yǔ)法,如果包含了{(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)下面有沒(méi)有子字節(jié),如果有就開始遞歸調(diào)用一直去解析其子節(jié)點(diǎn)。

如果沒(méi)有子節(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)重新放回頁(yè)面即可。

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è)新的空白文檔碎片,把原來(lái)目標(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)) {
      // 把子元素加到空白文檔中,加過(guò)去之后意味著目標(biāo)節(jié)點(diǎn)第一個(gè)節(jié)點(diǎn)遷移到空白文檔中,那么目標(biāo)節(jié)點(diǎn)的元素就會(huì)越來(lái)越少,全部變成空白文檔下的子元素
      f.appendChild(Node.firstChild)
    }
    return f
  }

(3)compile函數(shù)

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

可以看到這個(gè)函數(shù)讀取了我們傳過(guò)來(lái)的文檔碎片,如果一個(gè)一個(gè)遍歷,判斷你這個(gè)節(jié)點(diǎn)是否是含有子節(jié)點(diǎn)的,如果有子節(jié)點(diǎn)那就繼續(xù)遞歸遍歷,如果沒(méi)有就判斷你是什么類型的節(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é)那些{{}}語(yǔ)法的處理,在這里會(huì)匹配這個(gè)節(jié)點(diǎn)里面有沒(méi)有{{}}語(yǔ)法,如果有就調(diào)用compileFunc下的text函數(shù),給這個(gè)節(jié)點(diǎn)單獨(dú)添加watcher,使得你數(shù)據(jù)更新的時(shí)候可以用watcher里面的回調(diào)去修改頁(yè)面的對(duì)應(yīng)節(jié)點(diǎn)的數(shù)據(jù)。

// 文本編譯函數(shù)
textCompile(node) {
  // 獲取節(jié)點(diǎn)文字內(nèi)容
  const content = node.textContent;
  // 匹配插值語(yǔ)法,如果有{{}}語(yǔ)法則為其設(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)問(wèn)題后面再說(shuō))

找到所有的字符之后就開始循環(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)中通過(guò)閉包的方法我們提前傳入了包括對(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}}這種情況需要通過(guò)一個(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í)同步更新頁(yè)面,即數(shù)據(jù)更新視圖,并將這個(gè)作為回調(diào)傳入對(duì)應(yīng)的watcher中,這時(shí)data的key就可以通過(guò)調(diào)用依賴的方式更新視圖了。

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

model(node, prop, vm) {
    let func = value => {
        this.updater.modelUpdater(node, value)
    }
    new Watcher(vm, this.propFunc(prop, vm), func)
    node.oninput =(item)=>{
        // 這里還沒(méi)辦法處理字符串轉(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)問(wè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ù)初始化到頁(yè)面上
  // 創(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)該要修改頁(yè)面某個(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)行分析,分析完之后一次性加入到頁(yè)面中防止頻繁改動(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)系
            }
        }
        // 通過(guò)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í)例收集起來(lái)
                        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類,用來(lái)存儲(chǔ)依賴
        class Dep{
            constructor() {
                this.subs = [] // 定義一個(gè)subs數(shù)組用來(lái)存放依賴
            }
            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)行的,說(shuō)明已經(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)) {
                    // 把子元素加到空白文檔中,加過(guò)去之后意味著目標(biāo)節(jié)點(diǎn)第一個(gè)節(jié)點(diǎn)遷移到空白文檔中,那么目標(biāo)節(jié)點(diǎn)的元素就會(huì)越來(lái)越少,全部變成空白文檔下的子元素
                    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;
                // 匹配插值語(yǔ)法,如果有{{}}語(yǔ)法則為其設(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)=>{
                    // 這里還沒(méi)辦法處理字符串轉(zhuǎn)對(duì)應(yīng)對(duì)象
                    vm.data[prop] = item.srcElement.value
                }
            },
            // 處理取對(duì)象下的值的情況,因?yàn)閧{}}里面會(huì)有{{a.b.c}}這種情況需要通過(guò)一個(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>

代碼缺陷

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

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

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

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

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

相關(guān)文章

  • vue實(shí)現(xiàn)小球滑動(dòng)交叉效果

    vue實(shí)現(xiàn)小球滑動(dòng)交叉效果

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)小球滑動(dòng)交叉,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • vue3組合式api實(shí)現(xiàn)v-lazy圖片懶加載的方法實(shí)例

    vue3組合式api實(shí)現(xiàn)v-lazy圖片懶加載的方法實(shí)例

    vue作為前端主流的3大框架之一,目前在國(guó)內(nèi)有著非常廣泛的應(yīng)用,下面這篇文章主要給大家介紹了關(guān)于vue3組合式api實(shí)現(xiàn)v-lazy圖片懶加載的相關(guān)資料,需要的朋友可以參考下
    2022-09-09
  • vue中@click綁定事件點(diǎn)擊不生效的原因及解決方案

    vue中@click綁定事件點(diǎn)擊不生效的原因及解決方案

    根據(jù)Vue2.0官方文檔關(guān)于父子組件通訊的原則,父組件通過(guò)prop傳遞數(shù)據(jù)給子組件,子組件觸發(fā)事件給父組件,這篇文章主要介紹了vue中@click綁定事件點(diǎn)擊不生效的解決方案,需要的朋友可以參考下
    2022-12-12
  • 基于Vue實(shí)現(xiàn)HTML轉(zhuǎn)PDF并導(dǎo)出

    基于Vue實(shí)現(xiàn)HTML轉(zhuǎn)PDF并導(dǎo)出

    這篇文章主要為大家介紹了三種方法,可以實(shí)現(xiàn)將HTML頁(yè)面轉(zhuǎn)為PDF并實(shí)現(xiàn)下載。文中的示例代碼講解詳細(xì),感興趣的小伙伴可以學(xué)習(xí)一下
    2022-04-04
  • Hexo已經(jīng)看膩了,來(lái)手把手教你使用VuePress搭建個(gè)人博客

    Hexo已經(jīng)看膩了,來(lái)手把手教你使用VuePress搭建個(gè)人博客

    vuepress是尤大大4月12日發(fā)布的一個(gè)全新的基于vue的靜態(tài)網(wǎng)站生成器,實(shí)際上就是一個(gè)vue的spa應(yīng)用,內(nèi)置webpack,可以用來(lái)寫文檔。這篇文章給大家介紹了VuePress搭建個(gè)人博客的過(guò)程,感興趣的朋友一起看看吧
    2018-04-04
  • vue下使用fullcalendar案例講解

    vue下使用fullcalendar案例講解

    這篇文章主要介紹了vue下使用fullcalendar及簡(jiǎn)單案例,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧
    2024-01-01
  • Vue和React中diff算法的區(qū)別及說(shuō)明

    Vue和React中diff算法的區(qū)別及說(shuō)明

    React和Vue都使用虛擬DOM和diff算法來(lái)更新DOM,但它們?cè)趯?shí)現(xiàn)上有所不同,React采用基于索引的比較,Vue采用雙端比較算法,React在比較時(shí)不復(fù)用不同類型的節(jié)點(diǎn),而Vue會(huì)優(yōu)先復(fù)用兩端相同的節(jié)點(diǎn),React對(duì)key的依賴較高,而Vue在沒(méi)有key時(shí)也能通過(guò)雙端比較優(yōu)化
    2025-03-03
  • element?ui?日期選擇器el-date-picker如何修改指定日期背景(點(diǎn)擊指定背景色日期變深色)

    element?ui?日期選擇器el-date-picker如何修改指定日期背景(點(diǎn)擊指定背景色日期變深色)

    這篇文章主要介紹了element?ui?日期選擇器el-date-picker?修改指定日期背景,點(diǎn)擊指定背景色日期變深色,本文通過(guò)實(shí)例效果展示給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧
    2024-07-07
  • 利用vue+elementUI實(shí)現(xiàn)部分引入組件的方法詳解

    利用vue+elementUI實(shí)現(xiàn)部分引入組件的方法詳解

    這篇文章主要給大家介紹了關(guān)于利用vue+elementUI實(shí)現(xiàn)部分引入組件的相關(guān)資料,以及介紹了vue引入elementUI報(bào)錯(cuò)的解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-11-11
  • vue項(xiàng)目登錄成功后退出時(shí)清空sessionId和userId的問(wèn)題

    vue項(xiàng)目登錄成功后退出時(shí)清空sessionId和userId的問(wèn)題

    這篇文章主要介紹了vue項(xiàng)目登錄成功后退出時(shí)清空sessionId和userId的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12

最新評(píng)論

凤庆县| 宽城| 涿鹿县| 古交市| 炎陵县| 出国| 乌鲁木齐市| 繁昌县| 长葛市| 嘉鱼县| 孝昌县| 东辽县| 彭阳县| 合作市| 剑川县| 东山县| 泰来县| 酉阳| 炎陵县| 长海县| 天水市| 邻水| 石家庄市| 施秉县| 当涂县| 理塘县| 额济纳旗| 陆良县| 沾化县| 文水县| 新兴县| 固阳县| 夹江县| 乡宁县| 漳州市| 府谷县| 永康市| 黄龙县| 海林市| 马尔康县| 女性|