重读Vue教程

Vue.js 是什么?

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。

Vue 只关注视图层, 采用自底向上增量开发的设计。

Vue 是一个提供了 MVVM 模式的双向数据绑定的 Javascript 库,专注于 View 层。它的核心是 MVVM 中的 VM,也就是 ViewModel。 ViewModel 负责连接 View 和 Model,保证视图和数据的一致性。

MVVM是什么?

MVVM 是 Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对 View 和 ViewModel 的双向数据绑定,这使得 View 的变化会自动更新到 ViewModel,ViewModel 的变化也会自动同步到View 上显示,即所谓的数据双向绑定

  • View: 视图,即 DOM;对应 HTML 部分(代表 UI 组件),它负责将数据模型转化成 UI 展现出来。
  • Model: 模型,即数据模型;对应 Vue 组件里的 data,或者说是 vuex 里的数据;也可以在 Model 中定义数据修改和操作的业务逻辑。
  • ViewModel: 监听模型数据,也就是 data 的的改变和控制视图行为、处理用户交互,简单理解就是一个同步 View 和 Model 的对象,连接 Model 和 View。

MVVM 是基于 MVC 的设计,开发人员在 HTML 标签上写一些指定,利用一些指令绑定,就能在 Model 和ViewModel 保持不变的情况下,很方便的将 UI 设计与业务逻辑分离,从而大大的减少繁琐的 DOM 操作。

MVVM 框架与 MVC 框架的主要区别有两点:

  • 实现数据与视图的分离
  • 通过数据来驱动视图,开发者只需要关心数据变化,DOM操作被封装了。

Vue响应式原理

一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

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

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

Class 与 Style 绑定

v-bind 绑定 classstyle 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

<div v-bind:class="(isActive ? 'active' : '') + ' error'"></div>
<div v-bind:class="[isActive ? 'active' : '', 'error']"></div>
<div v-bind:class="{active: isActive, error: true }"></div>
<div v-bind:class="[{ active: isActive }, 'error']"></div>
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

<!--数组语法可以将多个样式对象应用到同一个元素上-->
<div v-bind:style="[
    { color: activeColor, fontSize: fontSize + 'px' },
  { border: '1px solid red'}
]"></div>

<!--2.3.0起,可以传一个包含多个值的数组,常用于提供多个带前缀的值-->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

事件处理

事件的绑定

  • v-on 指令: 指令可以直接运行一些 JavaScript 代码,也可以绑定事件处理方法,还可以在内联 JavaScript 语句中调用方法。

    <button v-on:click="window.alert('hello')">hello</button>
    <button v-on:click="hello">hello</button>
    <button v-on:click="hello()">hello</button>
    

    需要注意:绑定事件处理方法,不能传参,但会将 event 做为方法的第一个参数;而直接调用方法 hello(),是可以指定传参的。同时,如果需要用到 event 时,可以传入特殊变量 $event

    <button v-on:click="hello('lizh', $event)">hello</button>
    
  • vm.$on( event, callback ): 监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。

    vm.$on('test', msg => {
        console.log(msg)
    })
    vm.$emit('test', 'hello')
    // hello
    

将原生事件绑定到组件

使用 v-on.native 修饰符,可以在一个组件的根元素上直接监听一个原生事件。

<my-component v-on:focus.native="onFocus"></my-component>

不过,有时候你可能并不是想将事件传递给组件的根元素,而是组件中的某个特定元素。

Vue 提供了一个 $listeners property,它是一个对象,里面包含了作用在这个组件上的所有监听器。再配合 v-on="$listeners" 将所有的事件监听器指向组件中的某个特定的子元素。

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

2.4.0 开始,v-on 同样支持不带参数绑定一个事件/监听器键值对的对象。注意当使用对象语法时,是不支持任何修饰器的。

<input v-on="$listeners"></input>

事件修饰符

  • .stop: 阻止事件冒泡;
  • .prevent: 阻止事件默认行为;
  • .capture: 添加事件监听器时使用事件捕获模式;
  • .self: 只当在 event.target 是当前元素自身时触发处理函数;
  • .once: 事件将只会触发一次;
  • .passive: 事件的默认行为会立即触发,优先级高于 prevent

  • :keyup.enter: 监听键盘 enter 按键;

  • :keyup.page-down: 监听键盘 pageDon 按键;

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击。

需要注意:键盘按键的键值,在不同浏览器可以不一样。Vue 内置了以下别名,支持旧浏览器:.enter, .tab, .delete, .esc, .space, .up, .down, .left, .righ

组件注册

全局注册:

Vue.component('my-component-name', {
  // ... 选项 ...
})

局部注册:

const ComponentA = { /* ... */ }
const ComponentB = {
  components: {
    'component-a': ComponentA
  },
  // ...
}

自动化全局注册:

Webpack 有一个 API require.context(),用来获取指定的文件夹内的特定文件。require.context 返回的结果是一个函数,该函数的传参是匹配的文件名的相对路径,返回的是一个模块。

require.context(directory,useSubdirectories,regExp)

require.context 返回的函数,还有以下三个属性:

  • resolve:函数,接文件夹下面匹配文件的相对路径,返回该文件相对于整个工程的相对路径;

  • keys:函数,返回匹配成功文件的名字组成的数组;

  • id:执行环境的id,返回的是一个字符串,主要用在 module.hot.accept ,应该是热加载?

我们可以使用 require.context 全局注册一些非常通用的基础组件。

// 入口文件,如:src/main.js

import Vue from 'vue'
const requireComponent = require.context(
  '../../../components/form_items', // 其组件目录的相对路径
  false, // 是否查询其子目录
  /\.(vue|js)$/ // 匹配基础组件文件名的正则表达式
)
requireComponent.keys().forEach(fileName => {
  const componentName = fileName.split('/')[1].split('.')[0]
  const componentConfig = requireComponent(fileName)

  Vue.component(
    componentName,
    // 如果这个组件选项是通过 `export default` 导出的,那么就会优先使用 `.default`;
    // 否则回退到使用模块的根。
    componentConfig.default || componentConfig
  )
})

Prop和 Attribute

props 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值。

props: {
  propB: {
        type: [String, Array], // 原生构造函数,或原生构造函数组成的数组。包括:String、Number、Boolean、Array、Object、Date、Function、Symbol、任何自定义构造函数。
    default: any || () => ({}), // 默认值。对象或数组的默认值必须从一个工厂函数返回。
        required:Boolean, // 是否是必传项
    validator: value => { // 自定义验证函数,将该 prop 的值作为唯一的参数代入。
      return boolean
    }
  }
}

prop传值

传入一个对象的所有 property:

如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind (取代 v-bind:prop-name)。例如,对于一个给定的对象 post

post: {
  id: 1,
  title: 'My Journey with Vue'
}
<blog-post v-bind="post"></blog-post>

<!--等价于-->
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>

自定义类型:

function Person (firstName, lastName) {
  this.firstName = firstName
  this.lastName = lastName
}
props: {
  author: Person
}
<my-component :author="new Person('firstName', 'lastName')"></my-component>

通过 instanceof 来来验证 prop 的值是否是通过 new Person 创建的。

非 Prop 的 Attribute

一个非 prop 的 attribute 是指传向一个组件,但是该组件 prop 没有定义对应的 property。

对于绝大多数 attribute 来说,从外部提供给组件的值会替换掉组件根元素设置好的值。庆幸的是,classstyle attribute 会稍微智能一些,两边的值会被合并起来。

非 Prop 的 Attribute 通过 $attrs 来访问。 如果你希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false

prop的“伪"双向绑定

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。也就是说,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值,但是,你应该在一个子组件内部改变父组件传递的 prop。

子组件修改父组件的 prop,需要在父组件中监听一个事件,并根据需要更新一个本地的数据:

// 父组件:监听一个事件,用于更新本地的数据
<text-document
  v-bind:title="doc.title"
  v-on:update:title="doc.title = $event"
></text-document>
// 子组件:触发父组件的监听事件,传递更新的值
this.$emit('update:title', newTitle)

为了方便起见,我们为这种模式提供一个缩写,即 .sync 修饰符:

// 父组件:.sync修饰符,相当于添加了一个 update:title 监听事件
<text-document v-bind:title.sync="doc.title"></text-document>

自定义指令

除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令。因为,有的情况下,你仍然需要对普通 DOM 元素进行底层操作。如:当页面加载时,该元素将获得焦点。

注册全局指令:

Vue.directive('focus', {
  bind: function (el, binding, vnode) {},
  inserted: function (el, binding, vnode) {
    el.focus()
  },
  update: function (el, binding, vnode, oldVnode) {},
  componentUpdated: function (el, binding, vnode, oldVnode) {},
  unbind: function (el, binding, vnode) {}
})

注册局部指令,在组件中定义 directives 选项:

directives: {
  focus: {
    inserted: function (el, binding, vnode) {
      el.focus()
    }
  }
}

然后你可以在模板中任何元素上使用新的 v-focus property:

<input v-focus placeholder="自动聚集此元素">

