Vue 基本使用

特点

1.基于MVVM模式(Model-View-ViewModel),中间通过viewmodel层实现了双向绑定,其作为中间层会去监控后端数据的变化,并根据数据的变化去更新对应的前端展示内容,因此使用该框架就只需要操作数据而不用再通过操作DOM来手动更新视图。(JQuery框架由于需要频繁操作DOM,所以会降低性能)
2.模块化开发和虚拟DOM(减少真实DOM操作,在内存中模拟DOM操作,提升前端渲染效率)

理解Vue当中的双向绑定

通过下面的例子来了解为何说Vue实现了双向绑定,因此只需操作数据而无需关心其他:

<body>
    <div id="app">
        <ul v-for="n in name.length"><input type="text" v-model="name[n-1]">你是:{{name[n-1]}}
        </ul>
    </div>
</body>
<script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            name: [111, 222, 333]
        }
    });
</script>

在上面的代码中使用for循环依次将data里的name打印出来,并且当修改表单中的值时,对应显示的数据以及name里的数据也会同时更改,可以看出页面会动态监听name数据的变化,同时当表单值发生变化时也会动态修改name里的值。
再比如依次执行下面的语句:

app.name.push(444)
app.name.push(555)
app.name.pop()

可以看出是对name数组进行数据插入和弹出操作,期间也可以看见页面显示的数据在不断地修改

MVVM

Model:模型层,表示JavaScript对象
View:视图层,表示DOM
ViewModel:连接视图和模型的中间件,Vue主要就是这层的实现者,其核心就是实现了DOM监听和数据绑定(Ajax请求与后端进行数据交互,并根据数据变化更新前端视图展示;同时也监听前端视图的变化,并通知后端数据的改变)。

配置环境

非常好用的一个前端框架,配置方法和jquery之类的相似,都是引用vue的源文件就行,地址:
https://www.bootcdn.cn/vue/
一般选择标准版:https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.js
将js代码导入即可使用,比如下面代码:

<script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.js"></script>

使用步骤

(1)导入vue.js
(2)导入自己写的js文件
(3)实例化一个Vue对象,设置id键为el(element),数据都以对象形式保存在data键里,函数则保存在methods键里
(4)一般调用时通过id绑定,data里的数据通过{{ 属性 }}来读取
举例:

<body>
    <div id="app">
        {{name}}
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            name: 'aaa'
        }
    });
</script>

可以设置属性data并将对象存放到该值里,也可以将data设置为一个函数,举例:

<body>
    <div id="app">
        {{name}}
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data() {
            // 设置为方法,效果和前面相同
            return {
                name: 'aaa'
            }
        }
    });
</script>

Vue对象常用属性

官方api:https://cn.vuejs.org/v2/api/

el

element,代表元素,实例化vue类的id

data

存放数据

methods

存放函数,举例:

<body>
    <div id="app">
        <button v-on:click="abc(1)">click</button> {{cde()}}
        <!-- 直接调用方法要记得后面加括号 -->
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
    var app = new Vue({
        el: '#app',
        data: {},
        methods: {
            abc: function(x) {
                alert(x)
            },
            cde: function() {
                return "xxx";
            }
        }
    });
</script>
computed

监测并计算值,虽然methods也可以进行计算,但是computed会进行缓存,效率更高,并且监测的值是在computed当中定义的而非data里的属性值,当值没发生变化时则保存缓存的结果,否则重新计算,举例:

<body>
    <div id="app">
        <div>{{sum}}</div>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            age: [10, 20]
        },
        computed: {
            sum: function() {
                // 计算sum的值,computed的属性不能在data里定义过
                return this.age[0] + this.age[1];
            }
        }
    });
</script>
watch

也是监听监听值,但监听的是data里属性值的变化,功能上和computed有点像,举例:

<body>
    <div id="app">
        <button @click="add">add</button> {{ num }}:{{ count }}
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
    var app = new Vue({
        el: '#app',
        data: {
            count: 1,
            num: 0
        },
        methods: {
            add: function() {
                this.count ++;
            }
        }, 
        watch: {
            count: function (new_val, old_val) {
                // 监听data里的count属性,当值发生变化时,会将新的值和原来的值传入,并执行该方法
                console.log(new_val, old_val);
                this.num++;
            }
        }
    });
</script>

其还可以通过deep属性监听对象的变化,举例:

watch: {
  o: {
    handler: function(new_val, old_val) {
      ...
    },
    deep: true
  }
}

更多watch内容以及和computed区别参考:https://blog.csdn.net/weixin_43837268/article/details/92769669

mixins

可以引入对象里的属性,从而实现对象属性复用,比如定义一个对象存放很多data和methods,然后就可以在很多Vue对象里通过mixins属性引入复用属性(一般出现同名冲突时,mixins的一般会被覆盖;如果同名冲突的是钩子函数,那么就是先执行mixins中的钩子函数,然后再执行本身的钩子函数),举例:

<body>
    <div id="app">
        <span @click="test">{{name}}</span>
        <!-- 引入复用的base对象,显示aaa -->
    </div>
    <div id="app1">
        <span @click="test">{{name}}</span>
        <!-- 复用的数据被覆盖,因此显示bbb -->
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var base = {
        // 定义一个对象里面存放了Vue属性,包括可复用的数据和方法
        data: {
            name: 'aaa',
        },
        methods: {
            test: function() {
                this.name = 'ccc';
            }
        }
    }
    var app = new Vue({
        el: '#app',
        mixins: [base],
        // 通过mixins属性复用存放Vue属性的对象
    });
    var app1 = new Vue({
        el: '#app1',
        mixins: [base],
        data: {
            name: 'bbb'
                // 若属性出现重复,则会将mixins中的覆盖
        }
    });
</script>

全局引入:可以通过Vue.mixin({...})进行全局引入,此时所有基于该Vue对象创建的实例都将被加上该内容,举例:

Vue.mixin({
  mounted: function() {
    console.log(this.name);
    // 所有实例挂载完成后都会属性其name
  },
  data: () => ({
    a: 100
  })
});

生命周期

beforeCreate

在创建对象之前执行,可以做一些比如加载动画等

created

在创建对象之后、dom加载完成前执行,可以异步获取一些数据

beforeMount

在虚拟dom中执行编译模板中,dom还未生成

mounted

编译模板完成,此时页面内容将显示出来,此时dom生成完成

beforeUpdate

在组件更新事件前执行

updated

在组件更新事件后执行

beforeDestory

在销毁事件前执行

destoryed

在销毁事件后执行

多组件时生命周期顺序
同步父子组件
  • 加载顺序:
    父beforeCreate -> 父created -> 父beforeMount -> 子1beforeCreate -> 子1created -> 子1beforeMount -> 子2beforeCreate -> 子2created -> 子2beforeMount -> ... -> 子1mounted -> 子2mounted -> ... -> 父mounted
  • 更新顺序:
    父beforeUpdate -> 子1beforeUpdate -> 子2beforeUpdate -> 子2updated -> 子1updated -> ... -> 父updated
  • 销毁顺序:
    父beforeDestroy -> 子1beforeDestroy -> 子1destroyed -> 子2beforeDestroy -> 子2destroyed -> ... -> 父destroyed
同步同级组件
  • 多个同级组件加载顺序:
    同级1beforeCreate -> 同级1created -> 同级1beforeMount -> 同级2beforeCreate -> 同级2created -> 同级2beforeMount -> ... -> 同级1mounted -> 同级2mounted -> ...
  • 多个同级组件更新顺序:
    同级1beforeUpdate -> 同级2beforeUpdate -> 同级2updated -> 同级1updated -> ...
  • 多个同级组件销毁顺序:
    同级1beforeDestroy -> 同级1destroyed -> 同级2beforeDestroy -> 同级2destroyed -> ...
异步父子组件
  • 加载顺序:
    父beforeCreate -> 父created -> 父beforeMount -> 父mounted -> 父beforeUpdate -> 子1beforeCreate -> 子1created -> 子1beforeMount -> 子1mounted -> 父updated -> 父beforeUpdate -> 子2beforeCreate -> 子2created -> 子2beforeMount -> 子2mounted -> 父updated -> ...
  • 更新顺序:和同步一样
  • 销毁顺序:和同步一样
异步同级组件
  • 多个同级组件加载顺序:
    同级1beforeCreate -> 同级1created -> 同级1beforeMount -> 同级1mounted -> 同级2beforeCreate -> 同级2created -> 同级2beforeMount -> 同级2mounted -> ...
  • 多个同级组件更新顺序:和同步一样
  • 多个同级组件销毁顺序:和同步一样
代码示例
  • 父组件
<template>
  <div>
    <button @click="change">{{ label }}</button>
    <child1 ref="child1"></child1>
    <child2 ref="child2"></child2>
  </div>
</template>

