剖析Vue原理&实现双向绑定MVVM

本文深入剖析MVVM模式下的数据绑定机制,介绍发布者-订阅者模式、脏值检查及数据劫持三种核心实现方式,并通过具体示例代码详细解释Vue.js中数据劫持的具体实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载: https://segmentfault.com/a/1190000006599500

几种实现双向绑定的做法
目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(vue.js)

发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value)

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

DOM事件,譬如用户输入文本,点击按钮等。( ng-click )

XHR响应事件 ( $http )

浏览器Location变更事件 ( $location )

Timer事件( $timeout , $interval )

执行 $digest() 或 $apply()

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。


已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="mvvm-app">
  <input type="text" v-model="word">
  <p>{{word}}</p>
  <button v-on:click="sayHi">
    change model
  </button>
</div>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
  var vm = new MVVM({
    el: '#mvvm-app',
    data: {
      word: 'Hello World!'
    },
    methods: {
      sayHi () {
        this.word = 'Hi, everybody;' ;
      }
    }
  })
</script>
</body>
</html>

---------- mvvm.js

class MVVM {
  constructor (options) {
    this.$options = options || {}
    let data = this._data = this.$options.data
    // 属性代理,实现 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(key => {
      // 把data中的每个属性绑定到vue实例上(方便this.属性访问)
      this._proxyData(key)
    })
    // 处理计算属性
    this._initComputed()
    // 数据监视
    observer(data, this)
    // 解析模版
    this.$compile = new Compile(options.el || document.body, this)
  }
  $watch (key, cd, options) {
    new Watcher(this, key, cd)
  }
  _proxyData (key, setter, getter) {
    let me = this
    setter = setter || Object.defineProperty(me, key, {
      configurable: false,
      enumerable: true,
      get: function () {
        return me._data[key]
      },
      set: function (newVal) {
        me._data[key] = newVal
      }
    })
  }
  // 把计算属性绑定到vue实例上(方便this.属性访问),每次获取计算属性时,如果是函数,则重新执行取值
  _initComputed () {
    let me = this, computed = this.$options.computed
    if (typeof computed === 'object') {
      Object.keys(computed).forEach(function (key) {
        Object.defineProperty(me, key, {
          get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
          set: function () {}
        })
      })
    }
  }
}

---------

class Observer {
  constructor (data) {
    this.data = data
    this.walk(data)
  }
  walk(data) {
    Object.keys(data).forEach(key => {
      this.convert(key, data[key])
    })
  }
  convert (key, val) {
    this.defineReactive(this.data, key, val)
  }
  defineReactive (data, key, val) {
    let dep = new Dep()
    let childObj = observer(val) // 如果当前属性值是一个对象,继续进行数据监视处理
    Object.defineProperty(data, key, {
      enumerable: true, // 可枚举
      configurable: false, // 不能再define(配置)
      get: function () {
        // Dep.target 本质上就是一个watcher实例
        if (Dep.target) {
          // 当在compile中获取属性值的时候get函数激活, 把Dep的实例传给Watcher实例的addDep函数
          dep.depend()
        }
        return val
      },
      set: function (newVal) {
        if (newVal === val) {
          return
        }
        val = newVal
        childObj = observer(newVal)
        dep.notify() // 通知所有订阅者
      }
    })
  }
}
// mvvm调用时给vue实例下data的所有属性进行处理
function observer(value, vm) {
  if (!value || typeof value !== 'object') {
    return
  }
  return new Observer(value)
}
let uid = 0
class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub) {
    this.subs.push(sub)
  }
  depend () {
    Dep.target.addDep(this)
  }
  notify () {
    this.subs.forEach(sub => {
      // 本质是是调用Watcher实例的update方法
      sub.update()
    })
  }
}
Dep.target = null

---------