钩子函数

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

  • bind: 只调用一次,指令第一次绑定到元素时调用;
  • inserted: 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中);
  • update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前

  • componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind: 只调用一次,指令与元素解绑时调用。

钩子函数的参数

指令钩子函数会被传入以下参数:

  • el: 指令所绑定的元素,可以用来直接操作 DOM。除了 el 之外,其它参数都应该是只读的
  • binding: 一个对象,包含以下 property:
    • name:指令名,不包括 v- 前缀;
    • value:指令绑定的值;
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用;
    • expression:指令绑定的字符串形式的指令表达式;
    • arg:传给指令的参数。如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnode:当前虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

函数简写

如果只想在 bindupdate 时触发相同行为,而不关心其它的钩子,可以这样写:

Vue.directive('my-directive', function (el, binding) {
  el.style.backgroundColor = binding.value
})

过滤器

自定义过滤器可被用于一些常见的文本格式化。比如:双花括号插值和 v-bind 表达式。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:

{{ message | capitalize }}
<div v-bind:id="rawId | formatId"></div>

全局定义过滤器:

Vue.filter('capitalize', function (value) {
  return value ? value.replace(/^\w/, a => a.toUpperCase()) : ''
})

局部定义过滤器,在组件的选项中定义 filters 选项:

filters: {
  capitalize: function (value) {
    return value ? value.replace(/^\w/, a => a.toUpperCase()) : ''
  }
}

过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。同时,它可以接收参数:

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

这里,filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。

Vue API

Vue.use( plugin )

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。

该方法需要在调用 new Vue() 之前被调用。

install 方法被同一个插件多次调用,插件将只会被安装一次。

provide / inject

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 SymbolReflect.ownKeys 的环境下可工作。

inject 选项应该是:一个字符串数组,或一个对象。对象的 key 是本地的绑定名,value 可以是父组件中定义的 provide 中的 key,也可以是一个对象,包含 from、default 两个 property

// 父组件
export default {
  provide () {
    return {
      provideInst: this
    }
  },
}

// 子组件
export default {
  inject: {
    aliasInst: {
      from: 'provideInst',
      default: () => ({})
    }
  },
}

model

允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。

默认情况下:

<my-component v-model="name"></my-component>

相当于:

<my-component
    :value="name"
    @input="val => { name = val }"
></my-component>

如果我们在子组件 my-component 加入:

export default {
  model: {
    prop: 'testValue',
    event: 'change'
  },
}

那么:

<my-component v-model="name"></my-component>
<!--相当于-->
<my-component
    :testValue="name"
    @change="val => { name = val }"
></my-component>

v-on

绑定事件监听器。事件类型由参数指定,表达式可以是一个方法的名字或一个 Javascript 内联语句,还可以加对应修饰符(也可以省略)。

用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件。

在监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个 $event property : v-on:click="handle('ok', $event)"

从 2.4.0 开始,v-on 同样支持不带参数绑定一个事件/监听器键值对的对象。注意当使用对象语法时,是不支持任何修饰符的。

<my-component v-on:click="ckEvent"></my-component>
<my-component v-on:click="ckEvent('a', $event)"></my-component>
<my-component v-on:click.native="ckEvent"></my-component>
<my-component v-on:myEvent="doSomething"></my-component>
<my-component v-on="myEventObject"></my-component>
myEventObject: {
  click () => {},
    change () => {},
    myEvent () => {}
}

v-bind

动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。在绑定 prop 时,prop 必须在子组件中声明。

可以用修饰符指定不同的绑定类型。

  • .prop : 作为一个 DOM property 绑定而不是作为 attribute 绑定,即不会传递到子组件的 $attrs;

  • .camel: (2.1.0+) 将 kebab-case attribute 名转换为 camelCase;

  • .sync: (2.3.0+) 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器;

    <text-document v-bind:title.sync="doc.title"></text-document>
    <!--相当于-->
    <text-document
      v-bind:title="doc.title"
      v-on:update:title="doc.title = $event"
    ></text-document>
    

没有参数时,可以绑定到一个包含键值对的对象。注意此时 classstyle 绑定不支持数组和对象。

<my-component v-bind="$props"></my-component>

key

Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。

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

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

ref

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

v-for 用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组

常见问题

数据响应问题
  • 必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值;

  • Vue 不能检测对象的 property 的添加或移除: 由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

    对于已经创建的实例,可以使用 Vue.set(object, propertyName, value)vm.$set 实例方法将 property 转换为响应式 。

  • Vue 不能检测数组的某些变化: 利用索引直接设置一个数组项时、修改数组的长度。

    同样,可以使用 Vue.setvm.$set 来转换。

    另外,以下数组的变更方法会触发视图更新:push()、 pop()、 shift()、 unshift()、 splice()、 sort()、 reverse()

