观察者模式与数据劫持部分还没完成,有时间在继续更新
function Vue(options){
this.$el = options.el;
this.$data = options.data;
if(this.$el){//若这个el节点存在不为空,则编译模板
//将data中的数据 全部转化成Object.defineProperty来定义 这样我们可以做到数据劫持 在观察者模式下我们可以实时感受到数据的变化
new Observer(this.$data);
//用来编译模板 将模板中对应的数据渲染出来 比如{{school.name}} 渲染出真正的值
new Compiler(this.$el,this);
}
}
function Observer(data){
this.observer(data);
}
Observer.prototype.observer = function(data){
if(data && typeof data == "object"){
for(let key in data){
this.defineReactive(data,key,data[key]);
}
}
}
/**
* Object.defineProperty 是以树蕨劫持的方式来定义值
* 这里就是为obj(data)对象来定义一个名为key(school)的变量 这个变量的结果就是 return value
* 我们传统的定义方式是 let a = 10 改成数据劫持的方式就是 Object.defineProperty(window,10,{get(){return 10}})
* 为window对象定义名为a的变量 它的结果为10
*/
Observer.prototype.defineReactive = function(obj,key,value){
this.observer(value);
Object.defineProperty(obj,key,{
get(){
return value
},
set:(newValue)=>{
if(newValue!=value){
this.observer(newValue);
newValue = value;
}
}
})
}
/**
@el 初始的父节点
@vm 指向主类 Vue函数 用来调用Vue函数中的成员
@tip 这个函数主要用来编译模板
*/
function Compiler(el,vm){
this.vm = vm;
this.el = this.isElementNode(el)?el:document.querySelector(el);
//把当前节点中的元素全部获取到 然后放到内存当中去
let fragment = this.node2fragment(this.el);
//把节点中的内容进行替换
//编译模板 用数据编译
this.compile(fragment);
//把内存当中的节点片段重新添加到主节点el中
this.el.appendChild(fragment);
// console.log(fragment);
}
/**
* @attrName 属性名
* @tip startsWith 用来判断某个字符是以什么开头 若符合则返回true
*/
Compiler.prototype.isDirective=function(attrName){
return attrName.startsWith("v-");
}
/**
* @tip 用来编译dom节点 判断dom节点中是否有v-开头的元素 若有则进行相应的数据操作
* @tip [,directive] = name.split("-"); 为es6新增语法 split是将字符串以某种形式分割开来
* 然后以数组的形式存储 这里用横线分割 分割后会生成一个有两个值的数组第一个值是横线之前
* 的字符 第二个值是横线之后的字符 这里[,directive] 逗号之前的位置存储第一个字符,后面的就
* 存储第二个字符 这里我们不需要第一个字符 所以不用管,只写存储第二个字符的变量名
* @tip CompilerUtil[directive](node,value,this.vm) 就等同于
* CompilerUtil["model"](node,value,this.vm) 这种json对象格式的可以用这种方式调用
*/
Compiler.prototype.compileElement = function(node){
//node.attributes 就是我们定义在标签上的所有属性
let attributes = node.attributes;
// console.log([...attributes]);
[...attributes].forEach(attr=>{
//因为获取到的伪数组转换后是以键值对的方式存在的 所以我们可以获取key 和value
//{name,value} es6的语法 这句意思是将attr数组中的名字为name和value的0值赋给对应的变量
let {name,value} = attr;
if(this.isDirective(name)){
let [,directive] = name.split("-");
CompilerUtil[directive](node,value,this.vm);
// console.log(directive,node);
}
})
// console.log(attributes);//
}
/**
* @tip 用来编译文本节点 即空白位置 判断是否有{{}}存在 若有则进行相应的操作
* @tip textContent es6新增语法 用来获取节点中的文本内容
* @tip /\{\{.+?\}\}/ 即匹配一组花括号 .+?表示只匹配括号之间的内容
*/
Compiler.prototype.compileText = function(node){
let content = node.textContent;
//若找到了一组花括号 并且里面有至少一个内容
if(/\{\{.+?\}\}/.test(content)){
// console.log(content);
CompilerUtil["text"](node,content,this.vm);
}
}
/**
* 编译工具
* 用来根据 指令进行是编译操作 例如model 就是v-model 进行双向绑定操作 用来编译数据到表单上
* @model 进行双向绑定的数据编译工作
* @text 为空白出的文本节点进行数据编译工作 如{{school.name}}
*/
let CompilerUtil = {
/**
这个函数用来获取对象指向的值 例如 school.name值是多少
或者是school.id的值
*/
getVal(vm,value){
return value.split(".").reduce((data,current)=>{
return data[current];
},vm.$data)
},
//node是有v-属性指令的节点 value是属性指令的值例如(school.name) vm是当前Vue实例
model(node,value,vm){
let fn = this.updater["modelUpdater"];//首先获取到更新v-model表单的方法
let values = this.getVal(vm,value);//然后获取到data中的确定的值 比如shcool.name的值
fn(node,values);//然后将节点和获取到的值传过去
},
/**
*
* @param {*} node
* @param {*} content
* @param {*} vm
* @tip 这里需要注意 replace在使用回调函数时会有四个参数
* 但箭头函数没有实参列表,直接获取是获取不到这四个参数的, 所以我们可以使用es6的三点运算符,
* 在函数括号中使用时,可以将函数多余的参数组合成一个数组来存储,数组名写在三个点的后面
*/
text(node,content,vm){
//首先获得更新文本节点的函数
let fn = this.updater["textUpdater"];
//用正则表达式来获得匹配到的每一个{{}}中的数据
let contents = content.replace(/\{\{(.+?)\}\}/g,(...args)=>{
//然后将数据传到getVal中 用来获取数据的真实值 例如shcool.name,或是shcool.id
return this.getVal(vm,args[1]);
});
//然后将获取到的真实结果和文本节点传递到文本节点更新函数中
fn(node,contents);
},
updater:{
/**
* @param {*} node
* @param {*} value
* @tip 用来更新双向绑定的表单的值
*/
modelUpdater(node,value){
node.value = value;
},
/**
* @param {*} node
* @param {*} content
* @tip 用来更新文本节点的内容
*/
textUpdater(node,content){
node.textContent = content;
}
}
}
/**
* @node 获取到的主节点el中的所有的元素
* @tip 注意 空白的位置也属于一个节点 即文本节点
*/
Compiler.prototype.compile = function(node){
//这可以获取到一个类数组 即伪数组
//childNodes获取到的是当前子节点 但是只获取第一场 例如子节中还有子节点则不会获取
var childNode = node.childNodes;
//我们需要将类数组转换为真正的数组 可以使用es6新增的扩展运输符[...]
//或者使用 Array.from(需要转换的数组) 也是es6语法
[...childNode].forEach(child=>{
if(this.isElementNode(child)){//判断是否是元素 若是元素进行元素的数据编译工作
this.compileElement(child);
//如果这个节点是一个dom元素的话 那么便把它传进去 看他里面是否还有子节点存在
this.compile(child);
}else{//否则进行文本的数据编译工作
this.compileText(child);
}
})
}
/**
@node 获取到节点,用来判断这个node是否是真实的dom 还是字符串
@tip 这个方法主要用来判断传过来的节点是否是真实的dom
*/
Compiler.prototype.isElementNode=function(node){
//nodeType是dom元素上的一个属性 它的值为1
//这里如果这个node是一个dom元素的话那么它便会返回true
//如果是一个字符串的话它便不会存在这个属性 便返回false
return node.nodeType === 1;
}
/**
* @node 获取到的真实父节点 #app
* @tip 这个方法主要用来把节点中的片段移动到内存当中
* @tip appendChild 添加节点 如果该节点已经存在,则从当前父节点中删除,然后添加到新的父节点。
* @tip document.createDocumentFragment() 即文档碎片 我们可以先将所有的节点移动到文档
* 碎片上 然后一次性添加到body中 否则一条一条的添加很影响性能
*/
Compiler.prototype.node2fragment = function(node){
let fragment = document.createDocumentFragment();
let firstChild;
//没循环一次将主节点中的第一个片段赋值给 firstChild
//然后将firstChild重新移动到内存当中 即fragment中
while(firstChild = node.firstChild){
//appendChild 不仅可以添加一个节点 还可以移动节点
fragment.appendChild(firstChild);
}
return fragment;
}
<!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="school.id"/>
<input type="text" v-model="school.name"/>
{{school.name}}{{school.id}}
<div>{{school.name}}</div>
<div>{{school.id}}</div>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
</div>
<script type="text/javascript" src="mvvm.js"></script>
<script type="text/javascript">
let vm = new Vue({
el:"#app",
data:{
school:{
name:"财经院校"
,id:"1号"
}
}
})
</script>
</body>
</html>