Skip to content

数据双向绑定 MVVM

  • MVVM:
    • M:数据模型
    • V:视图
    • VM:视图模型

数据双向绑定:逻辑 -> 数据 <-> 视图

  1. 数据 -> 通过 Object.defineProperty()、Proxy 实现响应式的数据
  2. 视图 -> 通过 DOM 增加事件处理函数监听(input/keyup)实现视图的响应

原理

通过数据劫持和发布订阅模式实现数据双向绑定,使用 Object.defineProperty() 或 Proxy 实现数据劫持,劫持各个属性的 setter 和 getter,在数据变动时发布消息给订阅者,触发相应的监听回调,触发对应的监听渲染视图,从而实现数据的双向绑定。 第一步:observer 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。 第二步:compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。 第三步:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: 在自身实例化时往属性订阅器(dep)里面添加自己,自己有变化的时候,接收到属性订阅器的通知,调用自身的 update 方法,触发 Compile 中绑定的回调。 第四步:MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

源码

js
// 订阅器类
class Dep {
  constructor() {
    this.subs = []
  }

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

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

// 观察者类
class Observer {
  constructor(data) {
    this.walk(data)
  }

  walk(obj) {
    Object.keys(obj).forEach((key) => {
      this.defineReactive(obj, key, obj[key])
    })
  }

  defineReactive(obj, key, val) {
    const dep = new Dep()
    let childObj = observe(val) // 递归子属性
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        if (Dep.target) {
          dep.addSub(Dep.target)
        }
        return val
      },
      set(newVal) {
        if (newVal === val) return
        val = newVal
        childObj = observe(newVal) // 如果新值是对象,继续劫持
        dep.notify()
      }
    })
  }
}

function observe(value) {
  if (!value || typeof value !== 'object') {
    return
  }
  return new Observer(value)
}

// 订阅者类
class Watcher {
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
    this.value = this.get()
  }

  get() {
    Dep.target = this
    const value = this.vm.$data[this.key] // 触发 getter
    Dep.target = null
    return value
  }

  update() {
    const newValue = this.vm.$data[this.key]
    if (newValue !== this.value) {
      this.value = newValue
      this.cb.call(this.vm, newValue)
    }
  }
}

// 编译类
class Compile {
  constructor(el, vm) {
    this.vm = vm
    this.el = document.querySelector(el)
    this.fragment = null
    this.init()
  }

  init() {
    if (this.el) {
      this.fragment = this.nodeToFragment(this.el)
      this.compileElement(this.fragment)
      this.el.appendChild(this.fragment)
    }
  }

  nodeToFragment(el) {
    const fragment = document.createDocumentFragment()
    let child = el.firstChild
    while (child) {
      fragment.appendChild(child)
      child = el.firstChild
    }
    return fragment
  }

  compileElement(el) {
    const childNodes = el.childNodes
    ;[...childNodes].forEach((node) => {
      const reg = /\{\{(.*)\}\}/
      const text = node.textContent

      if (this.isElementNode(node)) {
        this.compile(node)
      } else if (this.isTextNode(node) && reg.test(text)) {
        this.compileText(node, reg.exec(text)[1])
      }

      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node)
      }
    })
  }

  compile(node) {
    const nodeAttrs = node.attributes
    ;[...nodeAttrs].forEach((attr) => {
      const attrName = attr.name
      if (this.isDirective(attrName)) {
        const exp = attr.value
        const dir = attrName.substring(2)
        if (this.isEventDirective(dir)) {
          this.compileEvent(node, this.vm, exp, dir)
        } else {
          this.compileModel(node, this.vm, exp, dir)
        }
        node.removeAttribute(attrName)
      }
    })
  }

  compileText(node, exp) {
    const initText = this.vm.$data[exp]
    this.updateText(node, initText)
    new Watcher(this.vm, exp, (value) => {
      this.updateText(node, value)
    })
  }

  compileEvent(node, vm, exp, dir) {
    const eventType = dir.split(':')[1]
    const cb = vm.methods && vm.methods[exp]

    if (eventType && cb) {
      node.addEventListener(eventType, cb.bind(vm), false)
    }
  }

  compileModel(node, vm, exp, dir) {
    let val = vm.$data[exp]
    this.modelUpdater(node, val)
    new Watcher(vm, exp, (value) => {
      this.modelUpdater(node, value)
    })

    node.addEventListener('input', (e) => {
      const newValue = e.target.value
      if (val === newValue) {
        return
      }
      vm.$data[exp] = newValue
      val = newValue
    })
  }

  updateText(node, value) {
    node.textContent = typeof value === 'undefined' ? '' : value
  }

  modelUpdater(node, value) {
    node.value = typeof value === 'undefined' ? '' : value
  }

  isDirective(attr) {
    return attr.indexOf('v-') === 0
  }

  isEventDirective(dir) {
    return dir.indexOf('on:') === 0
  }

  isElementNode(node) {
    return node.nodeType === 1
  }

  isTextNode(node) {
    return node.nodeType === 3
  }
}

// MVVM 类
class MVVM {
  constructor(options) {
    this.$data = options.data
    this.methods = options.methods

    // 1. 数据劫持
    observe(this.$data)

    // 2. 模板编译
    new Compile(options.el, this)

    // 3. 数据代理
    this.proxyData(this.$data)
  }

  proxyData(data) {
    Object.keys(data).forEach((key) => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key]
        },
        set(newVal) {
          data[key] = newVal
        }
      })
    })
  }
}

// 使用示例
const vm = new MVVM({
  el: '#app',
  data: {
    message: 'Hello MVVM',
    inputText: ''
  },
  methods: {
    handleClick() {
      alert('Button clicked!')
    }
  }
})

hancenter808@outlook.com