MVVM
以下代码我已做了详细的注释以及思维导图图片版,对指令解析以及依赖收集、属性监听、更新视图等的核心部分进行手写,如需思维导图的可联系博主,不喜勿喷(前端小白)。
1.首先MVue的作用是利用Compile这个类去解析例如v-text v-model等等这种指令以及差值表达式让数据可以正常的显示在页面上 是利用了updater这个类去初始化视图的并new Watcher以便于后续的视图更新 当new Watcher的过程中会调用getOldVal的方法但是由于做了数据劫持所以这里必定会访问到get()方法 当我们访问到该方法后 就将订阅了观察者 随后就释放以免多个观察者进行观察同一个数据 2.利用Observer中的defineProperty去对数据进行劫持这样可以看到数据是否发生变化,当数据发生变化之后我们需要做以下两点 (1)创建每个数据对应的观察者并将其添加到Dep中(创建观察者添加订阅) (2)然后利用订阅去告诉对应的观察者什么数据发生了变化 (3)观察者收到通知之后去更新视图 即根据是否产生了新值去利用updater这个类更新视图
所以整个线路分为两步 1.编译指令和插值表达式并初始化视图 2.进行数据劫持 两者之间的关联 当初始化视图的时候我们要new Watcher也就是为了后续数据的更新绑定观察者,当数据更新的时候调用回调更新视图。但是由于做了数据劫持所以在new Watcher的时候会访问getOldVal这个函数 也就意味着会调用get方法 那么此时就会形成观察者与订阅者的关联 并且将观察者订阅起来。最后当数据变化的时候 就会调用set()方法然后通知订阅者就会通知观察者去更新视图
一.html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>Document</title>
</head>
<body>
<div id="app">
<h2>{{person.name}} -- {{person.age}}</h2>
<h3>{{person.fav}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-text="x" class='a'></div>
<div v-text="person.name" class='a'></div>
<div v-html='str'></div>
<div v-html="person.fav"></div>
<input type="text" v-model="msg">
<button @click='handleClick'>@</button>
<button v-on:click="handleClick">on</button>
<a href="x">百度</a>
<a href="http://www.baidu.com">百度</a>
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el:"#app",
data:{
str:'123',
person:{
name:'zcl',
age:18,
fav:'computer'
},
msg:"学习mvvm原理",
x:'http://www.baidu.com'
},
methods:{
handleClick(){
console.log(this)
}
}
})
</script>
</body>
</html>
二.Compile
const compileUtil = {
// 工具类
getVal(expr,vm){
// 专门为了处理person.name这种形式的数据 获取真正的值
return expr.split('.').reduce((data,currentVal)=>{
return data[currentVal]
},vm.$data)
},
getContentVal(expr,vm){
return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(args[1],vm)
})
},
text(node,expr,vm){ // expr -> "x" "person.name" {{person.name}}--{{person.age}} {{person.fav}}
// 这是重点
let value
if(expr.indexOf('{{')!==-1){
// {{person.name}}--{{person.age}} {{person.fav}}
value = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
// replace() 方法的参数 replacement 可以是函数而不是字符串。在这种情况下,每个匹配都调用该函数,它返回的字符串将作为替换文本使用。该函数的第一个参数是匹配模式的字符串。
// 接下来的参数是与模式中的子表达式匹配的字符串,可以有 0 个或多个这样的参数。
// args[1] -> person.name
console.log(args[1])
new Watcher(vm,args[1],()=>{
// {{person.name}} -- {{person.age}} 如果是replace这种情况 会创建两个Watcher对该插值表达式进行观察
// 当你改变person.name的值的时候 只需要将前面{{person.name}}进行更新
// 如果写成this.updater.textUpdater(node,newVal)会产生什么后果:
// 首先我们传递进去的是两个expr 分别是person.name person.age
// 而我们回调回来的只可能是其中的一个 所以如果我们使用这种方法 当你修改person.name或person.age的时候都会将整个模板进行替换
// 而我们这个只需要更新修改的值 所以我们在更新的时候继续去匹配{{}}
this.updater.textUpdater(node,this.getContentVal(expr,vm))
})
return this.getVal(args[1],vm)
})
}else{
// 不是插值表达式
value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{
this.updater.textUpdater(node,newVal)
})
}
this.updater.textUpdater(node,value)
},
html(node,expr,vm){ // expr -> "x" "person.name"
const value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdater(node,newVal)
})
this.updater.htmlUpdater(node,value)
},
on(node,expr,vm,eventName){ // expr -> handleClick eventName->click
let fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(eventName,fn.bind(vm),false)
},
setVal(expr,vm,inputVal){
return expr.split('.').reduce((data,currentVal)=>{
data[currentVal] = inputVal
},vm.$data)
},
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)
},
updater:{
htmlUpdater(node,value){
node.innerHTML = value
},
textUpdater(node,value){
node.textContent = value
},
modelUpdater(node,value){
node.value = value
}
}
}
class Compile{
constructor(el,vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 创建文档碎片对象 减少页面的回流和重绘
const fragment = this.createFragment(this.el)
// 编译模板
this.compile(fragment)
this.el.appendChild(fragment)
}
isElementNode(node){
return node.nodeType === 1
}
createFragment(el){
const f = document.createDocumentFragment()
let firstChild
while(firstChild=el.firstChild){
f.appendChild(firstChild) // appendChild的时候会将el种的元素添加到文档碎片对象中并移除!
}
return f
}
isDirective(attrName){
return attrName.startsWith('v-')
}
isEventName(attrName){
return attrName.startsWith('@')
}
compileElement(node){
// <div v-text="x" class='a'></div> 里面含有v-text指令
// <div v-html="person.fav"></div> 里面含有v-html指令
// <input type="text" v-model="msg"> 里面含有v-model指令
// <button @click='handleClick'>@</button> 里面含有点击事件 又细分为@click与v-on:click
// <button v-on:click="handleClick">on</button>
const attributes = node.attributes;
[...attributes].forEach(attr=>{
const {name,value} = attr
// name-> v-text v-html type v-model @click v-on:click
if(this.isDirective(name)){
// 判断是否是指令 对于普通属性不需要处理如id class style等因为这些会在模板编译的时候进行处理
const [,dirctive] = name.split('-')
// dirctive -> text html model on:click 注意这里@click并不在里面
const [dirName,eventName] = dirctive.split(':')
// dirName -> text html model on
// eventName -> click
compileUtil[dirName](node,value,this.vm,eventName)
node.removeAttribute('v-'+ dirctive)
}else if(this.isEventName(name)){
// 处理@click这种指令
let [,eventName] = name.split('@')
compileUtil['on'](node,value,this.vm,eventName)
}
})
}
compileText(node){
// 这里主要是处理{{}}
const content = node.textContent
// content-> {{person.name}}---{{person.age}} {{person.fav}}
if(/\{\{(.+?)\}\}/.test(content)){
compileUtil['text'](node,content,this.vm)
}
}
compile(fragment){
const childNodes = fragment.childNodes; // childNodes用来获取所有的子节点包括元素和文本节点
[...childNodes].forEach(child=>{ // childNodes是类数组 需要将其转化为数组形式
if(this.isElementNode(child)){
// 解析元素节点
this.compileElement(child)
}else{
// 解析文本节点
this.compileText(child)
}
if(child.childNodes && child.childNodes.length){
// 递归 将所有子节点暴露出来 类似<ul><li></li></ul> <div>{{name}}</div>
this.compile(child)
}
})
}
}
class MVue{
constructor(options){
this.$options = options
this.$el = options.el
this.$data = options.data
if(this.$el){
//1.实现数据观察
new Observer(this.$data)
//2.指令解析
new Compile(this.$el,this)
}
}
}
三.Observer
class Watcher{
constructor(vm,expr,cb){
this.vm = vm
this.expr = expr
this.cb = cb
this.oldVal = this.getOldVal()
}
getOldVal(){
Dep.target = this // 让Dep与Watcher形成联系
const oldVal = compileUtil.getVal(this.expr,this.vm)
// 其实这里getVal 取值的方式是vm.$data.x 类似这种形式 所以这里会触发get方法
// 我们在触发get方法的时候需要进行依赖的收集
// 也就是说我们在这里需要对每个数据添加观察者 然后收集这些观察者
Dep.target = null // 收集完后就销毁 这时候subs里面已经有对应的观察者了
return oldVal
}
update(){
// 当设置新值的时候 就会触发set()方法 在set方法处 让Dep通知观察者更新
const newValue = compileUtil.getVal(this.expr,this.vm)
if(newValue!=this.oldVal){
// 通过回调调用updater方法 更新数据再次更新视图
this.cb(newValue)
}
}
}
class Dep{
constructor(){
this.subs = [] // 存储依赖 即观察者
}
addSub(watcher){
// 依赖收集 即收集观察者
this.subs.push(watcher)
}
notify(){
// 当设置新值的时候 就是我们通知的时候
// 通知对应的观察者进行视图更新
this.subs.forEach(w=>w.update())
}
}
class Observer{
constructor(data){
this.observe(data)
}
observe(data){
if(data && typeof data === 'object'){
Object.keys(data).forEach(key=>{
this.defineReactive(data,key,data[key])
})
}
}
defineReactive(obj,key,value){
this.observe(value)
const dep = new Dep()
Object.defineProperty(obj,key,{
// 使用defineProperty进行劫持
enumerable:true,
configurable:false, // 不可删除
get(){
Dep.target && dep.addSub(Dep.target)
return value
},
set:(newValue)=>{ // 如果使用function(){}这里内部的this指向Object 但是我们希望内部指向为Observer所以用箭头函数忽略内部指向
this.observe(newValue)
if(newValue!=value){
value = newValue
}
// 通知变化
dep.notify()
}
})
}
}