基于Proxy实现一个极简MVVM

761 阅读5分钟

MMVM模式并不是很新的东西,怎么实现网上也是有一大堆文章,虽然最后都能实现,但是过程感觉还是比较复杂,不够简练,为了便于自己更好理解,基于现有的一些技术手段,结合MVVM的思想,自己从0到1实现了这样一个功能,接下来将呈现我的整个一个思考过程

关于MVVM(Model-View-ViewModel)的概念这里就不做赘述了,相信用过Vue以及React的同学都已经非常熟悉,那么我们直接进入正题

视图模板

视图模板对应View视图层,是一个包含了一些指令信息的一个视图呈现,里面不处理任何逻辑,参考Vue,写下这样一个模板,以及实例化操作

<div id="mvvm">
    <input type="text" y-model="msg">
    <p>{{msg}}</p>
    <button y-click="handleClik">点我</button>
 </div>
 
 const vm = new Mvvm({
  el: '#mvvm',
  data: {
    msg: 'hello world!'
  },
  methods: {
    handleClik() {
      this.msg = 'hahhahahha'
    }
  },
})
 

这里是一个逆向的逻辑,首先我们先想象自己想要的一个模板呈现,可以是字符串、html节点、jsx等,这里我们用最简单的html来呈现,后面就是基于模板上的一些特殊指令(这里是带有特殊标识的字符串),这里我们需要实现如下功能,y-model这个指令绑定的值要响应式的呈现在input框中以及在进行输入交互时要更新到Model数据层、{{msg}}需要在数据更新时同步到p标签的文本节点,y-click这个指令标识在点击button时触发绑定的函数

数据模板

接下来是数据层(Model)的实现,数据的操作无非增删改查,这里我们主要是对取值(查)以及赋值(改)操作进行处理,我们希望在做这两个操作的时候自动的进行一些事件处理程序,而不是每次手动执行,那么就需要的这两个操作进行代理或者拦截,es5的实现方式是用过Object.defineProperty这个方法,那么在es6提供了Proxy方法,这里我们选择Proxy来实现

这里我们声明一个叫做Observer的类,传入我们的数据对象,并返回数据对象的代理

   class Observer {
      constructor (data) {

        this._data = new Proxy(data, {
          get: function(target, propkey) {
            return Reflect.get(target, propkey)
          },
          set: function(target, propkey, value) {
            Reflect.set(target, propkey, value)
            return true
          }
        })

      }
    }

这里用到了es6的Reflect将取值与赋值改写为函数行为

因为我们在之前的模板中同时在input和p中都绑定了msg这个变量,我们在输入框改变msg时希望p中的msg也能够改变,这就好比msg被观察了,任何关于msg的变动所有观察者都能够接收响应并执行对应操作,说简单的就是我们需要实现一个观察者模式来处理这种情况,关于观察者的实现网上也有很详细的解释,这里我们就直接使用

 class Dep {
  constructor() {
    this.subs = []
  }

  add(sub) {
    this.subs.push(sub)
  }

  notify() {
    this.subs.forEach(sub => sub.update())
  }
}

class Watcher {
  constructor(vm,cb) {
    this.vm = vm
    this.cb = cb

    cb.call(vm)
  }

  update() {
    this.cb.call(this.vm)
  }

}

这里说明一下,由于我们所有的数据处理都是跟实例挂钩的,所以我们传入一个上下文对象vm来指向实例,以便于内部的函数执行能正确访问到对应变量

那么我们的Observer现在要实例化一个观察者并添加到vm对象上

class Observer {
  constructor (data, vm) {

    vm.dep = new Dep()

    vm._data = new Proxy(data, {
      get: function(target, propkey) {
        return Reflect.get(target, propkey)
      },
      set: function(target, propkey, value) {
        Reflect.set(target, propkey, value)
        vm.dep.notify()
        return true
      }
    })

  }
}

现在,我们的vm对象上添加了_data这个被代理的数据对象以及一个dep对象,里面保存着_data的观察者

视图数据层

接下来是viewModel层的实现,这里要实现的逻辑是处理传入的视图模板,解析模板指令,并绑定数据同时需要实现我们一开始写模板时想要实现的一些功能,这里我们也用类来实现,叫做Compile

class Compile {
  constructor(el, vm) {
    this.el = el
    this.vm = vm
    this.init()
  }

  init() {
    
    [].slice.call(this.el.children).forEach(node => {
      let text = node.textContent
      let reg = /\{\{(.*)\}\}/

      if (reg.test(text)) {
        let key = reg.exec(text)[1]
        this.vm.dep.add(new Watcher(this.vm, () => node.innerText = this.vm[key]))     
      }

      if (node.getAttribute('y-model')) {
        let modelKey = node.getAttribute('y-model')

        node.addEventListener('input', e => {
          this.vm[modelKey] = e.target.value || ''
        })
        this.vm.dep.add( new Watcher(this.vm, () => node.value = this.vm[modelKey] ) )
        node.removeAttribute('y-model')
      }

      if (node.getAttribute('y-click')) {
        let eventKey = node.getAttribute('y-click')

        node.addEventListener('click', this.vm[eventKey].bind(this.vm), false)

        node.removeAttribute('y-click')
      }
    })
  }
}

