1.Vue基础原理:
(1)vue.js中有两个核心功能:响应式数据绑定,组件系统
(2)MVC,MVP,MVVM之间的区别和理解;
*****MVC:
1) 视图(View):用户界面
2) 控制器(Controller):业务逻辑
3) 模型(Model):数据保存
MVC各个部分之间的通信方式如下:
1)视图传送指令到控制器
2)控制器完成业务逻辑后要求模型改变状态
3)模型将新的数据发送到视图,用户得到反馈
以上的所有通信都是单向的;接受用户指令的时候,MVC有两种方式。一种是通过视图接受指令,然后传递给控制器;另一种是用户直接给控制器发送指令;
实际使用中可能更加灵活,下面是Backbone.js为例说明:
1) 用户可以向视图(View)发送指令(DOM事件),再由View直接要求Model改变状态;
2) 用户也可以向Controller发送指令(改变URL触发hashChange事件,再由Controller发送给View
3) Controller很薄,只起到路由作用,而View非常厚,业务逻辑都放在View,所以Backbone索性取消了Controller,只保留了Router(路由器)
*****MVP:MVP适用于
事件驱动的应用架构中,如asp.net web form,window forms应用
1)各部分之间的通信都是双向的;
2)视图(View)和模型(Model)不发生联系,都是通过表现(Presenter)传递的
3)View非常薄,不部署任何业务逻辑,称之为被动视图(Passive View)即没有任何主动性,而Presenter非常厚,所有逻辑都在这里
*****MVVM: MVVM模式将Presenter层替换为ViewModel,其他与MVP基本一致,示意图如下:
1) 它和MVP的区别是,采用双向绑定,视图层(View)的变动,自动反映在ViewModel,反之亦然,Angular和Vue,React采用这种方式
2) MVVM的提出源于WPF,主要用于分离应用界面层和业务逻辑层,WPF,Siverlight都基于数据驱动开发
3) MVVM模式中,一个ViewModel和一个View匹配,完全和View绑定,所有View中的修改变化,都会更新到ViewModel中,同时ViewModel的任何变化都会同步到View上显示;之所以自动同步是ViewModel的属性都实现了observable这样的接口,也就是说当使用属性的set方法,会同时出发属性修改的事件,使绑定的UI自动刷新;
(3)数据双向绑定的流程:
1) 建立虚拟DOM Tree,通过document.createDocumentFragment(),遍历指定根节点内部节点,根据{
{prop}},v-model等规则进行compile(主要负责给node节点赋值);
2) 通过Object.defineProperty()进行数据变化拦截
3) 截取到的数据变化,通过发布者-订阅者模式,触发Watcher,从而改变虚拟DOM中的具体数据;
订阅发布模式(又称为观察者模式)定义一种一对多的关系,让多个观察者同时监听一个主题对象,主题对象状态发生改变的时候通知所有的观察者。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作
4) 通过改变虚拟DOM元素值,从而改变最后渲染dom树的值,完成双向绑定
完成数据双向绑定的关键在于:Object.defineProperty()
Vue的数据驱动主要实现建立在是三个对象上
Dep(
主题对象
),Watcher,Compiler
Dep 主要负责依赖的收集
Watcher 主要负责Dep和Compiler之间的联系
Compiler 可以理解为virtual dom(虚拟DOM) + patch 也就是负责视图层的渲染
(4)简易双绑:首先,我们把注意力集中到这个属性上:
Object.defineProperty;
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象
语法:Object.defineProperty(obj,prop,descriptor)
obj:要在其上定义属性的对象
prop:要定义或者修改的属性名字
descriptor:将定义或修改的属性描述符
举例如下:
var obj = {};
Object.defineProperty(obj,'hello',{ //这里整个都是属性描述符
get:function(){
//我们在这里拦截到了数据
console.log("get方法被调用");
},
set:function(newValue){
//改变数据的值,拦截下来额
console.log("set方法被调用");
}
});
obj.hello//输出为“get方法被调用”,输出了值。
obj.hello = 'new Hello';//输出为set方法被调用,修改了新值
通过以上方法可以看出,
获取对象属性值触发get,设置对象属性值触发set,因此我们可以想象到数据模型对象的属性
设置和读取可以驱动view层的数据变化,view的数据变化传递给数据模型对象,在Set里面可以做很多事情。
在这基础上,我们可以做到数据的双向绑定:
let obj = {};
Object.defineProperty(obj, 'name', {
set: function(newValue){
console.log('触发setter');
document.querySelector('.text-box').innerHTML = newValue;
document.querySelector('.inp-text').value = newValue;
},
get: function(){
console.log('触发getter');
}
});
document.querySelector('.inp-text').addEventListener('keyup', function(e){
obj.name = e.target.value;
}, false);
html:
<input class="inp-text" type="text">
<div class="text-box"></div>
以上只是
Vue的核心思想,通过对象底层属性的set和get进行数据拦截,vue的虚拟DOM又是怎么实现的呢?且看以下分解
(5)虚拟DOM树:
*****创建虚拟DOM的关键:var frag =
document.createDocumentFragment()
DocumentFragment(文档片段)
可以看做是
节点容器
,它可以包含多个子节点,可以把它插入到DOM中,只有它的子节点会插入目标节点,所以可以把它看做是一组节点容器。使用DocumentFragment处理节点
速度和性能优于直接操作DOM
。Vue进行编译的时候就是将
挂载目标的所有子节点劫持到DocumentFragment
中,进过处理后再将DocumentFragment
整体返回到挂载目标
*****view层的{
{msg}}和 v-model的HTML如下:
<div id="container">
{
{ msg }}
<input class="inp-text" type="text" v-model="inpText">
<div class="text-box">
<p class="show-text">{
{ msg }}</p>
</div>
</div>
*****view层的{
{msg}}和 v-model的编译规则如下:
var container = document.getElementById('container');
//这里我们把vue实例中的data提取出来,更加直观
var data = {
msg: 'Hello world!',
inpText: 'Input text'
};
var fragment = virtualDom(container, data);
container.appendChild(fragment);
//虚拟dom创建方法,将目标盒子内所有子节点添加到其内部,注意这里只有子节点
function virtualDom(node, data){
let frag = document.createDocumentFragment();
let child;
// 遍历dom节点
while(child = node.firstChild){
compile(child, data);
frag.appendChild(child);
}
return frag;
}
//编译规则,子节点通过compile进行编译,a:如果节点为元素,其nodeType = 1;b:如果节点为文本,其nodeType = 3
function compile(node, data){
let reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){ // 标签
let attr = node.attributes;
for(let i = 0, len = attr.length; i < len; i++){
// console.log(attr[i].nodeName, attr[i].nodeValue);
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.value = data[name]; //给node节点赋值data
}
}
if(node.hasChildNodes()){
node.childNodes.forEach((item) => {
compile(item, data); // 递归,如果第二步子节点仍有子节点,通过hasChildNodes()来确认,如果有递归调用Compile方法
});
}
}
if(node.nodeType === 3){ // 文本节点
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
node.nodeValue = data[name];
}
}
}
(6)响应式原理:
第一步:核心思想:Object.defineProperty(obj,key,{set,get})----定义访问器属性
function defineReact(obj, key, value){
Object.defineProperty(obj, key, {
set: function(newValue){
console.log(`触发setter`);
value = newValue;
console.log(value);
},
get: function(){
console.log(`触发getter`);
return value;
}
});
}
第二步:这里只是针对data数据的属性的响应式定义(从数据出发去理解原理,数据驱动),但是如何去实现
vue实例vm绑定data每个属性,通过以下方法:
function observe(obj, vm){
Object.keys(obj).forEach((key) => {
defineReact(vm, key, obj[key]); //定义访问器属性
})
}
第三步:vue的构造函数:到这里就实现了Vue实例绑定data属性
function Vue(options){
this.data = options.data;
let id = options.el;
observe(this.data, this); // 将每个data属相绑定到Vue的实例上this
}
第四步:如何去实现Vue,实例化Vue:
var vm = new Vue({
el: 'container',
data: {
msg: 'Hello world!',
inpText: 'Input text'
}
});
console.log(vm.msg); // Hello world!
console.log(vm.inpText); // Input text
第五步:要实现第四步的效果,必要前提是在Vue内部初始化虚拟Dom:
function Vue(options){
this.data = options.data;
let id = options.el;
observe(this.data, this); // 将每个data属相绑定到Vue的实例上this
//------------------------添加以下代码
let container = document.getElementById(id);
let fragment = virtualDom(container, this); // 这里通过vm对象初始化
container.appendChild(fragment);
}
第六步:至此我们已经实现了
dom的初始化,
下一步我们在v-model元素添加监听事件,这样就可以通过view层的操作来修改vm对应的属性值;在compile编译的时候,可以准确的找到v-model属性元素,因此我们把监听事件添加到compile内部
function compile(node, data){
let reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){ // 标签
let attr = node.attributes;
for(let i = 0, len = attr.length; i < len; i++){
// console.log(attr[i].nodeName, attr[i].nodeValue);
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.value = data[name];
// ------------------------添加监听事件
node.addEventListener('keyup', function(e){
data[name] = e.target.value;
}, false);
// -----------------------------------
}
}
if(node.hasChildNodes()){
node.childNodes.forEach((item) => {
compile(item, data);
});
}
}
if(node.nodeType === 3){ // 文本节点
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
node.nodeValue = data[name];
}
}
}
第七步:
这一步我们操作页面输入框,可以看到以下效果,证明监听事件添加有效。
到这里我们已经实现了MVVM,
即 Model -> vm -> View || View -> vm -> Model 中间桥梁就是vm实例对象;
(7)进一步完善响应式数据绑定,引入观察者模式原理:
*****订阅者:三个订阅者都有update方法
var subscribe_1 = {
update: function(){
console.log('This is subscribe_1');
}
};
var subscribe_2 = {
update: function(){
console.log('This is subscribe_2');
}
};
var subscribe_3 = {
update: function(){
console.log('This is subscribe_3');
}
};
*****发布者:发布者通过notify方法对订阅者广播,订阅者通过update来接受信息
function Publisher(){
this.subs = [subscribe_1, subscribe_2, subscribe_3]; // 添加订阅者
}
Publisher.prototype = {
constructor: Publisher,
notify: function(){
this.subs.forEach(function(sub){
sub.update();
})
}
};
*****实例化publisher:
var publisher = new Publisher();
publisher.notify();
*****创建中间件来处理发布者-订阅者模式:
var publisher = new Publisher();
var middleware = {
publish: function(){
publisher.notify();
}
};
middleware.publish();
(8)观察者模式嵌入:
我们已经实现了,接下来要实现:更新视图,同事把订阅-发布者模式嵌入
1) 修改 v-model 属性元素 -> 触发修改vm的属性值 -> 触发set
2) 发布者添加订阅 -> notify分发订阅 -> 订阅者update数据
*****发布者:
function Publisher(){
this.subs = []; // 订阅者容器
}
Publisher.prototype = {
constructor: Publisher,
add: function(sub){
this.subs.push(sub); // 添加订阅者
},
notify: function(){
this.subs.forEach(function(sub){
sub.update(); // 发布订阅
});
}
};
*****订阅者:
考虑到要把订阅者绑定data的每个属性,来观察属性的变化,参数:name参数可以有compile中获取的name传参;
由于传入的node节点类型分为两种,可以分为两个订阅者来处理,同时可以对node节点类型进行判断,通过switch分别处理:
function Subscriber(node, vm, name){
this.node = node;
this.vm = vm;
this.name = name;
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name]; //赋值功能移到了订阅者这里
break;
case 3:
node.nodeValue = vm[name]; //赋值功能移到了订阅者这里
break;
default:
break;
}
}
};
*****我们要把订阅者添加到compile进行虚拟dom的初始化,替换掉原来的赋值:
function compile(node, data){
let reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){ // 标签
let attr = node.attributes;
for(let i = 0, len = attr.length; i < len; i++){
// console.log(attr[i].nodeName, attr[i].nodeValue);
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
// --------------------这里被替换掉
// node.value = data[name];
new Subscriber(node, data, name);
// ------------------------添加监听事件
node.addEventListener('keyup', function(e){
data[name] = e.target.value;
}, false);
}
}
if(node.hasChildNodes()){
node.childNodes.forEach((item) => {
compile(item, data);
});
}
}
if(node.nodeType === 3){ // 文本节点
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
// ---------------------这里被替换掉
// node.nodeValue = data[name];
new Subscriber(node, data, name);
}
}
}
*****既然是对虚拟dom编译的初始化,
Subscriber也要初始化,即Subscriber.update,因此要对Subscriber作进一步的处理:
function Subscriber(node, vm, name){
this.node = node;
this.vm = vm;
this.name = name;
this.update();
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name];
break;
case 3:
node.nodeValue = vm[name];
break;
default:
break;
}
}
};
*****发布者添加到 defineRect函数,来观察数据的变化:
function defineReact(data, key, value){
let publisher = new Publisher();
Object.defineProperty(data, key, {
set: function(newValue){
console.log(`触发setter`);
value = newValue;
console.log(value);
publisher.notify(); // 发布订阅
},
get: function(){
console.log(`触发getter`);
if(Publisher.global){ //这里为什么来添加判断条件,主要是让publisher.add只执行一次,初始化虚拟dom编译的时候来执行
publisher.add(Publisher.global); // 添加订阅者
}
return value;
}
});
}
*****这一步将订阅者添加到发布者容器内,
对订阅者改造:
function Subscriber(node, vm, name){
Publisher.global = this;
this.node = node;
this.vm = vm;
this.name = name;
this.update();
Publisher.global = null;
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name];
break;
case 3:
node.nodeValue = vm[name];
break;
default:
break;
}
}
};
2.Vue的状态管理Vuex:
(1)vuex是一个专门为vue.js设计的状态管理模式,并且也可以使用devtools进行调试,可以多个组件共享状态;
简单来说,就是
共享的状态用state存放,用mutations来操作state,但是需要用store.commit来主动式的操作mutations;
(2)举例说明:
***** 在使用vues之前要先安装依赖(前提是已经
用Vue脚手架工具构建好项目)
cnpm install vuex –save
*****在入口文件main.js里需要
引入 vuex,注册vuex,实例化store,把store放在全局的实例化对象里
import Vue from 'vue'
import App from './App'
//1.引入vuex
import Vuex from 'vuex'
import Apple from './components/Apple'
import Banana from './components/Banana'
Vue.config.productionTip = false
//2.注册
Vue.use(Vuex);
//3.实例化store
let store=new Vuex.Store({
state:{
totalPrice:0
},
mutations:{
increment(state,price){
state.totalPrice+=price
},
decrement(state,price){
state.totalPrice-=price
}
},
actions:{
increase (context,price){
context.commit('increment',price)
},
decrease (context,price){
context.commit('decrement',price)
}
},
getters:{
discount(state){
return state.totalPrice *0.8;
}
}
})
new Vue({
el: '#app',
//4.把store放在全局的实例化对象里,可以在全局的所有地方用
store,
components: { App},
template: '<App/>'
})
*****参数介绍:
1) state
vuex使用单一状态树,那么就可以用一个对象包含全部的应用层级状态,所以state就作为数据源
2) mutations
更改Vuex的store中的状态的唯一方法就是提交mutations,Vuex中的mutations非常类似于事件:每个mutation都有一个字符串的 事件类型(type)和一个回调函数(handler),这个回调函数就是我们实际进状态更改的地方,并且它会接受state作为第一个参数。不能直接调用一个mutation handler,这个选项更像是事件注册:当触发一个type为 increment的mutation时,就调用handler。要唤醒一个mutation handler,需要调用store.commit方法触发相应的type,可以向store.commit传入额外的参数,这个参数就叫做mutation的载荷。在更多的情况下,载荷应该是一个对象,这样可以包含更多的字段;
mutations必须是同步函数,那么我们如何来异步的更新state呢?答案就是actions
3) actions
actions类似于mutations,不同的是:
actions提交的是mutations,而不是直接变更状态,这也就形成了actions--mutations--state的过程;
actions可以包含任意异步操作;
action 函数接受一个与store实例具有相同方法和属性的context对象,因此你可以调用context.commit提交一个mutation,或者通过context.state和context.getter来获取state和getter,但是如何触发呢?答案是:store.dispatch
4) getters
有时候我们需要从store中的state中派生出一些状态,getter会暴露为store.getter对象在组件中使用。
5) modules
除了上边用到的4个参数,store还有另一个参数:modules;
vuex允许把store进行一个功能拆分,分割成不同的模块(module),每个模块都拥有自己的store,mutation,action,getters
*****App.vue:
<template>
<div id="app">
<Apple></Apple>
<Banana></Banana>
<p> 总价{
{totalPrice}}</p>
<p> 折后价:{
{discountPrice}}</p>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld'
import Apple from './components/Apple'
import Banana from './components/Banana'
export default {
name: 'App',
components: {
HelloWorld,
Apple,
Banana
},
computed:{
totalPrice(){
//由于vuex的状态存储是响应式的,所以从store实例中读取状态的最简单方法就是使用计算属性来返回某个状态:
return this.$store.state.totalPrice
},
discountPrice(){
//getter 会暴露为 store.getters 对象
return this.$store.getters.discount
}
}
}
</script>
*****当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余,为了解决这个问题,我们可以使用mapState辅助函数帮助我们生成计算属性;
import { mapState } from 'vuex'
computed: {
...mapState(['totalPrice'])
...
}
*****Banana.vue:
<template>
<div>
<p>{
{msg}} 单价{
{price}}</p>
<button @click="addOne">add one</button>
<button @click="minusOne">minus one</button>
</div>
</template>
<script>
export default{
data(){
return{
msg:'banana',
price:15
}
},
methods:{
addOne(){ //addOne()函数调用store.commit方法触发type为"increment"的mutation
//直接commit一个mutation
this.$store.commit('increment',this.price)
},
minusOne(){ //minusOne()函数调用store.commit方法触发type为"decrement"的mutation
this.$store.commit('decrement',this.price)
}
}
}
</script>
*****可以在组件中使用this.$store.commit('xxxx')提交mutation,或者使用
mapMutations辅助函数将组件中的methods映射为 store.commit调用;
methods:{
addOne(){
this.increment(this.price)
},
minusOne(){
this.decrement(this.price)
},
...mapMutations(['increment', 'decrement'])
}
*****Apple.vue:
action相当于中介
<template>
<div>
<p> {
{msg}}单价:{
{price}} </p>
<button @click="addOne">add one</button>
<button @click="minusOne">minus one</button>
</div>
</template>
<script>
export default{
data(){
return{
msg:'apple',
price:5
}
},
methods:{
addOne(){ //addOne()函数里调用store.dispatch方法触发名为"increase"的action,对应的,在increase这个action里再去调用context.commit方法触发type为"increment"的mutation
//dispatch一个action,以action作为一个中介再去commit一个mutation
this.$store.dispatch('increase',this.price)
},
minusOne(){
this.$store.dispatch('decrease',this.price)
}
}
}
</script>
*****mutation和actions的区别与联系:
1) action只能调用mutation不能直接更改state,执行action来分发(dispatch)事件通知store去改变
2) action里可以进行一些异步的操作,再去触发mutation
3) mutation里必须是同步的触发操作state