美文网首页
MVVM框架的实现

MVVM框架的实现

作者: MickeyMcneil | 来源:发表于2019-04-23 13:36 被阅读0次

    最近总被问到vue双向绑定的原理,所以整理实现一下

    MVVM框架

    • M:Model,模型层
    • V:View,视图层
    • VM:ViewModel,视图模型,VM连接的桥梁

    M层修改时,VM层会监测到变化,并通知V层修改;V层修改则会通知M层数据进行修改。

    双向数据绑定的方式

    发布-订阅者模式
    通过pub、sub来实现数据和视图的绑定,使用麻烦。
    脏值检查
    通过定时器轮训检测数据是否发生变化。angular.js用此方法。
    数据劫持
    通过Object.defineProperty()来劫持各个属性的settergetter。vue.js采用数据劫持 + 发布-订阅者模式,在数据变动时发布消息给订阅者。

    实现思路

    • Compile:模板解析器,对模板中的指令插值表达式进行解析,赋予不同的操作
    • Observe:数据监听器,对数据对象的所有属性进行监听
    • Watcher:将Compile的解析结果,与Observe的观察对象连接起来

    Compile

    对模板中的指令和插值表达式进行解析,并赋予不同的操作。

    • document.createDocumentFragment()
    • [].slice.call(likeArr)
      将伪数组转换为数组的方法。具体参考这里
    • getVMValue方法主要为了解决复杂数据类型带来的问题
    • 代码
      compile.js
    // 负责解析模板内容
    class Compile {
      constructor(el, vm) {
        this.el = typeof el === 'string' ? document.querySelector(el) : el
        this.vm = vm
        // 编译模板
        if (this.el) {
          // 1.把子节点存入内存 -- fragment
          let fragment = this.node2fragment(this.el)
          // 2.在内存中编译fragment
          this.compile(fragment)
          // 3.把fragment一次性添加到页面
          this.el.appendChild(fragment)
        }
      }
      // 核心方法
      node2fragment(node) { // 把el中的子节点添加到文档碎片中
        let fragment = document.createDocumentFragment()
        let childNodes = node.childNodes
        this.toArray(childNodes).forEach(element => {
          fragment.appendChild(element)
        });
        return fragment
      }
      compile(fragment) { // 编译文档碎片
        let childNodes = fragment.childNodes
        this.toArray(childNodes).forEach(node => {
          // 元素节点 - 解析指令
          if (this.isElementNode(node)) {
            this.compileElement(node)
          }
          // 文本节点 - 解析插值表达式
          if (this.isTextNode(node)) {
            this.compileText(node)
          }
          // 若还有子节点,递归解析
          if (node.childNodes && node.childNodes.length > 0) {
            this.compile(node)
          }
        })
    
      }
      compileElement(node) {
        // 获取当前节点所有属性
        let attr = node.attributes
        this.toArray(attr).forEach(attr => {
          // 解析vue指令
          let attrName = attr.name
          if (this.isDirective(attrName)) {
            let type = attrName.slice(2)
            let attrVal = attr.value
            if (this.isEventDirective(type)) {
              CompileUtil["eventHandler"](node, this.vm, type, attrVal)
            } else {
              CompileUtil[type] && CompileUtil[type](node, this.vm, attrVal)
            }
          }
        })
      }
      compileText(node) {
        CompileUtil.mustache(node, this.vm)
      }
      // 工具方法
      toArray(likeArr) { // 把伪数组转换成数组
        return [].slice.call(likeArr)
      }
      isElementNode(node) { // 判断元素节点
        return node.nodeType === 1
      }
      isTextNode(node) { // 判断文本节点
        return node.nodeType === 3
      }
      isDirective(attrName) { // 判断指令
        return attrName.startsWith('v-')
      }
      isEventDirective(attrName) { // 判断事件
        return attrName.split(":")[0] === "on"
      }
    }
    let CompileUtil = {
      mustache(node, vm) {
        let txt = node.textContent
        let reg = /\{\{(.+)\}\}/
        if (reg.test(txt)){
          let expr = RegExp.$1
          node.textContent = txt.replace(reg, CompileUtil.getVMValue(vm, expr))      
        }
      },
      text(node, vm, attrVal) {
        node.textContent = this.getVMValue(vm, attrVal)
      },
      html(node, vm, attrVal) {
        node.innerHTML = this.getVMValue(vm, attrVal)
      },
      model(node, vm, attrVal) {
        node.value = this.getVMValue(vm, attrVal)
      },
      eventHandler(node, vm, type, attrVal) {
        let eventType = type.split(":")[1]
        let fn = vm.$methods[attrVal]
        if (eventType && fn) {
          node.addEventListener(eventType, fn.bind(vm))
        }
      },
      // 获取VM中的数据
      getVMValue(vm, expr) {
        let data = vm.$data
        expr.split(".").forEach(key => {
          data = data[key]
        });
        return data
      }
    }
    

    vue.js

    class Vue {
      constructor(options = {}) {
        // 给vue增加实例属性
        this.$el = options.el
        this.$data = options.data
        this.$methods = options.methods
    
        if (this.$el) {
          new Compile(this.$el, this)
        }
      }
    }
    

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <p>复杂数据</p>
        <p>{{car.color}}</p>
        <div v-text="car.brand"></div>
        <p>简单数据</p>
        <p>大家好,{{text}}</p>
        <p>{{msg}}</p>
        <div v-text="msg" title="hhhh"></div>
        <div v-html="msg" title="aaaa"></div>
        <input type="text" v-model="msg">
        <button v-on:click="clickFn">点击</button>
      </div>
      <script src="./src/compile.js"></script>
      <script src="./src/vue.js"></script>
      <script>
        const vm = new Vue({
          el: '#app',
          data: {
            msg: 'hello world',
            text: 'hello text',
            car: {
              color: 'red',
              brand: 'polo'
            }
          },
          methods: {
            clickFn () {
              console.log(this.$data.msg)
            }
          }
        })
        console.log(vm)
      </script>
    </body>
    </html>
    

    observe

    数据劫持主要使用了object.defineProperty(obj, prop, descriptor)MDN链接

    结合下面的小案例,来综合说明过程

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    <body>
      <script>
        let obj = {
          name: 'fxd'
        }
        let temp = obj.name
        Object.defineProperty(obj, 'name', {
          configurable: true, // 属性可配置
          enumerable: true, // 属性可遍历
          get () {
            // 每次获取属性时,会被此方法劫持到
            console.log('获取属性')
            return temp
          },
          set (newValue) {
            console.log('改变属性')
            temp = newValue
          }
        })
      </script>
    </body>
    </html>
    

    执行结果如下


    在vue的源码中,大概实现思路如下

    1. 新建observe.js
    /* 
      observe给data中的所有数据添加getter和setter
      在获取或设置data数据时,方便实现逻辑
    */
    
    class Observe {
      constructor(data) {
        this.data = data
        this.walk(data)
      }
    
      // 核心方法
      walk (data) { // 遍历数据,添加上getter和setter
        if (!data || typeof data !== "object") {
          return
        }
        Object.keys(data).forEach(key => {
          // 给key设置getter和setter
          this.defineReactive(data, key, data[key])
          // 如果data是复杂类型,递归walk
          this.walk(data[key])
        })
      }
      // 数据劫持
      defineReactive(obj, key, value) {
        let that = this
        Object.defineProperty(obj, key, {
          configurable: true,
          enumerable: true,
          get () {
            console.log('获取',value)
            return value
          },
          set (newValue) {
            if (value === newValue) {
              return
            }
            console.log('设置', newValue)
            value = newValue
            // 如果value是对象
            that.walk(newValue)
          }
        })
      }
    }
    
    1. index.html中引入observe.js
    <script src="./src/observe.js"></script>
    
    1. vue.js中添加
        // 监视data中的数据
        new Observe(this.$data)
    

    Watcher

    • updatewatcher.js中对外暴露的更新页面的方法,在observe.jsset中调用。因为set劫持的是数据改变,这样当数据改变时,就会调用update实现页面更新
    • compile.js的指令/插值表达式处理的部分,newWatcher,用来将Watcher中新值填入对应的指令/插值表达式中

    上述方法的缺点:不同的指令/插值表达式各自new了不同Watcher,这样在在observe.jsset中不确定要调用哪一个Watcherupdate方法
    解决方法:发布-订阅者模式

    发布-订阅者模式
    订阅者:只需要订阅
    发布者:状态改变时,通知并自动更新给所有的订阅者
    优点:解耦合


    基本思路如下:
    • watch.js中设置Dep对象,用来管理、添加、通知订阅者;将Watcher中的this存储到Dep.target
    • observe.js的数据劫持中newDep,判断并调用添加和通知订阅者的方法

    最后,优化下复杂数据的更新,model指令中input双向绑定,以及把datamethods中的数据挂载到vm实例上即可

    index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <p>复杂数据</p>
        <p>{{car.color}}</p>
        <div v-text="car.brand"></div>
        <p>简单数据</p>
        <p>大家好,{{text}}</p>
        <p>{{msg}}</p>
        <div v-text="msg" title="hhhh"></div>
        <div v-html="msg" title="aaaa"></div>
        <input type="text" v-model="msg">
        <button v-on:click="clickFn">点击</button>
      </div>
      <script src="./src/watcher.js"></script>
      <script src="./src/observe.js"></script>
      <script src="./src/compile.js"></script>
      <script src="./src/vue.js"></script>
      <script>
        const vm = new Vue({
          el: '#app',
          data: {
            msg: 'hello world',
            text: 'hello text',
            car: {
              color: 'red',
              brand: 'polo'
            }
          },
          methods: {
            clickFn () {
              // this.$data.msg = '2222'
              this.msg = '2222222'
            }
          }
        })
        console.log(vm)
      </script>
    </body>
    </html>
    

    observe.js

    /* 
      observe给data中的所有数据添加getter和setter
      在获取或设置data数据时,方便实现逻辑
    */
    
    class Observe {
      constructor(data) {
        this.data = data
        this.walk(data)
      }
    
      // 核心方法
      walk (data) { // 遍历数据,添加上getter和setter
        if (!data || typeof data !== "object") {
          return
        }
        Object.keys(data).forEach(key => {
          // 给key设置getter和setter
          this.defineReactive(data, key, data[key])
          // 如果data是复杂类型,递归walk
          this.walk(data[key])
        })
      }
      // 数据劫持
      // data中的每一个数据都维护一个dep对象,保存了所有订阅了该数据的订阅者
      defineReactive(obj, key, value) {
        let that = this
        let dep = new Dep()
        Object.defineProperty(obj, key, {
          configurable: true,
          enumerable: true,
          get () {
            // 如果Dep.target中有watcher,存储到订阅者数组中
            Dep.target && dep.addSub(Dep.target)
            return value
          },
          set (newValue) {
            if (value === newValue) {
              return
            }
            value = newValue
            // 如果value是对象
            that.walk(newValue)
            // 发布通知,让所有的订阅者更新内容
            dep.notify()
          }
        })
      }
    }
    

    watch.js

    /* 
      watcher负责将compile和observe关联起来
    */
    class Watcher {
      // 参数分别是:当前实例,data中的名字,数据改变时的回调函数
      constructor(vm, expr, cb) {
        this.vm = vm
        this.expr = expr
        this.cb = cb
    
        // 将this存储到Dep.target上
        Dep.target = this
    
        // 将expr的旧值存储
        this.oldVal = this.getVMValue(vm, expr)
    
        // 清空Dep.target
        Dep.target = null
      }
      // 对外暴露更新页面的方法
      update () {
        let oldVal = this.oldVal
        let newVal = this.getVMValue(this.vm, this.expr)
        if (oldVal != newVal) {
          this.cb(newVal, oldVal)
        }
      }
      // 获取VM中的数据
      getVMValue(vm, expr) {
        let data = vm.$data
        expr.split(".").forEach(key => {
          data = data[key]
        });
        return data
      }
    }
    // dep对象 - 管理订阅者,通知订阅者
    class Dep {
      constructor () {
        // 管理订阅者
        this.subs = []
      }
      // 添加订阅者
      addSub (watcher) {
        this.subs.push(watcher)
      }
      // 通知订阅者
      notify () {
        // 遍历所有订阅者,调用watcher的update方法
        this.subs.forEach(sub => {
          sub.update()
        })
      }
    }
    

    compile.js

    // 负责解析模板内容
    class Compile {
      constructor(el, vm) {
        this.el = typeof el === 'string' ? document.querySelector(el) : el
        this.vm = vm
        // 编译模板
        if (this.el) {
          // 1.把子节点存入内存 -- fragment
          let fragment = this.node2fragment(this.el)
          // 2.在内存中编译fragment
          this.compile(fragment)
          // 3.把fragment一次性添加到页面
          this.el.appendChild(fragment)
        }
      }
      // 核心方法
      node2fragment(node) { // 把el中的子节点添加到文档碎片中
        let fragment = document.createDocumentFragment()
        let childNodes = node.childNodes
        this.toArray(childNodes).forEach(element => {
          fragment.appendChild(element)
        });
        return fragment
      }
      compile(fragment) { // 编译文档碎片
        let childNodes = fragment.childNodes
        this.toArray(childNodes).forEach(node => {
          // 元素节点 - 解析指令
          if (this.isElementNode(node)) {
            this.compileElement(node)
          }
          // 文本节点 - 解析插值表达式
          if (this.isTextNode(node)) {
            this.compileText(node)
          }
          // 若还有子节点,递归解析
          if (node.childNodes && node.childNodes.length > 0) {
            this.compile(node)
          }
        })
    
      }
      compileElement(node) {
        // 获取当前节点所有属性
        let attr = node.attributes
        this.toArray(attr).forEach(attr => {
          // 解析vue指令
          let attrName = attr.name
          if (this.isDirective(attrName)) {
            let type = attrName.slice(2)
            let attrVal = attr.value
            if (this.isEventDirective(type)) {
              CompileUtil["eventHandler"](node, this.vm, type, attrVal)
            } else {
              CompileUtil[type] && CompileUtil[type](node, this.vm, attrVal)
            }
          }
        })
      }
      compileText(node) {
        CompileUtil.mustache(node, this.vm)
      }
      // 工具方法
      toArray(likeArr) { // 把伪数组转换成数组
        return [].slice.call(likeArr)
      }
      isElementNode(node) { // 判断元素节点
        return node.nodeType === 1
      }
      isTextNode(node) { // 判断文本节点
        return node.nodeType === 3
      }
      isDirective(attrName) { // 判断指令
        return attrName.startsWith('v-')
      }
      isEventDirective(attrName) { // 判断事件
        return attrName.split(":")[0] === "on"
      }
    }
    let CompileUtil = {
      mustache(node, vm) {
        let txt = node.textContent
        let reg = /\{\{(.+)\}\}/
        if (reg.test(txt)){
          let expr = RegExp.$1
          node.textContent = txt.replace(reg, CompileUtil.getVMValue(vm, expr))  
          new Watcher(vm, expr, newVal => {
            node.textContent = txt.replace(reg, newVal)
          })    
        }
      },
      text(node, vm, attrVal) {
        node.textContent = this.getVMValue(vm, attrVal)
        // 通过Watcher监听attrVal,一旦变化,执行回调
        new Watcher(vm, attrVal, newVal => {
          node.textContent = newVal
        })
      },
      html(node, vm, attrVal) {
        node.innerHTML = this.getVMValue(vm, attrVal)
        new Watcher(vm, attrVal, newVal => {
          node.innerHTML = newVal
        })
      },
      model(node, vm, attrVal) {
        let that = this
        node.value = this.getVMValue(vm, attrVal)
        // 实现双向的数据绑定
        node.addEventListener('input', function () {
          that.setVMValue(vm, attrVal, this.value)
        })
        new Watcher(vm, attrVal, newVal => {
          node.value = newVal
        })
      },
      eventHandler(node, vm, type, attrVal) {
        let eventType = type.split(":")[1]
        let fn = vm.$methods[attrVal]
        if (eventType && fn) {
          node.addEventListener(eventType, fn.bind(vm))
        }
      },
      // 获取VM中的数据
      getVMValue(vm, expr) {
        let data = vm.$data
        expr.split(".").forEach(key => {
          data = data[key]
        });
        return data
      },
      setVMValue(vm, expr, value) {
        let data = vm.$data
        let arr = expr.split(".")
        arr.forEach((key, index) => {
          if (index < arr.length - 1) {
            data = data[key]
          } else {
            data[key] = value
          }
        })
      }
    }
    

    vue.js

    class Vue {
      constructor(options = {}) {
        // 给vue增加实例属性
        this.$el = options.el
        this.$data = options.data
        this.$methods = options.methods
    
        // 监视data中的数据
        new Observe(this.$data)
    
        // 将data和methods中的数据代理到vm上
        this.proxy(this.$data)
        this.proxy(this.$methods)
    
        if (this.$el) {
          new Compile(this.$el, this)
        }
      }
      proxy (data) {
        Object.keys(data).forEach(key => {
          Object.defineProperty(this, key, {
            enumerable: true,
            configurable: true,
            get () {
              return data[key]
            }, 
            set (newVal) {
              if (data[key] == newVal) {
                return
              }
              data[key] = newVal
            }
          })
        })
      }
    }
    

    相关文章

      网友评论

          本文标题:MVVM框架的实现

          本文链接:https://www.haomeiwen.com/subject/pjrvgqtx.html