大家好我是某行人👋,充电器一拔又是一年的开始,还记的去年年终总结立的FLag,今年从源码出发,作为Vue的喜爱者,理解原理是必不可少的,经过自己几天的研究与总结终于总结出一篇文章,在这里分享给大家。
- 什么是MVVM?
作为老生常谈的问题在面试的时候也会经常问到,那什么是MVVM呢?MVVM是Model-View-ViewModel的简写,MVVM就是模型-视图-视图模型,M是逻辑方法加上数据,V就是用户看到的界面,VM就是逻辑方法加上界面渲染的代码,双向数据绑定作为MVVM核心,View的变动,自动反映在 ViewModel,数据驱动视图更新的功能。其实MVVM它本质上就是MVC 的改进版。MVC全名Model View Controller,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。其中,View的定义比较清晰,就是用户界面
- MVVM与MVC的区别?
MVVM 实现了双向数据绑定,而MVC是单项
MVVM是真正将页面与数据逻辑分离放到js里去实现,而MVC里面未分离
- 实现MVVM方式?
- 发布者-订阅者模式:一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是vm.set(propertyName, value)
- 脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,在指定的事件触发时进入脏值检测
- 数据劫持:Vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持每个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调
- 响应式原理
Vue2.x是通过Object.defineProperty()实现的,Vue3.x是通过ES6中Proxy实现的,下面简单实现以下Vue2.x的响应式主要为下面MVVM的实现做铺垫。
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>vue双向数据绑定</title>
</head>
<body>
<input type="text" name="" id="in_id">
<p id="p_id"></p>
<!-- 实现核心是通过Object.defineProperty对data的每个属性进行劫持 -->
<script>
var objs = {};
var inp_dom = document.querySelector('#in_id');
var p_id = document.querySelector('#p_id')
Object.defineProperty(objs,'xx_objs',{
get:function(e){
return e;
},
set:function(e){
p_id.innerHTML=e;
}
})
inp_dom.addEventListener('input',(e)=>{
objs.xx_objs=inp_dom.value
})
</script>
<!-- 观察这模式:一对多的模式 -->
</body>
</html>
关于MVVM与响应式就介绍这么多,接下来我们直接从源码出发实现一个简易的MVVM
- 创建文件
| Vue-Mvvm
| Compiler.js // 模板编译
| Oberser.js // 数据劫持
| Watcher.js // 观察者对象
| Mvvm.js // vue实例
| index.html // 使用文件
- 模板编译
Compiler.js
// 模板编译
class Compiler{
constructor(el,vm){
this.el = this.isElementNode(el)?el:document.querySelector(el);
this.vm = vm;
// 把当前节点中的元素获取到放到内存中
let fragment = this.node2fragment(this.el);
// 把节点中的内容进行替换
// 模板编译
this.complie(fragment);
// 把内容再塞到页面中
this.el.appendChild(fragment);
}
// 编译元素
complieElement(node){
let attrs = node.attributes;
let _me = this;
[...attrs].forEach(attr=>{
let {name,value:expr} = attr;
if(_me.isDirective(name)){
let [,directive] = name.split('-');
let [directiveName,eventName] = directive.split(':');
// 调用不同的指令方法处理
CompilerUtil[directiveName](node,expr,_me.vm,eventName);
}else if(this.isEvent(name)){
let eventType = name.replace(/^\@/g,'');
CompilerUtil['on'](node,expr,_me.vm,eventType)
}
})
}
// 编译文本
complieText(node){
//当前文本是否包含{{ xxx }}
let content = node.textContent;
if(/\{\{(.+?)\}\}/.test(content)){
CompilerUtil['text'](node,content.trim(),this.vm)
}
}
// 核心编译方法
complie(node){
let childNodes = node.childNodes;
[...childNodes].forEach(child=>{
if(this.isElementNode(child)){
this.complieElement(child);
// 递归节点
this.complie(child);
}else if(this.isTextNode(child)){
this.complieText(child)
}
})
}
// 拿到碎片节点- 把节点移动到内存中
node2fragment(node){
// 创建文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = node.firstChild){
// appendChild具有移动性
fragment.appendChild(firstChild)
}
return fragment;
}
// 判断el是否是一个元素
isElementNode(node){
return node.nodeType===1;
}
// 判断是否为指令
isDirective(attrName){
return attrName.startsWith('v-');
}
// 判断文本
isTextNode(node){
return node.nodeType == 3;
}
// 判断@绑定的事件
isEvent(dir){
return dir.startsWith('@')
}
}
// 模板处理工具函数
const CompilerUtil={
// 根据表达式取到对应的数据
getVal(vm,expr){
if(/\./g.test(expr)){
return expr.split('.').reduce((data,current)=>{
return data[current]
},vm.$data);
}else{
return vm.$data[expr];
}
},
setVal(vm,expr,value){ // data 新数组 current key值 index 索引 arr 当前数组
return expr.split('.').reduce((data,current,index,arr)=>{
if(index == arr.length-1){
data[current] = value;
}
return data[current]
},vm.$data)
},
// 事件绑定
on(node,expr,vm,eventType){
let fn = vm.$options.methods && vm.$options.methods[expr];
if(eventType && fn){
node.addEventListener(eventType, fn.bind(vm), false);
}
},
model(node,expr,vm){
let fn = this.updater['modelUpdate'];
let value = this.getVal(vm,expr);
// 元素处理加观察者 每一个元素都添加成观察者模式
// 每个元素添加观察者都是通过模板编译阶段添加的
new Watcher(vm,expr,(newVal)=>{ // 给输入框加入观察者模式
fn(node,newVal);
})
node.addEventListener('input',(e)=>{
let value = e.target.value;
this.setVal(vm,expr,value)
})
fn(node,value);
},
html(node,expr,vm){
let fn = this.updater['htmlUpdate'];
let value = this.getVal(vm,expr)
new Watcher(vm,expr,(newVal)=>{ // 给输入框加入观察者模式
fn(node,newVal);
})
fn(node,value);
},
getContentValue(vm,expr){
return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(vm,args[1].trim());
})
},
text(node,expr,vm){
let fn = this.updater['textUpdate'];
let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
// 给每个 表达式加上观察者
new Watcher(vm,args[1].trim(),()=>{
fn(node,this.getContentValue(vm,expr));
})
return this.getVal(vm,args[1].trim());
})
fn(node,content);
},
updater:{
modelUpdate(node,value){
node.value = value
},
// 文本节点更新
textUpdate(node,value){
node.textContent = value;
},
htmlUpdate(node,value){
node.innerHTML = value;
}
}
}
- 数据劫持
Oberser.js
为每一个数据添加数据监听
class Observer{
constructor(data){
this.observer(data)
}
observer(data){
if(data && typeof data == 'object'){
for(let key in data){
this.defineReactive(data,key,data[key]);
}
}
}
// 定义响应式
defineReactive(obj,key,value){
this.observer(value);
let dep = new Dep() // 给每一个属性加上响应式功能
Object.defineProperty(obj,key,{
get(){
Dep.target && dep.addSub(Dep.target);
console.log(Dep.target)
return value
},
set(newValue){
if(value==newValue) return;
value = newValue;
// 广播之后会执行每个元素的 Watcher.update方法进行视图更新
dep.notify(); // 更新之后进行广播
}
})
}
}
- 观察者对象
Watcher.js
通过Observer模块的数据劫持,做广播从而导致视图更新
// 观察者(发布订阅) 观察者 被观察者
class Dep{
constructor(){
this.subs = [];
}
// 添加
addSub(Watcher){
this.subs.push(Watcher)
}
notify(){
this.subs.forEach(watcher=>watcher.update())
}
}
Dep.target=null;
class Watcher{
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 默认先放一个
this.oldValue = this.get(vm,expr);
}
get(vm,expr){
// 核心 -- 将模板编译与数据监听相结合
Dep.target = this;
let value = CompilerUtil.getVal(vm,expr);
// 注意这里的清空
Dep.target = null;
return value;
}
update(){
// 更新操作
let newValue = CompilerUtil.getVal(this.vm,this.expr);
if(newValue!==this.oldValue){
this.cb(newValue);
}
}
}
- Vue实例
Mvvm.js
这里就是我们在脚手架里使用到Vue实例了
class Vue{
constructor(options){
this.$el = options.el;
this.$data = options.data || {};
this.$options = options;
let computed = options.computed;
if(this.$el){
new Observer(this.$data); // 数据响应式
for(let key in computed){
// computed原理:因为computed存在依赖关系所以 先将每个方法代理到this.$data上然后调用proxyVm方法将数据代理到VM上
// 也可以向methods类似 通过$options 进行引用
Object.defineProperty(this.$data,key,{
get:()=>{
// 将捕获的值返回
return computed[key].call(this);
}
})
}
this.proxyVm(this.$data); // 数据代理
new Compiler(this.$el,this); // 模板编译
}
}
proxyVm(data){
for(let key in data){
Object.defineProperty(this,key,{
get(){
return data[key];
},
set(newVal){
data[key]=newVal;
}
})
}
}
}
- 引入使用
index.html
将我们所有编写好的模块每一个都引入进来
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mvvm</title>
</head>
<body>
<div id="app">
<input v-model="htmls" type="text">
<input v-model="infos.age" type="text">
{{ name }}
<p>
<a href="aaa">{{ infos.name }}{{ infos.age }}--{{getinfosAge}}</a>
</p>
<p v-html="htmls">aa</p>
<button v-on:click="setAgeNum">点击</button>
<button @click="setAgeNum">点击1</button>
</div>
<script src="./Mvvm.js"></script>
<script src="./Compiler.js"></script>
<script src="./Oberser.js"></script>
<script src="./Watcher.js"></script>
<script>
var vms = new Vue({
el:'#app',
data:{
name:'asaaaa',
htmls:'<h1>标题</h1>',
infos:{
name:'aaas',
age:18
}
},
// computed带有缓存--如果视图不变化,视图就不要会进行更新
computed:{
getinfosAge(){
return this.infos.age*10;
}
},
methods:{
setAgeNum(){
this.infos.age= this.infos.age*100;
console.log('asd');
}
}
})
</script>
</body>
</html>
效果
总结
其实MVVM在概念上才是真正将页面与数据逻辑分离的模式,它把数据绑定工作放到一个JS里去实现,通过Compiler 对html中的指令与模板进行编译,然后通过把mode绑定到UI上,更改数据,Oberser对新数据劫持,其次是Watcher派发更新视图实现了MVVM。其实理解了流程再实现很简单,通过探究原理自己也收获了不少。希望这篇文章能够帮助到你!
推荐