1. 程式人生 > 其它 >Vue響應式系統原理並實現一個雙向繫結

Vue響應式系統原理並實現一個雙向繫結

這一章就著重講兩個點:

  • 響應式系統如何收集依賴
  • 響應式系統如何更新檢視 我們知道通過Object.defineProperty做了資料劫持,當資料改變的時候,get方法收集依賴,進而set方法呼叫dep.notify方法去通知Watcher呼叫本身update方法去更新檢視。那麼我們拋開其他問題,就討論getnotifyupdate等方法,直接上程式碼:

get( )

  get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    }

我們知道Dep.target在建立Watcher的時候是null,並且它只是起到一個標記的作用,當我們建立Watcher例項的時候,我們的Dep.target就會被賦值到Watcher例項,進而放入target棧中,我們這裡呼叫的是pushTarget函式:

// 將watcher例項賦值給Dep.target,用於依賴收集。同時將該例項存入target棧中
export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

那我們繼續執行到if (Dep.target)語句的時候就會呼叫Dep.depend函式:

 // 將自身加入到全域性的watcher中
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

那下面的childOb是啥東西呢?

  let childOb = !shallow && observe(val)

我們通過這個變數判斷當前屬性下面是否還有ob屬性,如果有的話繼續呼叫Dep.depend函式,沒有的話則不處理。
我們還需要處理當前傳入的value型別,是陣列屬性的話則會呼叫dependArray

收集陣列依賴

// 收集陣列依賴
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
  e = value[i]
  e && e.__ob__ && e.__ob__.dep.depend()
  if (Array.isArray(e)) {
    dependArray(e)
  }
}
}

那麼收集依賴部分到這裡就完了現在進行下一步觸發更新

set( )

   set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 判斷NaN的情況
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

我們看到了下面的 set函式觸發了dep.notify()方法

notify( )

  // 通知所有訂閱者
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

notify裡面我們就做了一件事情,遍歷subs數組裡面的所有Watcher,逐一呼叫update方法,也就是我們說的通知所有的訂閱者Watcher呼叫自身update方法 update( )

  update () {
    if (this.lazy) {
      // 計算屬性會進來這段程式碼塊
      // 這裡將dirty賦值為true
      // 也不會馬上去讀取值
      // 當render-watcher的update被觸發時
      // 重新渲染頁面,計算屬性會重新讀值
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

那麼update方法實現了什麼呢?lazydirtysync又是啥?

   if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
     // 這裡將lazy的值賦值給了dirty
    // 就是說例項化的時候dirty = lazy = true
    this.dirty = this.lazy // for lazy watchers

那是控制計算屬性的,當render—watcher的方法update被呼叫的時候,this.dirty會變為true會重新計算computed值,渲染檢視,我們這裡不敘述。
那麼我們直接看queueWatcher()函式:

export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
 has[id] = true
 if (!flushing) {
   queue.push(watcher)
 } else {
   // if already flushing, splice the watcher based on its id
   // if already past its id, it will be run next immediately.
   let i = queue.length - 1
   while (i > index && queue[i].id > watcher.id) {
     i--
   }
   queue.splice(i + 1, 0, watcher)
 }
 // queue the flush
 if (!waiting) {
   waiting = true
   nextTick(flushSchedulerQueue)
 }
}
}

我們可以看到一個更新佇列,更新佇列指向:

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

我們的callback呼叫updated鉤子
講到這裡就有點超綱了,咱們初始化渲染會呼叫一個initRender函式建立dom,還有上面所述的nextTick,後期都會講,那麼瞭解了更新機制,下一章我們就來實現一個讓面試官都驚呆了雙向繫結

我們對Vue響應式系統有一定的瞭解,並且知道它是如何實現資料更新檢視檢視改變資料的,那麼有這樣的基礎,我們來手寫一個MVVM,以便面試的時候,吊打面試官(此為笑談,不足論,嘿嘿)。
那麼先丟擲一張在座的各位再也熟悉不過的圖:

1、當我們new MVVM之後有兩步操作,Observer,Compile,我們知道Observer是做資料劫持,Compile是解析指令,那麼問題來了:

  • Observer為什麼要做資料劫持?
  • Compile為什麼要做解析指令?
    帶著這兩個問題,我們回顧一下往期內容:
  • 什麼是資料響應式
  • 資料響應式原理是什麼?
  • 資料響應式是如何實現的?

參考 Vue面試題詳細解答

資料響應式就是資料雙向繫結,就是把Model繫結到View,當我們用JavaScript程式碼更新Model時,View就會自動更新;如果使用者更新了View,那麼Model資料也被自動更新了,這種情況就是雙向繫結。

資料響應式原理

  • Vue實現資料響應式原理就是通過Object.defineProperty()這個方法重新定義了物件獲取屬性值get設定屬性值set的操作來實現的
  • Vue3.0中是通過ECMAScript6中的proxy物件代理來實現的。
    那麼本章節就是來實現資料響應式的。

