2025-03-29 17:04:19 +08:00

68 KiB
Executable File
Raw Permalink Blame History

VUE 2.x

https://cn.vuejs.org/

介绍

mvvm, 数据驱动视图的渐进式框架

  • M: model, data中的数据
  • V: view, 模板代码
  • VM: ViewModel, 连接 View 和 Model, Vue实例

安装

  • 兼容性: Vue 不支持 IE8 及以下版本, 因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性
  • 项目开发中一般通过vue-cli搭建开发环境 https://cli.vuejs.org/zh/guide/

全家桶

脚手架: vue-cli

路由管理: vueRouter

状态管理: vuex

http库: axios

ui框架: element, ant design vue

优秀的相关项目

https://github.com/vuejs/awesome-vue#libraries--plugins

生命周期

|0x0

  1. beforeCreate 在内存中创建出vue实例数据观测 (data observer) 和 event/watcher 事件配置还没调用data 和 methods 属性还没初始化)
  2. 【执行数据观测 (data observer) 和 event/watcher 事件配置】
  3. created: 实例已完成数据观测 (data observer)property 和方法的运算watch/event 事件回调。(data 和 methods属性完成初始化还没开始编译模板可以进行Ajax请求)
  4. beforeMount模板编译完成还没有挂载到页面中相关的 render 函数首次被调用
  5. 【挂载模板到页面】
  6. mounted 模板已挂载到页面中真实的DOM渲染完成。可以操作DOM了
    1. mounted 不保证所有子组件也一起被挂载, 需要子组件都挂载后才执行的操作, 使用 vm.$nextTick
  7. 至此,实例结束创建期,进入运行期,等待数据发生变化。
  8. 【数据变化】数据变化时会触发beforeUpdate和updated但一般用watch
  9. beforeUpdate状态更新之前执行此函数此时data中的状态值是最新的但是界面上显示的数据还是旧的因为此时还没有开始重新渲染DOM节点
  10. 【更新页面】
  11. updated:实例更新完毕后调用此函数,此时 data 中的状态值和界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了。
    1. 可用于子组件向父组件传递数据的变化 updated(){ this.$emit('contentChange',this.content) }
  12. beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。常在这里清除定时器和事件绑定
  13. 【销毁实例】
  14. destroyedVue 实例销毁后调用。此时Vue实例绑定的所有东西都会解绑所有的事件监听器会被移除所有的子实例也会被销毁
  • 不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a) 或 vm.$watch('a', newValue => this.myMethod()), 因为箭头函数并没有 this

术语

  • 前缀 $, 表示Vue内部提供的一些属性或方法
  • vm (ViewModel 的缩写) 这个变量名表示 Vue 实例
  • {{}} 双大括号: 将数据解析成文本
  • js表达式和js代码: 表达式会产生一个值, 可以放在需要值的地方. x==y?'a':'b'

image-20211101172459354|1047x49

最简单的vue例子

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>调试</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
  <div id="app">
  {{ message }}
  </div>

  <script>
    const vm = new Vue({
      // 配置el属性
      el: '#app', // el用于指定当前Vue实例为哪个容器服务, css选择器
      // 对象式
      data: { //data中用于存储数据数据供el所指定的容器去使用
        message: 'Hello Vue!'
      },
    })

    console.log(vm)
  </script>
</body>
</html>

el 和 data 的第二种写法:

const vm = new Vue({
  data: function(){ // 函数式 // vue组件中必须使用函数式
    return {
      message: 'Hello Vue!'
    }
  }
})

vm.$mount('#app') // 先创建对象再通过mount指定el
console.log(vm)

ps: 实际项目开发中以单文件模式开发

模板语法

  • 插值语法: {{xxx}}, 可以直接读取 data 中的值
  • 指令语法: v-bind:href="xxx"

数据绑定

  • 单向绑定: v-bind
  • 双向绑定: v-model, 一般在表单元素上(如: input, select)

响应式数据

  1. data中的所有属性, 最后都出现在vm上
  2. vm上的属性, 以及Vue原型上的方法, 都可以在Vue模板中直接使用

Object.defineProperty 数据劫持

let number = 18
let person = {
  name: '张三',
  sex: 'man'
}

Object.defineProperty(person, 'age', {
  // value,  // 有value时不能设置get和set
  // enumerable:false, 控制属性是否可以被枚举, 默认false
  // writable:false, //控制属性是否可以被修改默认值是false
  // configurable:false //控制属性是否可以被删除默认值是false
  get(){
    console.log('读取age属性')
    return number
  },
  set(val){
    console.log(`设置age属性, 值为${val}`)
    number = val
  }
})
  • 如图, age 属性不直接显示, 而是用通过 getter 获取, 通过 setter 设置.

  • set 和 get 叫 存取描述符, 有 value 就是 数据描述符 (不能同时用)

  • 枚举属性: 不可枚举键是浅色的, 且 Object.keys 等方法读不到

  • 控制是否可被修改

let person = {
  name: '张三',
  sex: 'man'
}

Object.defineProperty(person, 'age', {
  value = 18,  // 有value时不能设置get和set
  writable:true, //控制属性是否可以被修改默认值是false
})

  • configurable 为 false, 删除会返回 false

Vue中的数据劫持

  1. 通过vm对象来劫持data对象中属性的操作读/写)
  2. 通过 Object.defineProperty() 把data对象中所有属性添加到vm上。
console.log(vm.$data === vm_data) // true

SFC

单文件组件

  • 模板
<template>
    <div class="container">

    </div>
</template>

<script>
export default {
  name: '',
  // extends: '',
  // mixins: [],
  components: {},
  props: {},
  // model: { prop: '', event: '' },
  data () {
    return {

    }
  },
  computed: {},
  watch: {},
  filters: {},
  // directives: {},
  created () {},
  mounted () {},
  // beforeDestroy () {},
  methods: {},
}
</script>

<style scoped lang="less">

</style>

vue 常见指令

v-bind(缩写 : ) 动态绑定

<a v-bind:href="url">...</a>

// 缩写
<a :href="url">...</a>
  • v-bind的值当成js解析, :rules="/(^1[035789]\d{3}$)|(^[A-Za-z]\w{2,10}$)/", 正则需要解析
  • 动态参数
<a v-bind:[attributeName]="url"> ... </a>

// 方括号里面作为表达式进行计算, 把结果当成属性名
// 表达式不能有空格或者引号, 可以用计算属性替代
// 避免使用大写, 因为会被转成小写

class 对象语法 数组语法

除了 string , 还可以是对象或者数组

ps: 其实也可以把复杂的字符串计算放到计算属性里

<div :class="{ red: isRed }"></div>      // 对象语法: isRed为true时绑定red类名

<div :class="[classA, classB]"></div>    //Array语法: 绑定列表里的class

<div :class="[classA, { classB: isB, classC: isC }]" />  // 可以混合使用

<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div> //根据条件切换class,这样写将始终添加 errorClass但是只有在 isActive  truthy时才添加activeClass

<div v-bind:class="[{ active: isActive }, errorClass]"></div>  //使用对象语法简写

