标签:还需要 源码 mod 使用 sub watch value 简单 console
关于实现MVVM,网上实在是太多了,本文为个人总结,结合源码以及一些别人的实现
双向绑定,从最基本的实现来说,就是在defineProperty绑定的基础上在绑定input事件,达到v-model的功能
代码思路图
简单版本的地址: 简单版本
? 这个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传进来的对象
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联系起来
于是我们之前创建的变量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
childNodes
解析对象[].slice.call
将对象转数组{{ }}匹配
指令的匹配
以及递归子节点
[].slice.call
循环遍历v-
的指令使用substring(2)
截取后面的属性名称v-on
这里就是匹配on
关键字,匹配到了就是事件指令,匹配不到就是普通指令_getVMVal
get会触发MVVM的_proxyData事件 在_proxyData事件里面触发data的get事件_getVMVal
获取到了数据modelUpdater
进行Dom上面的数据更新this
,订阅的属性
,回调
depend
里面执行了自己的addDep事件,并且将Observer自己的this传进去addDep
里面执行了Dispatcher
的addSub
事件,addUsb
事件里面将订阅存储到Dispatcher
里面的this.watchers
里面的计算属性的触发 查看这个例子
computed: {
getHelloWord: function () {
return this.someStr + this.child.someStr;
}
},
其实计算属性就是defineproperty的一个延伸
_getVMVal
获取计算属性的返回值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;
初始化完成,到这里还没有绑定数据,仅仅是初始化完成了
new Watcher()
this.parseGetter(expOrFn);
vm[getHelloword]
component[getHelloword]
this.someStr
Dispatcher.target
存着 getHelloWord 的 this.depend ()
所以执行depend()
,执行watcher的addDep(),执行 Dispatcher的addSub()
将当前的watcher存储到监听器这个执行顺序有点迷,第二第三方反来了
this.parseGetter(expOrFn);
就执行完毕了
目前来看为什么component会实时属性数据?
因为component的依赖属性一旦发生变化都会更新 getHelloword 的 watcher ,随之执行回调更新dom
watch的实现相对来说要简单很多
watch: function (key, cb) {
new Watcher(this, key, cb)
},
标签:还需要 源码 mod 使用 sub watch value 简单 console
原文地址:https://www.cnblogs.com/wuvkcyan/p/9602562.html