MVVM
MVVM是Model-View-ViewModel的简写,将视图 UI 和业务逻辑分开
ViewMode:也就是mvc 的控制器,取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑
源码
MVue.js
import {Observer, Watcher} from './Observer'
//编译类
class Compile{
constructor(el,vm){
//判断是否为节点
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
//1.获取文档碎片对象 ,放入内存中减少页面的回流与重绘
const fragment = this.node2Fragment(this.el)
//2.编译模板
this.compile(fragment)
//3.追加子元素到根元素
this.el.appendChild(fragment)
}
//编译
compile(fragment){
//1.获取子节点
const childNodes = fragment.childNodes;
[...childNodes].forEach( child => {
// console.log(item)
if(this.isElementNode(child)){
//元素节点
//编译元素节点
// console.log('元素节点'+child.tagName)
this.compileElement(child)
}else{
//文本节点
//编译文本节点
// console.log('文本节点'+child)
this.compileText(child)
}
if(child.childNodes && child.childNodes.length){
this.compile(child)
}
})
}
//编译元素节点
compileElement(node){
//取得属性
const attrs = node.attributes
//通过转换成数组来遍历属性
let arr = [...attrs]
arr.forEach(attr => {
const {name, value} = attr
//进行v-开头判定 true就是指令
if(this.isDirective(name)){
//v-text->text
const [,dirctive] = name.split('-')
//对on:click继续->click 而如果为text 就放在第一个参数
//v-bind:src
const [dirName,eventName] = dirctive.split(':')
//对不同类型的属性进行赋值到节点上 更新数据 数据驱动视图
compileUtil[dirName](node,value,this.vm,eventName)
//删除有指令的标签的属性
node.removeAttribute('v-'+ dirctive)
}
//@click='handle'
else if(this.isEventName(name)){
let [,eventName] = name.split('@')
compileUtil['on'](node, value, this.vm, eventName)
}
//:class='red'
else if(this.isEllipsisName(name)){
let [, propsName] = name.split(':')
compileUtil['bind'](node, value, this.vm, propsName)
}
});
}
//编译文本节点
compileText(node){
//获得文本
const content = node.textContent
//通过正则只取{{}}的文本
const expr = /\{\{(.+?)\}\}/
if(expr.test(content)){
compileUtil['text'](node,content,this.vm)
}
}
//@开头
isEventName(attrName){
return attrName.startsWith('@')
}
//:开头
isEllipsisName(attrName){
return attrName.startsWith(':')
}
//判断是否是以v-开头
isDirective(attrName){
return attrName.startsWith('v-')
}
//创建文档碎片对象
node2Fragment(el){
const fragment = document.createDocumentFragment()
let firstChild;
while(firstChild = el.firstChild){
fragment.appendChild(firstChild)
}
return fragment
}
//判断是不是节点对象
isElementNode(node){
return node.nodeType === 1 ;
}
}
//编译工具类
export const compileUtil = {
//node 节点 expr 属性指向的数据名 vm vue实例对象用于去的data数据 event 事件属性
//解决 person.name类型
getVal(expr,vm){
//[person, name] data 之前的值 curr 当前值
//执行过程 先将curr取到 person 将person对象付给data 再将curr取到name 就获得person.name
return expr.split('.').reduce((data,currentVal) => {
// console.log(currentVal)
return data[currentVal]
},vm.$options.data)
},
setVal(expr,vm,inputVal){
return expr.split('.').reduce((data,currentVal) => {
// console.log(currentVal)
data[currentVal] = inputVal
},vm.$options.data)
},
getContentVal(expr,vm){
let value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(args[1],vm)
})
return value
},
//处理文本
text(node, expr, vm){
let value
//处理{{}}文本
if(expr.indexOf('{{')!== -1){
//取得{{}}里的内容 并调getval取得data的数据
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
new Watcher(vm, args[1], () => {
this.updater.textUpdater(node,this.getContentVal(expr,vm))
})
return this.getVal(args[1],vm)
})
}
//处理v-text
else{
value = this.getVal(expr, vm)
}
//数据渲染到视图
this.updater.textUpdater(node,value)
},
html(node, expr, vm){
let value = this.getVal(expr,vm)
//绑定watcher 对数据监听 更改数据时,绑定对应更新的函数
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node,newVal)
})
this.updater.htmlUpdater(node,value)
},
model(node, expr, vm){
const value = this.getVal(expr,vm)
//数据=》视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node,newVal)
})
//视图=》数据
node.addEventListener('input', (e) => {
this.setVal(expr, vm, e.target.value)
})
this.updater.modelUpdater(node,value)
},
//event 响应事件函数
on(node, expr, vm, event){
let fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(event,fn.bind(vm),false)
},
//props 属性src等
bind(node, expr, vm, props){
const value = this.getVal(expr,vm)
this.updater.bindUpdater(node,props,value)
},
//更新函数类
updater:{
textUpdater(node,value){
node.textContent = value
},
htmlUpdater(node,value){
node.innerHTML = value
},
modelUpdater(node,value){
node.value = value
},
bindUpdater(node,props,value){
node.setAttribute(props,value)
}
}
}
//Vue类
export class MVue{
constructor(options){
this.$el = options.el
this.$data = options.$data
this.$options = options
if(this.$el){
//1.实现数据观察者
new Observer(this.$options.data)
//2.实现指令解析器
new Compile(this.$el,this)
this.proxyData(this.$options.data)
}
}
proxyData(data){
for(const key in data){
Object.defineProperty(this, key, {
get(){
return data[key]
},
set(newVal){
data[key] = newVal
}
})
}
}
}
// export default MVue
Observer.js
import {compileUtil} from './MVue'
//数据观察者类
export class Observer{
constructor(data){
this.observer(data)
}
observer(data){
if(data && typeof data === 'object'){
//遍历data中的属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
defineReactive(obj, key, value){
//递归下一层
this.observer(value)
const dep = new Dep()
//劫持数据 对所有属性进行劫持
Object.defineProperty(obj,key,{
//可遍历
enumerable: true,
//可更改
configurable:false,
get(){
//订阅数据发生变化时,往dep中添加观察者
Dep.target && dep.addSub(Dep.target)
return value
},
//当我修改数据时,会进到set 然后进行监听 然后进行值的更新
set: (newValue) => {
//对于新值进行监听
this.observer(newValue)
if(newValue !== value){
value = newValue
}
//告诉dep变化
//进行通知
dep.notify()
},
})
}
}
//数据依赖器
class Dep{
constructor(){
this.subs = []
}
//收集观察者
addSub(watcher){
this.subs.push(watcher)
}
//通知观察者更新
notify(){
//拿到观察者 然后更新
this.subs.forEach(w => {
w.update()
})
}
}
//观察数据
export class Watcher{
constructor(vm,expr,cb){
this.vm = vm
this.expr = expr
this.cb = cb
//先保存旧值
this.oldVla = this.getOldVal()
}
getOldVal(){
//未初始化 挂载在dep
Dep.target = this
let oldVal = compileUtil.getVal(this.expr, this.vm)
Dep.target = null
return oldVal
}
//在watcher中拿到新值 callback
update(){
let newVal = compileUtil.getVal(this.expr, this.vm)
if(newVal !== this.oldVla){
this.cb(newVal)
}
}
}
// module.exports = {
// Observer,
// Watcher
// }
index.js
import './index.less';
import {MVue} from './MVue'
const vm = new MVue({
el:'#app',
data:{
person:{
name:'xa',
age:18,
},
msg:'MVVM',
color:'red'
},
methods:{
handel(){
this.msg= '学习MVVM'
}
}
})
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webpack-template</title>
</head>
<body>
<div id="app">
<h2>{{person.name}}</h2>
<h3 v-html='person.age'></h3>
<h3 v-html='msg'></h3>
<div v-text='msg'></div>
<div v-text='person.name'></div>
<input type="text" v-model='msg'>
<button v-on:click='handel'>2222</button>
<button @click='handel'>@@@</button>
<ul>
<li v-bind:class='color'>{{msg}}</li>
<li :class='color'>{{msg}}</li>
<li>{{msg}}</li>
</ul>
</div>
</body>
</html>
上面文件通过webpack进行打包
面试题
阐述一下你所理解vue的MVVM响应式原理
vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果