vue基本应用
一、vue的生命周期与值处理
1、vue的生命周期
beforeCreat create
beforeMount mounted
beforeUpdata updated
beforeDestroy destroyed
2.插入值、表达式
<p>文本差值{{message}}</p>
<p>JS表达式{{flag?'yes':'no'}}(只能是表达式,不能是js语句)</p>
3.指令、动态属性
<p :id="dataVal">动态属性id</p>
data() {
return {
dataVal:`id-$Data.new()`
}
}
4、v-html:会有xss风险,会覆盖子组件
<p v-html="rawHTML">
<span>[注意] 使用 v-html 之后,会将覆盖子元素</span>
</p>
data() {
return{
rawHTML:'指令 - 原始 html <b>加粗</b> <i>斜体</i>',
}
}
二、computed和watch
1、computed基础应用
<div id="example">
<p>{{ message }}</p>
<p>{{ reversedMessage }}</p>
</div>
var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 计算属性的 getter
reversedMessage: function () {
// `this` 指向 vm 实例
return this.message.split('').reverse().join('')
}
}
})
2.watch的基础应用
<div id="demo">{{ fullName }}</div>
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar',
fullName: 'Foo Bar'
},
watch: {
firstName: function (val) {
this.fullName = val + ' ' + this.lastName
},
lastName: function (val) {
this.fullName = this.firstName + ' ' + val
}
}
})
3、computed有缓存,data不变则不会重新计算
<div>
<p>num {{num}}</p>
<p>double1 {{double1}}</p>
<input v-model="double2" />
</div>
export default {
data() {
return {
num:20
}
},
computed:{
double1(){
return this.num * 2
},
double2:{
get(){
return this.num * 3
},
set(val){
this.num = val / 2
}
}
}
}
2、watch如何深度监听,watch监听引用类型,拿不到oldVal
<input v-model="name" />
<input v-model="info.city" />
export default {
data() {
return {
name: '双越',
info: {
city: '北京'
},
num:'37'
}
},
watch: {
name(oldVal, val) {
console.log('watch name', oldVal, val); //值类型可以正常拿到oldVal和val
},
info: {
handler(oldVal, val) {
console.log('oldVal', oldVal, 'val', val); //引用类型,拿不到oldVal,因为指针相同,此时已经指向了新的val
},
deep: true // 深度监听
}
},
}
注意:
1.基本类型介绍
Undefined、Null、Boolean、Number、String
2.引用类型介绍
一类对象所具有的属性和方法
Object类型,Array类型,Date类型,RegExp类型(正则),function类型。还有基本包装类型,也是一种引用类型,ECMAScript还提供了 3个特殊的引用类型:Boolean、Number和String。
三、class和style
1、使用动态属性,使用驼峰式写法
<div>
<topNavi></topNavi>
<view :class="{black:isBlack,yellow:isYellow}">使用class</view>
<view :class="[black,yellow]">使用class</view>
<view :style="styleData">使用class</view>
</div>
export default {
data() {
return {
isBlack:true,
isYellow:true,
black:'black',
yellow:'yellow',
styleData:{
fontSize:'40px',//转换为驼峰式写法
color:'red',
backgroundColor:'#ccc',//转换为驼峰式写法
}
}
},
}
<style scoped>
.black{
color: #0000FF;
}
.yellow{
background: yellow;
}
</style>
四、条件渲染
1、v-if v-else 的用法,可使用变量,也可以使用===表达式
2、v-if和v-show的区别
v-if是直接渲染,另外一个直接不渲染
v-show是渲染两个,另外一个style是display:none;
3、v-if和v-show的使用场景
不频繁切换渲染用v-if,频繁切换渲染用v-show
<template>
<div>
<topNavi></topNavi>
<view v-if="type==='a'">A</view>
<view v-else-if="type==='b'">B</view>
<view v-else>other</view>
<view v-show="type==='a'">A</view>
<view v-show="type==='b'">B</view>
</div>
</template>
data() {
return {
type:'a',
}
},
五、循环(列表)渲染
1、如何遍历对象?---也可以用v-for
<template>
<div>
<topNavi></topNavi>
<view>数组的遍历</view>
<view>
<view v-for="(item,index) in listArr" :key="item.id">
{{index}} {{item.id}} {{item.title}}
</view>
</view>
<view>对象的遍历</view>
<view>
<view v-for="(val,key,index) in listObj" :key="key">
{{index}} {{key}} {{val.title}}
</view>
</view>
</div>
</template>
<script>
import topNavi from '../components/topNavi.vue';
export default {
data() {
return {
listArr: [{
id: 'a',
title: '标题1'
}, {
id: 'b',
title: '标题2'
}, {
id: 'c',
title: '标题3'
}],
listObj: {
a: {
title: '标题1'
},
b: {
title: '标题2'
},
c: {
title: '标题3'
}
}
}
},
components: {
topNavi
},
methods: {}
}
</script>
2、key的重要性、key不能乱写(如random或者index)
注意:key是dom元素的唯一标识,每一个页面的key必须要唯一。因为如果key不唯一,渲染页面变化时页面会出现错误操作。
3、v-for和v-if不能一起用!
注意:首先vue的规定是不能这么写,其次是因为v-for计算级别是高于v-if的,如果写在一起会每一个v-for的计算后都会去计算一遍v-if这样会降低浏览器性能。可以将v-if写在v-for外层或者内层里面。
六、事件
<template>
<div>
<topNavi></topNavi>
<view>{{number}}</view>
<button @click="increment1">+1</button>
<button @click="increment2(2,$event)">+1</button>
</div>
</template>
<script>
import topNavi from '../components/topNavi.vue';
export default {
data() {
return {
number:0
}
},
components: {
topNavi
},
methods: {
increment1(event){
console.log('event',event);
console.log('event',event.target);
console.log('event',event.currentTarget);
}
}
}
</script>
1、enent参数,自定义参数
onChange(val,event){
//val:自定义参数
//enent参数
}
注意:event是原生的
2、事件修饰符,按键修饰符
事件修饰符
<!--阻止单击事件继续传播-->
<a v-on:click.stop="doTish"></a>
按键修饰符
<!--按下ctrl触发-->
<button @click.ctrl="onClick">A</button>
3、【观察】事件绑定到了那里
注意:vue的事件我把它放在什么元素下,它就会挂载在什么元素下
七、表单
1、v-model
<template>
<div>
<topNavi></topNavi>
<!-- 去掉输入框前后的空格 -->
<input type="text" v-model.trim="name" />
<!-- 输入完成后才会变化 -->
<input type="text" v-model.lazy="name" />
<!-- 转化成数字 -->
<input type="text" v-model.number="age" placeholder="转化成数字" />
</div>
</template>
2、常见表单项textarea、checkbox、radio、select
<template>
<div>
<div>多行文本{{desc}}</div>
<textarea v-model="desc"></textarea>
<!-- 注意<textarea>{{desc}}</textarea>是不允许的 -->
<div>复选框{{checked}}</div>
<input type="checkbox" v-model="checked" />
<div>多个复选框{{checkNames}}</div>
<input type="checkbox" id="jack" value="jsck" v-model="checkNames" />
<label for="jack">jack</label>
<input type="checkbox" id="john" value="john" v-model="checkNames" />
<label for="john">john</label>
<input type="checkbox" id="mike" value="mike" v-model="checkNames" />
<label for="mike">mike</label>
<div>单选{{gender}}</div>
<input type="radio" id="male" value="male" v-model="gender" />
<label for="male">male</label>
<input type="radio" id="female" value="female" v-model="gender" />
<label for="female">female</label>
<div>下拉列表{{selected}}</div>
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<div>下拉列表多选{{selectedList}}</div>
<select v-model="selectedList" multiple>
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</div>
</template>
export default {
data() {
return {
desc:'',
checked:'',
checkNames:[],
gender:'',
selected:'',
selectedList:[],
}
},
mounted() {
},
methods: {
}
}
3、修饰符lazy、number、trim
八、Vue组件使用
1、props和$emit
props接收父组件的传值,$emit子组件调用父组件的方法且传值。
props应用的两种写法
//1、简单直接申明
props:['list'],
//2、标记处传入值的类型
props: {
list: {
type: Array,
default () {
return []
}
}
},
注意:props传入的值是不能直接进行修改,如果需要修改,可以赋值后再进行修改
2、组件间通讯
父子间传值通过(:属性名)传值,父组件接收子组件传值通过方法的触发。@子组件定义的方法名称。
父组件
<template>
<div>
<!-- 应用子组件 -->
<cc :name="name" @changeName="onChange"></cc>
</div>
</template>
<script>
import cc from './components/cc.vue';//引入子组件
export default {
data() {
return {
name:'简隋英'
}
},
components:{
cc//申明子组件
},
methods: {
onChange(val){
this.name = val;
}
}
}
</script>
<style>
</style>
子组件
<template>
<div>
{{name}}
<button @click="onChange">改变名称!</button>
</div>
</template>
<script>
export default {
data() {
return {
}
},
props:['name'],
mounted() {},
methods: {
onChange(){
this.$emit('changeName','李玉')
}
}
}
</script>
<style>
</style>
3、自定义事件 event
注意:vue本身就具有自定义事件的能力。
vue实例 event.js
import Vue from 'vue'
export default new Vue
父组件 C.vue
<template>
<div>
<!-- 应用子组件 -->
<cc></cc>
<bb></bb>
</div>
</template>
<script>
import cc from './components/cc.vue';//引入子组件
import bb from './components/bb.vue';//引入子组件
export default {
data() {
return {
name:'简隋英'
}
},
components:{
cc,//申明子组件
bb
},
methods: {
onChange(val){
this.name = val;
}
}
}
</script>
<style>
</style>
子组件bb.vue
注意event自定义事件一定要在beforeDestroy()及时销毁,否者可能会造成内存泄露
<template>
<div>
bb
</div>
</template>
<script>
import event from '../event.js';
export default {
data() {
return {
}
},
mounted() {
event.$on('onAddTitle',this.addTitle)
},
methods: {
addTitle(title){
console.log('打印传值',title);
}
},
beforeDestroy() {
//及时销毁,否者可能会造成内存泄露
event.$off('onAddTitle',this.addTitle)
}
}
</script>
<style>
</style>
子组件cc.vue
<template>
<div>
cc
<button @click="changeBB">改变bb的值</button>
</div>
</template>
<script>
import event from '../event.js';
export default {
data() {
return {
title:'cc',
}
},
methods: {
changeBB(){
event.$emit('onAddTitle',this.title);
}
}
}
</script>
<style>
</style>
4、组件生命周期
1、create和mounted有什么区别?
create是初始化了vue的实例,现在并没有开始渲染。mounted是页面在网页中真正绘制完成了。大部分时候我们都应该在mounted里面去进行操作。如:ajax或是绑定事件什么的。
2、beforeDestroy里面进行的操作
解除绑定,销毁子组件以及事件监听,定时任务settimeout需要销毁。
5、生命周期(父子组件)
父组件 :created 父组件:beforeUpdate
子组件 :created 子组件:beforeUpdate
子组件 :mounted 子组件:updated
父组件 :mounted 父组件:updated
vue的高级特性
一、自定义v-model
父组件
<template>
<div class="hello">
<!--自定义v-model-->
<p>{{name}}</p>
<customVmodel v-model="name"></customVmodel>
</div>
</template>
<script>
import customVmodel from './customVmodel.vue'
export default {
data(){
return {
name:''
}
},
components:{
customVmodel
}
}
</script>
<style scoped>
</style>
子组件
<template>
<div>
<input type="text" :value="text" @input="$emit('change',$event.target.value)" />
</div>
</template>
<script>
export default {
model:{
prop:'text',
event:'change'
},
props:{
text:String,
default(){
return ''
}
}
}
</script>
<style>
</style>
注意:1、上面input使用了 :value 而不是v-model
2、上面的change和model.event要对应起来
3、text属性要对应起来
二、$nextTick
1、Vue是异步渲染
2、data改变之后,Dom不会立刻更新
3、$nextTick会在Dom渲染之后被触发,以获取最新Dom节点
<template>
<div>
<div ref="ul1">
<div v-for="(item,index) in list">{{item}}</div>
</div>
<button @click="add">添加Dom</button>
</div>
</template>
<script>
export default {
data(){
return {
list:['a','b','c'],
}
},
methods:{
add(){
this.list.push('简隋英');
this.list.push('李玉');
//异步渲染
this.$nextTick(()=>{
const ulElem = this.$refs.ul1;
console.log(ulElem.childNodes.length);
})
}
}
}
</script>
<style scoped>
</style>
注意:1、vue是异步渲染,$nextTick 等待Dom渲染完再回调
2、页面渲染时会将data的修改做整合,多次data修改只会渲染一次。
三、slot插槽(父组件往子组件里插入内容)
1、基本使用
父组件
<template>
<div>
<SlotDemo :url="website.url">
{{website.title}}
</SlotDemo>
</div>
</template>
<script>
import SlotDemo from './SlotDemo.vue';
export default {
data(){
return {
website:{
url:'https://www.baidu.com',
title:'百度'
}
}
},
components:{
SlotDemo
}
}
</script>
<style scoped>
</style>
子组件
<template>
<div>
<a :href="url">
<slot>
默认值
</slot>
</a>
</div>
</template>
<script>
export default {
data(){
return {
}
},
props:['url']
}
</script>
<style scoped>
</style>
2、作用域插槽
父组件
<template>
<div>
<ScopedSlotDemo :url="website.url">
<template v-slot="slotProps">
{{slotProps.slotData.subTitle}}
</template>
</ScopedSlotDemo>
</div>
</template>
<script>
import ScopedSlotDemo from './ScopedSlotDemo.vue';
export default {
data() {
return {
website: {
url: 'https://www.baidu.com',
title: '百度'
}
}
},
components: {
ScopedSlotDemo
}
}
</script>
<style scoped>
</style>
子组件
<template>
<div>
<a :href="url">
<slot :slotData="website">
<!-- {{website.subTitle}} -->
</slot>
</a>
</div>
</template>
<script>
export default {
data(){
return {
website:{
url:'https://www.qq.com/',
title:'腾讯',
subTitle:'腾讯新闻'
}
}
},
props:['url']
}
</script>
<style>
</style>
注意:作用域插槽父组件访问子组件数据时,是需要用template里的 v-slot访问子组件(动态属性:)映射出来的数据。
3、具名插槽
父组件
<template>
<div>
<NameSlot>
<template v-slot:header>
<h1>将插入header slot 中</h1>
</template>
<p>将插入main slot 中</p>
<template #footer>
<p>将插入footer slot 中</p>
</template>
</NameSlot>
</div>
</template>
<script>
import NameSlot from './NameSlot.vue';
export default {
data() {
return {
website: {
url: 'https://www.baidu.com',
title: '百度'
}
}
},
components: {
NameSlot
}
}
</script>
<style scoped>
</style>
子组件
<template>
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<script>
export default {
data() {
return {}
}
}
</script>
<style>
</style>
注意:父组件template的v-slot:接,子组件slot的动态属性name将两个对应。如果没有动态属性name将自动匹配父组件插值
四、动态组件
1、:is="comonpent-name" 用法
<template>
<div>
<div v-for="(val,key) in newData" :key="key">
<component :is="val.key"></component>
</div>
</div>
</template>
<script>
export default {
data(){
return {
newData:{
1:{
key:'text'
},
2:{
key:'text'
},
3:{
key:'image'
}
}
}
}
}
</script>
<style>
</style>
2、需要根据数据,动态渲染的场景。即组件类型不确定
五、异步组件
1、import()函数(按需加载,异步加载大组件)
//同步引入组件
import componentName from './componentName';
export default {
components:{
componentName,//同步引入组件申明
componentName:() => import('./componentName'),//异步引入组件
componentName:() => {
return import('./componentName')//异步引入组件另一种写法
}
}
}
六、keep-alive
1、缓存组件
2、频繁切换,不需要重复渲染
3、Vue常见的性能优化
<template>
<div>
<button @click="changeBtl('A')">A</button>
<button @click="changeBtl('B')">B</button>
<button @click="changeBtl('C')">C</button>
<keep-alive><!--v-show-->
<AA v-if="showVal=='A'"></AA>
<AA v-if="showVal=='B'"></AA>
<AA v-if="showVal=='C'"></AA>
</keep-alive>
</div>
</template>
<script>
import AA from './AA.vue';
import BB from './BB.vue';
import CC from './CC.vue';
export default {
data() {
return {
showVal:null,
}
},
components: {
AA,
BB,
CC
}
}
</script>
<style scoped>
</style>
注意:keep-alive和vue的v-show看起来相似。但是v-show的实现方式是根据css的disply:none来实现。而keep-alive的实现方式是在vue框架层级进行的js对象的渲染。v-show相对于keep-alive而言简单粗暴了一些。
七、mixin
1、多个组件有相同逻辑,抽离出来
2、mixin并不是完美的解决方案,会有一些问题
3、Vue3提出的Composition API旨在解决这些问题
引入组件
<template>
<div>
<p>{{name}}{{city}}</p>
</div>
</template>
<script>
import myMixin from './myMixin.js';
export default {
mixins:[myMixin],
data(){
return {
name:'简隋英'
}
},
mounted() {
console.log('mounted',this.name);
}
}
</script>
<style scoped>
</style>
mixin公共js
export default {
data() {
return {
city: '北京'
}
},
mounted() {
console.log('mounted', this.city);
}
}
注意mixin的问题:
1、变量来源不明,不利于阅读。
2、多mixin可能会造成命名冲突。
3、mixin和组件可能出现多对多的关系,复杂度高。
vuex
将vuex映射到全局
import Vue from 'vue'
import App from './App.vue'
//引入store文件
import store from './store/index.js'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App),
}).$mount('#app')
一、state单一状态树
import Vue from 'vue'
import Vuex from 'vuex'
//挂载Vuex
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
name: '简隋英',
count: 1
}
})
export default store
//导出 默认值
1.1、state基本使用与简介
在组件中访问state单一状态树内的值this.$store.state访问
例如:访问state里的name值
this.$store.state.name
1.2、mapState辅助函数
使用辅助函数mapState需要先将它导入
import { mapState } from 'vuex'
- 基本使用方式
<template>
<div>
{{count}}{{countNum}}{{mergeVal}}
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
data() {
return {
countryName:'中国'
}
},
computed:mapState({
//箭头函数可使代码更简洁
count: state => state.count,
//传字符串参数‘count’等同于state => state.count
countNum:'count',
//为了能够使用‘this’获取局部状态,必须使用常规函数
mergeVal(state){
return state.count + this.countryName
}
})
}
</script>
<style scoped>
</style>
- 合并组件中原本的计算属性computed
import { mapState } from 'vuex'
export default {
//原本的computed计算属性
computed:{
computedName1(){ return ...},
computedName2(){ return ...},
},
//引入mapState后合并的计算属性
computed:mapState({
//将原本的computed计算属性复制过来
computedName1(){ return ...},
computedName2(){ return ...},
//再维护vuex
count: state => state.count,
countNum:'count',
mergeVal(state){
return state.count + this.countryName
}
})
}
- 另外的一种合并原始computed和mapState的方法 es6的 ...mapState({ })
import { mapState } from 'vuex'
export default {
//原本的computed计算属性
computed:{
computedName1(){ return ...},
computedName2(){ return ...},
},
//引入mapState后合并的计算属性
computed:{
computedName1(){ return ...},
computedName2(){ return ...},
//维护vuex
...mapState({
count: state => state.count,
countNum:'count',
mergeVal(state){
return state.count + this.countryName
}
})
},
}
二、getter
import Vue from 'vue'
import Vuex from 'vuex'
//挂载Vuex
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
name: '简隋英',
},
getters: {
nameInfo(state) {
return "姓名:" + state.name
}
},
})
export default store
//导出 默认值
2.1、getter基本使用与简介
getter类似于vue组件内的computed计算属性,它对state单一转态树内的数据进行计算,且具有缓存计算结果的功能
上面代码中用this.$store.state.name访问到的结果是。姓名:简隋英
2.2、mapGetters
使用辅助函数mapGetters前需要先将其引入
import { mapGetters } from 'vuex'
- 辅助函数的普通使用
<template>
<div>
<div>{{value1}}</div>
<div>{{value2}}</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: mapGetters({
value1: 'data1', //data1为是state中的值名称
value2: 'data2' //同上
})
}
</script>
<style>
</style>
另一种引入方式 es6...
<template>
<div>
<div>{{value1}}</div>
<div>{{value2}}</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters({
value1: 'data1', //data1为是state中的值名称
value2: 'data2' //同上
})
}
}
</script>
<style>
</style>
三、mutation
import Vue from 'vue'
import Vuex from 'vuex'
//挂载Vuex
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
countAdd(state){
// 变更状态
state.count++
},
countAddVal(state,val){
state.count = state.count + val
}
}
})
export default store
//导出 默认值
3.1、 mutation基本使用与简介
- 更改store中state内数据的状态是提交mutation,vuex中的mutation非常类似于事件methods
- 注意:mutation里的只能做同步操作
- 在vue组件中调用mutations中的方法需要通过store.commit 方法
例如:调用上代码中的countAdd方法
this.$store.commit("countAdd")
例如:调用上代码中的countAddVal方法,且进行传值
this.$store.commit("countAddVal" , 7 )
3.2、mapMutations
首先想要使用mapMutations辅助函数需要先将其引入,可将组件中的methods映射为store.commit调用,需要在根节点注入store
import { mapMutations } from 'vuex'
组件内引入mapMutations有两种写法
...mapMutations
- 数组方式引入[]
import { mapMutations } from 'vuex'
export default {
methods:{
...mapMutations([
"increment"//注意:必须写双引号
]),
go(){
this.increment('李玉')
//上面的写法和下面的写法可以做相同操作
// this.$store.commit('increment','李玉')
}
}
}
- 对象引入{}
import { mapMutations } from 'vuex'
export default {
methods:{
...mapMutations({
onChangeName:"increment"
}),
go(){
this.onChangeName('李玉')
//上面的写法和下面的写法可以做相同操作
// this.$store.commit('increment','李玉')
}
}
}
四、Action
import Vue from "vue"
import Vuex from "vuex"
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
valData: null
},
mutations: {
changeValData(state, stark) {
state.valData = stark
},
},
actions: {
changeAsyn({ commit },val){
setTimeout(() => {
commit('changeValData',val)
}, 1000)
}
}
})
export default store
4.1、action的基本使用与简介
- action类似于mutation但是又有所不同,不同在于:
- action提交的是mutation,而不是直接去操作state状态树内的值。
- atcion里可以提交包含任何异步操作
- action通过store.dispatch方法触发
例如:触发以上代码中的changeAsyn函数
this.$store.dispatch("changeAsyn",'简隋英')
4.2、actions同样支持载荷方式和对象方式进行分发
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
4.3、mapActions
首先想要使用mapActions辅助函数需要先将其引入,可将组件中的methods映射为store.dispatch调用,需要在根节点注入store
import { mapActions } from 'vuex'
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
4.4、组合actions
Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
首先,你需要明白 store.dispatch
可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch
仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
现在你可以:
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
vue-router
一、vue-router基础
1.1、vue3加入创建一个router
- 在src目录下创建一个router的文件,且在内新建一个index.js文件
- 在main.js文件中引入index.js文件并使用他
import router from './router/index'
createApp(App).use(router).mount('#app')
- router文件内index.js文件内容
import {createRouter, createWebHistory} from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
const routes = [
{ path: '/', component: HelloWorld }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
- 我们可以在任意组件中以
this.$router
的形式访问它,并且以this.$route
的形式访问当前路由:
//页面跳转
this.$router.push('/a')
//访问路由内容
this.$route
1.2、路由传值方式有三种
第一种:动态参数传值
配置:
传值:
接收:
第二种 :params和name
传值:
接收:
第三种:path和query
传值:
接收:
1.2、路由获取参数的两种常见方式params和query
由于动态路由也是传递params的,所以在this.$router.push()方法中path不能和params一起使用,否者params将无效,此时需要用name来指定页面
在目标页面通过this.$route.params获取参数:
this.$router.push({name:"menuLink",params:{id:123}})
this.$router.push('/User/123')
//接收
<p>提示:{{this.$route.params.id}}</p>
在目标页面通过this.$route.query 获取参数
//传值
this.$router.push({path:"/menLink",query:{alert:"页面跳转成功"}})
//用query获取值
<p>提示:{{this.$route.query.alert}}</p>
二、路由模式(hash 和 H5 history)
2.1、hash模式
hash模式使用 createWebHashHistory() 创建的
import { createRouter , createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes: [
...
]
})
它在内部传递的实际URL之前使用了一个哈希字符(#),由于这部分URL从未被发送到服务器,所以它不需要在服务器层面上进行任何特殊处理。
例如:http://abc.com/#/user/10
2.2、H5 history 模式
H5 history 模式用 createWebHistory() 创建的
import { createRouter, createWbeHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
...
]
})
使用这种模式时URL的显示
例如:https://abc.com/user/10
当访问到单个页面的客户端应用,如果没有适当的服务器配置,用户会得到一个404的错误返回。
此时解决这个问题需要在后端的服务器上添加一个回退路由。如果URL不匹配任何静态路由资源,他应当提供应用程序中index.html相同的页面。
服务器配置去vue-router官网看吧!
不同的历史模式 | Vue RouterVue.js 的官方路由https://router.vuejs.org/zh/guide/essentials/history-mode.html#html5-%E6%A8%A1%E5%BC%8F不过当想使用H5 history模式有需要显示不同的404好看的显示可以在前端做一个拦截。
const router = createRouter({
history: createWebHistory(),
routes: [
{path:'/:pathMatch(.*)', component: NotFoundComponent}
]
})
例如:腾讯qq.com
三、路由配置(动态路由、懒加载)
3.1、动态路由
路由参数用:表示。当一个路由被匹配时,它的params的值将在每个组件中以this.$route.params的形式暴露出来,因此我们可以通过更新User模板来呈现当前用户ID
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = [
//动态字段以冒号开始
{ path: '/users/:id', component: User},
]
3.2、懒加载
当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:
const User = () => import(./view/User)
const router = createRouter({
routes: [
path: '/users/:id', component: User
]
})
vue原理
一、组件化基础
- 组件化
“很久以前”就有组件化了,asp jsp php 就已经有组件化了,nodejs中也有类似的组件化。
传统组件,只是静态渲染,更新还要依赖于操作DOM(如:jQuery就是当时流行的操作DOM的工具)
- 数据驱动视图(MVVM:vue的 和 setState:React的)
这是在组件化上面进行了一个微创新,叫数据驱动视图
数据驱动视图是我们渲染页面时,不需要自己去操作DOM了。而是通过去更改数据,然后修改数据通过viewModel自己从新去修改DOM。
- MVVM
<template>标签内的东西 vue里面的data () { return {} }
M:对应Model,data
V:View ,template
VM:ViewModel,vue的后台更新Dom块的操作
二、vue响应式原理
- 组件data的数据一旦变化,立刻触发视图的更新
- 实现数据驱动视图的第一步
- 考察vue原理的第一道题
2.0、核心API - Object.defineProperty 基础写法
const obj = {}
let name = '楚晚宁'
Object.defineProperty(obj, 'url', {//obj.url=
get() {
console.log('第一个',name);
return name
},
set(newVal) {
console.log('可爱',newVal);
name = newVal
},
configurable: true, // 可删除
enumerable: true // 可枚举(遍历)
})
console.log(obj.url);
obj.url = '墨燃'
console.log('最后一个',name);
//第一个 楚晚宁
//楚晚宁
//可爱 墨燃
//最后一个 墨燃
2.1、核心API - Object.defineProperty
//准备数据
const data = {
name:'zhangsan',
age:20
}
observer(data)
function observer(target) {
if(typeof target !== 'object' || target ===null){
return target
}
for(let key in target){
defineReactive(target,key,target[key])
}
}
//重新定义属性监听起来
function defineReactive(target,key,value){
//核心API
Object.defineProperty(target,key,{
get(){
return name
},
set(newValue){
if(newValue != value){
value = newValue
updataView()
}
}
})
}
function updataView(){
console.log('更新视图')
}
- data.name = 'lishi';data.age = 21
将会输出两次“更新视图”。
- data.x = '100'
不会输入任何值,因为对象中没有对data.x进行声明
- 上面的代码无法实现深度监听准备对象,只能实现浅监听。想要实现深度监听只能如下代码:
function updataView(){
console.log('更新视图2')
}
//重庆定义属性监听起来
function defineReactive(target,key,value){
//深度监听
observer(value)
//核心API
Object.defineProperty(target,key,{
get(){
return name
},
set(newValue){
if(newValue != value){
//新增加属性的监听
observer(value)
value = newValue
updataView()
}
}
})
}
function observer(target) {
if(typeof target !== 'object' || target ===null){
return target
}
for(let key in target){
defineReactive(target,key,target[key])
}
}
const data = {
name:'zhangsan',
age:20,
info:{
address:'北京'
}
}
observer(data)
data.name = 'lishi'//普通监听
data.x = '100'//新增属性监听
data.info.address = '重庆'//深度监听
2.2、Object.defineProperty的缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增/删除属性(Vue.set Vue.delete)Vue2.0需要,Vue3.0不需要
- 无法原生监听数组,需要特殊处理
2.3、实现监听数组
function updataView(){
console.log('更新视图2')
}
//重新定义数组的原型
const oldArrayProperty = Array.prototype
//创建新对象,原型指向oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push','pop'].forEach(methodName =>{
arrProto[methodName] = function(){
console.log('asdf');
updataView()//更新视图
oldArrayProperty[methodName].call(this,...arguments)
}
})
//重新定义属性监听起来
function defineReactive(target,key,value){
//深度监听
observer(value)
//核心API
Object.defineProperty(target,key,{
get(){
return name
},
set(newValue){
if(newValue != value){
//新增加属性的监听
observer(newValue)
value = newValue
updataView()
}
}
})
}
function observer(target) {
if(typeof target !== 'object' || target ===null){
return target
}
//数组
if(Array.isArray(target)){
target.__proto__ = arrProto
console.log('显示',target);
}
//对象
for(let key in target){
defineReactive(target,key,target[key])
}
}
let data = {
name:'zhangsan',
age:20,
info:{
address:'北京'
},
nums: [10,20,30]
}
let nums = [10,20,30]
console.log(data.nums);
observer(data)
// data.name = 'lishi'//普通监听
// data.x = '100'//新增属性监听
// data.info.address = '重庆'//深度监听
data.nums.push(4)
2.4、 proxy
2.4.1、基本使用
let data = {
name:'周',
age:20
}
const proxyData = new Proxy(data,{
get(target,key,receiver){
const result = Reflect.get(target,key,receiver)
console.log('get',key);
return result
},
set() {
const result = Reflect.set(target,key,val,receiver)
console.log('set',key,val);
return result
},
deleteProperty(){
const result = Reflect.deleteProperty(target,key)
console.log('delete property',key);
return result
}
})
2.4.2、Reflect
- 和proxy能力一一对应
- 规范化、标准化、函数式
const obj = {a:100,b:100}
//判断数对象中是否有某个元数
//以前
if('a' in obj){}
//reflect
if(Reflect.has(obj,'a')){}
//还有一种
if(obj.hasOwnProperty('a')){}
- 替代掉Object上的工具函数
const obj = {a:100,b:100}
console.log(Object.getOwnPropertyNames(obj));
//['a','b']
console.log(Reflect.ownKeys(obj));
//['a','b']
2.4.3、实现响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
//不是对象或数组,则返回
return target
}
const proxyConf = {
get(target, key, receiver) {
//只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if(ownKeys.includes(key)){
console.log('get',key);//监听
}
const result = Reflect.get(target, key, receiver)
//深度监听
//性能如何提升的
return reactive(result)
},
set(target,key,val,receiver) {
//重复的数据,不处理
if(val === target[key]){
return true
}
const ownKeys = Reflect.ownKeys(target)
if(ownKeys.includes(key)){
console.log('已有的 key', key);
}else{
console.log('新增的 key', key);
}
const result = Reflect.set(target, key, val, receiver)
return result
},
deleteProperty(target,key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key);
return result
}
}
//生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
const data = {
name: 'zhang',
age: 20,
info:{
city:'beijing',
a:{
b:{
c:{
d:'e'
}
}
}
}
}
const proxyData = reactive(data)
proxyData.name = '11'
proxyData.city = '11'
console.log(proxyData.info);
delete proxyData.name
- 深度监听性能更好
- 可监听 新增/删除 属性
- 可原生监听数组的变化
2.5、总结
- proxy能避免Object.defineProperty的问题
- proxy无法兼容所有浏览器,无法polyfill
三、虚拟DOM(virtual DOM)和diff算法
- vdom是实现vue和react的重要基石
- diff算法是vdom中最核心、最关键的部分
3.1、虚拟DOM(vdom)
- DOM操作非常的耗费性能
- JS是非常快的
- 以前用JQuery,可以自行控制DOM操作的时机,手动调整
- Vue和react都是数据驱动视图,如何有效控制DOM操作?
3.2、解决方案-vdom (上面问题)
- 有了一定复杂度,想减少计算次数比较困难
- 能不能把计算,更多的转移动为JS计算?因为JS执行速度很快
- vdom-用JS模拟DOM结构,计算出最小的变更,操作DOM
3.3、用JS模拟DOM结构
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size: 20px;">
<li>a</li>
</ul>
</div>
{
tag:'div',
props:{
className:'container',
id:'div1'
},
children:[{
tag:'p',
children:'vdom'
},{
tag:'ul',
props:{
style:'font-size: 20px;'
},
children:[{
tag:'li',
children:'a'
}]
}]
}
3.4、通过snabbdom学习vdom
- 简洁强大的vdom库,易学易用
- Vue参考它实现的vdom和diff
- vue3.0重写了vdom的代码,优化了性
//引入
import {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
//初始化
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);
const container = document.getElementById("container");
//第一个vnode
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
//调用渲染
patch(container, vnode);
//第二个vnode函数
const newVnode = h(
"div#container.two.classes",
{ on: { click: anotherEventHandler } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
//覆盖上一个vnode更新
patch(vnode, newVnode);
3.5、vdom的总结
- jQuery更新DOM需要直接渲染操作全部的DOM
- 用JS模拟DOM结构(vnode)
- 新旧vnode对比,得出最小的更新范围,最后更新DOM
- 数据驱动视图模式下,有效的控制DOM的操作
四、diff算法概述
4.1、diff算法
- diff算法是vdom中最核心,最关键的部分
- diff算法能在日常中使用vue react中提现出来(如key)
4.2、diff算法概述
- diff即对比,是一个广泛的概念,如linux diff命令、git diff等
- 两个对象也可以做diff,如:http://github.com/cujojs/jiff
- 两颗树做diff,如上的vdom diff
4.3、树diff的时间复杂度O(n^3)
- 第一,遍历tree1;第二,遍历tree2;第三,排序
- 1000个节点,要计算1亿次,算法不可用
4.4、优化时间复杂度到O(n)
- 只比较第一层级,不跨级比较
- tag不想共,则直接删除重建,不再深度比较
- tag和key,两者都相同,则认为是相同节点,不再深度比较
tag不同
五、snabbdom-源码解读
5.1、h()函数
- h.ts
//只传入标签(h函数的第一个参数)
export function h(sel: string): VNode;
//只传入标签和data(如事件,h函数的第二个参数)
export function h(sel: string, data: VNodeData | null): VNode;
//只传入标签和children(h函数的最后一个[]参数)
export function h(sel: string, children: VNodeChildren): VNode;
//传入标签,data,children
export function h(sel: string,data: VNodeData | null,children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
//一系列处理
//返回一个vnode
return vnode(sel, data, children, text, undefined);
}
- vnode()函数:vnode.ts
export function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | DocumentFragment | Text | undefined
): VNode {
const key = data === undefined ? undefined : data.key;
//返回children, text二者只能有一个,要么是个文本,要么是子元素
//elm是DOM元素,决定返回的内容更新在什么地方
//key是唯一值,所有的组件都不拒绝key,可以不只是存在于v-for中
return { sel, data, children, text, elm, key };
}
5.2、patch()函数
- 传入值
return function patch(
//传入旧的vnode
oldVnode: VNode | Element | DocumentFragment,
//传入新的vnode
vnode: VNode
):
- cbs回调,执行pre hook,生命周期
//cbs回调,执行pre hook,生命周期
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
- 第一个参数是DOM的情况
//第一个参数不是vnode,patch(container, vnode),第一个参数是DOM元素
if (isElement(api, oldVnode)) {
//创建一个空的vnode,关联到这个DOM元素
oldVnode = emptyNodeAt(oldVnode);
} else if (isDocumentFragment(api, oldVnode)) {
oldVnode = emptyDocumentFragmentAt(oldVnode);
}
- 相同的vnode
//相同的vnode
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
}
- sameVnode判断vnode完全相同
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
//vnode1和vnode2的key相同
const isSameKey = vnode1.key === vnode2.key;
//vnode1和vnode2的data数据相同
const isSameIs = vnode1.data?.is === vnode2.data?.is;
//vnode1和vnode2的sel元素相同
const isSameSel = vnode1.sel === vnode2.sel;
//以上3个相同说明为同一个元素
return isSameSel && isSameKey && isSameIs;
}
- 两个vnode不相同(直接删除重建)
createElm(vnode, insertedVnodeQueue);
- patchVnode相同函数vnode处理计算
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
//类似于执行了一个生命周期的公式hook
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
//设置vnode.element,把旧的vnode新建牵引新的elem
const elm = (vnode.elm = oldVnode.elm)!;
//旧的子元素,children
const oldCh = oldVnode.children as VNode[];
//新的子元素,children
const ch = vnode.children as VNode[];
//做一个截断,oldVnode和vnode相同直接返回,
if (oldVnode === vnode) return;
//hook相关
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
vnode.data.hook?.update?.(oldVnode, vnode);
}
//isUndef相当于判断是不是undefined
//vnode.text === undefined
//就说明元素标签内的text内容为undefined,且标签内的children不为undefined
//children有值
if (isUndef(vnode.text)) {
//新旧都有 children
if (isDef(oldCh) && isDef(ch)) {
//新旧children不相同,更新children
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
//新的children有,旧的children无 (旧有text)
} else if (isDef(ch)) {
//旧的text有值,先设置为空
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
//添加新的children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
//新的children无,旧的children有
} else if (isDef(oldCh)) {
//直接移除旧的children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
//旧的text有值,新的text没有值
} else if (isDef(oldVnode.text)) {
//那elm内容直接标记为“”空
api.setTextContent(elm, "");
}
//vnode.text !== undefined
//说明标签内children为空
//新的vnode.text !== 旧的vnode.text
} else if (oldVnode.text !== vnode.text) {
//移除旧的children
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
//再设置新的text
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}
- updateChildren 新旧children不相同,更新children
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
//开始Startindex
let oldStartIdx = 0;
let newStartIdx = 0;
//结束EndIdx
let oldEndIdx = oldCh.length - 1;
let newEndIdx = newCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
//进行循环用到上面所定义的4个index
//循环的是以两边往中间进行
//结束是当循环走到中间的时候结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
//开始和开始做对比
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
//结束和结束做对比
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
//开始和结束做对比
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
//结束和开始做对比
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
//以上4个都未命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
//拿新节点的key,是否能对应上 oldch 中的某个节点的key
idxInOld = oldKeyToIdx[newStartVnode.key as string];
//没有对应上
if (isUndef(idxInOld)) {
// 直接插入 new element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
//对应上了
} else {
//对应上key的节点
elmToMove = oldCh[idxInOld];
//sel不相等
if (elmToMove.sel !== newStartVnode.sel) {
//直接插入 new element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
//sel相等,key相等
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
总结:不使用key和使用key的情况
不使用key直接全部新建,使用key还能对比一下做置换,这样能够提升性能
六、编译模板
- 模板是vue开发中最常用的一部分,即与使用相关的原理
- 它不是html,有指令、插值、js表达式
- 前置知识:JS的with语法
- vue template complier 将模板编译为render函数
- 执行render函数生成vnode
6.1、with语法
- 改变{ } 内自由变量的查找规则,当做obj属性来查找
- 如果找不到匹配的obj属性,就会报错
- with要慎用,它打破了作用域规则,易读性变差
6.2、编译模板
- 模板不是html,有指令、差值、JS表达式、能实现判断循环
- html是标签语言,只有JS才能实现判断、玄幻(图灵完备的)
- 因此,模板一定是转换为某种JS代码,即编译模板
const compiler= require('vue-template-compoler')
//插值后面就所有的模板都写在这里
const template = `<p>{{message}}</p>`
//编译
const res = compiler.compile(template)
console.log(res.render)
- console的打印:
with(this){return _c('p',[_v(_s(message))])}
- _c createElement() 相当于 h()
- _v createTextVNode()
- _s toString()
- h函数返回的是一个vnode,createElement函数也是返回一个vnode
const template = `<p>{{flag ? message : 'no message found'}}</p>`
- console的打印:
with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
- _s函数内是个表达是,本身返回的就是个函数,函数内执行三元运算就可以直接知道结果,所以_c函数最终也还是会返回一个vnode
const template = `<div id="div1" class="container">
<img :src="imgUrl">
</div>`
- console的打印:
with(this){return _c('div',
{staticClass:'container',attrs:{'id':'div1'}},
[_c('img',{attrs:{'src':imgUrl}})])}
const template = `<div>
<p v-if="flag === 'a'">A</p>
<p v-else>B</p>
</div>`
- console的打印:
with(this){return _c('div',[(flag === 'a'?_c('p',[_v("A")]):_c('p',[_v("B")])])}
const template = `<ul>
<li v-for="item in list" :key="item.id">{{item.title}}</li>
</ul>`
- console的打印:
with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}))}
const template = `<button @click="clickHander">submit</button>`
- console的打印:
with(this){return _c('button',{on:{click:'clickHander'}},[_v('submit')])}
const template = `<input type="text" v-model="name" />`
- console的打印:
with(){
return _c('input',{directives:[{name:"model",rawname:"v-model",value:(name),expression:"name"}],
attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event.target.composing)return;
name=$event.target.value}})
}
- 将input绑定输入on:input事件,将$event.target.value绑定到输入的值内
总结:
- 模板编译为render函数,执行render函数返回vnode
- 基于vnode再执行patch和diff
- 使用webpack vue-loader,会在开发环境下编译模板
6.3、vue组件中使用使用render代替template
Vue.component('heading',{
render: function(createElement) {
return createElement(
'h1',
[
createElement('a',{
attrs: {
name: 'headerId',
href: '#' + 'headerId'
}
},'this is a tag')
]
)
}
})
- 在一些复杂的情况下,不能用template的情况下,可以考虑用render
- react一直在用render,没用tempate
6.4、vue组件是如何渲染更新的
6.4.1、初次渲染过程
- 解析模板为render函数,(或在开发环境已完成,vue-loader)
- 触发响应式,监听data属性,getter和setter
- 执行render函数,生成vnode,patch(elem,vnode)
6.4.2、更新过程
- 修改data,触发setter(此前getter已经被监听)
- 重新执行render函数执行newVnode
- patch(vnode,newVnode)
6.5、异步渲染
- $nextTick()
- 汇总data的修改,一次性更新视图
- 减少dom操作次数,提高性能
6.6、总结
- 渲染和响应式的关系
- 渲染和模板编译的关系
- 渲染和vdom的关系
- 初次渲染过程
- 更新过程
- 异步渲染
七、前端路由原理
7.1、如何用js实现hash路由
- 稍微复杂的SPA,都需要路由(单页面应用SPA)
- vue-router也是vue全家桶的标配之一
- 属于“日常使用相关原理”
7.1.1、网页url组成部分
7.1.2、hash的特点
- hash变化会触发网页跳转,即浏览器的前进和后退
- hash变化不会刷新页面,SPA必须的特点
- hash永远不会提交到server 端(前端自生自灭)
//hash变化包括
//a、js修改url
//b、手动修改url的hash
//c、浏览器前进和后退
window.onhashChange = (event) =>{
console.log('old url',event.oldUrl)
console.log('new url',event.newUrl)
console.log('hash',location.hash)
}
//页面初次加载,获取hash
document.addEventListener('DOMContentLoade',()=>{
console.log('hash',location.hash)
})
//JS修改url
document.getElementById(btn1).addEventListener('click',{
location.url = '#/user'
})
7.2、H5 history
- 用url规范的路由,但跳转时不刷新页面
- history.pushState
- windw.onpopstate
//页面初次加载,获取path
document.addEventListener('DOMContentLoade',()=>{
console.log('load',location.hash)
})
//打开一个新的路由
//【注意】用pushState方式,浏览器不会刷新页面
document.getElementById(btn1).addEventListener('click',{
const state = {name: 'page1'}
console.log('切换路由到','page1')
history.pushState(state,'','page1')
})
//监听浏览器前进还是后退
window.onpopState = (event) =>{
console.log('onpopState',levent.state,location.pathname)
}
history模式需要server端的配和(node)
- 无论访问的什么路由最终返回的都是index.html
两者的选择
- to B的系统推荐用hash,简单易用,对url规范不敏感(toB后端管理系统)
- to C的系统可以考虑用history H5 ,但是需要服务端的支持(to C的系统不需要管理SU,不需要搜索引擎都可以不用history H5模式)