//错误示范
<van-cell title="性别" is-link :value="{'男': gender === 1, '女': gender === 0} " />  // value不能使用对象语法绑定, 提示value不能赋值对象
//改正: 可以写成三目运算符, ""里面的当成js解析
<van-cell title="性别" is-link :value="gender === 1? '男':'女'" />

style 对象语法 数组语法

  • 单个对象
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

// ...
data: {
  activeColor: 'red',
  fontSize: 30
}

更简洁

<div :style="styleObject"></div>

// ...
data: {
  styleObject: {
    color: 'red',
    fontSize: '13px'
  }
}
  • 多个对象
<div :style="[baseStyles, overridingStyles]"></div>

.sync 双向绑定修饰符

在自定义组件中, 对 porps 进行双向绑定, 和 v-model 相比, 可以多个

// 父组件中
<Child :prop.sync="value" />

// 子组件中
props: {
 prop: 类型
}
// ...
this.$emit('update:prop', newValue)

父组件其实是下面的简写

<Child :prop="value" @update:prop="prop = $event"/>

v-for

遍历Array或者Object (并不支持可响应的 MapSet 值,所以无法自动探测变更)

<div v-for="(item, index/key) in items" :key="item.id"></div>

// 也可以用 of 代替 in, 更贴近js迭代器语法
<div v-for="(item, inde) of items" :key="item.id"></div>
  • 不指定key编辑器会报错: key 用来区分每个元素, 确保高效更新和渲染.
  • key 不能用 index, 在增删时可能导致出错
  • map set 可以通过计算属性间接使用

template 的包裹

<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

数组的更新

push() pop() shift() unshift() splice() sort() reverse() 这类会改变原数据的方法, 都进行了包裹, 它们也会触发视图更新

filter() concat() slice() 这类返回新数组的方法, 则需要使用新数组代替旧数组

通过 this.array[index] = newValue 不能触发视图更新, 可以通过 this.array.splice(index, 1, newValue)

或者使用 this.$set(this.array, index, newValue)

对象的更新

对象属性的添加和删除无法触发更新, 因为初始化时就为对象的属性添加了 setter/getter 转化

  • 添加属性

如果要为对象添加响应式的属性, 可以使用 this.$set(this.obj, key , newValue)

直接使用 Object.assign() 也无法添加相应式属性, 应该把混合后的对象赋值给原来的对象 (使用解构会丢失响应性)

this.obj=Object.assign({}, this.obj, {a:1, b:2})

  • 删除属性 this.$delete(this.obj, key)

v-show

实际上是设置 display: block; / display: none;, 只是简单地基于 CSS 进行切换

<h1 v-show="true">Hello!</h1>

v-if

实际上是用<!---->注释内容, "真正"的条件渲染

  • v-if 和v-show 的区别:
    • 手段v-if 是动态的向DOM树内添加或者删除DOM元素v-show 是通过设置DOM元素的display样式属性控制显隐
    • 编译过程v-if切换有一个 局部编译/卸载 的过程切换过程中合适地销毁和重建内部的事件监听和子组件v-show只是简单的基于 css切换
    • 编译条件v-if是惰性的如果初始条件为假则什么也不做只有在条件第一次变为真时才开始局部编译编译被缓存编译被缓存后然后再切换的时候进行局部卸载); v-show是在任何条件下首次条件是否为真都被编译然后被缓存而且DOM元素保留
    • 性能消耗v-if有更高的切换消耗v-show有更高的初始渲染消耗
  • 场景:
    • 一般来说v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。
    • 因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
  • ===v-if和v-for的不能同时使用===
    • v-for 具有比 v-if 更高的优先级, v-for 的每次迭代都会进行 v-if 判断, 造成性能浪费
    • 逻辑混乱, 展示的数据和被迭代的数据不一样
    • 正确方法: 先用计算属性代替v-if进行过滤, 再对计算属性进行遍历

template 包裹

同样可以把逻辑写在 template 里

<template v-if="isTrue">
...
</template>

v-else

和v-if一起使用

<div v-if="Math.random() > 0.5">
  Now you see me
</div>
<div v-else>
  Now you don't
</div>

v-else-if

和v-if v-else-if 一起使用

<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

v-model

v-model 本质上是 .sync 的语法糖, 针对下面的这些场景做了内置简化

  • 限制
    • <input>
    • <select>
    • <textarea>
    • 自定义组件
      • 子组件中 props 要接收一个 value, 并且通过 @input="$emit('input', newValue)" 向父组件传递 input 事件
<input v-model="searchText">

等价于

<input
  :value="searchText"
  @input="searchText = $event.target.value"
>

修饰符

  • .number: 将用户的输入值转为数值类型

    <input v-model.number="age" type="number">
    
  • .trim 输入首尾空格过滤

  • .lazy 在 change 事件后进行同步, 默认是每次 input 都会触发

checkbox radio 不使用 value

input 类型为单选或者多选时, value 有其他作用, 这时可以自定义 model 的属性名

子组件: 在 model 里定义新的属性名, 同时需要在 props 里接声明

<template>
  <input
    type="checkbox"
    :checked="checked"
    @change="$emit('change', $event.target.checked)"
  />
</template>

<script>
export default {
  name: 'BaseCheckbox',
  model: {
    prop: 'checked',
    event: 'change',
  },
  props: {
    checked: {
      type: Boolean,
      default: false, // 设置默认值
    },
  },
};
</script>

父组件:

<base-checkbox v-model="lovingVue"></base-checkbox>

v-on (缩写 @ )

绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略

在监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个 $event property

<button v-on:click="handle('ok', $event)"></button>
<!-- 方法处理器 -->
<button v-on:click="handle"></button>
<!-- 缩写 -->
<button @click="doThis"></button>
<!-- 阻止默认行为 preventDefault没有表达式 -->
<form @submit.prevent></form>
<!-- 阻止冒泡 stopPropagation -->
<btuuon @click.stop></button>
<!--  串联修饰符, 有顺序 -->
<button @click.stop.prevent="doThis"></button>
  • 自定义事件名在渲染成 html 时会自动转化成全小写, 所以官方推荐用短横线命名法. 但在 sfc 中, 小驼峰也能识别到
  • 动态参数
<a @[event]="doSomething"> ... </a>

修饰符

  • .stop: 调用 event.stopPropagation()阻止事件冒泡(常用)
  • .prevent: 调用 event.preventDefault()阻止默认事件(常用)
  • .native: 监听组件根元素的原生事件 (内部组件冒泡的不监听)
  • .once: 只触发一次回调。(常用)
  • .capture:使用事件的捕获模式
  • .self只有event.target是当前操作的元素时才触发事件
  • .passive:事件的默认行为立即执行,无需等待事件回调执行完毕, 能够提高移动端的性能, 不能和 .prevent 一起使用
@click.native.prevent
/** 
阻止默认的冒泡事件
*/

有先后顺序: v-on:click.prevent.self 会阻止所有的点击, v-on:click.self.prevent 只会阻止对元素自身的点击

键盘事件

<!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter">

