MVVM原理及其实现(1)

栏目: 后端 · 发布时间: 5年前

内容简介:MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。要实现一个mvvm的库,我们首先要理解清楚其实现的整体思路。先看看下图的流程:

一. 什么是mvvm

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。

MVVM原理及其实现(1)

要实现一个mvvm的库,我们首先要理解清楚其实现的整体思路。先看看下图的流程:

MVVM原理及其实现(1)

1.实现compile,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且还需要绑定好更新函数;

2.实现Observe,监听所有的数据,并对变化数据发布通知;

3.实现watcher,作为一个中枢,接收到observe发来的通知,并执行compile中相应的更新方法。

4.结合上述方法,向外暴露mvvm方法。

二. 实现方法

首先编辑一个html文件,如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>MVVM原理及其实现</title>
</head>
<body>
<div id="app">
  <input type="text" v-model="message">
  <div>{{message}}</div>
  <ul><li></li></ul>
</div>
<script src="watcher.js"></script>
<script src="observe.js"></script>
<script src="compile.js"></script>
<script src="mvvm.js"></script>
<script>
  let vm = new MVVM({
    el: '#app',
    data: {
      message: 'hello world',
      a: {
        b: 'bbb'
      }
    }
  })
</script>
</body>
</html>

1.实现一个mvvm类(入口)

新建一个mvvm.js,将参数通过options传入mvvm中,并取出el和data绑定到mvvm的私有变量$el和$data中。

// mvvm.js
class MVVM {
  constructor(options) {
    this.$el = options.el
    this.$data = options.data
  }
}

2.实现compile(编译模板)

新建一个compile.js文件,在mvvm.js中调用compile。compile.js接收mvvm中传过来的el和vm实例。

// mvvm.js
class MVVM {
  constructor(options) {
    this.$el = options.el
    this.$data = options.data
    // 如果有要编译的模板 =>编译
    if(this.$el) {
      // 将文本+元素模板进行编译
      new Compile(this.$el, this)
    }
  }
}

(1)初始化传值

// compile.js
export default class Compile {
  constructor(el, vm) {
    // 判断是否是元素节点,是=》取该元素 否=》取文本
    this.el = this.isElementNode(el) ? el:document.querySelector(el)
    this.vm = vm
  },
  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
}

(2)先把真实DOM移入到内存中 fragment,因为fragment在内存中,操作比较快

// compile.js
class Compile {
  constructor(el, vm) {
    // 判断是否是元素节点,是=》取该元素 否=》取文本
    this.el = this.isElementNode(el) ? el:document.querySelector(el)
    this.vm = vm
    // 如果这个元素能获取到 我们才开始编译
    if(this.el) {
      // 1. 先把真实DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el)
    }
  },
  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 将el中的内容全部放到内存中
  node2fragment(el) { 
    let fragment = document.createDocumentFragment()
    let firstChild
    // 遍历取出firstChild,直到firstChild为空
    while (firstChild = el.firstChild) {
      fragment.appendChild(firstChild)
    }
    return fragment // 内存中的节点
  }
}

(3)编译 =》 在fragment中提取想要的元素节点 v-model 和文本节点

// compile.js
class Compile {
  constructor(el, vm) {
    // 判断是否是元素节点,是=》取该元素 否=》取文本
    this.el = this.isElementNode(el) ? el:document.querySelector(el)
    this.vm = vm
    // 如果这个元素能获取到 我们才开始编译
    if(this.el) {
      // 1. 先把真实DOM移入到内存中 fragment
      let fragment = this.node2fragment(this.el)
      // 2. 编译 =》 在fragment中提取想要的元素节点 v-model 和文本节点
      this.compile(fragment)
      // 3. 把编译好的fragment在放回到页面中
      this.el.appendChild(fragment)
    }
  }
  // 判断是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 是不是指令
  isDirective(name) {
    return name.includes('v-')
  }
  // 将el中的内容全部放到内存中
  node2fragment(el) {
    let fragment = document.createDocumentFragment()
    let firstChild
    // 遍历取出firstChild,直到firstChild为空
    while (firstChild = el.firstChild) {
      fragment.appendChild(firstChild)
    }
    return fragment // 内存中的节点
  }
  //编译 =》 提取想要的元素节点 v-model 和文本节点
  compile(fragment) {
    // 需要递归
    let childNodes = fragment.childNodes
    Array.from(childNodes).forEach(node => {
      // 是元素节点 直接调用文本编译方法 还需要深入递归检查
      if(this.isElementNode(node)) {
        this.compileElement(node)
        // 递归深入查找子节点
        this.compile(node)
      // 是文本节点 直接调用文本编译方法
      } else {
        this.compileText(node)
      }
    })
  }
  // 编译元素方法
  compileElement(node) {
    let attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      let attrName = attr.name
      // 判断属性名是否包含 v-指令
      if(this.isDirective(attrName)) {
        // 取到v-指令属性中的值(这个就是对应data中的key)
        let expr = attr.value
        // 获取指令类型
        let [,type] = attrName.split('-')
        // node vm.$data expr
        compileUtil[type](node, this.vm, expr)
      }
    })
  }
  // 这里需要编译文本
  compileText(node) {
    //取文本节点中的文本
    let expr = node.textContent
    let reg = /\{\{([^}]+)\}\}/g
    if(reg.test(expr)) {
      // node this.vm.$data text
      compileUtil['text'](node, this.vm, expr)
    }
  }
}
// 解析不同指令或者文本编译集合
const compileUtil = {
  text(node, vm, expr) { // 文本
    let updater = this.updater['textUpdate']
    updater && updater(node, getTextValue(vm, expr))
  },
  model(node, vm, expr){ // 输入框
    let updater = this.updater['modelUpdate']
    updater && updater(node, getValue(vm, expr))
  },
  // 更新函数
  updater: {
    // 文本赋值
    textUpdate(node, value) {
      node.textContent = value
    },
    // 输入框value赋值
    modelUpdate(node, value) {
      node.value = value
    }
  }
}
// 辅助 工具 函数
// 绑定key上对应的值,从vm.$data中取到
const getValue = (vm, expr) => {
  expr = expr.split('.') // [message, a, b, c]
  return expr.reduce((prev, next) => {
    return prev[next]
  }, vm.$data)
}
// 获取文本编译后的对应的数据
const getTextValue = (vm, expr) => {
  return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
    return getValue(vm, arguments[1])
  })
}

(3) 将编译后的fragment放回到dom中

let fragment = this.node2fragment(this.el)
  this.compile(fragment)
  // 3. 把编译好的fragment在放回到页面中
  this.el.appendChild(fragment)

进行到这一步,页面上初始化应该渲染完成了。如下图:

MVVM原理及其实现(1)


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

移动互联网商规28条

移动互联网商规28条

王吉斌、彭盾、程成 / 机械工业出版社 / 2014-6 / 49.00

每一次信息技术革命都会颠覆很多行业现有的商业模式和市场规则,当前这场移动互联网变革的波及面之广和蔓延速度之快,完全超出我们的想象。行业的边界被打破并互相融合,在此之前,我们只面临来自同行业的竞争,但是今天,我们不知道竞争对手会来自哪里。也许今天我们还是行业的巨人,但是明天就会被踩在脚下,当我们的体温犹热时,新的巨人已经崛起。诺基亚等传统科技巨头的衰退告诉我们,企业在一个时代的优势,到了另外一个新时......一起来看看 《移动互联网商规28条》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具