对类Vue的MVVM前端库的实现

关于实现MVVM,网上实在是太多了,本文为个人总结,结合源码以及一些别人的实现

关于双向绑定

  • vue 数据劫持 + 订阅 - 发布
  • ng 脏值检查
  • backbone.js 订阅-发布(这个没有使用过,并不是主流的用法)

双向绑定,从最基本的实现来说,就是在defineProperty绑定的基础上在绑定input事件,达到v-model的功能

代码思路图

两个版本:

  • 简单版本: 非常简单,但是因为是es6,并且代码极度简化,所以不谈功能,思路还是很清晰的
  • 标准版本: 参照了Vue的部分源码,代码的功能高度向上抽取,阅读稍微有点困难,实现了基本的功能,包括计算属性,watch,核心功能都实现没问题,但是不支持数组

简单版本

简单版本的地址: 简单版本

? 这个MVVM也许代码逻辑上面实现的并不完美,并不是正统的MVVM, 但是代码很精简,相对于源码,要好理解很多,并且实现了v-model以及v-on methods的功能,代码非常少,就100多行

class MVVM {
  constructor(options) {
    const {
      el,
      data,
      methods
    } = options
    this.methods = methods
    this.target = null
    this.observer(this, data)
    this.instruction(document.getElementById(el)) // 获取挂载点
  }

  // 数据监听器 拦截所有data数据 传给defineProperty用于数据劫持
  observer(root, data) {
    for (const key in data) {
      this.definition(root, key, data[key])
    }
  }

  // 将拦截的数据绑定到this上面
  definition(root, key, value) {
    // if (typeof value === ‘object‘) { // 假如value是对象则接着递归
    //   return this.observer(value, value)
    // }
    let dispatcher = new Dispatcher() // 调度员

    Object.defineProperty(root, key, {
      set(newValue) {
        value = newValue
        dispatcher.notify(newValue)
      },
      get() {
        dispatcher.add(this.target)
        return value
      }
    })
  }

  //指令解析器
  instruction(dom) {
    const nodes = dom.childNodes; // 返回节点的子节点集合
    // console.log(nodes); //查看节点属性
    for (const node of nodes) { // 与for in相反 for of 获取迭代的value值
      if (node.nodeType === 1) { // 元素节点返回1
        const attrs = node.attributes //获取属性

        for (const attr of attrs) {
          if (attr.name === ‘v-model‘) {
            let value = attr.value //获取v-model的值

            node.addEventListener(‘input‘, e => { // 键盘事件触发
              this[value] = e.target.value
            })
            this.target = new Watcher(node, ‘input‘) // 储存到订阅者
            this[value] // get一下,将 this.target 给调度员
          }
          if (attr.name == "@click") {
            let value = attr.value // 获取点击事件名

            node.addEventListener(‘click‘,
              this.methods[value].bind(this)
            )
          }
        }
      }

      if (node.nodeType === 3) { // 文本节点返回3
        let reg = /\{\{(.*)\}\}/; //匹配 {{  }}
        let match = node.nodeValue.match(reg)
        if (match) { // 匹配都就获取{{}}里面的变量
          const value = match[1].trim()
          this.target = new Watcher(node, ‘text‘)
          this[value] = this[value] // get set更新一下数据
        }
      }
    }

  }
}

//调度员 > 调度订阅发布
class Dispatcher {
  constructor() {
    this.watchers = []
  }
  add(watcher) {
    this.watchers.push(watcher) // 将指令解析器解析的数据节点的订阅者存储进来,便于订阅
  }
  notify(newValue) {
    this.watchers.map(watcher => watcher.update(newValue))
    // 有数据发生,也就是触发set事件,notify事件就会将新的data交给订阅者,订阅者负责更新
  }
}

//订阅发布者 MVVM核心
class Watcher {
  constructor(node, type) {
    this.node = node
    this.type = type
  }
  update(value) {
    if (this.type === ‘input‘) {
      this.node.value = value // 更新的数据通过订阅者发布到dom
    }
    if (this.type === ‘text‘) {
      this.node.nodeValue = value
    }
  }
}
<!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>MVVM</title>
</head>

