JS - 如何實現一個類似 vue 的雙向綁定 Github JS 實現代碼
先來看一張圖:
這張圖我做個簡要的描述:
首先創建一個實例對象,分別觸發了 compile 解析指令 和 observer 監聽器,
compile 解析指令則循環遞歸 解析 類似 v-model 這樣的指令,初始化 data 綁定數據,同時每個節點創建一個訂閱者 watcher ,
observer 監聽器 則利用了 Object.defineProperty() 方法的描述屬性里邊的 set,get方法,來監聽數據變化,
get 方法是在創建實例對象,生成dom節點的時候都會觸發,固:在compile 解析編譯的時候,依次給每一個節點添加了一個訂閱者到主題對象 Dep
set 方法則是數據發生改變了,通知Dep訂閱器里的所有wachter,然后找到對應訂閱者 wachter 觸發對應 update 更新視圖
簡單的說明就是這樣了。
雙向綁定原理
vue數據雙向綁定是通過數據劫持結合發布者-訂閱者模式的方式來實現的。
具體點兒
Vue雙向數據綁定的原理就是利用了 Object.defineProperty() 這個方法重新定義了對象獲取屬性值(get)和設置屬性值(set)的操作來實現的2.實現一個監聽器Observer,用來劫持並監聽所有屬性,如果有變動的,就通知訂閱者。
3.實現一個訂閱者Watcher,每一個Watcher都綁定一個 update,watcher 可以收到屬性的變化通知並執行相應的 update ,從而更新視圖。
4.實現MVVM,雙向綁定
以下實踐里邊的幾個方法我就不做介紹了,感興趣可查詢
Object.defineProperty()
createDocumentFragment()
Object.keys()
話不多說:直接上代碼:實現一個解析器Compile
/* 第一步 1,創建文檔碎片,劫持所有dom節點,重繪dom節點 2,重繪dom節點,初始化文檔碎片綁定數據 實現文檔編譯 compile */ function getDocumentFragment(node, vm) { var flag = document.createDocumentFragment(); var child; while (child = node.firstChild) { /* while (child = node.firstChild) 相當於 child = node.firstChild while (child) */ compile(child, vm); flag.appendChild(child); } node.appendChild(flag); } function compile(node, vm) { /* nodeType 返回數字,表示當前節點類型 1 Element 代表元素 Element, Text, 2 Attr 代表屬性 Text, EntityReference 3 Text 代表元素或屬性中的文本內容。 . . . 更多請查看文檔 */ if (node.nodeType === 1) { // 獲取當前元素的attr屬性 var attr = node.attributes; for (let i = 0; i < attr.length; i++) { // nodeName 是attr屬性 key 即名稱 , 匹配自定義 v-m if (attr[i].nodeName === 'v-m') { // 獲取當前值 即 v-m = "test" 里邊的 test let name = attr[i].nodeValue; // 當前節點輸入事件 node.addEventListener('keyup', function (e) { vm[name] = e.target.value; }); // 頁面元素寫值 vm.data[name] 即 vm.data['test'] 即 MVVM node.value = vm.data[name]; //最后移除標簽中的 v-m 屬性 node.removeAttribute('v-m'); // 為每一個節點創建一個 watcher new Watcher(vm, node, name, "input"); } } /* 繼續遞歸調用 文檔編譯 實現 視圖更新 ; */ if (child = node.firstChild) { /* if (child = node.firstChild) 相當於 child = node.firstChild id(child) */ compile(child, vm); } } if (node.nodeType === 3) { let reg = /\{\{(.*)\}\}/; if (reg.test(node.nodeValue)) { let name = RegExp.$1.trim(); node.nodeValue = vm.data[name]; // 為每一個節點創建一個 watcher new Watcher(vm, node, name, "text"); } } }
實現一個監聽器Observer
/* 第二步 實現一個數據監聽 1,獲取當前實例對象的 data 屬性 key observer(當前實例對象 data ,當前實例對象) 2,使用 Object.defineProperty 方法 實現監聽 */ function observe(data, vm) { Object.keys(data).forEach(function (key) { defineReactive(vm, key, data[key]); }); } function defineReactive(vm, key, val) { /* Object.defineProperty obj 要在其上定義屬性的對象。 prop 要定義或修改的屬性的名稱。 descriptor 將被定義或修改的屬性描述符。 描述符有很多,就包括我們要市用 set , get 方法 */ var dep = new Dep(); Object.defineProperty(vm, key, { get: function () { /* if (Dep.target) dep.addSub(Dep.target); 看到這段代碼不要差異,生成每一個 dom節點,都會走 get 方法 這里為每一個節點 添加一個訂閱者 到主題對象 Dep */ if (Dep.target) dep.addSub(Dep.target); console.log(val) return val; }, set: function (newValue) { if (newValue === val) return; val = newValue; console.log(val + "=>" + newValue) // 通知所有訂閱者 dep.notify(); } }); }
實現一個訂閱者Watcher
/* 第三步 1,實現一個 watcher 觀察者/訂閱者 訂閱者原型上掛在兩個方法 分別是 update 渲染視圖 2,定義一個消息訂閱器 很簡單,維護一個數組,用來收集訂閱者 消息訂閱器原型掛載兩個方法 分別是 addSub 添加一個訂閱者 notify 數據變動 通知 這個訂閱者的 update 方法 */ function Watcher(vm, node, name, nodeType) { Dep.target = this; this.vm = vm; this.node = node; this.name = name; this.nodeType = nodeType; this.update(); console.log(Dep.target) Dep.target = null; } Watcher.prototype = { update: function () { /* this.node 指向當前修改的 dom 元素 this.vm 指向當前 dom 的實例對象 根據 nodeType 類型 賦值渲染頁面 */ if (this.nodeType === 'text') { this.node.nodeValue = this.vm[this.name] } if (this.nodeType === 'input') { this.node.value = this.vm[this.name] } } } function Dep() { this.subs = []; } Dep.prototype = { addSub: function (sub) { this.subs.push(sub); }, notify: function () { this.subs.forEach(function (sub) { sub.update(); }); } }
實現類似Vue的MVVM
/* 創建一個構造函數,並生成實例化對象 vm */ function Vue(o) { this.id = o.el; this.data = o.data; observe(this.data, this); getDocumentFragment(document.getElementById(this.id), this); } var vm = new Vue({ el: 'app', data: { msg: 'HiSen', test: 'Hello,MVVM' } });
也許看到最后大家也沒有看出個所以然,曾幾何時的我跟你們一樣,看來看去,就是這么幾段代碼;建議:拿下我的源碼,自己跑一跑,看一看,是騾子是馬拉出來溜溜。