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

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

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

監(jiān)聽器

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

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

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

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

訂閱器

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

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

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

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

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

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

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

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

然后我們會顯示的單獨創(chuàng)建一個訂閱器實例

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

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

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

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

初步效果:

編譯器

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

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

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

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

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

所以我們需要有一個編譯器,去自動讀取頁面的目標節(jié)點下的所有節(jié)點,對每一個節(jié)點進行分析,比如a節(jié)點是個什么類型的節(jié)點。

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

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

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

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

同樣的最常見的v-model,我們用在對應的元素節(jié)點上時,我們在為這個節(jié)點創(chuàng)建watcher的時候,還得為這個節(jié)點設置oninput事件,因為當其內(nèi)容改變的時候,我們要用這個回調(diào)同步的修改我們目標data下的值。

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

(1)Compile類

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

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

(2)node2Fragment函數(shù)

這個是讀取目標節(jié)點的函數(shù),會創(chuàng)建一個新的空白文檔碎片,把原來目標節(jié)點下的子節(jié)點一個一個放進去

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

(3)compile函數(shù)

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

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

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

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

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

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

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

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

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

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

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

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)
        }
    },
    // 處理取對象下的值的情況,因為{{}}里面會有{{a.b.c}}這種情況需要通過一個函數(shù)的形式在watcher中進行get操作
    propFunc(value, vm) {
        return function() {value.split(".").reduce((p, c) => {
            return p[c];
        }, vm.data)}
    },
    // 實際操作dom方法
    updater: {
        textUpdater(node, value, prop, content) {
            node.textContent = content.replaceAll(`{{${prop}}}`, value);
        },
    }
}

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

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

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

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

