Vue框架入门与实践之一

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

猜你喜欢

转载自blog.csdn.net/woshiyuyanjia/article/details/134871894
今日推荐