欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

八四、MVVM框架进阶与实现(手动实现一个简易版vue)

程序员文章站 2022-07-01 20:50:31
...

GitHub代码点这里

MVVM框架介绍

  • M(Model,模型层 ),
  • V(View,视图层),
  • VM(ViewModel,视图模型,V与M连接的桥梁)
  • MVVM框架实现了数据双向绑定
    1. 当M层数据进行修改时,VM层会监测到变化,并且通知V层进行相应的修改
    2. 修改V层则会通知M层数据进行修改
    3. MVVM框架实现了视图与模型层的相互解耦

八四、MVVM框架进阶与实现(手动实现一个简易版vue)

几种双向数据绑定的方式

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

  • 一般通过pub、sub的方式来实现数据和视图的绑定,但是使用起来比较麻烦

2 脏值检查(angular.js)

  • angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图。类似于通过定时器轮训检测数据是否发生了改变。

3 数据劫持

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

Vue实现思路

  1. 实现一个Compiler模板解析器,能够对模版中的指令和插值表达式进行解析,并且赋予不同的操作
  2. 实现一个Observer数据监听器,能够对数据对象的所有属性进行监听
  3. 实现一个Watcher观察者,将Compile的解析结果,与Observer所观察的对象连接起来,建立关系,在Observer观察到对象数据变化时,接收通知,同时更新DOM
  4. 创建一个公共的入口对象,接收初始化的配置并且协调上面三个模块,也就是vue
  5. html中使用

八四、MVVM框架进阶与实现(手动实现一个简易版vue)

new Watcher
接受到通知 触发updata
初始化视图
new Dep 数据改变触发dep.notify
dep.notify通知变化
dep.addSub添加订阅者
new Vue
compile-解析指令 表达式
Observer-数据劫持
Watcher-负责把compile 模块 与observe 模块连接起来
updater-更新视图
Dep

new Vue > compile > 指令 表达式 解析(new Watcher -订阅数据变化 ) > observe 数据劫持(简体数据改变 new Dep > addSub(watcher存储起来)> 数据改变就通知dep.notify)> watcher 接受到通知 触发updata 更新视图
多个watcher 怎么管理?> 使用发布订阅者模式> 有watcher就存储起来 > 数据改变调用updata通知所有的订阅者更新数据

发布-订阅者模式,也叫观察者模式

它定义了一种一对多的依赖关系,即当一个对象的状态发生改变的时候,所有依赖于它的对象都会得到通知并自动更新,解决了主体对象与观察者之间功能的耦合。

例子:微信公众号

  • 订阅者:只需要要订阅微信公众号
  • 发布者(公众号):发布新文章的时候,推送给所有订阅者
  • 优点:解耦合(订阅者不用每次去查看公众号是否有新的文章
    发布者不用关心谁订阅了它,只要给所有订阅者推送即可)

上代码

Compiler.js

/* compile 专门扶着解析模板内容 */

class Compile {
  /**
   *
   * @param el --new Vue传递的选择器
   * @param vm --vue实例
   */
  constructor(el, vm) {
    console.log(vm)
    this.el = typeof el === 'string' ? document.querySelector(el) : el
    this.vm = vm
    //编译模板
    if (this.el) {
      //1. 把el中所有的子节点都放到内存中,fragment
      let fragment = this.node2fragment(this.el)
      //2. 在内存中编译fragment
      this.compile(fragment)
      //3. 把fragment一次性添加到页面
      this.el.appendChild(fragment)
    }
  }

  /*核心方法*/
  /**
   * 将节点添加到fragment 中
   * @param node
   * @returns {DocumentFragment}
   */
  node2fragment(node) {
    let fragment = document.createDocumentFragment()
    //把 el中所有的子节点挨个添加到文档碎片中
    let childNodes = node.childNodes
    this.toArray(childNodes).forEach(node => {
      //把所有的子节点都添加到fragment中
      fragment.appendChild(node)
    })
    return fragment
  }

