VUE MVVM-介绍和演示

雨燕双飞 提交于 2020-02-26 01:11:20

MVVM-介绍和演示

  • easyUI
  • knockout
  • smartclient
  • exit.js

MVVM

MVVM 在Vue中是用什么来实现的?

OK,我们来攻克这个题目

  • 首先第一个M,指的是 Model, 也就是**数据模型,其实就是数据, 换到Vue里面,其实指的就是 Vue组件实例中的data**, 但是这个data 我们从一开始就定义了 它叫 响应式数据

  • 第二个V,指的是View, 也就是**页面视图, 换到Vue中也就是 我们的template转化成的DOM对象**

  • 第三个 VM, 指的是**ViewModel, 也就是 视图和数据的管理者, 它管理着我们的数据 到 视图变化的工作,换到Vue中 ,它指的就是我们的当前的Vue实例, Model数据 和 View 视图通信的一个桥梁**

  • 简单一句话:数据驱动视图, 数据变化 =>视图更新 双向 绑定 视图更新 => 数据变化

Vue ==>MVVM => 双向数据绑定 => this.name = '张三 '

React => MVVM => 单向数据绑定 => 只能从数据 => 视图 => this.setState({ name: '张三' })

<!-- 视图 -->
<template>
  <div>{{ message }}</div>
</template>
<script>
// Model 普通数据对象
export default {
  data () {
    return {
      message: 'Hello World'
    }
  }
}
</script>

<style>

</style>

MVVM-响应式原理-Object.defineProperty()-基本使用

接下里,我们来重点研究MVVM的原理及实现方式,Vuejs官网给出了MVVM的原理方式

Vue文档说明

通过上面的文档我们可以发现, Vue的响应式原理(MVVM)实际上就是下面这段话:

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

从上面的表述中,我们发现了几个关键词, Object.defineProperty getter/setter

什么是 Object.defineProperty?

定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

语法: Object.defineProperty(obj, prop, descriptor)

参数: obj => 要在其上定义属性的对象。

prop => 要新增或者修改的属性名

descriptor => 将被定义或修改的属性描述符。

返回值 : 被传递给函数的对象。 也就是 传入的obj对象

通过上面的笔记 我们来看下 有哪些参数 需要学习

obj 就是一个对象 可以 new Object() 也可以 { }

prop 就是属性名 也就是一个字符串

descriptor 描述符是什么 ? 有哪些属性

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者

上面是官方描述 ,它告诉我们 defineProterty设计上有**两种模式存在,一种数据描述, 一种存取描述**

描述符必须是这两个中的一个 ,不能同时是两者, 也就是 一山不容二虎, 也不能 一山两虎都无

我们写一个最简单的 **数据描述符**的例子

        // Object.defineProperty(obj,prop, desciptor)
       //  desciptor  => 数据描述符  存取描述符
       var obj = {
          name: '曹扬'
       }
      var o = Object.defineProperty(obj, 'weight', {
           // 描述符是一个对象
           // 数据描述 存取描述
           value: '280kg'   // 数据描述符 value
       })
       console.log(o)

接下来进行详细分析

Object.defineProperty()-数据描述符模式

数据描述符有哪些属性?

  • value =>该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 unfined
  • writable => 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。

就这两个 ? 还有吗 ?

  • configurable => 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。决定writable可不可改
  • enumerable => 当且仅当该属性的enumerabletrue时,该属性才能够出现在对象的枚举属性中。默认为 false

为什么 configurableenumerable 不同时 和 value 还有 writable一起写呢 ?

因为这两个属性不但可以在数据描述符里出现 还可以在 存取描述符里出现

我们通过writeable 和 value属性来写一个 可写的属性 和不写的属性

   var obj = {
          name: '曹扬'
      }
     Object.defineProperty(obj, 'money', {
         value: "10k" // 薪水 此时薪水是不可改的
     })
     Object.defineProperty(obj, 'weight', {
         value: '150斤', // 给一万根头发
         writable: true
     })
     obj.money = '20k'
     obj.hair = '200斤'
     console.log(obj)

