理解
M: 简单理解为script里面的data数据 里面数据的值是看不到的 从后端传来的数据
V:在el管理的模块为view视图 在浏览器页面可以看到的数据
VM:数据监听与绑定
在视图中可以双向绑定模型中的值 改变视图中的值 则模型中的值相应的改变 相反也是
原理
mvvm分为两个模块 一个时编译模板Complier 找到页面中需要替换的指令并对其数据进行替换 一个是Observer 把数据变成响应式数据
下面详细介绍
分解 Vue 实例
如何入手?首先从怎么使用Vue开始。让我们一步步解析Vue的使用:
let vm = new Vue({
el: '#app'
data:{
school:{
name:'beida',
age:100
}
},
})
vue
当new Vue({}) 时就知道需要配置一个Vue类 {}里面的会传入到Vue类的constructor
class Vue{
constructor(options){ //options传来的是一个对象
this.$el = options.el
this.$data = options.data
}
if(this.$el){
//把数据变成响应式数据
new Observer(this.$data)
//编译模板
new Complier(this.$el,this)
}
}
Complier
在.html中el管理的模块中使用v-model v-text v-html v-if v-show {{}}等时数据并不会显示在浏览器页面上
在Vue类中new Complier() 则说明需要一个Complier类 传入到这个类的constructor
class Complier{
constructor(el,vm){
this.el = isElementNode(el) ? el : document.queryselector(el)
this.vm = vm
}
//会把浏览器页面的所有节点放到自己建好的空间中 这时浏览器页面上没有任何东西
let fragment = node2fragment(this.el)
//替换数据
this.complier(fragment)
//把替换好的数据重新放到浏览器上
this.el.appendChild(fragment)
}
判断是元素节点还是其他节点(属性节点、文本节点)
isElementNode(node){
//1代表元素节点 2代表属性节点 3代表文本节点
return node.nodeType == 1
}
把节点放到自己建好的空间
node2fragment(node){
let fragment = document.createDocumentFragment()
let firstChild
//因为不知道有多少个节点 所以使用while循环
while(firstChild = node.firstChild){
fragment.appendChild(firstChild)
}
return fragment
}
替换数据
首先需要判断是元素节点 还是文本节点 然后对其分别做处理
complier(node){
// console.dir(node) //#document-fragment 有时会把标签显示出来
//childNodes不包括li 那是孙子节点了
// console.dir(node.childNodes) //[text, input, text, div, text, div, text, ul, text] 换行是一个文本节点 如果都放到一行的话 就不会出现文本节点
// console.log(Array.isArray(node.childNodes)) //false 伪数组
let childNodes = node.childNodes;
// console.log(Array.isArray([...childNodes])); //true
// ...前面的一句必须加上分号
[...childNodes].forEach(child => {
if(this.isElementNode(child)){
//是元素节点
// console.log(child+'是一个元素节点')
this.complier(child)
this.complierElement(child)
}else{
//是文本节点
// console.log(child+'是一个文本节点')
this.complierText(child)
}
});
}
对元素节点进行处理之找到是以'v-'开头的属性 并且调用CompilerUtil
complierElement(node){
// console.dir(node) //所有的元素节点(例如<input type="text" v-model='school.name'>)
let attributes = node.attributes;
// console.log(typeof attributes) //object 伪数组
[...attributes].forEach(attr=>{
// console.log(attr) //type='text' v-model='school.name'
// console.log(typeof attr) //object
let {name,value:expr} = attr
// console.log(name) //必须是name type v-model
// console.log(value) //text school.name
if(this.isDirective(name)){
//元素节点的属性节点名字是以'v-'开头
// console.log(name+'是一个vue指令') //v-model是一个指令
// console.log(name.split('-')) //['v','model']
let [,directive] = name.split("-");
// console.log(directive) //model
CompilerUtil[directive](node,expr,this.vm);
}
})
}
对元素节点进行处理之获取到data里面的数据并进行更新数据 写一个对象 里面包含了不同指令的处理方法 是单独放出来的 没有在任何的类中
CompilerUtil = {
getVal(vm,expr){
return expr.split(".").reduce((data,current)=>{
// console.log(expr.split('.')) ['school','name']
//一共输出两次
// console.log(data) //1 vue实例 2{name:'beida',age:100}
// console.log(current) //1school 2name
return data[current]
},vm.$data);
},
setVal(vm,expr,value){
// console.log(vm) //vue实例
// console.log(expr) //school.name
// console.log(value) //在input框中重新输入的值
// console.log(expr.split('.')) //['school','name']
expr.split('.').reduce((data,current,index,arr)=>{
//一共输出两次
// console.log(data) //1vue实例 2{name:'beida',age:100}
// console.log(current) //1school 2name
// console.log(index) //1 0 2 1
// console.log(arr) // 1['school','name'] 2['school','name']
if(index == 1){
// console.log(data[current]) //beida
return data[current] = value
}
return data[current]
},vm.$data)
},
model(node,expr,vm){
let fn = this.updater["modelUpdater"]
// 给输入框添加一个观察者,如果后面data数据改变了,则也需要改变视图上的数据
new Watcher(vm,expr,(newVal)=>{
//更新数据
fn(node,newVal)
})
//给input输入框添加一个监听事件 这样会监听视图数据改变 那么data数据也改变
node.addEventListener('input',(e)=>{
// console.log(e.target.value)
let value = e.target.value
this.setVal(vm,expr,value)
})
let value = this.getVal(vm,expr)
fn(node,value);
},
}
对文本节点进行处理之找到{{}}并调用ComplierUtil['text']
complierText(node){
// console.dir(node) //#text '{{school.name}}' '{{school.age}}' '1' '1'
let content = node.textContent;
// console.log(content) //得到所有的文本节点中的内容 {{school.name}} {{school.age}} 1 1
let reg = /\{\{(.+?)\}\}/; // {}在正则中有特殊的含意,需要转义
if(reg.test(content)){
// console.log(content) // {{school.name}} {{school.age}}
CompilerUtil['text'](node,content,this.vm)
}
}
对文本节点进行处理之{{}}用什么代替和找到data里面的数据并更新 在Complier
ComplierUtil = {
//更新数据
updater:{
textUpdater(node,value){
// textContent得到文本节点中内容
node.textContent = value
}
},
// 得到新的内容
getContentValue(vm,expr){
return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(vm,args[1])
})
},
text(node,expr,vm){
let fn = this.updater["textUpdater"]
//{{}}用什么代替
let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
// console.log(args)
//["{{school.name}}", "school.name", 0, "{{school.name}}"]
//["{{school.age}}", "school.age", 0, "{{school.age}}"]
//["{{getSchool}}", "getSchool", 0, "{{getSchool}}"]
//添加一个water 如果后面data数据改变了,则也需要改变视图上的数据
new Watcher(vm,args[1],()=>{
fn(node,this.getContentValue(vm,expr));
})
return this.getVal(vm,args[1])
})
fn(node,content);
},
}
Observer
实现把数据变成响应式数据 获取数据或者数据改变时都会触发 这样vm就可以通知观察者进行改变数据 当获取数据时把所有的观察者存储到Dep中 数据改变时它会触发dep.notify()=>通知观察者改变数据water.update()
在Vue类中new Observer(this.$data) 说明要写一个Observer类
class Observer{
constructor(data){
// console.log(data) //不实现observer这个方法时 数据不是响应式的
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(); // 不同的watcher放到不同的dep中
Object.defineProperty(obj,key,{
// 当你获取school时,会调用get
get(){
Dep.target && dep.subs.push(Dep.target)
// console.log("get....")
// console.log(dep.subs)
return value
},
// 当你设置school时,会调用set
set:(newVal)=>{
// 当赋的值和老值一样,就不重新赋值
if(newVal != value){
this.observer(newVal)
value = newVal
// console.log('set...')
dep.notify();
}
}
})
}
}
Dep
为了存储不同的观察者 在Observer中new Dep() 所以需要建一个Dep类
class Dep{
constructor(){
this.subs = []
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher=>watcher.update())
}
}
Water
在元素节点或者文本节点的值改变时需要new Water 把新值重新渲染
在new Water()时它会监测到哪个属性改变 并把旧的值存储 如果改变 会把调自己的回调函数cb 把新值传过去
class Watcher{
constructor(vm,expr,cb){
this.vm = vm
this.expr = expr
this.cb = cb
this.oldValue = this.get()
// console.log(this.oldValue)
}
get(){
Dep.target = this
let value = CompilerUtil.getVal(this.vm,this.expr)
Dep.target = null
return value
}
update(){
let newVal = CompilerUtil.getVal(this.vm,this.expr)
// console.log(newVal)
if(newVal != this.oldValue){
this.cb(newVal)
}
}
}
使用vue脚手架时 在console直接输入vm.school 而不用输入vm.$data.school 这是因为把数据添加到了vue上面 在Vue类中
proxyVm(data){
// console.log(data) {school:{name:'beida',age:100}}
for(let key in data){
// console.log(key) //school
// console.log(this) //Vue {$el: "#app", $data: {…}} vue实例
Object.defineProperty(this,key,{
get(){
return data[key]
}
})
}
}
然后在Vue类中添加
class Vue{
this.proxyVm(this.$data)
}
在vue类中添加计算属性
class Vue{
this.computed = options.computed
for(let key in computed){
// console.log(key) //getSchool
Object.defineProperty(this.$data,key,{
get:()=>{
return computed[key].call(this);
}
})
}
}