<body>
  <div id="app">
    <input type="text" v-model="text">{{ text }}
    <br>
    <button @click="update">重置</button>
  </div>

  <script src="./index.js"></script>
  <script>
    let mvvm = new MVVM({
      el: ‘app‘,
      data: {
        text: ‘hello MVVM‘
      },
      methods: {
        update() {
          this.text = ‘‘
        }
      }
    })
  </script>
</body>

</html>

这个版本的MVVM因为代码比较少,并且是ES6的原因,思路非常清晰

我们来看看从new MVVM开始,他都做了什么

解读简单版本

new MVVM

首先,通过解构获取所有的new MVVM传进来的对象

class MVVM {
  constructor(options) {
    const {
      el,
      data,
      methods
    } = options
    this.methods = methods // 提取methods,便于后面将this给methods
    this.target = null // 后面有用
    this.observer(this, data)
    this.instruction(document.getElementById(el)) // 获取挂载点
  }

属性劫持

开始执行this.observer observer是一个数据监听器,将data的数据全部拦截下来

observer(root, data) {
    for (const key in data) {
      this.definition(root, key, data[key])
    }
  }

在this.definition里面把data数据都劫持到this上面

 definition(root, key, value) {
    if (typeof value === ‘object‘) { // 假如value是对象则接着递归
      return this.observer(value, value)
    }
    let dispatcher = new Dispatcher() // 调度员

    Object.defineProperty(root, key, {
      set(newValue) {
        value = newValue
        dispatcher.notify(newValue)
      },
      get() {
        dispatcher.add(this.target)
        return value
      }
    })
  }

此时data的数据变化我们已经可以监听到了,但是我们监听到后还要与页面进行实时相应,所以这里我们使用调度员,在页面初始化的时候get(),这样this.target,也就是后面的指令解析器解析出来的v-model这样的指令储存到调度员里面,主要请看后面的解析器的代码

指令解析器

指令解析器通过执行 this.instruction(document.getElementById(el)) 获取挂载点

instruction(dom) {
    const nodes = dom.childNodes; // 返回节点的子节点集合
    // console.log(nodes); //查看节点属性
    for (const node of nodes) { // 与for in相反 for of 获取迭代的value值
      if (node.nodeType === 1) { // 元素节点返回1
        const attrs = node.attributes //获取属性

        for (const attr of attrs) {
          if (attr.name === ‘v-model‘) {
            let value = attr.value //获取v-model的值

            node.addEventListener(‘input‘, e => { // 键盘事件触发
              this[value] = e.target.value
            })
            this.target = new Watcher(node, ‘input‘) // 储存到订阅者
            this[value] // get一下,将 this.target 给调度员
          }
          if (attr.name == "@click") {
            let value = attr.value // 获取点击事件名

            node.addEventListener(‘click‘,
              this.methods[value].bind(this)
            )
          }
        }
      }

      if (node.nodeType === 3) { // 文本节点返回3
        let reg = /\{\{(.*)\}\}/; //匹配 {{  }}
        let match = node.nodeValue.match(reg)
        if (match) { // 匹配都就获取{{}}里面的变量
          const value = match[1].trim()
          this.target = new Watcher(node, ‘text‘)
          this[value] = this[value] // get set更新一下数据
        }
      }
    }

  }

这里代码首先解析出来我们自定义的属性然后,我们将@click的事件直接指向methods,methds就已经实现了

现在代码模型是这样

调度员Dispatcher与订阅者Watcher

我们需要将Dispatcher和Watcher联系起来

于是我们之前创建的变量this.target开始发挥他的作用了

正执行解析器里面使用this.target将node节点,以及触发关键词存储到当前的watcher 订阅,然后我们获取一下数据

this.target = new Watcher(node, ‘input‘) // 储存到订阅者
this[value] // get一下,将 this.target 给调度员

在执行this[value]的时候,触发了get事件

get() {
   dispatcher.add(this.target)
   return value
}

这get事件里面,我们将watcher订阅者告知到调度员,调度员将订阅事件存储起来

//调度员 > 调度订阅发布
class Dispatcher {
  constructor() {
    this.watchers = []
  }
  add(watcher) {
    this.watchers.push(watcher) // 将指令解析器解析的数据节点的订阅者存储进来,便于订阅
  }
  notify(newValue) {
    this.watchers.map(watcher => watcher.update(newValue))
    // 有数据发生,也就是触发set事件,notify事件就会将新的data交给订阅者,订阅者负责更新
  }
}

与input不太一样的是文本节点不仅需要获取,还需要set一下,因为要让订阅者更新node节点

this.target = new Watcher(node, ‘text‘)
this[value] = this[value] // get set更新一下数据

所以在订阅者就添加了该事件,然后执行set

set(newValue) {
        value = newValue
        dispatcher.notify(newValue)
      },

notfiy执行,订阅发布者执行update更新node节点信息

class Watcher {
  constructor(node, type) {
    this.node = node
    this.type = type
  }
  update(value) {
    if (this.type === ‘input‘) {
      this.node.value = value // 更新的数据通过订阅者发布到dom
    }
    if (this.type === ‘text‘) {
      this.node.nodeValue = value
    }
  }
}

页面初始化完毕

更新数据

node.addEventListener(‘input‘, e => { // 键盘事件触发
   this[value] = e.target.value
})

this[value]也就是data数据发生变化,触发set事件,既然触发notfiy事件,notfiy遍历所有节点,在遍历的节点里面根据页面初始化的时候订阅的触发类型.进行页面的刷新

现在可以完成的看看new MVVM的实现过程了

最简单版本的MVVM完成

标准版本

标准版本额外实现了component,watch,因为模块化代码很碎的关系,看起来还是有难度的

从理念上来说,实现的思想基本是一样的,可以参照上面的图示,都是开始的时候都是拦截属性,解析指令

代码有将近300行,所以就贴一个地址标准版本MVVM

执行顺序