<script>
// 同步
// import child1 from "./child1";
// import child2 from "./child2";
// 异步
const child1 = () => import("./child1");
const child2 = () => import("./child2");
export default {
  name: "parent",
  components: {
    child1,
    child2
  },
  data() {
    return { label: "parent" };
  },
  methods: {
    change() {
      this.$refs.child1.label += ".";
      this.$refs.child2.label += ".";
      this.label += ".";
    }
  },
  beforeCreate() {
    console.log("parent beforeCreate");
  },
  created() {
    console.log("parent created");
  },
  beforeMount() {
    console.log("parent beforeMount");
  },
  mounted() {
    console.log("parent mounted");
  },
  beforeUpdate() {
    console.log("parent beforeUpdate");
  },
  updated() {
    console.log("parent updated");
  },
  beforeDestroy() {
    console.log("parent beforeDestroy");
  },
  destroyed() {
    console.log("parent destroyed");
  }
};
</script>
  • 子组件1:
<template>
  <div>
    {{ label }}
  </div>
</template>

<script>
export default {
  name: "child1",
  data() {
    return { label: "child1" };
  },
  beforeCreate() {
    console.log("child1 Create");
  },
  created() {
    console.log("child1 created");
  },
  beforeMount() {
    console.log("child1 beforeMount");
  },
  mounted() {
    console.log("child1 mounted");
  },
  beforeUpdate() {
    console.log("child1 beforeUpdate");
  },
  updated() {
    console.log("child1 updated");
  },
  beforeDestroy() {
    console.log("child1 beforeDestroy");
  },
  destroyed() {
    console.log("child1 destroyed");
  }
};
</script>
  • 子组件2:
<template>
  <div>
    {{ label }}
  </div>
</template>

<script>
export default {
  name: "child2",
  data() {
    return { label: "child2" };
  },
  beforeCreate() {
    console.log("child2 Create");
  },
  created() {
    console.log("child2 created");
  },
  beforeMount() {
    console.log("child2 beforeMount");
  },
  mounted() {
    console.log("child2 mounted");
  },
  beforeUpdate() {
    console.log("child2 beforeUpdate");
  },
  updated() {
    console.log("child2 updated");
  },
  beforeDestroy() {
    console.log("child2 beforeDestroy");
  },
  destroyed() {
    console.log("child2 destroyed");
  }
};
</script>

参考:https://blog.csdn.net/weixin_39147099/article/details/82956439

this下对象

$refs

绑定DOM对象,代替了原生的documentByxxx的方法,举例:

<body>
    <div id="app">
        <button @click="test" ref="msg1">{{msg}}</button>
        <!-- 设置dom标志为msg1  -->
    </div>
</body>
<script src="https://cdn.bootcss.com/vue/2.5.17-beta.0/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            msg: "aaa"
        },
        methods: {
            test() {
                this.$refs.msg1.style.background = 'black';
                // 获取dom元素,并修改背景色
                console.log(this.$refs.msg1);
                // <button style="background: black;">aaa</button>
            }
        }
    });
</script>

指令

一般写在标签里,和属性的使用比较像,Vue里很多指令有自带功能,比如v-model能够获取当前表单的值,v-show表示值为空时不显示标签,举例:

<body>
    <div id="app">
        <input type="text" v-model="name">
        <span v-show="name">你是:{{name}}</span>
    </div>
    <!-- 此时表单的值和span标签内容双向绑定,而且一旦表单内容为空,span标签也为空(连"你是:"都不显示) -->
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            name: 'aaa'
        }
    });
</script>
v-model

绑定表单和变量的值,前面例子就是一直基本用法,其下还有一些常用属性,比如:lazy——一般情况下表单的值和变量值会实时同步更新,但是设置lazy后,只有当鼠标点击表单外部时,变量的值才会更新;trim——把头和尾的空格都删掉再存到变量里;number——把值变成num型存到变量
注:
v-model只在<input>/<textarea>/<select>三个标签里面使用
注:
设置数据值的时候要根据数据类型来进行设置,比如多选框存的是一堆数据,所以需要用数组,举例:

<body>
    <div id="app">
        男:<input type="radio" v-model="sex" value="男"> 女:
        <input type="radio" v-model="sex" value="女"> 吃:
        <input type="checkbox" v-model="hobby" value="eat"> 喝:
        <input type="checkbox" v-model="hobby" value="drink">
        <p>{{sex}}{{hobby}}</p>
    </div>
</body>

<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            "sex": "女",
            "hobby": []
            // 字符串存放单选框数据,数组存放多选框数据
        }
    });
</script>

注1:
vue监听数组问题:对于监听数组时,如果数组当中的某一个值被修改,或者长度被修改,比如对于下面的代码:

<body>
    <div id="app">
        <ul v-for="n in name.length"><input type="text" v-model="name[n-1]">你是:{{name[n-1]}}
        </ul>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            name: [111, 222, 333]
        }
    });
</script>

不论输入下面任何一个命令,vue都不会监听到值的修改:

app.name[0] = "xxx"
// 修改数组的第一个值
app.name.length =2
// 修改数组长度,只取前两个

原因就是因为受到js本身的限制,vue只能监听数组的变化而无法监听到数组内部的变化,因此只有部分的方法操作能够被vue监听到,而支持监听的方法官方也进行了说明:链接(支持:push()/pop()/shift()/unshift()/splice()/sort()/reverse()),为了解决上述问题,这里可以使用splice()方法来修改数组内容或者改变数组长度,举例:

app.name.splice(0, 1, "aaa")
// 将数组第一个值改为aaa
app.name.splice(2, 1)
// 删除数组的第三个值

因为splice(0,0)将不会修改数组的内容,所以当数组内容发生改变,而我们又仅仅想更新数据的显示,那么可以通过执行一次splice(0,0)来实现,或者使用官方的强制更新——$forceUpdate()方法来实现
更多splice使用参考:
注2:
vue监听对象问题:首先,vue不允许动态添加根级别的属性(本身在没定义根级别的属性时调用该属性就会报错),并且也无法监听对象下属性的添加和删除,但是可以通过内置的$set/$delete方法实现,举例:

<body>
  <div id="app">
    <button @click="handle">add age attr</button>
    people info:{{ people }}
  </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
  var app = new Vue({
    el: "#app",
    data: {
      people: {
        name: "aaa"
      }
    },
    methods: {
      handle() {
        // this.people.age = 100
        // vue无法检测对象的修改和删除
        this.$set(this.people, "age", 100);
        // 可以通过$set方法实现响应监听
      }
    }
  });
</script>
v-for

for循环,举例:

<body>
    <div id="app">
        <ul v-for="n in nameList">你是:{{n.name}}<br>年龄:{{n.age}}</ul>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            nameList: [{
                name: 'aaa',
                age: 20
            }, {
                name: 'bbb',
                age: 30
            }, {
                name: 'aaa',
                age: 25
            }]
        }
    });
</script>

注:
v-for循环时默认还会传第二个参数进来,为索引下标,举例;

<body>
    <div id="app">
        <ul v-for="(n, index) in nameList">编号:{{index}} <br> 你是:{{n.name}} <br> 年龄:{{n.age}}</ul>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            nameList: [{
                name: 'aaa',
                age: 20
            }, {
                name: 'bbb',
                age: 30
            }, {
                name: 'aaa',
                age: 25
            }]
        }
    });
</script>

注2:
在组件化开发当中,使用v-for指令应该设置一个key属性,并且该属性不会重复,因此一般选择第二个参数索引index作为key,比如把上面v-for那行修改如下:

<ul v-for="(n, index) in nameList" :key="index">编号:{{index}} <br> 你是:{{n.name}} <br> 年龄:{{n.age}}</ul>
<!-- 获取参数n和index时也应该用括号包起来-->
v-if/v-else-if/v-else

判断语句,当条件正确或者值不为空的时候显示内容,使用举例:

<body>
    <div id="app">
        <div v-for="n in name">
            <span v-if="n == 'bbb'">b:{{n}}</span>
            <span v-else-if="n == 'ccc'">c:{{n}}</span>
            <span v-else>can't display</span>
        </div>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            url: "http://www.baidu.com",
            name: ["aaa", "bbb", "ccc"]
        }
    });
</script>
v-show

当值为非空时或者条件为真展示内容,和v-if很相似,但是还是有一点区别:当条件为false或者值为空时,v-if的所在的标签将会消失,而v-show仅仅是将内容通过display=none进行隐藏,比如下面的例子,你可以将v-if改成v-show对比观察代码变化的不同:

<body>
    <div id="app">
            <input type="text" v-if="name" v-model="name">{{name}}
            <!-- 尝试将这里的v-if改成v-show -->
        </div>
    </div>
</body>
<script src="./vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            name: "aaa"
        }
    });
</script>
v-bind

动态绑定标签属性值,举例:

<body>
    <div id="app">
        <a v-bind:href=url>click</a>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            url: "http://www.baidu.com"
        }
    });
</script>

此时在命令行里就可以通过下面命令来修改链接:

app.url = ""

注:
一般情况下v-bind可以直接用引号替代,比如上面的就可以改成:

<a :href=url>

注2:
vue里还提供了简易的对象传值方式,举例:

<body>
    <div id="app">
        <a :class="{active: judge, unactive: !judge}">click</a>
        <!-- 判断如果judge值为true,则类为active,否则类为unactive -->
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            judge: true,
        }
    });
</script>

