简单的实现MVVM框架

VUE作为国内最火的 MVVM框架,必须学习一下框架的实现原理。我们通过一个小的demo来理解一下 VUE2.x到底是如何实现MVVM的。

什么是MVVM

在前端领域, MVVM的出现无疑极大的节省了开发人员的心智,用 jquery 操作dom渲染页面的日子一去不复返。

  • Model:数据模型
  • View:视图
  • View-Model:Model和View的桥梁,通过它,数据模型的变化可以直接反应到视图上,同样在视图上修改数据也会反应到数据模型。

所以 MVVM的核心在于,数据驱动视图和双向数据绑定

数据驱动视图

数据驱动视图的关键在于,如何知道数据发生了变更,假如我们能监听到数据变更,然后再渲染相应的dom就可以实现数据驱动视图了。

Object.defineProperty

实际上官方提供了方法Object.defineProperty.通过该方法,我们可以动态的创建对象属性,并且监听属性的读取和修改操作。vue2.x便是使用了该方法。
先看一个简单的例子:

var data = {}
  Object.defineProperty(data,'name',{})
  console.log(data)

在这里插入图片描述
可以看到我们给 data对象增加了一个name属性
那我们如何监听该属性的变化呢,defineProperty()方法的第三个参数是属性描述,描述里的set属性会在name被读取时触发,get属性会在name属性被赋值时触发。
我们试一下

 var data = {}
  Object.defineProperty(data, 'name', {
    get(){
      console.log('我被读取了')
    },
    set(newVal){
      console.log('我被赋值了')
    }
  })
  //触发get
  data.name
  //触发set
  data.name = '张三'

在这里插入图片描述
可以看到赋值和读取都可以触发相应的函数。
对于setget的定义如下:

get
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。

set
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined。

需要注意标黄的部分,get在不设置return的情况下会始终返回undefined,这意味着我们必须正确的使用get才能取到属性正确的值。

看下错误的例子

 var data = {
    name: '李四'
  }
  Object.defineProperty(data, 'name', {
    get(){
      console.log('我被读取了')
    },
    set(newVal){
      console.log('我被赋值了')
    }
  })
  //触发set
  data.name = '张三'
  //触发get
  console.log('赋值之后重新获取属性值:',data.name)

在这里插入图片描述
取到了undefined
那我们直接在 getreturn data.name呢,这是不行的。因为return data.name本身也会触发get函数,这显然会导致死循环。

get和set联动

那如何实现setget的联动呢?
导致死循环的根本原因在于我们使用了原对象,我们可以借助中间变量来解决这个问题。

var data = {
    name: '李四'
  }
  var _name
  Object.defineProperty(data, 'name', {
    get(){
      console.log('我被读取了')
      return _name
    },
    set(newVal){
      console.log('我被赋值了')
      _name = newVal
    }
  })
  //触发set
  data.name = '张三'
  //触发get
  console.log('赋值之后重新获取属性值:',data.name)

在这里插入图片描述
借助中间变量 name实现了setget的联动。
接下来就是把属性值渲染到试图了。

数据渲染

拿到修改之后的数据直接渲染到页面上即可,之后data.name的修改都会反应到视图上

<body>
  <div id="app">
  </div>
</body>
<script>

  var data = {
    name: '李四'
  }
  var _name
  Object.defineProperty(data, 'name', {
    get() {
      console.log('我被读取了')
      return _name
    },
    set(newVal) {
      console.log('我被赋值了')
      _name = newVal
      document.getElementById('app').innerText = newVal
    }
  })
  //触发set
  data.name = '张三'
  //触发get
  console.log('赋值之后重新获取属性值:', data.name)
</script>
</html>

基本思路就是这些,接下来我们看怎么优化?

优化

要优化,自然要分析一下现在的实现方式的缺点是什么

  1. 实际的应用场景里,data的属性会有多个,我们要想办法遍历data的所有属性进行绑定。
  2. 对于上面的例子,如果有新的元素使用了data.name属性,就需要在set方法里新增加一条渲染语句,这显然是无法接受的。