  1. new MVVM
  2. 获取$options = 所以参数
  3. 获取data,便于后面劫持
  4. 因为是es5,后面forEach内部指向window,这不是我们想要的,所以存储当前this 为me
  5. _proxyData劫持所有data数据
  6. 初始化计算属性
  7. 通过Object.key()获取计算属性的属性名
  8. 初始化计算属性将计算属性挂载到vm上
  9. 开始observer监听数据
  10. 判断data是否存在
  11. 存在就new Observer(创建监听器)
  12. 数据全部进行进行defineProperty存取监听处理,让后面的数据变动都触发这个的get/set
  13. 开始获取挂载点
  14. 使用querySelector对象解析el
  15. 创建一个虚拟节点,并存储当前的dom
  16. 解析虚拟dom
  17. 使用childNodes解析对象
  18. 因为是es5,所以使用[].slice.call将对象转数组
  19. 获取到后进行 {{ }}匹配 指令的匹配 以及递归子节点
  20. 指令的匹配: 匹配到指令因为不知道多少个指令名称,所以这里还是使用[].slice.call循环遍历
  21. 解析到有 v-的指令使用substring(2)截取后面的属性名称
  22. 再判断是不是指令v-on 这里就是匹配on关键字,匹配到了就是事件指令,匹配不到就是普通指令
  23. 普通指令解析{{ data }} _getVMValget会触发MVVM的_proxyData事件 在_proxyData事件里面触发data的get事件
  24. 这时候到了observer的defineReactive的get里面获取到了数据,因为没有Dispatcher.target,所以不进行会触发调度员
  25. 至此_getVMVal获取到了数据
  26. modelUpdater进行Dom上面的数据更新
  27. 数据开始进行订阅,在订阅里面留一个回调函数用于更新dom
  28. 在watcher(订阅者)获取this订阅的属性回调
  29. 在this.getter这个属性上面返回一个匿名函数,用于获取data的值
  30. 触发get事件,将当前watcher的this存储到Dispatcher.garget上面
  31. 给this.getters,callvm的的this,执行匿名函数,获取劫持下来的data,又触发了MVVM的_proxyData的get事件,继而有触发了observer的defineReactive的get事件,不过这一次Dispatcher.target有值,执行了depend事件
  32. depend里面执行了自己的addDep事件,并且将Observer自己的this传进去
  33. addDep里面执行了DispatcheraddSub事件,
  34. addUsb事件里面将订阅存储到Dispatcher里面的this.watchers里面的
  35. 订阅完成,后面将这些自定义的指令进行移除
  36. 重复操作,解析所有指令,v-on:click = "data"直接执行methods[data].bind(vm)

更新数据:

  1. 触发input事件
  2. 触发_setVMVal事件
  3. 触发MVVM的set事件
  4. 触发observer的set事件
  5. 触发dep.notify()
  6. 触发watcher的run方法
  7. 触发new Watcher的回调 this.cb
  8. 触发compile里面的updaterFn 事件
  9. 更新视图

component的实现

计算属性的触发 查看这个例子

computed: {
        getHelloWord: function () {
          return this.someStr + this.child.someStr;
        }
      },

其实计算属性就是defineproperty的一个延伸

  1. 首先compile里面解析获取到{{ getHelloword }}‘
  2. 执行updater[textUpdater]
  3. 执行_getVMVal获取计算属性的返回值
  4. 获取vm[component]就会执行下面的get事件
Object.defineProperty(me, key, {
          get: typeof computed[key] === ‘function‘ ? computed[key] : computed[key].get,
          set: function () {}
        })

是function执行computed[getHelloword],也就是return 的 函数

this.someStr + this.child.someStr;
  1. 依次获取data,触发mvvm的get 以及observer的get,

初始化完成,到这里还没有绑定数据,仅仅是初始化完成了

  1. 开始订阅该事件 new Watcher()
  2. component不是函数所以不是function 执行this.parseGetter(expOrFn);
  3. 返回一个覆盖expOrrn的匿名函数
  4. 开始初始化 执行get()
  5. 存储当前this,开始获取vm[getHelloword]
  6. 触发component[getHelloword]
  7. 开始执行MVVM的get this.someStr
  8. 到MVVM的get 到 observer的get 因为 Dispatcher.target存着 getHelloWord 的 this.depend ()所以执行
  9. Dispatcher的depend(),执行watcher的addDep(),执行 Dispatcher的addSub() 将当前的watcher存储到监听器
  10. 开始get第二个数据 this.child.someStr,同理也将getHelloWord的this存入了当前的Dispatcher
  11. 开始get第三个数据 this.child,同理也将getHelloWord的this存入了当前的Dispatcher

这个执行顺序有点迷,第二第三方反来了

this.parseGetter(expOrFn);就执行完毕了

目前来看为什么component会实时属性数据?

因为component的依赖属性一旦发生变化都会更新 getHelloword 的 watcher ,随之执行回调更新dom

watch的实现

watch的实现相对来说要简单很多

