• 2

  • 474

我看了都会的MVVM原理,你看必须也会(vue MVVM)

3星期前

请问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)
  },
复制代码

实现一个Dep(依赖收集器)

Observer中将订阅者收集在数组中,当数据发生变化时,遍历数组,通知订阅者调用回调更新函数更新视图或者数据。

class Dep{
  constructor() {
    this.subs = []
  }
  // 收集订阅者
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 通知观察者去更新视图
  notify() {
    this.subs.forEach(w => {
      w.update()
    })
  }
}
复制代码

实现一个Observer(观察者)

劫持监听所有的属性,在初始化数据时(编译解析指令的时候,创建watcher,先挂载了wather到dep上,后获取数据调用get),此时将订阅者收集到依赖收集器中。数据变化时,在setter函数通知依赖收集器中的订阅者数据发生变化,调用更新函数。

class Observer{
  constructor(data) {
    this.observe(data)
  }
  observe(data) {
    if (data && typeof data === 'object') {
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key])
      })
    }
  }
  defineReactive(obj, key, value) {
    // 递归遍历 value中是否还是对象
    this.observe(value)
    const dep = new Dep()
    // 劫持并监听所有的属性
    Object.defineProperty(obj, key, {
      enumerable: true, // 表示能否通过for-in循环返回属性
      configurable: false, // 表示能否通过delete删除属性从而重新定义属性
      get() {
        // 初始化 编译解析指令的时候 获取数据时就会调用get
        // 订阅数据变化时, 往dep中添加订阅者 查看数据是否变化 更新对应视图
        // 订阅者在 新建watcher的时候挂载到Dep上
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set:(newVal) => {
        // 对新值劫持 并进行监听
        this.observe(newVal)
        if (newVal !== value) {
          value = newVal
        }
        // 通知数据变化
        dep.notify()
      }
    }) 
  }
}
复制代码

流程图

流程分析

  • 创建编译器,解析指令,初始化视图;创建订阅者,订阅数据变化,绑定更新函数。
  • 创建观察者,劫持监听所有属性,在getter中收集订阅者;在setter中监听数据变化,通知收集器中的订阅者,调用更新函数。
  • 创建依赖收集器,添加订阅者。

以上是我在实现MVVM时理解的流程,只包含部分代码;具体实现请戳gitHub

有什么不明白的地方欢迎留言,我很乐意解答,和大家一起进步~

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

程序员

474

相关文章推荐

未登录头像

暂无评论