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

20 篇文章 0 订阅
16 篇文章 1 订阅

GitHub代码点这里

MVVM框架介绍

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

MVVM

几种双向数据绑定的方式

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

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>


最后推荐一个 js常用的utils合集,帮我点个star吧~

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值