接下来 ,我们希望 去让一个不可变的属性变成可变的

  var obj = {
          name: '曹扬'
      }
     Object.defineProperty(obj, 'money', {
         value: '10k', // 薪水 此时薪水是不可改的
         configurable: true  // 只有这里为true时 才能去改writeable属性
     })
     Object.defineProperty(obj, 'weight', {
         value: '150斤', // 给一万根头发
         writable: true
     })
     obj.money = "20k"
     obj.weight = '200斤'
     console.log(obj)
     Object.defineProperty(obj, 'money', {
         writable: true 
     })
     obj.money = '20k'
     console.log(obj)

接下来,我们希望可以在遍历的时候 遍历到新添加的两个属性

      var obj = {
          name: '曹扬'
      }
     Object.defineProperty(obj, 'money', {
         value: '10k', // 薪水 此时薪水是不可改的
         configurable: true,
         enumerable: true
     })
     Object.defineProperty(obj, 'weight', {
         value: '150斤', // 给一万根头发
         writable: true,
         enumerable: true

     })
     obj.money = "20k"
     obj.weight = '200斤'
     console.log(obj)
     Object.defineProperty(obj, 'money', {
         writable: true 
     })
     obj.money = '20k'
     console.log(obj)
     for(var item in obj) {
         console.log(item)
     }

Object.defineProperty()-存取描述符模式

上一小节中,数据描述符 独有的属性 是 value 和 writable , 这也就意味着, 在存取描述模式中

value 和 writable属性不能出现

那么 存储描述符有啥属性 ?

  • get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
  • set 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

get/set 其实就是我们最常见的 读取值 和设置值得方法

读取值得时候 调用 get方法

设置值得时候调用 set方法

我们做一个 可以 通过 get 和 set 读取设置的方法

  var obj = {
          name: '曹操'
      }
      var wife = '小乔'
      Object.defineProperty(obj, 'wife',{
          get () {
              return wife
          },
          set (value) {
             wife = value
          }
      })
      console.log(obj.wife)
     obj.wife= '大乔'
      console.log(obj.wife)

但是,我们想要遍历怎么办 ? 注意哦 , 存储描述符的时候 依然拥有 configurableenumerable属性,

依然可以配置哦

 var obj = {
          name: '曹操'
      }
      var wife = '小乔'
      Object.defineProperty(obj, 'wife',{
          configurable:true,
          enumerable: true,
          get () {
              return wife
          },
          set (value) {
             wife = value
          }
      })
      console.log(obj.wife)
     obj.wife= '大乔'
      console.log(obj.wife)
      for(var item in obj) {
          console.log(item)
      }

Object.defineProperty()-模拟vm对象

通过两个小节,学习了 defineProperty的基本使用, 接下里我们要通过defineProperty模拟 Vue实例化的效果

Vue实例化的时候, 我们明明给data赋值了数据,但是却可以通过 **vm实例.属性**进行访问和设置

怎么做的 ?

实际上这就是 通过 Object.defineProperty实现的

 var data = {
        name: "张三"
      };
      var vm = {};
      Object.defineProperty(vm, "name", {
        set(value) {
          data.name = value;
        },
        get() {
          return data.name;
        }
      });
      console.log(vm.name);
      vm.name = "李四";
      console.log(vm.name);

上面代码中,我们实现了 vm中的数据代理了 data中的name 直接改vm就是改data

总结: 我们在 set和get的存取描述符中 代理了 data中的数据,

MVVM =>数据代理 =>object.defineProperty =>存取描述符get/set =>代理数据

MVVM不但要获取这些数据,并且将这些数据 进行 响应式的更新到DOM中, 也就是 数据变化时,我们要把数据**反映**到视图上

通过调试我们发现,我们是可以在set函数里面监听到数据的变化的,只需要在数据变化的时候, 通知对应的视图来更新就可以了

那么 怎么通知 ? 用什么技术来做 ? 下一小节中我们将带来发布订阅模式

发布订阅模式的介绍

发布订阅模式为何物?

其实我们早已用过很多遍, 发布 /订阅 即 有人**发布消息**, 有人 订阅消息,到了 数据层面 就是 多 => 多

即 A程序 可以触发多个消息 也可以订阅 多个消息

在黑马头条项目1 和项目2 中我们 曾经 用过一个eventBus 就是发布订阅模式的体现

这个模式我们拿来做什么?

