當我們對vue的用法較為熟練的時候,但有時候在排查bug的時候還是會有點迷惑。主要是因為對vue各種用法和各種api使用都是只知其然而不知其所以然。這時候我們想到可以去看看源碼,但是源碼太長,其實我們只要把大概實現流程實現一遍,很多開發中想不明白的地方就會豁然開朗。下面我們就來實現一個簡單的vue.js
vue采取數據劫持,配合觀察者模式,通過Object.defineProperty() 來劫持各個屬性的setter和getter,在數據變動時,發布消息給依賴收集器dep,去通知觀察者,做出對應的回調函數,去更新視圖。(也就是在getter中收集依賴,在setter中通知依賴更新。)
其實vue主要就是整合Observer,compile和watcher三者,通過Observer來監聽 model數據變化表,通過compile來解析編譯模板指令,最終利用Watcher搭起observer 和compile的通信橋梁,達到數據變化=>視圖變化,視圖變化=>數據變化的雙向綁定效果。
下面來一張圖↓
這個流程圖已經非常形象深刻的表達了vue的運行模式,當你理解了這個流程,再去看vue源碼時就會容易很多了
聲明一下,下面的代碼只簡單實現了vue里的
- v-model(數據的雙向綁定)
- v-bind/v-on
- v-text/v-html
- 沒有實現虛擬dom,采用文檔碎片(createDocumentFragment)代替
- 數據只劫持了Object,數組Array沒有做處理
代碼大致結構如下,初步定義了6個類
代碼如下,具體操作案例可以看==>GitHub
// 定義Vue類 class Vue { constructor(options) { // 把數據對象掛載到實例上 this.$el = options.el; this.$data = options.data; this.$options = options; // 如果有需要編譯的模板 if (this.$el) { // 數據劫持 就是把對象的所有屬性 改成get和set方法 new Observer(this.$data); // 用數據和元素進行編譯 new Compiler(this.$el, this); // 3. 通過數據代理實現 主要給methods里的方法this直接訪問data this.proxyData(this.$data); } } //用vm代理vm.$data proxyData(data){ for(let key in data){ Object.defineProperty(this,key,{ get(){ return data[key]; }, set(newVal){ data[key] = newVal; } }) } } } // 編譯html模板 class Compiler { // vm就是vue對象 constructor(el, vm) { this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; if(this.el){ // 如果該元素能獲取到,我們開始編譯 // 1.把真實的dom放到內存中fragment文檔碎片 let fragment = this.node2fragment(this.el); // console.log(fragment); // 2.編譯 => 提取想要的元素節點 v-model和文本節點{{}} this.compile(fragment); // 3.把編譯好的fragment再放到頁面里 this.el.appendChild(fragment); } } /* 一些輔助方法 */ isElementNode(node) { return node.nodeType === 1; } isDirective(name) { // 判斷是不是指令 return name.includes('v-'); } isEventName(attrName){ // 判斷是否@開頭 return attrName.startsWith('@'); } isBindName(attrName){ // 判斷是否:開頭 return attrName.startsWith(':'); } /* 核心方法區 */ node2fragment(el){ // 需要將el中的內容全部放到內存中 // 文檔碎片 let fragment = document.createDocumentFragment(); let firstChild; while(firstChild = el.firstChild){ fragment.appendChild(firstChild); } return fragment; // 內存中的節點 } compile(fragment){ // 1.獲取子節點 let childNodes = fragment.childNodes; // 2.遞歸循環編譯 [...childNodes].forEach(node=>{ if(this.isElementNode(node)){ this.compileElement(node); // 這里需要編譯元素 this.compile(node); // 是元素節點,還需要繼續深入的檢查 }else{ // 文本節點 // 這里需要編譯文本 this.compileText(node); } }); } compileElement(node){ // 編譯元素 // 帶v-model v-html ... let attrs = node.attributes; // 取出當前節點的屬性 // attrs是類數組,因此需要先轉數組 [...attrs].forEach(attr=>{ // console.log(attr); // type="text" v-model="content" v-on:click="handleclick" @click=""... let attrName = attr.name; // type v-model v-on:click @click if(this.isDirective(attrName)){ // 判斷屬性名字是不是包含v- // 取到對應的值放到節點中 let expr = attr.value; // content/變量 handleclick/方法名 // console.log(expr) let [, type] = attrName.split('-'); // model html on:click let [compileKey, detailStr] = type.split(':'); // 處理 on: bind: // node this.vm.$data expr CompileUtil[compileKey](node, this.vm, expr, detailStr); // 刪除有指令的標簽屬性 v-text v-html等,普通的value等原生html標簽不必刪除 node.removeAttribute('v-' + type); }else if(this.isEventName(attrName)){ // 如果是事件處理 @click='handleClick' let [, detailStr] = attrName.split('@'); CompileUtil['on'](node, this.vm, attr.value, detailStr); // 刪除有指令的標簽屬性 node.removeAttribute('@' + detailStr); }else if(this.isBindName(attrName)){ // 如果是:開頭,動態綁定值 let [, detailStr] = attrName.split(':'); CompileUtil['bind'](node, this.vm, attr.value, detailStr); // 刪除有指令的標簽屬性 node.removeAttribute(':' + detailStr); } }) } compileText(node){ // 編譯文本 // 帶{{}} let expr = node.textContent; // 取文本中的內容 let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} if(reg.test(expr)){ // node this.$data // console.log(expr); // {{content}} CompileUtil['text'](node, this.vm, expr); } } } // 編譯模版具體執行 const CompileUtil = { getVal(vm, expr){ // 獲取實例上對應的數據 expr = expr.split('.'); // [animal,dog]/[animal,cat] return expr.reduce((prev, next)=>{ // vm.$data. return prev[next]; }, vm.$data) }, // 這里實現input輸入值變化時 修改綁定的v-model對應的值 setVal(vm, expr, inputValue){ // [animal,dog] let exprs = expr.split('.'), len = exprs.length; exprs.reduce((data,currentVal, idx)=>{ if(idx===len-1){ data[currentVal] = inputValue; }else{ return data[currentVal] } }, vm.$data) }, getTextVal(vm, expr){ // 獲取編譯文本后的結果 return expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{ // console.log(args); // ["{{title}}", "title", 0, "{{title}}"] // ["{{ animal.dog }}", " animal.dog ", 0, "{{ animal.dog }}-vs-{{ animal.cat }}"] return this.getVal(vm, args[1].trim()); }); }, text(node, vm, expr){ // 文本處理 let updateFn = this.updater['textUpdater']; // {{content}} => "welcome to animal world" let value; if(expr.indexOf('{{')!==-1){ // dom里直接寫{{}}的時候 value = this.getTextVal(vm, expr); // {{a}} {{b}} 對多個值進行監控 expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{ new Watcher(vm, args[1].trim(), ()=>{ // 如果數據變化了,文本節點需要重新獲取依賴的屬性更新文本中的內容 updateFn && updateFn(node, this.getTextVal(vm, expr)); }) }); }else{ // v-text 的時候 value = this.getVal(vm, expr); new Watcher(vm, expr, (newVal)=>{ // 當值變化后會調用cb 將新值傳遞過來 updateFn && updateFn(node, newVal); }); } updateFn && updateFn(node, value); }, html(node, vm, expr) { // let updateFn = this.updater['htmlUpdater']; updateFn && updateFn(node, this.getVal(vm, expr)); }, model(node, vm, expr){ // 輸入框處理 let updateFn = this.updater['modelUpdater']; // console.log(this.getVal(vm, expr)); // "welcome to animal world" // 這里應該加一個監控 數據變化了 應該調用這個watch的callback new Watcher(vm, expr, (newVal)=>{ // 當值變化后會調用cb 將新值傳遞過來 updateFn && updateFn(node, newVal); }); // 視圖 => 數據 => 視圖 node.addEventListener('input', (e)=>{ this.setVal(vm, expr, e.target.value); }) updateFn && updateFn(node, this.getVal(vm, expr)); }, on(node, vm, expr, detailStr) { let fn = vm.$options.methods && vm.$options.methods[expr]; node.addEventListener(detailStr, fn.bind(vm), false); }, bind(node, vm, expr, detailStr){ // v-bind:src='...' => href='...' node.setAttribute(detailStr, expr); }, updater:{ // 文本更新 textUpdater(node, value){ node.textContent = value; }, // html更新 htmlUpdater(node, value){ node.innerHTML = value; }, // 輸入框更新 modelUpdater(node, value){ node.value = value; } } } // 觀察者 class Observer{ constructor(data){ this.observe(data); } observe(data){ // 要對data數據原有屬性改成set和get的形式 if(!data || typeof data !== 'object'){ // 不是對象就不劫持了 return } // 要劫持 先獲取到data的key和value Object.keys(data).forEach(key=>{ this.defineReactive(data, key, data[key]); // 劫持 this.observe(data[key]); // 深度遞歸劫持 }) } // 定義響應式 defineReactive(obj, key, value){ let dep = new Dep(); // 在獲取某個值的時候 Object.defineProperty(obj, key, { enumerable: true, // 可枚舉 configurable: true, // 可修改 get(){ // 當取值的時候 // 訂閱數據變化時,往Dev中添加觀察者 Dep.target && dep.addSub(Dep.target); return value; }, // 采用箭頭函數在定義時綁定this的定義域 set: (newVal)=>{ // 更改data里的屬性值的時候 if(value === newVal) return; this.observe(newVal); // 如果設置新值是對象,劫持 value = newVal; // 通知watcher數據發生改變 dep.notify(); } }) } } // 觀察者的目的就是給需要變化的那個元素增加一個觀察者,當數據變化后執行對應的方法 class Watcher{ constructor(vm, expr, cb) { this.vm = vm; this.expr = expr; this.cb = cb; // 先獲取一下老的值 this.oldVal = this.getOldVal(); } // 獲取實例上對應的老值 getOldVal(){ // 在利用getValue獲取數據調用getter()方法時先把當前觀察者掛載 Dep.target = this; const oldVal = CompileUtil.getVal(this.vm, this.expr); // 掛載完畢需要注銷,防止重復掛載 (數據一更新就會掛載) Dep.target = null; return oldVal; } // 對外暴露的方法 通過回調函數更新數據 update(){ const newVal = CompileUtil.getVal(this.vm, this.expr); if(newVal !== this.oldVal){ this.cb(newVal); // 對應watch的callback } } } // Dep類存儲watcher對象,並在數據變化時通知watcher class Dep{ constructor(arg) { // 訂閱的數組 this.subs = [] } addSub(watcher){ this.subs.push(watcher); } notify(){ // 數據變化時通知watcher更新 this.subs.forEach(w=>w.update()); } }