Vue实例

518 阅读12分钟

一、 \color{ #e917c8}{Vue实例的原型链}

了解过JS的原型链就可以知道,Vue是一个构造函数,我们通过new操作符创建一个Vue实例vm,那么可以得出

vm._proto_===Vue.prototype

图中#\color{ #17e97a}{419}所指代的内存区域就是Vue实例的原型

其中就包含了事件绑定相关的函数,他的下一层原型就是Object.prototype了,也就是根

在构造实例的时候Vue函数接受一个options参数,称为构造选项,他主要包括以下几个部分

二、 \color{ #e917c8}{options(官网链接)}

options是一个对象,它包含的以下几类属性

本文介绍几个关键属性,详情可去官网查询

1. \color{ #17e97a}{数据}

(1) \color{ #0000FF}{data}

Vue 实例的数据对象。Vue 将会递归将 data 的属性转换为 getter/setter,从而让 data 的属性能够响应数据变化。对象必须是纯粹的对象 (含有零个或多个的 key/value 对)
\color{ #ff0000}{注:} 当一个组件被定义,data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象,\color{ #ff0000}{所以推荐优先使用函数data}

(2) \color{ #0000FF}{props}(详情)

props 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值
//vue实例部分
import Demo from "./components/demo.vue" 
new Vue({
    compoments:{
    Demo
    },
    template:`
    <demo message="我把这个传给Demo"/>
    <demo :fn="add"/>
    `
})

//demo.vue部分
<tempalte></template>
<script>
export default {
    props:["message","fn"]
  }
}
</script>
<style scoped>
    
</style>
这个props部分写在被引用的vue组件中,由外部组件传入
\color{ #ff0000}{注:} 默认传入是字符串,如果要传入函数等其他类型,需加:,如上述中的fn传了一个函数add而不是字符串add

(3) \color{ #0000FF}{methods}

methods 对象上绑定的是事件处理函数和普通函数(例:filter),它将被混入到 Vue 实例中。可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用。方法中的 this 自动绑定为 Vue 实例。
\color{ #ff0000}{注:} methods中的函数不应该使用箭头函数,因为箭头函数函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.a 将是 undefined
\color{ #ff0000}{注:}每渲染一次Vue实例都会执行methods中的函数

(4) \color{ #0000FF}{computed}

计算属性computed是用来获取一个复杂逻辑的数据的,如果你想要在页面中显示一个处理过的data对象的值,为了避免反复复制处理过程,你可以将这个过程写入computed对象中,并给他一个set和get方法
{ [key: string]: Function | { get: Function, set: Function } }
var vm = new Vue({
  data: { a: 1 },
  computed: {
    // get only
    aDouble: function () {
      return this.a * 2
    },
    // both get and set
    aPlus: {
      get() {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})
当然你也可以在这里定义一个数据处理的函数,在页面中直接插入这个函数的调用就可以得出你想要展示的处理后的数据了
computed: {
    functionName() {
      console.log("这是个函数")
    }
  }
\color{ #ff0000}{注:}computed是有缓存功能的,如果发现值并没有改变则不会重新计算,只有当他依赖的对象发生改变时才会重新计算

\color{ #0000FF}{computed和methods中函数的区别}

methods:
中定义函数在data中的属性被页面引用且发生改变的时候就会执行,且不管是否methods中的函数是否引用了data中的属性
computed:
中定义的函数只有在其依赖的数据对象发生改变的时候才会执行,而且不像methods是全部调用,只调用引用了变化数据的部分函数,而且在使用computed中定义的函数时,不用加()而是把它当做一个this上的属性去调用

(5) \color{ #0000FF}{watch}

watch是一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。
{ [key: string]: string | Function | Object | Array }
\color{ #ff0000}{注:} 调用$watch()是在实例生成以后,他是一个异步的过程,如果你想在watch之后之后接一个操作,那么这个操作也应该是异步的,这样才能让异步队列先执行watch
new Vue({
  data:{
      n:0
  },
  watch:{
      n(newN,old){
          console.log(`现在执行watch${newN},${old}`)
        //这里watch还会接受两个参数,分别是监听对象变化前后的值
        //这是Vue自动传入的,看你是否需要
      }
  },
  methods:{
      showN(){
          this.n+=1//触发watch回调,但是不立刻执行
          console.log(`showN${this.n}`)
      }
  },
  template:`<div>
  {{n}}
  <button @click="showN">ddd</button>
  </div>`
}).$mount("#app")

当n发生改变的时候就会去调用watch中的回调函数,虽然调用写在showN的log之前,但是执行却是后执行,这就是因为watch回调是异步的,所以如果你想在watch之后执行一个操作就也得使用异步,这样异步队列中watch就会先调用。
\color{ #ff0000}{watch的调用是异步的,且在DOM更新之前}
methods:{
      showN(){
          this.n+=1
          this.$nextTick(()=>{
            console.log(`showN${this.n}`)
          },0)
      }
  }

这里的this.$nextTick()相当于setTimeout,他的作用是将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

wathch和computed都是在数据变化时执行的,不同的是watch是异步的,当你需要一个数据根据他的依赖变化时,可以用computed,当你想在数据变化时做出相应操作时,用watch

插播一个小实验
new Vue({
  data:{
      n:0
  },
  watch:{
      n:{
        handler(newN,old){
          console.log(`现在执行watch${newN},${old}`)
      },
      // immediate:true
      
    }
  },
  created(){
    console.log(this.n)
    this.n=1
  },
  updated(){
    debugger
    this.n=999
    console.log(this.n)
  }
  ,
  methods:{
      showN(){
          this.n+=1//触发watch回调,但是不立刻执行
          console.log(`showN${this.n}`)
      }
  },
  template:`<div>
  {{n}}
  <button @click="showN">ddd</button>
  </div>`
}).$mount("#app")

我们加一个created和updated,也就是创建和更新实例后执行,看看结果

1. 先是created打出n=0,然后马上赋值n=1
2. watch监听到,执行了函数,这时候n=1
3. 点击按钮让n+1,首先让n=2,watch监听到了但是没执行,打印出2
4. watch异步执行,并通知了updated,n更新了
5. updated收到更新,执行函数,把n=999,watch监听到了,还是没执行,打印出999
6. watch异步执行,通知updated,n更新了
7. updated收到更新,执行函数,n=999,watch监听到了,n没变,打印出999

watch中的其他属性

watch:{
      handler:{
          function
      },
      immediate:true|false(default),
      deep:true|false(default)
  }
immediate:在第一次渲染时是否执行,指从无到有
new Vue({
  data:{
      n:0
  },
  watch:{
      n:{
        handler(newN,old){
          console.log(`现在执行watch${newN},${old}`)
      },
      immediate:true
      
    }
  },
  created(){
    console.log(this.n)
  }
  ,
  methods:{
      showN(){
          this.n+=1//触发watch回调,但是不立刻执行
          console.log(`showN${this.n}`)
      }
  },
  template:`<div>
  {{n}}
}).$mount("#app")

deep:监听对象子元素的变化
new Vue({
  data: {
    n: 0,
    obj: {
      a: "a"
    }
  },
  template: `
    <div>
      <button @click="n += 1">n+1</button>
      <button @click="obj.a += 'hi'">obj.a + 'hi'</button>
      <button @click="obj = {a:'a'}">obj = 新对象</button>
    </div>
  `,
  watch: {
    n:function() {
      console.log("n 变了");
    },
    obj() {
      console.log("obj 变了");
    },
    "obj.a": function() {
      console.log("obj.a 变了");
      //子对象的watch语法
    }
  }
}).$mount("#app");
点击第一个

点击第二个,可以看到obj并没有监听到改变

刷新后只点击第三个,可以看到obj.a并没有被判定为改变了,但是原对象地址改变了,所以obj被判定为改变,但是如果我们要监听子对象obj.a的变化呢,就用这个deep属性

 watch: {
    n() {
      console.log("n 变了");
    },
    "obj.a"() {
      console.log("obj.a 变了");
    },
    obj: {
      handler(){
          console.log("obj 变了");
          //这里的handler是固定写法不能改函数名
          //多个用handle2...
      },
      deep:true
    }
  }
点击第二个,obj.n改变,触发了obj的watch事件,这就是deep的作用,加了这个属性我们就不需要分别给每个obj中的属性添加watch了,只需要给父对象添加watch然后加上deep属性为true,这个和深拷贝类似

watch的另一种写法
vm.$watch("name",function(){},{deep:boolean},{immediate:boolean})
\color{ #ff0000}{注:}在watch中定义的handler函数,不能使用箭头函数()=>{},这是因为箭头函数的this指向父级,但是watch没有父级函数,所以他的this是window

2. \color{ #17e97a}{DOM}

(1) \color{ #0000FF}{el}

el代表的是Vue实例的挂载点,你可以在options中配置此属性也可以在实例后面加上一个 $mounted("element")来挂载,如果el选项存在则实例立刻进入编译过程

(2) \color{ #0000FF}{template}

3种写template的方法

1. 直接在HTML中写好Vue语法的内容
2. 在HTML中只写一个
,然后把具体内容写在options中的template属性中,注意这个有id的div会整个被template替换掉
3. 在vue文件中写好template,然后在vm中render()
详见Vue的安装版本及其区别这篇博客
\color{ #ff0000}{注:} template中的部分不是HTML,而是XML格式,需要注意的是XML中的标签严格闭合,Vue中选择使用XML这是出于对编译器语法兼容考虑的
<input name="idName"/>
//HTML中可以省略闭合"/",但是XML不能省略

template中的Vue语法

1. 展示内容

2. v-bind 绑定属性

这里会将自动对x进行计算
3. 绑定事件

4. v-if
<div v-if="x>0">
    x>0
</div>
//条件成立则展示,反之则不展示
<div v-else-if="x===0">
    x=0
</div>
<div v-else="x<0">
    x<0
</div>
5. v-for (value,key) 可以是数组或者对象
<ul>
    <li v-for="(u,index) in users" :key="index">
    索引:{{index}},值:{{u}}
    </li>
</ul>
//注意这里的:key="index"起到一个标志的作用,确保他的唯一性,不能省略

<div v-for="(value, name, index) in object">
  {{ index }}. {{ name }}: {{ value }}
</div>
//第二参数为键名,可同时接受3个参数

你可以用of来代替in,在JS中,for..in用于遍历对象的键名,for..of用于遍历对象的键值

key的额外用法

key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

如果你需要一个元素在依赖变更时重新被替换而不是更改,可以将依赖绑定为key,这样当key改变,vue就会替换成新节点,从而触发他的,生命周期函数,以及过渡效果

6. v-show
<div v-show="n===0">n=0</div>
//等价于
<div :style="{display:n===0? 'block':'none'}">n=0</div>
\color{ #ff0000}{注:}所以v-show相当于一个条件判断的语法糖,他跟if的区别是,v-show只是改变了css,他实际还存在于DOM树中,而if若判断为flase则不渲染进DOM树
7.其他的v-x
7.1. v-model
7.2. v-slot
7.3. v-cloak
7.4. v-once

指令修饰符

指令修饰符是用来给例如v-on,v-bind等添加绑定条件的属性,

1. v-on:它支持两种写法:v-on:.{keycode|alias},keycode是指按键对应的ASCII码,而alias是指按键对应的别名例如enter alt ctrl等

<div v-on:@click.prevent>阻止click的默认点击事件</div>

2. v-bind:{prop|camel|sync}

(1)sync修饰符

在某些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。所以我们可以利用eventbus也就是组件间通信来解决这个问题
//父组件
<template>
  <div class="app">
    App.vue 我现在有 {{total}}
    <hr>
    <Child v-bind:money="total" v-on:update:money="total = $event"/>
    //父组件接监听子组件的update:money事件,用$event来接收到子组件传入的money-100这个参数
    //<Child :money.sync="total"/>就是sync的用法
  </div>
</template>

<script>
import Child from "./Child.vue";
export default {
  data() {
    return { total: 10000 };
  },
  components: { Child: Child }
};
</script>

<style>
.app {
  border: 3px solid red;
  padding: 10px;
}
</style>
//子组件---------------------------------------------------------------
<template>
  <div class="child">
    {{money}}
    <button @click="$emit('update:money', money-100)">
    //触发一个叫update:money的事件,这是官方推荐写法,传给父组件一个参数money-100
      <span>花钱</span>
    </button>
  </div>
</template>

<script>
export default {
  props: ["money"]
};
</script>


<style>
.child {
  border: 3px solid green;
}
</style>
Vue把这种组件间数据变化的同步用sync修饰符简化,如果你有多个外部参数需要传入,sync支持一个文件doc,他会将doc中每一个属性作为单独的prop传入子组件
<text-document v-bind.sync="doc"></text-document>

3. v-model:{lazy|number|trim}

v-model是Vue双向数据绑定的一种实现方式,比如用户在一个输入框中输入,我们需要将输入框中的数据展示在另一个span中该如何实现

<input v-bind:value="inputValue" v-on:input="inputValue = $event.target.value">
  1. 绑定input的value属性
  2. 监听input事件,将值传给inputValue
  3. 然后在另外的地方实时引用这个inputValue

上面这几部我们可以直接简化为,所以本质上这就是一个语法糖

<input v-model="inputValue">

当我们把input写入一个组件中,外部该如何得到input的值呢,需要用到emit来触发input事件,并将value传入

//子组件
<input :value1='inputValue' v-on:input='onInput'>

onInput(){
    this.$emit('input', value)
}

3. \color{ #17e97a}{生命周期钩子}

(1) \color{ #0000FF}{created}

当实例在内存中被创建出来时触发的函数,此时还未渲染进页面

(2) \color{ #0000FF}{mounted}

当实例被渲染到页面中的时候触发的函数

这里需要补充一点created和mounted的区别

当一个父组件中含有子组件的时候,他们的这两个钩子执行顺序是这样的

<row>
<col></col>
<col></col>
<col></col>
</row>

  1. 先执行父组件的created
  2. 再执行子组件的created
  3. 执行子组件的mounted
  4. 执行父组件的mounted

(3) \color{ #0000FF}{updated}

当实例的数据发生改变而被从新渲染时触发的函数

(4) \color{ #0000FF}{destroyed}

当实例在页面中消失时触发的函数

4. \color{ #17e97a}{资源}

(1) \color{ #0000FF}{components 组件}

import components-1 from "./components/components-1.vue"
你可以写好一个vue文件作为组件然后在conponents属性中引入写好的组件
const new Vue({
    conponents:{
    Keyname:components-1
    }
})
又或者可以将组件写在Vue.components
Vue.components(id:"Keyname",definition:{
    template:`<div>....</div>/`
})
还可以将keyname后面的内容替换为组件的内容
const new Vue({
    conponents:{
    Keyname:{
    template:`<div>....</div>/`
        }
    }
})
这里的definition接受的参数和new Vue中的option一样,所以可以认为,这里的Vue.components就是创建一个vue实例并给他一个id便于你去插入他
这样在template中使用这个<Keyname/>标签就可以将组件渲染进页面
\color{ #ff0000}{Tip:} 文件名(xxxx.vue)最好小写防止某些操作系统如win10大小写不分,组件名也就是Keyname最好大写防止和html标签冲突

(2) \color{ #0000FF}{directives}

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令,指令的主要作用是能减少代码的重复以及便于被绑定事件的生命周期管理,还能减少对DOM操作的重复
1. 定义一个全局指令,你可以在任何一个组件中使用v-focus指令
//Vue.directive("fnName",directiveOptions)
Vue.directive('focus', {
  inserted: function (el) {
    el.focus()
  }
})
2. 定义一个实例内局部指令,只能在组件内使用
new Vue({
directives: {
  focus: {
    inserted: function (el) {
      el.focus()
    }
  }
}
})

下面介绍下directiveOption的几个属性(钩子函数)

1. \color{#FF8C00}{bind}
只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
2. \color{#FF8C00}{inserted}
被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
3. \color{#FF8C00}{update}
所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
4. \color{#FF8C00}{componentUpdated}
指令所在组件的 VNode 及其子 VNode 全部更新后调用。
5. \color{#FF8C00}{unbind}
只调用一次,指令与元素解绑时调用。

下面介绍下钩子函数可以接受的参数

1. \color{#FF8C00}{el}
指令所绑定的元素,可以用来直接操作 DOM 。
2. \color{#FF8C00}{vnode}
Vue 编译生成的虚拟节点。
3. \color{#FF8C00}{oldVnode}
上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
4. \color{#FF8C00}{binding}
  1. name:指令名,不包括 v- 前缀。
  2. value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
  3. oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
  4. expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
  5. arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。
  6. modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
下面举个click事件的自定义指令实现的例子
new Vue({
  directives: {
    on2: {
      // bind 可以改为 inserted
      bind(el, binding) {
        el.addEventListener(binding.arg, binding.value);
        // Vue 自带的 v-on 并不是这样实现的,它更复杂,用了事件委托
      },
      unbind(el, binding) {
        el.removeEventListener(binding.arg, binding.value);
      }
    }
  },
  template: `
    <button v-on2:click="hi">点我</button>
  `,
  //这里的click就是binding.arg,hi就是binding.value
  methods: {
    hi() {
      console.log("hi");
    }
  }
}).$mount("#app");

5. \color{ #17e97a}{组合}

(1) \color{ #0000FF}{mixins}

mixins 选项接收一个混入对象的数组。这些混入对象可以像正常的实例对象一样包含实例选项,这些选项将会被合并到最终的选项中,使用的是和 Vue.extend() 一样的选项合并逻辑。mixins本质上就是把组件中methods,data,生命周期函数相同的部分抽离成一个对象,减少代码重复
\color{ #ff0000}{注:}
数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
具体见Vue.js
var mixin = {
  created: function () { console.log(1) }
  //这里可以包含多个生命周期函数
}
//这里也可以写在另一个js文件中,然后导出
var vm = new Vue({
//import mixin from "path"
  created: function () { console.log(2) },
  mixins: [mixin]
  //mixins是一个[{obj},{obj2}....]
})
// => 1
// => 2

(2) \color{ #0000FF}{extends}

extends属性可以试一个函数或者一个对象
extends的功能与mixins差不多,也是从一个组件上继承某些选项,比如mixins的选项太多,那么我可以声明一个新的Vue2=Vue.extend({obj})
//Vue2.js
import mix1 from "path1"
import Vue from "vue"
const Vue2=Vue.extend({
    mixins:[mix1,mix2....]
})
export default Vue2
//main.js
import Vu2 from "Vue2.js"
new Vue2({
    {options}
})
//这样Vue2实例就会继承Vue和mixins中的内容
//子组件中引入
<script>
import Vue2 from "Vue2.js"
export default {
data(){
    return {
        ...
    }
},
extends:{
    Vue2
    }
}
</script>

(3) \color{ #0000FF}{provide/inject}

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
provide:Object | () => Object
inject:Array<string> | { [key: string]: string | Symbol | Object }
provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。
inject 选项一个字符串数组,或一个对象
\color{ #ff0000}{注:}provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
<script>
export default {
  name: "App",
  provide() {
    return {
      themeName: this.themeName,
      fontSizeNmae: this.fontSizeName,
      changeTheme: this.changeTheme, 
      changeFontSize: this.changeFontSize
    };
  },
  //父级元素将属性或者方法写入provide等待被引用
  data() {
    return {
      themeName: "blue", // 'red'
      //如果在子组件中直接改themeName="red",是行不通的因为字符串不能重复定义
      //但是若themeName:{value:"blue"}就可以改themeName.value
    };
  },
  methods: {
    changeTheme() {
      if (this.themeName === "blue") {
        this.themeName = "red";
      } else {
        this.themeName = "blue";
      }
    },
    changeFontSize(size) {
      if (["normal", "big", "small"].indexOf(size) === -1) {
        throw new Error(`wront size: ${size}`);
      }
      this.fontSizeName = size;
    }
  }
};
</script>
<template>
  <div>
    <button @click="changeTheme">换肤</button>
    //这里其实可以不用引用父组件提供的changeTheme而自己在methods中定义一个函数
    //但是要注意父组件的data问题
    <button @click="changeFontSize('big')">大字</button>
    <button @click="changeFontSize('small')">小字</button>
    <button @click="changeFontSize('normal')">正常字</button>
  </div>
</template>
<script>
export default {
  inject: ["themeName", "changeTheme", "changeFontSize"]
  //子组件将需要引用的属性写入inject,这样当子组件调用这里的属性时会
  //在父组件中读取或者调用函数
};
</script>

6. \color{ #17e97a}{其他}

(1) \color{ #0000FF}{name}

只在组件中其作用,用来给一个组件命名,方便调试和报错提醒,同时能允许组件递归的调用自己

<=====To be continued