通过Object.keys()方法可以获取对象的所有属性,循环遍历就可以为每个属性。
每个属性需要渲染的元素不同,我们可以通过属性名称标识属性来区分。这便是指令的由来。指令就是元素的自定义属性,该属性用于建立元素和数据的联系。

比如我们规定 v-text属性表示,我们会修改元素的innerText,而innerText的值由v-text指向的数据模型决定
代码可以优化成

<body>
<div id="app">
  <!-- 表示需要将该元素的 innerText 渲染成name的值 -->
  <p v-text="name"></p>
  <p v-text="age"></p>
</div>
</body>
<script>

  var data = {
    name: '张三',
    age: 12
  }

  Object.keys(data).forEach((item) => {
    //声明中间变量
    let dataProperty = data[item]
    Object.defineProperty(data, item, {
      get() {
        return dataProperty
      },
      set(newVal) {
        //值没变不用重新渲染
        if (dataProperty === newVal) {
          return
        }
        dataProperty = newVal
        //必须指定根节点,通过根节点寻找元素
        document.getElementById('app').childNodes.forEach((ele) => {
          //只找 元素类型的节点
          if (ele.nodeType === 1) {
            //将 属性 和 对应的元素建立联系
            if (ele.getAttribute('v-text') === item) {
              ele.innerText = newVal
            }
          }
        })
      }
    })
  })
  data.name = '李四'
  data.age = 15
</script>

这样我们在修改属性值的时候,对应的元素就会重新渲染。
当然这个版本还有需要优化的地方,我们每次修改属性,都会从根节点重新寻找目标元素,这显然是不合理的。我们先不去关注这些细节,只需要理解思路。

解决了数据驱动视图,接下来是双向数据绑定。

双向数据绑定

首先上面我们已经解决了从m到v的数据渲染,接下来要解决 v到m。
比如说我们修改了input框的值,如何影响到 model层面呢?

其实很简单,就是 事件监听,监听inputonChange事件。

<body>
  <div id="app">
    <!-- 表示需要将该元素的 innerText 渲染成name的值 -->
    <p v-text="name"></p>
    <p v-text="age"></p>
    <input type="text" v-model="sex" />
  </div>
</body>
<script>

  var data = {
    name: '张三',
    age: 12,
    sex: '男'
  }

  Object.keys(data).forEach((item) => {
    //声明中间变量
    let dataProperty = data[item]
    Object.defineProperty(data, item, {
      get() {
        return dataProperty
      },
      set(newVal) {
        //值没变不用重新渲染
        if (dataProperty === newVal) {
          return
        }
        dataProperty = newVal
        //必须指定根节点,通过根节点寻找元素
        document.getElementById('app').childNodes.forEach((ele) => {
          //只找 元素类型的节点
          if (ele.nodeType === 1) {
            //将 属性 和 对应的元素建立联系
            if (ele.getAttribute('v-text') === item) {
              ele.innerText = newVal
            }
            //和 v-text 不同,修改的是元素的 value
            if (ele.getAttribute('v-model') === item) {
              ele.value = newVal
              //元素 value变更影响 model
              ele.onchange = function () {
                data[item] = ele.value
              }
            }
          }
        })
      }
    })
  })
  data.name = '李四'
  data.age = 15
  data.sex = '女'
</script>

我们改data.sex的时候input框里的值会变,修改input框的输入data.sex也会变。
这便实现了双向数据绑定。

优化

思考一下上面方案的缺点,再进一步改良

  1. 使用面向对象的方式,暴露统一的构造函数。
  2. 使用观察者模式,对数据变化进行监听,用闭包保存需要渲染的节点,避免重复查找元素。
  3. 上面的例子里只支持层元素,可以使用递归遍历所有后代节点进行动态绑定。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

csw_coder

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值