Vue 核心 API 学习

Code 代码
2月 08, 2020 ~

Demo 地址:Vue 进阶学习

1. Vue 实例

实例:对某个类,通过 new ClassA() 初始化而来的对象,叫做实例。对象初始化参考

初始化对象的几种方式

const app = new Vue({
  el: '#root',
  template: '<div>This is content</div>'
});

通过 new Vue(options) 初始化的 Vue 实例,最终会通过 render function 将 template 的内容挂载到 #root 根节点上,在页面中展示,而且挂载的方式是使用 template render 生成的新节点,替换掉 指定的 #root 节点,正式这个原因,如果在 template 中存在多个根节点,Vue 会报一个警告:

注:如果 options 传入了 template,则将 template 编译到 render 函数中去,否则会将 el 外部的 html 作为 template 进行编译

[Vue warn]: Error compiling template:
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.

因为 Vue 不知道用哪个节点替换指定的根节点。

不仅仅可以通过 options.el 指定要挂载的根节点,还可以手动进行挂载:

app.$mount('#root');

在 webpack 配置中,通过插件,可以指定以某个文件为模板,生成最终的 html 页面:

  new HTMLPlugin({
    template: path.join(__dirname, 'index.html')
  });

初始化 Vue 传入的 options 会与默认的初始化参数结合起来,最终产生 Vue 实例,而且 options 中的属性可以通过 app.$[attr] 访问,如:

app.$elapp.$rootapp.$optionsapp.$dataapp.$propsapp.$childrenapp.$refsapp.$slots 等等

  • app.$el 是对最终挂载生成 html 节点的引用

  • app.$root 是整个 Vue 树状结构的根节点,也是 Vue 实例,是对 app 的引用,即:app.$root === app

  • app.$options 是初始化 Vue 实例所有的参数,$options 中也可以看到诸如 datapropswatch 等属性,但不是对 app.$[attr] 的引用,所以如果更改 app.$options.data 的值,页面是不会响应式变化的,直接更改 app.$data 才有效

  • app.$data

定义在 data 对象中的属性,Vue 会复制一份引用到 app 层,即 app.[attr]app.$data[attr] 访问到的是同一份地址,

定义在打 data 中的全局属性,如果不对属性进行字面量声明,则在 Vue 初始化完成之后,给 data 对象增加属性,或修改属性值,Vue 不会对这个对象做响应式处理,所以即使该对象的属性值变化了,也不会响应式地重新渲染

但可以通过 $set 方法给在初始化完成之后再给 data 增加或修改属性,而且也是响应式的

  app.$set(app.obj, 'a', 'foo');

另外,也删除某个属性,同时会解除对该属性的响应式处理

  app.$delete(app.obj, 'a');

强制组件重新渲染一次,也可以在没有字面量定义 data 属性时,页面重新渲染(尽量不使用这个方法)

  app.$forceUpdate();
  • app.$refs 用于快速定位到模板的某个 html 节点,或某个组件实例

  • app.$isServer 用于服务端渲染判断

  • app.$watch

一下写法和在 options 中定义 watch 都可以监听到 foo 的变化:

  const unWatch = app.$watch('foo', (newVal, oldVal) => {
    console.log('newVal: ', newVal);
    console.log('oldVal: ', oldVal);
  })

区别在于,通过 app.$watch 定义,需要手动进行销毁(比如路由跳转后),该方法回调一个 unWatch 方法,用于手动回收:

  unWatch();

而定义在 options 中,Vue 会自动回收销毁

  • app.$on app.$emit app.$once

app.$onapp.$emit 都只能同时作用于一个 Vue 对象,才会生效,Vue 对象 app 监听了 test 事件,那么必须由对象 app 自己触发事件 test,才会被监听到,而且不会像 dom 事件一样冒泡

  app.$on('test', (a, b) => {
    console.log('$on test emitted: ', a, b);
  });
  app.$emit('test', 1, 2);

app.$once 只会触发一次

  app.$once('test', (a, b) => {
    console.log('$once test emitted', a, b);
  })

2. Vue 的生命周期方法

vue-lifecycle

  • beforeCreatecreated

