这是我参与「第四届青训营 」笔记创作活动的第1天
-
简介:实现一个简单的Mvvm框架,利用js es6模块化,less进行简单样式美化,webpack进行打包
-
实现的功能:1.数据劫持;2.发布订阅模式;3.数据的单向绑定;4.双向绑定
-
绑定指令语法:{{ }};v-text;v-model;
-
gitee pages: xieyusai.gitee.io/class-assig…
<!-- 简化后的html代码,源码稍加修饰 -->
<div id="app">
<h2>{{title}}</h2>
<span>{{msg}}</span>
<input v-model="msg" type="text" />
<div v-text="hello"></div>
<select v-model="hello" name="hello">
<option value="hello world">hello world</option>
<option value="hello html">hello html</option>
<option value="hello javascript">hello javascript</option>
<option value="hello vue">hello vue</option>
<option value="hello nodejs">hello nodejs</option>
<option value="hello typescript">hello typescript</option>
</select>
</div>
<script>
const vm = new Mvvm({
el: '#app',
data: {
msg: '请输入文字:',
title: 'Mvvm框架',
hello: '请下拉选择:',
},
})
</script>
效果:
前言
通过Object.defineProperty()中的get与set来实现对数据的劫持,数据变动时通知订阅者,触发相应的回调函数,以监听数据变动。 1、创建数据监听器Observer,对数据对象的属性进行监听。 2、创建指令解析器Compiler,对每个元素节点的指令进行编译 3、创建观察者Watcher,监听模型数据的变动以更新视图 4、创建订阅器Dep,收集依赖属性的watcher 5.各个模块如下:
1、mvvm入口函数
Mvvm构造器如下,在其中做了属性代理操作,把data的属性都映射到Mvvm中,这样直接使用vm就可以操作data属性。 在构造器中,先获取操作的dom对象,调用observer模块进行数据监听,然后调用compiler模块进行编译,以识别{{}}语法(也添加了v-model与v-text语法)。
class Mvvm {
constructor(options) {
// 获取元素dom对象
this.$el = document.querySelector(options.el);
// 转存数据,保存传递过来的data数据
this.$data = options.data || {};
// 进行数据的代理
this._proxyData(this.$data);
// 数据劫持
new Observer(this.$data);
// 模板编译
new Compiler(this)
}
// 数据的代理
_proxyData(data) {
Object.keys(data).forEach((key) => {
// 利用Object.keys方法,取出data中每一项数据的属性名key
Object.defineProperty(this, key, {
// 设置可以枚举
enumerable: true,
// 设置可以配置
configurable: true,
// 获取数据;读数据
get() {
return data[key]
},
// 设置数据;写数据
set(newValue) {
// 判断新值和旧值是否相等,若相等则return退出函数
if (newValue === data[key]) {
return
}
// 若不相等,则设置新值
data[key] = newValue
},
})
})
}
}
// 挂载到window
window.Mvvm = Mvvm;
2、Observer,对Mvvm中data数据进行劫持
数据可能存在子数据,为进行深度数据劫持创建walk遍历函数进行递归调用,递归的终止条件:data为空或者非对象。
import Dep from "./dep";//导入Dep模块
export default class Observer {
constructor(data) {
this.data = data;
// 遍历对象,完成所有对象(对象的子数据)的劫持
this.walk(data)
}
// 遍历对象函数
walk(data) {
// 递归的终止条件,data为空或者非对象
if (!data || typeof data !== 'object') {
return
}
// 利用Object.keys方法,取出data中每一项数据的属性名key
Object.keys(data).forEach((key) => {
// 进行数据绑定,完成数据劫持
this.dataHijack(data, key, data[key])
})
}
// 设置响应式数据(get与set),完成数据劫持
dataHijack(obj, key, value) {
// 对数据值进行遍历
this.walk(value)
// 暂存当前this指向
const that = this
// 新建Dep对象,收集该依赖属性的Watcher对象
let dep = new Dep()
Object.defineProperty(obj, key, {
// 可遍历
enumerable: true,
// 可再配置
configurable: false,
// 获取数据;读数据
get() {
// 添加观察者对象
if (Dep.target) {
dep.addSub(Dep.target)
}
return value
},
// 设置数据;写数据
set(newValue) {
// 判断新值和旧值是否相等,若相等则return退出函数
if (newValue === value) {
return
}
// 若不相等,则设置新值
value = newValue
// newValue对象可能存在属性,对其业进行遍历
that.walk(newValue)
// 通知订阅者
dep.notify()
},
})
}
}
Observer中调用了Dep,在get时添加Dep.target(watcher),在set时触发notify(通知每个watcher)。 实现Observer后,程序已经能够监听数据以及通知订阅者,接下来在Compiler模块中编译模板。
3、Compiler,对HTML进行模板编译
compiler模块用来解析模板指令,同时将模板中的变量替换成数据,然后对文本内容进行替换赋值给节点。同时添加订阅者watcher以监听数据,数据变动时会收到通知。在指令的处理上,文本节点利用正则表达式识别{{}}指令,元素节点利用order函数通过字符去匹配对应的指令,能够识别节点属性中的v-text与v-model指令。
import Watcher from "./watcher";//导入Watcher模块
export default class Compiler {
constructor(vm) {
// 拿到vm与el
this.vm = vm
this.el = vm.$el
// 编译模板
this.compile(this.el)
}
// 编译模板函数
compile(el) {
// 拿到元素子节点
let childNodes = [...el.childNodes]
// 对子节点进行遍历,判断每个节点node类型,以匹配不同的编译方法
childNodes.forEach((node) => {
// 文本节点类型===3
if (node.nodeType === 3) {
// 编译文本节点
this.compileTextNode(node)
} else if (node.nodeType === 1) {
//元素节点类型===1,编译元素节点
this.compileElementNode(node)
}
// 如果子节点还存在子节点,则进行递归遍历
if (node.childNodes && node.childNodes.length > 0) {
// 继续递归编译模板
this.compile(node)
}
})
}
// 编译文本节点函数
compileTextNode(node) {
// 匹配 {{}}内容的正则表达式
let reg = /{{(.+?)}}/
// 获取文本节点的内容
let val = node.textContent
// 判断文本是否存在{{}}
if (reg.test(val)) {
// 获取{{ }}中内容同时去除前后空格
let key = RegExp.$1.trim()
// 对文本内容进行替换,同时赋值给节点
node.textContent = val.replace(reg, this.vm[key])
// 创建一个观察者
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 编译元素节点函数,编译指令
compileElementNode(node) {
// 遍历元素节点的属性
![...node.attributes].forEach((attr) => {
// 得到属性名
let attrName = attr.name
// 判断属性名是否以v-开头
if (attrName.startsWith('v-')) {
// 若是v-开头,则除去v-
attrName = attrName.substr(2);
// 设置属性值
let key = attr.value;
// 执行对应的指令方法
// 利用order指令方法去执行对应的指令
this.order(node, key, attrName)
}
})
}
// 添加指令方法 并且执行
order(node, key, attrName) {
// 利用字符去匹配对应的指令
// textOrder,modelOrder
let orderFunction = this[`${attrName}Order`]
// 调用方法
if (orderFunction) {
orderFunction.call(this, node, key, this.vm[key])
}
}
// 设置了两个指令方法
// v-text
textOrder(node, key, value) {
node.textContent = value
// 创建观察者
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// v-model
modelOrder(node, key, value) {
node.value = value
// 创建观察者
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// input值时进行数据同步,实现双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
}
4、Dep,收集所有Watcher订阅者
创建一个Dep类,数据劫持操作时实例化dep对象,在监听到数据属性变化时收集依赖属性的watcher,添加到数组中存储。类似一个消息订阅器,创建一个subs数组收集订阅者,当数据变动触发notify通知sub数组中每个watcher,再触发每个观察者的update更新方法。
export default class Dep {
constructor() {
// 创建数组以存储watcher
this.subs = []
}
// sub数组中添加watcher
addSub(watcher) {
// 判断观察者是否存在 和 是否拥有update方法
if (watcher && watcher.update) {
this.subs.push(watcher)
}
}
// 通知sub数组中每个watcher
notify() {
// 触发每个观察者的更新方法
this.subs.forEach(watcher => {
watcher.update()
})
}
}
5、Watcher,对Compiler提取的数据进行订阅
当编译模板的时候,实例化watcher观察者对象,以监听模型数据的变动,并在数据变动时调用callback函数更新视图
import Dep from "./dep";//导入Dep模块
export default class Watcher {
constructor(vm, key, cb) {
// 拿到vm与属性key
this.vm = vm
this.key = key
// callback回调函数用来执行更新视图的方法
this.cb = cb
// 负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中
Dep.target = this
// 存储旧数据,同时触发get方法,通过observer.js中的dep.addSub(Dep.target)把watcher添加到了sub数组中
this.oldValue = vm[key]
}
// updata函数,让发布者通知watcher进行更新
update() {
// 获取新值
let newValue = this.vm[this.key]
// 比较旧值和新值,若值不改变则return
if (newValue === this.oldValue) {
return
}
// 若值改变则调用callback函数更新视图
this.cb(newValue)
}
}
总结
至此,一个基本的Mvvm框架就完成了,实现了1.数据劫持;2.发布订阅模式;3.数据的单向绑定;4.双向绑定。 在效果的展示中添加了css样式以让视图更美观,在作业的完成过程中参考了一些对vue2.0的解析文章,自身的代码水平还有待进一步的提高。