手写一个MVVM
最近看了珠峰的架構課——實現一個MVVM。
首先,我們來了解一下什么是MVVM。
MVVM是Model-View-ViewModel的簡寫。它本質上就是MVC 的改進版。MVVM 就是將其中的View 的狀態和行為抽象化,讓我們將視圖 UI 和業務邏輯分開。當然這些事 ViewModel 已經幫我們做了,它可以取出 Model 的數據同時幫忙處理 View 中由于需要展示內容而涉及的業務邏輯。
先貼一下代碼,然后再做分析。
<!DOCTYPE html> <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>myMVVM</title> </head> <body><div id="root"><input type="text" v-model='person.name' ><h1>{{message}}</h1><ul><li>1</li><li>2</li></ul>{{person.age}}<br /><button v-on:click="change">點我</button><br />{{getNewName}}<div v-html="message"></div></div><!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> --><script src="Vue.js"></script><script>let vm = new Vue({el: '#root',data: {message: 'she is a bad girl',person: {age: 20,name: 'zhangsan'}},computed: {getNewName() {return this.person.name + 'hahaha'}},methods: {change() {console.log("11111")}}})</script> </body> </html>MVVM的簡易實現(還有很多功能沒有涉及到)
/*** 訂閱發布 調度中心*/ class Dep {constructor() {this.subs = [] // 存放所有的watcher }// 添加watcher, 訂閱 addSub(watcher) {this.subs.push(watcher)}// 發布 notify() {this.subs.forEach(watcher => watcher.update())} }/*** 觀察者模式 觀察者,被觀察者*/ class Watcher {constructor(vm, expr, cb) {this.vm = vmthis.expr = exprthis.cb = cb// 默認先存放舊值this.oldValue = this.get(vm, expr)}// 獲取舊的值 get() {Dep.target = thislet value = CompileUtil.getVal(this.vm, this.expr)Dep.target = nullreturn value}// 數據更新 update() {let newVal = CompileUtil.getVal(this.vm, this.expr)if(newVal != this.oldValue) {this.cb(newVal)}} }/*** 實現數據劫持*/ class Observer {constructor(data) {this.observe(data)}observe(data) {if(data && typeof data == 'object') {for(let key in data) {this.defineReactive(data, key, data[key])}}}defineReactive(obj, key, value) {this.observe(value)let dep = new Dep() // 給每一個屬性都加上一個具有發布訂閱的功能 Object.defineProperty(obj, key, {get() {Dep.target && dep.addSub(Dep.target)return value},set: (newValue) => {if(newValue != value) {this.observe(newValue)value = newValuedep.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) {// startsWith('v-') 或 split('-')return attrName.indexOf('v-') !== -1}// 編譯元素 compileElement(node) {let attributes = node.attributes// [...attributes] 或 Array.from 等價 Array.prototype.slice.callArray.from(attributes).forEach(attr => {let { name, value: expr } = attrif(this.isDirective(name)) {// let [, directive] = name.split('-')// console.log(name,node, expr, directive)if(directive.indexOf(':' !== -1)) {let [directiveName, eventName] = directive.split(':')CompileUtil[directiveName](node, expr, this.vm, eventName)} else {CompileUtil[directive](node, expr, this.vm)}}})}// 編譯文本 找{{}} compileText(node) {let content = node.textContentif(/\{\{(.+?)\}\}/.test(content)) {// console.log(content) // 找到所有文本CompileUtil['text'](node, content, this.vm)}}// 編譯內存中的dom節點 核心的編譯方法 compile(node) {let childNodes = node.childNodesArray.from(childNodes).forEach(child => {if(this.isElementNode(child)) {this.compileElement(child)// 如果是元素的話,需要除去自己,再去遍歷子節點this.compile(child)} else {this.compileText(child)}})}// 把節點移動到內存中 appendChild方法 node2fragment(node) {let fragment = document.createDocumentFragment()let firstChildwhile(firstChild = node.firstChild) {fragment.appendChild(firstChild)}return fragment}// 判斷是否為元素節點 isElementNode(node) {return node.nodeType === 1} }/*** 編譯工具函數*/ CompileUtil = {// 根據表達式取對應的數據 getVal(vm, expr) {return expr.split('.').reduce((data, current) => {return data[current]}, vm.$data)},getContentVal(vm, expr) {return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {return this.getVal(vm, args[1])})},setVal(vm, expr, value) {expr.split('.').reduce((data, current, index, arr) => {if(index === arr.length - 1) {return data[current] = value}return data[current]}, vm.$data)},// 解析v-model指令 model(node, expr, vm) {// node.valuelet fn = this.updater['modelUpdater']new Watcher(vm, expr, (newVal) => { // 給輸入框加一個觀察者,稍后數據更新,觸發此方法,用新值給輸入框賦值 fn(node, newVal)})node.addEventListener('input', e => {let value = e.target.valuethis.setVal(vm, expr, value)})let value = this.getVal(vm, expr)fn(node, value)},on(node, expr, vm, eventName) {node.addEventListener(eventName, e => {vm[expr].call(vm, e)})},text(node, expr, vm) {let fn = this.updater['textUpdater']let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {// 給表達式每個變量都加上觀察者new Watcher(vm, args[1], () => {fn(node, this.getContentVal(vm, expr)) // 返回一個全的字符串 })return this.getVal(vm, args[1])})fn(node, content)},html(node, expr, vm) {// node.innerHTMLlet fn = this.updater['htmlUpdater']new Watcher(vm, expr, (newVal) => { // 給輸入框加一個觀察者,稍后數據更新,觸發此方法,用新值給輸入框賦值 fn(node, newVal)})let value = this.getVal(vm, expr)fn(node, value)},updater: {modelUpdater(node, value) {node.value = value},textUpdater(node, value) {node.textContent = value},htmlUpdater(node, value) {node.innerHTML = value}} }/*** Vue構造函數*/ class Vue {constructor(options) {this.$el = options.elthis.$data = options.datalet computed = options.computedlet methods = options.methodsif(this.$el) {// 做數據劫持new Observer(this.$data)// console.log(this.$data)for(let key in computed) { // 依賴關系Object.defineProperty(this.$data, key, {get: () => {return computed[key].call(this)}})}for(let key in methods) {Object.defineProperty(this, key, {get() {return methods[key]}})}// vm上的取值操作都代理上vm.$data上this.proxyVm(this.$data)// 編譯模板new Compiler(this.$el, this)}}// 代理 proxyVm(data) {for(let key in data) {Object.defineProperty(this, key, {get() {return data[key]},set(newVal) {data[key] = newVal}})}} }未完待續......
轉載于:https://www.cnblogs.com/zyl-Tara/p/11263687.html
總結
- 上一篇: 怎么解决64位Access与32位不能同
- 下一篇: Es聚合查询