  1. 我们只要将watch监听的数据告诉订阅者就可以了
  2. 这样,wacth更新了
  3. 触发set,set触发notify
  4. notify更新watcher
  5. watcher执行run
  6. run方法去执行watch的回调
  7. 即完成了watch的监听
watch: function (key, cb) {
    new Watcher(this, key, cb)
},

原文地址:https://www.cnblogs.com/wuvkcyan/p/9602562.html

时间: 2024-10-21 15:45:18

对类Vue的MVVM前端库的实现的相关文章

Vue 浅谈前端js框架vue

Vue Vue近几年来特别的受关注,三年前的时候angularJS霸占前端JS框架市场很长时间,接着react框架横空出世,因为它有一个特性是虚拟DOM,从性能上碾轧angularJS,这个时候,vue1.0悄悄 的问世了,它的优雅,轻便也吸引了一部分用户,开始收到关注,16年中旬,VUE2.0问世,这个时候vue不管从性能上,还是从成本上都隐隐超过了react,火的一塌糊涂,这个时候,angular 开发团队也开发了angular2.0版本,并且更名为angular,吸收了react.vue的

现代前端库开发指南系列(一):融入现代前端生态

本系列文章讲什么内容? 本系列文章主要介绍如何在现代前端生态下,创建一个工业级别的库.近几年来,前端工程化.模块化.组件化的大潮铺天盖地而来,在解决以往的架构痛点之余,却又产生了信息过载的问题:我希望通过分享自己的经验,帮助大家少踩坑多出活. 为什么需要开发一个前端库呢? 在项目开发过程中,总有一些功能是相同或类似的,如果你只是单纯地复制粘贴这部分代码,那么恭喜你,假以时日,需求一改,你就只能自尝苦果了. 你说,这还不简单,抽成公共方法公用不就好了吗?对的没错,但请把视野再放广一点:在工作中,我

现代前端库开发指南系列(三):从说明文档看库的前世今生

前言 我们在工作中很多时候都要做技术选型,去找寻既能满足自己需求又靠谱的第三方库:在前端开源生态季度繁盛的现状下,只要不是太小众的需求,我们很容易就能找到一堆相关的开源库,那我们具体要怎么做决策呢?我的做法是,先阅读开源库的说明文档让自己有一个感性的认识,然后挑选出其中的两三个库来进行更深入更全面的了解.如此说来,这说明文档是不是就很像我们求职时的简历呢?"简历"关都过不了,何谈"offer"啊! 本文将介绍一个库(即不局限于前端领域)所要具备的说明文档,主要包括

[转]VUE优秀UI组件库合集

原文链接 随着SPA.前后端分离的技术架构在业界越来越流行,前端的业务复杂度也越来越高,导致前端开发者需要管理的内容,承担的职责越来越多,这一切,使得业界对前端开发方案的思考多了很多,以react.vue等框架为代表推动的组件化开发模式越来越被开发者认可,这种模式极大的降低了我们开发与维护的成本.vue作为一款深受广大群众以及尤大崇拜者的喜欢,特此列出在github上开源的vue优秀UI组件库供大家参考,期待开发者们推出更多优秀的组件库. 本文分为两大部分介绍:PC端和移动端. 首先介绍PC端

在ASP.NET 5中如何方便的添加前端库

(此文章同时发表在本人微信公众号“dotNET每日精华文章”,欢迎右边二维码来关注.) 题记:ASP.NET 5和之前的ASP.NET版本有很大的不同,其中之一就是对前端库的管理不再使用Nuget,而是使用业界常用的做法——依赖Bower来管理.那么如何方便的添加前端库呢,今天就简单分享一下我的心得. 要通过Bower来添加前端库(以之前文章介绍过的MetroUI安装为例),打开项目下面的bower.json文件,在“dependencies”里面,添加一行描述:"metro": &q

vue的MVVM

vue的相关知识有 MVVM 虚拟dom和domdiff 字符串模板 MVVM MVVM 设计模式,是由 MVC(最早来源于后端).MVP 等设计模式进化而来 M - 数据模型(Model) VM - 视图模型(ViewModel) V - 视图层(View) 在 Vue 的 MVVM 设计中,我们主要针对Compile(模板编译),Observer(数据劫持),Watcher(数据监听),Dep(发布订阅)几个部分来实现,核心逻辑流程可参照下图: 数据监听API vue2.0和vue2.x是用

目前流行前端几大UI框架 ----vue Vue的UI组件库

在前端项目开发过程中,总是会引入一些UI框架,已为方便自己的使用,很多大公司都有自己的一套UI框架,下面就是最近经常使用并且很流行的UI框架. 一.Mint UI 屏幕快照 2019-01-18 下午3.03.59.png Mint UI是 饿了么团队开发基于vue .js的移动端UI框架,它包含丰富的 CSS 和 JS 组件,能够满足日常的移动端开发需要. 官网:https://mint-ui.github.io/#!/zh-cn Github: https://github.com/Elem

从Vue.js窥探前端行业

近年来前端开发趋势 1.旧浏览器逐渐淘汰,移动端需求增加: 旧浏览器主要指的是IE6-IE8,它是不支持ES5特性的:IE9+.chrome.sarafi.firefox对ES5是完全支持的,移动端大部分浏览器是基于webkit内核,所以ES5在移动端也是全面支持的,因此vue可以在移动端以及现代浏览器中大显身手. 2.前端交互越来越多,功能越来越复杂: 现在的前端可谓是包罗万象,比如高大上的技术库和框架.酷炫的运营活动页面.H5小游戏,当然前端技术的应用在更多具有商业价值的应用上,比如下图.

vue和mvvm的一些小区别

Vue.js 和 MVVM 小细节 MVVM 是Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定. Vue.js 是一个提供了 MVVM 风格的双向数据绑定的 Javascript 库,专注于View 层.它的核心是 MVVM 中的 VM,也就是 ViewModel. ViewModel负责连接 View 和 Mode