  /**
   * 编译文档碎片
   * @param 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)
      }
    })
  }

  /**
   * 解析html标签
   * @param node
   */
  compileElement(node) {
    //1 获取到当前节点下的所有属性
    let attributes = node.attributes
    this.toArray(attributes).forEach(attr => {
      let attrName = attr.name
      //2 解析vue指令(以v-on开头的指令)
      if (this.isDirective(attrName)) {
        //指令类型
        let type = attrName.slice(2)
        //指令值
        let expr = attr.value

        if (this.isEventDirective(type)) {
          CompileUtil['enentHandler'](node, this.vm, type, expr)
        } else {
          CompileUtil[type] && CompileUtil[type](node, this.vm, expr)
        }
      }
    })

  }

  /**
   * 解析文本节点
   * @param node
   */
  compileText(node) {
    CompileUtil.mustache(node, this.vm)
  }

  /*工具方法*/
  /**
   * 转化为数组
   * @param likeArray
   * @returns {*[]}
   */
  toArray(likeArray) {
    return [].slice.call(likeArray)
  }

  /**
   * nodeType:节点类型 1:元素节点 3:文本节点
   * @param node
   */
  isElementNode(node) {
    return node.nodeType === 1
  }

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

  /**
   * 判断是否为v-开头的指令
   * @param attr
   * @returns {boolean}
   */
  isDirective(attr) {
    return attr.startsWith("v-")
  }

  /**
   * 判断是否是事件指令
   * @param attr
   * @returns {boolean}
   */
  isEventDirective(attr) {
    return attr.split(':')[0] === 'on'
  }
}


//util 将编译的方法提取出来 方便增删改查
let CompileUtil = {
  //处理文本
  mustache(node, vm) {
    let text = node.textContent
    let reg = /\{\{(.+)\}\}/
    if (reg.test(text)) {
      //通过正则分组 取到内容 将原来的美容替换为data里的数据
      let expr = RegExp.$1
      node.textContent = text.replace(reg, this.getVMValue(vm, expr))
      new Watcher(vm, expr, newVlue => {
        node.textContent = text.replace(reg, newVlue)
      })
    }
  },

  //处理v-text
  text(node, vm, expr) {
    node.textContent = this.getVMValue(vm, expr)

    //通过watcher监听expr的数据变化,一旦改变执行回调
    new Watcher(vm, expr, newVlue => {
      node.textContent = newVlue
    })
  },
  //处理v-html
  html(node, vm, expr) {
    node.innerHTML = this.getVMValue(vm, expr)
    new Watcher(vm, expr, newVlue => {
      node.innerHTML = newVlue
    })
  },
  //处理v-model
  model(node, vm, expr) {
    let that = this
    node.value = this.getVMValue(vm, expr)

    //实现双向数据绑定,给node注册input事件,当前元素value值发生改变,data里数据也要改变
    node.addEventListener('input', function () {
      console.log(this.value)
      that.setVMValue(vm, expr, this.value)
    })

    new Watcher(vm, expr, newVlue => {
      node.value = newVlue
    })
  },
  //处理事件
  enentHandler(node, vm, type, expr) {
    let eventType = type.split(":")[1]
    let fn = vm.$methods && vm.$methods[expr]
    if (eventType && fn) node.addEventListener(eventType, fn.bind(vm))
  },
  /**
   * 获取VM中的数据 (主要解决对象中的数据)
   * @param vm
   * @param expr
   * @returns {*}
   */
  getVMValue(vm, expr) {
    let data = vm.$data
    expr.split('.').forEach(key => {
      data = data[key]
    })
    return data
  },
  /**
   * 获取VM中的数据 (主要解决对象中的数据)
   * @param vm
   * @param expr
   * @returns {*}
   */
  setVMValue(vm, expr, value) {
    let data = vm.$data
    // debugger
    let arr = expr.split('.')
    arr.forEach((key, i) => {
      if (i < arr.length - 1) {
        data = data[key]
      } else {
        data[key] = value
      }
    })
  }
}


Observe.js

/* observe 用于给data中所有的数据天机getter setter 方便我们在获取或者设置data中数据的时候,实现一下逻辑 */

class Observer {
  constructor(data) {
    this.data = data
    this.walk(data)
  }

  /*核心方法*/

