请问vue中双向数据绑定是如何实现的?MVVM原理是什么?

vue中的双向数据绑定是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter;在数据变动时发布消息给订阅者,触发相应的回调更新函数。通过Observer来监听model数据变化,通过Compile来解析编译模板指令;当数据发生变化时,Observer发布消息给Watcher(订阅者),订阅者通过调用更新函数来更新视图。Watcher搭起了Observer和Compile之间的通信桥梁,从而达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

实现一个Compile(编译器)

用于解析指令,并初始化视图。并在此时创建订阅者,绑定更新函数,在数据变化时更新视图或数据。
class Compile{
  constructor(el, vm) {
    // 判断el是否是一个元素节点对象
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    // 1 获取文档碎片对象 放入内存中 减少页面的回流和重绘
    const fragement = this.node2Fragment(this.el)
    // 2 编译模板
    this.compile(fragement)
    // 3 追加子元素到根元素上
    this.el.appendChild(fragement)
  }
  compile(fragement) {
    // 1 获取所有子节点
    let childNodes = fragement.childNodes
    childNodes = [...childNodes]
    childNodes.forEach(child => {
      if (this.isElementNode(child)) {
        // 是元素节点
        // 编译元素节点
        // console.log('元素节点', child)
        this.compileElement(child)
      } else {
        // console.log('文本节点', child)
        // 编译文本节点
        this.compileText(child)
      }
      if (child.childNodes && child.childNodes.length) {
        // 递归遍历编译所有子节点
        this.compile(child)
      }
    })
  }
  // 编译解析元素节点
  compileElement(node){
    // 元素节点  v-html v-model v-text等指令或者事件绑定
    let attributes = node.attributes
    attributes = [...attributes]
    // 拿到所有的属性 解析出指令
    attributes.forEach(attr => {
      const {name, value} = attr // 例如:v-text msg
      // console.log(attr, name, value)
      if (this.isDirective(name)) {
        // 判断是否是v-开始  表示是一个指令 v-text v-html v-model v-on:click
        const [, dirctive] = name.split('-') // text html model on:click
        const [dirName, eventName] = dirctive.split(':') // dirName: text html model on eventName: click
        // 更新数据 数据驱动视图
        compileUtil[dirName](node, value, this.vm, eventName)

        // 删除带有指令标签的属性
        node.removeAttribute('v-'+dirctive)
      } else if (this.isEventName(name)) {
        // @click="handleClick"
        let [,eventName] = name.split('@')
        compileUtil['on'](node, value, this.vm, eventName)
        // 删除带有@的属性
        node.removeAttribute('@'+eventName)
      } else if (this.isBindName(name)) {
        let [, attrName] = name.split(':')
        compileUtil['bind'](node, value, this.vm, attrName)
        // 删除带有:的属性
        node.removeAttribute(':'+attrName)
      }
    })
  }
  // 编译解析文本节点
  compileText(node) {
    // {{}} 对应类似v-text
    const content = node.textContent
    if (/\{\{(.+?)\}\}/.test(content)) {
      // 正则匹配含有双大括号的文本 并且
      // console.log(content)
      compileUtil['text'](node, content, this.vm)
    }
  }

实现一个Update(更新方法)

通过解析指令以及文本,在数据变化时,通过操作dom节点,更新视图;修改data,更新数据。
  // 更新函数
  updater: {
    textUpdater(node, value) {
      node.textContent = value
    },
    htmlUpdater(node, value) {
      node.innerHTML = value
    },
    modelUpdater(node, value) {
      node.value = value
    },
    bindUpdater(node, attrName, value) {
      node.setAttribute(attrName, value)
    }
  }
}

实现一个Watcher(订阅者)

数据发生变化时,调用回调函数,更新视图或数据。
class watcher{
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    // 先保存旧值 用于判断新值传入时 是否有变化
    this.oldVal = this.getOldVal()
  }
  getOldVal() {
    Dep.target = this
    const oldVal = compileUtil.getVal(this.expr, this.vm)
    // 在调用getVal时会触发observer中defineReactive中 object.defineProperty 中的get函数
    // 在get函数中拿到该watcher并添加到dep中
    Dep.target = null
    return oldVal
  }
  update() {
    const newVal = compileUtil.getVal(this.expr, this.vm)
    if (this.oldVal !== newVal) {
      this.oldVal = newVal
      this.cb(newVal)
    }
  }
}
编译模板指令时,初始化一个watcher实例并绑定更新函数
 text(node, expr, vm) {
    let value
    if (expr.indexOf('{{') !== -1) {
      // 处理 存在双大括号的文本 {{personalbar.name}} {{msg}}
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        // replace 替换回调函数参数分别有:0 匹配到的字符串 1在使用组匹配 组匹配到的值 匹配值在原字符串中的索引 原字符串
        // 绑定观察者 将来数据发生变化 触发这里的回调 进行更新
        new watcher(vm, args[1], (newVal) => {
          // console.log('newVal', newVal, this.getContentVal(expr, vm))
          // 在此有个疑问 newVal和getContentVal重新解析原表达式获取的值是一样的 不知作者为啥要重新解析一遍?
          this.updater.textUpdater(node, this.getContentVal(expr, vm))
        })
        return this.getVal(args[1], vm)
      })
    } else {
      // 处理v-text expr: msg vm: 整个实例
      new watcher(vm, expr, (newVal) => {
        this.updater.textUpdater(node, newVal)
      })
      value = this.getVal(expr, vm)
    }
    this.updater.textUpdater(node, value)
  },
  html(node, expr, vm) {
    const value = this.getVal(expr, vm)
    new watcher(vm, expr, (newVal) => {
      this.updater.htmlUpdater(node, newVal)
    })
    this.updater.htmlUpdater(node, value)
  },
  model(node, expr, vm) {
    const value = this.getVal(expr, vm)
    // 创建监听者 并通过watcher中的update来绑定回调这个更新函数  数据 =》 视图
    new watcher(vm, expr, (newVal) => {
      this.updater.modelUpdater(node, newVal)
    })
    // 视图 =》 数据 =》 视图
    node.addEventListener('input', (e) => {
      // 设置值
      this.setVal(expr, vm, e.target.value)
    })
    this.updater.modelUpdater(node, value)
  },
  on(node, expr, vm, eventName) {
    // 找到对应的函数方法 绑定监听函数
    let fn = vm.$options.methods && vm.$options.methods[expr]
    // 修改函数this指向为当前vue实例
    node.addEventListener(eventName, fn.bind(vm), false)
  },
  bind(node, expr, vm, attrName) {
    const value = this.getVal(expr, vm)
    this.updater.bindUpdater(node, attrName, value)
  },

10-17 01:38
查看更多