首頁 >web前端 >Vue.js >vuejs實作雙向綁定的原理是什麼

vuejs實作雙向綁定的原理是什麼

青灯夜游
青灯夜游原創
2021-09-28 14:06:273647瀏覽

vuejs實現雙向綁定的原則:利用資料劫持和發布訂閱模式,透過「Object.defineProperty()」來劫持各個屬性的setter、getter,在資料變動時發布訊息給訂閱者,觸發對應的監聽回調,進而對檢視進行更新。

vuejs實作雙向綁定的原理是什麼

本教學操作環境:windows7系統、vue2.9.6版,DELL G3電腦。

Vue 資料雙向綁定原則

Vue實作資料雙向綁定主要利用的是: 資料劫持和發佈訂閱模式,利用的Object.defineProperty() 方法進行的資料劫持,然後通知發布者(主題對象)去通知所有觀察者,觀察者收到通知後,就會對視圖進行更新。

https://jsrun.net/RMIKp/embedded/all/light

MVVM 框架主要包含兩個方面,資料變更更新視圖,視圖變更更新資料。

視圖變更更新數據,如果是像input 這種標籤,可以使用oninput 事件..

資料變更更新視圖可以使用Object.definProperty() 的set方法可以偵測資料變化,當資料改變就會觸發這個函數,然後更新視圖。

實作過程

我們知道如何實現雙向綁定了,首先要對資料進行劫持監聽,所以我們需要設定一個 Observer 函數,用來監聽所有屬性的變化。

如果屬性發生了變化,那就要告訴訂閱者watcher 看是否需要更新數據,如果訂閱者有多個,則需要一個Dep 來收集這些訂閱者,然後在監聽器observer 和watcher 之間進行統一管理。

還需要一個指令解析器 compile,對需要監聽的節點和屬性進行掃描和解析。

因此,流程大概是這樣的:

  • 實作一個監聽器 Observer,用來劫持並監聽所有屬性,如果發生變動,則通知訂閱者。

  • 實作一個訂閱者 Watcher,當接到屬性變化的通知時,執行對應的函數,然後更新視圖,使用 Dep 來收集這些 Watcher。

  • 實作一個解析器 Compile,用於掃描和解析的節點的相關指令,並根據初始化模板以及初始化對應的訂閱器。

vuejs實作雙向綁定的原理是什麼

顯示一個Observer

#Observer 是一個資料監聽器,核心方法是利用Object.defineProperty() 透過遞歸的方式對所有屬性都添加setter、getter 方法進行監聽。

var library = {
  book1: {
    name: "",
  },
  book2: "",
};
observe(library);
library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍”

// 为数据添加检测
function defineReactive(data, key, val) {
  observe(val); // 递归遍历所有子属性
  let dep = new Dep(); // 新建一个dep
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      if (Dep.target) {
        // 判断是否需要添加订阅者,仅第一次需要添加,之后就不用了,详细看Watcher函数
        dep.addSub(Dep.target); // 添加一个订阅者
      }
      return val;
    },
    set: function(newVal) {
      if (val == newVal) return; // 如果值未发生改变就return
      val = newVal;
      console.log(
        "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”"
      );
      dep.notify(); // 如果数据发生变化,就通知所有的订阅者。
    },
  });
}

// 监听对象的所有属性
function observe(data) {
  if (!data || typeof data !== "object") {
    return; // 如果不是对象就return
  }
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  });
}
// Dep 负责收集订阅者,当属性发生变化时,触发更新函数。
function Dep() {
  this.subs = {};
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  notify: function() {
    this.subs.forEach((sub) => sub.update());
  },
};

思路分析中,需要有一個可以容納訂閱者訊息訂閱器​​ Dep,用於收集訂閱者,在屬性發生變化時執行對應的更新函數。

從程式碼上看,將訂閱器 Dep 加入在 getter 裡,是為了讓 Watcher 初始化時觸發,,因此,需要判斷是否需要訂閱者。

在 setter 中,如果有資料發生變化,則通知所有的訂閱者,然後訂閱者就會更新對應的函數。

到此為止,一個比較完整的Observer 就完成了,接下來開始設計Watcher.

實作Watcher

訂閱者Watcher 需要在初始化的時候將自己添加到訂閱器Dep 中,我們已經知道監聽器Observer 是在get 時執行的Watcher 操作,所以只需要在Watcher 初始化的時候觸發對應的get 函數去添加對應的訂閱者操作即可。

那給如何觸發 get 呢?因為我們已經設定了 Object.defineProperty(),所以只需要取得對應的屬性值就可以觸發了。

我們只需要在訂閱者 Watcher 初始化的時候,在 Dep.target 上快取下訂閱者,添加成功之後在將其去掉就可以了。

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // 将自己添加到订阅器的操作
}

