摘要:上篇文章,我們講解了的屬性映射和方法的重定義,鏈接地址如下源碼解析一屬性映射和函數(shù)引用的重定義這篇文章給大家?guī)淼氖堑碾p向綁定講解。這就是的雙向綁定。使用定時(shí)器定時(shí)檢查的值,發(fā)生變化就通知訂閱者。這個(gè)方法不好,定時(shí)器不能實(shí)時(shí)反應(yīng)變化。
文章中的代碼時(shí)階段,可以下載源碼測試一下。
git項(xiàng)目地址:https://github.com/xubaodian/...
項(xiàng)目使用webpack構(gòu)建,下載后先執(zhí)行:
npm install
安裝依賴后使用指令:
npm run dev
可以運(yùn)行項(xiàng)目。
上篇文章,我們講解了Vue的data屬性映射和方法的重定義,鏈接地址如下:
Vue源碼解析(一)data屬性映射和methods函數(shù)引用的重定義
這篇文章給大家?guī)淼氖荲ue的雙向綁定講解。
我們看一張圖:
可以看到,輸入框上方的內(nèi)同和輸入框中的值是一致的。輸入框的之變化,上方的值跟著一起變化。
這就是Vue的雙向綁定。
我們先不著急了解Vue時(shí)如何實(shí)現(xiàn)這一功能的,如果我們自己要實(shí)現(xiàn)這樣的功能,如何實(shí)現(xiàn)呢?
我的思路是這樣:
可以分為幾個(gè)步驟,如下:
1、首先給輸入框添加input事件,監(jiān)視輸入值,存放在變量value中。
2、監(jiān)視value變量,確保value變化時(shí),監(jiān)視器可以發(fā)現(xiàn)。
3、若value發(fā)生變化,則重新渲染視圖。
上面三個(gè)步驟,1(addEventListener)和3(操作dom)都很好實(shí)現(xiàn),對于2的實(shí)現(xiàn),可能有一下兩個(gè)方案:
1、使用Object.defineProperty()重新定義對象set和get,在值發(fā)生變化時(shí),通知訂閱者。
2、使用定時(shí)器定時(shí)檢查value的值,發(fā)生變化就通知訂閱者。(這個(gè)方法不好,定時(shí)器不能實(shí)時(shí)反應(yīng)value變化)。
Vue源碼中采用了方案1,我們首先用方案1實(shí)現(xiàn)對對象值的監(jiān)聽,代碼如下:
function defineReactive(obj, key, val, customSetter) { //獲取對象給定屬性的描述符 let property = Object.getOwnPropertyDescriptor(obj, key); //對象該屬性不可配置,直接返回 if (property && property.configurable === false) { return; } //獲取屬性get和set屬性,若此前該屬性已經(jīng)進(jìn)行監(jiān)聽,則確保監(jiān)聽屬性不會被覆蓋 let getter = property && property.get; let setter = property && property.set; if (arguments.length < 3) { val = obj[key]; } //監(jiān)聽屬性 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; } //自定義響應(yīng)函數(shù) if (customSetter) { customSetter(newVal); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } console.log(`屬性${key}發(fā)生變化:${value} => ${newValue}`); } }) }
下面我們測試下,測試代碼如下:
let obj = { name: "xxx", age: 20 }; defineReactive(obj, "name"); let name = obj.name; obj.name = "1111";
控制臺輸出為:
讀取了name屬性 test.html:51 屬性name發(fā)生變化:xxx => 1111
可見,我們已經(jīng)實(shí)現(xiàn)了對obj對象name屬性讀和寫的監(jiān)聽。
實(shí)現(xiàn)了監(jiān)聽,這沒問題,但是視圖怎么知道這些屬性發(fā)生了變化呢?可以使用發(fā)布訂閱模式實(shí)現(xiàn)。
發(fā)布訂閱模式什么是發(fā)布訂閱模式呢?
我畫了一個(gè)示意圖,如下:
發(fā)布訂閱模式有幾個(gè)部分構(gòu)成:
1、訂閱中心,管理訂閱者列表,發(fā)布者發(fā)消息時(shí),通知相應(yīng)的訂閱者。
2、訂閱者,這個(gè)是訂閱消息的主體,就像關(guān)注微信公眾號一樣,有文章就會通知關(guān)注者。
3、發(fā)布者,類似微信公眾號的文章發(fā)布者。
訂閱中心的代碼如下:
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(); }); } } //訂閱中心 靜態(tài)變量,訂閱時(shí)使用 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]; }
訂閱中心已經(jīng)實(shí)現(xiàn),還有發(fā)布者和訂閱者,先看下發(fā)布者,這里誰是發(fā)布者呢?
沒錯(cuò),就是defineReactive函數(shù),這個(gè)函數(shù)實(shí)現(xiàn)了對data屬性的監(jiān)聽,它可以檢測到data屬性的修改,發(fā)生修改時(shí),通知訂閱中心,所以defineReactive做一些修改,如下:
//屬性監(jiān)聽 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屬性,若此前該屬性已經(jīng)進(jìn)行監(jiān)聽,則確保監(jiān)聽屬性不會被覆蓋 let getter = property && property.get; let setter = property && property.set; if (arguments.length < 3) { val = obj[key]; } //如果監(jiān)聽的是一個(gè)對象,繼續(xù)深入監(jiān)聽 let childOb = observe(val); //監(jiān)聽屬性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val; //這段代碼時(shí)添加訂閱時(shí)使用的 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; } //自定義響應(yīng)函數(shù) if (customSetter) { customSetter(newVal); } if (setter) { setter.call(obj, newVal); } else { val = newVal; } //如果新的值為對象,重新監(jiān)聽 childOb = observe(newVal); /** * 訂閱中心通知所有訂閱者 **/ dep.notify(); } }) }
這里設(shè)計(jì)到閉包的概念,我們在函數(shù)里定義了:
const dep = new Dep();
由于set和get函數(shù)一直都存在的,所有dep會一直存在,不會被回收。
當(dāng)值發(fā)生變化后,利用下面的代碼通知訂閱者:
dep.notify();
訂閱中心和發(fā)布者都有了,我們何時(shí)訂閱呢?或者什么時(shí)間訂閱合適呢?
我們是希望實(shí)現(xiàn)當(dāng)讀取data屬性時(shí)候,實(shí)現(xiàn)訂閱。所以在defineReactive函數(shù)的get監(jiān)聽中添加了如下代碼:
if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); } } return value;
Dep.target是一個(gè)靜態(tài)變量,用來存儲訂閱者的,每次訂閱前指向訂閱者,訂閱者置為null。
訂閱者代碼如下:
let uid = 0; //訂閱者類 export class Watcher{ //構(gòu)造器,vm是vue實(shí)例 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() { //訂閱前,設(shè)置Dep.target變量,指向自身 pushTarget(this) let value; const vm = this.vm; /** * 這個(gè)地方讀取data屬性,觸發(fā)下面的訂閱代碼, * if (Dep.target) { * dep.depend(); * if (childOb) { * childOb.dep.depend(); * } * } * return value; **/ value = this.getter.call(vm, vm); //訂閱后,置Dep.target為null popTarget(); return value } //值變化,調(diào)用回調(diào)函數(shù) update() { this.cb(this.value); } //添加依賴 addDep(dep) { this.deps.push(dep); dep.addSub(this); } } //解析類屬性的路徑,例如obj.sub.name,返回實(shí)際的值 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; } }
除了發(fā)布訂閱以外,雙向綁定還需要編譯dom。
主要實(shí)現(xiàn)兩個(gè)功能:
1、將dom中的{{key}}元素替換為Vue中的屬性。
2、檢測帶有v-model屬性的input元素,添加input事件,有修改時(shí),修改Vue實(shí)例的屬性。
檢測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(`未發(fā)現(xiàn)dom: ${el}`); } } else { console.error("vue實(shí)例未綁定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造成的內(nèi)存泄露我們暫不考慮,這些問題后續(xù)解決 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(`未發(fā)現(xiàn)dom: ${el}`); } } else { console.error("vue實(shí)例未綁定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); } }); } }
到此位置,就實(shí)現(xiàn)了雙向綁定。
測試代碼如下,因?yàn)槲矣脀ebpack構(gòu)建的前端項(xiàng)目,html模板如下:
test {{name}}
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項(xiàng)目地址:https://github.com/xubaodian/...
項(xiàng)目使用webpack構(gòu)建,下載后先執(zhí)行:
npm install
安裝依賴后使用指令:
npm run dev
可以運(yùn)行項(xiàng)目。
如有疑問,歡迎留言或發(fā)送郵件至472784995@qq.com。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/100593.html
摘要:執(zhí)行的時(shí)候,會綁定上下文對象為組件實(shí)例于是中的就能取到組件實(shí)例本身,的代碼塊頂層作用域就綁定為了組件實(shí)例于是內(nèi)部變量的訪問,就會首先訪問到組件實(shí)例上。其中的獲取,就會先從組件實(shí)例上獲取,相當(dāng)于。 寫文章不容易,點(diǎn)個(gè)贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內(nèi)部詳情,讓我們一起學(xué)習(xí)吧研究基于 Vue版本 【2.5.17】 如果你覺得...
摘要:所以無需太過介懷是實(shí)現(xiàn)的單向或雙向綁定。監(jiān)聽數(shù)據(jù)綁定更新函數(shù)的處理是在這個(gè)方法中,通過添加回調(diào)來接收數(shù)據(jù)變化的通知至此,一個(gè)簡單的就完成了,完整代碼。 本文能幫你做什么?1、了解vue的雙向數(shù)據(jù)綁定原理以及核心代碼模塊2、緩解好奇心的同時(shí)了解如何實(shí)現(xiàn)雙向綁定為了便于說明原理與實(shí)現(xiàn),本文相關(guān)代碼主要摘自vue源碼, 并進(jìn)行了簡化改造,相對較簡陋,并未考慮到數(shù)組的處理、數(shù)據(jù)的循環(huán)依賴等,也...
摘要:接下來要看看這個(gè)訂閱者的具體實(shí)現(xiàn)了實(shí)現(xiàn)訂閱者作為和之間通信的橋梁,主要做的事情是在自身實(shí)例化時(shí)往屬性訂閱器里面添加自己自身必須有一個(gè)方法待屬性變動通知時(shí),能調(diào)用自身的方法,并觸發(fā)中綁定的回調(diào),則功成身退。 本文能幫你做什么?1、了解vue的雙向數(shù)據(jù)綁定原理以及核心代碼模塊2、緩解好奇心的同時(shí)了解如何實(shí)現(xiàn)雙向綁定為了便于說明原理與實(shí)現(xiàn),本文相關(guān)代碼主要摘自vue源碼, 并進(jìn)行了簡化改造,...
摘要:首先,兄弟,容我先說幾句涉及源碼很多,篇幅很長,我都已經(jīng)分了上下三篇了,依然這么長,但是其實(shí)內(nèi)容都差不多一樣,但是我還是毫無保留地給你了。 寫文章不容易,點(diǎn)個(gè)贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內(nèi)部詳情,讓我們一起學(xué)習(xí)吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點(diǎn)擊 下面鏈接 或者 拉到 下面關(guān)注公眾號也...
閱讀 2343·2021-11-23 09:51
閱讀 1146·2021-11-22 13:52
閱讀 3620·2021-11-10 11:35
閱讀 1198·2021-10-25 09:47
閱讀 3005·2021-09-07 09:58
閱讀 1068·2019-08-30 15:54
閱讀 2825·2019-08-29 14:21
閱讀 3036·2019-08-29 12:20