一、MVVM应用
MVVM最最根本的就是实现数据改变,视图更新,视图变化,数据也发生改变。
二、MVVM原理
一、Vue整体调度与编译模板Compiler
第一步:在创建Vue实例时直接new上了Vue,且在html文件中是直接script标签引入,不再使用import,所以要在js中创建一个Vue类,也不需要export将类导出啦!Vue类中constructor的参数options是创建的Vue实例中传递过去的对象,里面存在着$el参数。
第二步:如果对象中el存在,就能找到上面的HTML模板(视图),将模板中需要替换数据的元素进行替换,又称为编译(编译模板)。当存在el会调用编译模板(将$el和vue实例传过去,这样就找到模板)。
第三步:创建编译模板这样一个类,编译之前判断要编译的是否为元素节点;
判断el是否为元素节点,是的话就使用它本身,否则就拿到#app对应的一堆元素节点。
第四步:HTML要渲染出一张网页,要形成一棵DOM树,在DOM树上有两类节点:元素节点和文本节点。根据节点的属性nodeType来判断是哪种节点:元素节点:nodeType属性返回1;属性节点:nodeType属性返回2。
第五步:拿到模板后将模板中的内容一条条挪到内存空间(文档碎片)中,主要利用到了appendChild,往文档碎片当中移动性的扔数据。
第六步:将文档碎片中的数据进行替换(模板编译),将编译后的数据返回到网页中渲染出来。
先拿到节点,判断是元素节点还是文本节点
进行模板编译具体操作:
1)首先拿到属性节点,判断属性节点是否是以v-打头的指令,通过正则也找到插值表达式对应的节点;
2)封装个对象(工具),里面存在着不同指令对应的不同处理方法;
// 封装方法,存放不同指令对应的不同处理方法
CompilerUtil = {
// 处理得到指令对应的值
getval(vm, expr) {
return expr.split(".").reduce((data, current) => {
return data[current]
}, vm.$data)
},
// 处理v-model指令,实现数据绑定
model(node, expr, vm) {
let val = this.getval(vm, expr)
let fn = this.updater["modelUpdater"]
fn(node, val)
},
// 插值表达式去掉 v-text
text(node, content, vm) {
// 通过正则使数据中{{}}全部由后面得到的数据替换
let newcontent = content.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getval(vm, args[1])
})
let fn = this.updater["textUpdater"]
fn(node, newcontent)
},
// 更新数据
updater: {
// 更新v-model指令数据
modelUpdater(node, value) {
node.value = value;
},
// 更新文本内容数据
textUpdater(node, content) {
node.textContent = content;
}
}
}
3)编译元素节点:封装v-model对应的处理方法,给input框上附上value值,将原本的死数据给替换掉。
4)编译文本节点:封装插值表达式对应的处理方法,使用正则将{{}}中的内容替换掉。
5)实现真正的文本节点与元素节点的编译
二、Observer数据劫持
Observer实现数据劫持,把数据变成响应式(给数据添加get()和set()),当数据修改时可以感应到数据修改。
响应式数据设置时可以利用方法defineProperty来精细化设置某个属性,给其添加上get和set方法。
三、Watch观察者
在编译元素、文本时,触发相应方法来修改数据,此时会创建watcher(),进入观察者模式。观察者中保存老状态,数据改变更新状态,调用回调函数,根据状态干件事。
四、Dep类
Dep类是存储观察者的;当获取数据时将watcher推到Dep中,数据改变时会通知watcher调用update方法。
五、完整代码
class Dep{
constructor(){
this.subs = [];
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher=>watcher.update())
}
}
class Watcher{
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.oldState = this.get();
}
get(){
Dep.target = this
let state = CompilerUtil.getval(this.vm,this.expr)
Dep.target = null;
return state;
}
update(){
let newstate = CompilerUtil.getval(this.vm,this.expr)
if(newstate !=this.oldState){
this.cb(newstate)
}
}
}
class Observer{
constructor(data){
this.observer(data)
}
observer(data){
if(data && typeof data=='object'){
for(let key in data){
this.defindReactive(data,key,data[key])
}
}
}
defindReactive(obj,key,value){
this.observer(value)
let dep = new Dep()
Object.defineProperty(obj,key,{
get(){
Dep.target && dep.subs.push(Dep.target)
return value
},
// 设置数据
set:(newval)=>{
if(newval != value){
this.observer(newval)
value = newval;
dep.notify()//通知watcher调用update方法
}
},
})
}
}
class Compiler{
constructor(el,vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm;
let fragment = this.node2fragment(this.el)
this.compile(fragment);
this.el.appendChild(fragment)
}
compileElement(node){
let attributes = node.attributes;
[...attributes].forEach(attr=>{
let {name,value:expr} = attr
if(this.isDirective(name)){
let [,directive] = name.split("-")
CompilerUtil[directive](node,expr,this.vm)
}
})
}
isDirective(attrName){
return attrName.startsWith("v-")
}
compileText(node){
let content = node.textContent
let reg = /\{\{(.+?)\}\}/;
if(reg.test(content)){
CompilerUtil['text'](node,content,this.vm)
}
}
compile(fragment){
let childNodes = fragment.childNodes;
[...childNodes].forEach(child=>{
if(this.isElementNode(child)){
this.compileElement(child)
this.compile(child)
}else{
this.compileText(child)
}
})
}
node2fragment(node){
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild=node.firstChild){
fragment.appendChild(firstChild)
}
return fragment;
}
isElementNode(node){
return node.nodeType === 1;
}
}
CompilerUtil = {
getval(vm,expr){
return expr.split(".").reduce((data,current)=>{
return data[current]
},vm.$data)
},
getContentValue(vm,expr){
return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getval(vm,args[1])
})
},
setValue(value,expr,vm){
expr.split(".").reduce((data,current,index,arr)=>{
if(index==arr.length-1){
data[current] = value
}
return data[current]
},vm.$data)
},
model(node,expr,vm){
let val = this.getval(vm,expr)
let fn = this.updater["modelUpdater"]
new Watcher(vm,expr,(newstate)=>{
fn(node,newstate)
})
node.addEventListener('input',(e)=>{
let value = e.target.value
this.setValue(value,expr,vm)
})
fn(node,val)
},
text(node,content,vm){
let newcontent = content.replace(/\{\{(.+?)\}\}/g,(...args)=>{
new Watcher(vm,args[1],()=>{
fn(node,this.getContentValue(vm,content))
})
return this.getval(vm,args[1])
})
let fn = this.updater["textUpdater"]
fn(node,newcontent)
},
updater:{
modelUpdater(node,value){
node.value = value;
},
textUpdater(node,content){
node.textContent = content;
}
}
}
class Vue{
constructor(options){
this.$el = options.el;
this.$data = options.data;
if(this.$el){
new Observer(this.$data)
// console.log(this.$data)
// 需要让vm代理this.$data
this.proxyVm(this.$data)
new Compiler(this.$el,this)
}
}
// 让vm代理data
proxyVm(data){
for(let key in data){
// console.log(key)//school
// console.log(data)//school对象
// console.log(data[key])//school对象的值
// 将数据key交给vm代理 给vm上挂上key这个属性
Object.defineProperty(this,key,{
// vm.school 要想拿到数据就是获取数据,调用get()
get(){
return data[key]//将数据返回就OK
}
})
}
}
}
// vue中使用vue实例代理了data vm.$data.xxx--->vm.xxx
15,Observer实现数据劫持,把数据变成响应式(给数据添加get()和set()),当数据修改时可以感应到数据修改 16,在编译元素、文本时,触发相应方法来修改数据,此时会创建watcher(),进入观察者模式 17,观察者中保存老状态,数据改变更新状态,调用回调函数,根据状态干件事。 18,Dep类中包含多个Watcher。它是在获取数据时存放watcher的;当数据改变时会通知watcher调用update方法
1,Vue类是最高层,它负责整体的调度;当new上它是会调用constructor,且当el存在时会进行模板编译new Compiler; 2,Compiler类是编译模板;将元素节点转成文档碎片,在内存中编译元素,编译文本,将编译好的渲染到网页上 3,Observer类是数据劫持;把数据变成响应式(给数据添加get()和set()),当数据修改时可以感应到数据修改啦 4,Watcher类是观察者;在编译元素、文本时,触发相应方法来修改数据,此时会创建watcher(),调用watcher() 5,Dep类是存储观察者的;当获取数据时将watcher推到Dep中,数据改变时会通知watcher调用update方法 6,给input框监听一个input事件
编译模板:去页面中找到带有插值表达式和带有v-指令的节点 数据劫持:将所有数据都变成响应式的。 获取数据,触发get方法会创建watcher方法;将watcher全部存到Dep中 修改数据,触发set方法会调用notify方法通知Dep中所有Watcher调用其update方法 update方法会调用回调将数据重新赋值