异步更新问题

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么。比如:

<my-component id="MyComponent" v-if="visible"></my-component>
this.visible = true
const height = document.querySelector('#MyComponent').clientHeight

这种情况下,document.querySelector('#MyComponent') 很可能会报错,因为 this.visible = true 修改后,DOM 的更新是一个异步操作,也就是这个时候,DOM 是还不存在的。

为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

this.visible = true
let height = null
this.$nextTick(() => {
    height = document.querySelector('#MyComponent').clientHeight  
})
为什么 data 必须是一个函数?

因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

为什么不推荐 v-for 与 v-if 一同使用?

当它们处于同一节点,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。

<ul>
  <li v-for="user in users" :key="user.id" v-if="user.isActive">
    {{ user.name }}
  </li>
</ul>

假设 users 有100个子项,而user.isActive 的子项只有一个。实际执行中, li 将被循环100次,再通过 v-if 判断,过滤不符合条件的99个。从性能角度来看,这不太友好。

推荐做法,将 users 替换为一个计算属性,让其返回过滤后的列表:

computed: {
  activeUsers () => this.users.filter(u => u.isActive)
}
<ul>
  <li v-for="user in activeUsers" :key="user.id">
    {{ user.name }}
  </li>
</ul>
this作用域问题
  • 避免在 setTimeout、setInterval 内使用 this。因为,内部的 this 指向的不是 Vue 实例。

  • 不要在选项 property 或回调上使用箭头函数。

    created: () => console.log(this.a)
    vm.$watch('a', newValue => this.myMethod())
    

    因为箭头函数并没有 thisthis 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefinedUncaught TypeError: this.myMethod is not a function 之类的错误。

computed、watch、methods的区别

计算属性基于 data 中声明过或者父组件传递的 props 中的数据通过计算得到的一个新值,这个新值只会根据所依赖的值的变化而变化,简言之:这个属性依赖其他属性,由其他属性计算而来的。

同样,我们可以将一个计算属性定义为一个方法,在需要引用计算属性的地方直接调用该方法。两种方式的最终结果确实是完全相同的。不同的是计算属性是基于它们的响应式依赖进行缓存的。也就是说,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。相比之下,多处引用方法,每次都会再执行函数。

侦听属性监听 Vue 实例上的属性变化,或某些特定数据的变化,然后执行某些具体的业务逻辑操作。当属性变化时,回调函数自动调用,在函数内部进行计算。其可以监听的数据来源:data,props,computed 内的数据。

虽然计算属性在大多数情况下更合适,但有时也需要自定义的侦听属性。比如:当需要在数据变化时执行异步或开销较大的操作时、一个数据改变影响多个数据。

注意: 组件初始化时,如果 watch 对象的选项 immediate 值为 true,则该 watch 计算会先于 computed 计算,其执行顺序:beforeCreate -> watch -> created -> beforeMount -> computed(如果计算的属性未使用,则不会触发) -> mounted。

Vue 组件有哪些通信方式?

8 种常规的 Vue 组件通信方案:

  • 通过 props 传递。

  • 通过 $emit 触发自定义事件。

  • 使用 ref。

  • $parent 或$root。

  • $attrs 与 $listeners:逐级向下传属性 $attrs 和 $listeners。

  • Provide 与 Inject:在祖先组件定义 provide 属性,返回传递的值;在后代组件通过inject接收组件传递过来的值。

  • Vuex:共享数据中心。

  • EventBus(适用兄弟组件传值):创建一个中央事件总线 EventBus,兄弟组件通过 $emit 触发自定义事件,另一个兄弟组件通过 $on 监听自定义事件。

    // Bus.js
    class Bus {  
      constructor() {
        this.callbacks = {};
      }
      $on(name, fn) {
        this.callbacks[name] = this.callbacks[name] || [];  
        this.callbacks[name].push(fn);
      }
      $emit(name, args) {
        if (this.callbacks[name]) {
          this.callbacks[name].forEach((cb) => cb(args));  
        }
      }
    }
    
    // main.js  
    Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  
    // 另一种方式  
    Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能
    
    <!-- Children1.vue -->
    this.$bus.$emit('foo')
    
    <!-- Children2.vue -->
    this.$bus.$on('foo', this.handle)
    

参考资料

Vue.js教程

© lizhao all right reserved,powered by Gitbook文件修订时间: 2022-10-28 21:54:41

results matching ""

    No results matching ""