这里我们需要传入一个元素,也就是我们一开始模板里的id="mvvm"这个元素对象,并对其包含的子元素进行了遍历处理,在init这个方法里,我们分别对我们一开始先要实现的功能进行了处理,这里需要注意的地方就是我们在梳理{{msg}}以及y-model这两个指令分别实例化了Watcher并传入的需要执行的事件处理程序,并在Observer阶段添加的dep上添加了Watcher示例,这样在进行赋值操作的时候,所有绑定了对应变量的地方都会响应到变化并更新视图

结合Dep、Watcher、Observer、Compile这四个类以及可以基本实现我们一开始想要的功能了,接下来我们开始使用

const Mvvm = function(options) {
  this.$options = options
  const el = document.querySelector(this.$options.el)
  
  new Observer(this.$options.data, this)

  new Compile(el, this)
}

这里我们声明一个构造函数,传入基本配置,这样基本已经可以使用了,但是请注意我们在Compile传入的vm对象,在这里是构造函数的this,我们在init的方法中调用变量的时候都是直接在this这个对象上查找的,但实际我们传入的this对象上并不一定都有这些变量,我们把传入的配置添加到了this.$options上,dep还是直接添加在了this上。所以并不是Compile所有的变量都能在this上直接访问到,如果先要实现上面这种写法,那么我们需要在Compile示例化之前再做一层处理

此处实现有两种方式,Object.defineProperty和Proxy,因为我们是一个基于Proxy的实现,还是用Proxy来实现,我们的Mvvm构造函数相应变成

const Mvvm = function(options) {
  this.$options = options
  const el = document.querySelector(this.$options.el)

  const vm = new Proxy(this, {
    get(target, propKey) {
      return Reflect.get(Reflect.has(target, propKey) ? target : Reflect.has(target._data, propKey) ? target._data : target.$options.methods, propKey)
    },
    set(target, propkey, value) {
      Reflect.set(target._data, propkey, value)
      return true
    }
  })
  
  new Observer(this.$options.data, this)

  new Compile(el, vm)
}

我们先对this对象做了一层代理并返回代理对象vm,在get方法中处理查值操作,此时我们传入Compile中的不再是this,而是其代理对象vm,这样我们再直接在vm上对变量进行操作了

完整代码

<div id="mvvm">
    <input type="text" y-model="msg">
    <p>{{msg}}</p>
    <button y-click="handleClik">点我</button>
 </div>
 
<script>
    class Dep {
      constructor() {
        this.subs = []
      }

      add(sub) {
        this.subs.push(sub)
      }

      notify() {
        this.subs.forEach(sub => sub.update())
      }
    }

    class Watcher {
      constructor(vm,cb) {
        this.vm = vm
        this.cb = cb

        cb.call(vm)
      }

      update() {
        this.cb.call(this.vm)
      }

    }

    class Observer {
      constructor (data, vm) {

        vm.dep = new Dep()

        vm._data = new Proxy(data, {
          get: function(target, propkey) {
            return Reflect.get(target, propkey)
          },
          set: function(target, propkey, value) {
            if ( target[propkey] === value ) return true
            Reflect.set(target, propkey, value)
            vm.dep.notify()
            return true
          }
        })

      }
    }

    class Compile {
      constructor(el, vm) {
        this.el = el
        this.vm = vm
        this.init()
      }

      init() {
        
        [].slice.call(this.el.children).forEach(node => {
          let text = node.textContent
          let reg = /\{\{(.*)\}\}/

          if (reg.test(text)) {
            let key = reg.exec(text)[1]
            this.vm.dep.add(new Watcher(this.vm, () => node.innerText = this.vm[key]))     
          }

          if (node.getAttribute('y-model')) {
            let modelKey = node.getAttribute('y-model')

            node.addEventListener('input', e => {
              this.vm[modelKey] = e.target.value || ''
            })
            this.vm.dep.add( new Watcher(this.vm, () => node.value = this.vm[modelKey] ) )
            node.removeAttribute('y-model')
          }

          if (node.getAttribute('y-click')) {
            let eventKey = node.getAttribute('y-click')

            node.addEventListener('click', this.vm[eventKey].bind(this.vm), false)

            node.removeAttribute('y-click')
          }
        })
      }
    }

    const Mvvm = function(options) {
      this.$options = options
      const el = document.querySelector(this.$options.el)

      const vm = new Proxy(this, {
        get(target, propKey) {
          return Reflect.get(Reflect.has(target, propKey) ? target : Reflect.has(target._data, propKey) ? target._data : target.$options.methods, propKey)
        },
        set(target, propkey, value) {
          Reflect.set(target._data, propkey, value)
          return true
        }
      })
      
      new Observer(this.$options.data, this)

      new Compile(el, vm)
    }

    const vm = new Mvvm({
      el: '#mvvm',
      data: {
        msg: 'hello world!'
      },
      methods: {
        handleClik() {
          this.msg = 'hahhahahha'
        }
      },
    })
</script>

当然这只是一个简单的不能再简单的MVVM模式,也没有对其中的步骤做任何优化,但是我们这里重点要呈现的是一个思路,体现自己的思考过程