注3:
可以通过v-bind传入一个对象,那么会以对象的key作为属性名,value作为对应的值进行批量绑定

<body>
  <div id="app">
    <a v-bind="options">click</a>
    <!-- v-bind传入一个对象可以批量绑定,其中对象的key作为属性名,value作为对应的值 -->
  </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  var app = new Vue({
    el: "#app",
    data: {
      // options是一个对象
      options: {
        href: "http://www.baidu.com",
        name: "aaa"
      }
    }
  });
</script>

注4:
在2.6版本新增了属性名的绑定(原来只能绑定属性值),通过[xxx]绑定,举例:

<body>
    <div id="app">
        事件名:<input type="text" v-model="event">
        <button @[event]=handle>{{event}}</button>
        <!-- @(v-on指令的简写,下一个指令就介绍这个)绑定事件名,执行对应事件 -->
    </div>
</body>
<script type="text/javascript" src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            event: "click",
        },
        methods: {
            handle(){
                alert("你触发了:" + this.event + "事件!")
            }
        },
    });
</script>
v-on

绑定事件,类似于原来的onclick,对应的事件写在vue对象的methods属性里,举例:

<body>
    <div id="app">
        <button v-on:click="abc(2)">click</button>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            url: "http://www.baidu.com"
        },
        methods: {
            abc: function(x) {
                alert(x)
            }
        }
    });
</script>

注:
一般情况下v-on:可以替换成@,所以上面的可以改成:

<button @click="abc(1)">

注2:
对于多事件绑定,可以通过传入对象实现,举例:

<body>
    <div id="app">
        <button v-on="{click:clicked, mouseenter:mouseentered}">click</button>
        <!-- 注意v-on后面的冒号要换成= -->
    </div>
</body>

<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {},
        methods: {
            clicked: function(e) {
                console.log(e);

            },
            mouseentered: function() {
                alert("mouseenter");
            }
        }
    });
</script>

其实原来仅根据函数绑定和通过传对象绑定事件是有区别的:
前者相当于:onclick="function xxx()",而后者相当于jquery的事件绑定语句:$(xxx).click(function(e){console.log(e);}),因此打印以后可以发现e是点击事件
简单总结就是:前面是绑定函数,即只是调用一个函数,可以进行传参;而后者是绑定事件,无法进行传参
注3:
v-on还绑定了一些事件方法,如阻止事件传播-stopPropagation(),则用.stop
,取消默认事件-preventDefault(),则用.prevent,如监听键盘的按下某个键,则用keydown.键,举例:

<body>
    <div id="app">
        <form @keydown.a='aaa' @submit.prevent='bbb'>
            <!-- 按下a键时执行aaa方法,提交时阻止事件传播 -->
            <input type="text">
            <input type="submit">
        </form>
    </div>
</body>

<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {},
        methods: {
            aaa: function(e) {
                console.log(e);
            },
            bbb: function(e) {
                // 使用了.prevent相当于在这里调用了:e.preventDefault();
                console.log(e);
            },
        }
    });
</script>

注4:
v-on基本支持所有原生事件,事件参考:https://developer.mozilla.org/zh-CN/docs/Web/Events
注5
默认事件会传递的参数可以通过$event接收,举例:

<body>
  <div id="app">
      <button v-on:click="abc($event, 2)">click</button>
  </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            url: "http://www.baidu.com"
        },
        methods: {
            abc: function(x, y) {
                console.log(x, y);
            }
        }
    });
</script>
v-html

将数据按html格式输出,举例:

<body>
    <div id="app">
        <span>{{raw}}</span>
        <!-- 可以看到这里输出的是原始字符串 -->
        <span v-html="raw"></span>
        <!-- 会发现输出的不会是原始字符串,而是编译后的代码 -->
    </div>
</body>

<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var app = new Vue({
        el: '#app',
        data: {
            raw: `<h1>这是一行html代码<h1>`
        }
    });
</script>
自定义指令

前面那些指令都是vue里自带的,但我们也可以自己来定义vue,通过:Vue.directive()来定义,举例:

<body>
    <div id="app">
        <span v-print="name" id="app">111</span>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    Vue.directive('print', function(el, data) {
        // 这里定义了一个v-print
        // 默认自定义指令里接收第一个参数是组件本身所以这里el代表<span>
        // data才是接收的参数,但接收后的data是个对象,真正接收到的值在其属性value里
        el.style.background = '#000000';
        console.log(data.value);
        // 显示传入的name的值,即:aaa
    })
    new Vue({
        el: '#app',
        data: {
            name: 'aaa'
        }
    });
</script>
自定义指令传入多属性

上面的自定义指令只传入一个值,如果想要再传入几个属性,可以在指令后通过.attr1.attr2...实现,此时这些属性就会存入到接收数据对象的modifiers属性里,值是true,举例:

<body>
    <div id="app">
        <span v-print.a.b.c="name" id="app">111</span>
        <!-- 传入a、b、c三个属性 -->
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    Vue.directive('print', function(el, data) {
        el.style.background = '#000000';
        console.log(data.modifiers);
        // 可以看到结果为:{a: true, b: true, c: true}
    })
    new Vue({
        el: '#app',
        data: {
            name: 'aaa'
        }
    });
</script>
自定义指令传入参数

如果想要给指令传入参数,可以通过下面方式实现:

<body>
    <div id="app">
        <span v-print:s.a.b.c="name" id="app">111</span>
        <!-- 传入参数s,然后再传a、b、c三个属性 -->
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    Vue.directive('print', function(el, data) {
        el.style.background = '#000000';
        console.log(data.arg);
        // 可以看到结果为:s
    })
    new Vue({
        el: '#app',
        data: {
            name: 'aaa'
        }
    });
</script>
局部自定义指令

往Vue对象的directives属性里添加即可,举例:

<body>
    <div id="app">
        <span v-print:s.a.b.c="name" id="app">111</span>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    new Vue({
        el: '#app',
        data: {
            name: 'aaa'
        },
        directives:{
            // 局部自定义指令
            print(el, data){
                el.style.background = '#000000';
                console.log(data.arg);
            }
        }
    });
</script>

组件

即各种标签,组件是Vue的一大特性,可以通过定义组件来定义出具有自定义功能的标签

全局定义

可以自己定义标签,通过component来定义,举例:

<script>
    Vue.component('alert', {
        // 定义一个标签类型<alert>,是一个button,点击弹窗aaa
        template: '<button @click=abc>click</button>',
        methods: {
            abc: function() {
                alert('aaa')
            }
        }
    });
</script>

要调用时直接new就行了,举例:

<body>
    <div id="app">
        <alert></alert>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
    Vue.component('alert', {
        // 定义一个标签类型<alert>,是一个button,点击弹窗aaa
        template: '<button @click=abc>click</button>',
        methods: {
            abc: function() {
                alert('aaa')
            }
        }
    });
    new Vue({
        el: '#app'
        // 绑定id=app的才能用这个组件
    });
</script>

注:
组件当中data属性设置问题:在组件当中如果直接往data属性里面传入对象将会报错,比如下面代码:

<body>
    <div id="app">
        <alert></alert>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
    Vue.component('alert', {
        data: {
            count:1
        },
        // 在组件当中直接给data传入一个对象,结果会报错
        template: '<button @click=abc>click</button>',
        methods: {
            abc: function() {
                alert(this.count)
            }
        }
    });
    new Vue({
        el: '#app'
    });
</script>

