我们本次来手写一套VUE的MVVM的实现原理,是基于VUE2.0的Object.defineProperty来实现的
首先我们来先写几个标签,来展示一下视图部分
<div id="app">
<h1>姓名是{{ name }}</h1>
<h2>年龄是{{age}}</h2>
{{name}}
<h3>姓名是{{ name }};年龄是{{age}}</h3>
<input type="text" v-model='name'>
<input type="text" v-model='age'>
</div>
然后来编写原创建VUE实例的代码
let vm = new Vue({
el:'#app',
data:{
name:"哈哈666",
age:100,
c:{
b:123
}
}
});
接着就是实现MVVM的过程了
function observe(data){
if(typeof data !== 'object')return;
// 用来劫持数据
let keys = Object.keys(data);// keys 是所有属性名组成的数组
keys.forEach(key=>{
defineReactive(data,key,data[key])
})
}
function defineReactive(obj,key,value){
//专门调用defineProperty 实现数据数据劫持
observe(value);
let dep = new Dep;
Object.defineProperty(obj,key,{
get(){
console.log('get')
if(Dep.target123){
//Dep.target 就是watcher实例
dep.addSub(Dep.target123)
}
return value
},
set(newVal){
console.log('set')
if(value !== newVal){
value = newVal;
observe(value);
dep.notify();
}
}
})
}
function nodeToFragment(node,vm){
// 把元素节点 转移到了 文档碎片上
let child;
let fragment = document.createDocumentFragment();
while(child = node.firstChild){
fragment.appendChild(child)
compile(child,vm)
}
// while循环 只是 把node中的每一个子节点 都转移到了 fragment上
// 转移完成之后 页面中的 node 节点 里边 就没有元素了
node.appendChild(fragment)
// 又把 fragment上的所有节点 一次性还给了 node
}
function compile(node,vm){
// 判断node的节点类型 看他是不是元素节点
if(node.nodeType == 1){
//证明是元素节点 那么 我们要去处理行内属性
let attrs = node.attributes;// 所有的行内属性,然后看那个是v-开头的
[...attrs].forEach(item=>{
if(/^v\-/.test(item.nodeName)){
//证明这个属性是v-开头的
let valName = item.nodeValue;// 获取 "name" 这个单词
new Watcher(node,vm,valName)
let val = vm.$data[valName];// 获取"name"对应的值:珠峰
node.value = val;//把 珠峰 这两个字 放到input框中;
node.addEventListener('input',(e)=>{
//要把更改之后的input框的内容 设置给name
vm.$data[valName] = e.target.value
})
}
});
[...node.childNodes].forEach(item=>{
//针对有子节点的元素 接着进行编译
compile(item,vm)
})
}else{
// 文本节点
// debugger
let str = node.textContent; // "{{ name }}{{ age }}"
node.str = str;
// console.log(str)
if(/\{\{(.+?)\}\}/.test(str)){
str = str.replace(/\{\{(.+?)\}\}/g,(a,b)=>{
// console.log(a,b)
b = b.replace(/^ +| +$/g,'');// 去除首尾空格
new Watcher(node,vm,b)
return vm.$data[b]
})
// console.log(str)
node.textContent = str
}
}
}
// 订阅器
class Dep{
constructor(){
this.subs = [];
}
addSub(sub){
this.subs.push(sub)
}
notify(){
this.subs.forEach(item=>{
// 让对应的事件 做更新操作
item.update();
})
}
}
class Watcher{
constructor(node,vm,key){
Dep.target123 = this;
this.node = node;
this.vm = vm;
this.key = key;
this.get123();
Dep.target123 = null;
}
update(){
this.get123();
if(this.node.nodeType==1){
// 就是input
this.node.value = this.value
}else{
let str = this.node.str;// 姓名是{{name}}
str = str.replace(/\{\{(.+?)\}\}/g,(a,b)=>{
b = b.trim();
// if(b == this.key){
// return this.value
// }else{
return this.vm.$data[b]
// }
})
this.node.textContent = str
}
}
get123(){
this.value = this.vm.$data[this.key]
}
}
/*
每创造一个watcher实例 我们都要把这个实例放到对应属性的事件池中
怎么实现?
每当 new Watcher的时候 我们都把实例 放到 订阅器的一个静态属性上;
然后 主动出发 该属性的 对应的get函数? 是通过 主动调用实例的get123方法
get123 这个方法使用了 对应的属性 这是就会触发对应的get
等对应的get执行完成之后呢? 再把这个静态属性清空? 因为使用这个属性的方式很多
我们只单单要 再模板编译的时候(new Watcher)的时才需要向事件池中添加内容
*/
function Vue(options){
// $el 存储的是当前元素
this.$el = document.querySelector(options.el)
// $data存储的是data中的属性
this.$data = options.data;
observe(this.$data)// 数据劫持
nodeToFragment(this.$el,this)// 模板编译
// 我们使用观察者模式 把这两条线 联系起来
}