上个小节,我们已经能够捕捉数据的变化,接下来,我们就要尝试在数据变化的时候通过 发布订阅这个模式 来改变我们的视图

我们先写出这个发布订阅核心代码的几个要素

首先,我们希望 可以通过实例化 得到 发布订阅对象

发布消息 $emit

订阅消息 $on

根据上述思想,我们得到如下代码

 //  创建一个构造函数
      function Events (){}
        // 订阅消息
        Events.prototype.$on = function(){}
        //  发布消息
        Events.prototype.$emit = function(){}

发布订阅模式的实现

    function Events () {
            this.subs = {}
        }
        Events.prototype.$on = function (eventName, fn) {
           this.subs[eventName] = this.subs[eventName]  || []
           this.subs[eventName].push(fn)
        }
        Events.prototype.$emit = function (eventName, ...params) {
            if(this.subs[eventName]) {
                this.subs[eventName].forEach(fn => {
                    //fn.apply(this, [...params])
                    //fn.call(this, ...params)
                    fn.bind(this,...params)()
                });
            }
        }
        var test = new Events()
        test.$on("updateABC", function(a,b,c){
          console.log(a+'-'+b+'-'+c)
          console.log(this)
        })
        var go = function(){
            test.$emit("updateABC", 1,2,3)
        }

这里用到了call/apply/bind方法修改函数内部的this指向

利用发布订阅模式可以实现当事件触发时会通知到很多人去做事情,Vue中做的事情是更新DOM

MVVM实现-DOM复习

我们学习了 Object.defineProperty 和 发布订阅模式, 几乎拥有了手写一个MVVM的能力,

但是在实现MVVM之前,我们还是复习一下 View中也就是 Dom中的含义及结构

DOM是什么?

文档对象模型 document

Dom的作用是什么?

可以通过**对象**去操作页面元素

Dom中的对象节点都有什么类型

可以通过下面的一个小例子检查

  <div id="app">
        <h1>众志成城,共抗疫情</h1>
        <div>
            <span style='color:red;font-weight: bold;'>老高:</span>
            <span>祝所有同学前程似锦</span>
        </div>
    </div>
    <script>
       var app = document.getElementById("app")
       console.dir(app)
    </script>

通过上面的输出查看, 我们可以发现

元素类型的节点类型 nodeType 为1 文本类型为 3, document对象里面的每个内容都是**节点**

childNodes 是所有的节点 childer 值的是 所有的元素 =>nodeType =>节点类型

所有的子节点都放在 childNodes 这个属性下,childNodes是伪数组 => 伪数组不具有数组方法,有length属性

所有标签的属性集合是什么?

attributes

分析DOM对象做什么呢? 我们前面准备的数据捕获和 发布订阅就是为了来更新DOM的

接下来我们开始手写一个MVVM示例

MVVM实现-实现Vue的构造函数和数据代理

挑战来了,我们要手写 一个简易的**vuejs**, 提升我们自身的技术实力.

我们要实现mvvm的构造函数

构造函数 模仿vuejs 分别有 data /el

data最终被代理给当前的vm实例, 即可以通过 vm访问,也可以通过 this.$data访问

        // 首先实现一个构造函数
        function Vue (options) {
            this.$options = options // 所有属性都给this的一个属性
            this.$data = options.data || {}
            this.$el = typeof options.el ==="string"  ? document.querySelector(options.el) : options.el
            // 把所有data的数据 代理给 当前实例
            this.$proxyData()
        }
        // 代理数据
        Vue.prototype.$proxyData  = function () {
            Object.keys(this.$data).forEach(key => {
                Object.defineProperty(this, key, {
                    get () {
                        return this.$data[key]
                    },
                    set (value) {
                        if(this.$data[value] === value) return
                        this.$data[key] = value
                    }
                })
            })
        }
       var vm = new Vue({
          el: '#app',
          data: {
              name: '曹扬',
              company: '揽月一颗'
          }
        })
        console.log(vm.company)
        vm.company = '九天揽月'
        console.log(vm.$data.company)
        vm.$data.company = '下海捉鳖'
        console.log(vm.company)

MVVM实现-数据劫持Observer

OK,接下来这一步非常关键,我们要做**数据劫持**, 劫持谁? 为什么要劫持?