<!-- 键修饰符,键代码 -->
<input @keydown.13="onEnter">

<!--
常见的按键别名
回车 => enter
删除 => delete (捕获“删除”和“退格”键)
退出 => esc
空格 => space
换行 => tab (特殊必须配合keydown去使用)
上 => up
下 => down
左 => left
右 => right
-->

对于没有别名的按键, 可以自定义别名

Vue.config.keyCodes.自定义键名 = 键码

$listeners

.native 只能监听根元素的原生事件, 组件内部事件冒泡的并不监听

父组件:

<base-input v-on:focus.native="onFocus"></base-input>

子组件:

<label>
  {{ label }}
  <input
    v-bind="$attrs"
    v-bind:value="value"
    v-on:input="$emit('input', $event.target.value)"
  >
</label>

尝试监听子组件 <base-input>根元素focus 事件。然而,由于子组件的根元素是 <label>,而 label 标签本身并不会触发 focus 事件,因此父组件的监听器不会被调用,导致 onFocus 事件处理函数失效

vue 提供了 $listeners 的属性来解决这个问题. $listeners 是个对象, 里面包含里作用在这个组件上的所有监听器, 然后合并父级监听器和自定义监听器, 指向需要监听的子元素

把父组件传进来的事件都绑定到子组件某个元素上

<template>
  <label>
    {{ label }}
    <input
      v-bind="$attrs"
      v-bind:value="value"
      v-on="inputListeners"
    >
  </label>
</template>

<script>
export default {
  name: 'BaseInput',
  inheritAttrs: false, // 禁用默认继承的属性绑定
  props: {
    label: String, // 定义 label 属性
    value: [String, Number], // 定义 v-model 对应的 value
  },
  computed: {
    inputListeners() {
      const vm = this;
      // 合并父级监听器和自定义监听器
      return Object.assign({}, 
        this.$listeners, 
        {
          // 确保组件配合 v-model 工作
          input(event) {
            vm.$emit('input', event.target.value);
          },
        }
      );
    },
  },
};
</script>

<style scoped>
/* 根据需要添加样式 */
</style>

v-onece

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能

<my-component v-once :comment="msg"></my-component>

v-text

<span v-text="msg"></span>
<!-- 和下面的一样 -->
<span>{{msg}}</span>

v-html

<div v-html="html"></div>

将数据解析成html

而{{}}是解析成文本

在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 XSS 攻击。只在可信内容上使用 v-html永不用在用户提交的内容上。

v-slot 插槽

在子组件中定义占位符,由父组件传入内容动态填充

v-slot官方文档https://cn.vuejs.org/v2/guide/components-slots.html

旧属性 slot/slot-scopev-slot 取代

  • 父组件的标签对子组件传 HTML 结构
<MyButton>注册</MyButton>
  • 子组件通过 <slot> 接收传递过来的 HTML 结构
<button>
  <slot>按钮</slot> 
  <!--
      传递过来的 HTML 结构会替换到 slot 的位置如果没有传递则使用 slot 的默认值
      slot位置由子组件决定, 内容由父组件决定
  -->
</button>

后备内容

子模板可以设置默认内容, 当父组件没有提供内容就会显示这些默认的内容

<!--子组件-->
<button type="submit">
  <slot>Submit</slot>
</button>

<!--父组件-->
<submit-button></submit-button>

具名插槽

具有名字的插槽, 父组件想区别子组件中的几个插槽那就要用slot标签的name属性来标识 而没有name属性的叫匿名插槽

<!--
1. 子组件: <slot>元素元素添加name属性, 没有name属性的实际上默认name="default"
-->
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

<!--
2. 父组件: <template> 添加 v-slot:插槽名
-->
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

//<template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。
动态插槽名
  • 插槽名也可以是动态的
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>
具名插槽的缩写

v-slot: 换成 #

  • default 不能省略
<current-user #default="{ user }">
  {{ user.firstName }}
</current-user>

编译作用域

父级组件里的所有内容都是在父级作用域中编译的 子组件里的所有内容都是在子作用域中编译的 也就是说, 子组件没办法直接拿到父组件的变量, 父组件也不能直接拿到子组件的变量

子组件 <navigation-link> :

<a
  v-bind:href="url"
  class="nav-link"
>
  <slot></slot>
</a>

父组件:

<navigation-link>
  这里实际拿不到子组件的 {{ url }}
</navigation-link>

==作用域插槽==

https://blog.csdn.net/willard_cui/article/details/82469114

在子组件的 slot 标签上绑定属性, 在父组件用 v-slot="xxx" 接收 目的: 父组件访问子组件中的变量, 向子组件的插槽传递带有子组件变量的模板 简单理解: 在父组件的插槽中, 控制子组件的变量的展示方式

/**
1. 子组件在<slot> 绑定数据
   <slot :属性名1="值1" :属性名2="值2"> </slot>
2. 父组件在 <template> 接收数据
   <template  v-slot="slotProps">
   包含所有子组件的所有插槽prop的对象命名为 slotPros, 也可以命名为其他名字
3. 父组件操作数据, {{slotProps.属性名1}}  {{slotPros.属性名2}}
*/
<!--子组件-->
<template>
  <div class="子组件">
    <table>
      <tbody>
        <!-- 必须写:key, 否则eslint报错 -->
        <tr v-for="(item, index) in data" :key="index">
          <!-- 作用域插槽: 把变量绑定到属性上 -->
          <slot :row="item" />
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {
  props: ['data']
}
</script>

<style scoped lang="scss">
</style>
<!--父组件-->
<template>
  <div>
    <myTable :data="data">

      <!-- 用v-slot接收插槽的变量 -->
      <template v-slot:default="slotProps">
      <!--使用具名插槽时, name不能省略; 使用默认插槽时, :default可以省略 --> 
        <!-- 在父组件中控制子组件的插槽数据以什么形式显示 -->
        <td>{{ slotProps.row.tittle }}</td>
        <td>{{ slotProps.row.content }}</td>
      </template>
    </myTable>
  </div>
</template>

<script>
import myTable from '@/components/作用域插槽-子组件.vue'
export default {
  components: {
    myTable
  },
  data() {
  return {
    data: [
      {
        tittle: '标题1',
        content: '内容1'
      },
      {
        tittle: '标题2',
        content: '内容2'
      },
      {
        tittle: '标题3',
        content: '内容3'
      }
    ]

  }
}
}
</script>

<style>
</style>
  • 只有默认插槽, 可以把 template 省略掉, 把 v-slot 写到组件上, default 也可以省略掉
    • 当既有默认插槽, 又有具名插槽, 不可省略
<myTable :data="data" v-slot="slotProps">
    <td>{{ slotProps.row.tittle }}</td>
    <td>{{ slotProps.row.content }}</td>
</myTable>

slotProps 解构

v-slot 的值实际是 js 表达式, 所以在 slotProps 里的多个值, 可以用解构取出一个, 也可以重命名

<current-user v-slot="{ user }">
  {{ user.firstName }}
</current-user>
  • 重命名
<current-user v-slot="{ user: person }">
  {{ person.firstName }}
</current-user>

