Vue 核心 API 学习

Code 代码
2020年2月8日 ~

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

大前端进阶中

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.