上小节代码中, 我们可以通过 vm.company = '值' 也可以通过 vm.$data.name = '值', 那么在哪里捕捉数据的变化呢?

不论是 this.data 还是 this.$data 改的都是data的数据,所以我们需要对 data的数据进行**劫持**, 也就是监听它的set

        // 监听数据
        Vue.prototype.observer = function () {
           Object.keys(this.$data).forEach(key => {
            let value =  this.$data[key]
            Object.defineProperty(this.$data, key, {
                get () {
                    return value
                },
                set (newValue) {
                    if(newValue === value) return;
                    value = newValue
                    // 如果数据变化了 我们需要 去改变视图
                }
            })
           })
        }

在构造函数中完成对数据的劫持

  // 首先实现一个构造函数
        function Vue (options) {
            this.$options = options // 所有属性都给this的一个属性
            this.$data = options.data || {}
            this.$el = typeof options.el ==="string"  ? document.querySelector(options.el) : options.el
            // 把所有data的数据 代理给 当前实例
            this.$proxyData()
            this.observer() // 开启监听数据
        }

MVVM实现-编译模板Compiler-设计结构

现在我们基本实现了 实例化数据,并且完成了对数据的劫持,接下来我们需要实现几个方法

数据变化时 => 根据最新数据把模板转化成最新的对象

判断是否是文本节点

判断是否是 元素节点

判断是否是指令

处理元素节点

处理文本节点

所以我们定义下面几个方法

        // 编译模板
        Vue.prototype.compile = function () {}
        // 处理文本节点
        Vue.prototype.compileTextNode = function (node){}
        // 处理元素节点
        Vue.prototype.compileElementNode = function (node) {}
        // 判断是否是文本节点
        Vue.prototype.isTextNode = function(node) {};
        // 判断是否是元素节点
       Vue.prototype.isElementNode = function(node) {};
        // 判断属性是否是指令
        Vue.prototype.isDirective = function(attr) {};

MVVM实现-编译模板Compiler实现基本的框架逻辑

我们已经通过构造函数拿到了$el,也就是页面的dom元素,接下来我们可以实现 一下编译的基本逻辑

        // 编译模板
        Vue.prototype.compile = function (rootnode) {
            let nodes = Array.from(rootnode.childNodes) // 先把伪数组转成数组
            nodes.forEach(item => {
                if(this.isTextNode(item)) {
                    this.compileTextNode(node)
                }
                if(this.isElementNode(item)) {
                    this.compileElementNode(node)
                    this.compile(node) // 递归的思路
                }
            })
        }
        // 处理文本节点
        Vue.prototype.compileTextNode = function (node){}
        // 处理元素节点
        Vue.prototype.compileElementNode = function (node) {}
        // 判断是否是文本节点
        Vue.prototype.isTextNode = function(node) {
            return node.nodeType === 3;
        };
        // 判断是否是元素节点
       Vue.prototype.isElementNode = function(node) {
         return node.nodeType === 1;
       };
        // 判断属性是否是执行
        Vue.prototype.isDirective = function(attr) {
            return attr.startsWith("v-");
        };

上述代码的基本逻辑就是 碰到 文本节点就用文本节点的方法处理 碰到元素节点 用元素节点的方法处理

MVVM实现-编译模板Compiler-处理文本节点

   // 处理文本节点
      Vue.prototype.compileTextNode = function (node){
            const text = node.textContent 
            const reg = /\{\{(.+?)\}\}/g
            if(reg.test(text)) {
                // 如果满足双大括号
                const key = RegExp.$1.trim()
                this.$on(key, () => {
                    node.textContent = text.replace(reg, this[key]) // 如果找到大括号 就替换对应的数据
                })
                node.textContent = text.replace(reg, this[key]) // 如果找到大括号 就替换对应的数据
            }
        }

提示: 实际开发时正则不需要记 但是要能看懂

MVVM实现-编译模板Compiler-处理元素节点

   // 处理元素节点
        Vue.prototype.compileElementNode = function (node) {
          let atts = Array.from(node.attributes) 
          attrs.forEach(attr => {
              if(this.isDirective(attr.name)) {
                //  判断是否是指令
                if(attr.name === 'v-text') {
                    node.textContent = this[attr.value] // 等于当前属性的值
                }
                if(attr.name === 'v-model') {
                    // v-model绑定的是表单的value属性
                    node.value = this[attr.value]
                }
              }
          })
        }