$slots

$slots是组件插槽集是组件所有默认插槽、具名插槽的集合可以用来获取当前组件的插槽集

例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到

v-pre

跳过编译过程直接显示

<div v-pre>{{msg}}</div>
<!-- 这里就直接显示{{msg}} -->

自定义指令

实现对普通 DOM 元素进行底层操作

使用方式: v-xxx=""

注册

全局注册

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})

局部注册

// 局部注册: 组件中接受一个 directives 的选项
directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}

使用:

<input v-focus>

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选)

  • bind:指令第一次绑定到元素时调用, 可以用来初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:指令与元素解绑时调用。

钩子函数的参数

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnodeVue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

动态指令的参数

  • 指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value" 中,argument 参数可以根据组件实例数据进行更新
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>

// ...
data(){
    return {
        direction: 'left'
    }
}
Vue.directive('pin', {
  bind: function (el, binding, vnode) {
    el.style.position = 'fixed'
    var s = (binding.arg == 'left' ? 'left' : 'top')
    el.style[s] = binding.value + 'px'
  }
})
  • 需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>

特殊属性

key

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

有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。

最常见的用例是结合 v-for

<ul>
  <li v-for="item in items" :key="item.id">...</li>
</ul>

它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:

  • 完整地触发组件的生命周期钩子
  • 触发过渡

例如:

<transition>
  <span :key="text">{{ text }}</span>
</transition>

text 发生改变时,<span> 总是会被替换而不是被修改,因此会触发过渡。

  • 为什么在v-for时不能用index作为key
  1. 触发的更新变多
  2. 数据可能混乱

ref

ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:

<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>

<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>
  • $refs 只会在组件渲染完成后生效, 并且不是响应式的, 不要在模板或者计算属性中使用

is

用在动态组件上, <component> 是 vue 内置的一个特殊组件, 用来动态渲染其他组件

<!--  `currentView` 改变时组件也跟着改变 -->
<component :is="currentTabComponent"></component>

currentTabComponent 可以包括

  • 已注册组件的名字,或
  • 一个组件的选项对象

props

property 复数 properties 的缩写

  • 列出类型, 对象的 key 是属性名, 值是属性类型
props: {
  title: String,
  likes: Number,
  isPublished: Boolean,
  commentIds: Array,
  author: Object,
  callback: Function,
  contactsPromise: Promise // or any other constructor
}

// 使用: this.title, 当成data用
  • 可以传静态, 也可以传动态 (父组件通过 v-bind)
  • 传入一个对象作为所有属性 (父组件中 v-bind="obj")
  • 添加校验
props: {
  // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
  propA: Number,
  // 多个可能的类型
  propB: [String, Number],
  // 必填的字符串
  propC: {
    type: String,
    required: true
  },
  // 带有默认值的数字
  propD: {
    type: Number,
    default: 100
  },
  // 带有默认值的对象
  propE: {
    type: Object,
    // 对象或数组默认值必须从一个工厂函数获取
    default: function () {
      return { message: 'hello' }
    }
  },
  // 自定义验证函数
  propF: {
    validator: function (value) {
      // 这个值必须匹配下列字符串中的一个
      return ['success', 'warning', 'danger'].includes(value)
    }
  }
}

type 可以是这些构造函数 String Number Boolean Array Object Date Function Symbol

还可以是自定义构造函数, 通过 instanceOf 来校验

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}
props: {
  p: Person
}

$attrs

当一个组件没有声明任何 prop 时, 父组件的属性默认会添加到子组件的根标签上, 可通过this.$attrs.name直接获取属性

<!--父组件-->
<MyInput err_message="请输入6到18位的密码"/>

<!--子组件-->
<input>

//打印
cosole.log(this.$attrs)  //输出对象, 必须是props中没有声明

data

响应数据

  • Vue组件中的data为什么是函数?

Object是引用数据类型,如果不用 function 返回,每个组件的 data 都是同一个内存地址,一个数据改变了其他也改变了

举个例子:

const MyComponent = function( ) {};
MyComponent.prototype.data = {
  a: 1,
  b: 2,
}
const component1 = new MyComponent();
const component2 = new MyComponent();

component1.data.a === component2.data.a; // true
component1.data.b = 5;
component2.data.b // 5

如果两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改;

两个实例应该有自己各自的域才对,需要通过下面的方法来进行处理:

const MyComponent = function( ) {
  this.data = this.data();
};  
MyComponent.prototype.data = function( ) {
  return {
    a: 1,
    b: 2,
  }
};

这样么一个实例的data属性都是独立的,不会相互影响了。

components

注册组件

<script>
export default {
  import Hello from './components/Hello.vue'
  // ...
  components: {
    Helllo
   }
  // ...
}

</script>

computed 计算属性

计算属性: 模板中的复杂逻辑使用计算属性

  • 特点:

    1. 基于响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
    2. 计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性。
    3. computed能做的watch都能做反之则不行. 能用computed的尽量用computed
<div>{{testComputed}}</div>
<div>{{testComputed}}</div>
<div>{{testComputed}}</div>
<!-- 只打打印了一次 -->
computed: {
  testComputed(){
    console.log('testComputed')
    return 'testComputed'
  }
},

反例: 函数在模板里每次都会触发

<div>{{testMethod()}}</div>
<div>{{testMethod()}}</div>
<div>{{testMethod()}}</div>
<!-- 会打印3次 -->
methods: {
  testMethod(){
    console.log('abc')
    return 'testMethod'
  }
},
  • 应用:
  1. 复杂的逻辑

    <div id="example">
      {{ message.split('').reverse().join('') }}
    </div>
    

    改为

    <div id="example">
      {{reversedMessage}}
    </div>
    
    computed: {
    // 计算属性的 getter
      reversedMessage: function () {
        // `this` 指向 vm 实例
        return this.message.split('').reverse().join('')
      }
    }
    
  2. 数据只用来渲染, 不改变刷新 (放在data里会消耗更多的资源)

    computed: {
      test(){ // 如果把数据放data里, 多了很多数据劫持的资源消耗
        return {
          a: 1,
          b: [
            c: 2,
            d: 3
            e: [4, 5]
          ]
        }
      }
    }
    
  • 完整写法和setter(上面是只有getter时的简写)

    data(){
      return {
        message : 'msg'
      }
    },
    computed : {
      test: {
        get(){
          return this.message + '--'
        },
        set(val){
          this.message = val  // set里面是对依赖进行修改
        }
      }
    },
    mounted(){
      this.test = 'msg2' // 调用computed里面的set
    }
    

使用场景: 把计算属性双向绑定, 当计算属性修改时, 对依赖的响应数据进行修改

watch 侦听属性

数据变化时执行异步或者开销较大的操作

