1. VUE 核心技术 

node_modules 下 vue 有多个版本,因为 vue 提供不同环境的支持

import vue 默认使用 vue.runtime.esm.js

vue.runtime.esm.js; // 开发环境默认

vue.runtime.min.js; // 正式环境

runtime 和没有 runtime 的区别在于是否可以在 vue 对象里写 template,runtime 文件不能写入模板

resolve: {
  alias: {
    'vue': path.join(__dirname, '../node_modules/vue/dist/vue.esm.js') // 指定使用哪个vue文件
  }
}
"rules": {
  "no-new": "off" // eslint默认不允许直接使用new
}

1.1 vue 实例

  • VUE 实例的创建和作用
  • VUE 实例的属性
  • VUE 实例的方法

1.1.1 VUE 实例的创建和作用

创建实例

import Vue from 'vue';

new Vue({
  el: '#root',
  template: '<div>hello world</div>'
});

另外一种方式

import Vue from 'vue'

const app = new Vue({
  template: '<div>hello world</div>'
})
app.$mount('#root)

绑定数据方式

import Vue from 'vue';

const app = new Vue({
  template: '<div>{{text}}</div>',
  data: {
    text: 0
  }
});
app.$mount('#root');

setInterval(() => {
  app.text += 1;
}, 500);

1.1.2 vue 实例属性

app.$data;
app.$props;
app.$el;
app.$options;
app.$options.render = h => {
  return h('div', {}, 'test');
};
app.$root === app; // true,每个节点都可以调用,返回的都是顶层的root
app.$children;
app.$slots; // 插槽
app.$scopedSlots;
app.$refs; // 快速定位节点,返回html节点,组件会返回组件实例
app.$isServer; // 服务端渲染判断

1.1.3 vue 实例方法

监听变化

app.$watch('text', (newText, oldText) => {
  // 监听新值和旧值
  console.log(`${newText}, ${oldText}`);
});
// 这种写法需要手动去注销值
const unWatch = app.$watch('text', (newText, oldText) => {
  // 监听新值和旧值
  console.log(`${newText}, ${oldText}`);
});
setTimeout(() => {
  unWatch();
}, 2000);
// 常用写法,这种方式会自动注销
const app = new Vue({
  template: '<div>{{text}}</div>',
  data: {
    text: 0
  },
  watch: {
    text(newText, oldText){}
  }
})

事件监听

app.$on('test', (a, b) => {
  // 作用域于一个对象才会生效,不会冒泡
  console.log(`${a},${b}`); // 1 2
});
app, $emit('test', 1, 2); // 可以传递参数
app.$once('test', () => {}); // 只触发一次
app, $emit('test');

强制重新渲染组件

vue 中,如果没有给 data 中的属性赋值,那这个属性就是非响应式的,不会引起 vue 进行一个重新渲染的过程

const app = new Vue({
  template: '<div>{{text}}{{obj.a}}</div>',
  data: {
    text: 0,
    obj: {}
  }
}).$mount('#root')

let i = 0
setInterval(() => {
  i++
  app.obj.a = i
}, 1000)

此时发现 obj 并没有渲染出来,但是值在变化

let i = 0;
setInterval(() => {
  i++;
  app.obj.a = i;
  app.$forceUpdate(); // 强制重新渲染组件
}, 1000);

但是不建议这么做,可以提前声明一个默认值。还有另外一种方法

let i = 0;
setInterval(() => {
  i++;
  app.obj.a = i;
  app.$set(app.obj, 'a', i); // 相当于补上一个响应式属性
  app.$delete; // 对应的删除方法
}, 1000);

vue 的渲染过程是异步的,在改了属性并不是立刻会渲染刷新掉,会有一个异步队列,如果连续改了多次,它会一次性渲染,整个过程异步渲染。如果想在值渲染结束后对它进行修改,可以使用 app.$nextTick(callback)

app.$nextTick(callback)在下一次更新后才会传入 callback

1.2 VUE 的生命周期

import Vue from 'vue';

new Vue({
  el: '#root',
  template: '<div>{{text}}</div>',
  data: {
    text: 0
  },
  beforeCreate() {
    console.log(this, 'beforeCreate');
  },
  created() {
    console.log(this, 'created');
  },
  beforeMount() {
    // 组件被渲染前执行
    console.log(this, 'beforeMount');
  },
  mounted() {
    // 组件被渲染执行
    console.log(this, 'mounted');
  },
  beforeUpdate() {
    // 数据更新前才会执行
    console.log(this, 'beforeUpdate');
  },
  updated() {
    // 数据更新才会执行
    console.log(this, 'updated');
  },
  activated() {
    // keep-alive触发
    console.log(this, 'activated');
  },
  deactivated() {
    console.log(this, 'deactivated');
  },
  beforeDestroy() {
    console.log(this, 'beforeDestroy');
  },
  destroyed() {
    console.log(this, 'destroyed');
  }
});

setTimeout(() => {
  // app.$destroy() // 销毁组件
}, 1000);

// beforeCreate
// created
// beforeMount
// mounted
// el: '#root', 注释掉这句,发现下面这两句无论如何都会执行
// beforeCreate
// created

由此可以发现,mount 就是把组件生成的 html 内容挂载到 dom 上的过程

beforeCreate () { // 事件已经ok,响应式数据还没有生成,所以不要修改data里数据,如果ajax异步请求数据,最早也要在create里去做
  console.log(this.$el, 'beforeCreate') //undefined
},
created () {
  console.log(this.$el, 'created') //undefined
},
beforeMount () { // 渲染初始化,之后判断是否有template,有的话会解析成 render(h) 函数,之后进行到mounted
  console.log(this.$el, 'beforeMount') //<div id="root"></div>
},
mounted () {
  console.log(this.$el, 'mounted') //<div>0</div>
},

在服务端渲染的时候只会调用 beforeCreate 和 created。因为 mount 渲染是跟 dom 有关,服务端渲染没有 dom 执行的环境

vue 内部还有 renderError()函数。只会在开发环境使用

render(h) {
  throw new TypeError('render error')
}
renderError(h, err){
  return h('div', {}, err.stack) // render函数出错会调用
}
errorCaptured(){ // 收集线上错误,如果在根组件定义,会捕捉所有子组件错误
  // 会向上冒泡,并且正式环境可以使用
}

1.3 VUE 数据绑定

import Vue from 'vue';

new Vue({
  el: '#root',
  template: `
    <div v-bind:id="box" v-on:click="handleClick">
      <p v-html="html"></p>
    </div>
  `,
  data: {
    text: 'aaa',
    html: `<span>haha</span>`,
    box: 'content'
  },
  methods: {
    handleClick() {
      console.log('click');
    }
  }
});

// <div id="content"><p><span>haha</span></p></div>
v-html // 绑定html节点要使用v-html,防止注入攻击。

v-bind // 绑定动态数据。

v-on // 绑定事件。可以简写成 @click

:class // 动态绑定class

动态绑定 class

template: `
  <div :class="{ active: isActive }">
    <p v-html="html"></p>
  </div>
`,
data: {
  isActive: false
}

// 另一种写法
template: `
  <div :class="[isActive ? 'active' : '']">
    <p v-html="html"></p>
  </div>
`,
data: {
  isActive: false
}

// 第三种写法:对象方式
template: `
  <div :class="[{ active: isActive }]">
    <p v-html="html"></p>
  </div>
`,
data: {
  isActive: false
}

:style 样式绑定

template: `
  <div
    :class="[{ active: isActive }]"
    :style="[styles1, styles2]"
  >
    <p v-html="html"></p>
  </div>
`,
data: {
  styles1: {
    color: 'red'
  },
  styles2: {
    font-size: 16px
  }
}

1.4 computed 计算属性

import Vue from 'vue';

new Vue({
  el: '#root',
  template: `
    <div>
      <p>Name:{{name}}</p>
      <p>Name:{{getName()}}</p>
      <p><input type="text" v-model="number" />></p>
    </div>
  `,
  data: {
    firstName: 'liu',
    lastName: 'sixin',
    number: 0
  },
  computed: {
    name() {
      console.log('name');
      return `${this.firstName} ${this.lastName}`;
    }
  },
  methods: {
    getName() {
      console.log('getName');
      return `${this.firstName} ${this.lastName}`;
    }
  }
});

输入框  每次改变会重新渲染页面,同时会看到 getName 会多次调用,而 computed 里面则会缓存值

computed: {
  name: {
    get(){},
    set(name){}
  }
}

尽量不要使用 set,会导致项目逻辑变得很复杂

1.5 watch

有点类似于 computed

第一次绑定不会执行,只有数据变化才会, 如果需要首次就调用

watch: {
  firstName: {
    handler(newName, oldName){
      this.fullName = newName + ' ' + this.lastName
    },
    immediate: true, // 首次就执行
    deep: true // 深度监听,对象的属性变化也会被监听,性能开销大
  }
}

watch 有个明显劣势就是监听多个东西就要加多个 handler,并不适用于显示某个数据,用 computed 更好。

watch 更多用于监听到数据变化向后台发请求,computed 做不到。具体就是监听到某个数据变化要做某个操作的时候用 watch。

deep 对性能开销比较大,可以修改成字符串形式

watch: {
  'obj.a': {
    handler(){
      console.log('a is changed')
    },
    immediate: true
  }
}

重点:不要在 computed 和 watch 里修改原来的属性,最好是生成新的**

1.6 VUE 原生指令

v-text  // 标签里要显示的内容,内容多的话使用数据绑定

v-html

v-show  // 相当于display:none

v-if  // 会增删节点,影响重绘
v-else-if
v-else // 对应v-if

v-for
<li v-for="(item, index) in arr" :key="item">{{item}}</li>
<li v-for="(val, key, index) in obj">{{val}}:{{key}}{{index}}</li>
data:{
  arr: [1,2,3],
  obj: {
    a: 123,
    b: 456,
    c: 789
  }
}

v-on

v-model

v-pre   // 内容不会做任何解析

v-cloak

v-once  // 数据绑定内容只执行一次,节省开销

key 值尽量不要用 index 去做,因为数组发生变化后,index 也会跟着变化,可能会导致错误

修饰符

template: `
  <div>
    <div>
      <input type="text" v-model.number="text" />
    </div>
    <div>
      <input type="checkbox" :value="1" v-model="arr" />
      <input type="checkbox" :value="2" v-model="arr" />
      <input type="checkbox" :value="3" v-model="arr" />
    </div>
    <div>
      <input type="radio" v-model="picked" />
      <input type="radio" v-model="picked" />
    </div>
  </div>
`,
data: {
  text: 0,
  active: false,
  arr: [1, 2, 3],
  picked: ''
}

.number 修饰符,会自动转化为数字

.trim 修饰符,会去除空格

.lazy 事件修饰符,会增加 change 事件,不加默认是 input

2. VUE 组件

2.1 组件核心

import Vue from 'vue';

const component = {
  template: `<div>component</div>`
};

Vue.component('CompOne', component);

new Vue({
  el: '#root',
  template: `<comp-one></comp-one>`
});

组件的数据传递

import Vue from 'vue';

const component = {
  props: {
    active: Boolean
  },
  template: `
    <div>
      <p><span v-show="active">{{num}}</span></p>
      <p><span>{{num}}</span></p>
      <p><input type="button" @click="hander" value="+" /></p>
    </div>
  `,
  data() {
    return {
      num: 0
    };
  },
  methods: {
    hander() {
      this.num++;
    }
  }
};

Vue.component('comp', component);

new Vue({
  el: '#root',
  template: `
    <div>
      <comp :active="true"></comp>
      <comp></comp>
    </div>
  `
});
import Vue from 'vue';

const component = {
  props: {
    active: Boolean,
    propOne: String
  },
  template: `
    <div>
      <input type="text" v-model="text" />
      <p><span @click=""handleChange>{{propOne}}</span></p>
      <p><span v-show="active">{{text}}</span></p>
    </div>
  `,
  data() {
    return {
      text: 0
    };
  },
  methods: {
    handleChange() {
      this.$emit('change');
    }
  }
};

Vue.component('comp', component);

new Vue({
  el: '#root',
  template: `
    <div>
      <comp :active="true" :prop-one="prop1" @change="handleChange"></comp>
      <comp :active="false" prop-one="prop2"></comp>
    </div>
  `,
  methods: {
    handleChange() {
      this.prop1 += 1;
    }
  }
});

默认值

import Vue from 'vue';

const component = {
  props: {
    active: {
      type: Boolean,
      required: true,
      default: false
    }
  }
};

require 和 default 二选一即可

props: {
  active: {
    type: Boolean,
    default () { // 如果指定的是对象,要使用这种方式
      return {

      }
    },
  }
}

函数方式校验

props: {
  active: {
    validator (value) { // 更严格的校验,去掉type
      return typeof value === 'boolean'
    }
  }
}
new Vue({
  el: '#root',
  template: `
    <div>
      <comp :active="true" :prop-one="prop1" @change="handleChange"></comp>
    </div>
  `
})

2.2 extend 组件继承

const CompVue = Vue.extend(component)

new CompVue({
  el: '#root'
  propsData: {
    propOne: 'xxx'
  },
  data: {
    text: 'haha'
  },
  mounted () {
    console.log('comp mounted111')
  }
})

通过 propsData 拿到值

mounted 会在  原组件先被调用,再调用继承组件内的 mounted

另一种方式

const component1 = {};

const component2 = {
  extend: component,
  data() {
    return {
      text: 1
    };
  },
  mounted() {
    console.log('comp2 mounted');
    console.log(this.$parent.$options.name); // Root
  }
};

new Vue({
  name: 'Root',
  el: '#root',
  components: {
    Comp: component2
  },
  template: `<comp></comp>`
});
const parent = new Vue({
  name: 'parent'
});

const component2 = {
  extend: component,
  data() {
    return {
      text: 1
    };
  },
  mounted() {
    console.log(this.$parent.$options.name);
  }
};

new Vue({
  parent: parent,
  name: 'Root',
  el: '#root',
  components: {
    Comp: component2
  },
  data: {
    text: 23333
  },
  template: `
    <div>
      <span>{{text}}</span>
      <comp></comp>
    </div>
  `,
  mounted() {
    console.log(this.$parent.$options.name);
  }
});
// Root
// parent

2.3 组件实现自定义双响绑定

import Vue from 'vue';

const component = {
  props: ['value'],
  template: `
    <div>
      <input type="text" @input="handleInput" :value="value" />
    </div>
  `,
  methods: {
    handleInput(e) {
      this.$emit('input', e.target.value);
    }
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one :value="value" @input="value = arguments[0]"></comp-one>
    </div>
  `
});

可以直接写成 v-model

import Vue from 'vue';

const component = {
  props: ['value'],
  template: `
    <div>
      <input type="text" @input="handleInput" :value="value" />
    </div>
  `,
  methods: {
    handleInput(e) {
      this.$emit('input', e.target.value);
    }
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one :value="value" v-model="value"></comp-one>
    </div>
  `
});

这就是在组件内部实现 v-model 最简单的方式,只需要在一个组件里加上 props,通过事件把这个值 emit 出去,这就实现了双向绑定。

v-model 内部帮我们处理了双响绑定的逻辑

有时候 prop 要处理的值和我们定义的值会冲突,我们不想同时改变,可以这样写

import Vue from 'vue';

const component = {
  model: {
    prop: 'value1',
    event: 'change'
  },
  props: ['value1'],
  template: `
    <div>
      <input type="text" @input="handleInput" :value="value1" />
    </div>
  `,
  methods: {
    handleInput(e) {
      this.$emit('change', e.target.value);
    }
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one :value="value" v-model="value"></comp-one>
    </div>
  `
});

2.4 组件高级属性

2.4.1 插槽

import Vue from 'vue';

const component = {
  template: `
    <div :style="style">
      <slot></slot>
    </divd>
  `,
  data() {
    return {
      style: {
        width: '200px',
        height: '200px',
        border: '1px solid #aaa'
      }
    };
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one>
        <span>this is content</span>
      </comp-one>
    </div>
  `
});

2.4.2 具名插槽

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>
    </divd>
  `,
  data() {
    return {
      style: {
        width: '200px',
        height: '200px',
        border: '1px solid #aaa'
      }
    };
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one>
        <span slot="header">this is header</span>
        <span slot="body">this is body</span>
      </comp-one>
    </div>
  `
});