beforeCreate、created 方法在 new Vue(options) 的过程中一定会被执行,而且 beforeCreate、created 两个生命周期中都不能进行 dom 操作,因为这时 Vue 还没有生成最终的根节点,一般操作 dom 相关的动作,要放在 mounted 中。操作数据相关的动作一般放在 created 中,但不要放在 beforeCreate 中,因为 beforeCreate 还没有进行数据的 reactive 响应式相关的初始化

  • beforeMountmounted

mounted 之后会把当前 Vue 生成的 html 挂载到 dom 上,即:把生成的 html 把 <div id="root"></div> 替换掉。如果在 options 中不指定 el,则不会执行 beforeMount、mounted。在 mounted 之后,所有生命周期中拿到的根节点,都是 mounted 之后产生的节点

beforeCreate、created、beforeMount、mounted 在整个组件生命周期中只会被调用一次,而且 beforeMount、mounted 在服务端渲染中不会被调用,因为这两个生命周期都和操作 dom 有关

  • beforeUpdateupdated

只要响应式处理过的数据有变化,页面就会响应重新渲染,这两个方法也都会被重复调用

  • activateddeactivated

与组件的 keep-alive 相关

  • beforeDestroydestroyed

beforeDestroy 中会解除所有事件监听以及所有的 watch 监听

  • render

render 方法第一次将在 beforeMount 和 mounted 之间执行,之后都将在 beforeUpdate 和 updated 之间执行

  • renderError

只有在开发环境才会触发,并且只能用于当前组件,它的子组件的错误无法捕获到

  • errorCaptured

不管是开发环境还是正式环境,只要捕获到当前组件,或者它的子组件渲染发生错误时,都会触发,除非子组件阻止了事件冒泡

3. Vue 的数据绑定

import Vue from 'vue';

var globalVar = '111'; // eslint-disable-line

new Vue({
  el: '#root',
  template: `
    <div :id="id" :class="activeClass" @click="handleClick">
      <div :class="{active: isActive}">
        模板中只能做用一行语句就能有结果的表达式
        {{isActive ? "active" : "not active"}}
      </div>
      <div :class="[{active: isActive}, inactiveClass]">
        模板中可以访问 Vue 默认的全局变量白名单,但自己定义的全局变量不能访问(如 globalVar)
        {{Date.now()}}
      </div>
      <div :style="[activeStyle, inactiveStyle]">
        对 data 中定义的 HTML,Vue 会自动进行转移成纯的字符串,为了防止注入攻击
        要以 v-html 方式使用
        <div v-html="html"></div>
        <div>{{getJoinedArr(arr)}}</div>
      </div>
    </div>
  `,
  data: {
    id: 'aaa',
    activeClass: 'active',
    inactiveClass: 'inactive',
    activeStyle: {
      color: 'red',
      // Vue 会给需要加前缀的样式属性名自动加上前缀
      // 该属性用于消除浏览默认样式
      appearance: 'none'
    },
    inactiveStyle: {
      // html 中所写样式的 '-' 分隔符格式转化成驼峰格式
      fontSize: '15px'
    },
    isActive: true,
    arr: [1, 2, 3],
    html: '<span>123</span>'
  },
  methods: {
    handleClick () {
      console.log('##### handleClick #####');
    },
    // 建议这种情况使用 computed,
    // 因为 computed 会先判断数据源是否有变化,再来渲染页面,效率会更高
    getJoinedArr (arr = []) {
      return arr.join('、');
    }
  }
});

4. computed 和 watch

  • computed:

定义在 computed 中的方法,可以像访问变量的属性一样去调用,这是因为 Vue 会对 fullName 声明 getset 方法,并且会对计算的结果做缓存,只有当 computed 中所依赖的响应式变量(即:firstName 或 lastName)发生变化,才会重新计算。而如果是在 methods 中做同样的逻辑计算,那么只要 data 或者 props 中任何一个属性有变化,都会重新渲染页面,从而重新调用 methods 中的方法。所有定义在 computed 中性能消耗会更小,特别是计算的逻辑很复杂的情况

  const computed = {
     fullName () {
       return `${this.firstName} ${this.lastName}`;
     }
  };

也可以通过显示地定义 getset 方法来定义 computed