<template>
  <div>
    <input v-model="query" placeholder="输入搜索内容" />
    <p v-if="loading">正在搜索...</p>
    <ul>
      <li v-for="result in results" :key="result.id">{{ result.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      query: "", // 用户的搜索内容
      results: [], // 搜索结果
      loading: false // 是否正在加载
    };
  },
  watch: {
    query: function (newQuery) {
      // 如果输入为空,清空结果
      if (!newQuery) {
        this.results = [];
        return;
      }

      this.loading = true; // 开始加载

      // 模拟异步请求,使用 setTimeout 替代实际的 API 调用
      setTimeout(() => {
        // 假设返回了一些搜索结果
        this.results = [
          { id: 1, name: `${newQuery} - 结果 1` },
          { id: 2, name: `${newQuery} - 结果 2` },
          { id: 3, name: `${newQuery} - 结果 3` }
        ];
        this.loading = false; // 加载完成
      }, 1000); // 模拟 1 秒延迟
    }
  }
};
</script>

filters

过滤器

用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式

备注: |叫管道符

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!--  `v-bind`  -->
<div v-bind:id="rawId | formatId"></div>

局部注册

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

全局注册

// 在vue实例化之前
Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

过滤器可以串联

{{ message | filterA | filterB }}

过滤器可以接收参数

{{ message | filterA('arg1', arg2) }}

methods

===不应该使用箭头函数来定义 method 函数===, 箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例

created

创建前生命周期钩子

mounted

挂载前生命周期钩子

自定义组件

==组件封装==

Hello.vue

<template>
  <p>{{msg}}</p>
</template>

<script>
export default {
  data () {
    return {
      msg: 'hello'
    }
  }
}
</script>

<style lang="less" scoped>
  p {
    color: red;
  }
</style>

组件命名:

  1. 短横线命名法 "my-component-name", 使用 <my-component-name>
  2. 大驼峰命名法 "MyCoomponentName", 在模板中使用 <my-component-name><MyComponentName> 都可以, 但直接在 html 中使用必须使用短横线, 因为不识别大小写, 全部转化成小写

组件使用

局部注册

  1. 创建 vue 单文件
  2. 导入 vue 文件
  3. 注册组件
  4. 使用组件

Parent.vue

<template>
  <Hello></Hello>
</template>

<script>
  import Hello from './components/Hello.vue'
  export default {
    // ...
    components: {
      Hello
    }
    // ...
  }
</script>

<style>
</style>

全局注册

import XXX from '@/components/test'
Vue.component('abc', XXX)

示例

方法一: 在入口 main.js 注册

//Vue.component() 方法传入两个参数, 1.组件名, 2.组件对象(先引入)
import PageTools from '@/components/PageTools'
Vue.component('PageTools', PageTools)

方法二: 批量注册(像element-ui那样), 自己设计的第三方包

// Vue.use注册第三方包, 第三方包里写多个Vue.component()
// 1. src/components/index.js
import PageTools from '@/components/PageTools'
// import ...
// import ...
export default {
    // 固定写法: install方法, 在里面写逻辑, 自动接受一个形参, 就是Vue包
    install(Vue) {
       Vue.component('PageTools', PageTools)
       // Vue.component()...
       // Vue.component()...
    }
}

// 2. src/main.js
import Component from '@/components'
Vue.use(Component)

==组件传值==

父传子

父组件通过 属性传递,子组件通过 props 接收

props

// 简单语法,不做类型检查 
Vue.component('props-demo-simple', {
  props: ['size', 'myMessage']
})

// 对象语法,提供验证
Vue.component('props-demo-advanced', {
  props: {
    // 检测类型
    height: Number,
    rules: RegExp, // 正则
    // 检测类型 + 其他验证
    age: {
      type: Number,
      default: 0,
      required: true,
      validator: function (value) {
        return value >= 0
      }
    }
  }
})

this.$attrs: 父组件的属性默认会传到子组件的根标签上

子传父

通过this发出的事件只能由父组件监听到 this.$emit(事件名, 参数)

  • 子组件向父组件传对象数据的时候
    • 如果对象里只有一个数据

      //Object.keys(obj)能拿到对象的键, 以数组的形式
      //Object.values(obj)能拿到对象的值
      //console.log(Object.keys(data)[0])
      
    • 如果有多个数据//用for in 遍历

兄弟互传(除了父子都当成兄弟处理)

  1. 创建事件总线(new Vue), 传递和监听事件
  2. 子组件(发送方)引入事件总线, 向总线传递事件名和数据
  3. 兄弟组件(接收方)引入事件总线, 用钩子函数监听总线里的对应事件名, 把接收的参数赋值给自己的实例

示例

/**  mybus.js
1.单文件组件是组件实例
2.组件实例是可复用的vue实例
3.意味着使用this可以做的事情vue实例都能做到
4.所以创建一个全局的vue实例来进行事件的发出和监听
*/
//事件总线, 用来传递和监听事件
import Vue from 'vue'
export default new Vue()
/**子组件
*/
<template>
    <div class="son">
        <h1>子组件</h1>
        <span>跟我的兄弟说:"{{saytobrother}}"</span><button type="button" @click="tellmybrother">say</button>
    </div>
</template>

<script>
    //引入事件总线
    import bus from '@/utils/mybus.js'
    export default {
        data () {
            return {
                saytobrother: '你是头猪'
            }
        },

        methods: {
            //向总线发出事件, 传递数据
            tellmybrother () {
                //第一个参数, 事件名, 第二个参数, 传递的数据
                bus.$emit('brothersay', this.saytobrother)
            }
        }

    };
</script>

<style lang="less" scoped>
    .son {
        width: 400px;
        background-color: skyblue;
    }
</style>
/**兄弟组件
*/
<template>
    <div class="brother">
        <h1>子组件的兄弟组件</h1>
        <span>我的兄弟跟我说:"{{mybrothersay}}"</span>
    </div>
</template>

<script>
    //引入事件总线
    import bus from '@/utils/mybus.js'
    export default {
        data () {
            return {
                mybrothersay: ''
            }
        },
        // 监听
        created () {
            bus.$on('brothersay', (data) => {
                this.mybrothersay = data
            })
        },
    }
</script>

<style lang="less" scoped>
    .brother {
        width: 500px;
        background-color: orange;
    }
</style>
/**父组件
*/
<template>
    <div class="father">
        <h1>父组件</h1>
        <son></son>
        <brother></brother>
    </div>
</template>

<script>
    // 引入
    import son from './子组件.vue'
    import brother from './兄弟组件'
    export default {
        // 注册
        components: {
            son, brother
        }
    }
</script>

<style lang="less" scoped>
    .father {
        background-color: pink;
    }
</style>

$refs $parent

  • this.$refs: 父传子: 1. 子组件 ref="xxx" 2. this.$refs.xxx.子组件中的方法或data
  • this.$parent: 子传父: this.$parent.父组件的方法或data 缺点: 父组件不明确, 不推荐使用

依赖注入 provide inject

不仅仅是儿子, 所有后代都能使用

  1. 父组件在 provide 函数中返回一个对象
<!-- ParentComponent.vue -->
<template>
  <div>
    <h1>父组件</h1>
    <ChildComponent />
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  name: 'ParentComponent',
  components: { ChildComponent },
  provide() {
    return {
      message: '这是从父组件传递的数据',
    };
  },
};
</script>

  1. 子组件通过 inject 数组接收
