MVVM 可以写成 MV-VM,是 Model View - ViewModel 的缩写, ViewModel 主要靠 DataBinding 把 View 和 Model 做了自动关联,框架替应用开发者实现数据变化后的视图更新,下面我们就来进行一个简单的实现:
class Vue {
constructor(options){
this.$el = options.el;
this.$data = options.data;
let methods = options.methods;
let computed = options.computed;
if(this.$el){
new Observer(this.$data);
//将data定义到当前实例上面
this.proxyVm(this.$data)
//设置计算机属性,有依赖关系
for(let key in computed){
Object.defineProperty(this.$data,key,{
get:()=>{
return computed[key].call(this)
}
})
}
//设置方法
for(let key in methods){
Object.defineProperty(this,key,{
get:()=>{
return methods[key].bind(this)
}
})
}
//编译模板
new Compiler(this.$el,this)
}
}
proxyVm(data){
for(let key in data){
//实现可以通过vm取到对应的内容
Object.defineProperty(this,key,{
get(){
// console.log('是咧上面的数据:',key)
return data[key]
},
set(newVal){
if(data[key] !== newVal){
data[key] = newVal
}
}
})
}
}
}
class Dep {
constructor(){
this.subs = [];//这里面存放所有的watcher
}
//订阅
addSub(watcher){ // 添加watcher
this.subs.push(watcher);
}
//发布
notify(){
this.subs.forEach(watch=>{
return watch.update();
})
}
}
class Watch {
//属性一变化 , 就会执行cb回调函数
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
//默认会先存放一个老值
this.oldValue = this.get();
}
get(){
Dep.target = this;
let value = CompileUtil.getValue(this.vm,this.expr);
Dep.target = null;//?
return value
}
//更新操作,数据变化后,会调用观察者的update方法
update(){
let newValue = CompileUtil.getValue(this.vm,this.expr);
if(newValue !== this.oldValue){
this.cb(newValue)
}
}
}
class Compiler {
constructor(el,vm){
//用户有可能传入的el是一个元素
this.el = this.isElement(el) ? el : document.querySelector(el);
// console.log('dom:',dom);
this.vm = vm;
let fragment = this.nodeToFragment( this.el);
// console.log('fragment:',fragment);
this.compile(fragment);
this.el.appendChild(fragment)
}
compile(node){
let childNode = node.childNodes;
Array.from(childNode).forEach(child =>{
if(this.isElement(child)){
this.compileElementNode(child);
//有可能子节点下面还有子节点
this.compile(child)
}else if(this.isText(child)){
this.compileTextNode(child)
}
})
}
//处理元素节点
compileElementNode(node){
let attrs = Array.from(node.attributes);
// console.log('attrs:',attrs)
attrs.forEach(attr =>{
let {name,value:expr} = attr
if(this.isDirective(name)){
//如果是指令
let [, directive] = name.split('-');
//需要调用不同指令来处理
CompileUtil[directive](node, expr, this.vm);
}else if(this.isEvent(name)){
//如果是事件 @click='handler'
let directive = name.slice(1);
CompileUtil['eventHandle'](node,directive, expr, this.vm);
}
})
}
isDirective(attr){
return attr.startsWith('v-')
}
isEvent(attr){
return attr.startsWith('@')
}
//处理文本节点 {{a}} {{b}}
compileTextNode(node){
let content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
// console.log('ss:', content);
//文本节点
CompileUtil['textInserted'](node, content, this.vm);//{{a}}{{b}}
}
}
nodeToFragment(node){
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = node.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
//判断是元素节点
isElement(node){
return node.nodeType === 1
}
//判断是文本节点
isText(node){
return node.nodeType === 3
}
}
const CompileUtil = {
//获取值
getValue(vm,expr){
return expr.split('.').reduce((current,key)=>{
return current[key]
},vm.$data)
},
//设置值
setValue(newValue,expr,vm){
return expr.split('.').reduce((current,key,index,arr)=>{
if(index === arr.length - 1){
return current[key] = newValue
}
return current[key]
},vm.$data)
},
//处理{{}}
textInserted(node,expr,vm){
// console.log('content:',content)
let fn = this.updater['textInsertedUpdater'];
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watch(vm,args[1],()=>{
fn(node,this.getContentValue(vm,expr));//返回一个全的字符串
});
return this.getValue(vm, args[1])
});
fn(node, content)
},
getContentValue(vm,expr){
//遍历表达式,将内容重新替换成一个完整的内容,返回回去
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(vm, args[1])
})
},
text(node,expr,vm){
let fn = this.updater['textUpdater'];
// console.log('expr:',expr)
let value = this.getValue(vm,expr);
new Watch(vm,expr,(newValue)=>{
fn(node,newValue)
})
// console.log('expr text:',value);
fn(node,value)
},
model(node,expr,vm){
let fn = this.updater['modelUpdater'];
let value = this.getValue(vm,expr)
// console.log('model:',node.nodeName);
new Watch(vm,expr,(newVal)=>{
fn(node,newVal)
});
if(node.nodeName === 'INPUT' || node.nodeName === 'TEXTAREA'){
node.addEventListener('input',(e)=>{
this.setValue(e.target.value,expr,vm)
})
}
fn(node, value)
// node.addEventListener('')
},
html(node,expr,vm){
let fn = this.updater['htmlUpdater'];
let value = this.getValue(vm,expr);
new Watch(vm,expr,(newVal)=>{
fn(node,newVal)
});
fn(node,value)
},
//处理事件
eventHandle(node,eventName,expr,vm){
node.addEventListener(eventName,(e)=>{
vm[expr] && vm[expr].call(vm,e)
})
},
updater:{
textUpdater(node,value){
node.innerText = value
},
textInsertedUpdater(node,content){
node.textContent = content
},
htmlUpdater(node,value){
node.innerHTML = value;
},
modelUpdater(node,value){
node.value = value;
}
}
}
//定义响应式数据
class Observer {
constructor(data){
this.observerData(data)
}
observerData(data){
if(data && typeof data === 'object'){
for(let key in data){
this.defineReactive(key,data[key],data)
}
}
}
defineReactive(key , value, data){
this.observerData(value);//有可能value也是一个对象,也需要进行响应式处理
let dep = new Dep() ;//给每一个属性都加上一个具有发布订阅的功能
Object.defineProperty(data,key,{
get:()=>{
//创建watcher时会取到对应的内容,并且把watcher放到全局上面
Dep.target && dep.addSub(Dep.target);
return value;
},
set:(newValue)=> {
if(value !== newValue){
value = newValue;
//有可能用户重新修改的数据也是一个对象
this.observerData(newValue);
dep.notify();
}
}
})
}
}
下面我们新建一个html页面来测试这个vue文件 html代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<style>
.red{
color: red;
}
</style>
<body>
<div id="app">
<input type="text" v-model="school.name">
<div>{{school.age}}</div>
<div>{{school.name}} {{school.age}}</div>
<span>1</span>
<span>{{getName}}</span>
<span v-text="school.text"></span>
<button @click="changeValue">点我</button>
<div v-html="school.message">
</div>
</div>
<script src="./vue.js"></script>
<script>
let vm = new Vue({
el:'#app',
data:{
school:{
age:20,
name:'你好啊',
text:'这个是v-text',
message:'<h1>欢迎大家</h1>'
}
},
methods:{
changeValue(e){
this.school.name = 'click事件'
this.school.message = '<h3>变了没有呢</h3>'
}
},
computed:{
getName(){
return this.school.name + 'hello world'
}
}
});
</script>
</body>
</html>
同学们可以自己测试一下,一些模仿vue的基本功能是实现了的哦