结果会报这样的错:[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.,原因是在源码当中其设置了如果是一个Vue对象,那么data属性可以传入一个对象,而如果是一个组件,那么data属性必须为一个函数方法,否则所有的组件共享同样的对象,这和原来设计组件的初衷不符,因此将上面的data值改成如下函数即可:

<body>
    <div id="app">
        <alert></alert>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
    Vue.component('alert', {
        data: function(){
            return {count:1}
        },
        // data属性改成一个函数,返回需要的值
        template: '<button @click=abc>click</button>',
        methods: {
            abc: function() {
                alert(this.count)
            }
        }
    });
    new Vue({
        el: '#app'
    });
</script>

注2:
组件标签模板也可以在html中定义,然后通过选择器将其传给template属性即可,举例:

<body>
    <div id="app">
        <alert></alert>
    </div>
    <template id="new-component">
        <button @click="abc">aaa</button>
    </template>
    <!-- 在html中定义一个组件模板 -->
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
    template = {
        // 将模板封装起来
        alert: {
            template: '#new-component',
            // 传入选择器即可获取html中定义的模板
            methods: {
                abc: function() {
                    alert('aaa')
                }
            }
        }
    };
    new Vue({
        el: '#app',
        components: template
    });
</script>
局部定义

前面那种是全局定义,所以后面new的全都包含这个组件,如果要定义局部变量就在new里面定义,举例:

<script>
    new Vue({
        el: '#app',
        components: {
            alert: {
                template: '<button @click=abc>click</button>',
                methods: {
                    abc: function() {
                        alert('aaa')
                    }
                }
            }
        }
    });
</script>>
内置组件
<component>

能够实现组件的动态绑定,通过该组件的is属性,可以动态绑定某一组件,因此通过该插件,我们能够实现简单的分页功能,举例:

<body>
  <div id="app">
    <button @click=changePage('component1')>显示页面1</button>
    <button @click=changePage('component2')>显示页面2</button>
      <component :is=page></component>
      <!-- 通过component标签的is属性绑定组件 -->
  </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script>
  Vue.component('component1', {template: '<h1>我是页面1</h1>'});
  Vue.component('component2', {template: '<h1>我是页面2</h1>'});
  new Vue({
      el: '#app',
      data:{
        page: "component1"
      },
      methods: {
        changePage(page){
          this.page = page
        }
      },
  })
</script>
<slot>

插槽,通过该组件能使得定义的组件可以插入我们的自定义数据,举例:

<body>
    <div id="app">
        <page>
            <div slot="header">XXX</div>
            <div slot="main">YYY</div>
            <!-- 调用的这个组件结果显示为:XXX\nYYY\nmore...
                (header插槽内容被XXX覆盖,main插槽插入YYY,footer插槽保持默认) -->
        </page>
    </div>

    <template id="page-tpl">
        <div style="width:100px; border: 1px solid black;">
            <div>
                <slot name="header">aaa</slot>
                <!-- 定义了3个插槽,分别用来存放自定义数据 -->
            </div>
            <hr>
            <div>
                <slot name="main"></slot>
            </div>
            <hr>
            <div>
                <slot name="footer">more...</slot>
            </div>
        </div>
    </template>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    Vue.component("page", {
        template: "#page-tpl"
    })
    var app = new Vue({
        el: '#app',
    });
</script>
<keep-alive>

缓存路由组件对象,这个在模块化开发时,结合vue-router,可以实现跳转路由时缓存<keep-alive>包裹的组件的当前状态,而不会销毁其原来的状态,从而在下次访问时还能继续上次的操作,详细参考:
https://www.cnblogs.com/goloving/p/9256212.html
https://www.jianshu.com/p/4b55d312d297

<transition>

实现动画过渡效果的组件

更多内置组件参考

文档:https://cn.vuejs.org/v2/api/#内置的组件
https://blog.csdn.net/tangxiujiang/article/details/80144195

组件通信
如何区分父子组件

假如在A组件中调用了B组件,那么A组件就是父组件,B组件就是子组件,因此vue中的app也就是整体的父组件

父组件向子组件通信

自定义的组件如果要获取基于其生成的标签上的传值,可以通过在标签中设置属性和对应的值,其中如果设置方式为属性="值",那么值为字符串,如果设置方式为v-bind:属性="值",那么值为data或methods里对应的属性的值,然后在定义时加入props属性,然后将绑定的变量名放在里面即可,举例:

<body>
    <div id="app">
        <alert aaa="111" v-bind:bbb="ccc" v-for="item in items" v-bind:item="item"></alert>
        <!-- 设置aaa属性的值为111,设置bbb的值为data里的ccc的值,设置item为data里items循环的每个值,并将值传给子组件 -->
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    Vue.component('alert', {
        template: '<button @click=abc>{{item}}</button>',
        // 定义子组件alert,由于其调用了button,所以button相当于子组件
        props: ['aaa', 'bbb', 'item'],
        // 存放所有绑定或者定义的属性
        // 变量名格式为:['变量名1', '变量名2', ...]
        methods: {
            abc: function() {
                alert(this.aaa + this.bbb);
                // 这里获取aaa属性和data里的ccc的值
            }
        }
    })
    new Vue({
        el: '#app',
        data: {
            ccc: "222",
            items: ['x', 'y', 'z']
        }
    });
</script>

从上面代码可以看出对于v-for循环绑定的值应该循环的每一个值而不是一整个数组,因为最终你要使用的是数组里面的值

子组件向父组件通信

可以通过子组件用$emit('监听事件x', 传递值)方法传值,父组件通过@监听事件x来获取,举例:

<body>
    <div id="app">
        <alert></alert>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    Vue.component('alertBase', {
    // 定义子组件
        template: '<button @click="setxxx()">click</button>',
        methods: {
            setxxx: function() {
                this.$emit('xxx', {
                    // 给继承该组件的标签添加一个xxx事件,功能为传入一个对象{name:"bbb"}
                    name: "bbb"
                });
            }
        }
    });
    Vue.component('alert', {
    // 定义父组件
        template: '<div><alertBase @xxx="getxxx"></alertBase>{{name}}</div>',
        // 基于alertBase组件的组件
        // 里面监听xxx事件,当监听到时执行getxxx方法
        data: function() {
            // 组件初始化时name为aaa
            return {
                name: "aaa"
            }
        },
        methods: {
            getxxx: function(data) {
                // data是监听事件xxx获取的emit传来的值
                console.log(data);
                this.name = data.name;
                // 此时name被改为bbb
            }
        }
    });
    new Vue({
        el: '#app',
    })
</script>

注:
在上面我们监听事件时只绑定了方法而没有绑定参数,这样默认接收的是事件传递的参数,假如我们自己想要传递值,又希望能够接收事件传递的参数,可以通过$event传递事件参数,举例:

// 上面的例子中修改alert组件如下
Vue.component("alert", {
  template: '<div><alertBase @xxx="getxxx($event, 1)"></alertBase>{{name}}</div>',
  // 同时接收事件传递值和自定义值
  data: function () {
    return {
      name: "aaa"
    };
  },
  methods: {
    getxxx: function (data, data2) {
      // data是监听事件xxx获取的emit传来的值,data2是自定义的值,这里是1
      console.log(data, data2);
      this.name = data.name;
      // 此时name被改为bbb
    }
  }
});
不同组件间传递值

当两个组件之间的关系不为父子关系时,我们可以通过设置一个公共事件对象Event = Vue(),然后在该对象当中设置监听事件和值,然后在要接收的对象里设置mounted属性,在里面通过监听事件对象的对应事件来获取值,举例:

<body>
    <div id="app">
        <send></send>
        <get></get>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    var Event = new Vue();
    // 定义事件对象用于收发数据
    Vue.component('send', {
        // 定义组件用于发送数据
        template: '<button @click="setxxx()">click</button>',
        methods: {
            setxxx: function() {
                Event.$emit("xxx", {
                    name: "bbb"
                });
                // 设置一个xxx事件,当监听到这个事件时能够获取对象{name: "bbb"}
            }
        }
    });
    Vue.component('get', {
        // 定义一个组件用于接收数据
        template: '<div>{{name}}</div>',
        data: function() {
            return {
                name: "aaa"
            }
        },
        mounted: function() {
            // 通过mounted钩子函数创建方法
            var _this = this;
            Event.$on("xxx", function(data) {
                // 监听事件当中的xxx事件
                _this.name = data.name;
                // 此处this是Event对象,所以使用提前定义的_this
            })
        }
    });
    new Vue({
        el: '#app',
    })
</script>

注:
由于我们是通过第三方Vue进行监听,因此即使当前组件被销毁,第三方Vue的监听事件也并不会解绑,这样造成的结果就是即使绑定监听事件的组件没了,当相关事件产生了,还是会执行对应的事件行为。为了避免这个问题,我们应该在组件销毁前将该事件解绑,举例:

beforeDestroy() {
    Event.$off("xxx");
    // 销毁前解绑对应事件
  }
父组件调用子组件方法

子组件本身定义的方法,如果在父组件引入时想要调用,可以通过给子组件设置ref属性,在父组件当中通过this.$ref.xxx.方法名()调用即可

子组件调用父组件方法
  • 通过this.$parent.方法名()调用
  • 子组件发送事件,父组件监听并调用方法
  • 父组件将方法直接传入子组件

详细参考:https://www.cnblogs.com/jin-zhe/p/9523782.html

组件递归

组件可以通过调用自身实现递归操作,但要注意几点:

  • 被递归的组件需要设置name值,否则无法指定递归的组件对象
  • 一定要有递归结束的条件,否则会导致栈溢出

这里简单实现将对象的节点通过递归进行展示,举例:

<body>
  <div id="app">
    <Tree :treedata="treedata"></Tree>
  </div>
  <template id="tree">
    <div>
      <ul
        :treedata="treedata"
        v-for="(children, node, index) in treedata"
        :key="index"
      >
        {{node}}
        <Tree v-if="children instanceof Object" :treedata="children"></Tree>
        <!-- 这里进行了组件递归,当children不为对象时结束递归 -->
      </ul>
    </div>
  </template>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  Vue.component("Tree", {
    template: "#tree",
    props: ["treedata"]
  });
  var app = new Vue({
    el: "#app",
    data: {
      treedata: {
        x: {
          xx: {
            xxx: {
              xxxx: 1
            }
          }
        },
        y: {
          yy: 2
        },
        z: 3
      }
    }
  });
</script>

过滤器

通过Vue.filter('过滤器名', 过滤函数)来定义过滤器,然后通过语法{{ 传入值 | 过滤器名 }}调用过滤器,学过Python Web开发的会发现和jinjia2的过滤器写法很像,举例:

<body>
    <div id="app">
        {{ "a book" | bookname }}
        <!-- 使用bookname过滤器 -->
        {{ 1.213 | price(2) }}
        <!-- 使用price过滤器四舍五入精确到2位小数 -->
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    Vue.filter('bookname', function(name) {
        // 定义一个过滤器叫bookname,功能为给值加上书名号
        return `【${name}】`;
    })
    Vue.filter('price', function(price, dot) {
        // 定义一个过滤器叫price,功能为给值四舍五入到固定位
        return price.toFixed(dot);
    })
    new Vue({
        el: '#app'
    })
</script>

从上面的price过滤器可以看出其含有2个参数,其中默认|左边的为传入的第一个参数,而剩下的参数则添加在调用过滤器的地方

局部过滤器

直接在Vue对象的filters属性里面定义,举例:

<body>
    <div id="app">
        <span id="app">{{name | toUpper}}</span>
    </div>
</body>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
    new Vue({
        el: '#app',
        data: {
            name: 'aaa'
        },
        filters:{
            // 定义局部过滤器
            toUpper(data){
                return data.toUpperCase()
            }
        }
    });
</script>

异步通信

由于Vue自身不带有异步通信功能,而使用jQuery的ajax有点违背少操作不操作DOM的原则,因此推荐使用Axios组件,导入的CDN链接如下:

<script src="https://cdn.bootcss.com/axios/0.19.0/axios.js"></script>
简单示例
axios({
        method: 'get',
        url: 'http://jsonplaceholder.typicode.com/users',
        data: {}
    })
    .then(function(response) {
        console.log(response);
        // 返回一个对象,包括状态码、返回内容等,其中返回内容在data属性里
    })
    .catch(function(error) {
        console.log(error);
    });
简单示例2
axios({
        method: 'get',
        url: 'http://jsonplaceholder.typicode.com/users',
        data: {}
    })
    .then(response => console.log(response))
    // 使用箭头函数,该语法和前面等价,即获取结果存在response里,然后执行=>右边的方法
    .catch(function(error) {
        console.log(error);
    });

使用参考:
https://www.jianshu.com/p/7a9fbcbb1114
https://www.jianshu.com/p/13cf01cdb81f
axios和ajax、fetch区别:
https://www.jianshu.com/p/8bc48f8fde75


分割线


前面讲的都是Vue的基本使用语法和一些特性,并且是在html或js文件当中书写的代码示例。然而实际当中,真正的Vue开发往往是建立一个基于node.js的前端工程项目(即完全意义上的前后端分离项目),下面就来介绍一些关于开发Vue项目的知识点:

Vue项目开发

前端服务器:node.js(基于js的服务器)[官网]
项目模板生成:vue-cli脚手架(生成整个项目架构)[官网]
路由管理:vue-router(无刷新访问路由界面)[官网]
状态管理:vuex(管理全局数据)[官网]
界面样式:sass(css的预编译语言)[官网]
UI样式:ElementUI(提供样式组件,类似基于vue版本的bootstrap)[官网]
网站模板:vue-element-admin(提供一个网站模板)[官网]
模拟数据:mockjs(拦截ajax并生成假数据返回)[官网]

node.js

基于JS的服务器,具体内容不赘述,这里只介绍安装过程:
1.下载:http://nodejs.cn/download/
2.直接安装,然后配置下环境变量,在命令行依次输入下面命令测试是否安装成功:

node -v
# 测试node是否安装成功
npm -v
# 测试npm是否安装成功(安装node.js附带安装的包管理工具)

3.可以输入下面命令安装淘宝镜像加速从而提高下载包的速度(可选):

npm install cnpm -g
# -g代表全局安装

4.如果安装了cnpm加速,那么通过cnpm下载的命令就改为:

cnpm install 包名
# 例如:cnpm install jquery

vue-cli

官方提供的脚手架,用于快速生成Vue项目的模板,需要先安装node.js环境

特点

1.统一目录结构
2.本地调试
3.热部署(代码修改后无需刷新,页面自动改变)
4.单元测试
5.集成打包(兼容es5、es6等语法规范)

安装

在基于安装了node.js和npm的环境下,输入下面命令:

# npm install -g vue-cli
# 旧版(2.x)的安装命令,在新版本(3.x+)里已经改名为@vue/cli,安装命令如下:
npm install -g @vue/cli

安装完成后可以用vue -V来查看是否安装成功
注:
如果要全局装,那么后面加上-g参数,如果嫌下载慢,就改用cnpm

创建项目

通过下面命令创建:

# vue init webpack 项目名
# 旧版(2.x)的创建项目命令,一般会用webpack打包项目,如果不用则可以不加webpack
vue create 项目名
# 新版本(3.x+)的创建项目命令
# 创建当中会有很多选项,根据自己需求设置
# 注意运行命令的目录下不能有vue.js文件,否则会冲突

创建时的配置可以直接按默认创建,或者自己定制,这里给一个自己定制的方案:

? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, Vuex, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with dart-sass)
? Pick a linter / formatter config: Prettier
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? (y/N) y

