最近看了珠峰的架构课——实现一个MVVM。

首先,我们来了解一下什么是MVVM。

MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑。

先贴一下代码,然后再做分析。

<!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>myMVVM</title>
</head>
<body>
<div id="root">
<input type="text" v-model='person.name' >
<h1>{{message}}</h1>
<ul>
<li>1</li>
<li>2</li>
</ul>
{{person.age}}
<br />
<button v-on:click="change">点我</button>
<br />
{{getNewName}}
<div v-html="message"></div>
</div>
<!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
<script src="Vue.js"></script>
<script>
let vm = new Vue({
el: '#root',
data: {
message: 'she is a bad girl',
person: {
age: 20,
name: 'zhangsan'
}
},
computed: {
getNewName() {
return this.person.name + 'hahaha'
}
},
methods: {
change() {
console.log("11111")
}
} })
</script>
</body>
</html>

MVVM的简易实现(还有很多功能没有涉及到)

/**
* 订阅发布 调度中心
*/
class Dep {
constructor() {
this.subs = [] // 存放所有的watcher
}
// 添加watcher, 订阅
addSub(watcher) {
this.subs.push(watcher)
}
// 发布
notify() {
this.subs.forEach(watcher => watcher.update())
}
} /**
* 观察者模式 观察者,被观察者
*/
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb // 默认先存放旧值
this.oldValue = this.get(vm, expr)
}
// 获取旧的值
get() {
Dep.target = this
let value = CompileUtil.getVal(this.vm, this.expr)
Dep.target = null
return value
}
// 数据更新
update() {
let newVal = CompileUtil.getVal(this.vm, this.expr)
if(newVal != this.oldValue) {
this.cb(newVal)
}
}
} /**
* 实现数据劫持
*/
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
if(data && typeof data == 'object') {
for(let key in data) {
this.defineReactive(data, key, data[key])
}
}
} defineReactive(obj, key, value) {
this.observe(value)
let dep = new Dep() // 给每一个属性都加上一个具有发布订阅的功能
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newValue) => {
if(newValue != value) {
this.observe(newValue)
value = newValue
dep.notify()
}
}
})
}
} /**
* 编译模板
*/
class Compiler {
constructor(el, vm) {
this.vm = vm
// 判断el是否是个元素,如果不是就获取它
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// console.log(this.el)
// 把当前节点中的元素放到内存中
let fragment = this.node2fragment(this.el) // 把节点中的内容进行替换 // 编译模板 用数据来编译
this.compile(fragment) // 把内容再塞回页面中
this.el.appendChild(fragment)
} // 是否是指令 v-开头的
isDirective(attrName) {
// startsWith('v-') 或 split('-')
return attrName.indexOf('v-') !== -1
}
// 编译元素
compileElement(node) {
let attributes = node.attributes
// [...attributes] 或 Array.from 等价 Array.prototype.slice.call
Array.from(attributes).forEach(attr => {
let { name, value: expr } = attr
if(this.isDirective(name)) {
//
let [, directive] = name.split('-')
// console.log(name,node, expr, directive)
if(directive.indexOf(':' !== -1)) {
let [directiveName, eventName] = directive.split(':')
CompileUtil[directiveName](node, expr, this.vm, eventName)
} else {
CompileUtil[directive](node, expr, this.vm)
} }
})
}
// 编译文本 找{{}}
compileText(node) {
let content = node.textContent
if(/\{\{(.+?)\}\}/.test(content)) {
// console.log(content) // 找到所有文本
CompileUtil['text'](node, content, this.vm)
}
} // 编译内存中的dom节点 核心的编译方法
compile(node) {
let childNodes = node.childNodes
Array.from(childNodes).forEach(child => {
if(this.isElementNode(child)) {
this.compileElement(child)
// 如果是元素的话,需要除去自己,再去遍历子节点
this.compile(child)
} else {
this.compileText(child)
}
})
}
// 把节点移动到内存中 appendChild方法
node2fragment(node) {
let fragment = document.createDocumentFragment()
let firstChild
while(firstChild = node.firstChild) {
fragment.appendChild(firstChild)
} return fragment
}
// 判断是否为元素节点
isElementNode(node) {
return node.nodeType === 1
}
} /**
* 编译工具函数
*/
CompileUtil = {
// 根据表达式取对应的数据
getVal(vm, expr) {
return expr.split('.').reduce((data, current) => {
return data[current]
}, vm.$data)
},
getContentVal(vm, expr) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1])
})
},
setVal(vm, expr, value) {
expr.split('.').reduce((data, current, index, arr) => {
if(index === arr.length - 1) {
return data[current] = value
}
return data[current]
}, vm.$data)
},
// 解析v-model指令
model(node, expr, vm) {
// node.value
let fn = this.updater['modelUpdater']
new Watcher(vm, expr, (newVal) => { // 给输入框加一个观察者,稍后数据更新,触发此方法,用新值给输入框赋值
fn(node, newVal)
})
node.addEventListener('input', e => {
let value = e.target.value
this.setVal(vm, expr, value)
})
let value = this.getVal(vm, expr)
fn(node, value)
},
on(node, expr, vm, eventName) {
node.addEventListener(eventName, e => {
vm[expr].call(vm, e)
})
},
text(node, expr, vm) {
let fn = this.updater['textUpdater']
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 给表达式每个变量都加上观察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentVal(vm, expr)) // 返回一个全的字符串
})
return this.getVal(vm, args[1])
})
fn(node, content)
},
html(node, expr, vm) {
// node.innerHTML
let fn = this.updater['htmlUpdater']
new Watcher(vm, expr, (newVal) => { // 给输入框加一个观察者,稍后数据更新,触发此方法,用新值给输入框赋值
fn(node, newVal)
})
let value = this.getVal(vm, expr)
fn(node, value) },
updater: {
modelUpdater(node, value) {
node.value = value
},
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
}
}
} /**
* Vue构造函数
*/
class Vue {
constructor(options) {
this.$el = options.el
this.$data = options.data let computed = options.computed
let methods = options.methods if(this.$el) {
// 做数据劫持
new Observer(this.$data)
// console.log(this.$data) for(let key in computed) { // 依赖关系
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this)
}
})
} for(let key in methods) {
Object.defineProperty(this, key, {
get() {
return methods[key]
}
})
} // vm上的取值操作都代理上vm.$data上
this.proxyVm(this.$data) // 编译模板
new Compiler(this.$el, this)
}
}
// 代理
proxyVm(data) {
for(let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newVal) {
data[key] = newVal
}
})
}
}
}

