文章中的代碼時階段,能夠下載源碼測試一下。
git項目地址:https://github.com/xubaodian/...
項目使用webpack構建,下載後先執行:javascript
npm install
安裝依賴後使用指令:html
npm run dev
能夠運行項目。
前端
上篇文章,咱們講解了Vue的data屬性映射和方法的重定義,連接地址以下:
Vue源碼解析(一)data屬性映射和methods函數引用的重定義vue
這篇文章給你們帶來的是Vue的雙向綁定講解。
java
咱們看一張圖:
能夠看到,輸入框上方的內同和輸入框中的值是一致的。輸入框的之變化,上方的值跟着一塊兒變化。
這就是Vue的雙向綁定。
node
咱們先不着急瞭解Vue時如何實現這一功能的,若是咱們本身要實現這樣的功能,如何實現呢?
個人思路是這樣:
能夠分爲幾個步驟,以下:
react
一、首先給輸入框添加input事件,監視輸入值,存放在變量value中。
webpack
二、監視value變量,確保value變化時,監視器能夠發現。
git
三、若value發生變化,則從新渲染視圖。
github
上面三個步驟,1(addEventListener)和3(操做dom)都很好實現,對於2的實現,可能有一下兩個方案:
一、使用Object.defineProperty()從新定義對象set和get,在值發生變化時,通知訂閱者。
二、使用定時器定時檢查value的值,發生變化就通知訂閱者。(這個方法很差,定時器不能實時反應value變化)。
Vue源碼中採用了方案1,咱們首先用方案1實現對對象值的監聽,代碼以下:
function defineReactive(obj, key, val, customSetter) { //獲取對象給定屬性的描述符 let property = Object.getOwnPropertyDescriptor(obj, key); //對象該屬性不可配置,直接返回 if (property && property.configurable === false) { return; } //獲取屬性get和set屬性,若此前該屬性已經進行監聽,則確保監聽屬性不會被覆蓋 let getter = property && property.get; let setter = property && property.set; if (arguments.length < 3) { val = obj[key]; } //監聽屬性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val; console.log(`讀取了${key}屬性`); return value; }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val; //若是值沒有變化,則不作改動 if (newVal === value) { return; } //自定義響應函數 if (customSetter) { customSetter(newVal); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } console.log(`屬性${key}發生變化:${value} => ${newValue}`); } }) }
下面咱們測試下,測試代碼以下:
let obj = { name: 'xxx', age: 20 }; defineReactive(obj, 'name'); let name = obj.name; obj.name = '1111';
控制檯輸出爲:
讀取了name屬性 test.html:51 屬性name發生變化:xxx => 1111
可見,咱們已經實現了對obj對象name屬性讀和寫的監聽。
實現了監聽,這沒問題,可是視圖怎麼知道這些屬性發生了變化呢?可使用發佈訂閱模式實現。
什麼是發佈訂閱模式呢?
我畫了一個示意圖,以下:
發佈訂閱模式有幾個部分構成:
一、訂閱中心,管理訂閱者列表,發佈者發消息時,通知相應的訂閱者。
二、訂閱者,這個是訂閱消息的主體,就像關注微信公衆號同樣,有文章就會通知關注者。
三、發佈者,相似微信公衆號的文章發佈者。
訂閱中心的代碼以下:
export class Dep { constructor() { this.id = uid++; //訂閱列表 this.subs = []; } //添加訂閱 addSub(watcher) { this.subs.push(watcher); } //刪除訂閱者 remove(watcher) { let index = this.subs.findIndex(item => item.id === watcher.id); if (index > -1) { this.subs.splice(index, 1); } } depend () { if (Dep.target) { Dep.target.addDep(this); } } //通知訂閱者 notify() { this.subs.map(item => { item.update(); }); } } //訂閱中心 靜態變量,訂閱時使用 Dep.target = null; const targetStack = []; export function pushTarget (target) { targetStack.push(target); Dep.target = target; } export function popTarget () { targetStack.pop(); Dep.target = targetStack[targetStack.length - 1]; }
訂閱中心已經實現,還有發佈者和訂閱者,先看下發布者,這裏誰是發佈者呢?
沒錯,就是defineReactive函數,這個函數實現了對data屬性的監聽,它能夠檢測到data屬性的修改,發生修改時,通知訂閱中心,因此defineReactive作一些修改,以下:
//屬性監聽 export function defineReactive(obj, key, val, customSetter) { //獲取對象給定屬性的描述符 let property = Object.getOwnPropertyDescriptor(obj, key); //對象該屬性不可配置,直接返回 if (property && property.configurable === false) { return; } //訂閱中心 const dep = new Dep(); //獲取屬性get和set屬性,若此前該屬性已經進行監聽,則確保監聽屬性不會被覆蓋 let getter = property && property.get; let setter = property && property.set; if (arguments.length < 3) { val = obj[key]; } //若是監聽的是一個對象,繼續深刻監聽 let childOb = observe(val); //監聽屬性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val; //這段代碼時添加訂閱時使用的 if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); } } return value; }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val; //若是值沒有變化,則不作改動 if (newVal === value) { return; } //自定義響應函數 if (customSetter) { customSetter(newVal); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } //若是新的值爲對象,從新監聽 childOb = observe(newVal); /** * 訂閱中心通知全部訂閱者 **/ dep.notify(); } }) }
這裏設計到閉包的概念,咱們在函數裏定義了:
const dep = new Dep();
因爲set和get函數一直都存在的,全部dep會一直存在,不會被回收。
當值發生變化後,利用下面的代碼通知訂閱者:
dep.notify();
訂閱中心和發佈者都有了,咱們什麼時候訂閱呢?或者什麼時間訂閱合適呢?
咱們是但願實現當讀取data屬性時候,實現訂閱。因此在defineReactive函數的get監聽中添加了以下代碼:
if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); } } return value;
Dep.target是一個靜態變量,用來存儲訂閱者的,每次訂閱前指向訂閱者,訂閱者置爲null。
訂閱者代碼以下:
let uid = 0; //訂閱者類 export class Watcher{ //構造器,vm是vue實例 constructor(vm, expOrFn, cb) { this.vm = vm; this.cb = cb; this.id = uid++; this.deps = []; if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) } this.value = this.get(); } //將訂閱這添加到訂閱中心 get() { //訂閱前,設置Dep.target變量,指向自身 pushTarget(this) let value; const vm = this.vm; /** * 這個地方讀取data屬性,觸發下面的訂閱代碼, * if (Dep.target) { * dep.depend(); * if (childOb) { * childOb.dep.depend(); * } * } * return value; **/ value = this.getter.call(vm, vm); //訂閱後,置Dep.target爲null popTarget(); return value } //值變化,調用回調函數 update() { this.cb(this.value); } //添加依賴 addDep(dep) { this.deps.push(dep); dep.addSub(this); } } //解析類屬性的路徑,例如obj.sub.name,返回實際的值 export function parsePath (path){ const segments = path.split('.'); return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return; obj = obj[segments[i]]; } return obj; } }
除了發佈訂閱之外,雙向綁定還須要編譯dom。
主要實現兩個功能:
一、將dom中的{{key}}元素替換爲Vue中的屬性。
二、檢測帶有v-model屬性的input元素,添加input事件,有修改時,修改Vue實例的屬性。
檢測v-model,綁定事件的代碼以下:
export function initModelMixin(Vue) { Vue.prototype._initModel = function () { if (this._dom == undefined) { if (this.$options.el) { let el = this.$options.el; let dom = document.querySelector(el); if (dom) { this._dom = dom; } else { console.error(`未發現dom: ${el}`); } } else { console.error('vue實例未綁定dom'); } } bindModel(this._dom, this); } } //input輸入框有V-model屬性,則綁定input事件 function bindModel(dom, vm) { if (dom) { if (dom.tagName === 'INPUT') { let attrs = Array.from(dom.attributes); attrs.map(item => { if (item.name === 'v-model') { let value = item.value; dom.value = getValue(vm, value); //綁定事件,暫不考慮清除綁定,所以刪除dom形成的內存泄露咱們暫不考慮,這些問題後續解決 dom.addEventListener('input', (event) => { setValue(vm, value, event.target.value); }); } }) } let children = Array.from(dom.children); if (children) { children.map(item => { bindModel(item, vm); }); } } }
替換dom中{{key}}相似的屬性代碼:
export function renderMixin(Vue) { Vue.prototype._render = function () { if (this._dom == undefined) { if (this.$options.el) { let el = this.$options.el; let dom = document.querySelector(el); if (dom) { this._dom = dom; } else { console.error(`未發現dom: ${el}`); } } else { console.error('vue實例未綁定dom'); } } replaceText(this._dom, this); } } //替換dom的innerText function replaceText(dom, vm) { if (dom) { let children = Array.from(dom.childNodes); children.map(item => { if (item.nodeType === 3) { if (item.originStr === undefined) { item.originStr = item.nodeValue; } let str = replaceValue(item.originStr, function(key){ return getValue(vm, key); }); item.nodeValue = str; } else if (item.nodeType === 1) { replaceText(item, vm); } }); } }
到此位置,就實現了雙向綁定。
測試代碼以下,由於我用webpack構建的前端項目,html模板以下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>test </title> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <div id="app"> <div class="test">{{name}}</div> <input type="text" v-model="name"> </div> </body> </html>
main.js代碼:
import { Vue } from '../src/index'; let options = { el: '#app', data: { name: 'xxx', age: 18 }, methods: { sayName() { console.log(this.name); } } } let vm = new Vue(options);
效果以下:
能夠下載源碼嘗試,git項目地址:https://github.com/xubaodian/...
項目使用webpack構建,下載後先執行:
npm install
安裝依賴後使用指令:
npm run dev
能夠運行項目。若有疑問,歡迎留言或發送郵件至472784995@qq.com。