MVVM基本實現

MVVM基本實現

來自專欄 ELSE5 人贊了文章

MVVM基本實現

Object.defineProperty

function MVVM (options = {}) { this.$options = options; this.$methods = options.methods let data = this._data = this.$options.data observe(data) // 實現數據的監聽 new Compile(options.el, this) // 編譯 } function observe (data) { if (!data || typeof data !== object) return return new Observer(data) } function Observer (data) { let dep = new Dep() Object.keys(data).forEach(key => { let val = data[key] observe(val) // key對應的值進行遞歸檢查,如果值是object類型則再次對key進行監聽 Object.defineProperty(data, key, { configurable: false, enumerable: true, get () { Dep.target && dep.addSub(Dep.target) 1?? 添加訂閱器, dep為訂閱器的容器(即數組) return val }, set (newVal) { if (val === newVal) return val = newVal observe(newVal) // 賦值後需要對新值再次監聽 dep.notify() // 2?? 當對key重新賦值後,需要發布訂閱, 通知使用此key的小夥伴們一起變 } } } } 1??處解釋:對象訪問內部key時, 會觸發get的執行(即1??處),每次訪問key都會觸發, 所以會在此處添加訂閱器,以便當key對應的value發生改變時發布訂閱器, 對於每一個key都會擁有一個訂閱器,但每次訪問key都會觸發get的執行, 是不是會對同一個key添加多個訂閱器呢?為了避免此情況的出現, 我們會在初始化時為其執行一邊key的訪問,並且在此時添加訂閱器, 此後對於key的訪問將過濾掉訂閱器的添加,所以①處添加訂閱器前進行判斷真假

訂閱器 容器

function Dep () { this.subs = [] //容器,存儲訂閱器 } Dep.prototype = { addSub (sub) { this.subs.push(sub) }, notify() { //2??處對應,通知大家我已經變了奧 this.subs.forEach(sub => sub.update()) } }

編譯

model層已經有了相互關聯,該如何使view和model產生關聯呢? function Compile (el, vm) { vm.$el = document.querySelector(el) let fragment = this.createFragment(vm.$el) this.compileContent(fragment, vm) // 核心 vm.$el.appendChild(fragment) } Compile.prototype = { createFragment: function (el) { let child = el.firstChild let fragment = document.createDocumentFragment() while (child) { fragment.appendChild(child) child = el.firstChild } return fragment }, compileContent: function (frag, vm) { // 正式編譯view內容 let _this = this Array.from(frag.childNodes).forEach(node => { let txt = node.textContent let reg = /{{(s*[a-zA-Z_]+(w|.)*s*)}}/g if (node.childNodes && node.childNodes.length) { this.compileContent(node, vm) } if (node.nodeType === 3 && reg.test(txt)) { //判斷節點是否為文本節點 let arr = txt.match(reg) this.compileText(node, vm, arr) arr.forEach(item => { new Watcher(vm, item, () => {_this.compileText(node, vm, arr)}) }) } if (node.nodeType === 1) { // 是否文dom節點 let attrs = node.attributes Array.from(attrs).forEach(attr => { let attrName = attr.name let attrValue = attr.value if (this.isDirective(attrName)) { let type = attrName.substring(2) if (this.isEvent(type)) { // 事件指令 將執行methods中的方法 let ev = type.substring(3) this.handleEvent(ev, node, vm, attrValue) } else { // 非事件指令 具體是model還是其他指令在handleOtherDirective詳細處理 this.handleOtherDirective(type, node, vm, attrValue) } } }) } }) }, handleEvent: function (ev, node, vm, attrValue) { vm.$methods && vm.$methods[attrValue] && node.addEventListener(ev, vm.$methods[attrValue].bind(vm), false) }, handleOtherDirective: function (type, node, vm, attrValue) { Object.keys(compileUtil).forEach(key => { if (key === type) { compileUtil[type](node, vm, attrValue) } }) }, isDirective: function (attrName) { return attrName.indexOf(v-) === 0 }, isEvent: function (type) { return type.indexOf(on:) === 0 }, compileText: function (node, vm, arr) { let content = arr.forEach(exp => { let reg = /{{(s*[a-zA-Z_]+(w|.)*s*)}}/g let keys = reg.exec(exp)[1] let val = this.compileKeys(keys, vm) content += val }) node.textContent = content }, compileKeys: function (keys, vm) { let data = vm._data let key_arr = keys.split(.) key_arr.forEach(key => { data = data[key] }) return data } } view層的處理主要是對{{}}和指令的處理;而對於指令則需要按照不同指令進行不同處理, 如v-model, v-on...const compileUtil = { compileText: function (node, vm, attrValue) { let keys = attrValue.split(.) let data = vm._data keys.forEach(key => { data = data[key] }) node.textContent = data }, model: function (node, vm, attrValue) { let data = vm._data let _this = this let keys = attrValue.split(.) keys.forEach(key => { data = data[key] }) node.value = data node.addEventListener(input, function (e) { let value = e.target.value if (value === data) return _this.setVal(vm, value, keys) }, false) new Watcher(vm, {{ + attrValue + }}, () => { let data = vm._data let _this = this let keys = attrValue.split(.) keys.forEach(key => { data = data[key] }) node.value = data }) }, setVal: function (vm, value, keys) { if (keys.length === 1) { vm[keys[0]] = value } else { for (let i = 0; i < keys.length; i++) { this.setVal(vm[keys[i]], value, keys.slice(i + 1)) } } }}

訂閱器

function Watcher (vm, keys, cb) { this.cb = cb this.vm = vm this.keys = keys Dep.target = this let reg = /{{(s*[a-zA-Z_]+(w|.)*s*)}}/g keys = reg.exec(keys)[1] let arr = keys.split(.) let data = vm arr.forEach(key => { data = data[key] // 觸發訂閱器的添加 }) Dep.target = null}Watcher.prototype.update = function () { let reg = /{{(s*[a-zA-Z_]+(w|.)*s*)}}/g let arr = reg.exec(this.keys)[1].split(.) let val = this.vm arr.forEach(key => { val = val[key] }) this.cb() // 數據變化後調用回調函數重新賦值}

推薦閱讀:

三角形的 N 種畫法與瀏覽器的開放世界
探索Virtual DOM的前世今生
一看就暈的React事件機制
ELSE 技術周刊(2018.05.07期)
回答在職前端的疑問:平時工作是主抓業務還是主抓技術?

TAG:前端開發 | 前端學習 | MVVM |