首頁 >web前端 >Vue.js >一文帶你深入解析vue雙向綁定原理(徹底搞懂它)

一文帶你深入解析vue雙向綁定原理(徹底搞懂它)

青灯夜游
青灯夜游轉載
2022-02-18 19:47:514098瀏覽

本篇自訂一個vue,逐步實現資料的雙向綁定,給大家透過實例來一步步搞懂vue雙向綁定原理,希望對大家有幫助!

一文帶你深入解析vue雙向綁定原理(徹底搞懂它)

自訂vue類別

  • #vue最少需要兩個參數:模板和data。 【相關推薦:vue.js影片教學

  • 建立Compiler對象,將資料渲染到模板後,掛載到指定跟節點中。

class MyVue {
  // 1,接收两个参数:模板(根节点),和数据对象
  constructor(options) {
    // 保存模板,和数据对象
    if (this.isElement(options.el)) {
      this.$el = options.el;
    } else {
      this.$el = document.querySelector(options.el);
    }
    this.$data = options.data;
    // 2.根据模板和数据对象,渲染到根节点
    if (this.$el) {
      // 监听data所有属性的get/set
      new Observer(this.$data);
      new Compiler(this)
    }
  }
  // 判断是否是一个dom元素
  isElement(node) {
    return node.nodeType === 1;
  }
}

實現資料首次渲染到頁面

#Compiler

    • 1,node2fragment函數將模板元素提取到記憶體中,方便將資料渲染到模板後,再一次掛載到頁面中
  • 2,模板提取到記憶體後,使用buildTemplate函數遍歷該模板元素

元素節點

##使用buildElement函數檢查元素上以v-開頭的屬性

##文本節點

用buildText函數檢查文字中有無{{}}內容

#3,建立CompilerUtil類,用於處理vue指令和{{}},完成資料的渲染4,到此就完成了首次資料渲染,接下來需要實作資料變更時,自動更新視圖。

class Compiler {
  constructor(vm) {
    this.vm = vm;
    // 1.将网页上的元素放到内存中
    let fragment = this.node2fragment(this.vm.$el);
    // 2.利用指定的数据编译内存中的元素
    this.buildTemplate(fragment);
    // 3.将编译好的内容重新渲染会网页上
    this.vm.$el.appendChild(fragment);
  }
  node2fragment(app) {
    // 1.创建一个空的文档碎片对象
    let fragment = document.createDocumentFragment();
    // 2.编译循环取到每一个元素
    let node = app.firstChild;
    while (node) {
      // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
      fragment.appendChild(node);
      node = app.firstChild;
    }
    // 3.返回存储了所有元素的文档碎片对象
    return fragment;
  }
  buildTemplate(fragment) {
    let nodeList = [...fragment.childNodes];
    nodeList.forEach(node => {
      // 需要判断当前遍历到的节点是一个元素还是一个文本
      if (this.vm.isElement(node)) {
        // 元素节点
        this.buildElement(node);
        // 处理子元素
        this.buildTemplate(node);
      } else {
        // 文本节点
        this.buildText(node);
      }
    })
  }
  buildElement(node) {
    let attrs = [...node.attributes];
    attrs.forEach(attr => {
      // v-model="name" => {name:v-model  value:name}
      let { name, value } = attr;
      // v-model / v-html / v-text / v-xxx
      if (name.startsWith('v-')) {
        // v-model -> [v, model]
        let [_, directive] = name.split('-');
        CompilerUtil[directive](node, value, this.vm);
      }
    })
  }
  buildText(node) {
    let content = node.textContent;
    let reg = /\{\{.+?\}\}/gi;
    if (reg.test(content)) {
      CompilerUtil['content'](node, content, this.vm);
    }
  }
}
let CompilerUtil = {
  getValue(vm, value) {
    // 解析this.data.aaa.bbb.ccc这种属性
    return value.split('.').reduce((data, currentKey) => {
      return data[currentKey.trim()];
    }, vm.$data);
  },
  getContent(vm, value) {
    // 解析{{}}中的变量
    let reg = /\{\{(.+?)\}\}/gi;
    let val = value.replace(reg, (...args) => {
      return this.getValue(vm, args[1]);
    });
    return val;
  },
  // 解析v-model指令
  model: function (node, value, vm) {
    // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
    new Watcher(vm, value, (newValue, oldValue) => {
      node.value = newValue;
    });
    let val = this.getValue(vm, value);
    node.value = val;
  },
  // 解析v-html指令
  html: function (node, value, vm) {
    // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
    new Watcher(vm, value, (newValue, oldValue) => {
      node.innerHTML = newValue;
    });
    let val = this.getValue(vm, value);
    node.innerHTML = val;
  },
  // 解析v-text指令
  text: function (node, value, vm) {
    // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
    new Watcher(vm, value, (newValue, oldValue) => {
      node.innerText = newValue;
    });
    let val = this.getValue(vm, value);
    node.innerText = val;
  },
  // 解析{{}}中的变量
  content: function (node, value, vm) {
    let reg = /\{\{(.+?)\}\}/gi;
    let val = value.replace(reg, (...args) => {
      // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
      new Watcher(vm, args[1], (newValue, oldValue) => {
        node.textContent = this.getContent(vm, value);
      });
      return this.getValue(vm, args[1]);
    });
    node.textContent = val;
  }
}
實作資料驅動視圖

    Observer
  • 1,使用defineRecative函式對data做Object.defineProperty處理,使得data中的每個資料都可以進行get/set監聽
  • 2,接下來將考慮如何在監聽到data值改變後,更新視圖內容呢?使用觀察者設計模式,建立Dep和Wather類別。

    class Observer {
      constructor(data) {
        this.observer(data);
      }
      observer(obj) {
        if (obj && typeof obj === 'object') {
          // 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
          for (let key in obj) {
            this.defineRecative(obj, key, obj[key])
          }
        }
      }
      // obj: 需要操作的对象
      // attr: 需要新增get/set方法的属性
      // value: 需要新增get/set方法属性的取值
      defineRecative(obj, attr, value) {
        // 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
        this.observer(value);
        // 第三步: 将当前属性的所有观察者对象都放到当前属性的发布订阅对象中管理起来
        let dep = new Dep(); // 创建了属于当前属性的发布订阅对象
        Object.defineProperty(obj, attr, {
          get() {
            // 在这里收集依赖
            Dep.target && dep.addSub(Dep.target);
            return value;
          },
          set: (newValue) => {
            if (value !== newValue) {
              // 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
              this.observer(newValue);
              value = newValue;
              dep.notify();
              console.log('监听到数据的变化');
            }
          }
        })
      }
    }
  • 使用觀察者設計模式,建立Dep和Wather類別

    • 1,使用觀察者設計模式的目的是:
  • 解析模板,收集data中某個資料在模板中被使用的dom節點集合,當資料改變時,更新該dom節點集合就實現了資料更新。

    Dep:用於收集某個data屬性所依賴的dom節點集合,並提供更新方法
  • Watcher:每個dom節點的包裹物件
  • attr:該dom使用的data屬性
  • cb:修改該dom值的回呼函數,在建立的時候會接收

