天天看點

解析vue中的資料響應式(雙向綁定)

目錄

資料響應式是啥?

原理分析

具體實作

LVue

Observer(資料劫持)

watcher

Dep

Compile

結語

資料響應式是啥?

先來說說vue架構,它本質上是一個MVVM架構,而MVVM架構的三要素:資料響應式、模闆引擎、渲染

資料響應式:監聽資料變化并在視圖中更新 Object.defifineProperty() Proxy 模版引擎:提供描述視圖的模版文法 插值: {{}} 指令: v-bind , v-on , v-model , v-for , v-if 渲染:如何将模闆轉換為 html 模闆 => vdom => dom   是以簡單來說,資料變更能夠響應在視圖中,就是資料響應式。其中渲染涉及的虛拟dom我還沒有深入研究,目前先就vue中的資料響應式和模闆引擎做一個簡單的解析和實踐。  

原理分析

首先,我們先看一段vue實際使用的代碼,從中分析雙向綁定的實作方式和原理。

<!DOCTYPE html>
<html >
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  <div id="app">
    <p l-text="count"></p>
    <p l-html="desc"></p>
  </div>
  <script src="lCompile.js"></script>
  <script src="lvue.js"></script>
  <script>
    const app = new LVue({
      el:'#app',
      data:{
        count:1,
        desc:'<span style="color:red">這是lvue?</span>'
      }
      }
    })
  </script>
</body>
</html>
           

1. new LVue() 首先執行初始化,對 data 執行響應化處理,這個過程發生在 Observer 中 2. 同時對模闆執行編譯,找到其中動态綁定的資料,從 data 中擷取并初始化視圖,這個過程發生在 Compile中 3. 同時定義一個更新函數和 Watcher ,将來對應資料變化時 Watcher 會調用更新函數 4. 由于 data 的某個 key 在一個視圖中可能出現多次,是以每個 key 都需要一個管家 Dep 來管理多個 Watcher 5. 将來 data 中資料一旦發生變化,會首先找到對應的 Dep ,通知所有 Watcher 執行更新函數   好了我知道你們不想看文字,然後我花了大力氣畫的圖,好好看!肯定能看懂!看不懂找我!  

解析vue中的資料響應式(雙向綁定)

  好了,下面開始代碼部分,代碼部分這裡先直接貼一個連結,大家可以直接去看。 代碼  

具體實作

LVue

相當于入口檔案吧,處理整體邏輯,把資料存起來,模闆拿過來處理。

class LVue{
  constructor(options){
    this.$options = options
    this.$data = options.data
    // 代理方法
    proxy(this,'$data')

    // 建立observe觀察者
    observe(this.$data)
    // 編譯模闆,下面寫
    new Compile(options.el, this)
  }
}
 // 代理方法,目的是可以直接用this通路到$data中的内容
function proxy(vm,str) {
  Object.keys(vm[str]).forEach(val=>{
    Object.defineProperty(vm,val,{
      get(){
        return vm[str][val]
      },
      set(newVal){
        vm[str][val] = newVal
      }
    })
  })
}

 // 就簡單的看一下是不是對象,因為defineReactive是對象的方法,
 // 至于數組則是通過重寫數組操作方法實作資料劫持的,不過vue3中使用了ES6的Proxy
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    // 希望傳入的是obj
    return
  }
  // 建立Observer執行個體,進行資料劫持,下面寫
  new Observer(obj)
}
           

Observer(資料劫持)

我們知道可以利用

Obeject.defineProperty()

來監聽屬性變動,但是你不能簡單的對那個對象監聽一下,萬一對象内部屬性還是個對象呢??是以需要将observe的資料對象進行遞歸周遊,包括子屬性對象的屬性,都加上setter和getter,這樣的話,給這個對象的某個值指派,就會觸發setter,那麼就能監聽到了資料變化。

class Observe {
  constructor(value){
    this.value = value
    this.walk(value)
  }
  // 對傳入的參數進行劫持
  walk(obj){
    // 因為前面已經判斷過是對象了,直接循環執行資料劫持方法就行了
      Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
      })
  }
}

// 對象的響應式
function defineReactive(obj,key,val){
  observe(val)
  const dep = new Dep() // 這裡是消息訂閱器,用來建立資料更新與頁面更新的對應關系。
                        // 所有dep都先不看,下面會具體分析
  Object.defineProperty(obj,key,{
    get:function(){
      Dep.target&&dep.addDep(Dep.target)
      return val
    },
    set:function (newVal) {
    //當給data屬性中設定值的時候, 更改擷取的屬性的值
      if (newVal !== val) {
        observe(newVal)
        val = newVal
        dep.notify() // 改變值時觸發dep内部的循環更新
      }
    }
  })
}
           

監聽到變化之後就是怎麼通知訂閱者Watcher了,有人說,那就直接通知Watcher不就好了?!那下面,說一下依賴收集。

視圖中會用到vue的data中的某個值,這稱為依賴。同一個值,可能會出現很多次,每次出現都需要将它收集出來,用一個Watcher進行維護,這就是依賴收集。 而某個值出現多次,則需要多個Watcher,這時候我們就需要一個Dep來管理,我們在修改資料時由Dep通知Watcher批量更新。

來個簡單版解釋: 代碼中某個值,在很多地方使用,每個使用的地方對應一個更新操作。需要把這些操作放到一個盒子裡,值改變的時候把盒子裡所有更新操作觸發一下。

watcher