# 从上往下依次是:
# 选择默认模板还是自己手动配置
# 选择需要的插件(手动配置才会有的选项,通过空格选择)
# 是否使用history模式
# 选择css预处理器(在插件中勾选了css预处理器才会有该选项)
# 选择linter配置
# 选择何时进行linter检查
# 配置文件分开保存还是一起保存
# 是否将这些配置保存成模板

创建完成后进入到项目目录下,输入下面命令安装依赖:

# npm install
# 如果在文件里添加了依赖,则执行这句,否则不需要

然后可以通过下面命令运行项目:

# npm run dev
# 旧版本(2.x)的运行命令
npm run serve
# 新版本(3.x+)的运行命令

参考:https://www.jianshu.com/p/fbcad30031c2

项目目录
-bulid  webpack配置文件
-config  webpack配置文件
  -index.js  指定后台的服务端口号以及静态资源的文件夹
-node_modules  项目依赖的js
-src  源码内容
  -main.js  项目入口js文件
-static  静态资源
.barbelrc  babel编译文件
-index.html    主页面
-package.json  webpack打包配置文件
package.json属性
  • name:项目名
  • version:项目版本
  • description:项目描述
  • author:作者
  • private:是否私有
  • scripts:定义一些npm命令
  • dependencies:生产环境依赖
  • devDependencies:开发环境依赖
  • engines:引擎
  • browserslist:浏览器兼容
组件编写

在脚手架搭建的项目下,默认每个组件文件(.vue文件)无外乎就三个编写内容:

  1. 组件模板,举例:
<template>
  ...
</template>
  1. 导出数据,举例:
<script>
  export default {
    name: '...',
    data () {
      return {...}
    }
  }
</script>
  1. 组件样式,举例:
<style scoped>
// 如果不加scoped则是全局样式,加上则只对当前组件有效
// 要注意的是在使用vue-router时,此处样式会对子路由的组件也生效
  ...
</style>

当组件编写完成后,注册在components属性里即可

项目打包和发布

1.构建打包项目:npm run build

  • 通过nginx部署:
  1. 将dist目录放在html目录下或者随便一个最好不含中文的路径下,然后在nginx的配置文件当中进行如下配置:
location / {
    root   dist目录的路径(xxx/xxx/dist);
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;  # 如果没有配置这句,那么当直接访问的路径不是/下的话将会是404页面
}
  • 通过vue server部署:
    2.安装servenpm install -g serve
    3.启动serveserve dist
    4.访问serve端口(默认是5000)即可
vue-cli官方文档

https://cli.vuejs.org/zh/guide/

vue-router

负责vue的路由管理,能够实现更新视图却不用刷新界面

安装
npm install vue-router
简单示例

1.在components文件夹下新建文件Login.vue,编写代码如下:

<template>
  <div>
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'login',
  data () {
    return {
      msg: 'this is login page'
    }
  }
}
</script>

2.在router文件夹下新建index.js文件,编写代码如下:

import Vue from 'vue'
import Router from 'vue-router'
// 导入vue-router
import Login from '../components/Login'
// 导入Login.vue
Vue.use(Router);
// 使用vue-router

export default new Router({
    routes: [
        // 存放着所有路由的数组
        {
            path: '/login',
            // 路由地址
            name: 'login',
            component: Login
                // 代表使用Login.vue组件
        }
    ]
})

3.修改main.js代码如下:

import Vue from 'vue';
import App from './App';
import VueRouter from 'vue-router'
// 导入vue-router
import router from './router'
// 使用router目录下的vue-router对象
Vue.use(VueRouter)
// 使用vue-router
new Vue({
    el: '#app',
    router,
    // 传入router目录下的vue-router对象,此时就可以使用this.$router对象
    render: h => h(App),
});

4.修改App.vue如下:

<template>
  <div id="app">
    <router-link to="/">首页</router-link>
    <!-- 设置路由的超链接,类似<a href="/">...</a> -->
    <router-link to="/login">登录页</router-link>
    <router-view></router-view>
    <!-- 显示对应路由视图的内容 -->
  </div>
</template>

<script>
export default {
  name: 'App',
}
</script>

5.在命令行输入命令:npm run dev运行项目
注:
默认vue-router运行时的路由模式是按照hash模式,即路径开头会有/#/标识符,代表访问当前页下的哪个路径,如果不希望有/#/标识符,可以在定义Router时添加一个mode: 'history'即可,举例:

export default new Router({
    mode: 'history',
    // 设置为history模式
    routes: [
    ...
    ]
})

其中history模式和hash模式区别参考:https://blog.csdn.net/lyn1772671980/article/details/80804419
history模式下服务器/后端对应配置参考:https://www.cnblogs.com/mica/p/10876822.html

注2:
定义路由时,其路径是支持字符串匹配的,举例:

{
    path: '/aaa/*',
    // 匹配aaa下所有路由,但优先级低
    name: 'hello',
    component: Hello
}

注3:
Vue.use()功能和原理参考:
https://www.cnblogs.com/fps2tao/p/10830804.html
https://blog.csdn.net/Fabulous1111/article/details/88696006

子路由

前面的示例当中都是一级路由,如果需要设置一个能够基于父路由模板的子路由,则可以在父级路由当中设置children属性,然后配置路由信息。例如将上面示例中/login页面放在/main页面下,则修改index.js如下:

import Vue from 'vue'
import Router from 'vue-router'
// 导入vue-router
import Login from '../components/Login'
// 导入Login.vue
import Main from '../components/Main'
// 导入Main.vue
Vue.use(Router);
// 使用vue-router

export default new Router({
    routes: [{
            path: '/main',
            name: 'main',
            component: Main,
            children: [
                // 设置子路由,子路由的内容在main的<router-view>标签里显示
                {
                    path: '/main/login',
                    // 登录路由地址
                    name: 'login',
                    component: Login
                }
            ]
        },
        // 下面这个虽然路径看起来也是子路由,但视图的展示并不基于父路由
        // {
        //     path: '/main/login',
        //     // 登录路由地址
        //     name: 'login',
        //     component: Login
        // }
    ]
})

然后在main.vue当中添加<router-view>标签存放子路由内容,举例:

<template>
  <div>
    <h1>{{ msg }}</h1>
    <router-view></router-view>
    <!-- 添加视图显示,用于存放子路由内容 -->
  </div>
</template>

<script>
export default {
  name: 'main',
  data () {
    return {
      msg: 'this is main page'
    }
  }
}
</script>

此时打开页面即可发现父路由和子路由的内容都一同显示

路由传参

常用的路由传参数方法有两种:
1.通过路由/:参数名传参(多个参数就用&隔开,例如:路由/:参数1&:参数2),然后接收方通过{{ $route.params.参数名 }}获取,举例:
路由定义:

{
    path: '/login/:id&:name',
    // 登录路由地址,并且传入id和name参数
    name: 'login',
    component: Login
}

访问路由:

<router-link to="/login/1&2">登录页</router-link>

接收方:

{{ $route.params.id }}
{{ $route.params.name}}

2.访问路由改为传入一个对象(需要v-bind绑定to属性),根据路由的name绑定对应路由,参数放在params属性当中,举例(路由定义和接收方与前面相同):
访问路由:

<router-link :to="{name:'login', params:{id:1, name:'aaa'}}">登录页</router-link>
<!-- to设置为绑定属性,并绑定name为login的路由,并传两个参数进去 -->
路由接收get参数

对于get请求传来的参数(如:http://xxx/?参数1=xxx&参数2=yyy&...),可以通过{{ $route.query.参数名 }}获取

路由传属性

前面介绍的路由传参是通过$route.params.参数调用获取内容,我们还可以通过props属性接收路由传递的参数,此时需要在路由配置当中添加属性props:true代表接收属性,然后在对应的组件文件当中添加要获取的属性名,传参方法依然是上面的方式即可,举例:
路由定义:

{
    path: '/login/:id&:name',
    // 登录路由地址,并且传入id参数
    props: true,
    // 允许传入属性
    name: 'login',
    component: Login
}

接收方:

<template>
  <div>
    <h1>{{ msg }}</h1>
    {{ id }}
    {{ name }}
    <!-- 直接使用props接收到的属性 -->
  </div>
</template>

<script>
export default {
  name: 'login',
  props: ["id", "name"],
  // 定义接收的属性
  data () {
    return {
      msg: 'this is login page'
    }
  }
}
</script>
组件重定向

组件相同,但路径不同可以通过重定向实现,举例:

{
    path: '/regist',
    redirect: '/login'
    // 重定向到/login
}
路由属性
$route

路由对象,代表当前路由,里面存放了一些路由数据信息,这里列举几个常用属性(可以在vue-dev调试工具里查看该对象下内置的属性方法等):

params  接收传递的参数
query  接收get请求传递的参数
meta  自定义一些标签属性,比如设置一个bool属性判断某个组件在当前路由下是否显示
path  当前路径
$router

路由器对象,这主要用于操作路由导航,这里列举几个常用方法:

back()  路由回退
push()  路由跳转,可以回退的
replace()  路由跳转,无法回退
go()  路由前进/回退多少步

详细可以参考文档:https://router.vuejs.org/zh/guide/essentials/navigation.html

路由钩子

vue-router提供了一系列钩子函数和属性使得可以在进入、离开路由等期间执行对应的操作,下面列举一些:
钩子函数:

  • beforeEach:进入所有路由前
  • afterEach:进入所有路由后
    使用举例:
router = new VueRouter({...})
router.beforeEach(function(to, from, next) {
// 传入三个对象参数:前往的路由,前一个路由,以及执行的功能函数(afterEach里没有next参数)
// 可以根据对象内的属性,比如params(参数)、path(路径)等判断接下来的操作
    next();
    // 进入下一个路由,也可以往方法里传参,比如:next('/')-跳转到/路径下
})

钩子属性:

  • beforeRouteEnter:进入路由前执行
  • beforeRouteLeave:离开路由前执行

使用举例:

<template>
  <div>
    <h1>{{ msg }}</h1>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'main',
  data () {
    return {
      msg: 'this is main page'
    }
  },
  beforeRouteEnter:(to, from, next) => {
      console.log("进入路由前...");
  }
}
</script>
路由缓存

两种方案:

  • 使用内置的<keep-alive>组件
  • 在对应的路由里设置meta属性:添加keepAlive:true,然后在组件当中进行逻辑判断,举例:
<keep-alive>
  <!-- keep-alive为true时 -->
  <router-view :key="key" v-if="$route.meta.keepAlive" />
</keep-alive>
  <!-- keep-alive为false时 -->
<router-view :key="key" v-if="!$route.meta.keepAlive" />
路由懒加载

对于整个vue工程,默认情况下会将所有模块打包成1个js文件,此时在访问界面时会一次性引入所有的组件模块,即一个特别大的js文件,这个时候可能就会十分消耗访问页面的时间,而且也没必要,毕竟我们一般访问不到所有的路由。于是我们可以通过路由懒加载功能,当第一次访问特定路由的时候才去引入对应的组件,从而减少不必要的开销,实现的方法也很简单,对于那些大的组件(特别是一级路由下的),可以将原来通过import关键字导入的方式改成通过import()函数按需加载,举例:

// import Login from '../view/Login'
// 原来导入Login模块的方式

const Login = () =>import('../view/Login')
// 改成通过import函数返回,此时只有在需要用到该组件的路由时才会执行这句导入Login模块

详细参考:
https://blog.csdn.net/qq_38614249/article/details/79468609
https://blog.csdn.net/weixin_42420559/article/details/88706631

异步请求

1.通过下面语句导入axios

import axios from 'axios'
Vue.prototype.axios = axios;

2.通过this.axios调用axios请求

异步请求封装

在模块化开发中,我们往往会将异步请求进行封装,而其中大部分的配置都是相似的,因此此时我们可以先通过create方法对请求进行封装后,再用封装后的axios进行请求,举例:

import axios from "axios";
import qs from "qs";

const config = {
  // 配置url前缀,如果传入的url不是http开头就会加上该前缀
  baseURL: "http://127.0.0.1:6666/",
  // timeout时间
  timeout: 5 * 1000,
  // CORS跨域是否需要携带资源凭证,配置了该项后端才能获取到cookie
  // 但要求后台配置Access-Control-Allow-Origin为当前源地址,且Access-Control-Allow-Credentials为true
  withCredentials: true,
  // 跨域请求
  crossDomain: true,
  // params参数处理
  paramsSerializer: params => qs.stringify(params, { indices: false }),
  // data请求数据处理
  transformRequest: [data => qs.stringify(data, { indices: false })],
  // 成功请求状态码判断([200, 400)都算成功)
  validateStatus: status => status >= 200 && status < 400,
  // 流数据传输
  responseType: "blob",
  // 自定义请求头
  headers: {},
  // 参数
  params: {},
  // 数据
  data: {}
  // ...
};

const _axios = axios.create(config);
// 基本配置

_axios.interceptors.request.use(
  // 请求拦截器
  config => {
    // 若token存在则添加token
    const token = window.localStorage.getItem("token");
    token && (config.headers.Authorization = token);
    return config;
  },
  error => {
    return Promise.error(error);
  }
);

