大家好,今天為大家講解一下 Vue 中的觀察者,及發布和訂閱的實現
1、首先我們來創建一個觀察者。
/** * 觀察者 */ class Watcher{ constructor(vm,expr,cd){ this.vm = vm; this.expr = expr; this.cd = cd; //存放舊值 this.oldValue = this.get(); } get(){ //獲取舊值 Dep.target = this; //先把自己放到 this 上 //取值時 把這個觀察者和數據關聯起來 let value = CompileUtil.getVal(this.vm,this.expr); Dep.target = null; //不取消 任何值取都會 添加watcher return value; } updata(){ let newVal = CompileUtil.getVal(this.vm,this.expr); //獲取新值 if(newVal !== this.oldValue){ this.cd(newVal) } } }
接下來我們來創建一個發布和訂閱的構造函數
/** * (發布訂閱) 觀察者 被觀察者 */ class Dep{ constructor(){ this.subs = [] //存放所有的 watcher } //訂閱 addSub(watcher){ //添加 watcher this.subs.push(watcher) } //發布 notify(){ this.subs.forEach(watcher=>watcher.updata()); } }
在觀察者中有這樣的一段代碼
get(){ //獲取舊值 Dep.target = this; //先把自己放到 this 上 //取值時 把這個觀察者和數據關聯起來 let value = CompileUtil.getVal(this.vm,this.expr); Dep.target = null; //不取消 任何值取都會 添加watcher return value; }
因為 js 是單線程的,所以我們就可以在每一個觀察者獲取舊值的時候,給這個觀察者添加一個 target 屬性,這個屬性就指向它這個觀察者本身。之后就把這個 target 清除。
而在清除之前都發生了什么呢?
其實就發生了數據劫持,這個在上一篇中講過。
//實現數據劫持 defineReactive(obj,key,value){ this.observer(value); //如果傳進來的參數是對象,就回調一下這個函數,就是一個遞歸函數 let dep = new Dep(); //給每一個屬性都添加一個具有發布和訂閱的功能 Object.defineProperty(obj,key,{ get(){ //創建watcher時,會獲取到對應的內容 並且把watcher放到了全局上 Dep.target && dep.addSub(Dep.target); return value; }, set: (newVal)=>{ if(value !== newVal){ this.observer(newVal); //給設置的新值也加上 get set 方法 value = newVal; dep.notify(); //執行觀察者更新時的函數 } } }) }
在數據劫持中,首先我們 new 了一個具有發布和訂閱的功能實例,給每一個屬性都添加上具有發布和訂閱的功能,
同時,在給這個屬性重新賦值的時候,我們給這個具有發布和訂閱的功能功能的值,讓它執行它的訂閱函數。
2、好了,現在每一個值都有發布和訂閱功能了,那么這個觀察者又是都給誰添加呢,要知道我們的觀察者還沒有執行過呢 0.0
首先,v-model 是需要綁定的一個觀察者的,因為數值是可以變化的對吧
model(node,expr,vm){ //node是節點 expr是表達式 vm是實例 // console.log(node) let fn = this.updater['modelUpdater']; //給輸入框加一個觀察者 一旦數據變化,會把新值賦值給輸入框 new Watcher(vm,expr,(newVal)=>{ //newVal 是原生方法中自帶的,是重新獲取的新值 fn(node,newVal); }); node.addEventListener('input',(e)=>{ let value = e.target.value; //獲取用戶輸入的值 this.setValue(vm,expr,value) //設置v-model的值 }) let value = this.getVal(vm,expr); // console.log(value) fn(node,value); },
之后 {{}} 表達式是不是也需要一個啊,因為一個標簽中是可以存在多個表達式的,所以我們要遍歷表達式
列如:<p> {{ shool.name }} {{ shool.name }} </p>
getContentValue(vm,expr){ //遍歷表達式 將內容 重新替換成一個完整的內容 返還回去 return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ return this.getVal(vm,args[1]); }); }, text(node,expr,vm){ let fn = this.updater['textUpdater'] //console.log(expr) :{{ school.name }} let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ //console.log(args) :["{{ school.name }}", " school.name ", 0, "{{ school.name }}"] //給每個表達式 都加上觀察者 new Watcher(vm,args[1],()=>{ fn(node,this.getContentValue(vm,expr)); //返回一個全的字符串 }) return this.getVal(vm,args[1]); }) // console.log(content) // fn(node,content); },
v-html 是不是也需要添加一個啊,當然還有好多,基本都是這樣的一種形式,可以仿照者寫
html(node,expr,vm,eventName){ let fn = this.updater['htmlUpdater'] //給輸入框加一個觀察者 一旦數據變化,會把新值賦值給輸入框 new Watcher(vm,expr,(newVal)=>{ //newVal 是原生方法中自帶的,是重新獲取的新值 fn(node,newVal); }); let value = this.getVal(vm,expr); fn(node,value); },
3、好啦,vue的數據雙向綁定就實現啦,下面是完整的代碼,大家可以在理解理解,運行一下試試吧!
/** * (發布訂閱) 觀察者 被觀察者 */ class Dep{ constructor(){ this.subs = [] //存放所有的 watcher } //訂閱 addSub(watcher){ //添加 watcher this.subs.push(watcher) } //發布 notify(){ this.subs.forEach(watcher=>watcher.updata()); } } /** * 觀察者 */ class Watcher{ constructor(vm,expr,cd){ this.vm = vm; this.expr = expr; this.cd = cd; //存放舊值 this.oldValue = this.get(); } get(){ //獲取舊值 Dep.target = this; //先把自己放到 this 上 //取值時 把這個觀察者和數據關聯起來 let value = CompileUtil.getVal(this.vm,this.expr); Dep.target = null; //不取消 任何值取都會 添加watcher return value; } updata(){ let newVal = CompileUtil.getVal(this.vm,this.expr); //獲取新值 if(newVal !== this.oldValue){ this.cd(newVal) } } } /** * 數據劫持 */ class Observer{ constructor(data) { this.observer(data); } observer(data){ //如果是對象才觀察 if(data && typeof data === 'object'){ for (let key in data) { //循環 data 中的所有子項 this.defineReactive(data,key,data[key]); } } } //實現數據劫持 defineReactive(obj,key,value){ this.observer(value); //如果傳進來的參數是對象,就回調一下這個函數,就是一個遞歸函數 let dep = new Dep(); //給每一個屬性都添加一個具有發布和訂閱的功能 Object.defineProperty(obj,key,{ get(){ //創建watcher時,會獲取到對應的內容 並且把watcher放到了全局上 Dep.target && dep.addSub(Dep.target); return value; }, set: (newVal)=>{ if(value !== newVal){ this.observer(newVal); //給設置的新值也加上 get set 方法 value = newVal; dep.notify(); //執行觀察者更新時的函數 } } }) } } /** * 模板編譯 */ class Compiler{ constructor(el,vm) { this.vm = vm; //判斷el屬性 是不是一個元素 不是就獲取 this.el = this.isElementNode(el) ? el : document.querySelector(el); // console.log(this.el); //把當前的節點元素 獲取到 放到內存中 創建文檔碎片 let fragment = this.node2fragment(this.el); //把節點中的內容進行替換 //模板編譯 用數據編譯 this.compile(fragment); //把內容在塞到頁面中 this.el.appendChild(fragment); } //判斷屬性是不是以 v- 開頭 isDirective(attrName){ return attrName.startsWith('v-'); // return /^v-/.test(attrName) } //編譯元素的 compileElement(node){ let attributes = node.attributes; //類數組, 獲取所有node節點的屬性和屬性值 // console.log(attributes) attributes = [...attributes] // console.log(attributes) attributes.forEach(attr=>{ //是一個屬性對象attr let {name, value:expr} = attr; //:expr是給value起一個別名叫 expr **school.name //判斷是不是vue指令 if(this.isDirective(name)){ // v-model='asdad' v-on:click='dsa' let [,directive] = name.split('-'); let [directiveName, eventName] = directive.split(':'); //需要調用不同的指令來處理 *** v-if v-modle v-show v-else CompileUtil[directiveName](node,expr,this.vm,eventName); } }) } //編譯文本的 compileText(node){ //判斷節點中是否包含 {{}} let content = node.textContent; if(/\{\{.+?\}\}/.test(content)){ CompileUtil['text'](node,content,this.vm); } } //用來編譯內存中的dom節點 核心方法 compile(node){ let childNodes = node.childNodes; //獲取node的所有子節點 [...childNodes].forEach(child=>{ if(this.isElementNode(child)){ //判斷是不是元素節點 this.compileElement(child); //編譯元素指令 //如果是元素節點的話 需要把自己傳不進去 再去遍歷子元素節點 this.compile(child); }else{ //文本元素 this.compileText(child); //編譯文本指令 } }) } //獲取所有元素,放到內存中 node2fragment(node){ //創建一個文檔碎片 let fragment = document.createDocumentFragment(); let firstChild; //將node節點的的第一個節點給firstChild 如果node節點的的第一個節點為空則結束 while(firstChild = node.firstChild){ //appendChild具有移動性 fragment.appendChild(firstChild); } return fragment; } // 是不是元素節點 isElementNode(node){ return node.nodeType === 1; } } CompileUtil = { //根據表達式獲取對應的數據 getVal(vm,expr){ // 7. reduce() 方法 // 函數的參數 (第一參數)1.相加的初始值,2.循環出來的那一項,3.索引 4.循環的數組 // (第二個參數)初始值 // 返回值:總和的結果 expr = expr.trim() return expr.split('.').reduce((data,current)=>{ //[school,name] // console.log(data,current) return data[current]; },vm.$data); }, setValue(vm,expr,value){ expr.split('.').reduce((data,current,index,arr)=>{ //[school,name] if(index === arr.length - 1){ //如果循環到最后一項時執行 //console.log(data,current) //對象賦值是賦值對象的地址 所以更改data會改變相對應的對象 data[current] = value; //將數據從新賦值 school.name = xxxx } return data[current]; },vm.$data); }, model(node,expr,vm){ //node是節點 expr是表達式 vm是實例 // console.log(node) let fn = this.updater['modelUpdater']; //給輸入框加一個觀察者 一旦數據變化,會把新值賦值給輸入框 new Watcher(vm,expr,(newVal)=>{ //newVal 是原生方法中自帶的,是重新獲取的新值 fn(node,newVal); }); node.addEventListener('input',(e)=>{ let value = e.target.value; //獲取用戶輸入的值 this.setValue(vm,expr,value) //設置v-model的值 }) let value = this.getVal(vm,expr); // console.log(value) fn(node,value); }, on(node,expr,vm,eventName){ node.addEventListener(eventName, (e)=>{ //給node添加事件 vm[expr].call(vm,e) }) }, html(node,expr,vm,eventName){ let fn = this.updater['htmlUpdater'] //給輸入框加一個觀察者 一旦數據變化,會把新值賦值給輸入框 new Watcher(vm,expr,(newVal)=>{ //newVal 是原生方法中自帶的,是重新獲取的新值 fn(node,newVal); }); let value = this.getVal(vm,expr); fn(node,value); }, getContentValue(vm,expr){ //遍歷表達式 將內容 重新替換成一個完整的內容 返還回去 return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ return this.getVal(vm,args[1]); }); }, text(node,expr,vm){ let fn = this.updater['textUpdater'] //console.log(expr) :{{ school.name }} let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ //console.log(args) :["{{ school.name }}", " school.name ", 0, "{{ school.name }}"] //給每個表達式 都加上觀察者 new Watcher(vm,args[1],()=>{ fn(node,this.getContentValue(vm,expr)); //返回一個全的字符串 }) return this.getVal(vm,args[1]); }) // console.log(content) // fn(node,content); }, updater: { //把數據插入到value中 modelUpdater(node,value){ node.value = value; }, htmlUpdater(node,value){ node.innerHTML = value; }, //處理文本節點 textUpdater(node,value){ //textContent 屬性設置或返回指定節點的文本內容,以及它的所有后代。 node.textContent = value; } } } class Vue{ constructor(options) { this.$el = options.el; this.$data = options.data; this.computed = options.computed; this.methods = options.methods; //這個根元素存在則編譯模板 if(this.$el){ //數據劫持 把 this.$data 數據全部編譯成 用 Object.defineProperty 來定義 new Observer(this.$data); //把 vm上的數據獲取操作 都代理到 vm.$data 上 this.proxyVm(this.$data); //計算機屬性 for (let key in this.computed) { Object.defineProperty(this.$data,key,{ //有依賴關系 數據 get:()=>{ return this.computed[key].call(this); } }) }; //函數的儲存 for (let key in this.methods) { Object.defineProperty(this,key,{ get: ()=>{ return this.methods[key] } }) } //模板編譯 new Compiler(this.$el,this); } } //給vm.$data 代理 proxyVm(data){ for (let key in data) { //實現了通過vm.xxx 可以取到vm.$data.xxx Object.defineProperty(this,key,{ //this 是 Vue 實例 get:()=>{ //取vm 的值 等於 取vm.$data的值 // console.log(this) return data[key]; // 進行轉換操作 }, set: (newVal)=>{ //設置vm 的值 等於 設置vm.$data的值 data[key] = newVal } }) } } }