// 元素編譯函數(shù)
elementCompile(node) {
    // 獲得目標的節(jié)點的屬性
    const attributes = node.attributes;
    [...attributes].forEach(item => {
        // 解構出參數(shù)的name和value, name相當于v指令如v-model等,value則是對應的值 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ù)干了啥,首先定義一個回調(diào)函數(shù),函數(shù)的核心是modelUpdater,類似于textUpdater,做的事情是data的key改變時同步更新頁面,即數(shù)據(jù)更新視圖,并將這個作為回調(diào)傳入對應的watcher中,這時data的key就可以通過調(diào)用依賴的方式更新視圖了。

同時我們也看到我們給這個節(jié)點加了oninput事件,因為這個是雙向綁定,視圖一樣要改變數(shù)據(jù),所以當輸入了內(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)對應對象
        vm.data[prop] = item.srcElement.value
    }
},
...
modelUpdater(node, value) {
    node.value = value;
},

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

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

又因為我們在寫編譯器的時候其實已經(jīng)將watcher整合進compile了,所以之后我們其實只需要創(chuàng)建一個complie實例即可監(jiān)聽下面所有的節(jié)點。

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

總結(jié)

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

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

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

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

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

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

緩存我們傳入的目標節(jié)點和data,在init方法中調(diào)用監(jiān)聽器,并且實例化編譯器對每一個節(jié)點掛載對應的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雙向綁定原理實現(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)聽對象的所有get和set
        function observe(data) {
            // 如果data不存在或data不是object
            if (!data || typeof data !== 'object') {
                return;
            }
            // 遍歷data,獲得當前的key
            for (const key in data) {
                // 監(jiān)聽data下的當前key的屬性變動
                defineReactive(data, key, data[key]); // 一次只處理data和一個key的監(jiān)聽關系
            }
        }
        // 通過Object.defineProperty去監(jiān)聽data下的key
        function defineReactive(data, key, value) {
            // 處理key對應的value是一個對象的情況
            observe(value)
            // 創(chuàng)建Dep() 實例 用于存儲key的依賴,以及觸發(fā)key的依賴
            let dep = new Dep()
            Object.defineProperty(data, key, {
                // 返回value值
                get: function () {
                    // get的時候進行依賴收集
                    if(Dep.target) { 
                        // Dep.target默認為null,不會收集依賴,但是創(chuàng)建watcher實例的時候
                        // 會為Dep.target賦值并指向當前的watcher實例,同時會為watcher掛載上update函數(shù)
                        // 然后get的時候會出現(xiàn)觸發(fā)依賴收集,因為這時候Dep.target指向當前的watcher實例,就可以將這個watcher實例收集起來
                        dep.addSub(Dep.target)
                    }
                    return value
                },
                set: function (newValue) {
                    // get的時候觸發(fā)依賴
                    val = newValue;
                    // 依賴觸發(fā),遍歷依賴里面的watcher并調(diào)用update函數(shù)
                    dep.notify(newValue)
                }
            }
        )}
        // 創(chuàng)建Dep類,用來存儲依賴
        class Dep{
            constructor() {
                this.subs = [] // 定義一個subs數(shù)組用來存放依賴
            }
            addSub(sub) {
                // get的時候存儲依賴
                // 注:更新函數(shù)會被掛載到單獨創(chuàng)建的watcher實例,存儲依賴的時候,實際上存儲的是創(chuàng)建的watcher實例
                this.subs.push(sub)
            }
            notify(newValue) {
                // set的時候觸發(fā)依賴
                // 注: 遍歷依賴,開始執(zhí)行里面watcher實例的更新函數(shù)
                this.subs.forEach(item => {
                    item.update(newValue)
                })
            }
            target = null
        }
        // 訂閱器
        class Watcher{
            constructor(vm, prop, callback) {
                this.vm = vm  // 監(jiān)聽的節(jié)點和監(jiān)聽的數(shù)據(jù)對象
                this.prop = prop // 監(jiān)聽的數(shù)據(jù)對象的某個參數(shù)key,如 name、age 等
                this.callback = callback // 存儲回調(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的時候開始依賴收集因為Dep.target = this,所以target非空,Watcher會被插入到data的key下的依賴集合中
                const value = this.prop() // 由于要處理取對象下面的對象的值的情況,這里需要返回一個函數(shù)在get()的時候再顯式的收集依賴
                Dep.target = null // 關閉依賴收集,防止每次都收集依賴
                return value // 獲取當前的值
            }
            // 注意調(diào)用update的時候是在監(jiān)聽器的notify里面進行的,說明已經(jīng)set操作完了,值已經(jīng)變了,但是watcher的value值在get的時候先緩存了上一次value值
            update(newValue) { // 更新函數(shù)
                const value = newValue // 獲取當前的值
                const oldValue = this.value // 獲取之前緩存的值
                if(value != oldValue) { // 如果當前的值和之前緩存的值不同則觸發(fā)更新函數(shù),并重置value
                    this.value = value
                    this.callback(value); // 觸發(fā)更新回調(diào)函數(shù)
                }
            }
        }
        // 編譯器
        class Compile {
            // 對數(shù)據(jù)進行解析
            constructor(el, vm) { // el當前目標元素根節(jié)點,vm雙向綁定構造函數(shù)實例
                this.el = this.isElementNode(el) ? el : document.getElementById(el) // 獲取目標節(jié)點
                this.vm = vm // 暫存構造函數(shù)實例
                // 獲取文檔碎片節(jié)點,暫存在內(nèi)容中
                const fragmentNode = this.node2Fragment(this.el)
                // 模板編譯,主要是為每一個節(jié)點去建立映射關系為其設置對應的watcher,把上面創(chuàng)建的節(jié)點傳入解析器去編譯分析
                this.compile(fragmentNode)
                // 將處理后端
                this.el.appendChild(fragmentNode)
            }
            // 解析元素節(jié)點
            node2Fragment(Node) {
                // 創(chuàng)建空白文檔
                const f = document.createDocumentFragment()
                // 每次先看看Node.firstChild是否存在
                while((Node.firstChild)) {
                    // 把子元素加到空白文檔中,加過去之后意味著目標節(jié)點第一個節(jié)點遷移到空白文檔中,那么目標節(jié)點的元素就會越來越少,全部變成空白文檔下的子元素
                    f.appendChild(Node.firstChild)
                }
                return f
            }
            // 編譯模板
            compile(fragmentNode) {
                // 獲取模板的子節(jié)點
                const childNodes = fragmentNode.childNodes;
                // 遍歷所有節(jié)點
                [...childNodes].forEach( child => {
                    // 判斷子節(jié)點中是否還有節(jié)點 遞歸調(diào)用
                    if (child.childNodes && child.childNodes.length) {
                        this.compile(child);
                    }
                    // 如果是元素節(jié)點類型
                    else if(this.isElementNode(child)) {
                        // 使用元素編譯
                        this.elementCompile(child)
                    } 
                    // 如果是文本節(jié)點類型
                    else {
                        // 使用文本編譯
                        this.textCompile(child)
                    }
                })
            }
            // 元素編譯函數(shù)
            elementCompile(node) {
                // 獲得目標的節(jié)點的屬性
                const attributes = node.attributes;
                [...attributes].forEach(item => {
                    // 解構出參數(shù)的name和value, name相當于v指令如v-model等,value則是對應的值 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é)點文字內(nèi)容
                const content = node.textContent;
                // 匹配插值語法,如果有{{}}語法則為其設置watcher
                if(/{{.+?}}/.test(content)) {
                    compileFunc['text'](node, content, this.vm)
                }
            }
            // 判斷是否node節(jié)點
            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)對應對象
                    vm.data[prop] = item.srcElement.value
                }
            },
            // 處理取對象下的值的情況,因為{{}}里面會有{{a.b.c}}這種情況需要通過一個函數(shù)的形式在watcher中進行get操作
            propFunc(value, vm) {
                return function() {value.split(".").reduce((p, c) => {
                    return p[c];
                }, vm.data)}
            },
            // 實際操作dom方法
            updater: {
                textUpdater(node, value, prop, content) {
                    node.textContent = content.replaceAll(`{{${prop}}}`, value);
                },
                modelUpdater(node, value) {
                    node.value = value;
                },
            }
        }
        // 雙向綁定構造函數(shù)
        class MyVue {
            constructor(options, prop) {
                this.options = options // 需要雙向綁定的對象,包括數(shù)據(jù)對象和對應的節(jié)點
                this.data = options.data // 需要監(jiān)聽的數(shù)據(jù)對象
                this.el = options.el // 需要更新視圖節(jié)點
                this.prop = prop // 對應的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)建一個對象,對這個對象數(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>

代碼缺陷

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

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

v-model的時候還不支持a.b.c因為在oninput事件中set還有點問題

對于文本節(jié)點時應該以{{}}為一個單獨節(jié)點。不然替換的時候太麻煩了

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

相關文章

  • Vue的過濾器你真了解嗎

    Vue的過濾器你真了解嗎

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

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

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

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

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

    vue?axios中的get請求方式

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

    vue和小程序項目中使用iconfont的方法

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

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

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

    在 Vue 中編寫 SVG 圖標組件的方法

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

    vue-quill-editor的使用及個性化定制操作

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

    vue 根據(jù)選擇的月份動態(tài)展示日期對應的星期幾

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

    詳解vue配置后臺接口方式

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

最新評論