好了,大概知道Watcher和Dep是什麼了,那下面給大家先來個實作思路:

  1. 劫持時defifineReactive為每一個key建立一個Dep(就是上面說的那個管理Watcher的東西)執行個體。
  2. 初始化視圖時每一次讀取某個key,例如name1,建立一個watcherName1。
  3. 此時就會觸發key(name1)的getter方法,是以就可以在getter方法中将watcherName1添加到name1對應的Dep中。
  4. 當key(name1)更新時,setter觸發,此時便可通過對應Dep通知其管理所有Watcher更新,這樣Dep中所有的watcher都觸發一次更新,就實作了資料的響應。

這時候看完這些再回去了解前面劫持部分的代碼有關dep的部分是不是就了解了呢。那下面我們來看一下實作:

class Watcher {
  constructor(vm,key,updateFn){
    // vue執行個體
    this.vm = vm
    // 可觸發/依賴 的key
    this.key = key
    // 更新函數
    this.updateFn = updateFn
    
    // 下面兩行要回去對照資料劫持getter部分看一下
    Dep.target = this   // 把Watcher存一下,get中直接dep.addDep(Dep.target)存進去
    this.vm[this.key];  // 這裡的意思就行調用了一下對應的key,這樣就能出發getter方法了
    Dep.target = null
  }

  update(){
    this.updateFn.call(this.vm,this.vm[this.key])
  }
}
           

Dep

Dep就相對簡單多了,本質上就是維護了一組Watcher,有一個更新事假,執行的是循環出發Watcher中的更新方法。代碼如下:

class Dep{
  constructor(){
    this.deps = []
  }
  addDep(dep){
    this.deps.push(dep)
  }
  notify(){
    // deps裡面是一個一個的 watch , 改變值後循環觸發update方法
    this.deps.forEach(dep => dep.update());
  }
}
           

Compile

接下來是編譯部分,說實話這一部分了解起來特别簡單,就是以傳進來的根節點為基礎周遊整個dom,然後找出節點上屬性中“v-”,"{{}}"等等這一類的辨別屬性,挨個建立Watcher來監聽他們就好了。雖然了解簡單,但是代碼卻很多,是以代碼細節就不一行一行的解釋了,大家可以仔細看看代碼,我還是寫了不少注釋的~ 有疑問也歡迎騷擾~

class Compile {
  constructor(el, vm){
    this.$el = document.querySelector(el)
    this.$vm = vm

    if (this.$el){
      this.compile(this.$el)
    }
  }

  // 編譯,vue的文法
   compile(el){
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node=>{
      if (this.isElement(node)){
        // console.log("編譯元素" + node.nodeName)
        this.compileElement(node)
      } else if(this.isInterpolation(node)){
        // console.log("編譯內插補點文本" + node.textContent)
        this.compileText(node)
      }
      if (node.childNodes&&node.childNodes.length>0) {
        this.compile(node)
      }
    })
  }
  isElement(node){
    return node.nodeType === 1
  }
  isInterpolation(node){
    return node.nodeType === 3 &&/\{\{(.*)\}\}/.test(node.textContent)
  }

  // node為元素時編譯方法
  compileElement(node){
    let nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach(attr=>{
      let attrName = attr.name
      let exp = attr.value
      console.log(exp)
      // 屬性名以 l- 開頭時處理
      if (attrName.indexOf("l-")===0){
        let dir = attrName.substring(2)
        // 拿出後面的html、text 等,html、text會被在内部定義方法
        this[dir]&&this[dir](node,exp)
      }
      // 時間處理
      if(this.isEvent(attrName)){
        // @click = onClick
        const dir = attrName.substring(1) // click
        
        // 事件監聽
        this.eventHandler(node,exp,dir)
      }
    })
  }
  isEvent(dir){
    return dir.indexOf('@') == 0
  }
  eventHandler(node,exp,dir){
    const fn = this.$vm.$options.methods &&
    this.$vm.$options.methods[exp]
    node.addEventListener(dir,fn.bind(this.$vm))
  }
  // node為文本時處理
  compileText(node){
    this.update(node,RegExp.$1,'text')
  }

  // 初始化時執行 更新方法,并傳入text
  text(node,exp){
    this.update(node,exp,'text')
  }

  //  初始化時執行 更新方法 目的是 update中建立了Watcher,可以傳入改變方法,在資料監聽時就可執行了
  html(node,exp){
    this.update(node,exp,'html')
  }

  model(node,exp){
    // update方法隻完成指派和更新
      this.update(node,exp,'model')
    // 是以還需要事件監聽
      node.addEventListener('input',e=>{
        // 将新的值指派給資料即可
        this.$vm[exp]=e.target.value
      })
  }

  // 建立更新函數,和watcher綁定
  update(node,exp,dir){
    const fn = this[dir+'Updater']
    fn && fn(node,this.$vm[exp])
    new Watcher(this.$vm,exp,function (val) {
      fn && fn(node,val)
    })
  }

  // v-text 綁定text方法
  textUpdater(node,val){
    node.textContent = val
  }

  // v-html 綁定html方法
  htmlUpdater(node,val){
    node.innerHTML = val
  }

  modelUpdater(node,val){
    // 多用在表單元素,暫時隻考慮表單元素指派
    node.value = val
  }
}
           

結語

 這篇文章主要是介紹了一下Vue的資料響應式和他的原理以及實作。有疑問歡迎提問,當然發現問題也歡迎随之指正。

另從開始寫到結束寫了一個月,當然不是一直在寫,主要是最近還準備面試了,寫文章的時間被壓縮的太少。前段時間還覺得我都能看源碼了我可以了我。然後最近被阿裡、位元組跳動啥的面試官一頓社會主義毒打。唉,不說了,說多了都是淚,學無止境,繼續好好學習吧。另外,在這裡也提醒自己一下,學啥武功都得紮馬步,基礎一定要!紮!實!