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變更的雙向綁定效果
視訊講解