MVVM即是“Model-View-ViewModel”,它是一种设计模式,用于实现用户界面的分离和交互。
主要职责
View中视图变化,通过ViewModel中的
监听器
反馈给model进行数据的更新Model中数据的变化,通过ViewModel中的
解析器
反馈给View进行视图的更新
vue.js是采用
数据劫持
结合发布者-订阅者模式
的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。
进行数据的准备,我们目的是为了实现双向数据绑定
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<div id="app">
<h3>姓名:{{name}}h3>
<h3>年龄:{{more.age}}h3>
输入姓名:<input type="text" v-model="name">
<br>
输入年龄:<input type="text" v-model="more.age">
div>
<script src="./vue.js">script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: '张三',
more: {
age: 18
}
}
})
console.log(vm);
script>
body>
html>
数据初始化
//定义vue类
class Vue {
//构造函数
constructor(obj_instance) {
//执行初始化
this.$data = obj_instance.data
console.log(this.$data);
}
我们创建的vm实例已经传给vue类,为了模拟vue中 d a t a ,也在构造函数利用 t h i s . data,也在构造函数利用this. data,也在构造函数利用this.data来存储我们创建的vm实例中的data数据
只是此时的数据都还不是响应式的
//数据劫持 - 监听实例中的数据
function Observer(data_instance) {
//递归出口
if (!data_instance || typeof data_instance !== 'object') return;
//object.keys以数组形式返回对象中的属性
//遍历属性属性,通过obj.defineProperty来进行数据监视
Object.keys(data_instance).forEach((key) => {
let value = data_instance[key];
//递归将 子属性的值进行数据劫持
Observer(value);
//三个参数,(对象, 监视的属性, 回调)
Object.defineProperty(data_instance, key, {
//可以枚举 属性描述符可以改变
enumerable: true,
configurable: true,
//通过getter 和 setter函数进行数据监视
get() {
//访问属性时候 调用getter函数 返回return 值
console.log(`访问了属性:${key} -> 值为${value}`);
// console.log(Dependency.temp);
return value;
},
//修改的新属性值
set(newValue) {
console.log(`将属性:${key}的值${value} 修改为->${newValue}`);
value = newValue;
Observer(newValue);
},
});
});
}
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
//创建vue类
class Vue {
//执行初始化
constructor(obj_instance) {
this.$data = obj_instance.data;
//调用Observe - 对data中的每个数据进行数据劫持
//对data中的每一项进行响应式处理
Observer(this.$data);
//解析模板
Compile(obj_instance.el, this);
}
}
//HTML模板解析 - {{}}替换dom
function Compile(element, vm) {
//获取id为app的dom元素 绑定到vm.$el上
vm.$el = document.querySelector(element);
// console.log(vm.$el);
//创建文档碎片节点 临时存储数据的改变 避免过频繁地操作dom 文档片段存储在于内存中不在dom中,元素的改变不会引起页面的回流
const fragment = document.createDocumentFragment();
//循环将vm.$el中的dom元素 插入到fragment文档碎片中
let child;
while ((child = vm.$el.firstChild)) {
//使用fragment.append会将原先dom删除
fragment.append(child);
}
// console.log(fragment);
// console.log(fragment.childNodes);
//要将{{}}替换 所以节点类型为 1 和 3为h3
fragment_compile(fragment);
//替换文档碎片内容
function fragment_compile(node) {
//正则匹配 {{ 属性 }}
const pattern = /\{\{\s*(\S+)s*}\}/;
//如果节点为文本节点
if (node.nodeType === 3) {
const temp = node.nodeValue
//输出正则验证过后 去除换行符等一些不需要的元素 返回的数组 "{{ name }}" "name" 需要索引为1的值 不需要{{}}
const result_regex = pattern.exec(node.nodeValue);
if (result_regex) {
// console.log(vm.$data[result_regex[1]]);
const arr = result_regex[1].split('.');
//reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm.$data是初始值,total的初始值
const value = arr.reduce(
(total, current) => total[current],
vm.$data
);
//将 {{name}} {{more.age}} 替换成value
node.nodeValue = temp.replace(pattern, value);
}
return;
}
//将文档碎片 fragment渲染到el中
vm.$el.appendChild(fragment);
}
//递归遍历
node.childNodes.forEach((child) => fragment_compile(child));
要将name 和 more.age 替换成数据,通过reduce方法获取数据
reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm. d a t a 是初始值, t o t a l 的初始值,无法通过 v m . data是初始值,total的初始值,无法通过vm. data是初始值,total的初始值,无法通过vm.data[more.age]来获取数据
const value = arr.reduce(
(total, current) => total[current],
vm.$data
);
node.nodeValue = temp.replace(pattern, value);
vm.$el.appendChild(fragment);
//依赖 --收集和通知订阅者
class Dependency {
constructor() {
//收集订阅者
this.subscribers = [];
}
//添加订阅者
addSub(sub) {
this.subscribers.push(sub);
}
//通知订阅者
notify() {
//遍历订阅者 让订阅者触发自己的update函数
this.subscribers.forEach((sub) => sub.update());
}
}
//订阅者
class Watcher {
//三个参数
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
//临时属性 --触发getter
//因为想要将watcher实例添加到依赖的数组中
Dependency.temp = this;
//触发getter时候 将订阅者实例添加到订阅者数组中
key.split('.').reduce((total, current) => total[current], vm.$data )
//避免多次重复添加到订阅者数组中
Dependency.temp = null
}
//更新函数
update() {
//获取属性值
const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data )
this.callback(value);
}
}
将watcher类实例添加到Dep数组中来实现数据视图的绑定
//因为想要将watcher实例添加到依赖的数组中
Dependency.temp = this;
//触发getter时候 将订阅者实例添加到订阅者数组中
key.split('.').reduce((total, current) => total[current], vm.$data )
//避免多次重复添加到订阅者数组中
Dependency.temp = null
创建Dependency.temp用于临时存储创建的watcher实例,触发getter
在observer类中触发getter时,将临时存储的watcher实例添加到Dependency的存储订阅者的数组之中
get() {
//将订阅者实例添加到订阅者数组中
Dependency.temp && dependency.addSub(Dependency.temp)
},
视图与数据的绑定
在fragment_compile()函数中
//找v-model属性的元素 更改其nodeValue
if(node.nodeType === 1 && node.nodeName === 'INPUT'){
const attr = Array.from(node.attributes)
attr.forEach(item => {
if(item.nodeName === 'v-model'){
// console.log(item.nodeValue);
//修改nodeValue
const value = item.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
// console.log(value);
node.value = value
//创建watcher实例
new Watcher(vm, item.nodeValue, newValue => {
node.value = newValue
})
//触发input事件来通过视图修改数据
node.addEventListener('input', e => {
const arr1 = item.nodeValue.split('.')
// console.log(arr1);
const arr2 = arr1.slice(0, arr1.length - 1)
const final = arr2.reduce((total, current) => total[current], vm.$data)
// console.log(final);
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
在文档碎片fragment中遍历node,通过node.attributes方法来找到属性值为v-model的node节点
遍历的节点 item.nodeValue,是name, more.age
final[arr1[arr1.length - 1]] = e.target.value
html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<div id="app">
<h3>姓名:{{name}}h3>
<h3>年龄:{{more.age}}h3>
输入姓名:<input type="text" v-model="name">
<br>
输入年龄:<input type="text" v-model="more.age">
div>
<script src="./vue.js">script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: '张三',
more: {
age: 18
}
}
})
console.log(vm);
script>
body>
html>
vue.js
//创建vue类
class Vue {
//执行初始化
constructor(obj_instance) {
this.$data = obj_instance.data;
//调用Observe - 对data中的每个数据进行数据劫持
//对data中的每一项进行响应式处理
Observer(this.$data);
//解析模板
Compile(obj_instance.el, this);
}
}
//数据劫持 - 监听实例中的数据
function Observer(data_instance) {
//递归出口
if (!data_instance || typeof data_instance !== 'object') return;
//创建订阅者实例
const dependency = new Dependency()
//object.keys以数组形式返回对象中的属性
// console.log(Object.keys(data_instance));
//遍历属性属性,通过obj.defineProperty来进行数据监视
Object.keys(data_instance).forEach((key) => {
let value = data_instance[key];
//递归将 子属性的值进行数据劫持
Observer(value);
//三个参数,(对象, 监视的属性, 回调)
Object.defineProperty(data_instance, key, {
//可以枚举 属性描述符可以改变
enumerable: true,
configurable: true,
//通过getter 和 setter函数进行数据监视
get() {
//访问属性时候 调用getter函数 返回return 值
console.log(`访问了属性:${key} -> 值为${value}`);
// console.log(Dependency.temp);
//将订阅者实例添加到订阅者数组中
Dependency.temp && dependency.addSub(Dependency.temp)
return value;
},
//修改的新属性值
set(newValue) {
console.log(`将属性:${key}的值${value} 修改为->${newValue}`);
value = newValue;
Observer(newValue);
dependency.notify()
},
});
});
}
//HTML模板解析 - {{}}替换dom
function Compile(element, vm) {
//获取id为app的dom元素 绑定到vm.$el上
vm.$el = document.querySelector(element);
// console.log(vm.$el);
//创建文档碎片节点 临时存储数据的改变 避免过频繁地操作dom 文档片段存储在于内存中不在dom中,元素的改变不会引起页面的回流
const fragment = document.createDocumentFragment();
//循环将vm.$el中的dom元素 插入到fragment文档碎片中
let child;
while ((child = vm.$el.firstChild)) {
//使用fragment.append会将原先dom删除
fragment.append(child);
}
// console.log(fragment);
// console.log(fragment.childNodes);
//要将{{}}替换 所以节点类型为 1 和 3为h3
fragment_compile(fragment);
//替换文档碎片内容
function fragment_compile(node) {
//正则匹配 {{ 属性 }}
const pattern = /\{\{\s*(\S+)s*}\}/;
//如果节点为文本节点
if (node.nodeType === 3) {
const temp = node.nodeValue
//输出正则验证过后 去除换行符等一些不需要的元素 返回的数组 "{{ name }}" "name" 需要索引为1的值 不需要{{}}
const result_regex = pattern.exec(node.nodeValue);
// console.log(result_regex);
if (result_regex) {
// console.log(vm.$data[result_regex[1]]);
const arr = result_regex[1].split('.');
//reduce迭代累加器 遍历arr数组 total[current] 不断地迭代的链式获取最终的值 ,reduce两个参数 , 第一个参数是个回调函数,第二参数vm.$data是初始值,total的初始值
const value = arr.reduce(
(total, current) => total[current],
vm.$data
);
//将 {{name}} {{more.age}} 替换成value
node.nodeValue = temp.replace(pattern, value);
//文档碎片替换的时候添加创建订阅者
new Watcher(vm, result_regex[1], newValue => {
//wacther的回调函数 会将文档碎片中的nodevalue更新为我们修改的newValue
node.nodeValue = temp.replace(pattern, newValue);
});
}
return;
}
//找v-model属性的元素 更改其nodeValue
if(node.nodeType === 1 && node.nodeName === 'INPUT'){
const attr = Array.from(node.attributes)
attr.forEach(item => {
if(item.nodeName === 'v-model'){
console.log(item.nodeValue);
//修改nodeValue
const value = item.nodeValue.split('.').reduce((total, current) => total[current], vm.$data)
// console.log(value);
node.value = value
//创建watcher实例
new Watcher(vm, item.nodeValue, newValue => {
node.value = newValue
})
//触发input事件来通过视图修改数据
node.addEventListener('input', e => {
const arr1 = item.nodeValue.split('.')
// console.log(arr1);
const arr2 = arr1.slice(0, arr1.length - 1)
const final = arr2.reduce((total, current) => total[current], vm.$data)
// console.log(final);
final[arr1[arr1.length - 1]] = e.target.value
})
}
})
}
//递归遍历
node.childNodes.forEach((child) => fragment_compile(child));
}
//将文档碎片 fragment渲染到el中
vm.$el.appendChild(fragment);
}
//依赖 --收集和通知订阅者
class Dependency {
constructor() {
//收集订阅者
this.subscribers = [];
}
//添加订阅者
addSub(sub) {
this.subscribers.push(sub);
}
//通知订阅者
notify() {
//遍历订阅者 让订阅者触发自己的update函数
this.subscribers.forEach((sub) => sub.update());
}
}
//订阅者
class Watcher {
//三个参数
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
this.callback = callback;
//临时属性 --触发getter
//因为想要将watcher实例添加到依赖的数组中
Dependency.temp = this;
//触发getter时候 将订阅者实例添加到订阅者数组中
key.split('.').reduce((total, current) => total[current], vm.$data )
//避免多次重复添加到订阅者数组中
Dependency.temp = null
}
//更新函数
update() {
//获取属性值
const value = this.key.split('.').reduce((total, current) => total[current], this.vm.$data )
this.callback(value);
}
}