<!-- ChildComponent.vue -->
<template>
  <div>
    <h2>子组件</h2>
    <p>接收到的数据: {{ message }}</p>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  inject: ['message'],
};
</script>
  • inject 除了可以是数组, 还能是对象, 方便设置别名或设置默认值
<!-- ChildComponent.vue -->
<template>
  <div>
    <h2>子组件</h2>
    <p>默认注入: {{ injectedMessage }}</p>
    <p>别名注入: {{ aliasMessage }}</p>
    <p>默认值注入: {{ defaultMessage }}</p>
  </div>
</template>

<script>
export default {
  name: 'ChildComponent',
  inject: {
    // 简单映射
    injectedMessage: 'message',
    // 使用别名
    aliasMessage: 'anotherMessage',
    // 设置默认值
    defaultMessage: {
      from: 'nonExistMessage',
      default: '这是默认值', // 父组件为未供时设置默认值
    },
  },
};
</script>

内置的组件

component

:is 一起用来渲染动态组件

<component :is="view"></component>

transition

内置组件, 给元素或组件添加进入/离开动画 v-if、v-show、动态组件、根组件

<!-- 简单元素 -->
<transition>
  <div v-if="ok">toggled content</div>
</transition>

<!-- 动态组件 -->
<transition name="fade" mode="out-in" appear>
  <component :is="view"></component>
</transition>

keep-alive

缓存组件的状态

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
  <component :is="view"></component>
</keep-alive>
  • Props
    • include - 字符串或正则表达式或数组。只有名称匹配的组件会被缓存。
    • exclude - 字符串或正则表达式或数组。任何名称匹配的组件都不会被缓存。
    • max - 数字。最多可以缓存多少组件实例。
  • 用法
    • <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

注意点:

<div>
    <keep-alive>
      <router-view v-if="keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!keepAlive"></router-view>
</div>
  1. 直接加上keep-alive的话会把所有的 router-view 层级下的视图的组件都缓存
  2. 只想缓存部分,不想缓存所有的, 需要使用 include , exclude
  3. 需要被缓存的组件必须要有名字

组件的名称与include的值相同的才会被缓存

export default {
  name:XXX  // 这里的name
}
  • keep-alive的组件以后组件上就会自动加上了 ``activated钩子和deactivated`钩子
初始进入和离开 created ---> mounted ---> activated --> deactivated
后续进入和离开 activated --> deactivated

vm.$nextTick ()

DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

(数据更改后不会马上更新视图,而是进入一个队列, 把最终的结果去重再更新视图)

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

// 作为一个 Promise 使用 (2.1.0 起新增)
Vue.nextTick()
  .then(function () {
    // DOM 更新了
  })

vm.$forceUpdate ()

强制实例重新渲染

vm.$set()

vm.$set( target, propertyName/index, value )
// 第一个参数 添加的位置
// 第二个参数, 键
// 第三个参数, 值

如果在实例创建之后添加新的属性到实例上,它不会触发视图更新。

data () {
  return {
    student: {
      name: '',
      sex: ''
    }
  }
}
mounted () {
  this.student.age = 24
}

受 ES5 的限制Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。

正确写法:this.$set(this.data,”key”,value')

  • 在vue的内部针对于数组中对象是会通过==Object.defineProperty==来对所有的属性进行劫持,来完成响应式的,所以数组中对象元素都是响应式的,这里并没有通过数组下标去改变值,而是获取相应的对象,而这个对象是响应式的。
  • https://vuejs.bootcss.com/guide/reactivity.html 深入响应式原理

vm.$delete()

删除对象的 property。如果对象是响应式的确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制

Vue.use()

安装插件

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

new Vue({
  // ...组件选项
})

开发插件

Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:

MyPlugin.install = function (Vue, options) {
  // 1. 添加全局方法或 property
  Vue.myGlobalMethod = function () {
    // 逻辑...
  }

  // 2. 添加全局资源
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      // 逻辑...
    }
    ...
  })

  // 3. 注入组件选项
  Vue.mixin({
    created: function () {
      // 逻辑...
    }
    ...
  })

  // 4. 添加实例方法
  Vue.prototype.$myMethod = function (methodOptions) {
    // 逻辑...
  }
}

Vue.mixin

混入: 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项

  1. 方法和参数在各组件中不共享
  2. 值为对象的选项,如 methods, components 等,选项会被递归合并,发生冲突则使用组件的
  3. 值为函数的选项,如 created, mounted 等,就会被合并调用,组件的函数后调用

避免使用

// mixin.js
export default {
  data() {
    return {
      message: "Hello from mixin!"
    };
  },
  methods: {
    showMessage() {
      alert(this.message);
    }
  }
};
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="showMessage">点击显示消息</button>
  </div>
</template>

<script>
import myMixin from "./mixin";

export default {
  mixins: [myMixin] // 使用 mixin
};
</script>

Vue.version

提供字符串形式的 Vue 安装版本号。这对社区的插件和组件来说非常有用,你可以根据不同的版本号采取不同的策略。

var version = Number(Vue.version.split('.')[0])

if (version === 2) {
  // Vue v2.x.x
} else if (version === 1) {
  // Vue v1.x.x
} else {
  // Unsupported versions of Vue
}

Vue.observable

让一个对象可响应。Vue 内部会用它来处理 data 函数返回的对象。

返回的对象可以直接用于渲染函数和计算属性,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:

const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: { click: () => { state.count++ }}
    }, `count is: ${state.count}`)
  }
}

路由

路由创建

  • 导入 vue-router
  • 安装 VueRouter
  • 创建 VueRouter 实例,为了方便维护,一般会把路由配置表抽离出去用一个变量存起来。
  • 导出 VueRouter 实例,在 main.js 入口文件中使用。
  • PS我们日常开发其实只需要关心 路由配置表 的配置关系即可。

在原型上添加变量

//src\utils\baseURL.js
// 导入 Vue
import Vue from 'vue'
// API 服务器的地址
export const baseURL = `http://127.0.0.1:3000`

// 把 baseURL 挂载到 Vue 的原型上,好处:所有的 Vue 组件都能共享该变量
Vue.prototype.$baseURL = baseURL
  • 所有组件都能共享变量。
  • 组件的 template 模板中直接通过变量名即可访问 如: $baseURL
  • 组件的 script 代码中,可通过 this.$baseURL 访问到变量。

对于没有导入Vue组件的api, 要使用baseURL还是得先导入baseURL

生产环境配置dev-tool

Vue.config.devtools = true

开发版本默认为 true,生产版本默认为 false。生产版本设为 true 可以启用vue-devtools 。

render 渲染函数

render 函数不方便阅读, 尽量使用 jsx

https://v2.cn.vuejs.org/v2/guide/render-function.html

节点、树、虚拟 DOM

这是 html 内容

<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>

在浏览器中, 会创建这样的节点树来追踪内容

|700x445

在 vue 模板中

<h1>{{ blogTitle }}</h1>

对应的渲染函数

