vue知识点总结(持续更新)
一、Vue2知识点
1、MVVM是什么
1.简述
MVVM是Model(模型)-View(视图)-ViewModel(视图模型)的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。
2.MVVM模式的组成部分
模型:代表内容的数据访问层(以数据为中心)。
视图:就像在MVC和MVP模式中一样,视图是用户在屏幕上看到的结构、布局和外观(UI)。
视图模型:是View的抽象,负责View与Model之间信息转换,将View的Command传送到Model。
绑定器:声明性数据和命令绑定隐含在MVVM模式中。在Microsoft解决方案堆中,绑定器是一种名为XAML的标记语言。绑定器使开发人员免于被迫编写样板式逻辑来同步视图模型和视图。在微软的堆之外实现时,声明性数据绑定技术的出现是实现该模式的一个关键因素。
3.View与ViewModule连接可以通过下面的方式
Binding Data:实现数据的传递
Command:实现操作的调用
AttachBehavior:实现控件加载过程中的操作
4.MVVM优点
MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点
- 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
- 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
- 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xaml代码。
- 可测试。界面素来是比较难于测试的,测试可以针对ViewModel来写。
2、手写Mvvm(基于Vue)
思路: 要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer
,能够对数据对象的所有属性进行监听;
2、实现一个指令解析器Compile
,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数;
3、实现一个订阅者Dep
,如有监听数据对象属性值有变动可拿到最新值并通知订阅者;
4、实现一个观察者Watcher
,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图;
5、mvvm入口函数,整合以上三者。
1.创建Mvvm构造函数
用于初始化一个mvvm模块,提供控制的区域和挂载的数据。
function Mvvm(options = {}) {// 设置默认值,防止不传报错
this.$options = options;// 将所有属性挂载到$options上
let data = this._data = this.$options.data; // this._data 这里也和Vue一样
observe(data);// 观察数据的变化
}
// 将控制区域和响应式数据挂载到mvvm实例上
let mvvm = new Mvvm({
el: '#app',
data: {
singer: '张芸京',
song: '偏爱',
about: {
phone: '17773635475',
address: '地球'
}
}
});
2.数据劫持
Obeject.defineProperty()
来监听属性变动 那么将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter 这样的话,给这个对象的某个值赋值,就会触发setter,就能监听到数据变化。
function Observe(data) {
console.log('数据劫持');
for (let key in data) {
let val = data[key];// 当前属性值
observe(val);// 递归数据,深度数据劫持
Object.defineProperty(data, key, {
configurable: true,// 设置该属性可删除
get() {// 获取属性值
return val;
},
set(newVal) {// 获取属性值
if (val === newVal) {// 设置的值一样则不管(性能优化)
return;
}
val = newVal;
observe(newVal);// 设置新值后也需要定义成属性
}
})
}
}
// 外面再写一个函数
// 不用每次调用都写个new
// 也方便递归调用
function observe(data) {
if (!data || typeof data !== 'object') {// 防止递归栈溢出
return;
}
return new Observe(data);// 每次new都是一个新的方法
}
此时数据已经挂载到mvvm实例上,并且监听数据的获取和更新。
看到这里大家应该知道为什么vue中不能新增不存在的属性了吗?
因为不存在的属性没有get和set
// 重写数组的部分非纯函数方法
const arrayProto = Array.prototype;//拿到数组的原型
const arrayMethods = Object.create(arrayProto);
/**
* @description:对会改变数组的方法重构,从而弥补defineProperty的缺陷
*/
['push', 'pop', 'shift', 'unshift' ,'sort', 'splice', 'reverse'].forEach(method => {
arrayMethods[method] = function () {
arrayProto[method].call(this, ...arguments);
render();
}
})
/**
* @description:对象的添加,弥补defineProperty的缺陷
*/
function $set (data, key, value) {
if(Array.isArray(data)) {//如果是数组,调用重构的splice方法
data.splice(key, 1, value);
return value;
}
defineReactive(data, key, value);//同理,给新增的对象属性增加get,set描述符
render();
return value;
}
/**
* @description:对象的删除,弥补defineProperty的缺陷
*/
function $delete(data, key) {
if(Array.isArray(data)) {//如果是数组,调用重构的splice方法
data.splice(key, 1);
return;
}
delete data[key];
render();
}
如图被标记的地方就是通过递归observe(val)进行数据劫持添加上了get和set,递归继续向about里面的对象去定义属性,所以输出了 “数据劫持” 两次。
可能有读者不明白的为什么设置新值也需要observe(newVal)
,如果我不添加该代码,然后对数据进行操作呢?
对比前面的图,发现新设置的about对象并没有被数据劫持到。
3.数据代理(性能优化)
我们在使用vue时,明显不是每次通过mvvm._data.about这样去设置我们的挂载的数据的。(新增的代码使用+
标记)
数据代理就是让我们每次拿data里的数据时,不用每次都写一长串,直接在当前this对象下拿取即可。
function Mvvm(options = {}) {// 设置默认值,防止不传报错
this.$options = options;// 将所有属性挂载到$options上
let data = this._data = this.$options.data; // this._data 这里也和Vue一样
// this代理this._data
+ for(let key in data){// data和this._data是一样的
Object.defineProperty(this, key, {// 用this代理
configurable: true,
get() {
return this._data[key];// this即this._data
},
set(newVal) {
this._data[key] = newVal;
}
});
+ }
observe(data);// 观察数据的变化
}
4.数据编译
compile主要做的事情是解析模板指令,将模板中的变量( {{}}
插值表达式 )替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数。
// MVVM
function Mvvm(options = {}) {// 设置默认值,防止不传报错
this.$options = options;// 将所有属性挂载到$options上
let data = this._data = this.$options.data; // this._data 这里也和Vue一样
// this代理this._data
for (let key in data) {// data和this._data是一样的
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
});
}
observe(data);// 观察数据的变化
+ new Compile(options.el, this);// 此时已经this代理了this._data,接着就是数据替换更新{{}}
}
// 数据编译渲染
function Compile(el, vm) {
vm.$el = document.querySelector(el);// 将dom元素挂载到实例上(注意querySelector非实时性)
let fragment = document.createDocumentFragment();// 创建了一虚拟的节点对象,节点对象包含所有属性和方法
while (child = vm.$el.firstChild) {
fragment.appendChild(child);// 将el中的元素依次放入虚拟节点中
}
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {// 遍历挂载区域的子节点
let text = node.textContent;// 节点的文本内容
let reg = /\{\{(.*?)\}\}/g;// 匹配vue{{}}的插值表达式
if ((node.nodeType === 3) && reg.test(text)) {// 节点为文本节点且有插值表达式
let arr = RegExp.$1.split('.');// RegExp.$1指与正则表达式匹配的第一个,即{{ ** }}中的值,注意拆分的字符可能有空格
let val = vm;// 将实例赋值,前面已经用this代理了this._data
arr.forEach(key => {
val = val[key.trim()];// 获取this代理的_data,注意去除空格
// 如果是{{}}中about.phone,会先拿到vm[about],再about[phone]
});
// 用trim方法去除一下首尾空格
node.textContent = text.replace(reg, val).trim();// 替换文本
}
// 如元素节点还有子节点,继续递归replace,直到拿到文本节点的{{}}插值表达式
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment); // 寻找{{}}插值表达式并替换内容
vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}
此时我们发现,我们改变数据时,页面并没有响应式渲染呐?因为你的Compile
数据编译渲染只是在new Mvvm时执行了一次,后面改变数据当然不会从新渲染呐。( 下面我们就来看看怎么处理,其实这里就用到了特别常见的设计模式,发布订阅模式 )
5.发布订阅
发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行。
消息订阅器
维护一个数组,用来收集订阅者,数据变动触发notify,从而调用订阅者的update方法
// 消息订阅器,订阅和发布
function Dep() {
this.subs = [];// 维护一个数组(存放订阅者函数的事件池)
}
Dep.prototype = {
adddSub() {// 收集订阅者
this.subs.push(...arguments);// 不设置参数,想传多少个都可以添加进函数事件池
},
notify() {// 数据变动触发notify
this.subs.forEach(sub => sub.update());// 给每个订阅者绑定update
}
};
订阅者
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
// 监听函数
function Watcher(fn) {
this.fn = fn;// 将fn放到实例上
}
Watcher.prototype.update = function() {// 通过Watcher这个类创建的实例,都拥有update方法
this.fn();// 调用update就会执行监听的函数
};
// 示例
let watcher1 = new Watcher(()=>{console.log(1)});
let watcher2 = new Watcher(()=>{console.log(2)});
let dep = new Dep();
dep.adddSub(watcher1,watcher2);// 订阅者 订阅消息
dep.notify();// 数据变动订阅器 通知订阅者(发布)
接下来我们要使用发布订阅,当数据改变需要重新刷新视图,这就需要在replace替换的逻辑里来处理。
6.数据更新视图
1.前面的发布订阅只是模型,这里根据实际情况对监听函数进行完善
// 监听函数
function Watcher(vm, exp, fn) {
this.fn = fn;// 订阅者需要执行的函数
this.vm = vm;// 挂载的数据
this.exp = exp;// 匹配到的{{}}内容
// 添加一个事件
// 这里我们先定义一个属性
Dep.target = this;// 通过Dep定义一个全局target属性,暂存watcher
let arr = exp.split('.');
let val = vm;
arr.forEach(key => {
val = val[key.trim()];// 触发了get方法,从而添加订阅者
});
Dep.target = null;// 添加完订阅者移除watcher
}
2.在数据编译渲染函数中调用监听函数(增加的代码用+标记)
// 数据编译渲染
function Compile(el, vm) {
vm.$el = document.querySelector(el);// 将dom元素挂载到实例上(注意querySelector非实时性)
let fragment = document.createDocumentFragment();// 创建了一虚拟的节点对象,节点对象包含所有属性和方法
while (child = vm.$el.firstChild) {
fragment.appendChild(child);// 将el中的元素依次放入虚拟节点中
}
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {// 遍历挂载区域的子节点
let text = node.textContent;// 节点的文本内容
let reg = /\{\{(.*?)\}\}/g;// 匹配vue{{}}的插值表达式
if ((node.nodeType === 3) && reg.test(text)) {// 节点为文本节点且有插值表达式
let arr = RegExp.$1.split('.');// RegExp.$1指与正则表达式匹配的第一个,即{{ ** }}中的值,注意拆分的字符可能有空格
let val = vm;// 将实例赋值,前面已经用this代理了this._data
arr.forEach(key => {
val = val[key.trim()];// 获取this代理的_data,注意去除空格
// 如果是{{}}中about.phone,会先拿到vm[about],再about[phone]
});
// 用trim方法去除一下首尾空格
node.textContent = text.replace(reg, val).trim();// 替换文本
// 实例化一个订阅者,等待数据变化传入一个更新页面的函数
+ new Watcher(vm, RegExp.$1, newVal => {
+ node.textContent = text.replace(reg, newVal).trim();
+ });
}
// 如元素节点还有子节点,继续递归replace,直到拿到文本节点的{{}}插值表达式
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment); // 寻找{{}}插值表达式并替换内容
vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}
3.当获取值的时候就会自动调用get方法,设置值的时候就会自动调用set方法,于是我们去找一下数据劫持。
// 数据劫持
function Observe(data) {
let dep = new Dep();
for (let key in data) {
let val = data[key];// 当前属性值
observe(val);// 递归数据,深度数据劫持
Object.defineProperty(data, key, {
configurable: true,// 设置该属性可删除
get() {// 获取属性值
Dep.target && dep.addSub(Dep.target);// target存在,将watcher添加到订阅事件中
return val;
},
set(newVal) {// 获取属性值
if (val === newVal) {// 设置的值一样则不管
return;
}
val = newVal;
observe(newVal);// 设置新值后也需要定义成属性
dep.notify();// 更新值后让所有watcher的update方法执行即可
}
});
}
}
当set修改值的时候执行了dep.notify方法,这个方法里是执行watcher的update方法。
// 数据更新,更新视图
Watcher.prototype.update = function () {// 通过Watcher这个类创建的实例,都拥有update方法
// notify的时候值已经更改了
// 再通过vm, exp来获取新的值
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key.trim()]; // 通过get获取到新的值
});
this.fn(val);// 调用update就会执行传入的函数更新页面
};
7.优化
到这里其实我们我们已经完整写了一个简单地mvvm实现,但是细心的你会发现,在数据编译渲染函数Compile
中其实我们只能匹配到文本节点的第一个{{}}插值表达式(没发现的再回去看看哦)。
// 数据编译渲染
function Compile(el, vm) {
vm.$el = document.querySelector(el);// 将dom元素挂载到实例上(注意querySelector非实时性)
let fragment = document.createDocumentFragment();// 创建了一虚拟的节点对象,节点对象包含所有属性和方法
while (child = vm.$el.firstChild) {
fragment.appendChild(child);// 将el中的元素依次放入虚拟节点中
}
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {// 遍历挂载区域的子节点
let text = node.textContent;// 节点的文本内容
let reg = /\{\{(.*?)\}\}/g;// 匹配vue{{}}的插值表达式
if ((node.nodeType === 3) && reg.test(text)) {// 节点为文本节点且有插值表达式
function replaceTxt() {
node.textContent = text.replace(reg, (matched, placeholder) => {
// placeholder是匹配到的分组 如:song, about.phone
new Watcher(vm, placeholder, replaceTxt); // 监听变化,进行匹配替换内容
return placeholder.split('.').reduce((val, key) => {
return val[key.trim()];
}, vm);// vm是reduce的初始值
});
};
// 替换
replaceTxt();
}
// 如元素节点还有子节点,继续递归replace,直到拿到文本节点的{{}}插值表达式
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment); // 寻找{{}}插值表达式并替换内容
vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}
8.完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id='app'>
<h3>姓名:{{ singer }}</h3>
<h3>歌名:{{ song }}</h3>
联系方式:{{ about.phone }},联系地址:{{ about.address }}
</div>
</body>
</html>
<script>
// MVVM
function Mvvm(options = {}) {// 设置默认值,防止不传报错
this.$options = options;// 将所有属性挂载到$options上
let data = this._data = this.$options.data; // this._data 这里也和Vue一样
// this代理this._data
for (let key in data) {// data和this._data是一样的
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
});
}
observe(data);// 观察数据的变化
new Compile(options.el, this);// 别忘了此时已经this代理了this._data
}
// 数据劫持
function Observe(data) {
let dep = new Dep();
for (let key in data) {
let val = data[key];// 当前属性值
observe(val);// 递归数据,深度数据劫持
Object.defineProperty(data, key, {
configurable: true,// 设置该属性可删除
get() {// 获取属性值
Dep.target && dep.addSub(Dep.target);// 将watcher添加到订阅事件中 [watcher]
return val;
},
set(newVal) {// 获取属性值
if (val === newVal) {// 设置的值一样则不管
return;
}
val = newVal;
observe(newVal);// 设置新值后也需要定义成属性
dep.notify();// 让所有watcher的update方法执行即可
}
});
}
}
function observe(data) {
if (!data || typeof data !== 'object') {// 防止递归栈溢出
return;
}
return new Observe(data);// 每次new都是一个新的方法
}
// 数据编译渲染
function Compile(el, vm) {
vm.$el = document.querySelector(el);// 将dom元素挂载到实例上(注意querySelector非实时性)
let fragment = document.createDocumentFragment();// 创建了一虚拟的节点对象,节点对象包含所有属性和方法
while (child = vm.$el.firstChild) {
fragment.appendChild(child);// 将el中的元素依次放入虚拟节点中
}
function replace(frag) {
Array.from(frag.childNodes).forEach(node => {// 遍历挂载区域的子节点
let text = node.textContent;// 节点的文本内容
let reg = /\{\{(.*?)\}\}/g;// 匹配vue{{}}的插值表达式
if ((node.nodeType === 3) && reg.test(text)) {// 节点为文本节点且有插值表达式
function replaceTxt() {
node.textContent = text.replace(reg, (matched, placeholder) => {
// placeholder是匹配到的分组 如:song, about.phone
new Watcher(vm, placeholder, replaceTxt); // 监听变化,进行匹配替换内容
return placeholder.split('.').reduce((val, key) => {
return val[key.trim()];
}, vm);// vm是reduce的初始值
});
};
// 替换{{}}表达式
replaceTxt();
}
// 如元素节点还有子节点,继续递归replace,直到拿到文本节点的{{}}插值表达式
if (node.childNodes && node.childNodes.length) {
replace(node);
}
});
}
replace(fragment); // 寻找{{}}插值表达式并替换内容
vm.$el.appendChild(fragment); // 再将文档碎片放入el中
}
// 发布订阅模式,订阅和发布,如[fn1, fn2, fn3]
function Dep() {
this.subs = [];// 一个数组(存放函数的事件池)
}
Dep.prototype = {
addSub() {// 用于添加函数
this.subs.push(...arguments);// 不设置参数,想传多少个都可以
},
notify() {
this.subs.forEach(sub => sub.update());// 绑定的方法,都有一个update方法
}
};
// 监听函数
function Watcher(vm, exp, fn) {
this.fn = fn;// 需要执行的函数
this.vm = vm;// 挂载的数据
this.exp = exp;// 匹配到的{{}}内容
// 添加一个事件
// 这里我们先定义一个属性
Dep.target = this;
let arr = exp.split('.');
let val = vm;
arr.forEach(key => {
val = val[key.trim()];// 匹配的key获取对应的挂载的数据,默认就触发get方法
});
Dep.target = null;
}
Watcher.prototype.update = function () {// 通过Watcher这个类创建的实例,都拥有update方法
// notify的时候值已经更改了
// 再通过vm, exp来获取新的值
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key.trim()]; // 通过get获取到新的值
});
this.fn(val);// 调用update就会执行监听的函数
};
let mvvm = new Mvvm({
el: '#app',
data: {
singer: '张芸京',
song: '偏爱',
about: {
phone: '17773635475',
address: '地球'
}
}
});
</script>
本文中本人已经做了极为详细的注释,一步一步讲解mvvm的实现过程,如果这样你还不是很能理解的话,可要多读几次哦。例外,如发现代码有问题等欢迎反馈哦,感谢。
最后,mvvm实现这里是阅读了掘金一位大佬的博客(优化了少数代码),感兴趣的可以去看看大佬怎么实现的哦。点此跳转呢.
博主开始运营自己的公众号啦,感兴趣的可以关注“飞羽逐星”微信公众号哦,拿起手机就能阅读感兴趣的博客啦!