2.4.3 scoped slot 作用域插槽

import Vue from 'vue';

const component = {
  template: `
    <div :style="style">
      <slot value="456"></slot>
    </divd>
  `,
  data() {
    return {
      style: {
        width: '200px',
        height: '200px',
        border: '1px solid #aaa'
      }
    };
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one>
        <span slot-scope="props">{{props.value}}</span>
      </comp-one>
    </div>
  `
});

同样可以使用 v-bind 方式传值

import Vue from 'vue';

const component = {
  template: `
    <div :style="style">
      <slot :value="value"></slot>
    </divd>
  `,
  data() {
    return {
      style: {
        width: '200px',
        height: '200px',
        border: '1px solid #aaa'
      },
      value: 'hahaha'
    };
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one ref="comp">
        <span ref="span" slot-scope="props">{{props.value}} {{value}}</span>
      </comp-one>
    </div>
  `,
  mounted() {
    console.log(this.$refs.comp, this.$refs.span);
  }
});

注意:this.$refs.comp 会打印出组件的实例,可以调用 options。而 this.$refs.span 打印的是 html 节点。

2.4.4 provide 跨级数据联动

子孙组件获取祖先组件的方式使用 provide,跨级数据联动

import Vue from 'vue';

const ChildComponent = {
  template: '<div>child component: {{value}}</div>',
  inject: ['grandParent', 'value'],
  mounted() {
    console.log(this.grandParent);
  }
};

const component = {
  name: 'comp',
  components: {
    ChildComponent
  },
  template: `
    <div :style="style">
      <slot :value="value" aaa="111"></slot>
      <child-component />
    </div>
  `,
  data() {
    return {
      style: {
        width: '200px',
        height: '200px',
        border: '1px solid #aaa'
      },
      value: 'component value'
    };
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  provide() {
    return {
      grandParent: this,
      value: this.value
    };
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one>
        <span slot-scope="props">{{props.value}}</span>
      </comp-one>
    </div>
  `
});

默认 provide 不提供响应式属性,不管父组件怎么变,子组件都不会显示。要想数据能够传递,我们需要自己定义响应式

import Vue from 'vue';

const ChildComponent = {
  template: '<div>child component: {{data.value}}</div>',
  inject: ['grandParent', 'data'],
  mounted() {
    console.log(this.grandParent, this.value);
  }
};

const component = {
  name: 'comp',
  components: {
    ChildComponent
  },
  template: `
    <div :style="style">
      <slot :value="value" aaa="111"></slot>
      <child-component />
    </div>
  `,
  data() {
    return {
      style: {
        width: '200px',
        height: '200px',
        border: '1px solid #aaa'
      },
      value: 'component value'
    };
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  provide() {
    const data = {};

    Object.defineProperty(data, 'value', {
      //指定这方法相当于子组件每次调用value值时实际调用的是get方法,这个方法每次会获取最新的value
      get: () => this.value,
      enumerable: true // 可以被读取
    });
    return {
      grandParent: this,
      data //必须作为return的整个object属性返回,如果直接返回data,会直接调用value.get()方法,每次得到的就是一个值,不会进行更新,只有通过data.value去调用的时候每次才会再次调用get方法。
    };
  },
  data() {
    return {
      value: '123'
    };
  },
  template: `
    <div>
      <comp-one>
        <span slot-scope="props">{{props.value}}</span>
      </comp-one>
      <input type="text" v-model="value" />
    </div>
  `
});

官方不推荐这么使用,vue 版本升级可能会改变

2.5 组件的 render function

2.5.1 渲染函数

template 是字符串,要经过编译转换成 html 节点这个过程。
在使用 template 的时候会经过生命周期的一个过程,叫做编译,编译成一个 js 的函数,叫做 render function

new Vue({
  el: '#root',
  template: `
    <div>
      <comp-one>
        <span ref="span">{{value}}</span>
      </comp-one>
    </div>
  `,
  render() {
    return this.$createElement();
  },
  // 也可以写成
  render(createElement) {
    return createElement();
  }
});
import Vue from 'vue';

const component = {
  name: 'comp',
  // template: `
  //   <div :style="style">
  //     <slot></slot>
  //   </div>
  // `,
  render(createElement) {
    return createElement(
      'div',
      {
        style: this.style
      },
      this.$slots.default
    );
  },
  data() {
    return {
      style: {
        width: '200px',
        height: '200px',
        border: '1px solid #aaa'
      },
      value: 'component value'
    };
  }
};

new Vue({
  el: '#root',
  components: {
    CompOne: component
  },
  data() {
    return {
      value: '123'
    };
  },
  // template: `
  //   <div>
  //     <comp-one>
  //       <span ref="span">{{value}}</span>
  //     </comp-one>
  //   </div>
  // `,
  render(createElement) {
    return createElement(
      'comp-one',
      {
        ref: 'comp'
      },
      [
        createElement(
          'span',
          {
            ref: 'span'
          },
          this.value
        )
      ]
    );
  }
});

createElement 就是 vue 中虚拟 dom 的概念。创建出来的并不是真正的 dom 节点,而是 vnode 的一个类,vnode 会在内存中存储,会和真正的 dom 进行对比,如果发现需要更新,才会把 vnode 转换成 dom 内容,插入到真正的 dom 中。

render (createElement) {
  return createElement(
    'comp-one',
    {
      ref: 'comp',
      props: {
        props1: this.value
      },
      on: {
        click: this.handleClick
      }
    }, [
      createElement('span', {
        ref: 'span'
      }, this.value)
    ])
}