那麼回答前面的兩個問題,為什麼要劫持資料?為什麼要解析指令

  • 只有劫持到資料,才能對資料做到監聽,以便於資料更改能夠及時做到更新檢視。
  • Vue中自定義了N多指令,只有解析它,我們JavaScript才能認識它,並執行它。
    諸如此類問題我們不再複述,下面開始實現資料響應式。

寫一個demo之前,我們應當整理好思路:

1. 首先實現整體的一個架構(包括MVVM類或者VUE類、Watcher類),   /這裡用到一個訂閱釋出者設計模式。
2. 然後實現MVVM中的由M到V,把模型裡面的資料繫結到檢視。
3. 最後實現V-M, 當文字框輸入文字的時候,由文字事件觸發更新模型中的資料
4. 同時也更新相對應的檢視。
//html程式碼
<div id="app">
      <h1>MVVM雙向繫結</h1>
      <div>
        <div v-text="myText"></div>
        <div v-text="myBox"></div>
        <input type="text" v-model="myText" />
        <input type="text" v-model="myBox" />
      </div>
</div>

我們建立了兩個divinput實現input框資料關聯,說白了也就是相同的資料來源,那我們的資料來源在哪呢?

//資料來源data
const app = new Vue({
        el: "#app",
        data: {
          myText: "大吉大利!今晚吃雞!",
          myBox: "我是一個盒子!",
        },
});

可見我們需要一個Vue類,也就是一個釋出者,那麼直接上程式碼:

//Vue類(釋出者)
class Vue{

}

釋出者有了,我們還需要有訂閱者:

//Watcher類(訂閱者)
class Watcher{

}

可見兩者都有了,那麼我們該怎麼實現呢?

  • 獲取data資料
  • 獲取元素物件
  • 構造一個存放訂閱者的物件
 class Vue {
        constructor(optios) {
          this.$data = optios.data; //獲取資料
          this.$el = document.querySelector(optios.el); //獲取元素物件
          this._directive = {}; // 存放訂閱者
        }
 }       

那麼我們說了,我們需要劫持資料解析指令,那麼我們得構造兩個方法。

   class Vue {
       constructor(optios) {
         this.$data = optios.data; //獲取資料
         this.$el = document.querySelector(optios.el); //獲取元素物件
         this._directive = {}; // 存放訂閱者
         this.Observer(this.$data);
         this.Compile(this.$el);
       }
       //劫持資料
       Observer(data) {
           Object.defineProperty(this.$data, key, {
             get: function(){},
             set: function(){}
             },
           });
       }
       //解析指令   //檢視 --- >物件 -- >指令
       Compile(el) {

       }
   }

一個是劫持資料,一個是解析元素指令,劫持到的屬性要根據屬性分配容器,噹噹前容器不存在該屬性的時候,我們便需要把他新增到訂閱器物件裡面,等待通知更新。

  for (let key in data) {
          this._directive[key] = [];
          let val =data[key];
          let watch = this._directive[key];
  }

那麼解析指令,首先必須要遞迴當前節點,是否還有子節點,是否有v-text指令,v-model指令。

       let nodes = el.children;
          for (let i = 0; i < nodes.length; i++) {
            let node = nodes[i];
            //遞迴 查詢所有當前物件子類是否再含有子類
            if (node.children.length) {
              this.Compile(nodes[i]);
            }
            //判斷是否含有V-text指令
            if (node.hasAttribute("v-text")) {
              let attrVal = node.getAttribute("v-text");

              this._directive[attrVal].push(
                new Watcher(node, this, "innerHTML", attrVal)
              );
            }

            //判斷是否含有V-model指令
            if (node.hasAttribute("v-model")) {
              let attrVal = node.getAttribute("v-model");

              this._directive[attrVal].push(
                new Watcher(node, this, "value", attrVal)
              );
              node.addEventListener("input", () => {
                //賦值到模型
                this.$data[attrVal] = node.value;
                // console.log(this.$data);
              });
            }
          }

那麼我們觸發更新時候需要收集依賴,我們直接吧收集到的依賴return出去

 Object.defineProperty(this.$data, key, {
              get: function(){
                    return val;
              }
 }

那麼我們訂閱者長什麼樣呢?我們訂閱者,接收當前元素資訊,MVVM物件,標識,屬性。並且需要構造一個更新方法update

    class Watcher {
        constructor(el, vm, exp, attr) {
          this.el = el;
          this.vm = vm;
          this.exp = exp;
          this.attr = attr;
          this.update();
        }
        //更新檢視
        update() {
          this.el[this.exp] = this.vm.$data[this.attr];
          //div.innerHTML/value = this.Vue.$data["myText/myBox"]
        }
    }

到這裡已經快完成了,那麼我們收集了依賴就要去,通知watcher去更新檢視啊,那麼來了:

    Object.defineProperty(this.$data, key, {
              get: function(){
                    return val;
              },
              set: function(newVal){
                  if(newVal !== val){
                    val = newVal;
                    watch.forEach(element => {
                        element.update();  
                    });
                  }
              },
    });

做到這裡,你就可以實現一個數據響應式了。


我們已經掌握了響應式原理,那我們開始著手Vue的另一個核心概念元件系統