2,到這裡感覺思路是沒問題了,已經是勝券在握了。那Dep和Watcher該怎麼使用呢?

為每個屬性添加一個dep,用來收集依賴的dom

因為頁面首次渲染的時候會讀取data數據,這時候會觸發該data的getter,所以在此收集dom具體如何收集呢,在CompilerUtil類別解析v-model,{{}}等指令時,會觸發getter ,我們在觸發之前創建Wather,為Watcher添加一個靜態屬性,指向該dom,然後在getter函數裡面獲取該靜態變量,並添加到依賴中,就完成了一次收集。因為每次觸發getter之前都會對該靜態變數賦值,所以不存在收集錯依賴的情況。

class Dep {
  constructor() {
    // 这个数组就是专门用于管理某个属性所有的观察者对象的
    this.subs = [];
  }
  // 订阅观察的方法
  addSub(watcher) {
    this.subs.push(watcher);
  }
  // 发布订阅的方法
  notify() {
    this.subs.forEach(watcher => watcher.update());
  }
}
class Watcher {
  constructor(vm, attr, cb) {
    this.vm = vm;
    this.attr = attr;
    this.cb = cb;
    // 在创建观察者对象的时候就去获取当前的旧值
    this.oldValue = this.getOldValue();
  }
  getOldValue() {
    Dep.target = this;
    let oldValue = CompilerUtil.getValue(this.vm, this.attr);
    Dep.target = null;
    return oldValue;
  }
  // 定义一个更新的方法, 用于判断新值和旧值是否相同
  update() {
    let newValue = CompilerUtil.getValue(this.vm, this.attr);
    if (this.oldValue !== newValue) {
      this.cb(newValue, this.oldValue);
    }
  }
}

3,到這裡就實現了資料綁定時,視圖自動更新,本來想程式碼一步步實現的,但是發現不好處理,就把完整的class貼出來了。 實作視圖驅動資料

其實就是監聽輸入框的input、change事件。修改CompilerUtil的model方法。具體程式碼如下

model: function (node, value, vm) {
    new Watcher(vm, value, (newValue, oldValue)=>{
        node.value = newValue;
    });
    let val = this.getValue(vm, value);
    node.value = val;
	// 看这里
    node.addEventListener('input', (e)=>{
        let newValue = e.target.value;
        this.setValue(vm, value, newValue);
    })
},
###總結############vue雙向綁定原理############vue接收一個範本和data參數。 1,先將data中的資料遞歸遍歷,對每個屬性執行Object.defineProperty,定義get和set函數。並為每個屬性新增一個dep數組。當get執行時,會為所呼叫的dom節點建立一個watcher存放在該陣列中。當set執行時,重新賦值,並呼叫dep數組的notify方法,通知所有使用了該屬性watcher,並更新對應dom的內容。 2,將範本載入到記憶體中,遞歸模板中的元素,偵測到元素有v-開頭的指令或雙大括號的指令,就會從data中取對應的值去修改範本內容,這個時候就將該dom元素加入了該屬性的dep陣列中。這就實現了資料驅動視圖。在處理v-model指令的時候,為該dom添加input事件(或change),輸入時就去修改對應的屬性的值,實現了頁面驅動資料。 3,將模板與資料綁定後,將模板新增至真實dom樹。 #########如何將watcher放在dep陣列中? #########在解析模板的時候,會根據v-指令取得對應data屬性值,這個時候就會呼叫屬性的get方法,我們先建立Watcher實例,並在其內部取得該屬性值,作為舊值存放在watcher內部,我們在獲取該值之前,在Watcher原型物件上添加屬性Watcher.target = this;然後取值,將講Watcher.target = null;這樣get在被調用的時候就可以根據Watcher.target取得到watcher實例物件。 ###

methods的原理

建立vue實例的時候,接收methods參數

在解析模板的時候遇到v-on的指令。會對該dom元素新增對應事件的監聽,並使用call方法將vue綁定為該方法的this:vm.$methods[value].call(vm, e);

computed的原理

建立vue實例的時候,接收computed參數

初始化vue實例的時候,為computed的key進行Object .defineProperty處理,並加入get屬性。

(學習影片分享:web前端

以上是一文帶你深入解析vue雙向綁定原理(徹底搞懂它)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除