_axios.interceptors.response.use(
  // 响应拦截器
  response => {
    // 成功请求直接返回
    return response;
  },
  error => {
    // 失败请求判断
    if (error.response) {
      switch (error.response.status) {
        // 401: 未登录
        case 401:
          alert("身份验证失败");
          break;
        // 403:token过期
        case 403:
          alert("登录过期");
          // 清除token
          break;
        // 404:请求不存在
        case 404:
          alert("网页不存在");
          break;
        // 其他错误
        default:
          alert(error.response.data.message);
      }
      return Promise.reject(error.response);
    } else {
      if (!window.navigator.onLine) {
        // 网络连接失败处理
        alert("网络连接失败");
      }
      return Promise.reject(error);
    }
  }
);

export function post(url, params = {}, data = {}) {
  return _axios({
    method: "post",
    url,
    params,
    data
  });
}

export function get(url, params = {}) {
  return _axios({
    method: "get",
    url,
    params
  });
}

export function put(url, params = {}, data = {}) {
  return _axios({
    method: "put",
    url,
    params,
    data
  });
}

export function _delete(url, params = {}) {
  // delete是js关键字,所以前面加了个_
  return _axios({
    method: "delete",
    url,
    params
  });
}

export default _axios;

更多参考:
https://www.bbsmax.com/A/mo5kZbnJwR/
https://www.kancloud.cn/yunye/axios/234845

vue-router官方文档

https://router.vuejs.org/zh/

vuex

负责Vue开发当中的状态管理模式,比如设置公共变量,保持多个组件之间使用一致的数据

安装
npm install vuex
使用步骤

1.安装vuex
2.import导入vuex,并通过Vue.use(vuex)载入vuex
3.创建一个vuex.Store对象,配置常用属性
4.添加到Vue对象当中
5.在组件当中通过import {mapState} from 'vuex'获取state(同理通过mapMutations获取mutations、通过mapActions获取actions、通过mapGetters获取getters

Vuex.Store属性
  • state:全局状态,存放各种属性
  • mutations:对属性的操作,这里面定义的函数会传入state属性,并且只有通过该属性里的方法操作属性才有状态改变记录
  • actions:也是对属性的操作(但不是直接操作,而是通过调用mutations里的函数),这里面定义的函数会传入context上下文参数,可以在上下文中通过commit('函数名')执行mutations里对应的函数,一般简单的操作放在mutations里,复杂放actions
  • getters:获取属性,这里面定义的函数会传入state属性,然后可以读取state里面的数据后根据需求返回内容,并且每当state里的数据被操作以后都会执行
  • modules:模块,可以将多个模块文件里配置上面的属性后,一起放在主vuex文件的modules属性里导入配置

注:
在vuex中,可以使用commit调用mutations里的方法,也可以使用this.$store.dispatch()调用actions里的方法,前者只能同步,而后者可以异步操作,可以参考:
https://www.jb51.net/article/147581.htm

辅助函数
  • mapState:返回Store对象里的state属性对象,传入字符串数组,字符串为需要获取的属性名
  • mapMutations:同理
  • mapActions:同理
  • mapGetters:同理
5个属性和辅助函数详细参考

https://blog.csdn.net/wh710107079/article/details/88181015

简单示例
  • store/index.js(配置vuex属性)
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

const store = new Vuex.Store({
    // 定义Store对象
    state: {
        count: 0,
        // 设置一个count属性,实现一个计算加减法工具
    },
    mutations: {
        add(state) {
            // mutations里传入state属性,并对其进行基本操作
            state.count++;
        },
        sub(state) {
            state.count--;
        }
    },
    actions: {
        add2(context) {
            // actions里传入上下文,通过commit方法传入要执行的函数
            context.commit('add');
            context.commit('add');
            // alert(context.state.count);
            // 上下文对象里存放着state,所以也可以直接获取state的数据
        }
    },
    getters: {
        getCount(state) {
            // getters里传入state属性,state里的值每操作一次,这里都会运行一次
            console.log(state.count);
            // state的值一被操作,这里就输出
            return `result:${state.count}`;
            // 返回数据
        }
    }
})

export default store;
  • main.js(配置vue)
...
import store from './store'

new Vue({
    el: '#app',
    ...
    store,
    // 传入store对象,此时就可以使用this.$store对象
    render: h => h(App),
});
  • xxx.vue(获取vuex属性)
<template>
  <div>
    <h1>{{ count }}</h1>
    <!-- 也可以使用this.$store.state.count替换 -->
    <button @click="add">add</button>
    <button @click="sub">sub</button>
    <button @click="add2">add2</button>
    <button @click="showGetCount">showGetCount</button>
  </div>
</template>

<script>
import {mapState, mapMutations, mapActions, mapGetters} from 'vuex'
// 导入vuex下几个属性对象
export default {
  name: 'App',
  computed: {
      ...mapState(["count"])
      // 获取state里的count属性,并将其结构
  },
  methods: {
    ...mapMutations(['add', 'sub']), 
    // 同理将对应的方法结构
    // 也可以改成:add: function(){this.$store.commit("add")},
    ...mapActions(['add2']),
    ...mapGetters(['getCount']),
    // getter的内容也可以放在computed里直接使用
    showGetCount: function(){
      // getters里的方法只能返回内容,因此通过this.$store.getters调用获取
      console.log(this.$store.getters.getCount);
    }
  }
}
</script>
vuex日志打印配置
import Vuex from "vuex";
import logger from "vuex/dist/logger";

Vue.use(Vuex);
const debug = process.env.NODE_ENV !== "production";

const store = new Vuex.Store({
  state,
  ...
  plugins: debug ? [logger()] : []
});

export default store;
vuex官方文档

https://vuex.vuejs.org/zh/guide/

页面刷新导致vuex状态消失解决
1.根组件监听页面刷新事件,将数据存储本地
export default {
  name: "app",
  created() {
    //在页面刷新时将vuex里的信息保存到localStorage里
    window.addEventListener("beforeunload", () => {
      localStorage.setItem("messageStore", JSON.stringify(this.$store.state));
    });

    //在页面加载时读取localStorage里的状态信息
    localStorage.getItem("messageStore") &&
      this.$store.replaceState(
        Object.assign(
          this.$store.state,
          JSON.parse(localStorage.getItem("messageStore"))
        )
      );
    localStorage.removeItem("messageStore");
  }
};
2.使用插件vuex-persistedstate

安装:npm install vuex-persistedstate --save
使用示例:

import createPersistedState from "vuex-persistedstate"
const store = new Vuex.Store({
 // ...
 plugins: [createPersistedState()]
})
3.重载页面时向后端进行相应数据请求

参考:
https://www.jb51.net/article/160918.htm
https://www.jianshu.com/p/c2078f6f63b3

ElementUI

提供了很多样式界面布局组件,功能和bootstrap类似,只不过bootstrap是基于jQuery,而ElementUI基于vue组件,在结合vue开发时可以作为替代bootstrap定义样式的方案

安装
npm i element-ui -S
ElementUI官方文档

https://element.eleme.cn/#/zh-CN/component/installation

Vue-element-admin

是基于Vue+ElementUI提供的网站模板

Vue-element-admin官方文档

https://panjiachen.github.io/vue-element-admin-site/zh/guide/

mockjs模拟数据

使用步骤:

  1. 安装mockjs:npm install mockjs
  2. 导入mockjs模块,并编写假数据接口
import Mock from 'mockjs'
Mock.mock("/a", {
  "user": "aaa"
});

export default Mock
  1. 在main.js中导入mock的假数据接口:
import mock from '@/mock/index'
  1. ajax访问对应的假数据接口(在network里可以发现没有发起网络请求,请求被拦截并返回了假数据)

参考:https://www.cnblogs.com/vickya/p/8568447.html

Vue加密参考

md5加密

可以使用blueimp-md5js-md5,都是专门提供md5加密的轻量级框架,安装参考:

npm install --save js-md5
npm install --save blueimp-md5

使用举例:

// ----------------------------------------
// vue项目引用举例
import md5 from "blueimp-md5";
import Vue from "vue";

Vue.prototype.$md5 = md5;

// ----------------------------------------
// 使用举例
let val = md5("123");
crypto

是一个提供了各种加密工具的库

Vue当中XSS防范参考

https://blog.csdn.net/qiumen/article/details/88119275

Vue面试题总结

https://www.jianshu.com/p/182a67f4b254

MVVM实现

原理:数据劫持+发布订阅

原型实现
// Vue对象初始化配置
function MyVue(option = {}) {
  // 将所有数据挂载到option中
  this.$option = option;
  let data = this._data = this.$option.data;
  // 观察对象
  observe(data);
  // 对所有数据绑定动态操作和访问功能
  for (let key in data) {
    Object.defineProperty(this, key, {
      configurable: true,
      enumerable: true,
      get() {
        return this._data[key];
      },
      set(val) {
        this._data[key] = val;
      }
    })
  }
  // 绑定computed属性
  initComputed.call(this);
  // 模板编译,对vue绑定区域进行数据编译
  new Compile(this.$option.el, this);
}

// computed属性解析实现
function initComputed() {
  let vm = this;
  let computed = this.$option.computed;
  Object.keys(computed || {}).forEach(key => {
    Object.defineProperty(vm, key, {
      get:typeof computed[key] === "function"? computed[key]: computed[key].get,
      set() {}
    })
  })
}

// 模板编译实现
function Compile(el, vm) {
  vm.$el = document.querySelector(el);
  let fragment = document.createDocumentFragment();
  // 获取所有vue绑定区域的子元素
  while (child = vm.$el.firstChild) {
    fragment.appendChild(child);
  }
  // 对所有子元素进行编译替换
  replace(fragment);
  // 编译替换实现
  function replace(fragment) {
    Array.from(fragment.childNodes).forEach(node => {
      // 获取子节点内容
      let text = node.textContent;
      // 匹配{{xxx}}格式内容
      let reg = /\{\{(.*)\}\}/;
      // 如果是文本元素
      if (node.nodeType === 3 && reg.test(text)) {
        let arr = RegExp.$1.split(".");
        let val = vm;
        // 遍历到指定数据节点,如:x.y,则遍历到y的值
        arr.forEach(k => (val = val[k]));
        // 添加监听对象,监听数据变化
        new Watcher(vm, RegExp.$1, newVal => {
          node.textContent = text.replace(reg, newVal);
        })
        // 替换节点文本
        node.textContent = text.replace(reg, val);
      }
      // 如果是标签元素
      if (node.nodeType === 1) {
        // 获取标签属性
        let nodeAttrs = node.attributes;
        Array.from(nodeAttrs).forEach(attr => {
          let name = attr.name;
          let exp = attr.value;
          // 解析执行,这里解析v-model
          if(name.indexOf("v-") === 0) {
            node.value = vm[exp];
          }
          // 添加监听者监听标签变化
          new Watcher(vm, exp, newVal => {
            node.value = newVal;
          });
          // 添加input监听事件
          node.addEventListener("input", function(e) {
            let newVal = e.target.value;
            vm[exp] = newVal;
          })
        })
      }
      // 替换成编译后的节点
      if (node.childNodes) {
        replace(node);
      }
    })
  }
  // 将vue绑定的区域替换成编译后的结果
  vm.$el.appendChild(fragment);
}

// 观察者对象
function Observe(data) {
  // 创建订阅对象绑定所有监听内容
  let dep = new Dep();
  for (let key in data) {
    let val = data[key];
    observe(val);
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        // 添加订阅
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set(newVal) {
        if (val === newVal) return;
        val = newVal;
        // 修改值时需要将修改的值也变为观察者对象
        observe(newVal);
        // 发布通知,更新所有订阅的监听者
        dep.notify();
      }
    })
  }
}