  /**
   * 遍历data中的数据,都添加上getter,setter
   * @param data
   */
  walk(data) {
    if (!data || typeof data != "object") {
      return
    }
    Object.keys(data).forEach(key => {
      //给data对象的可以设置setter,getter
      this.defineReactive(data, key, data[key])
      //如果$data[key]是复杂类型 递归walk
      this.walk(data[key])
    })
  }

  /**
   * 定义响应式的数据(数据劫持)
   * data中的每一个数据都应该维护一个dep对象
   * dep保存了所有的订阅了该数据的订阅者
   * @param obj
   * @param key
   * @param value
   * @returns {*}
   */
  defineReactive(obj, key, value) {
    let that = this
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      configurable: true, // 表示属性可以配置
      enumerable: true, // 表示这个属性可以遍历

      // 每次获取对象的这个属性的时候,就会被这个get方法给劫持到 getter
      get() {
        Dep.target && dep.addSub(Dep.target)
        return value
      },

      // 每次设置这个对象的属性的时候,就会被set方法劫持到
      // 设置的值也会劫持到 setter
      set(newValue) {
        console.log('set方法执行了---',newValue)
        value !== newValue ? value = newValue : null
        //如果newValue也是一个对象 也要调用walk
        that.walk(value)

        //发生改变 调用wather的updata方法 (发布通知)
        dep.notify()

      }
    })
  }
}


Watcher.js

/*watcher 模块负责把compile 模块 与observe 模块连接起来(桥梁)*/

class Watcher {
  /**
   * @param vm:当前vm实例
   * @param expr:data中数据的名字
   * @param cb:一旦数据发生了改变,需要调用cb
   */
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb

    //this 表示新创建的watcher对象 存储到dep.target 属性上
    Dep.target = this

    //把expr的旧值储存起来
    this.oldValue = this.getVMValue(vm, expr)

    //清空dep.target
    Dep.target = null

  }

  /**
   * 对外暴露的方法,用于更新页面
   * 对比新旧的值 改变就调用cb
   */
  updata() {
    let oldValue = this.oldValue
    let newValue = this.getVMValue(this.vm, this.expr)
    if (oldValue != newValue) {
      this.cb(newValue, oldValue)
    }
  }

  /**
   * 获取VM中的数据 (主要解决对象中的数据)
   * @param vm
   * @param expr
   * @returns {*}
   */
  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.updata()
    })
  }
}


Vue.js

/* 定义一个类,用于创造vue实例*/
class Vue {
  constructor(options = {}) {
    //给vue实例增加属性
    this.$el = options.el
    this.$data = options.data
    this.$methods = options.methods

    //通过observe监视data数据
    new Observer(this.$data)

    //把data中的数据代理到vm上
    this.proxy(this.$data)

    //把methods的数据代理到vm上
    this.proxy(this.$methods)

    //如果指定了el参数,对el进行解析
    if (this.$el) {
      //compile 负责解析模板的内容
      //  需要:模板、数据
      let c = new Compile(this.$el, this)

    }
  }

  /**
   *使用proxy代理 将this.$data上的数据代理到this上
   * @param data
   */
  proxy(data) {
    Object.keys(data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return data[key]
        },
        set(v) {
          if(data[key] == v) return
          data[key] = v
        }
      })
    })
  }

}


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>vue-mvvm-demo</title>
</head>

<body>
<div id="app">
  插值表达式
  <h3>{{msg}}</h3>
  <div>
    hhh,{{text}}
    <p v-html="demo"></p>
  </div>
  <h4>{{color.red}}</h4>
  <h4>{{color.other.block}}</h4>
  <!-- vue的指令 -->
  <p v-text="msg"></p>
  <input type="text" v-model="msg">
  <button v-on:click='_handleClick'>按钮</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>
  // let app = document.getElementById('app')
  const vm = new Vue({
    el: '#app',
    data: {
      msg: 'hello vue',
      demo: '<h1>我是h1标签</h1>',
      text: '呵呵呵呵',
      color: {
        red: 'red',
        yellow: 'yellow',
        other: {
          block: 'block'
        }
      }
    },
    methods: {
      _handleClick() {
        //vue 中this指向当前vm实例
        console.log(this.msg)
        this.msg = '改变red'
      }
    }
  })
</script>
</body>

</html>