实现mvvm

192 阅读2分钟
  1. 实现一个数据监听器Observer,通过递归遍历对象,给对象上所有的属性,包括现有的和新添加的属性与子属性,都加上 setter和getter。这个对象的某个值赋值,就会触发setter,那么就能监听到了数据的变化拿到最新值并通知订阅者。
  2. 实现一个指令解析器Compile,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
  3. 实现一个Watcher,作为连接Observer和Compile的桥梁。主要做的事情是:
    • 在自身实例化时往属性订阅器(dep)里面添加自己。
    • 自身必须有一个update()方法。
    • 待属性变动dep.notify()通知时,能调用自身的update()方法,并触发Compile中绑定的回调。
  4. MVVM作为数据绑定的入口,整合Observer、Compile、Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定。
<!DOCTYPE html>
<html lang="en">

<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Document</title>
</head>

<body>
 <div id="app">
   <input type="text" v-model='name'>
   <p class="ss">{{name}}</p>
   <p>{{obj.a}}</p>
 </div>
</body>

</html>
<script>
 class MVVM {
   constructor(options) {
     let data = this.$data = options.data
     this.$el = options.el
     //将this代理到this.$data
     Object.keys(data).forEach(key => {
       Object.defineProperty(this, key, {
         enumerable: true,
         configurable: false,
         get() {
           return this.$data[key]
         },
         set(newVal) {
           this.$data[key] = newVal
         }
       })
     })
     //数据劫持
     observe(data)
     //指令解析器Compile
     new Compile(this.$el, this)
   }
 }

 function observe(data) {
   if (!data || typeof data !== 'object') return
   Object.keys(data).forEach(key => {
     defineReactive(data, key, data[key])
   })
 }
 function defineReactive(data, key, value) {
   let dep = new Dep()
   //可能data.obj 还是个对象,继续劫持
   observe(value)
   Object.defineProperty(data, key, {
     enumerable: true,
     configurable: false,
     get() {
       //把watcher添加到Dep中
       Dep.target && dep.addSub(Dep.target)
       return value
     },
     set(newVal) {
       if (newVal === value) return
       value = newVal
       //劫持新的对象
       observe(newVal)
       //下发通知
       dep.notify()
     }
   })
 }
 class Compile {
   constructor(el, vm) {
     this.$el = document.querySelector(el)
     this.vm = vm
     //创建文档碎片
     this.$fragment = this.createFragement(this.$el)
     //编译模板
     this.init(this.$fragment)
     //返回渲染
     this.$el.appendChild(this.$fragment)
   }
   createFragement(dom) {
     let fragment = document.createDocumentFragment()
     let child
     while (child = dom.firstChild) {
       fragment.appendChild(child);
     }
     return fragment
   }
   init(fragment) {
     let childNodes = fragment.childNodes;
     Array.from(childNodes).forEach(node => {
       let text = node.textContent
       let reg = /\{\{(.*)\}\}/;
       if (node.nodeType === 1) {
         //元素节点 v-mode
         this.compileElement(node)
       } else if (node.nodeType === 3 && reg.test(text)) {
         //文本节点 {{}}
         this.compileText(node, RegExp.$1)
       }
       //循环遍历子节点
       if (node.childNodes) {
         this.init(node)
       }
     })
   }
   compileElement(node) {
     let attrs = node.attributes
     // console.log(Array.from(attrs));//[type,v-model]
     Array.from(attrs).forEach(attr => {
       let name = attr.name
       let val = attr.value // v-model='name' 取出name
       if (name.indexOf('v-') === 0) {
         node.value = this.vm[val];
       }
       new Watcher(this.vm, val, newVal => node.value = newVal)
       node.addEventListener('input', (e) => {
         let value = e.target.value
         this.vm[val] = value    //给input框设置值 则会调用set。触发Watcher
       })
     })
   }
   compileText(node, exp) {
     //exp: name obj.a
     let arr = exp.split('.')
     let text = arr.reduce((pre, cur) => {
       return pre[cur]
     }, this.vm)
     new Watcher(this.vm, exp, newVal => node.textContent = newVal)
     node.textContent = text
   }
 }

 class Dep {
   constructor() {
     this.subs = []
   }
   addSub(sub) {
     this.subs.push(sub)
   }
   notify() {
     this.subs.forEach(sub => sub.updata())
   }
 }

 function Watcher(vm, exp, cb) {
   this.vm = vm
   this.exp = exp
   this.cb = cb
   Dep.target = this
   //触发get
   let val = vm
   let arr = exp.split('.')
   arr.forEach(key => {
     val = this.vm[key]
   })
   Dep.target = null
 }
 Watcher.prototype.updata = function () {
   let arr = this.exp.split('.')
   let text = arr.reduce((pre, cur) => {
     return pre[cur]
   }, this.vm)
   this.cb(text)
 }

 let vm = new MVVM({
   el: '#app',
   data: {
     name: 'xgw',
     obj: {
       a: '二级'
     }
   }
 })
</script>