Watcher.prototype = {
  update: function() {
    this.run();
  },
  run: function() {
    var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function() {
    Dep.target = this; // 缓存自己,用于判断是否添加watcher。
    var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
    Dep.target = null; // 释放自己
    return value;
  },
};

到此為止, 簡單的額 Watcher 設計完畢,然後將 Observer 和 Watcher 關聯起來,就可以實現一個簡單的的雙向綁定了。

因為還沒有設計解析器 Compile,所以可以先將模板資料寫死。

將程式碼轉換為 ES6 建構子的寫法,預覽試試。

https://jsrun.net/8SIKp/embedded/all/light

#這段程式碼因為沒有實作編譯器而是直接傳入了所綁定的變量,我們只在一個節點上設定一個資料(name)進行綁定,然後在頁面上進行new MyVue,就可以實現雙向綁定了。

並兩秒後進行值得改變,可以看到,頁面也發生了變化。

// MyVue
proxyKeys(key) {
    var self = this;
    Object.defineProperty(this, key, {
        enumerable: false,
        configurable: true,
        get: function proxyGetter() {
            return self.data[key];
        },
        set: function proxySetter(newVal) {
            self.data[key] = newVal;
        }
    });
}

上面這段程式碼的功能是將 this.data 的 key 代理到 this 上,使得我可以方便的使用 this.xx 就可以取到 this.data.xx。

實作Compile

雖然上面實作了雙向資料綁定,但是整個過程都沒有解析DOM 節店,而是固定替換的,所以接下來要實作一個解析器來做數據的解析和綁定工作。

解析器 compile 的實作步驟:

  • 解析模板指令,並取代模板數據,初始化視圖。

  • 将模板指定对应的节点绑定对应的更新函数,初始化相应的订阅器。

为了解析模板,首先需要解析 DOM 数据,然后对含有 DOM 元素上的对应指令进行处理,因此整个 DOM 操作较为频繁,可以新建一个 fragment 片段,将需要的解析的 DOM 存入 fragment 片段中在进行处理。

function nodeToFragment(el) {
  var fragment = document.createDocumentFragment();
  var child = el.firstChild;
  while (child) {
    // 将Dom元素移入fragment中
    fragment.appendChild(child);
    child = el.firstChild;
  }
  return fragment;
}

接下来需要遍历各个节点,对含有相关指令和模板语法的节点进行特殊处理,先进行最简单模板语法处理,使用正则解析“{{变量}}”这种形式的语法。

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/; // 匹配{{xx}}
        var text = node.textContent;
        if (self.isTextNode(node) && reg.test(text)) {  // 判断是否是符合这种形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 继续递归遍历子节点
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 将初始化的数据初始化到视图中
    new Watcher(this.vm, exp, function (value) {  // 生成订阅器并绑定更新函数
        self.updateText(node, value);
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

获取到最外层的节点后,调用 compileElement 函数,对所有的子节点进行判断,如果节点是文本节点切匹配{{}}这种形式的指令,则进行编译处理,初始化对应的参数。

然后需要对当前参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。

这样就完成了解析、初始化、编译三个过程了。

接下来改造一个 myVue 就可以使用模板变量进行双向数据绑定了。

https://jsrun.net/K4IKp/embedded/all/light

添加解析事件

添加完 compile 之后,一个数据双向绑定就基本完成了,接下来就是在 Compile 中添加更多指令的解析编译,比如 v-model、v-on、v-bind 等。

添加一个 v-model 和 v-on 解析:

function compile(node) {
  var nodeAttrs = node.attributes;
  var self = this;
  Array.prototype.forEach.call(nodeAttrs, function(attr) {
    var attrName = attr.name;
    if (isDirective(attrName)) {
      var exp = attr.value;
      var dir = attrName.substring(2);
      if (isEventDirective(dir)) {
        // 事件指令
        self.compileEvent(node, self.vm, exp, dir);
      } else {
        // v-model 指令
        self.compileModel(node, self.vm, exp, dir);
      }
      node.removeAttribute(attrName); // 解析完毕,移除属性
    }
  });
}
// v-指令解析
function isDirective(attr) {
  return attr.indexOf("v-") == 0;
}
// on: 指令解析
function isEventDirective(dir) {
  return dir.indexOf("on:") === 0;
}

上面的 compile 函数是用于遍历当前 dom 的所有节点属性,然后判断属性是否是指令属性,如果是在做对应的处理(事件就去监听事件、数据就去监听数据..)

完整版 myVue

在 MyVue 中添加 mounted 方法,在所有操作都做完时执行。

class MyVue {
  constructor(options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;
    Object.keys(this.data).forEach(function(key) {
      self.proxyKeys(key);
    });
    observe(this.data);
    new Compile(options.el, this);
    options.mounted.call(this); // 所有事情处理好后执行mounted函数
  }
  proxyKeys(key) {
    // 将this.data属性代理到this上
    var self = this;
    Object.defineProperty(this, key, {
      enumerable: false,
      configurable: true,
      get: function getter() {
        return self.data[key];
      },
      set: function setter(newVal) {
        self.data[key] = newVal;
      },
    });
  }
}

然后就可以测试使用了。

https://jsrun.net/Y4IKp/embedded/all/light

总结一下流程,回头在哪看一遍这个图,是不是清楚很多了。

vuejs實作雙向綁定的原理是什麼

可以查看的代码地址:Vue2.x 的双向绑定原理及实现

相关推荐:《vue.js教程

以上是vuejs實作雙向綁定的原理是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn