手写mvvm_1
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<h1>{{ name }}</h1>
<!-- 这是一个注释 -->
<h2>{{age}}</h2>
{{ name }}{{age }}
<input type="text" v-model='name'>
<input type="text" v-model='age'>
</div>
<!-- <script src="../node_modules/vue/dist/vue.js"></script> -->
<script>
function Vue(options) {
// $el 存储的是当前元素
this.$el = document.querySelector(options.el);
// $data 存储的是data中的属性和值
this.$data = options.data;
// $data 应该是被劫持的,以及它的每一个属性都应该被劫持
observe(this.$data); // 将 this.$data 中的属性进行劫持
// 将当前元素转译到文档碎片上,并将实例传过去
nodeToFragment(this.$el, this);
}
// 实现数据劫持的核心: Object.defineProperty
function observe(data) {
for (let key in data) {
if (data.hasOwnProperty(key)) {
let value = data[key];
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
// console.log('触发get函数');
return value;
},
set(val) {
// console.log('触发set函数');
if(typeof val === 'object') {
// 如果赋值为对象,继续进行深度劫持
observe(val);
}
value = val;
}
});
// 深度劫持
if(typeof value === 'object') {
observe(value);
}
}
}
}
// 模板编译:将元素节点转移到文档碎片上
function nodeToFragment(node, vm){
// 创建一个文档碎片
let fragment = document.createDocumentFragment(),
child;
// 将node的子节点转移到文档碎片上,直到全部转完为止
while(node.firstChild) { // 只知道开始和结束条件,用while循环
child = node.firstChild;
// console.dir(child);
// 每一次转移,都需要将节点进行编译
// 在编译的时候,需要将编译的内容例如{{name}}跟实例的$data中的数据关联起来,所以需要把节点 node 和当前实例 vm 传给模板编译函数
compile(child, vm);
fragment.appendChild(child);
};
// console.log(node, node.firstChild, fragment);
// 最后将编译好的模板"还"给元素节点
node.appendChild(fragment);
}
function compile(node, vm) {
// 对于每一种节点类型,编译的逻辑不一样
/*
node.nodeType =1 :元素节点
node.nodeType =3:文本节点,换行+文本内容
node.nodeType =8:注释节点
*/
if(node.nodeType === 1) { // 处理元素节点
// 1、获取元素节点的行内属性,看看是否有v-开头的指令(这里只考虑v-model)
let attrs = node.attributes; // 获取元素节点的所有行内属性(是一个类数组)
[...attrs].forEach(item => {
if(/^v\-/.test(item.nodeName)) { // 如果是 v- 开头的,获取其值
// 获取该行内属性的属性值 "name"
let key = item.nodeValue;
// 获取vm.$data中对应属性的值
let val = vm.$data[key];
// 将拿到的值赋值给 input标签的value(放到input框中)
node.value = val;
// 监听input框的input事件,将更改的值同步到vm.$data上
// 封装监听事件使用DOM2级事件绑定,这样绑定的方法,除非手动移除,否则不会被其他方法替换掉
node.addEventListener('input', e =>{
// console.log(e);
vm.$data[key] = e.target.value;
// console.log(vm.$data[key])
});
}
});
// 处理元素节点的子节点
let ch = node.childNodes
for(let i=0; i<ch.length; i++) {
// console.log(ch[i]);
compile(ch[i], vm);
}
}
if(node.nodeType === 3) { // 处理文本节点
let txt = node.textContent; // 获取文本节点内容 "{{ name }}"
let str = '';
// 去除拿到的内容中的所有空格
for(let i=0; i<txt.length; i++) {
if(txt[i] != ' ') {
str+= txt[i];
}
}
str = str.replace(/{{(\w+)}}/g, (a,b) => {
// console.log(a,b);
return vm.$data[b];
});
node.textContent = str;
}
}
// 最后使用观察者模式将 数据劫持 和 模板编译联系起来
class Dep{
constructor() {
this.deps = [];
}
add(dep) {
// 这里添加的是每一个被观察者
this.deps.app(dep);
}
del(dep) {
let index = this.deps.indexOf(dep);
this.deps.splice(index, 1);
}
notify() {
this.deps.forEach(item => {
item.updata();
})
}
}
let vm = new Vue({
el: '#app',
data: {
name: '测试',
age: 30,
obj: {
a: 10
}
}
});
</script>
</body>
</html>