class Compile {
  constructor(el, vm) {
    // el vue的html区域
    // vm vue的实例
    this.$vm = vm
    this.$el = this.isElementNode(el) ? el : document.querySelector(el)
    if (this.$el) {
      // 把dom对象放入html片段(createDocumentFragment)中,防止修改时多次进行重排重绘
      this.$fragment = this.node2Fragment(this.$el)
      this.init()
      // 把更新后的dom片段放入到页面中
      this.$el.appendChild(this.$fragment)
    }
  }
  node2Fragment(el) {
    let fragment = document.createDocumentFragment(), child
    while (child = el.firstChild) {
      fragment.appendChild(child)
    }
    return fragment
  }
  isElementNode (el) {
    return typeof el === 'object' && el.nodeType === 1
  }
  isTextNode (node) {
    return typeof node === 'object' && node.nodeType === 3
  }
  init () {
    this.compileElement(this.$fragment)
  }
  compileElement (el) {
    let childNodes = el.childNodes, me = this;
    // 对每个节点进行处理
    [].slice.call(childNodes).forEach(node => {
      let text = node.textContent
      let reg = /\{\{(.*)\}\}/
      // 判定节点类型,进行对应的处理
      if (me.isElementNode(node)) {
        me.compile(node)
      } else if (me.isTextNode(node) && reg.test(text)) {
        me.compileText(node, RegExp.$1)
      }
      if (node.childNodes && node.childNodes.length) {
        me.compileElement(node)
      }
    })
  }
  compileText (node, exp) {
    compileUtil.text(node, this.$vm, exp)
  }
  // 如果是元素节点,则获取元素的属性,处理v-开头的指令
  compile (node) {
    let nodeAttrs = node.attributes, me = this;
    [].slice.call(nodeAttrs).forEach(attr => {
      let attrName = attr.name
      if (me.isDirective(attrName)) {
        let exp = attr.value // 绑定的属性或函数的名称
        let dir = attrName.substring(2)
        if (me.isEventDirective(dir)) { // 判定是事件还是其他指令
          compileUtil.eventHandler(node, me.$vm, exp, dir)
        } else {
          compileUtil[dir] && compileUtil[dir](node, me.$vm, exp)
        }
        node.removeAttribute(attrName)
      }
    })
  }
  isDirective(attr) {
    return attr.indexOf('v-') === 0
  }
  isEventDirective(attr) {
    return attr.indexOf('on') === 0
  }
}
let compileUtil = {
  text (node, vm, exp) {
    this.bind(node, vm, exp, 'text')
  },
  html (node, vm, exp) {
    this.bind(node, vm, exp, 'html')
  },
  model (node, vm, exp) {
    this.bind(node, vm, exp, 'model')
    let val = this._getVMVal(vm, exp)
    node.addEventListener('input',e => {
      let newValue = e.target.value
      if (val === newValue) {
        return
      }
      this._setVMVal(vm, exp, newValue)
      val = newValue
    })
  },
  bind(node, vm, exp, dir) {
    let updaterFn = updater[dir + 'Updater']
    updaterFn && updaterFn(node, this._getVMVal(vm, exp)) // 把dom中属性替换成对应的值
    new Watcher(vm, exp ,(value, oldValue) => { // 监视,在get函数中使Dep.target值为Watcher实例
      updaterFn && updaterFn(node, value, oldValue)
    }).get()
  },
  eventHandler (node, vm, exp, dir) {
    let eventType = dir.split(':')[1],
      fn = vm.$options.methods && vm.$options.methods[exp]
    if (eventType && fn)  {
      node.addEventListener(eventType, fn.bind(vm), false)
    }
  },
  // 获取对象属性的值
  _getVMVal (vm, exp) {
    let val = vm
    exp = exp.split('.')
    exp.forEach(k => {
      val = val[k]
    })
    return val
  },
  _setVMVal (vm, exp, value) {
    let val = vm
    exp = exp.split('.')
    exp.forEach(function (k, i) {
      if (i < exp.length -1) {
        val = val[k]
      } else {
        val[k] = value
      }
    })
  }
}
let updater = {
  textUpdater (node, value) {
    node.textContent = typeof value === 'undefined' ? '' : value
  },
  htmlUpdater (node, value) {
    node.innerHTML = typeof value === 'undefined' ? '' : value
  },
  classUpdater (node, value, oldValue) {
    let className = node.className
    className = className.replace(oldValue, '').replace(/\s$/, '')
    let space = className && String(value) ? ' ' : ''
    node.className = className + space + value
  },
  modelUpdater (node, value, oldValue) {
    node.value = typeof value === 'undefined' ? '' : value
  }
}

---------

class Watcher {
  constructor (vm, expOrFn, cb) {
    // vm vue 实例
    // expOrFn 绑定的属性或函数的名称
    // cb 回调函数
    this.cb = cb
    this.vm = vm
    this.expOrFn = expOrFn
    this.depIds = {}
    // 获取被绑定属性对应的值
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = this.parseGetter(expOrFn)
    }
  }
  update () {
    this.run()
  }
  run () {
    let value = this.get()
    let oldVal = this.value
    if (value !== oldVal) {
      this.value  = value
      // 更新dom
      this.cb.call(this.vm, value, oldVal)
    }
  }
  addDep (dep) {
    // 1. 每次调用run()的时候会触发相应属性的getter
    // getter里面会触发dep.depend(),继而触发这里的addDep
    // 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
    // 则不需要将当前watcher添加到该属性的dep里
    // 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
    // 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
    // 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
    // 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
    // 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
    // 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
    // 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
    // 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
    // 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
    // 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
    if (!this.depIds.hasOwnProperty(dep.id)) {
      dep.addSub(this)
      this.depIds[dep.id] = dep
    }
  }
  get () {
    // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
    Dep.target = this
    // 获取属性对应的值,会触发observer > defineReactive > 属性取值函数 > watcher.addDep > dep.addSub(watcher实例对象)
    let value = this.getter.call(this.vm, this.vm)
    Dep.target = null
    return value
  }
  parseGetter (exp) {
    if (/[^\w.$]/.test(exp)) return
    let exps = exp.split('.')
    return function (obj) {
      for (let i = exps.length; i--;) {
        if (!obj) return
        obj = obj[exps[i]]
      }
      return obj
    }
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值