<script>
export default {
    render: function (createElement) {  
      return createElement('h1', this.blogTitle)
    }
}
</script>
  • createElement 返回的是虚拟节点 VNode, 描述的是 vue 该如何渲染这个节点的信息, 多个虚拟节点构成的就是虚拟 DOM
  • createElement 通常简写成 h, 上面的渲染函数可以简写成
<script>
export default {
    render(h) {
      return h('h1', this.blogTitle)
    }
}
</script>

createElement 的参数

createElement(
  tag,       // {String | Object | Function}
  data,      // {Object}
  children   // {String | Array | Function}
)
  • tag: 可以是标签, 组件, 或者返回标签/组件的函数
  • data (可选): Vnote 数据对象, 类似 v-bind , 可以设置 class、style、attrs、props、on 等
h('div', { 
  class: 'my-class',
  style: { color: 'red' },
  attrs: { id: 'app' },
  props: { value: 'Hello' },
  on: { click: () => alert('Clicked!') }
}, '内容')
  • children (可选): 子节点, 和 tag 一样, 可以是标签, 组件, 或者返回标签/组件的函数
h('div', [
  h('p', '我是段落'),
  h('button', { on: { click: () => alert('按钮点击') } }, '点击我')
])

// 第二个参数 data 不需要设置, 自动把第二个参数识别成 children
// 多个子节点用数组包裹
// 只有一个子节点直接传

Vnote 不可复用

Vue 基于 Vnote 进行 Diff 算法, 复用会导致无法正常追踪和更新

render(h) {
  var myParagraphVNode = createElement('p', 'hi')
  return h('div', [
    // 错误 - 重复的 VNode
    myParagraphVNode, myParagraphVNode
  ])
}

v-if v-for 代替

  • v-if 用 if else 代替
  • v-for 用 map 代替
props: ['items'],
render(h) {
    if (this.items.length) {
      return h('ul', this.items.map( item => {
        return h('li', item.name)  
      }))
    } else {
      return h('p', 'No items found.')
    }
}

v-model

需要自己实现

  • 子组件
export default {
  props: ['value'],
  render(h) {
    var self = this
    return h('input', {
      domProps: {
        value: self.value
      },
      on: {
        input: function (event) {
          self.$emit('input', event.target.value)
        }
      }
    })
  }
};

domProps 是原生 html 的属性, props 是 vue 组件的属性

  • 父组件
export default {
  data() {
    return {
      message: ''
    };
  },
  render(h) {
    return h(CustomInput, {
      props: {
        value: this.message
      },
      on: {
        input: (val) => {
          this.message = val;
        }
      }
    });
  }
};

修饰符

事件修饰符 前缀
.passive &
.capture !
.once ~
.capture.once 或
.once.capture
~!

例子

on: {  
'!click': this.doThisInCapturingMode,  
'~keyup': this.doThisOnce,  
'~!mouseover': this.doThisOnceInCapturingMode  
}
修饰符 处理函数中的等价操作
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
按键:
.enter.13
if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码)
修饰键:
.ctrl.alt.shift.meta
if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKeyshiftKey 或者 metaKey)

插槽

静态插槽

this.$slots.插槽名 是一个 Vnote 数组

render(h) {
  // `<div><slot></slot></div>`
  return h('div', this.$slots.default)
}

作用域插槽

this.$scopedSlots.插槽名(参数对象) 一个函数, 返回 Vnote 数组

子组件:

props: ['message'],
render(h) {
  // `<div><slot :text="message"></slot></div>`
  return h('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])

在父组件中设置模板

数据对象 scopedSlots 的 key 是插槽名称, 值一个返回 Vnote 的函数, 接收的参数是 slotProps

render (h) {
  // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
  return h('div', [
    h('child', {
      // 在数据对象中传递 `scopedSlots`
      // 格式为 { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return h('span', props.text)
        }
      }
    })
  ])
}

jsx

jsx 最终也是解析成 render 函数

安装 babel 插件

https://github.com/vuejs/jsx-vue2

npm install @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props

babel.config.js 添加

module.exports = {
  presets: ['@vue/babel-preset-jsx'],
}

使用

render() {
  return <p>hello</p>
}

响应数据

render() {
  return <p>hello { this.message }</p>
}

组件

import MyComponent from './my-component'

export default {
  render() {
    return <MyComponent>hello</MyComponent>
  },
}

添加属性

render() {
  return <input type="email" />
}
  • 动态属性
render() {
  return <input
    type="email"
    placeholder={this.placeholderText}
  />
}
  • 展开运算符
render() {
  const inputAttrs = {
    type: 'email',
    placeholder: 'Enter your email'
  }

  return <input {...{ attrs: inputAttrs }} />
}

插槽

  • 具名插槽
render() {
  return (
    <MyComponent>
      <header slot="header">header</header>
      <footer slot="footer">footer</footer>
    </MyComponent>
  )
}
  • 作用域插槽
render() {
  const scopedSlots = {
    header: () => <header>header</header>,
    footer: () => <footer>footer</footer>
  }

  return <MyComponent scopedSlots={scopedSlots} />
}

指令

<input vModel={this.newTodoText} />
  • 修饰符
<input vModel_trim={this.newTodoText} />
  • 监听事件
<input vOn:click={this.newTodoText} />
  • 事件修饰符
<input vOn:click_stop_prevent={this.newTodoText} />
  • v-html
<p domPropsInnerHTML={html} />

函数式组件

  • 默认导出
export default ({ props }) => <p>hello {props.message}</p>
  • 声明为变量
const HelloWorld = ({ props }) => <p>hello {props.message}</p>

函数式组件

没有 响应式数据 data , 也没有 this 上下文, 只依赖 props 渲染 (也没有 methods computed 等) 主要用于性能优化,因为它们没有响应式系统的开销,渲染速度更快

  1. 添加 functional: true 选项
  2. render 增加第二个参数, context
export default {
  functional: true,
  props: {  // props 是可选的
    // ...  
  },
  render(h, context) {
    return h('div', 'Hello, Vue!')
  }
};
  • props 是可选的, 省略 props 选项,所有组件上的 attribute 都会被自动隐式解析为 prop
  • 返回的不是 Vnote, 而是 HTMLElement
    • 如果在组件上绑定 ref=xxx, 那么 this.$refs. xxx 是 HTMLElement
  • 也可以在 template 标签加 functional, 选项里的去掉
<template functional>
</template>

context

组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:

  • props:提供所有 prop 的对象
  • childrenVNode 子节点的数组
  • slots:一个函数,返回了包含所有插槽的对象
  • scopedSlots:一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,包含组件的 attrsclassstyle 等信息, 作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners:一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections:如果使用了 inject 选项,则该对象包含了应当被注入的 property。
<template functional>
  <!-- 函数式组件不需要 template 内容 -->
</template>