未完待续......

手写一个MVVM的更多相关文章

  1. 剖析手写Vue,你也可以手写一个MVVM框架

    剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...

  2. 『练手』手写一个独立Json算法 JsonHelper

    背景: > 一直使用 Newtonsoft.Json.dll 也算挺稳定的. > 但这个框架也挺闹心的: > 1.影响编译失败:https://www.cnblogs.com/zih ...

  3. 教你如何使用Java手写一个基于链表的队列

    在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...

  4. 【spring】-- 手写一个最简单的IOC框架

    1.什么是springIOC IOC就是把每一个bean(实体类)与bean(实体了)之间的关系交给第三方容器进行管理. 如果我们手写一个最最简单的IOC,最终效果是怎样呢? xml配置: <b ...

  5. 放弃antd table,基于React手写一个虚拟滚动的表格

    缘起 标题有点夸张,并不是完全放弃antd-table,毕竟在react的生态圈里,对国人来说,比较好用的PC端组件库,也就antd了.即便经历了2018年圣诞彩蛋事件,antd的使用者也不仅不减,反 ...

  6. 只会用就out了,手写一个符合规范的Promise

    Promise是什么 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果.从语法上说,Promise 是一个对象,从它可以获取异步操作的消息.Prom ...

  7. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

  8. 利用SpringBoot+Logback手写一个简单的链路追踪

    目录 一.实现原理 二.代码实战 三.测试 最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简 ...

  9. 手写一个简单的ElasticSearch SQL转换器(一)

    一.前言 之前有个需求,是使ElasticSearch支持使用SQL进行简单查询,较新版本的ES已经支持该特性(不过貌似还是实验性质的?) ,而且git上也有elasticsearch-sql 插件, ...

随机推荐

  1. [LeetCode] 137. 只出现一次的数字,其余三次 II ☆☆☆

    描述 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次.找出那个只出现了一次的元素. 说明: 你的算法应该具有线性时间复杂度. 你可以不使用额外空间来实现吗? 示例 1: 输 ...

  2. [#Linux] CentOS 7 安装微信详细过程

    微信安装 微信安装过程如下: 1,下载最新版本tar.gz压缩包 wget https://github.com/geeeeeeeeek/electronic-wechat/releases/down ...

  3. Android笔记(五) Activity的启动模式

    Android中Activity是由返回栈来管理的,在默认情况下,每当启动一个新的Activity,它都会在返回栈中入栈,并且出于栈的顶端.但是有些时候Activity已经在栈的顶端了,也就不需要再启 ...

  4. java - day016 - IO续(输入输出), 手写双向链表

    课程回顾 对象的创建过程 类加载 加载父类,父类的静态变量分配内存 加载子类,子类的静态变量分配内存 父类静态变量赋值运算, 和静态初始化块 子类静态变量赋值运算, 和子类初始化块 创建对象 创建父类 ...

  5. 1.Git & GitHup

    1.常见的版本控制(管理代码的版本迭代)工具: @ svn:集中式版本控制系统: SVN是集中式版本控制系统,版本库是集中放在中央服务器的,而干活的时候,用的都是自己的电脑,所以首先要从中央服务器哪里 ...

  6. P1005 矩阵取数游戏[区间dp]

    题目描述 帅帅经常跟同学玩一个矩阵取数游戏:对于一个给定的\(m*n\)的矩阵,矩阵中的每个元素\(a_{i,j}\)均为非负整数.游戏规则如下: 每次取数时须从每行各取走一个元素,共n个.经过m次后 ...

  7. java.sql.SQLException: The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver

    SpringBoot运行报错——java.sql.SQLException: The server time zone value '�й���׼ʱ��' is unrecognized or rep ...

  8. m_strcpy

    自己实现strcpy函数 #include <stdio.h> #include <assert.h> //如果它的条件返回错误,则终止程序执行 char *m_strcpy( ...

  9. 数据插入异常,原因是: (1054, "Unknown column '\ufeff95001' in 'field list'")

    今天用python调用本地文本插入数据库时出现标题错误,多了个ufeff. 这涉及的编码知识和各编码之间的转换问题. 方法1:只需在后面加入decode 方法2: 用编辑器打开,选择相应编码 选择UT ...

  10. region特征

    一: 查看阈值之后的region特征,可以通过特征检测来看,在工具栏上 region特征分三部分: 1.基础特征: region面积,中心,宽高,左上角及右下角坐标,长半轴短半轴椭圆方向,洞数及其面积 ...