// 观察者方法,递归设置属性
function observe(data) {
  if (typeof data !== "object") return;
  return new Observe(data);
}

// 订阅者
function Dep() {
  this.subs = [];
}

// 添加订阅方法
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub);
}

// 通知所有订阅的监听者更新视图
Dep.prototype.notify = function() {
  this.subs.forEach(sub => sub.update());
}

// 观察者对象
function Watcher(vm, exp, fn) {
  // 绑定对应元素
  this.vm = vm;
  this.exp = exp;
  this.fn = fn;
  Dep.target = this;
  let val = vm;
  let arr = exp.split(".");
  arr.forEach(k => {
    val = val[k];
  });
  Dep.target = null;
}

// 观察者更新方法
Watcher.prototype.update = function() {
  let val = this.vm;
  let arr = this.exp.split(".");
  arr.forEach(k => {
    val = val[k];
  })
  this.fn(val);
}
面向对象实现
// Vue对象初始化配置
class MyVue {
  constructor(option = {}) {
    // 将所有数据挂载到option中
    this.$option = option;
    // 绑定data属性
    this.initData();
    // 绑定computed属性
    this.initComputed();
    // 模板编译,对vue绑定区域进行数据编译
    new Compile(this.$option.el, this);
  }
  // data属性解析实现
  initData() {
    let data = (this._data = this.$option.data);
    // 观察对象
    Observe.observe(data);
    // 对所有数据绑定动态操作和访问功能
    for (let key in data) this.defineDataObserve(key);
  }
  // computed属性解析实现
  initComputed() {
    let computed = this.$option.computed;
    Object.keys(computed || {}).forEach(key =>
      this.defineComputedObserve(computed, key)
    );
  }
  // 定义data对象属性
  defineDataObserve(key) {
    Object.defineProperty(this, key, {
      configurable: true,
      enumerable: true,
      get() {
        return this._data[key];
      },
      set(val) {
        this._data[key] = val;
      }
    });
  }
  // 定义computed对象属性
  defineComputedObserve(computed, key) {
    Object.defineProperty(this, key, {
      get:
        typeof computed[key] === "function" ? computed[key] : computed[key].get,
      set() {}
    });
  }
}

// 模板编译对象
class Compile {
  constructor(el, vm) {
    this.vm = vm;
    this.vm.$el = document.querySelector(el);
    // 获取所有vue绑定区域的子元素
    let fragment = this.getFragment();
    // 对所有子元素进行编译替换
    this.replace(this.vm, fragment);
    // 将vue绑定的区域替换成编译后的结果
    this.vm.$el.appendChild(fragment);
  }
  // 获取所有vue绑定区域的子元素实现
  getFragment() {
    let fragment = document.createDocumentFragment();
    let child = null;
    while ((child = this.vm.$el.firstChild)) {
      fragment.appendChild(child);
    }
    return fragment;
  }
  // 编译替换实现
  replace(vm, fragment) {
    Array.from(fragment.childNodes).forEach(node => {
      // 如果是文本元素
      if (node.nodeType === 3) this.compileText(vm, node);
      // 如果是标签元素
      if (node.nodeType === 1) this.compileLabel(vm, node);
      // 替换成编译后的节点
      if (node.childNodes) this.replace(vm, node);
    });
  }
  // 文本元素编译
  compileText(vm, node) {
    // 匹配{{xxx}}格式正则
    let reg = /\{\{(.*)\}\}/;
    if (!reg.test(node.textContent)) return;
    // 获取子节点内容
    let text = node.textContent;
    let arr = RegExp.$1.split(".");
    let val = vm;
    // 遍历到指定数据节点,如:x.y,则遍历到y的值
    arr.forEach(k => (val = val[k]));
    // 添加监听对象,监听数据变化
    new Watcher(vm, RegExp.$1, newVal => {
      node.textContent = text.replace(reg, newVal);
    });
    // 替换节点文本
    node.textContent = text.replace(reg, val);
  }
  // 标签元素编译
  compileLabel(vm, node) {
    // 获取标签属性
    let nodeAttrs = node.attributes;
    Array.from(nodeAttrs).forEach(attr => {
      let name = attr.name;
      let exp = attr.value;
      // 解析执行,这里解析v-model
      if (name.trim() === "v-model") {
        node.value = vm[exp];
      }
      // 添加监听者监听标签变化
      new Watcher(vm, exp, newVal => {
        node.value = newVal;
      });
      // 添加input监听事件
      node.addEventListener("input", function (e) {
        let newVal = e.target.value;
        vm[exp] = newVal;
      });
    });
  }
}

// 观察者对象
class Observe {
  constructor(data) {
    // 创建订阅对象绑定所有监听内容
    let dep = new Dep();
    for (let key in data) this.defineObserve(data, key, dep);
  }
  // 定义观察者属性
  defineObserve(data, key, dep) {
    let val = data[key];
    Observe.observe(val);
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        // 添加订阅
        Dep.target && dep.addSub(Dep.target);
        return val;
      },
      set(newVal) {
        if (val === newVal) return;
        val = newVal;
        // 修改值时需要将修改的值也变为观察者对象
        Observe.observe(newVal);
        // 发布通知,更新所有订阅的监听者
        dep.notify();
      }
    });
  }
  // 观察者方法,递归设置属性
  static observe(data) {
    if (typeof data !== "object") return;
    return new Observe(data);
  }
}

// 订阅者对象
class Dep {
  constructor() {
    this.subs = [];
  }
  // 添加订阅
  addSub(sub) {
    this.subs.push(sub);
  }
  // 通知所有订阅的监听者更新视图
  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

// 观察者对象
class Watcher {
  constructor(vm, exp, fn) {
    // 绑定对应元素
    this.vm = vm;
    this.exp = exp;
    this.fn = fn;
    this.bindDeco(this.bindWatcher)(this);
  }
  // 绑定方法
  bindWatcher() {
    let val = this.vm;
    let arr = this.exp.split(".");
    arr.forEach(k => {
      val = val[k];
    });
  }
  // 绑定装饰器
  bindDeco(fn) {
    let deco = (proto, ...args) => {
      Dep.target = this;
      fn.call(proto, ...args)
      Dep.target = null;
    }
    return deco;
  }
  // 观察者更新方法
  update() {
    let val = this.vm;
    let arr = this.exp.split(".");
    arr.forEach(k => {
      val = val[k];
    });
    this.fn(val);
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,290评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,399评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,021评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,034评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,412评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,651评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,902评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,605评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,339评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,586评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,076评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,400评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,060评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,851评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,685评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,595评论 2 270