<script>
export default {
  name: 'PracticalFunctionalDemo',
  functional: true,
  props: {
    title: String
  },
  inject: ['appTheme'],
  
  render(h, { 
    props, 
    children, 
    slots, 
    scopedSlots, 
    data, 
    parent, 
    listeners, 
    injections 
  }) {
    return h('div', {
      class: ['functional-box', data.class],
      style: { 
        ...data.style,
        border: `1px solid ${injections.appTheme === 'dark' ? '#333' : '#eee'}`
      },
      attrs: {
        ...data.attrs,
        'data-functional': 'true'
      },
      on: {
        ...listeners,
        mouseover: () => console.log('鼠标移入')
      }
    }, [
      // 使用 props
      h('h3', props.title),
      
      // 使用 children
      children.length > 0 ? h('div', { class: 'content' }, children) : null,
      
      // 使用普通插槽
      slots().default || h('p', '默认内容'),
      
      // 使用作用域插槽
      scopedSlots.footer ? scopedSlots.footer({ 
        parentName: parent.$options.name 
      }) : null,
      
      // 显示注入的值
      h('p', `当前主题: ${injections.appTheme}`)
    ])
  }
}
</script>

JSX 版本

// PracticalFunctionalDemo.jsx
export default {
  name: 'PracticalFunctionalDemo',
  functional: true,
  props: {
    title: String
  },
  inject: ['appTheme'],
  
  render(h, context) {
    const { 
      props, 
      children, 
      slots, 
      scopedSlots, 
      data, 
      parent, 
      listeners, 
      injections 
    } = context

    // 合并事件(原生事件 + 父组件传递的事件)
    const mergedListeners = {
      ...listeners,
      mouseover: () => console.log('鼠标移入')
    }

    return (
      <div
        class={['functional-box', data.class]}
        style={{
          ...data.style,
          border: `1px solid ${injections.appTheme === 'dark' ? '#333' : '#eee'}`
        }}
        {...{ attrs: data.attrs }}
        on={mergedListeners}
      >
        {/* 使用 props */}
        <h3>{props.title}</h3>
        
        {/* 使用 children */}
        {children.length > 0 && (
          <div class="content">{children}</div>
        )}
        
        {/* 使用普通插槽 */}
        {slots().default || <p>默认内容</p>}
        
        {/* 使用作用域插槽 */}
        {scopedSlots.footer && scopedSlots.footer({
          parentName: parent.$options.name
        })}
        
        {/* 显示注入的值 */}
        <p>当前主题: {injections.appTheme}</p>
      </div>
    )
  }
}

css 深度作用选择器

希望 scoped 样式中的一个选择器能够作用得"更深",例如影响子组件

https://www.cnblogs.com/CyLee/p/10006065.html

https://vue-loader-v14.vuejs.org/zh-cn/features/scoped-css.html

<style lang="scss" scoped>
.select {
  width: 100px;

  /deep/ .el-input__inner {
    border: 0;
    color: #000;
  }
}
</style>

响应式原理

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项Vue 将遍历此对象所有的 property并使用 Object.defineProperty 把这些 property 全部转为 getter/setter

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把"接触"过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher从而使它关联的组件重新渲染。

|700x438

数组的增删

push() pop() shift() unshift() splice() sort() reverse() 这类会改变原数据的方法, 都进行了包裹, 它们也会触发视图更新

filter() concat() slice() 这类返回新数组的方法, 则需要使用新数组代替旧数组

通过 this.array[index] = newValue 不能触发视图更新, 可以通过 this.array.splice(index, 1, newValue)

或者使用 this.$set(this.array, index, newValue)

对象属性的增删

对象属性的添加和删除无法触发更新, 因为初始化时就为对象的属性添加了 setter/getter 转化

  • 添加属性

如果要为对象添加响应式的属性, 可以使用 this.$set(this.obj, key , newValue)

直接使用 Object.assign() 也无法添加相应式属性, 应该把混合后的对象赋值给原来的对象 (使用解构会丢失响应性)

this.obj=Object.assign({}, this.obj, {a:1, b:2})

  • 删除属性 this.$delete(this.obj, key)

data 声明后不允许新增

必须在初始化实例前声明所有根级响应式 property哪怕只是一个空值, 不允许动态添加

<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  data() {
    return {
      // 声明 message 为一个空值字符串
      message: ''
    }
  },
  // 可以在 mounted 钩子中设置初始值
  mounted() {
    this.message = 'Hello!'
  }
}
</script>

异步更新队列

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环"tick"中Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

this.message = '第一次更新'
this.$nextTick(()=>console.log(this.message))
this.message = '第二次更新'
this.message = '第三次更新'

// DOM只会更新一次显示"第三次更新"

在数据变化之后立即使用 vm.$nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

  1. 同步代码执行阶段
    • this.message = '第一次更新':触发响应式更新,将 watcher 加入队列
    • this.$nextTick(...):将回调函数注册到 nextTick 队列
    • this.message = '第二次更新':去重处理(同一个 watcher 不会重复入队)
    • this.message = '第三次更新':同上
  2. 异步更新阶段下一个事件循环tick
    • 第一步:执行 DOM 更新(此时 message 的最终值是 '第三次更新'
    • 第二步:执行 nextTick 回调函数
  3. 控制台输出结果: '第三次更新'
  • JS 标准的事件循环机制
[调用栈] → [微任务队列] → [渲染] → [宏任务队列] → [微任务队列] → ...
  • 在 Vue 中
[调用栈]
  │
  ├─ 同步代码执行包括Vue数据修改 → 收集到更新队列)
  │
[微任务队列]Vue在此插入两个关键点
  │
  ├─ [Vue前置flush] → 处理watch/computed等
  │
  ├─ [Vue主更新]    → 执行组件re-render (这就是"tick"的核心)
  │
  ├─ [nextTick回调] → 此时DOM已更新
  │
  ├─ 其他微任务Promise等
  │
[渲染](如需重绘)
  │
[宏任务队列]
  │
  ├─ setTimeout/setInterval等
  │
  ├─ 事件回调
  │
[新的微任务队列](循环继续...

举例:

methods: {
  async update() {
    // ====== 阶段1同步执行当前调用栈/当前tick ======
    this.a = 1                   // [同步] 数据变更加入当前tick的更新队列
    console.log('同步代码')       // [同步] 立即执行
    
    await Promise.resolve()      // [微任务边界] 后续代码包装为微任务
    // ------ 此处是tick分界线 ------
    
    // ====== 阶段2微任务执行下一个tick ======
    this.b = 2                   // [微任务] 数据变更加入新tick的更新队列
    
    setTimeout(() => {           // [宏任务] 回调推入任务队列
      // ====== 阶段3宏任务执行未来tick ======
      this.c = 3                 // [宏任务] 触发独立更新周期
    }, 0)
  }
}

流程分析:

[主线程调用栈]
│
├─ 1. this.a = 1        (加入当前tick更新队列)
├─ 2. console.log       (立即执行)
├─ 3. await Promise     (后续代码包装为微任务)
│
[微任务阶段]
│
├─ 4. Vue DOM更新       (处理a=1的变更)
├─ 5. this.b = 2        (加入新tick更新队列)
├─ 6. 注册setTimeout    (回调进入宏任务队列)
│
[宏任务阶段]
│
├─ 7. setTimeout回调执行
   ├─ 8. this.c = 3     (触发新的更新周期)
   ├─ 9. Vue DOM更新    (处理c=3的变更)