最近手癢,固然也是爲了近階段的跳槽作準備,利用週五時光,仿照vue用法,實現一下mvvm的雙向綁定、數據代理、大鬍子{{}}模板、指令v-on,v-bind等。固然因爲時間緊迫,裏面的編碼細節沒有作優化,還請各位看官多多包涵!看招:html
_observe(obj){ // 遞歸遍歷 // let value; for (const key in obj) { let value; if (obj.hasOwnProperty(key)){ // 利用原理 劫持數據---發佈訂閱 value = obj[key]; if (typeof value === 'object') { console.log('value', value) this._observe(value) } // 訂閱(key)數據 if (!this._binding[key]) {this._binding[key]= []}; let binding = this._binding[key] // 重寫getter, setter Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { return value }, set(newVal) { if (value === newVal) return false; value = newVal console.log('newvalue', value) // 主要value更新,就發佈通知(監聽這個key的全部的)watcher更新(改變dom) binding.forEach(watcher => { console.log('watcher', watcher) watcher.update() }); } }) } } }
_proxyData(data, vm) { // data身上的全部屬性所有掛載到vm實例上 for (const key in data) { // let val = data[key]; // ctx.key = val; Object.defineProperty(vm, key, { configurable: true, enumerable: true, get() { return data[key]; }, set(newVal) { data[key] = newVal; vm._observe(newVal) } }) } }
_compile(root){ // 獲取全部節點 let nodes = root.childNodes // 遞歸編譯 Array.from(nodes).forEach(node => { // 針對每個節點進行處理 // 元素節點 if (node.nodeType === 1) {//只考慮綁定了一個指令 // 獲取節點的屬性集合 const attributes = Array.from(node.attributes); // 指令進行編譯 if (hasDirective(attributes, 'v-bind')) { const attrVal = getDirectiveValue(node, attributes, 'v-bind'); const exp = getDirectiveParams(attributes, 'v-bind'); // const node.setAttribute(exp, this.$data[attrVal]) this._binding[attrVal].push(new watcher({ vm: this, el: node, exp, attr: attrVal })) } if (hasDirective(attributes, 'v-on')) { const eventName = getDirectiveParams(attributes, 'v-on'); node.addEventListener(eventName, (e) => { this.$methods[getDirectiveValue(node, attributes, 'v-on')].call(this) }) } if (node.hasAttribute('v-model') && node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') { let attrVal = node.getAttribute('v-model'); this._binding[attrVal].push(new Watcher({ vm: this, el: node, attr: attrVal, name: 'v-model' })) node.addEventListener('input', e=> { this.$data[attrVal] = node.value; }) node.value = this.$data[attrVal] } // 遞歸接着處理 if (node.hasChildNodes()) { this._compile(node) } } // 文本節點 if (node.nodeType === 3) { let text = node.textContent; let keyArr = []; // 獲取{{變量}},用正則去匹配;watcher去觀察{{變量}}(包裹元素), let newText = text.replace(/\{\{(\w+)\}\}/g, (match, p0)=> { keyArr = [...keyArr, p0]; // 替換屬性爲真正的屬性值 return this.$data[p0] }) node.textContent = newText; // 把整個文本節點進行監控{{v1}}-----{{v2}};添加到訂閱到數組裏等待通知 keyArr.forEach(key => { // !this._binding[key] && (this._binding[key] = []) this._binding[key].push(new Watcher({ vm: this, el: node, attr: text })) }) } }) }
class Watcher { constructor({ vm, name, el, exp, attr, }) { this.vm = vm; this.el = el; this.name = name; this.exp = exp; this.attr = attr; } // 更新text,或更新屬性 update() { // 改變節點的屬性 if (this.el.nodeType === 1) { // this.el.value = this.vm.$data[this.exp] if (this.name === 'v-model') { console.log('value', this.el) this.el.value = this.vm.$data[this.attr] } this.el[this.attr] = this.vm.$data[this.exp] } // 文本節點 else { let text = this.attr; // 獲取{{變量}},用正則去匹配;watcher去觀察{{變量}}(包裹元素), let newText = text.replace(/\{\{(\w+)\}\}/g, (match, p0)=> { // 替換屬性爲真正的屬性值 return this.vm.$data[p0] }) this.el.textContent = newText; } } }
<html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>vue實現</title> <style> </style> </head> <body> <div id="app"> <!-- <form> --> <h2>{{title}}</h2> <p>{{text}}</p> <input type="text" v-model="number"> <button v-on:click="increase">增長</button> <button v-on:click="decrease">減小</button> <!-- </form> --> <p>我點的時候就會變化{{number}}---{{number}}</h2> </div> <script> // 構造watcher類,用來觀察數據變化來(本質更新dom的指令屬性或innertext) /** * vm: vue實例 * name: 指令名 * el: 節點 * exp: 指令對應的參數 * attr: 指令值(綁定的屬性名) **/ class Watcher { constructor({ vm, name, el, exp, attr, }) { this.vm = vm; this.el = el; this.name = name; this.exp = exp; this.attr = attr; } // 更新text,或更新屬性 update() { // 改變節點的屬性 if (this.el.nodeType === 1) { // this.el.value = this.vm.$data[this.exp] if (this.name === 'v-model') { console.log('value', this.el) this.el.value = this.vm.$data[this.attr] } this.el[this.attr] = this.vm.$data[this.exp] } // 文本節點 else { let text = this.attr; // 獲取{{變量}},用正則去匹配;watcher去觀察{{變量}}(包裹元素), let newText = text.replace(/\{\{(\w+)\}\}/g, (match, p0)=> { // 替換屬性爲真正的屬性值 return this.vm.$data[p0] }) this.el.textContent = newText; } } } function hasDirective(attrs, dir) { return attrs.some(attr => attr.name.indexOf(dir) !== -1) } function getDirectiveParams(attrs, dir) { dir = attrs.find(attr => attr.name.indexOf(dir) !== -1).name return dir.split(':')[1] ? dir.split(':')[1].split('.')[0] : ''; } function getDirectiveValue(node, attrs, dir) { return attrs.find(attr => attr.name.indexOf(dir) !== -1).value; } class DuVue { constructor(options) { this._init(options); } _init(options) { this.$options = options this.$data = options.data this.$methods = options.methods this.$el = document.querySelector(options.el) this._binding = {} this._observe(this.$data) // 代理全部數據 this._proxyData(this.$data, this) this._compile(this.$el) } _observe(obj){ // 遞歸遍歷 // let value; for (const key in obj) { let value; if (obj.hasOwnProperty(key)){ // 利用原理 劫持數據---發佈訂閱 value = obj[key]; if (typeof value === 'object') { console.log('value', value) this._observe(value) } // 訂閱(key)數據 if (!this._binding[key]) {this._binding[key]= []}; let binding = this._binding[key] // 重寫getter, setter Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { return value }, set(newVal) { if (value === newVal) return false; value = newVal console.log('newvalue', value) // 主要value更新,就發佈通知(監聽這個key的全部的)watcher更新(改變dom) binding.forEach(watcher => { console.log('watcher', watcher) watcher.update() }); } }) } } } // 實例代理數據 _proxyData(data, vm) { // data身上的全部屬性所有掛載到vm實例上 for (const key in data) { // let val = data[key]; // ctx.key = val; Object.defineProperty(vm, key, { configurable: true, enumerable: true, get() { return data[key]; }, set(newVal) { data[key] = newVal; vm._observe(newVal) } }) } } _compile(root){ // 獲取全部節點 let nodes = root.childNodes // 遞歸編譯 Array.from(nodes).forEach(node => { // 針對每個節點進行處理 // 元素節點 if (node.nodeType === 1) {//只考慮綁定了一個指令 // 獲取節點的屬性集合 const attributes = Array.from(node.attributes); // 指令進行編譯 if (hasDirective(attributes, 'v-bind')) { const attrVal = getDirectiveValue(node, attributes, 'v-bind'); const exp = getDirectiveParams(attributes, 'v-bind'); // const node.setAttribute(exp, this.$data[attrVal]) this._binding[attrVal].push(new watcher({ vm: this, el: node, exp, attr: attrVal })) } if (hasDirective(attributes, 'v-on')) { const eventName = getDirectiveParams(attributes, 'v-on'); node.addEventListener(eventName, (e) => { this.$methods[getDirectiveValue(node, attributes, 'v-on')].call(this) }) } if (node.hasAttribute('v-model') && node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') { let attrVal = node.getAttribute('v-model'); this._binding[attrVal].push(new Watcher({ vm: this, el: node, attr: attrVal, name: 'v-model' })) node.addEventListener('input', e=> { this.$data[attrVal] = node.value; }) node.value = this.$data[attrVal] } // 遞歸接着處理 if (node.hasChildNodes()) { this._compile(node) } } // 文本節點 if (node.nodeType === 3) { let text = node.textContent; let keyArr = []; // 獲取{{變量}},用正則去匹配;watcher去觀察{{變量}}(包裹元素), let newText = text.replace(/\{\{(\w+)\}\}/g, (match, p0)=> { keyArr = [...keyArr, p0]; // 替換屬性爲真正的屬性值 return this.$data[p0] }) node.textContent = newText; // 把整個文本節點進行監控{{v1}}-----{{v2}};添加到訂閱到數組裏等待通知 keyArr.forEach(key => { // !this._binding[key] && (this._binding[key] = []) this._binding[key].push(new Watcher({ vm: this, el: node, attr: text })) }) } }) } } window.onload = function(){ var duVue = new DuVue({ el: '#app', data: { number: 0, title: '手寫vue', text: '用到es6 class', obj: {a:1} }, methods: { increase() { console.log('click-increase') this.number++ }, decrease() { this.number-- } } }) console.log(duVue) } </script> </body> </html>