MVVM实现-数据驱动视图-发布订阅管理器

目前响应式数据有了, 编译模板也有了, 我们需要在数据变化的时候编译模板

之前讲了, 这一步需要 通过发布订阅来做 ,所以我们在Vue的基础上实现发布订阅

        // 首先实现一个构造函数
        function Vue (options) {
            this.subs = {} //发布订阅管理器
            this.$options = options // 所有属性都给this的一个属性
            this.$data = options.data || {}
            this.$el = typeof options.el ==="string"  ?                 document.querySelector(options.el) : options.el
            // 把所有data的数据 代理给 当前实例
            this.$proxyData()
            this.observer() // 开启监听数据
        }
       Vue.prototype.$on=  function (eventName, fn) {
           this.subs[eventName] = this.subs[eventName]  || []
           this.subs[eventName].push(fn)
        }
        Vue.prototype.$emit = function (eventName, ...params) {
            if(this.subs[eventName]) {
                this.subs[eventName].forEach(fn => {
                    //fn.apply(this, [...params])
                    //fn.call(this, ...params)
                    fn.bind(this,...params)()
                });
            }
        }

MVVM实现-数据变化时 驱动视图变化

现在万事俱备,只欠东风

我们的数据代理,数据劫持,模板编译, 事件发布订阅统统搞定 现在只需要在数据变化时 ,通过事件发布,然后

通知 数据进行编译即可

     // 监听数据
        Vue.prototype.observer = function () {
           Object.keys(this.$data).forEach(key => {
            let value =  this.$data[key]
            Object.defineProperty(this.$data, key, {
                get () {
                    return value
                },
                set: (newValue) => {
                    if(newValue === value) return;
                    value = newValue
                    
                    // 如果数据变化了 我们需要 去改变视图
                    this.$emit(key) //触发数据改变
                }
            })
           })
        }

监听数据改变

  // 处理文本节点
        Vue.prototype.compileTextNode = function (node){
            const text = node.textContent 
            if(/\{\{(.+)\}\}/.test(text)) {
                // 如果满足双大括号
                const key = RegExp.$1.trim()
                this.$on(key, () => {
                    node.textContent = text.replace(reg, this[key]) // 如果找到大括号 就替换对应的数据
                })
                node.textContent = text.replace(reg, this[key]) // 如果找到大括号 就替换对应的数据
            }
        }
        // 处理元素节点
        Vue.prototype.compileElementNode = function (node) {
          let atts = Array.from(node.attributes) 
          attrs.forEach(attr => {
              if(this.isDirective(attr.name)) {
                //  判断是否是指令
                if(attr.name === 'v-text') {
                    node.textContent = this[attr.value] // 等于当前属性的值
                    this.$on(attr.value, () => {
                       node.textContent =  this[attr.value] // 如果找到大括号 就替换对应的数据
                   })
                }
                if(attr.name === 'v-model') {
                    // v-model绑定的是表单的value属性
                    node.value = this[attr.value]
                    this.$on(attr.value, () => {
                       node.value =  this[attr.value] // 如果找到大括号 就替换对应的数据
                    })
                }
              }
          })
        }

然后我们写个例子来测试一把

MVVM实现-视图变化更新数据

最后我们希望实现双向绑定,即视图改变时 数据同时变化

        // 处理元素节点
        Vue.prototype.compileElementNode = function (node) {
          let attrs = Array.from(node.attributes) 
          attrs.forEach(attr => {
              if(this.isDirective(attr.name)) {
                //  判断是否是指令
                if(attr.name === 'v-text') {
                    node.textContent = this[attr.value] // 等于当前属性的值
                    this.$on(key, () => {
                       node.textContent =  this[attr.value] // 如果找到大括号 就替换对应的数据
                   })
                }
                if(attr.name === 'v-model') {
                    // v-model绑定的是表单的value属性
                    node.value = this[attr.value]
                    this.$on(key, () => {
                       node.value =  this[attr.value] // 如果找到大括号 就替换对应的数据
                    })
                    node.oninput = () => {
                        this[attr.value] = node.value
                    }
                }
              }
          })
        }
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!