人都说面试造火箭,工作拧螺丝,其实也能够理解,如果不这样又怎么能证明你的学习能力呢?和相亲是一个道理,第一次相亲,你穿的衣衫褴褛的进去了,应该也就没有下文了吧?哈哈
我最大的特点就是懒,总是不喜欢特别长的文章,这不100行的代码实现了vue的MVVM的双向绑定的原理,基本上要点全部都有注释,并且基本上全是大白话,没有官话和套话,学过js你就能看懂。。。没有学过js的就先把js学了再过来
这版代码简化了一些没必要的操作,比如很多是吧数据代理和数据劫持进行分离的,我直接是同时进行的,跟读小说一样,只要你花10分钟,你就能完全理解了,要是能再写一遍就更好了,不要觉得代码长,其实不到100行,,,不要怕,敢不敢赌一把?
html是这样的。。。没啥好说的
<div id="app">
<input type="text" v-model="text">
{{ text }}
<button @click="reset" >重置</button>
</div>
<script>
var vm = new saoVm({
el: 'app',
data: {
text: ''
},
methods: {
reset() {
this.text = '';
},
},
});
</script>
复制代码
关键的部分来了,屏息凝神10分钟。。。先大致浏览一下架子结构,再看。。。
class saoVm {
constructor(options) {
const {
el,
data,
methods
} = options;
this.methods = methods;
this.target = null;
// 发布初始化
this.observe(this, data);
// 订阅初始化
this.compile(document.getElementById(el));
}
/*
为啥是Object.defineProperty?
双向绑定不就是设置值的时候,改变视图,视图改变,值也变化吗?
那我总得知道什么时候他设置了值,什么时候改变了视图啊?
这就需要用到这个API的get和set,这点不再做详细介绍
通过循环data中的属性为data中的每一个属性都通过Object.defineProperty进行定义
从而重写data的set和get函数来实现的
*/
observe(root, data) {
for (const key in data) {
this.defineReactive(root, key, data[key]);
}
}
defineReactive(root, key, value) {
/*
比如data数据是 data:{foo:{bar:ggg}}
那我们给foo进行数据劫持的同时,也要对bar做数据劫持啊
不然bar的改变就不会触发视图改变了
所以,这里使用递归对value(也就相当于data.foo)的值进行了判断并在此进行数据劫持
*/
if (typeof value == 'object') {
return this.observe(value, value);
}
const dep = new Dispatcher();
/* 事件代理的同时进行事件劫持,这就是为什么第一个参数直接把this传进来
,本来是this.data.xxx,后续就可以使用this.xxx来访问了,同时又重写了get和set,一举两得
*/
Object.defineProperty(root, key, {
set(newValue) {
// 如果值和原来一样就不做处理 直接返回
if (value == newValue) return;
value = newValue;
// 数据改变通知触发视图替换操作,这叫发布
dep.notify(newValue);
},
get() {
/* add的过程就叫做订阅,这里的this.target是什么?
在compile阶段中你就会得到答案,往下看
*/
dep.add(this.target);
return value;
}
});
}
//编译模板-也就是将 {{ msg }} 替换为msg真正的值的过程
compile(dom) {
//拿到$el这个dom节点下的所有子节点(注意node节点和元素节点的区别
const nodes = dom.childNodes;
//开始遍历所有的节点...(注意:for..in 拿到的是key,for..of拿到的是value)
for (const node of nodes) {
//nodeType为1是元素节点,2是属性节点,3是文本节点。
// 此处属性节点不需要关心,,,可以剔除
// 元素节点
if (node.nodeType == 1) {
// 拿到所有的元素属性,比如v-model,style,@click这些属性
const attrs = node.attributes;
for (const attr of attrs) {
// 如果这个属性名称是v-model
if (attr.name == 'v-model') {
const name = attr.value;
// 绑定input事件
node.addEventListener('input', e = >{
/*
比如v-model="xxx"
下边的目的是this.xxx = 输入的值,改变了data中的数据
设置值就会触发发布订阅的发布,继而更新视图
*/
this[name] = e.target.value;
});
/*
还记得上边留的那个坑吗?
this.target是什么?
是一个watcher的实例,每一个watcher都有一个
update方法来执行视图的更新操作,这里把这个watcher赋值给了this之后
。。。this.target就有值了,这够大白话了吧,没有一点专业术语了,哈哈
*/
this.target = new Watcher(node, 'input');
// 这一步纯粹是为了触发该属性的get方法
// 从而执行get中的addS方法,来订阅事件
this[name];
}
// 如果是@click就监听为这个node监听事件
if (attr.name == '@click') {
const name = attr.value;
node.addEventListener('click', this.methods[name].bind(this));
}
}
}
// text节点--
if (node.nodeType == 3) {
// -匹配的就是{{ xxx }} 这段文本
const reg = /\{\{(.*)\}\}/;
// node.nodeValue此处就相当于 innerText
const match = node.nodeValue.match(reg);
if (match) {
// match[1],看上边正则是不是有个括号,match[1]
//就是匹配括号中的内容并去两侧空格
const name = match[1].trim();
// 这不用解释了吧。上边解释过了,在强调下,这里的
// 第二个参数是因为,input更新视图改变的是它的value值
// 而别的文本节点改变的就是它的nodeValue,
//因此这里当时候会通过这个入参进行区分
this.target = new Watcher(node, 'text');
this[name];
// 这一步是直接将 {{ xxx }} 替换为空
node.nodeValue = '';
}
}
}
}
}
class Dispatcher {
constructor() {
this.watchers = [];
}
add(watcher) {
this.watchers.push(watcher);
}
notify(value) {
this.watchers.forEach(watcher = >watcher.update(value));
}
}
// 一个指令类Watcher,用来绑定更新函数,实现对DOM元素的更新
class Watcher {
constructor(node, type) {
this.node = node;
this.type = type;
}
update(value) {
// 如果是input需要改变value值
if (this.type == 'input') {
this.node.value = value;
}
// 如果是text需要改变value值
if (this.type == 'text') {
this.node.nodeValue = value;
}
}
}
复制代码
总结
- 一个observe循环data数据绑定getter和setter
- 一个compile来解析模板,循环childNodes,解析模板语法以及指令
- 一个发布订阅模式,在get中订阅,在set中发布,和订报纸,发报纸一样的道理
- 在compile阶段设置this.target,并在此触发get方法,来进行发布监听的操作
都看到这了?小老弟您可真牛x,关注和赞走一个鼓励一下。。。❥(^_-)
觉得对你有帮助,不妨点个
赞
,后续持续输出这种简短有效的文章,帮助你用最短的时间内掌握最多的内容,毕竟谁不喜欢一劳永逸不是? ❥(^_-) thank you ~