get: 获取 fullName 的值时会做哪些计算
set: 给 fullName 赋值时会做哪些计算。不建议使用 set,因为将多个值通过计算组装成一个值很简单,但把一个值拆解成多个值是很麻烦的,而且很容易出问题,造成死循环

  const computed = {
     fullName: {
       get () {
         return `${this.firstName} ${this.lastName}`;
       },
       set (val) {
         const names = val.split(' ');
         this.firstName = names[0];
         this.lastName = names[1];
       }
     }
  };
  • watch:

watch 中可以对 data props computed 中的响应式变量进行监听,只要其发生变化,就执行一些逻辑处理

只有当 data.age 发生变化才会执行 age 方法中的逻辑

  const watch = {
     age (newVal, oldVal) {
       this.notYoung = newVal >= 30;
     }
  }

也可以通过以下方式定义 watch

immediate:表示是否立即执行

作用:如果 age 的初始值本身已经大于 30,那么按照上面👆的形式定义 watch,第一次 handler 方法是不会执行的,只有当 age 再次发生变化才会执行,而 immediate 就是用来解决这个问题的

  const watch = {
     age: {
       handler (newVal, oldVal) {
         this.notYoung = newVal >= 30;
       },
       immediate: true
     }
  };

deep:深度观察

作用:如果 other 是一个对象,那么只是修改 other 中属性的值,而不是直接给 other 重新赋值,那么按照上面👆的形式定义 watch,handle 方法也是不会执行的。但如果设置 deep: true,那么 other 不管哪个属性变化,handle 方法都会执行

原理:Vue 会逐层遍历 other 所有的属性,并为每个属性增加一个响应式监听,所有只要 other 任何层级的任何属性发生变化,都会执行 handle 方法

  const watch = {
     other: {
       handler (newVal, oldVal) {
         console.log('other changed');
       },
       deep: true
     }
  };

但这种写法对性能开销较大,建议使用以下👇写法

  const watch = {
     'other.hobby' () {
       console.log('other.hobby changed');
     }
  };

5. Vue 的原生指令

  import Vue from 'vue';
  
  new Vue({
    el: '#root',
    template: `
      <div>
        <div>{{text}}</div>
        <div v-text="text">aaa</div>
        <div v-html="html">aaa</div>
        <div>类似于 dom 的 innerText 和 innerHtml</div>
        <br/>
        <div v-pre>将内容当做纯文本显示: {{text}}</div>
        <br/>
        <div v-once="text">
          <div>数据绑定的内容只执行一次,之后数据变化后也不会随之变化,</div>
          <div>用处:展示静态内容时,通过声明 v-once 减少性能开销,</div>
          <div>v-once 中的所有节点,Vue 都不会将其和虚拟 dom 进行检测对比,从而减少重新渲染的开销。</div>
        </div>
        <br/>
        <div v-show="active">根据 show 的值,给 div 增加 display 的样式</div>
        <div v-if="active">根据 show 的值,决定是否把 div 添加到 dom 流中</div>
        <div v-else>else content</div>
        <div>如果只是单纯想控制元素的显示和隐藏,那么最好使用 v-show,</div>
        <div>因为 v-if 会对 dom 节点进行增删操作,导致重绘和重新排版,有性能的影响。</div>
        <br/>
        <div>v-for 中的 key 是用来做数据缓存的,需要保证唯一,</div>
        <div>当数据源发生变化时,Vue 会根据每个 item 的 key 在缓存中寻找,是否已经存在 key,</div>
        <div>如果已经存在,则直接在缓存中复用 item 的 dom 节点,而不重新创建新的 dom 节点,提高渲染性能。</div>
        <div>注:不要用 idx 作为 key,因为数组元素的顺序和具体值没有什么直接关系,</div>
        <div>用 idx 作为 key,在数据源发生增、删之后,可能会导致产生错误的缓存。</div>
        <ul>
          <li v-for="(item, idx) in arr" :key="item">index: {{idx}}, value: {{item}}</li>
        </ul>
        <ul>
          <li v-for="(val, key, idx) in obj">key: {{key}}, value: {{val}}, index: {{idx}}</li>
        </ul>
        <br/>
        <div v-on:click="divClicked">
          <div>v-on 做的事情是:</div>
          <div>如果 v-on 加在普通的 dom 节点元素上,则会通过 document.addEventListener 给该节点增加事件监听</div>
          <div>如果 v-on 加在 Vue 组件上,实际上是在 Vue 对象实例上绑定一个事件</div>
        </div>
        <br/>
        <div>
          <input type="text" v-model="text">
          <input type="text" v-model.number="number">
          <input type="text" v-model.trim="text">
          <input type="text" v-model.lazy="text">
          <input type="checkbox" v-model="active">
        </div>
        <br/>
        <div>
          <div>每个 checkbox 所绑定的值是固定的,checkbox 的 active/inactive 变化后:</div>
          <div>active -> inactive: 数据移除值为当前 checkbox 所绑定值的元素</div>
          <div>inactive -> active: 数据 push 一个值为当前 checkbox 所绑定值的元素</div>
          <input type="checkbox" value="a" v-model="arr">
          <input type="checkbox" value="b" v-model="arr">
          <input type="checkbox" value="c" v-model="arr">
        </div>
        <br/>
        <div>
          <input type="radio" value="one" v-model="picked">
          <input type="radio" value="two" v-model="picked">
        </div>
      </div>
    `,
    data: {
      text: 'text',
      number: 0,
      html: '<span>html</span>',
      active: true,
      arr: ['a', 'b', 'c'],
      obj: {
        a: 'a',
        b: 'b',
        c: 'c'
      },
      picked: 'one'
    },
    watch: {
      text () {
        console.log('##### text changed #####', this.text);
      },
      arr () {
        console.log('##### arr changed #####', this.arr);
      },
      picked () {
        console.log('##### arr picked #####', this.picked);
      }
    },
    methods: {
      divClicked () {
        console.log('##### divClicked #####');
      }
    }
  });

6. Vue 组件

6.1 组件的定义
  • 组件的全局注册和局部注册
  const component = {
    template: `<div>This is a component</div>`
  };

  // 全局注册:
  Vue.component('CompOne', component); // 组件名称命名规则:大驼峰命名法(因为 component 本身也是一个 Vue 的 class)
  
  // 局部注册
  new Vue({
    component: {CompOne: component},
    template: `<comp-one></comp-one>` // 组件使用命名规则:全小写 '-' 分割
  });
  • 组件 data 定义:

不是通过 new Vue({options}) 创建的组件,data 必须以 function 形式返回,否则当一个父组件有多个相同类型的子组件时,所有的子组件都会引用同一份数据源,并且 Vue 会报警告:

[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.

  const component = {
      template: `<div>This is a component</div>`,
      data () {
        return {
          a: 'foo'
        }
      }
    };
  • 组件 props 定义:
  const component = {
      template: `<div @click="handleChange">This is a component</div>`,
      props: {
        active: {
          type: Boolean, // 类型
          required: true, // 是否必填
          default: true, // 默认值
          validator (val) { // 自定义校验
            return typeof val === 'boolean'
          }
        },
        propOne: Number, // 属性命名规则:小驼峰命名法
        // 如果是对象,则必须以 function 形式返回一个对象
        // 原因和不是通过 new Vue() 创建的组件,data 必须以 function 形式返回一样
        obj () {
          return {
            default: {a: 'foo'}
          }
        }
      },
      // 也可以通过数组定义 props,但相对不严谨
      // props: ['active', 'propOne'],
      // mounted () {
      //   this.propOne = 1;
      // },
      methods: {
        handleChange () {
          this.$emit('change')
        }
      }
    };

Vue 不推荐在子组件中更改 props 的值,如果直接更改,会报警告:

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "propOne"

如果要更改,可以通过子组件 $emit 事件,回调到父组件进行更改

  new Vue({
    el: '#root',
    components: {
      Comp: component
    },
    template: `
      <div>
        // 传递 props 时,使用全小写 '-' 分割
        <comp :active="true" :prop-one="propA" @change="handleChange"></comp>
      </div>
    `,
    data: {
      propA: 1
    },
    methods: {
      handleChange () {
        this.propA += 1;
      }
    }
  });
6.2 组件的继承
  const component = {
    template: `
      <div>
        <input type="text" v-model="text">
        <span v-show="active">show if active</span>
        <span @click="handleChange">{{propOne}}</span>
      </div>
    `,
    props: {
      active: Boolean,
      propOne: String
    },
    data () {
      return {
        text: 0
      };
    },
    mounted () {
      console.log('##### component mounted #####');
    },
    methods: {
      handleChange () {
        this.$emit('change');
      }
    }
  };

以上 component 配置只是一个普通的 object,如何让配置生成 Vue 对象,除了通过 new Vue({component: {Comp: component}}) 以外,还可以对 Vue 进行继承,生成 Vue 对象:

  import Vue from 'vue';

  const CompVue = Vue.extend(component);
  
  new CompVue({
    el: '#root',
    // 通过 props 无法将外部属性传入
    // props: {
    //   propOne: 'props 1'
    // },
    // 需要通过 propsData 传入
    propsData: {
      propOne: 'propsData 1'
    },
    // 可以传入 data 与 CompVue 默认的 data 合并覆盖
    data () {
      return {
        text: 3
      };
    },
    // 生命周期方法执行顺序:先执行 CompVue 默认配置中的生命周期方法,再执行 CompVue 实例中的生命周期方法
    mounted () {
      console.log('##### instance mounted #####');
    }
  });

CompVue 是 Vue 的一个子类,通过 new Vue(options) 初始化而来 Vue 对象,是默认内置配置下生成的,没有 data、props 等相关配置,通过 new CompVue() 生成的 Vue 对象会默认带有 data、props、methods 配置

也可以通过以下这种方式实现对 component 的继承:

  import Vue from 'vue';

  const component2 = {
    extends: component,
    data () {
      return {
        text: 1
      };
    },
    mounted () {
      console.log('##### instance mounted #####');
    }
  };
  
  new Vue({
    el: '#root',
    components: {
      Comp: component2
    },
    template: `<comp prop-one="123"></comp>`
  });

使用场景:当我们开发好一个组件,可能比较公用,很多项目都会用到,而且功能描述比较泛,使用起来需要传入很多的配置项,而在某个具体项目中,很多参数都使用它的默认值,不需要传入,或者需要在原有基础上扩展一些特定的属性,这时候就可以通过 extend 的方式,在原有组件基础上,继承扩展出我们需要的组件,而不需要从头开始写一个新的组件

6.3 组件自定义绑定
  import Vue from 'vue';
  
  // 通过 v-model 实现父子组件的双向绑定
  const component = {
    template: `
      <div>
        <input type="text" :value="value1" @input="handleInput">
      </div>
    `,
    model: {
      prop: 'value1', // 指定双向绑定 prop 的 key
      event: 'change' // 指定回调时间的方法名
    },
    props: ['value', 'value1'],
    methods: {
      handleInput (event) {
        // this.$emit('input', event.target.value);
        this.$emit('change', event.target.value);
      }
    }
  };
  
  new Vue({
    el: '#root',
    components: {
      Comp: component
    },
    // template: `<comp :value="value" @input="value = arguments[0]"></comp>`,
    template: `<comp v-model="value"></comp>`,
    data () {
      return {
        value: '123'
      };
    }
  });
6.4 组件高级属性
  • slot 和 slot-scope

在子组件中可以通过指定不同插槽名,定义多个插槽:

  import Vue from 'vue';
  
  const component = {
    template: `
      <div :style="style">
        <div class="header">
          <slot name="header"></slot>
        </div>
        <div class="body">
          <slot name="body"></slot>
        </div>
        <div class="footer">
          <slot name="footer"></slot>
        </div>
      </div>
    `,
    data () {
      return {
        style: {
          width: '200px',
          height: '200px',
          border: '1px solid #aaa'
        }
      };
    }
  };

在父组件中根据插槽名传入不同的插槽:

  import Vue from 'vue';

  new Vue({
      el: '#root',
      components: {
        CompOne: component
      },
      template: `
        <div>
          <comp-one>
            <div slot="header">slot header</div>
            <div slot="body">slot body</div>
            <div slot="footer">slot footer</div>
          </comp-one>
        </div>
      `
    });

带作用域的插槽:在子组件插槽中定义内部属性,父组件通过 slot-scop 可以访问到子组件的属性:

  import Vue from 'vue';
  
  const component = {
    template: `
      <div :style="style">
        <slot :slotValue1="slotProp" :slotValue2="slotData"></slot>
      </div>
    `,
    props: {
      slotProp: String
    },
    data () {
      return {
        style: {
          width: '200px',
          height: '200px',
          border: '1px solid #aaa'
        },
        slotData: 'abc'
      };
    }
  };

通过指定 "props" 作为 key,将插槽中的属性包装成 object,这样父组件就可以通过 props[key] 来访问插槽中的属性

  import Vue from 'vue';

  new Vue({
    el: '#root',
    components: {
      CompOne: component
    },
    template: `
      <div>
        <comp-one :slot-prop="value1">
          <div slot-scope="props">
            <div>slotValue1: {{props.slotValue1}}</div>
            <div>slotValue2: {{props.slotValue2}}</div>
            <div>selfValue: {{value2}}</div>
          </div>
        </comp-one>
      </div>
    `,
    data () {
      return {
        value1: 'def',
        value2: 'ijk'
      };
    }
  });
  • provide 和 inject

对于跨多级的组件中,父组件和子组件的参数传递,可以通过在父组件中定义 provide,子组件中定义 inject 来实现

孙组件:

  const GrandsonComponent = {
    template: '<div>child component</div>',
    inject: ['const', 'grandparent', 'value'],
    mounted () {
      console.log('##### const #####', this.const); // xyz
      console.log('##### grandparent #####', this.grandparent); // undefined
      console.log('##### value #####', this.value); // 在父组件中报错
    }
  };

子组件:

  const ChildComponent = {
    components: {
      GrandsonComponent
    },
    template: `
      <grandson-component></grandson-component>
    `
  };

父组件:

通过 provide: {} 的方式定义 provide,在子孙组件中是拿不到父组件的 this 的,只能接收到传入的常量,因为这样初始化 provide 对象,其实 Vue 本身还没有初始化完成,所有访问不了 this

  import Vue from 'vue';

  new Vue({
    el: '#root',
    components: {
      ChildComponent
    },
    provide: {
      const: 'xyz',
      grandparent: this,
      value: this.value // 报错
    },
    template: `
      <child-component></child-component>
    `,
    data () {
      return {
        value: 'abc'
      };
    }
  });

孙组件:

  const GrandsonComponent = {
    template: '<div>child component: {{value}}</div>',
    inject: ['const', 'grandparent', 'value'],
    mounted () {
      console.log('##### const #####', this.const); // xyz
      console.log('##### grandparent #####', this.grandparent); // 父组件 Vue 实例
      console.log('##### value #####', this.value); // abc
    }
  };

父组件:

通过方法返回定义 provide,才子孙组件中接收到父组件的 this,但是父组件中的响应式变量,不会在子孙组件中响应式地变化

  import Vue from 'vue';

  new Vue({
    el: '#root',
    components: {
      ChildComponent
    },
    provide () {
      return {
        const: 'xyz',
        grandparent: this,
        value: this.value
      };
    },
    template: `
      <child-component></child-component>
    `,
    data () {
      return {
        value: 'abc'
      };
    }
  });

孙组件:

  const GrandsonComponent = {
    template: '<div>child component: {{value}}</div>',
    inject: ['const', 'grandparent', 'data'],
    mounted () {
      console.log('##### const #####', this.const); // xyz
      console.log('##### grandparent #####', this.grandparent); // 父组件 Vue 实例
      console.log('##### value #####', this.data); // {value: 父组件的 value}
    }
  };

父组件:

只有在 provide 中重定义 get() 方法,让子孙组件每次获取到的 value 都是其最新值,从而实现响应式。这也是 Vue 实现响应式的最基本原理

  import Vue from 'vue';

  new Vue({
    el: '#root',
    components: {
      ChildComponent
    },
    provide () {
      const data = {};
      Object.defineProperty(data, 'value', {
        get: () => this.value,
        enumerable: true
      });
      return {
        const: 'xyz',
        grandparent: this,
        data
      };
    },
    template: `
      <child-component></child-component>
      <input type="text" v-model="value"/>
    `,
    data () {
      return {
        value: 'abc'
      };
    }
  });
6.5 组件 render function

标签

Henry

大前端进阶中