多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
# vue官方教程 # 安装 ### [](https://cn.vuejs.org/v2/guide/installation.html#%E5%85%BC%E5%AE%B9%E6%80%A7 "兼容性")兼容性 Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有[兼容 ECMAScript 5 的浏览器](https://caniuse.com/#feat=es5)。 ### [](https://cn.vuejs.org/v2/guide/installation.html#%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97 "更新日志")更新日志 最新稳定版本:2.5.17 每个版本的更新日志见 [GitHub](https://github.com/vuejs/vue/releases)。 ## [Vue Devtools](https://cn.vuejs.org/v2/guide/installation.html#Vue-Devtools "Vue Devtools") 在使用 Vue 时,我们推荐在你的浏览器上安装 [Vue Devtools](https://github.com/vuejs/vue-devtools#vue-devtools)。它允许你在一个更友好的界面中审查和调试 Vue 应用。 ## [直接用 `<script>` 引入](https://cn.vuejs.org/v2/guide/installation.html#%E7%9B%B4%E6%8E%A5%E7%94%A8-lt-script-gt-%E5%BC%95%E5%85%A5 "直接用 引入") 直接下载并用 `<script>` 标签引入,`Vue` 会被注册为一个全局变量。 在开发环境下不要使用压缩版本,不然你就失去了所有常见错误相关的警告! [开发版本](https://vuejs.org/js/vue.js)包含完整的警告和调试模式 [生产版本](https://vuejs.org/js/vue.min.js)删除了警告,30.90KB min+gzip ### [CDN](https://cn.vuejs.org/v2/guide/installation.html#CDN "CDN") 我们推荐链接到一个你可以手动更新的指定版本号: | script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js">script> | 你可以在 [cdn.jsdelivr.net/npm/vue](https://cdn.jsdelivr.net/npm/vue/) 浏览 NPM 包的源代码。 Vue 也可以在 [unpkg](https://unpkg.com/vue@2.5.17/dist/vue.js) 和 [cdnjs](https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js) 上获取 (cdnjs 的版本更新可能略滞后)。 请确认了解[不同构建版本](https://cn.vuejs.org/v2/guide/installation.html#%E5%AF%B9%E4%B8%8D%E5%90%8C%E6%9E%84%E5%BB%BA%E7%89%88%E6%9C%AC%E7%9A%84%E8%A7%A3%E9%87%8A)并在你发布的站点中使用生产环境版本,把 `vue.js` 换成 `vue.min.js`。这是一个更小的构建,可以带来比开发环境下更快的速度体验。 ## [NPM](https://cn.vuejs.org/v2/guide/installation.html#NPM "NPM") 在用 Vue 构建大型应用时推荐使用 NPM 安装[[1]](https://cn.vuejs.org/v2/guide/installation.html#footnote-1)。NPM 能很好地和诸如 [webpack](https://webpack.js.org/) 或 [Browserify](http://browserify.org/) 模块打包器配合使用。同时 Vue 也提供配套工具来开发[单文件组件](https://cn.vuejs.org/v2/guide/single-file-components.html)。 | # 最新稳定版 $ npm install vue | ## [命令行工具 (CLI)](https://cn.vuejs.org/v2/guide/installation.html#%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%B7%A5%E5%85%B7-CLI "命令行工具 (CLI)") Vue 提供了一个[官方的 CLI](https://github.com/vuejs/vue-cli),为单页面应用 (SPA) 快速搭建繁杂的脚手架。它为现代前端工作流提供了 batteries-included 的构建设置。只需要几分钟的时间就可以运行起来并带有热重载、保存时 lint 校验,以及生产环境可用的构建版本。更多详情可查阅 [Vue CLI 的文档](https://cli.vuejs.org/)。 CLI 工具假定用户对 Node.js 和相关构建工具有一定程度的了解。如果你是新手,我们强烈建议先在不用构建工具的情况下通读[指南](https://cn.vuejs.org/v2/guide/),在熟悉 Vue 本身之后再使用 CLI。 ## [对不同构建版本的解释](https://cn.vuejs.org/v2/guide/installation.html#%E5%AF%B9%E4%B8%8D%E5%90%8C%E6%9E%84%E5%BB%BA%E7%89%88%E6%9C%AC%E7%9A%84%E8%A7%A3%E9%87%8A "对不同构建版本的解释") 在 [NPM 包的 `dist/` 目录](https://cdn.jsdelivr.net/npm/vue/dist/)你将会找到很多不同的 Vue.js 构建版本。这里列出了它们之间的差别: | | UMD | CommonJS | ES Module | | --- | --- | --- | --- | | 完整版 | vue.js | vue.common.js | vue.esm.js | | 只包含运行时版 | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js | | 完整版 (生产环境) | vue.min.js | - | - | | 只包含运行时版 (生产环境) | vue.runtime.min.js | - | - | ### [术语](https://cn.vuejs.org/v2/guide/installation.html#%E6%9C%AF%E8%AF%AD "术语") * 完整版:同时包含编译器和运行时的版本。 * 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。 * 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。 * [UMD](https://github.com/umdjs/umd):UMD 版本可以通过 `<script>` 标签直接用在浏览器中。jsDelivr CDN 的 [https://cdn.jsdelivr.net/npm/vue](https://cdn.jsdelivr.net/npm/vue) 默认文件就是运行时 + 编译器的 UMD 版本 (`vue.js`)。 * [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1):CommonJS 版本用来配合老的打包工具比如 [Browserify](http://browserify.org/) 或 [webpack 1](https://webpack.github.io/)。这些打包工具的默认文件 (`pkg.main`) 是只包含运行时的 CommonJS 版本 (`vue.runtime.common.js`)。 * [ES Module](http://exploringjs.com/es6/ch_modules.html):ES module 版本用来配合现代打包工具比如 [webpack 2](https://webpack.js.org/) 或 [Rollup](https://rollupjs.org/)。这些打包工具的默认文件 (`pkg.module`) 是只包含运行时的 ES Module 版本 (`vue.runtime.esm.js`)。 ### [运行时 + 编译器 vs. 只包含运行时](https://cn.vuejs.org/v2/guide/installation.html#%E8%BF%90%E8%A1%8C%E6%97%B6-%E7%BC%96%E8%AF%91%E5%99%A8-vs-%E5%8F%AA%E5%8C%85%E5%90%AB%E8%BF%90%E8%A1%8C%E6%97%B6 "运行时 + 编译器 vs. 只包含运行时") 如果你需要在客户端编译模板 (比如传入一个字符串给 `template` 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版: | // 需要编译器 new Vue({ template: '{{ hi }}' }) // 不需要编译器 new Vue({ render (h) { return h('div', this.hi) } }) | 当使用 `vue-loader` 或 `vueify` 的时候,`*.vue` 文件内部的模板会在构建时预编译成 JavaScript。你在最终打好的包里实际上是不需要编译器的,所以只用运行时版本即可。 因为运行时版本相比完整版体积要小大约 30%,所以应该尽可能使用这个版本。如果你仍然希望使用完整版,则需要在打包工具里配置一个别名: #### [](https://cn.vuejs.org/v2/guide/installation.html#webpack "webpack")webpack | module.exports = { // ... resolve: { alias: { 'vue$': 'vue/dist/vue.esm.js' // 用 webpack 1 时需用 'vue/dist/vue.common.js' } } } | #### [](https://cn.vuejs.org/v2/guide/installation.html#Rollup "Rollup")Rollup | const alias = require('rollup-plugin-alias') rollup({ // ... plugins: [ alias({ 'vue': 'vue/dist/vue.esm.js' }) ] }) | #### [](https://cn.vuejs.org/v2/guide/installation.html#Browserify "Browserify")Browserify 添加到你项目的 `package.json`: | { // ... "browser": { "vue": "vue/dist/vue.common.js" } } | #### [](https://cn.vuejs.org/v2/guide/installation.html#Parcel "Parcel")Parcel 在你项目的 `package.json` 中添加: | { // ... "alias": { "vue" : "./node_modules/vue/dist/vue.common.js" } } | ### [开发环境 vs. 生产环境模式](https://cn.vuejs.org/v2/guide/installation.html#%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83-vs-%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E6%A8%A1%E5%BC%8F "开发环境 vs. 生产环境模式") 对于 UMD 版本来说,开发环境/生产环境模式是硬编码好的:开发环境下用未压缩的代码,生产环境下使用压缩后的代码。 CommonJS 和 ES Module 版本是用于打包工具的,因此我们不提供压缩后的版本。你需要自行将最终的包进行压缩。 CommonJS 和 ES Module 版本同时保留原始的 `process.env.NODE_ENV` 检测,以决定它们应该运行在什么模式下。你应该使用适当的打包工具配置来替换这些环境变量以便控制 Vue 所运行的模式。把 `process.env.NODE_ENV` 替换为字符串字面量同时可以让 UglifyJS 之类的压缩工具完全丢掉仅供开发环境的代码块,以减少最终的文件尺寸。 #### [](https://cn.vuejs.org/v2/guide/installation.html#webpack-1 "webpack")webpack 在 webpack 4+ 中,你可以使用 `mode` 选项: | module.exports = { mode: 'production' } | 但是在 webpack 3 及其更低版本中,你需要使用 [DefinePlugin](https://webpack.js.org/plugins/define-plugin/): | var webpack = require('webpack') module.exports = { // ... plugins: [ // ... new webpack.DefinePlugin({ 'process.env': { NODE_ENV: JSON.stringify('production') } }) ] } | #### [](https://cn.vuejs.org/v2/guide/installation.html#Rollup-1 "Rollup")Rollup 使用 [rollup-plugin-replace](https://github.com/rollup/rollup-plugin-replace): | const replace = require('rollup-plugin-replace') rollup({ // ... plugins: [ replace({ 'process.env.NODE_ENV': JSON.stringify('production') }) ] }).then(...) | #### [](https://cn.vuejs.org/v2/guide/installation.html#Browserify-1 "Browserify")Browserify 为你的包应用一次全局的 [envify](https://github.com/hughsk/envify) 转换。 | NODE_ENV=production browserify -g envify -e main.js | uglifyjs -c -m > build.js | 也可以移步[生产环境部署](https://cn.vuejs.org/v2/guide/deployment.html)。 ### [CSP 环境](https://cn.vuejs.org/v2/guide/installation.html#CSP-%E7%8E%AF%E5%A2%83 "CSP 环境") 有些环境,如 Google Chrome Apps,会强制应用内容安全策略 (CSP),不能使用 `new Function()` 对表达式求值。这时可以用 CSP 兼容版本。完整版本依赖于该功能来编译模板,所以无法在这些环境下使用。 另一方面,运行时版本则是完全兼容 CSP 的。当通过 [webpack + vue-loader](https://github.com/vuejs-templates/webpack-simple) 或者 [Browserify + vueify](https://github.com/vuejs-templates/browserify-simple) 构建时,模板将被预编译为 `render` 函数,可以在 CSP 环境中完美运行。 ## [开发版本](https://cn.vuejs.org/v2/guide/installation.html#%E5%BC%80%E5%8F%91%E7%89%88%E6%9C%AC "开发版本") 重要: GitHub 仓库的 `/dist` 文件夹只有在新版本发布时才会提交。如果想要使用 GitHub 上 Vue 最新的源码,你需要自己构建! | git clone https://github.com/vuejs/vue.git node_modules/vue cd node_modules/vue npm install npm run build | ## [Bower](https://cn.vuejs.org/v2/guide/installation.html#Bower "Bower") Bower 只提供 UMD 版本。 | # 最新稳定版本 $ bower install vue | ## [AMD 模块加载器](https://cn.vuejs.org/v2/guide/installation.html#AMD-%E6%A8%A1%E5%9D%97%E5%8A%A0%E8%BD%BD%E5%99%A8 "AMD 模块加载器") 所有 UMD 版本都可以直接用作 AMD 模块。 译者注 [1] 对于中国大陆用户,建议将 NPM 源设置为[国内的镜像](https://npm.taobao.org/),可以大幅提升安装速度。 # 介绍 ## [Vue.js 是什么](https://cn.vuejs.org/v2/guide/index.html#Vue-js-%E6%98%AF%E4%BB%80%E4%B9%88 "Vue.js 是什么") Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与[现代化的工具链](https://cn.vuejs.org/v2/guide/single-file-components.html)以及各种[支持类库](https://github.com/vuejs/awesome-vue#libraries--plugins)结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。 如果你想在深入学习 Vue 之前对它有更多了解,我们[制作了一个视频](https://cn.vuejs.org/v2/guide/index.html#),带您了解其核心概念和一个示例工程。 如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看[对比其它框架](https://cn.vuejs.org/v2/guide/comparison.html)。 ## [起步](https://cn.vuejs.org/v2/guide/index.html#%E8%B5%B7%E6%AD%A5 "起步") 官方指南假设你已了解关于 HTML、CSS 和 JavaScript 的中级知识。如果你刚开始学习前端开发,将框架作为你的第一步可能不是最好的主意——掌握好基础知识再来吧!之前有其它框架的使用经验会有帮助,但这不是必需的。 尝试 Vue.js 最简单的方法是使用 [JSFiddle 上的 Hello World 例子](https://jsfiddle.net/chrisvfritz/50wL7mdz/)。你可以在浏览器新标签页中打开它,跟着例子学习一些基础用法。或者你也可以[创建一个 `.html` 文件](https://gist.githubusercontent.com/chrisvfritz/7f8d7d63000b48493c336e48b3db3e52/raw/ed60c4e5d5c6fec48b0921edaed0cb60be30e87c/index.html),然后通过如下方式引入 Vue: | script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js">script> | 或者: | script src="https://cdn.jsdelivr.net/npm/vue">script> | [安装教程](https://cn.vuejs.org/guide/installation.html)给出了更多安装 Vue 的方式。请注意我们不推荐新手直接使用 `vue-cli`,尤其是在你还不熟悉基于 Node.js 的构建工具时。 如果你喜欢交互式的东西,你也可以查阅[这个 Scrimba 上的系列教程](https://scrimba.com/playlist/pXKqta),它揉合了录屏和代码试验田,并允许你随时暂停和播放。 ## [声明式渲染](https://cn.vuejs.org/v2/guide/index.html#%E5%A3%B0%E6%98%8E%E5%BC%8F%E6%B8%B2%E6%9F%93 "声明式渲染") [在 Scrimba 上尝试这节课](https://scrimba.com/p/pXKqta/cQ3QVcr) Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统: | div id="app"> {{ message }} div> | | var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } }) | Hello Vue! 我们已经成功创建了第一个 Vue 应用!看起来这跟渲染一个字符串模板非常类似,但是 Vue 在背后做了大量工作。现在数据和 DOM 已经被建立了关联,所有东西都是响应式的。我们要怎么确认呢?打开你的浏览器的 JavaScript 控制台 (就在这个页面打开),并修改 `app.message` 的值,你将看到上例相应地更新。 除了文本插值,我们还可以像这样来绑定元素特性: | div id="app-2"> span v-bind:title="message"> 鼠标悬停几秒钟查看此处动态绑定的提示信息! span> div> | | var app2 = new Vue({ el: '#app-2', data: { message: '页面加载于 ' + new Date().toLocaleString() } }) | 鼠标悬停几秒钟查看此处动态绑定的提示信息! 这里我们遇到了一点新东西。你看到的 `v-bind` 特性被称为指令。指令带有前缀 `v-`,以表示它们是 Vue 提供的特殊特性。可能你已经猜到了,它们会在渲染的 DOM 上应用特殊的响应式行为。在这里,该指令的意思是:“将这个元素节点的 `title` 特性和 Vue 实例的 `message` 属性保持一致”。 如果你再次打开浏览器的 JavaScript 控制台,输入 `app2.message = '新消息'`,就会再一次看到这个绑定了 `title` 特性的 HTML 已经进行了更新。 ## [条件与循环](https://cn.vuejs.org/v2/guide/index.html#%E6%9D%A1%E4%BB%B6%E4%B8%8E%E5%BE%AA%E7%8E%AF "条件与循环") [在 Scrimba 上尝试这节课](https://scrimba.com/p/pXKqta/cEQe4SJ) 控制切换一个元素是否显示也相当简单: | div id="app-3"> p v-if="seen">现在你看到我了p> div> | | var app3 = new Vue({ el: '#app-3', data: { seen: true } }) | 现在你看到我了 继续在控制台输入 `app3.seen = false`,你会发现之前显示的消息消失了。 这个例子演示了我们不仅可以把数据绑定到 DOM 文本或特性,还可以绑定到 DOM 结构。此外,Vue 也提供一个强大的过渡效果系统,可以在 Vue 插入/更新/移除元素时自动应用[过渡效果](https://cn.vuejs.org/v2/guide/transitions.html)。 还有其它很多指令,每个都有特殊的功能。例如,`v-for` 指令可以绑定数组的数据来渲染一个项目列表: | div id="app-4"> ol> li v-for="todo in todos"> {{ todo.text }} li> ol> div> | | var app4 = new Vue({ el: '#app-4', data: { todos: [ { text: '学习 JavaScript' }, { text: '学习 Vue' }, { text: '整个牛项目' } ] } }) | 1. 学习 JavaScript 2. 学习 Vue 3. 整个牛项目 在控制台里,输入 `app4.todos.push({ text: '新项目' })`,你会发现列表最后添加了一个新项目。 ## [处理用户输入](https://cn.vuejs.org/v2/guide/index.html#%E5%A4%84%E7%90%86%E7%94%A8%E6%88%B7%E8%BE%93%E5%85%A5 "处理用户输入") [在 Scrimba 上尝试这节课](https://scrimba.com/p/pXKqta/czPNaUr) 为了让用户和你的应用进行交互,我们可以用 `v-on` 指令添加一个事件监听器,通过它调用在 Vue 实例中定义的方法: | div id="app-5"> p>{{ message }}p> button v-on:click="reverseMessage">逆转消息button> div> | | var app5 = new Vue({ el: '#app-5', data: { message: 'Hello Vue.js!' }, methods: { reverseMessage: function () { this.message = this.message.split('').reverse().join('') } } }) | Hello Vue.js! 逆转消息 注意在 `reverseMessage` 方法中,我们更新了应用的状态,但没有触碰 DOM——所有的 DOM 操作都由 Vue 来处理,你编写的代码只需要关注逻辑层面即可。 Vue 还提供了 `v-model` 指令,它能轻松实现表单输入和应用状态之间的双向绑定。 | div id="app-6"> p>{{ message }}p> input v-model="message"> div> | | var app6 = new Vue({ el: '#app-6', data: { message: 'Hello Vue!' } }) | Hello Vue! ## [组件化应用构建](https://cn.vuejs.org/v2/guide/index.html#%E7%BB%84%E4%BB%B6%E5%8C%96%E5%BA%94%E7%94%A8%E6%9E%84%E5%BB%BA "组件化应用构建") [在 Scrimba 上尝试这节课](https://scrimba.com/p/pXKqta/cEQVkA3) 组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。仔细想想,几乎任意类型的应用界面都可以抽象为一个组件树: ![Component Tree](https://cn.vuejs.org/images/components.png) 在 Vue 里,一个组件本质上是一个拥有预定义选项的一个 Vue 实例。在 Vue 中注册组件很简单: | // 定义名为 todo-item 的新组件 Vue.component('todo-item', { template: '这是个待办项' }) | 现在你可以用它构建另一个组件模板: | ol> todo-item>todo-item> ol> | 但是这样会为每个待办项渲染同样的文本,这看起来并不炫酷。我们应该能从父作用域将数据传到子组件才对。让我们来修改一下组件的定义,使之能够接受一个 [prop](https://cn.vuejs.org/v2/guide/components.html#Props): | Vue.component('todo-item', { // todo-item 组件现在接受一个 // "prop",类似于一个自定义特性。 // 这个 prop 名为 todo。 props: ['todo'], template: '{{ todo.text }}' }) | 现在,我们可以使用 `v-bind` 指令将待办项传到循环输出的每个组件中: | div id="app-7"> ol> 现在我们为每个 todo-item 提供 todo 对象 todo 对象是变量,即其内容可以是动态的。 我们也需要为每个组件提供一个“key”,稍后再 作详细解释。 --> todo-item v-for="item in groceryList" v-bind:todo="item" v-bind:key="item.id"> todo-item> ol> div> | | Vue.component('todo-item', { props: ['todo'], template: '{{ todo.text }}' }) var app7 = new Vue({ el: '#app-7', data: { groceryList: [ { id: 0, text: '蔬菜' }, { id: 1, text: '奶酪' }, { id: 2, text: '随便其它什么人吃的东西' } ] } }) | 1. 蔬菜 2. 奶酪 3. 随便其它什么人吃的东西 尽管这只是一个刻意设计的例子,但是我们已经设法将应用分割成了两个更小的单元。子单元通过 prop 接口与父单元进行了良好的解耦。我们现在可以进一步改进 `<todo-item>` 组件,提供更为复杂的模板和逻辑,而不会影响到父单元。 在一个大型应用中,有必要将整个应用程序划分为组件,以使开发更易管理。在[后续教程](https://cn.vuejs.org/v2/guide/components.html)中我们将详述组件,不过这里有一个 (假想的) 例子,以展示使用了组件的应用模板是什么样的: | div id="app"> app-nav>app-nav> app-view> app-sidebar>app-sidebar> app-content>app-content> app-view> div> | ### [与自定义元素的关系](https://cn.vuejs.org/v2/guide/index.html#%E4%B8%8E%E8%87%AA%E5%AE%9A%E4%B9%89%E5%85%83%E7%B4%A0%E7%9A%84%E5%85%B3%E7%B3%BB "与自定义元素的关系") 你可能已经注意到 Vue 组件非常类似于自定义元素——它是 [Web 组件规范](https://www.w3.org/wiki/WebComponents/)的一部分,这是因为 Vue 的组件语法部分参考了该规范。例如 Vue 组件实现了 [Slot API](https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md) 与 `is` 特性。但是,还是有几个关键差别: 1. Web Components 规范已经完成并通过,但未被所有浏览器原生实现。目前 Safari 10.1+、Chrome 54+ 和 Firefox 63+ 原生支持 Web Components。相比之下,Vue 组件不需要任何 polyfill,并且在所有支持的浏览器 (IE9 及更高版本) 之下表现一致。必要时,Vue 组件也可以包装于原生自定义元素之内。 2. Vue 组件提供了纯自定义元素所不具备的一些重要功能,最突出的是跨组件数据流、自定义事件通信以及构建工具集成。 ## [准备好了吗?](https://cn.vuejs.org/v2/guide/index.html#%E5%87%86%E5%A4%87%E5%A5%BD%E4%BA%86%E5%90%97%EF%BC%9F "准备好了吗?") 我们刚才简单介绍了 Vue 核心最基本的功能——本教程的其余部分将涵盖这些功能以及其它高级功能更详细的细节,所以请务必读完整个教程! # Vue 实例 ## [创建一个 Vue 实例](https://cn.vuejs.org/v2/guide/instance.html#%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AA-Vue-%E5%AE%9E%E4%BE%8B "创建一个 Vue 实例") 每个 Vue 应用都是通过用 `Vue` 函数创建一个新的 Vue 实例开始的: | var vm = new Vue({ // 选项 }) | 虽然没有完全遵循 [MVVM 模型](https://zh.wikipedia.org/wiki/MVVM),但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 `vm` (ViewModel 的缩写) 这个变量名表示 Vue 实例。 当创建一个 Vue 实例时,你可以传入一个选项对象。这篇教程主要描述的就是如何使用这些选项来创建你想要的行为。作为参考,你也可以在 [API 文档](https://cn.vuejs.org/v2/api/#%E9%80%89%E9%A1%B9-%E6%95%B0%E6%8D%AE) 中浏览完整的选项列表。 一个 Vue 应用由一个通过 `new Vue` 创建的根 Vue 实例,以及可选的嵌套的、可复用的组件树组成。举个例子,一个 todo 应用的组件树可以是这样的: | 根实例 └─ TodoList ├─ TodoItem │ ├─ DeleteTodoButton │ └─ EditTodoButton └─ TodoListFooter ├─ ClearTodosButton └─ TodoListStatistics | 我们会在稍后的[组件系统](https://cn.vuejs.org/v2/guide/components.html)章节具体展开。不过现在,你只需要明白所有的 Vue 组件都是 Vue 实例,并且接受相同的选项对象 (一些根实例特有的选项除外)。 ## [数据与方法](https://cn.vuejs.org/v2/guide/instance.html#%E6%95%B0%E6%8D%AE%E4%B8%8E%E6%96%B9%E6%B3%95 "数据与方法") 当一个 Vue 实例被创建时,它向 Vue 的响应式系统中加入了其 `data` 对象中能找到的所有的属性。当这些属性的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。 | // 我们的数据对象 var data = { a: 1 } // 该对象被加入到一个 Vue 实例中 var vm = new Vue({ data: data }) // 获得这个实例上的属性 // 返回源数据中对应的字段 vm.a == data.a // => true // 设置属性也会影响到原始数据 vm.a = 2 data.a // => 2 // ……反之亦然 data.a = 3 vm.a // => 3 | 当这些数据改变时,视图会进行重渲染。值得注意的是只有当实例被创建时 `data` 中存在的属性才是响应式的。也就是说如果你添加一个新的属性,比如: | vm.b = 'hi' | 那么对 `b` 的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个属性,但是一开始它为空或不存在,那么你仅需要设置一些初始值。比如: | data: { newTodoText: '', visitCount: 0, hideCompletedTodos: false, todos: [], error: null } | 这里唯一的例外是使用 `Object.freeze()`,这会阻止修改现有的属性,也意味着响应系统无法再*追踪*变化。 | var obj = { foo: 'bar' } Object.freeze(obj) new Vue({ el: '#app', data: obj }) | | div id="app"> p>{{ foo }}p> button v-on:click="foo = 'baz'">Change itbutton> div> | 除了数据属性,Vue 实例还暴露了一些有用的实例属性与方法。它们都有前缀 `$`,以便与用户定义的属性区分开来。例如: | var data = { a: 1 } var vm = new Vue({ el: '#example', data: data }) vm.$data === data // => true vm.$el === document.getElementById('example') // => true // $watch 是一个实例方法 vm.$watch('a', function (newValue, oldValue) { // 这个回调将在 `vm.a` 改变后调用 }) | 以后你可以在 [API 参考](https://cn.vuejs.org/v2/api/#%E5%AE%9E%E4%BE%8B%E5%B1%9E%E6%80%A7)中查阅到完整的实例属性和方法的列表。 ## [实例生命周期钩子](https://cn.vuejs.org/v2/guide/instance.html#%E5%AE%9E%E4%BE%8B%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%92%A9%E5%AD%90 "实例生命周期钩子") 每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。 比如 [`created`](https://cn.vuejs.org/v2/api/#created) 钩子可以用来在一个实例被创建之后执行代码: | new Vue({ data: { a: 1 }, created: function () { // `this` 指向 vm 实例 console.log('a is: ' + this.a) } }) // => "a is: 1" | 也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 [`mounted`](https://cn.vuejs.org/v2/api/#mounted)、[`updated`](https://cn.vuejs.org/v2/api/#updated)和 [`destroyed`](https://cn.vuejs.org/v2/api/#destroyed)。生命周期钩子的 `this` 上下文指向调用它的 Vue 实例。 不要在选项属性或回调上使用[箭头函数](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions),比如 `created: () => console.log(this.a)` 或 `vm.$watch('a', newValue => this.myMethod())`。因为箭头函数是和父级上下文绑定在一起的,`this` 不会是如你所预期的 Vue 实例,经常导致 `Uncaught TypeError: Cannot read property of undefined` 或 `Uncaught TypeError: this.myMethod is not a function` 之类的错误。 ## [生命周期图示](https://cn.vuejs.org/v2/guide/instance.html#%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%9B%BE%E7%A4%BA "生命周期图示") 下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。 ![Vue 实例生命周期](https://cn.vuejs.org/images/lifecycle.png) # 模板语法 Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。 在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。 如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,[直接写渲染 (render) 函数](https://cn.vuejs.org/v2/guide/render-function.html),使用可选的 JSX 语法。 ## [插值](https://cn.vuejs.org/v2/guide/syntax.html#%E6%8F%92%E5%80%BC "插值") ### [文本](https://cn.vuejs.org/v2/guide/syntax.html#%E6%96%87%E6%9C%AC "文本") 数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值: | span>Message: {{ msg }}span> | Mustache 标签将会被替代为对应数据对象上 `msg` 属性的值。无论何时,绑定的数据对象上 `msg` 属性发生了改变,插值处的内容都会更新。 通过使用 [v-once 指令](https://cn.vuejs.org/v2/api/#v-once),你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定: | span v-once>这个将不会改变: {{ msg }}span> | ### [原始 HTML](https://cn.vuejs.org/v2/guide/syntax.html#%E5%8E%9F%E5%A7%8B-HTML "原始 HTML") 双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 `v-html` 指令: | p>Using mustaches: {{ rawHtml }}p> p>Using v-html directive: span v-html="rawHtml">span>p> | Using mustaches: This should be red. Using v-html directive: This should be red. 这个 `span` 的内容将会被替换成为属性值 `rawHtml`,直接作为 HTML——会忽略解析属性值中的数据绑定。注意,你不能使用 `v-html` 来复合局部模板,因为 Vue 不是基于字符串的模板引擎。反之,对于用户界面 (UI),组件更适合作为可重用和可组合的基本单位。 你的站点上动态渲染的任意 HTML 可能会非常危险,因为它很容易导致 [XSS 攻击](https://en.wikipedia.org/wiki/Cross-site_scripting)。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。 ### [特性](https://cn.vuejs.org/v2/guide/syntax.html#%E7%89%B9%E6%80%A7 "特性") Mustache 语法不能作用在 HTML 特性上,遇到这种情况应该使用 [v-bind 指令](https://cn.vuejs.org/v2/api/#v-bind): | div v-bind:id="dynamicId">div> | 在布尔特性的情况下,它们的存在即暗示为 `true`,`v-bind` 工作起来略有不同,在这个例子中: | button v-bind:disabled="isButtonDisabled">Buttonbutton> | 如果 `isButtonDisabled` 的值是 `null`、`undefined` 或 `false`,则 `disabled` 特性甚至不会被包含在渲染出来的 `<button>` 元素中。 ### [使用 JavaScript 表达式](https://cn.vuejs.org/v2/guide/syntax.html#%E4%BD%BF%E7%94%A8-JavaScript-%E8%A1%A8%E8%BE%BE%E5%BC%8F "使用 JavaScript 表达式") 迄今为止,在我们的模板中,我们一直都只绑定简单的属性键值。但实际上,对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式支持。 | {{ number + 1 }} {{ ok ? 'YES' : 'NO' }} {{ message.split('').reverse().join('') }} div v-bind:id="'list-' + id">div> | 这些表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。 | {{ var a = 1 }} {{ if (ok) { return message } }} | 模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 `Math` 和 `Date` 。你不应该在模板表达式中试图访问用户定义的全局变量。 ## [指令](https://cn.vuejs.org/v2/guide/syntax.html#%E6%8C%87%E4%BB%A4 "指令") 指令 (Directives) 是带有 `v-` 前缀的特殊特性。指令特性的值预期是单个 JavaScript 表达式 (`v-for` 是例外情况,稍后我们再讨论)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。回顾我们在介绍中看到的例子: | p v-if="seen">现在你看到我了p> | 这里,`v-if` 指令将根据表达式 `seen` 的值的真假来插入/移除 `<p>` 元素。 ### [参数](https://cn.vuejs.org/v2/guide/syntax.html#%E5%8F%82%E6%95%B0 "参数") 一些指令能够接收一个“参数”,在指令名称之后以冒号表示。例如,`v-bind` 指令可以用于响应式地更新 HTML 特性: | a v-bind:href="url">...a> | 在这里 `href` 是参数,告知 `v-bind` 指令将该元素的 `href` 特性与表达式 `url` 的值绑定。 另一个例子是 `v-on` 指令,它用于监听 DOM 事件: | a v-on:click="doSomething">...a> | 在这里参数是监听的事件名。我们也会更详细地讨论事件处理。 ### [修饰符](https://cn.vuejs.org/v2/guide/syntax.html#%E4%BF%AE%E9%A5%B0%E7%AC%A6 "修饰符") 修饰符 (Modifiers) 是以半角句号 `.` 指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,`.prevent` 修饰符告诉 `v-on` 指令对于触发的事件调用 `event.preventDefault()`: | form v-on:submit.prevent="onSubmit">...form> | 在接下来对 [`v-on`](https://cn.vuejs.org/v2/guide/events.html#%E4%BA%8B%E4%BB%B6%E4%BF%AE%E9%A5%B0%E7%AC%A6) 和 [`v-for`](https://cn.vuejs.org/v2/guide/forms.html#%E4%BF%AE%E9%A5%B0%E7%AC%A6) 等功能的探索中,你会看到修饰符的其它例子。 ## [缩写](https://cn.vuejs.org/v2/guide/syntax.html#%E7%BC%A9%E5%86%99 "缩写") `v-` 前缀作为一种视觉提示,用来识别模板中 Vue 特定的特性。当你在使用 Vue.js 为现有标签添加动态行为 (dynamic behavior) 时,`v-` 前缀很有帮助,然而,对于一些频繁用到的指令来说,就会感到使用繁琐。同时,在构建由 Vue.js 管理所有模板的[单页面应用程序 (SPA - single page application)](https://en.wikipedia.org/wiki/Single-page_application) 时,`v-` 前缀也变得没那么重要了。因此,Vue.js 为 `v-bind` 和 `v-on` 这两个最常用的指令,提供了特定简写: ### [`v-bind` 缩写](https://cn.vuejs.org/v2/guide/syntax.html#v-bind-%E7%BC%A9%E5%86%99 "v-bind 缩写") | a v-bind:href="url">...a> a :href="url">...a> | ### [`v-on` 缩写](https://cn.vuejs.org/v2/guide/syntax.html#v-on-%E7%BC%A9%E5%86%99 "v-on 缩写") | a v-on:click="doSomething">...a> a @click="doSomething">...a> | 它们看起来可能与普通的 HTML 略有不同,但 `:` 与 `@` 对于特性名来说都是合法字符,在所有支持 Vue.js 的浏览器都能被正确地解析。而且,它们不会出现在最终渲染的标记中。缩写语法是完全可选的,但随着你更深入地了解它们的作用,你会庆幸拥有它们。 # 计算属性和侦听器 ## [计算属性](https://cn.vuejs.org/v2/guide/computed.html#%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7 "计算属性") 模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如: | div id="example"> {{ message.split('').reverse().join('') }} div> | 在这个地方,模板不再是简单的声明式逻辑。你必须看一段时间才能意识到,这里是想要显示变量 `message` 的翻转字符串。当你想要在模板中多次引用此处的翻转字符串时,就会更加难以处理。 所以,对于任何复杂逻辑,你都应当使用计算属性。 ### [基础例子](https://cn.vuejs.org/v2/guide/computed.html#%E5%9F%BA%E7%A1%80%E4%BE%8B%E5%AD%90 "基础例子") | div id="example"> p>Original message: "{{ message }}"p> p>Computed reversed message: "{{ reversedMessage }}"p> div> | | var vm = new Vue({ el: '#example', data: { message: 'Hello' }, computed: { // 计算属性的 getter reversedMessage: function () { // `this` 指向 vm 实例 return this.message.split('').reverse().join('') } } }) | 结果: Original message: "Hello" Computed reversed message: "olleH" 这里我们声明了一个计算属性 `reversedMessage`。我们提供的函数将用作属性 `vm.reversedMessage` 的 getter 函数: | console.log(vm.reversedMessage) // => 'olleH' vm.message = 'Goodbye' console.log(vm.reversedMessage) // => 'eybdooG' | 你可以打开浏览器的控制台,自行修改例子中的 vm。`vm.reversedMessage` 的值始终取决于 `vm.message` 的值。 你可以像绑定普通属性一样在模板中绑定计算属性。Vue 知道 `vm.reversedMessage`依赖于 `vm.message`,因此当 `vm.message` 发生改变时,所有依赖 `vm.reversedMessage` 的绑定也会更新。而且最妙的是我们已经以声明的方式创建了这种依赖关系:计算属性的 getter 函数是没有副作用 (side effect) 的,这使它更易于测试和理解。 ### [计算属性缓存 vs 方法](https://cn.vuejs.org/v2/guide/computed.html#%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7%E7%BC%93%E5%AD%98-vs-%E6%96%B9%E6%B3%95 "计算属性缓存 vs 方法") 你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果: | p>Reversed message: "{{ reversedMessage() }}"p> | | // 在组件中 methods: { reversedMessage: function () { return this.message.split('').reverse().join('') } } | 我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。只在相关依赖发生改变时它们才会重新求值。这就意味着只要 `message` 还没有发生改变,多次访问 `reversedMessage` 计算属性会立即返回之前的计算结果,而不必再次执行函数。 这也同样意味着下面的计算属性将不再更新,因为 `Date.now()` 不是响应式依赖: | computed: { now: function () { return Date.now() } } | 相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。 我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 A,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 A 。如果没有缓存,我们将不可避免的多次执行 A 的 getter!如果你不希望有缓存,请用方法来替代。 ### [计算属性 vs 侦听属性](https://cn.vuejs.org/v2/guide/computed.html#%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7-vs-%E4%BE%A6%E5%90%AC%E5%B1%9E%E6%80%A7 "计算属性 vs 侦听属性") Vue 提供了一种更通用的方式来观察和响应 Vue 实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 `watch`——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 `watch` 回调。细想一下这个例子: | div id="demo">{{ fullName }}div> | | var vm = new Vue({ el: '#demo', data: { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' }, watch: { firstName: function (val) { this.fullName = val + ' ' + this.lastName }, lastName: function (val) { this.fullName = this.firstName + ' ' + val } } }) | 上面代码是命令式且重复的。将它与计算属性的版本进行比较: | var vm = new Vue({ el: '#demo', data: { firstName: 'Foo', lastName: 'Bar' }, computed: { fullName: function () { return this.firstName + ' ' + this.lastName } } }) | 好得多了,不是吗? ### [计算属性的 setter](https://cn.vuejs.org/v2/guide/computed.html#%E8%AE%A1%E7%AE%97%E5%B1%9E%E6%80%A7%E7%9A%84-setter "计算属性的 setter") 计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter : | // ... computed: { fullName: { // getter get: function () { return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } // ... | 现在再运行 `vm.fullName = 'John Doe'` 时,setter 会被调用,`vm.firstName` 和 `vm.lastName` 也会相应地被更新。 ## [侦听器](https://cn.vuejs.org/v2/guide/computed.html#%E4%BE%A6%E5%90%AC%E5%99%A8 "侦听器") 虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 `watch` 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。 例如: | div id="watch-example"> p> Ask a yes/no question: input v-model="question"> p> p>{{ answer }}p> div> | | script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js">script> script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js">script> script> var watchExampleVM = new Vue({ el: '#watch-example', data: { question: '', answer: 'I cannot give you an answer until you ask a question!' }, watch: { // 如果 `question` 发生改变,这个函数就会运行 question: function (newQuestion, oldQuestion) { this.answer = 'Waiting for you to stop typing...' this.debouncedGetAnswer() } }, created: function () { // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。 // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率 // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于 // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识, // 请参考:https://lodash.com/docs#debounce this.debouncedGetAnswer = _.debounce(this.getAnswer, 500) }, methods: { getAnswer: function () { if (this.question.indexOf('?') === -1) { this.answer = 'Questions usually contain a question mark. ;-)' return } this.answer = 'Thinking...' var vm = this axios.get('https://yesno.wtf/api') .then(function (response) { vm.answer = _.capitalize(response.data.answer) }) .catch(function (error) { vm.answer = 'Error! Could not reach the API. ' + error }) } } }) script> | 结果: Ask a yes/no question:  I cannot give you an answer until you ask a question! 在这个示例中,使用 `watch` 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。 除了 `watch` 选项之外,您还可以使用命令式的 [vm.$watch API](https://cn.vuejs.org/v2/api/#vm-watch)。 # Class 与 Style 绑定 操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是属性,所以我们可以用 `v-bind` 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 `v-bind` 用于 `class` 和 `style` 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。 ## [绑定 HTML Class](https://cn.vuejs.org/v2/guide/class-and-style.html#%E7%BB%91%E5%AE%9A-HTML-Class "绑定 HTML Class") ### [对象语法](https://cn.vuejs.org/v2/guide/class-and-style.html#%E5%AF%B9%E8%B1%A1%E8%AF%AD%E6%B3%95 "对象语法") 我们可以传给 `v-bind:class` 一个对象,以动态地切换 class: | div v-bind:class="{ active: isActive }">div> | 上面的语法表示 `active` 这个 class 存在与否将取决于数据属性 `isActive` 的 [truthiness](https://developer.mozilla.org/zh-CN/docs/Glossary/Truthy)。 你可以在对象中传入更多属性来动态切换多个 class。此外,`v-bind:class` 指令也可以与普通的 class 属性共存。当有如下模板: | div class="static" v-bind:class="{ active: isActive, 'text-danger': hasError }"> div> | 和如下 data: | data: { isActive: true, hasError: false } | 结果渲染为: | div class="static active">div> | 当 `isActive` 或者 `hasError` 变化时,class 列表将相应地更新。例如,如果 `hasError` 的值为 `true`,class 列表将变为 `"static active text-danger"`。 绑定的数据对象不必内联定义在模板里: | div v-bind:class="classObject">div> | | data: { classObject: { active: true, 'text-danger': false } } | 渲染的结果和上面一样。我们也可以在这里绑定一个返回对象的[计算属性](https://cn.vuejs.org/v2/guide/computed.html)。这是一个常用且强大的模式: | div v-bind:class="classObject">div> | | data: { isActive: true, error: null }, computed: { classObject: function () { return { active: this.isActive && !this.error, 'text-danger': this.error && this.error.type === 'fatal' } } } | ### [数组语法](https://cn.vuejs.org/v2/guide/class-and-style.html#%E6%95%B0%E7%BB%84%E8%AF%AD%E6%B3%95 "数组语法") 我们可以把一个数组传给 `v-bind:class`,以应用一个 class 列表: | div v-bind:class="[activeClass, errorClass]">div> | | data: { activeClass: 'active', errorClass: 'text-danger' } | 渲染为: | div class="active text-danger">div> | 如果你也想根据条件切换列表中的 class,可以用三元表达式: | div v-bind:class="[isActive ? activeClass : '', errorClass]">div> | 这样写将始终添加 `errorClass`,但是只有在 `isActive` 是 truthy[[1]](https://cn.vuejs.org/v2/guide/class-and-style.html#footnote-1) 时才添加 `activeClass`。 不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法: | div v-bind:class="[{ active: isActive }, errorClass]">div> | ### [用在组件上](https://cn.vuejs.org/v2/guide/class-and-style.html#%E7%94%A8%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A "用在组件上") > 这个章节假设你已经对 [Vue 组件](https://cn.vuejs.org/v2/guide/components.html)有一定的了解。当然你也可以先跳过这里,稍后再回过头来看。 当在一个自定义组件上使用 `class` 属性时,这些类将被添加到该组件的根元素上面。这个元素上已经存在的类不会被覆盖。 例如,如果你声明了这个组件: | Vue.component('my-component', { template: 'Hi' }) | 然后在使用它的时候添加一些 class: | my-component class="baz boo">my-component> | HTML 将被渲染为: | p class="foo bar baz boo">Hip> | 对于带数据绑定 class 也同样适用: | my-component v-bind:class="{ active: isActive }">my-component> | 当 `isActive` 为 truthy[[1]](https://cn.vuejs.org/v2/guide/class-and-style.html#footnote-1) 时,HTML 将被渲染成为: | p class="foo bar active">Hip> | ## [绑定内联样式](https://cn.vuejs.org/v2/guide/class-and-style.html#%E7%BB%91%E5%AE%9A%E5%86%85%E8%81%94%E6%A0%B7%E5%BC%8F "绑定内联样式") ### [对象语法](https://cn.vuejs.org/v2/guide/class-and-style.html#%E5%AF%B9%E8%B1%A1%E8%AF%AD%E6%B3%95-1 "对象语法") `v-bind:style` 的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS 属性名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用单引号括起来) 来命名: | div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }">div> | | data: { activeColor: 'red', fontSize: 30 } | 直接绑定到一个样式对象通常更好,这会让模板更清晰: | div v-bind:style="styleObject">div> | | data: { styleObject: { color: 'red', fontSize: '13px' } } | 同样的,对象语法常常结合返回对象的计算属性使用。 ### [数组语法](https://cn.vuejs.org/v2/guide/class-and-style.html#%E6%95%B0%E7%BB%84%E8%AF%AD%E6%B3%95-1 "数组语法") `v-bind:style` 的数组语法可以将多个样式对象应用到同一个元素上: | div v-bind:style="[baseStyles, overridingStyles]">div> | ### [自动添加前缀](https://cn.vuejs.org/v2/guide/class-and-style.html#%E8%87%AA%E5%8A%A8%E6%B7%BB%E5%8A%A0%E5%89%8D%E7%BC%80 "自动添加前缀") 当 `v-bind:style` 使用需要添加[浏览器引擎前缀](https://developer.mozilla.org/zh-CN/docs/Glossary/Vendor_Prefix)的 CSS 属性时,如 `transform`,Vue.js 会自动侦测并添加相应的前缀。 ### [多重值](https://cn.vuejs.org/v2/guide/class-and-style.html#%E5%A4%9A%E9%87%8D%E5%80%BC "多重值") > 2.3.0+ 从 2.3.0 起你可以为 `style` 绑定中的属性提供一个包含多个值的数组,常用于提供多个带前缀的值,例如: | div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }">div> | 这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 `display: flex`。 译者注 [1] truthy 不是 `true`,详见 [MDN](https://developer.mozilla.org/zh-CN/docs/Glossary/Truthy) 的解释。 # 条件渲染 ## [`v-if`](https://cn.vuejs.org/v2/guide/conditional.html#v-if "v-if") 在字符串模板中,比如 Handlebars,我们得像这样写一个条件块: | {{#if ok}} h1>Yesh1> {{/if}} | 在 Vue 中,我们使用 `v-if` 指令实现同样的功能: | h1 v-if="ok">Yesh1> | 也可以用 `v-else` 添加一个“else 块”: | h1 v-if="ok">Yesh1> h1 v-else>Noh1> | ### [在 `<template>` 元素上使用 `v-if` 条件渲染分组](https://cn.vuejs.org/v2/guide/conditional.html#%E5%9C%A8-lt-template-gt-%E5%85%83%E7%B4%A0%E4%B8%8A%E4%BD%BF%E7%94%A8-v-if-%E6%9D%A1%E4%BB%B6%E6%B8%B2%E6%9F%93%E5%88%86%E7%BB%84 "在 元素上使用 v-if 条件渲染分组") 因为 `v-if` 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 `<template>` 元素当做不可见的包裹元素,并在上面使用 `v-if`。最终的渲染结果将不包含 `<template>` 元素。 | template v-if="ok"> h1>Titleh1> p>Paragraph 1p> p>Paragraph 2p> template> | ### [`v-else`](https://cn.vuejs.org/v2/guide/conditional.html#v-else "v-else") 你可以使用 `v-else` 指令来表示 `v-if` 的“else 块”: | div v-if="Math.random() > 0.5"> Now you see me div> div v-else> Now you don't div> | `v-else` 元素必须紧跟在带 `v-if` 或者 `v-else-if` 的元素的后面,否则它将不会被识别。 ### [`v-else-if`](https://cn.vuejs.org/v2/guide/conditional.html#v-else-if "v-else-if") > 2.1.0 新增 `v-else-if`,顾名思义,充当 `v-if` 的“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-else`,`v-else-if` 也必须紧跟在带 `v-if` 或者 `v-else-if` 的元素之后。 ### [用 `key` 管理可复用的元素](https://cn.vuejs.org/v2/guide/conditional.html#%E7%94%A8-key-%E7%AE%A1%E7%90%86%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E5%85%83%E7%B4%A0 "用 key 管理可复用的元素") Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做除了使 Vue 变得非常快之外,还有其它一些好处。例如,如果你允许用户在不同的登录方式之间切换: | template v-if="loginType === 'username'"> label>Usernamelabel> input placeholder="Enter your username"> template> template v-else> label>Emaillabel> input placeholder="Enter your email address"> template> | 那么在上面的代码中切换 `loginType` 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,`<input>` 不会被替换掉——仅仅是替换了它的 `placeholder`。 自己动手试一试,在输入框中输入一些文本,然后按下切换按钮: Username  Toggle login type 这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 `key` 属性即可: | template v-if="loginType === 'username'"> label>Usernamelabel> input placeholder="Enter your username" key="username-input"> template> template v-else> label>Emaillabel> input placeholder="Enter your email address" key="email-input"> template> | 现在,每次切换时,输入框都将被重新渲染。请看: Username  Toggle login type 注意,`<label>` 元素仍然会被高效地复用,因为它们没有添加 `key` 属性。 ## [`v-show`](https://cn.vuejs.org/v2/guide/conditional.html#v-show "v-show") 另一个用于根据条件展示元素的选项是 `v-show` 指令。用法大致一样: | h1 v-show="ok">Hello!h1> | 不同的是带有 `v-show` 的元素始终会被渲染并保留在 DOM 中。`v-show` 只是简单地切换元素的 CSS 属性 `display`。 注意,`v-show` 不支持 `<template>` 元素,也不支持 `v-else`。 ## [`v-if` vs `v-show`](https://cn.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show "v-if vs v-show") `v-if` 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。 `v-if` 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。 相比之下,`v-show` 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。 一般来说,`v-if` 有更高的切换开销,而 `v-show` 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 `v-show` 较好;如果在运行时条件很少改变,则使用 `v-if` 较好。 ## [`v-if` 与 `v-for` 一起使用](https://cn.vuejs.org/v2/guide/conditional.html#v-if-%E4%B8%8E-v-for-%E4%B8%80%E8%B5%B7%E4%BD%BF%E7%94%A8 "v-if 与 v-for 一起使用") 不推荐同时使用 `v-if` 和 `v-for`。请查阅[风格指南](https://cn.vuejs.org/v2/style-guide/#%E9%81%BF%E5%85%8D-v-if-%E5%92%8C-v-for-%E7%94%A8%E5%9C%A8%E4%B8%80%E8%B5%B7-%E5%BF%85%E8%A6%81)以获取更多信息。 当 `v-if` 与 `v-for` 一起使用时,`v-for` 具有比 `v-if` 更高的优先级。请查阅[列表渲染指南](https://cn.vuejs.org/v2/guide/list.html#v-for-with-v-if) 以获取详细信息。 # 列表渲染 ## [用 `v-for` 把一个数组对应为一组元素](https://cn.vuejs.org/v2/guide/list.html#%E7%94%A8-v-for-%E6%8A%8A%E4%B8%80%E4%B8%AA%E6%95%B0%E7%BB%84%E5%AF%B9%E5%BA%94%E4%B8%BA%E4%B8%80%E7%BB%84%E5%85%83%E7%B4%A0 "用 v-for 把一个数组对应为一组元素") 我们用 `v-for` 指令根据一组数组的选项列表进行渲染。`v-for` 指令需要使用 `item in items` 形式的特殊语法,`items` 是源数据数组并且 `item` 是数组元素迭代的别名。 | ul id="example-1"> li v-for="item in items"> {{ item.message }} li> ul> | | var example1 = new Vue({ el: '#example-1', data: { items: [ { message: 'Foo' }, { message: 'Bar' } ] } }) | 结果: * Foo * Bar 在 `v-for` 块中,我们拥有对父作用域属性的完全访问权限。`v-for` 还支持一个可选的第二个参数为当前项的索引。 | ul id="example-2"> li v-for="(item, index) in items"> {{ parentMessage }} - {{ index }} - {{ item.message }} li> ul> | | var example2 = new Vue({ el: '#example-2', data: { parentMessage: 'Parent', items: [ { message: 'Foo' }, { message: 'Bar' } ] } }) | 结果: * Parent - 0 - Foo * Parent - 1 - Bar 你也可以用 `of` 替代 `in` 作为分隔符,因为它是最接近 JavaScript 迭代器的语法: | div v-for="item of items">div> | ## [一个对象的 `v-for`](https://cn.vuejs.org/v2/guide/list.html#%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%9A%84-v-for "一个对象的 v-for") 你也可以用 `v-for` 通过一个对象的属性来迭代。 | ul id="v-for-object" class="demo"> li v-for="value in object"> {{ value }} li> ul> | | new Vue({ el: '#v-for-object', data: { object: { firstName: 'John', lastName: 'Doe', age: 30 } } }) | 结果: * John * Doe * 30 你也可以提供第二个的参数为键名: | div v-for="(value, key) in object"> {{ key }}: {{ value }} div> | firstName: John lastName: Doe age: 30 第三个参数为索引: | div v-for="(value, key, index) in object"> {{ index }}. {{ key }}: {{ value }} div> | 0\. firstName: John 1\. lastName: Doe 2\. age: 30 在遍历对象时,是按 `Object.keys()` 的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下是一致的。 ## [`key`](https://cn.vuejs.org/v2/guide/list.html#key "key") 当 Vue.js 用 `v-for` 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。这个类似 Vue 1.x 的 `track-by="$index"` 。 这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。 为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 `key` 属性。理想的 `key` 值是每项都有的唯一 id。这个特殊的属性相当于 Vue 1.x 的 `track-by` ,但它的工作方式类似于一个属性,所以你需要用 `v-bind` 来绑定动态值 (在这里使用简写): | div v-for="item in items" :key="item.id"> div> | 建议尽可能在使用 `v-for` 时提供 `key`,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。 因为它是 Vue 识别节点的一个通用机制,`key` 并不与 `v-for` 特别关联,key 还具有其他用途,我们将在后面的指南中看到其他用途。 ## [数组更新检测](https://cn.vuejs.org/v2/guide/list.html#%E6%95%B0%E7%BB%84%E6%9B%B4%E6%96%B0%E6%A3%80%E6%B5%8B "数组更新检测") ### [变异方法](https://cn.vuejs.org/v2/guide/list.html#%E5%8F%98%E5%BC%82%E6%96%B9%E6%B3%95 "变异方法") Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。这些方法如下: * `push()` * `pop()` * `shift()` * `unshift()` * `splice()` * `sort()` * `reverse()` 你打开控制台,然后用前面例子的 `items` 数组调用变异方法:`example1.items.push({ message: 'Baz' })` 。 ### [替换数组](https://cn.vuejs.org/v2/guide/list.html#%E6%9B%BF%E6%8D%A2%E6%95%B0%E7%BB%84 "替换数组") 变异方法 (mutation method),顾名思义,会改变被这些方法调用的原始数组。相比之下,也有非变异 (non-mutating method) 方法,例如:`filter()`, `concat()` 和 `slice()` 。这些不会改变原始数组,但总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组: | example1.items = example1.items.filter(function (item) { return item.message.match(/Foo/) }) | 你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的、启发式的方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。 ### [注意事项](https://cn.vuejs.org/v2/guide/list.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9 "注意事项") 由于 JavaScript 的限制,Vue 不能检测以下变动的数组: 1. 当你利用索引直接设置一个项时,例如:`vm.items[indexOfItem] = newValue` 2. 当你修改数组的长度时,例如:`vm.items.length = newLength` 举个例子: | var vm = new Vue({ data: { items: ['a', 'b', 'c'] } }) vm.items[1] = 'x' // 不是响应性的 vm.items.length = 2 // 不是响应性的 | 为了解决第一类问题,以下两种方式都可以实现和 `vm.items[indexOfItem] = newValue` 相同的效果,同时也将触发状态更新: | // Vue.set Vue.set(vm.items, indexOfItem, newValue) | | // Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue) | 你也可以使用 [`vm.$set`](https://vuejs.org/v2/api/#vm-set) 实例方法,该方法是全局方法 `Vue.set` 的一个别名: | vm.$set(vm.items, indexOfItem, newValue) | 为了解决第二类问题,你可以使用 `splice`: | vm.items.splice(newLength) | ## [对象更改检测注意事项](https://cn.vuejs.org/v2/guide/list.html#%E5%AF%B9%E8%B1%A1%E6%9B%B4%E6%94%B9%E6%A3%80%E6%B5%8B%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9 "对象更改检测注意事项") 还是由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除: | var vm = new Vue({ data: { a: 1 } }) // `vm.a` 现在是响应式的 vm.b = 2 // `vm.b` 不是响应式的 | 对于已经创建的实例,Vue 不能动态添加根级别的响应式属性。但是,可以使用 `Vue.set(object, key, value)` 方法向嵌套对象添加响应式属性。例如,对于: | var vm = new Vue({ data: { userProfile: { name: 'Anika' } } }) | 你可以添加一个新的 `age` 属性到嵌套的 `userProfile` 对象: | Vue.set(vm.userProfile, 'age', 27) | 你还可以使用 `vm.$set` 实例方法,它只是全局 `Vue.set` 的别名: | vm.$set(vm.userProfile, 'age', 27) | 有时你可能需要为已有对象赋予多个新属性,比如使用 `Object.assign()` 或 `_.extend()`。在这种情况下,你应该用两个对象的属性创建一个新的对象。所以,如果你想添加新的响应式属性,不要像这样: | Object.assign(vm.userProfile, { age: 27, favoriteColor: 'Vue Green' }) | 你应该这样做: | vm.userProfile = Object.assign({}, vm.userProfile, { age: 27, favoriteColor: 'Vue Green' }) | ## [显示过滤/排序结果](https://cn.vuejs.org/v2/guide/list.html#%E6%98%BE%E7%A4%BA%E8%BF%87%E6%BB%A4-%E6%8E%92%E5%BA%8F%E7%BB%93%E6%9E%9C "显示过滤/排序结果") 有时,我们想要显示一个数组的过滤或排序副本,而不实际改变或重置原始数据。在这种情况下,可以创建返回过滤或排序数组的计算属性。 例如: | li v-for="n in evenNumbers">{{ n }}li> | | data: { numbers: [ 1, 2, 3, 4, 5 ] }, computed: { evenNumbers: function () { return this.numbers.filter(function (number) { return number % 2 === 0 }) } } | 在计算属性不适用的情况下 (例如,在嵌套 `v-for` 循环中) 你可以使用一个 method 方法: | li v-for="n in even(numbers)">{{ n }}li> | | data: { numbers: [ 1, 2, 3, 4, 5 ] }, methods: { even: function (numbers) { return numbers.filter(function (number) { return number % 2 === 0 }) } } | ## [一段取值范围的 `v-for`](https://cn.vuejs.org/v2/guide/list.html#%E4%B8%80%E6%AE%B5%E5%8F%96%E5%80%BC%E8%8C%83%E5%9B%B4%E7%9A%84-v-for "一段取值范围的 v-for") `v-for` 也可以取整数。在这种情况下,它将重复多次模板。 | div> span v-for="n in 10">{{ n }} span> div> | 结果: 1 2 3 4 5 6 7 8 9 10 ## [`v-for` on a `<template>`](https://cn.vuejs.org/v2/guide/list.html#v-for-on-a-lt-template-gt "v-for on a ") 类似于 `v-if`,你也可以利用带有 `v-for` 的 `<template>` 渲染多个元素。比如: | ul> template v-for="item in items"> li>{{ item.msg }}li> li class="divider" role="presentation">li> template> ul> | ## [`v-for` with `v-if`](https://cn.vuejs.org/v2/guide/list.html#v-for-with-v-if "v-for with v-if") 当它们处于同一节点,`v-for` 的优先级比 `v-if` 更高,这意味着 `v-if` 将分别重复运行于每个 `v-for` 循环中。当你想为仅有的*一些*项渲染节点时,这种优先级的机制会十分有用,如下: | li v-for="todo in todos" v-if="!todo.isComplete"> {{ todo }} li> | 上面的代码只传递了未完成的 todos。 而如果你的目的是有条件地跳过循环的执行,那么可以将 `v-if` 置于外层元素 (或 [`<template>`](https://cn.vuejs.org/v2/guide/conditional.html#%E5%9C%A8-lt-template-gt-%E4%B8%AD%E9%85%8D%E5%90%88-v-if-%E6%9D%A1%E4%BB%B6%E6%B8%B2%E6%9F%93%E4%B8%80%E6%95%B4%E7%BB%84))上。如: | ul v-if="todos.length"> li v-for="todo in todos"> {{ todo }} li> ul> p v-else>No todos left!p> | ## [一个组件的 `v-for`](https://cn.vuejs.org/v2/guide/list.html#%E4%B8%80%E4%B8%AA%E7%BB%84%E4%BB%B6%E7%9A%84-v-for "一个组件的 v-for") > 了解组件相关知识,查看 [组件](https://cn.vuejs.org/v2/guide/components.html)。完全可以先跳过它,以后再回来查看。 在自定义组件里,你可以像任何普通元素一样用 `v-for` 。 | my-component v-for="item in items" :key="item.id">my-component> | > 2.2.0+ 的版本里,当在组件中使用 `v-for` 时,`key` 现在是必须的。 然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要用 `props` : | my-component v-for="(item, index) in items" v-bind:item="item" v-bind:index="index" v-bind:key="item.id" >my-component> | 不自动将 `item` 注入到组件里的原因是,这会使得组件与 `v-for` 的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。 下面是一个简单的 todo list 的完整例子: | div id="todo-list-example"> form v-on:submit.prevent="addNewTodo"> label for="new-todo">Add a todolabel> input v-model="newTodoText" id="new-todo" placeholder="E.g. Feed the cat" > button>Addbutton> form> ul> li is="todo-item" v-for="(todo, index) in todos" v-bind:key="todo.id" v-bind:title="todo.title" v-on:remove="todos.splice(index, 1)" >li> ul> div> | 注意这里的 `is="todo-item"` 属性。这种做法在使用 DOM 模板时是十分必要的,因为在 `<ul>` 元素内只有 `<li>` 元素会被看作有效内容。这样做实现的效果与 `<todo-item>` 相同,但是可以避开一些潜在的浏览器解析错误。查看 [DOM 模板解析说明](https://cn.vuejs.org/v2/guide/components.html#%E8%A7%A3%E6%9E%90-DOM-%E6%A8%A1%E6%9D%BF%E6%97%B6%E7%9A%84%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9) 来了解更多信息。 | Vue.component('todo-item', { template: '\ \ {{ title }}\ Remove\ \ ', props: ['title'] }) new Vue({ el: '#todo-list-example', data: { newTodoText: '', todos: [ { id: 1, title: 'Do the dishes', }, { id: 2, title: 'Take out the trash', }, { id: 3, title: 'Mow the lawn' } ], nextTodoId: 4 }, methods: { addNewTodo: function () { this.todos.push({ id: this.nextTodoId++, title: this.newTodoText }) this.newTodoText = '' } } }) | Add a todo  Add * Do the dishes Remove * Take out the trash Remove * Mow the lawn Remove # 事件处理 ## [监听事件](https://cn.vuejs.org/v2/guide/events.html#%E7%9B%91%E5%90%AC%E4%BA%8B%E4%BB%B6 "监听事件") 可以用 `v-on` 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。 示例: | div id="example-1"> button v-on:click="counter += 1">Add 1button> p>The button above has been clicked {{ counter }} times.p> div> | | var example1 = new Vue({ el: '#example-1', data: { counter: 0 } }) | 结果: Add 1 The button above has been clicked 0 times. ## [事件处理方法](https://cn.vuejs.org/v2/guide/events.html#%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86%E6%96%B9%E6%B3%95 "事件处理方法") 然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 `v-on` 指令中是不可行的。因此 `v-on` 还可以接收一个需要调用的方法名称。 示例: | div id="example-2"> button v-on:click="greet">Greetbutton> div> | | var example2 = new Vue({ el: '#example-2', data: { name: 'Vue.js' }, // 在 `methods` 对象中定义方法 methods: { greet: function (event) { // `this` 在方法里指向当前 Vue 实例 alert('Hello ' + this.name + '!') // `event` 是原生 DOM 事件 if (event) { alert(event.target.tagName) } } } }) // 也可以用 JavaScript 直接调用方法 example2.greet() // => 'Hello Vue.js!' | 结果: Greet ## [内联处理器中的方法](https://cn.vuejs.org/v2/guide/events.html#%E5%86%85%E8%81%94%E5%A4%84%E7%90%86%E5%99%A8%E4%B8%AD%E7%9A%84%E6%96%B9%E6%B3%95 "内联处理器中的方法") 除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法: | div id="example-3"> button v-on:click="say('hi')">Say hibutton> button v-on:click="say('what')">Say whatbutton> div> | | new Vue({ el: '#example-3', methods: { say: function (message) { alert(message) } } }) | 结果: Say hi Say what 有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 `$event` 把它传入方法: | button v-on:click="warn('Form cannot be submitted yet.', $event)"> Submit button> | | // ... methods: { warn: function (message, event) { // 现在我们可以访问原生事件对象 if (event) event.preventDefault() alert(message) } } | ## [事件修饰符](https://cn.vuejs.org/v2/guide/events.html#%E4%BA%8B%E4%BB%B6%E4%BF%AE%E9%A5%B0%E7%AC%A6 "事件修饰符") 在事件处理程序中调用 `event.preventDefault()` 或 `event.stopPropagation()` 是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。 为了解决这个问题,Vue.js 为 `v-on` 提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。 * `.stop` * `.prevent` * `.capture` * `.self` * `.once` * `.passive` | a v-on:click.stop="doThis">a> form v-on:submit.prevent="onSubmit">form> a v-on:click.stop.prevent="doThat">a> form v-on:submit.prevent>form> div v-on:click.capture="doThis">...div> div v-on:click.self="doThat">...div> | 使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 `v-on:click.prevent.self` 会阻止所有的点击,而 `v-on:click.self.prevent` 只会阻止对元素自身的点击。 > 2.1.4 新增 | a v-on:click.once="doThis">a> | 不像其它只能对原生的 DOM 事件起作用的修饰符,`.once` 修饰符还能被用到自定义的[组件事件](https://cn.vuejs.org/v2/guide/components-custom-events.html)上。如果你还没有阅读关于组件的文档,现在大可不必担心。 > 2.3.0 新增 Vue 还对应 [`addEventListener` 中的 `passive` 选项](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters)提供了 `.passive` 修饰符。 | div v-on:scroll.passive="onScroll">...div> | 这个 `.passive` 修饰符尤其能够提升移动端的性能。 不要把 `.passive` 和 `.prevent` 一起使用,因为 `.prevent` 将会被忽略,同时浏览器可能会向你展示一个警告。请记住,`.passive` 会告诉浏览器你*不*想阻止事件的默认行为。 ## [按键修饰符](https://cn.vuejs.org/v2/guide/events.html#%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6 "按键修饰符") 在监听键盘事件时,我们经常需要检查常见的键值。Vue 允许为 `v-on` 在监听键盘事件时添加按键修饰符: | input v-on:keyup.13="submit"> | 记住所有的 `keyCode` 比较困难,所以 Vue 为最常用的按键提供了别名: | input v-on:keyup.enter="submit"> input @keyup.enter="submit"> | 全部的按键别名: * `.enter` * `.tab` * `.delete` (捕获“删除”和“退格”键) * `.esc` * `.space` * `.up` * `.down` * `.left` * `.right` 可以通过全局 `config.keyCodes` 对象[自定义按键修饰符别名](https://cn.vuejs.org/v2/api/#keyCodes): | // 可以使用 `v-on:keyup.f1` Vue.config.keyCodes.f1 = 112 | ### [自动匹配按键修饰符](https://cn.vuejs.org/v2/guide/events.html#%E8%87%AA%E5%8A%A8%E5%8C%B9%E9%85%8D%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6 "自动匹配按键修饰符") > 2.5.0 新增 你也可直接将 [`KeyboardEvent.key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) 暴露的任意有效按键名转换为 kebab-case 来作为修饰符: | input @keyup.page-down="onPageDown"> | 在上面的例子中,处理函数仅在 `$event.key === 'PageDown'` 时被调用。 有一些按键 (`.esc` 以及所有的方向键) 在 IE9 中有不同的 `key` 值, 如果你想支持 IE9,它们的内置别名应该是首选。 ## [系统修饰键](https://cn.vuejs.org/v2/guide/events.html#%E7%B3%BB%E7%BB%9F%E4%BF%AE%E9%A5%B0%E9%94%AE "系统修饰键") > 2.1.0 新增 可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。 * `.ctrl` * `.alt` * `.shift` * `.meta` > 注意:在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其在 MIT 和 Lisp 机器的键盘、以及其后继产品,比如 Knight 键盘、space-cadet 键盘,meta 被标记为“META”。在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”。 例如: | input @keyup.alt.67="clear"> div @click.ctrl="doSomething">Do somethingdiv> | 请注意修饰键与常规按键不同,在和 `keyup` 事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 `ctrl` 的情况下释放其它按键,才能触发 `keyup.ctrl`。而单单释放 `ctrl` 也不会触发事件。如果你想要这样的行为,请为 `ctrl` 换用 `keyCode`:`keyup.17`。 ### [`.exact` 修饰符](https://cn.vuejs.org/v2/guide/events.html#exact-%E4%BF%AE%E9%A5%B0%E7%AC%A6 ".exact 修饰符") > 2.5.0 新增 `.exact` 修饰符允许你控制由精确的系统修饰符组合触发的事件。 | button @click.ctrl="onClick">Abutton> button @click.ctrl.exact="onCtrlClick">Abutton> button @click.exact="onClick">Abutton> | ### [鼠标按钮修饰符](https://cn.vuejs.org/v2/guide/events.html#%E9%BC%A0%E6%A0%87%E6%8C%89%E9%92%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6 "鼠标按钮修饰符") > 2.2.0 新增 * `.left` * `.right` * `.middle` 这些修饰符会限制处理函数仅响应特定的鼠标按钮。 ## [为什么在 HTML 中监听事件?](https://cn.vuejs.org/v2/guide/events.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E5%9C%A8-HTML-%E4%B8%AD%E7%9B%91%E5%90%AC%E4%BA%8B%E4%BB%B6 "为什么在 HTML 中监听事件?") 你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 `v-on` 有几个好处: 1. 扫一眼 HTML 模板便能轻松定位在 JavaScript 代码里对应的方法。 2. 因为你无须在 JavaScript 里手动绑定事件,你的 ViewModel 代码可以是非常纯粹的逻辑,和 DOM 完全解耦,更易于测试。 3. 当一个 ViewModel 被销毁时,所有的事件处理器都会自动被删除。你无须担心如何清理它们。 # 表单输入绑定 ## [基础用法](https://cn.vuejs.org/v2/guide/forms.html#%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95 "基础用法") 你可以用 `v-model` 指令在表单 `<input>`、`<textarea>` 及 `<select>` 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 `v-model` 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。 `v-model` 会忽略所有表单元素的 `value`、`checked`、`selected` 特性的初始值而总是将 Vue 实例的数据作为数据来源。你应该通过 JavaScript 在组件的 `data` 选项中声明初始值。 对于需要使用[输入法](https://zh.wikipedia.org/wiki/%E8%BE%93%E5%85%A5%E6%B3%95) (如中文、日文、韩文等) 的语言,你会发现 `v-model` 不会在输入法组合文字过程中得到更新。如果你也想处理这个过程,请使用 `input` 事件。 ### [文本](https://cn.vuejs.org/v2/guide/forms.html#%E6%96%87%E6%9C%AC "文本") | input v-model="message" placeholder="edit me"> p>Message is: {{ message }}p> | Message is: ### [多行文本](https://cn.vuejs.org/v2/guide/forms.html#%E5%A4%9A%E8%A1%8C%E6%96%87%E6%9C%AC "多行文本") | span>Multiline message is:span> p style="white-space: pre-line;">{{ message }}p> br> textarea v-model="message" placeholder="add multiple lines">textarea> | Multiline message is: 在文本区域插值 (`<textarea></textarea>`) 并不会生效,应用 `v-model` 来代替。 ### [复选框](https://cn.vuejs.org/v2/guide/forms.html#%E5%A4%8D%E9%80%89%E6%A1%86 "复选框") 单个复选框,绑定到布尔值: | input type="checkbox" id="checkbox" v-model="checked"> label for="checkbox">{{ checked }}label> |  false 多个复选框,绑定到同一个数组: | div id='example-3'> input type="checkbox" id="jack" value="Jack" v-model="checkedNames"> label for="jack">Jacklabel> input type="checkbox" id="john" value="John" v-model="checkedNames"> label for="john">Johnlabel> input type="checkbox" id="mike" value="Mike" v-model="checkedNames"> label for="mike">Mikelabel> br> span>Checked names: {{ checkedNames }}span> div> | | new Vue({ el: '#example-3', data: { checkedNames: [] } }) |  Jack  John  Mike  Checked names: [] ### [单选按钮](https://cn.vuejs.org/v2/guide/forms.html#%E5%8D%95%E9%80%89%E6%8C%89%E9%92%AE "单选按钮") | div id="example-4"> input type="radio" id="one" value="One" v-model="picked"> label for="one">Onelabel> br> input type="radio" id="two" value="Two" v-model="picked"> label for="two">Twolabel> br> span>Picked: {{ picked }}span> div> | | new Vue({ el: '#example-4', data: { picked: '' } }) |  One   Two  Picked: ### [选择框](https://cn.vuejs.org/v2/guide/forms.html#%E9%80%89%E6%8B%A9%E6%A1%86 "选择框") 单选时: | div id="example-5"> select v-model="selected"> option disabled value="">请选择option> option>Aoption> option>Boption> option>Coption> select> span>Selected: {{ selected }}span> div> | | new Vue({ el: '...', data: { selected: '' } }) | 请选择 A B C Selected: 如果 `v-model` 表达式的初始值未能匹配任何选项,`<select>` 元素将被渲染为“未选中”状态。在 iOS 中,这会使用户无法选择第一个选项。因为这样的情况下,iOS 不会触发 change 事件。因此,更推荐像上面这样提供一个值为空的禁用选项。 多选时 (绑定到一个数组): | div id="example-6"> select v-model="selected" multiple style="width: 50px;"> option>Aoption> option>Boption> option>Coption> select> br> span>Selected: {{ selected }}span> div> | | new Vue({ el: '#example-6', data: { selected: [] } }) | A B C  Selected: [] 用 `v-for` 渲染的动态选项: | select v-model="selected"> option v-for="option in options" v-bind:value="option.value"> {{ option.text }} option> select> span>Selected: {{ selected }}span> | | new Vue({ el: '...', data: { selected: 'A', options: [ { text: 'One', value: 'A' }, { text: 'Two', value: 'B' }, { text: 'Three', value: 'C' } ] } }) |        One            Two            Three      Selected: A ## [值绑定](https://cn.vuejs.org/v2/guide/forms.html#%E5%80%BC%E7%BB%91%E5%AE%9A "值绑定") 对于单选按钮,复选框及选择框的选项,`v-model` 绑定的值通常是静态字符串 (对于复选框也可以是布尔值): | input type="radio" v-model="picked" value="a"> input type="checkbox" v-model="toggle"> select v-model="selected"> option value="abc">ABCoption> select> | 但是有时我们可能想把值绑定到 Vue 实例的一个动态属性上,这时可以用 `v-bind` 实现,并且这个属性的值可以不是字符串。 ### [复选框](https://cn.vuejs.org/v2/guide/forms.html#%E5%A4%8D%E9%80%89%E6%A1%86-1 "复选框") | input type="checkbox" v-model="toggle" true-value="yes" false-value="no" > | | // 当选中时 vm.toggle === 'yes' // 当没有选中时 vm.toggle === 'no' | 这里的 `true-value` 和 `false-value` 特性并不会影响输入控件的 `value` 特性,因为浏览器在提交表单时并不会包含未被选中的复选框。如果要确保表单中这两个值中的一个能够被提交,(比如“yes”或“no”),请换用单选按钮。 ### [单选按钮](https://cn.vuejs.org/v2/guide/forms.html#%E5%8D%95%E9%80%89%E6%8C%89%E9%92%AE-1 "单选按钮") | input type="radio" v-model="pick" v-bind:value="a"> | | // 当选中时 vm.pick === vm.a | ### [选择框的选项](https://cn.vuejs.org/v2/guide/forms.html#%E9%80%89%E6%8B%A9%E6%A1%86%E7%9A%84%E9%80%89%E9%A1%B9 "选择框的选项") | select v-model="selected"> option v-bind:value="{ number: 123 }">123option> select> | | // 当选中时 typeof vm.selected // => 'object' vm.selected.number // => 123 | ## [修饰符](https://cn.vuejs.org/v2/guide/forms.html#%E4%BF%AE%E9%A5%B0%E7%AC%A6 "修饰符") ### [`.lazy`](https://cn.vuejs.org/v2/guide/forms.html#lazy ".lazy") 在默认情况下,`v-model` 在每次 `input` 事件触发后将输入框的值与数据进行同步 (除了[上述](https://cn.vuejs.org/v2/guide/forms.html#vmodel-ime-tip)输入法组合文字时)。你可以添加 `lazy` 修饰符,从而转变为使用 `change`事件进行同步: | input v-model.lazy="msg" > | ### [`.number`](https://cn.vuejs.org/v2/guide/forms.html#number ".number") 如果想自动将用户的输入值转为数值类型,可以给 `v-model` 添加 `number` 修饰符: | input v-model.number="age" type="number"> | 这通常很有用,因为即使在 `type="number"` 时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 `parseFloat()` 解析,则会返回原始的值。 ### [`.trim`](https://cn.vuejs.org/v2/guide/forms.html#trim ".trim") 如果要自动过滤用户输入的首尾空白字符,可以给 `v-model` 添加 `trim` 修饰符: | input v-model.trim="msg"> | ## [在组件上使用 `v-model`](https://cn.vuejs.org/v2/guide/forms.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-v-model "在组件上使用 v-model") > 如果你还不熟悉 Vue 的组件,可以暂且跳过这里。 HTML 原生的输入元素类型并不总能满足需求。幸好,Vue 的组件系统允许你创建具有完全自定义行为且可复用的输入组件。这些输入组件甚至可以和 `v-model` 一起使用!要了解更多,请参阅组件指南中的[自定义输入组件](https://cn.vuejs.org/v2/guide/components.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-v-model)。 # 组件基础 ## [基本示例](https://cn.vuejs.org/v2/guide/components.html#%E5%9F%BA%E6%9C%AC%E7%A4%BA%E4%BE%8B "基本示例") 这里有一个 Vue 组件的示例: | // 定义一个名为 button-counter 的新组件 Vue.component('button-counter', { data: function () { return { count: 0 } }, template: 'You clicked me {{ count }} times.' }) | 组件是可复用的 Vue 实例,且带有一个名字:在这个例子中是 `<button-counter>`。我们可以在一个通过 `new Vue` 创建的 Vue 根实例中,把这个组件作为自定义元素来使用: | div id="components-demo"> button-counter>button-counter> div> | | new Vue({ el: '#components-demo' }) | You clicked me 0 times. 因为组件是可复用的 Vue 实例,所以它们与 `new Vue` 接收相同的选项,例如 `data`、`computed`、`watch`、`methods` 以及生命周期钩子等。仅有的例外是像 `el`这样根实例特有的选项。 ## [组件的复用](https://cn.vuejs.org/v2/guide/components.html#%E7%BB%84%E4%BB%B6%E7%9A%84%E5%A4%8D%E7%94%A8 "组件的复用") 你可以将组件进行任意次数的复用: | div id="components-demo"> button-counter>button-counter> button-counter>button-counter> button-counter>button-counter> div> | You clicked me 0 times. You clicked me 0 times. You clicked me 0 times. 注意当点击按钮时,每个组件都会各自独立维护它的 `count`。因为你每用一次组件,就会有一个它的新实例被创建。 ### [`data` 必须是一个函数](https://cn.vuejs.org/v2/guide/components.html#data-%E5%BF%85%E9%A1%BB%E6%98%AF%E4%B8%80%E4%B8%AA%E5%87%BD%E6%95%B0 "data 必须是一个函数") 当我们定义这个 `<button-counter>` 组件时,你可能会发现它的 `data` 并不是像这样直接提供一个对象: | data: { count: 0 } | 取而代之的是,一个组件的 `data` 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝: | data: function () { return { count: 0 } } | 如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到*其它所有实例*: You clicked me 0 times. You clicked me 0 times. You clicked me 0 times. ## [组件的组织](https://cn.vuejs.org/v2/guide/components.html#%E7%BB%84%E4%BB%B6%E7%9A%84%E7%BB%84%E7%BB%87 "组件的组织") 通常一个应用会以一棵嵌套的组件树的形式来组织: ![Component Tree](https://cn.vuejs.org/images/components.png) 例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。 为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。至此,我们的组件都只是通过 `Vue.component` 全局注册的: | Vue.component('my-component-name', { // ... options ... }) | 全局注册的组件可以用在其被注册之后的任何 (通过 `new Vue`) 新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中。 到目前为止,关于组件注册你需要了解的就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把[组件注册](https://cn.vuejs.org/v2/guide/components-registration.html)读完。 ## [通过 Prop 向子组件传递数据](https://cn.vuejs.org/v2/guide/components.html#%E9%80%9A%E8%BF%87-Prop-%E5%90%91%E5%AD%90%E7%BB%84%E4%BB%B6%E4%BC%A0%E9%80%92%E6%95%B0%E6%8D%AE "通过 Prop 向子组件传递数据") 早些时候,我们提到了创建一个博文组件的事情。问题是如果你不能向这个组件传递某一篇博文的标题或内容之类的我们想展示的数据的话,它是没有办法使用的。这也正是 prop 的由来。 Prop 是你可以在组件上注册的一些自定义特性。当一个值传递给一个 prop 特性的时候,它就变成了那个组件实例的一个属性。为了给博文组件传递一个标题,我们可以用一个 `props` 选项将其包含在该组件可接受的 prop 列表中: | Vue.component('blog-post', { props: ['title'], template: '{{ title }}' }) | 一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 `data` 中的值一样。 一个 prop 被注册之后,你就可以像这样把数据作为一个自定义特性传递进来: | blog-post title="My journey with Vue">blog-post> blog-post title="Blogging with Vue">blog-post> blog-post title="Why Vue is so fun">blog-post> | ### My journey with Vue ### Blogging with Vue ### Why Vue is so fun 然而在一个典型的应用中,你可能在 `data` 里有一个博文的数组: | new Vue({ el: '#blog-post-demo', data: { posts: [ { id: 1, title: 'My journey with Vue' }, { id: 2, title: 'Blogging with Vue' }, { id: 3, title: 'Why Vue is so fun' } ] } }) | 并想要为每篇博文渲染一个组件: | blog-post v-for="post in posts" v-bind:key="post.id" v-bind:title="post.title" >blog-post> | 如上所示,你会发现我们可以使用 `v-bind` 来动态传递 prop。这在你一开始不清楚要渲染的具体内容,比如[从一个 API 获取博文列表](https://jsfiddle.net/chrisvfritz/sbLgr0ad)的时候,是非常有用的。 到目前为止,关于 prop 你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把 [prop](https://cn.vuejs.org/v2/guide/components-props.html) 读完。 ## [单个根元素](https://cn.vuejs.org/v2/guide/components.html#%E5%8D%95%E4%B8%AA%E6%A0%B9%E5%85%83%E7%B4%A0 "单个根元素") 当构建一个 `<blog-post>` 组件时,你的模板最终会包含的东西远不止一个标题: | h3>{{ title }}h3> | 最最起码,你会包含这篇博文的正文: | h3>{{ title }}h3> div v-html="content">div> | 然而如果你在模板中尝试这样写,Vue 会显示一个错误,并解释道 every component must have a single root element (每个组件必须只有一个根元素)。你可以将模板的内容包裹在一个父元素内,来修复这个问题,例如: | div class="blog-post"> h3>{{ title }}h3> div v-html="content">div> div> | 看起来当组件变得越来越复杂的时候,我们的博文不只需要标题和内容,还需要发布日期、评论等等。为每个相关的信息定义一个 prop 会变得很麻烦: | blog-post v-for="post in posts" v-bind:key="post.id" v-bind:title="post.title" v-bind:content="post.content" v-bind:publishedAt="post.publishedAt" v-bind:comments="post.comments" >blog-post> | 所以是时候重构一下这个 `<blog-post>` 组件了,让它变成接受一个单独的 `post`prop: | blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post" >blog-post> | | Vue.component('blog-post', { props: ['post'], template: ` {{ post.title }} ` }) | 上述的这个和一些接下来的示例使用了 JavaScript 的[模板字符串](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals)来让多行的模板更易读。它们在 IE 下并没有被支持,所以如果你需要在不 (经过 Babel 或 TypeScript 之类的工具) 编译的情况下支持 IE,请使用[折行转义字符](https://css-tricks.com/snippets/javascript/multiline-string-variables-in-javascript/)取而代之。 现在,不论何时为 `post` 对象添加一个新的属性,它都会自动地在 `<blog-post>` 内可用。 ## [通过事件向父级组件发送消息](https://cn.vuejs.org/v2/guide/components.html#%E9%80%9A%E8%BF%87%E4%BA%8B%E4%BB%B6%E5%90%91%E7%88%B6%E7%BA%A7%E7%BB%84%E4%BB%B6%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF "通过事件向父级组件发送消息") 在我们开发 `<blog-post>` 组件时,它的一些功能可能要求我们和父级组件进行沟通。例如我们可能会引入一个可访问性的功能来放大博文的字号,同时让页面的其它部分保持默认的字号。 在其父组件中,我们可以通过添加一个 `postFontSize` 数据属性来支持这个功能: | new Vue({ el: '#blog-posts-events-demo', data: { posts: [/* ... */], postFontSize: 1 } }) | 它可以在模板中用来控制所有博文的字号: | div id="blog-posts-events-demo"> div :style="{ fontSize: postFontSize + 'em' }"> blog-post v-for="post in posts" v-bind:key="post.id" v-bind:post="post" >blog-post> div> div> | 现在我们在每篇博文正文之前添加一个按钮来放大字号: | Vue.component('blog-post', { props: ['post'], template: ` {{ post.title }} Enlarge text ` }) | 问题是这个按钮不会做任何事: | button> Enlarge text button> | 当点击这个按钮时,我们需要告诉父级组件放大所有博文的文本。幸好 Vue 实例提供了一个自定义事件的系统来解决这个问题。我们可以调用内建的 [`$emit` 方法](https://cn.vuejs.org/v2/api/#vm-emit)并传入事件的名字,来向父级组件触发一个事件: | button v-on:click="$emit('enlarge-text')"> Enlarge text button> | 然后我们可以用 `v-on` 在博文组件上监听这个事件,就像监听一个原生 DOM 事件一样: | blog-post ... v-on:enlarge-text="postFontSize += 0.1" >blog-post> | ### My journey with Vue Enlarge text ...content... ### Blogging with Vue Enlarge text ...content... ### Why Vue is so fun Enlarge text ...content... ### [使用事件抛出一个值](https://cn.vuejs.org/v2/guide/components.html#%E4%BD%BF%E7%94%A8%E4%BA%8B%E4%BB%B6%E6%8A%9B%E5%87%BA%E4%B8%80%E4%B8%AA%E5%80%BC "使用事件抛出一个值") 有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让 `<blog-post>` 组件决定它的文本要放大多少。这时可以使用 `$emit` 的第二个参数来提供这个值: | button v-on:click="$emit('enlarge-text', 0.1)"> Enlarge text button> | 然后当在父级组件监听这个事件的时候,我们可以通过 `$event` 访问到被抛出的这个值: | blog-post ... v-on:enlarge-text="postFontSize += $event" >blog-post> | 或者,如果这个事件处理函数是一个方法: | blog-post ... v-on:enlarge-text="onEnlargeText" >blog-post> | 那么这个值将会作为第一个参数传入这个方法: | methods: { onEnlargeText: function (enlargeAmount) { this.postFontSize += enlargeAmount } } | ### [在组件上使用 `v-model`](https://cn.vuejs.org/v2/guide/components.html#%E5%9C%A8%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-v-model "在组件上使用 v-model") 自定义事件也可以用于创建支持 `v-model` 的自定义输入组件。记住: | input v-model="searchText"> | 等价于: | input v-bind:value="searchText" v-on:input="searchText = $event.target.value" > | 当用在组件上时,`v-model` 则会这样: | custom-input v-bind:value="searchText" v-on:input="searchText = $event" >custom-input> | 为了让它正常工作,这个组件内的 `<input>` 必须: * 将其 `value` 特性绑定到一个名叫 `value` 的 prop 上 * 在其 `input` 事件被触发时,将新的值通过自定义的 `input` 事件抛出 写成代码之后是这样的: | Vue.component('custom-input', { props: ['value'], template: ` v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > ` }) | 现在 `v-model` 就应该可以在这个组件上完美地工作起来了: | custom-input v-model="searchText">custom-input> | 到目前为止,关于组件自定义事件你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把[自定义事件](https://cn.vuejs.org/v2/guide/components-custom-events.html)读完。 ## [通过插槽分发内容](https://cn.vuejs.org/v2/guide/components.html#%E9%80%9A%E8%BF%87%E6%8F%92%E6%A7%BD%E5%88%86%E5%8F%91%E5%86%85%E5%AE%B9 "通过插槽分发内容") 和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样: | alert-box> Something bad happened. alert-box> | 可能会渲染出这样的东西: Error! Something bad happened. 幸好,Vue 自定义的 `<slot>` 元素让这变得非常简单: | Vue.component('alert-box', { template: ` Error! ` }) | 如你所见,我们只要在需要的地方加入插槽就行了——就这么简单! 到目前为止,关于插槽你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把[插槽](https://cn.vuejs.org/v2/guide/components-slots.html)读完。 ## [动态组件](https://cn.vuejs.org/v2/guide/components.html#%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6 "动态组件") 有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里: HomePostsArchive Home component 上述内容可以通过 Vue 的 `<component>` 元素加一个特殊的 `is` 特性来实现: | component v-bind:is="currentTabComponent">component> | 在上述示例中,`currentTabComponent` 可以包括 * 已注册组件的名字,或 * 一个组件的选项对象 你可以在[这里](https://jsfiddle.net/chrisvfritz/o3nycadu/)查阅并体验完整的代码,或在[这个版本](https://jsfiddle.net/chrisvfritz/b2qj69o1/)了解绑定组件选项对象,而不是已注册组件名的示例。 到目前为止,关于动态组件你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把[动态和异步组件](https://cn.vuejs.org/v2/guide/components-dynamic-async.html)读完。 ## [解析 DOM 模板时的注意事项](https://cn.vuejs.org/v2/guide/components.html#%E8%A7%A3%E6%9E%90-DOM-%E6%A8%A1%E6%9D%BF%E6%97%B6%E7%9A%84%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9 "解析 DOM 模板时的注意事项") 有些 HTML 元素,诸如 `<ul>`、`<ol>`、`<table>` 和 `<select>`,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 `<li>`、`<tr>` 和 `<option>`,只能出现在其它某些特定的元素内部。 这会导致我们使用这些有约束条件的元素时遇到一些问题。例如: | table> blog-post-row>blog-post-row> table> | 这个自定义组件 `<blog-post-row>` 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 `is` 特性给了我们一个变通的办法: | table> tr is="blog-post-row">tr> table> | 需要注意的是如果我们从以下来源使用模板的话,这条限制是*不存在*的: * 字符串 (例如:`template: '...'`) * [单文件组件 (`.vue`)](https://cn.vuejs.org/v2/guide/single-file-components.html) * [`<script type="text/x-template">`](https://cn.vuejs.org/v2/guide/components-edge-cases.html#X-Templates) 到这里,你需要了解的解析 DOM 模板时的注意事项——实际上也是 Vue 的全部*必要内容*,大概就是这些了。恭喜你!接下来还有很多东西要去学习,不过首先,我们推荐你先休息一下,试用一下 Vue,自己随意做些好玩的东西。 如果你感觉已经掌握了这些知识,我们推荐你再回来把完整的组件指南,包括侧边栏中组件深入章节的所有页面读完。 # 组件注册 > 该页面假设你已经阅读过了[组件基础](https://cn.vuejs.org/v2/guide/components.html)。如果你还对组件不太了解,推荐你先阅读它。 ## [组件名](https://cn.vuejs.org/v2/guide/components-registration.html#%E7%BB%84%E4%BB%B6%E5%90%8D "组件名") 在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了: | Vue.component('my-component-name', { /* ... */ }) | 该组件名就是 `Vue.component` 的第一个参数。 你给予组件的名字可能依赖于你打算拿它来做什么。当直接在 DOM 中使用一个组件 (而不是在字符串模板或[单文件组件](https://cn.vuejs.org/v2/guide/single-file-components.html)) 的时候,我们强烈推荐遵循 [W3C 规范](https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name)中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。 你可以在[风格指南](https://cn.vuejs.org/v2/style-guide/#%E5%9F%BA%E7%A1%80%E7%BB%84%E4%BB%B6%E5%90%8D-%E5%BC%BA%E7%83%88%E6%8E%A8%E8%8D%90)中查阅到关于组件名的其它建议。 ### [组件名大小写](https://cn.vuejs.org/v2/guide/components-registration.html#%E7%BB%84%E4%BB%B6%E5%90%8D%E5%A4%A7%E5%B0%8F%E5%86%99 "组件名大小写") 定义组件名的方式有两种: #### [](https://cn.vuejs.org/v2/guide/components-registration.html#%E4%BD%BF%E7%94%A8-kebab-case "使用 kebab-case")使用 kebab-case | Vue.component('my-component-name', { /* ... */ }) | 当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 `<my-component-name>`。 #### [](https://cn.vuejs.org/v2/guide/components-registration.html#%E4%BD%BF%E7%94%A8-PascalCase "使用 PascalCase")使用 PascalCase | Vue.component('MyComponentName', { /* ... */ }) | 当使用 PascalCase (驼峰式命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 `<my-component-name>` 和 `<MyComponentName>` 都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。 ## [全局注册](https://cn.vuejs.org/v2/guide/components-registration.html#%E5%85%A8%E5%B1%80%E6%B3%A8%E5%86%8C "全局注册") 到目前为止,我们只用过 `Vue.component` 来创建组件: | Vue.component('my-component-name', { // ... 选项 ... }) | 这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的 Vue 根实例 (`new Vue`) 的模板中。比如: | Vue.component('component-a', { /* ... */ }) Vue.component('component-b', { /* ... */ }) Vue.component('component-c', { /* ... */ }) new Vue({ el: '#app' }) | | div id="app"> component-a>component-a> component-b>component-b> component-c>component-c> div> | 在所有子组件中也是如此,也就是说这三个组件*在各自内部*也都可以相互使用。 ## [局部注册](https://cn.vuejs.org/v2/guide/components-registration.html#%E5%B1%80%E9%83%A8%E6%B3%A8%E5%86%8C "局部注册") 全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。 在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件: | var ComponentA = { /* ... */ } var ComponentB = { /* ... */ } var ComponentC = { /* ... */ } | 然后在 `components` 选项中定义你想要使用的组件: | new Vue({ el: '#app', components: { 'component-a': ComponentA, 'component-b': ComponentB } }) | 对于 `components` 对象中的每个属性来说,其属性名就是自定义元素的名字,其属性值就是这个组件的选项对象。 注意局部注册的组件在其子组件中*不可用*。例如,如果你希望 `ComponentA` 在 `ComponentB` 中可用,则你需要这样写: | var ComponentA = { /* ... */ } var ComponentB = { components: { 'component-a': ComponentA }, // ... } | 或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像: | import ComponentA from './ComponentA.vue' export default { components: { ComponentA }, // ... } | 注意在 ES2015+ 中,在对象中放一个类似 `ComponentA` 的变量名其实是 `ComponentA: ComponentA` 的缩写,即这个变量名同时是: * 用在模板中的自定义元素的名称 * 包含了这个组件选项的变量名 ## [模块系统](https://cn.vuejs.org/v2/guide/components-registration.html#%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F "模块系统") 如果你没有通过 `import`/`require` 使用一个模块系统,也许可以暂且跳过这个章节。如果你使用了,那么我们会为你提供一些特殊的使用说明和注意事项。 ### [在模块系统中局部注册](https://cn.vuejs.org/v2/guide/components-registration.html#%E5%9C%A8%E6%A8%A1%E5%9D%97%E7%B3%BB%E7%BB%9F%E4%B8%AD%E5%B1%80%E9%83%A8%E6%B3%A8%E5%86%8C "在模块系统中局部注册") 如果你还在阅读,说明你使用了诸如 Babel 和 webpack 的模块系统。在这些情况下,我们推荐创建一个 `components` 目录,并将每个组件放置在其各自的文件中。 然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的 `ComponentB.js` 或 `ComponentB.vue` 文件中: | import ComponentA from './ComponentA' import ComponentC from './ComponentC' export default { components: { ComponentA, ComponentC }, // ... } | 现在 `ComponentA` 和 `ComponentC` 都可以在 `ComponentB` 的模板中使用了。 ### [基础组件的自动化全局注册](https://cn.vuejs.org/v2/guide/components-registration.html#%E5%9F%BA%E7%A1%80%E7%BB%84%E4%BB%B6%E7%9A%84%E8%87%AA%E5%8A%A8%E5%8C%96%E5%85%A8%E5%B1%80%E6%B3%A8%E5%86%8C "基础组件的自动化全局注册") 可能你的许多组件只是包裹了一个输入框或按钮之类的元素,是相对通用的。我们有时候会把它们称为[基础组件](https://cn.vuejs.org/v2/style-guide/#%E5%9F%BA%E7%A1%80%E7%BB%84%E4%BB%B6%E5%90%8D-%E5%BC%BA%E7%83%88%E6%8E%A8%E8%8D%90),它们会在各个组件中被频繁的用到。 所以会导致很多组件里都会有一个包含基础组件的长列表: | import BaseButton from './BaseButton.vue' import BaseIcon from './BaseIcon.vue' import BaseInput from './BaseInput.vue' export default { components: { BaseButton, BaseIcon, BaseInput } } | 而只是用于模板中的一小部分: | BaseInput v-model="searchText" @keydown.enter="search" /> BaseButton @click="search"> BaseIcon name="search"/> BaseButton> | 幸好如果你使用了 webpack (或在内部使用了 webpack 的 [Vue CLI 3+](https://github.com/vuejs/vue-cli)),那么就可以使用 `require.context` 只全局注册这些非常通用的基础组件。这里有一份可以让你在应用入口文件 (比如 `src/main.js`) 中全局导入基础组件的示例代码: | import Vue from 'vue' import upperFirst from 'lodash/upperFirst' import camelCase from 'lodash/camelCase' const requireComponent = require.context( // 其组件目录的相对路径 './components', // 是否查询其子目录 false, // 匹配基础组件文件名的正则表达式 /Base[A-Z]\w+\.(vue|js)$/ ) requireComponent.keys().forEach(fileName => { // 获取组件配置 const componentConfig = requireComponent(fileName) // 获取组件的 PascalCase 命名 const componentName = upperFirst( camelCase( // 剥去文件名开头的 `./` 和结尾的扩展名 fileName.replace(/^\.\/(.*)\.\w+$/, '$1') ) ) // 全局注册组件 Vue.component( componentName, // 如果这个组件选项是通过 `export default` 导出的, // 那么就会优先使用 `.default`, // 否则回退到使用模块的根。 componentConfig.default || componentConfig ) }) | 记住全局注册的行为必须在根 Vue 实例 (通过 `new Vue`) 创建之前发生。[这里](https://github.com/chrisvfritz/vue-enterprise-boilerplate/blob/master/src/components/_globals.js)有一个真实项目情景下的示例。 # Prop > 该页面假设你已经阅读过了[组件基础](https://cn.vuejs.org/v2/guide/components.html)。如果你还对组件不太了解,推荐你先阅读它。 ## [Prop 的大小写 (camelCase vs kebab-case)](https://cn.vuejs.org/v2/guide/components-props.html#Prop-%E7%9A%84%E5%A4%A7%E5%B0%8F%E5%86%99-camelCase-vs-kebab-case "Prop 的大小写 (camelCase vs kebab-case)") HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名: | Vue.component('blog-post', { // 在 JavaScript 中是 camelCase 的 props: ['postTitle'], template: '{{ postTitle }}' }) | | blog-post post-title="hello!">blog-post> | 重申一次,如果你使用字符串模板,那么这个限制就不存在了。 ## [Prop 类型](https://cn.vuejs.org/v2/guide/components-props.html#Prop-%E7%B1%BB%E5%9E%8B "Prop 类型") 到这里,我们只看到了以字符串数组形式列出的 prop: | props: ['title', 'likes', 'isPublished', 'commentIds', 'author'] | 但是,通常你希望每个 prop 都有指定的值类型。这时,你可以以对象形式列出 prop,这些属性的名称和值分别是 prop 各自的名称和类型: | props: { title: String, likes: Number, isPublished: Boolean, commentIds: Array, author: Object } | 这不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的 JavaScript 控制台提示用户。你会在这个页面接下来的部分看到[类型检查和其它 prop 验证](https://cn.vuejs.org/v2/guide/components-props.html#Prop-%E9%AA%8C%E8%AF%81)。 ## [传递静态或动态 Prop](https://cn.vuejs.org/v2/guide/components-props.html#%E4%BC%A0%E9%80%92%E9%9D%99%E6%80%81%E6%88%96%E5%8A%A8%E6%80%81-Prop "传递静态或动态 Prop") 像这样,你已经知道了可以像这样给 prop 传入一个静态的值: | blog-post title="My journey with Vue">blog-post> | 你也知道 prop 可以通过 `v-bind` 动态赋值,例如: | blog-post v-bind:title="post.title">blog-post> blog-post v-bind:title="post.title + ' by ' + post.author.name">blog-post> | 在上述两个示例中,我们传入的值都是字符串类型的,但实际上*任何*类型的值都可以传给一个 prop。 ### [传入一个数字](https://cn.vuejs.org/v2/guide/components-props.html#%E4%BC%A0%E5%85%A5%E4%B8%80%E4%B8%AA%E6%95%B0%E5%AD%97 "传入一个数字") | blog-post v-bind:likes="42">blog-post> blog-post v-bind:likes="post.likes">blog-post> | ### [传入一个布尔值](https://cn.vuejs.org/v2/guide/components-props.html#%E4%BC%A0%E5%85%A5%E4%B8%80%E4%B8%AA%E5%B8%83%E5%B0%94%E5%80%BC "传入一个布尔值") | blog-post is-published>blog-post> blog-post v-bind:is-published="false">blog-post> blog-post v-bind:is-published="post.isPublished">blog-post> | ### [传入一个数组](https://cn.vuejs.org/v2/guide/components-props.html#%E4%BC%A0%E5%85%A5%E4%B8%80%E4%B8%AA%E6%95%B0%E7%BB%84 "传入一个数组") | blog-post v-bind:comment-ids="[234, 266, 273]">blog-post> blog-post v-bind:comment-ids="post.commentIds">blog-post> | ### [传入一个对象](https://cn.vuejs.org/v2/guide/components-props.html#%E4%BC%A0%E5%85%A5%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1 "传入一个对象") | blog-post v-bind:author="{ name: 'Veronica', company: 'Veridian Dynamics' }">blog-post> blog-post v-bind:author="post.author">blog-post> | ### [传入一个对象的所有属性](https://cn.vuejs.org/v2/guide/components-props.html#%E4%BC%A0%E5%85%A5%E4%B8%80%E4%B8%AA%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%89%80%E6%9C%89%E5%B1%9E%E6%80%A7 "传入一个对象的所有属性") 如果你想要将一个对象的所有属性都作为 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> | ## [单向数据流](https://cn.vuejs.org/v2/guide/components-props.html#%E5%8D%95%E5%90%91%E6%95%B0%E6%8D%AE%E6%B5%81 "单向数据流") 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。 额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。 这里有两种常见的试图改变一个 prop 的情形: 1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。在这种情况下,最好定义一个本地的 data 属性并将这个 prop 用作其初始值: | props: ['initialCounter'], data: function () { return { counter: this.initialCounter } } | 2. 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性: | props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } } | 注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。 ## [Prop 验证](https://cn.vuejs.org/v2/guide/components-props.html#Prop-%E9%AA%8C%E8%AF%81 "Prop 验证") 我们可以为组件的 prop 指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则 Vue 会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。 为了定制 prop 的验证方式,你可以为 `props` 中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如: | Vue.component('my-component', { props: { // 基础的类型检查 (`null` 匹配任何类型) 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'].indexOf(value) !== -1 } } } }) | 当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。 注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的属性 (如 `data`、`computed` 等) 在 `default` 或 `validator` 函数中是不可用的。 ### [类型检查](https://cn.vuejs.org/v2/guide/components-props.html#%E7%B1%BB%E5%9E%8B%E6%A3%80%E6%9F%A5 "类型检查") `type` 可以是下列原生构造函数中的一个: * `String` * `Number` * `Boolean` * `Array` * `Object` * `Date` * `Function` * `Symbol` 额外的,`type` 还可以是一个自定义的构造函数,并且通过 `instanceof` 来进行检查确认。例如,给定下列现成的构造函数: | function Person (firstName, lastName) { this.firstName = firstName this.lastName = lastName } | 你可以使用: | Vue.component('blog-post', { props: { author: Person } }) | 来验证 `author` prop 的值是否是通过 `new Person` 创建的。 ## [非 Prop 的特性](https://cn.vuejs.org/v2/guide/components-props.html#%E9%9D%9E-Prop-%E7%9A%84%E7%89%B9%E6%80%A7 "非 Prop 的特性") 一个非 prop 特性是指传向一个组件,但是该组件并没有相应 prop 定义的特性。 因为显式定义的 prop 适用于向一个子组件传入信息,然而组件库的作者并不总能预见组件会被用于怎样的场景。这也是为什么组件可以接受任意的特性,而这些特性会被添加到这个组件的根元素上。 例如,想象一下你通过一个 Bootstrap 插件使用了一个第三方的 `<bootstrap-date-input>` 组件,这个插件需要在其 `<input>` 上用到一个 `data-date-picker` 特性。我们可以将这个特性添加到你的组件实例上: | bootstrap-date-input data-date-picker="activated">bootstrap-date-input> | 然后这个 `data-date-picker="activated"` 特性就会自动添加到 `<bootstrap-date-input>` 的根元素上。 ### [替换/合并已有的特性](https://cn.vuejs.org/v2/guide/components-props.html#%E6%9B%BF%E6%8D%A2-%E5%90%88%E5%B9%B6%E5%B7%B2%E6%9C%89%E7%9A%84%E7%89%B9%E6%80%A7 "替换/合并已有的特性") 想象一下 `<bootstrap-date-input>` 的模板是这样的: | input type="date" class="form-control"> | 为了给我们的日期选择器插件定制一个主题,我们可能需要像这样添加一个特别的类名: | bootstrap-date-input data-date-picker="activated" class="date-picker-theme-dark" >bootstrap-date-input> | 在这种情况下,我们定义了两个不同的 `class` 的值: * `form-control`,这是在组件的模板内设置好的 * `date-picker-theme-dark`,这是从组件的父级传入的 对于绝大多数特性来说,从外部提供给组件的值会替换掉组件内部设置好的值。所以如果传入 `type="text"` 就会替换掉 `type="date"` 并把它破坏!庆幸的是,`class`和 `style` 特性会稍微智能一些,即两边的值会被合并起来,从而得到最终的值:`form-control date-picker-theme-dark`。 ### [禁用特性继承](https://cn.vuejs.org/v2/guide/components-props.html#%E7%A6%81%E7%94%A8%E7%89%B9%E6%80%A7%E7%BB%A7%E6%89%BF "禁用特性继承") 如果你不希望组件的根元素继承特性,你可以在组件的选项中设置 `inheritAttrs: false`。例如: | Vue.component('my-component', { inheritAttrs: false, // ... }) | 这尤其适合配合实例的 `$attrs` 属性使用,该属性包含了传递给一个组件的特性名和特性值,例如: | { class: 'username-input', placeholder: 'Enter your username' } | 有了 `inheritAttrs: false` 和 `$attrs`,你就可以手动决定这些特性会被赋予哪个元素。在撰写[基础组件](https://cn.vuejs.org/v2/style-guide/#%E5%9F%BA%E7%A1%80%E7%BB%84%E4%BB%B6%E5%90%8D-%E5%BC%BA%E7%83%88%E6%8E%A8%E8%8D%90)的时候是常会用到的: | Vue.component('base-input', { inheritAttrs: false, props: ['label', 'value'], template: ` {{ label }} v-bind="$attrs" v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > ` }) | 这个模式允许你在使用基础组件的时候更像是使用原始的 HTML 元素,而不会担心哪个元素是真正的根元素: | base-input v-model="username" class="username-input" placeholder="Enter your username" >base-input> | # 自定义事件 > 该页面假设你已经阅读过了[组件基础](https://cn.vuejs.org/v2/guide/components.html)。如果你还对组件不太了解,推荐你先阅读它。 ## [事件名](https://cn.vuejs.org/v2/guide/components-custom-events.html#%E4%BA%8B%E4%BB%B6%E5%90%8D "事件名") 不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件: | this.$emit('myEvent') | 则监听这个名字的 kebab-case 版本是不会有任何效果的: | my-component v-on:my-event="doSomething">my-component> | 不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或属性名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 `v-on` 事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 `v-on:myEvent` 将会变成 `v-on:myevent`——导致 `myEvent` 不可能被监听到。 因此,我们推荐你始终使用 kebab-case 的事件名。 ## [自定义组件的 `v-model`](https://cn.vuejs.org/v2/guide/components-custom-events.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6%E7%9A%84-v-model "自定义组件的 v-model") > 2.2.0+ 新增 一个组件上的 `v-model` 默认会利用名为 `value` 的 prop 和名为 `input` 的事件,但是像单选框、复选框等类型的输入控件可能会将 `value` 特性用于[不同的目的](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#Value)。`model` 选项可以用来避免这样的冲突: | Vue.component('base-checkbox', { model: { prop: 'checked', event: 'change' }, props: { checked: Boolean }, template: ` type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)" > ` }) | 现在在这个组件上使用 `v-model` 的时候: | base-checkbox v-model="lovingVue">base-checkbox> | 这里的 `lovingVue` 的值将会传入这个名为 `checked` 的 prop。同时当 `<base-checkbox>` 触发一个 `change` 事件并附带一个新的值的时候,这个 `lovingVue` 的属性将会被更新。 注意你仍然需要在组件的 `props` 选项里声明 `checked` 这个 prop。 ## [将原生事件绑定到组件](https://cn.vuejs.org/v2/guide/components-custom-events.html#%E5%B0%86%E5%8E%9F%E7%94%9F%E4%BA%8B%E4%BB%B6%E7%BB%91%E5%AE%9A%E5%88%B0%E7%BB%84%E4%BB%B6 "将原生事件绑定到组件") 你可能有很多次想要在一个组件的根元素上直接监听一个原生事件。这时,你可以使用 `v-on` 的 `.native` 修饰符: | base-input v-on:focus.native="onFocus">base-input> | 在有的时候这是很有用的,不过在你尝试监听一个类似 `<input>` 的非常特定的元素时,这并不是个好主意。比如上述 `<base-input>` 组件可能做了如下重构,所以根元素实际上是一个 `<label>` 元素: | label> {{ label }} input v-bind="$attrs" v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > label> | 这时,父级的 `.native` 监听器将静默失败。它不会产生任何报错,但是 `onFocus` 处理函数不会如你预期地被调用。 为了解决这个问题,Vue 提供了一个 `$listeners` 属性,它是一个对象,里面包含了作用在这个组件上的所有监听器。例如: | { focus: function (event) { /* ... */ } input: function (value) { /* ... */ }, } | 有了这个 `$listeners` 属性,你就可以配合 `v-on="$listeners"` 将所有的事件监听器指向这个组件的某个特定的子元素。对于类似 `<input>` 的你希望它也可以配合 `v-model` 工作的组件来说,为这些监听器创建一个类似下述 `inputListeners` 的计算属性通常是非常有用的: | Vue.component('base-input', { inheritAttrs: false, props: ['label', 'value'], computed: { inputListeners: function () { var vm = this // `Object.assign` 将所有的对象合并为一个新对象 return Object.assign({}, // 我们从父级添加所有的监听器 this.$listeners, // 然后我们添加自定义监听器, // 或覆写一些监听器的行为 { // 这里确保组件配合 `v-model` 的工作 input: function (event) { vm.$emit('input', event.target.value) } } ) } }, template: ` {{ label }} v-bind="$attrs" v-bind:value="value" v-on="inputListeners" > ` }) | 现在 `<base-input>` 组件是一个完全透明的包裹器了,也就是说它可以完全像一个普通的 `<input>` 元素一样使用了:所有跟它相同的特性和监听器的都可以工作。 ## [`.sync` 修饰符](https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-%E4%BF%AE%E9%A5%B0%E7%AC%A6 ".sync 修饰符") > 2.3.0+ 新增 在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。 这也是为什么我们推荐以 `update:myPropName` 的模式触发事件取而代之。举个例子,在一个包含 `title` prop 的假设的组件中,我们可以用以下方法表达对其赋新值的意图: | this.$emit('update:title', newTitle) | 然后父组件可以监听那个事件并根据需要更新一个本地的数据属性。例如: | text-document v-bind:title="doc.title" v-on:update:title="doc.title = $event" >text-document> | 为了方便起见,我们为这种模式提供一个缩写,即 `.sync` 修饰符: | text-document v-bind:title.sync="doc.title">text-document> | 注意带有 `.sync` 修饰符的 `v-bind` 不能和表达式一起使用 (例如 `v-bind:title.sync=”doc.title + ‘!’”` 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 `v-model`。 当我们用一个对象同时设置多个 prop 的时候,也可以将这个 `.sync` 修饰符和 `v-bind` 配合使用: | text-document v-bind.sync="doc">text-document> | 这样会把 `doc` 对象中的每一个属性 (如 `title`) 都作为一个独立的 prop 传进去,然后各自添加用于更新的 `v-on` 监听器。 将 `v-bind.sync` 用在一个字面量的对象上,例如 `v-bind.sync=”{ title: doc.title }”`,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。 # 插槽 > 该页面假设你已经阅读过了[组件基础](https://cn.vuejs.org/v2/guide/components.html)。如果你还对组件不太了解,推荐你先阅读它。 ## [插槽内容](https://cn.vuejs.org/v2/guide/components-slots.html#%E6%8F%92%E6%A7%BD%E5%86%85%E5%AE%B9 "插槽内容") Vue 实现了一套内容分发的 API,这套 API 基于当前的 [Web Components 规范草案](https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md),将 `<slot>` 元素作为承载分发内容的出口。 它允许你像这样合成组件: | navigation-link url="/profile"> Your Profile navigation-link> | 然后你在 `<navigation-link>` 的模板中可能会写为: | a v-bind:href="url" class="nav-link" > slot>slot> a> | 当组件渲染的时候,这个 `<slot>` 元素将会被替换为“Your Profile”。插槽内可以包含任何模板代码,包括 HTML: | navigation-link url="/profile"> span class="fa fa-user">span> Your Profile navigation-link> | 甚至其它的组件: | navigation-link url="/profile"> font-awesome-icon name="user">font-awesome-icon> Your Profile navigation-link> | 如果 `<navigation-link>` 没有包含一个 `<slot>` 元素,则任何传入它的内容都会被抛弃。 ## [具名插槽](https://cn.vuejs.org/v2/guide/components-slots.html#%E5%85%B7%E5%90%8D%E6%8F%92%E6%A7%BD "具名插槽") 有些时候我们需要多个插槽。例如,一个假设的 `<base-layout>` 组件的模板如下: | div class="container"> header> header> main> main> footer> footer> div> | 对于这样的情况,`<slot>` 元素有一个特殊的特性:`name`。这个特性可以用来定义额外的插槽: | div class="container"> header> slot name="header">slot> header> main> slot>slot> main> footer> slot name="footer">slot> footer> div> | 在向具名插槽提供内容的时候,我们可以在一个父组件的 `<template>` 元素上使用 `slot` 特性: | base-layout> template slot="header"> h1>Here might be a page titleh1> template> p>A paragraph for the main content.p> p>And another one.p> template slot="footer"> p>Here's some contact infop> template> base-layout> | 另一种 `slot` 特性的用法是直接用在一个普通的元素上: | base-layout> h1 slot="header">Here might be a page titleh1> p>A paragraph for the main content.p> p>And another one.p> p slot="footer">Here's some contact infop> base-layout> | 我们还是可以保留一个未命名插槽,这个插槽是默认插槽,也就是说它会作为所有未匹配到插槽的内容的统一出口。上述两个示例渲染出来的 HTML 都将会是: | div class="container"> header> h1>Here might be a page titleh1> header> main> p>A paragraph for the main content.p> p>And another one.p> main> footer> p>Here's some contact infop> footer> div> | ## [插槽的默认内容](https://cn.vuejs.org/v2/guide/components-slots.html#%E6%8F%92%E6%A7%BD%E7%9A%84%E9%BB%98%E8%AE%A4%E5%86%85%E5%AE%B9 "插槽的默认内容") 有的时候为插槽提供默认的内容是很有用的。例如,一个 `<submit-button>` 组件可能希望这个按钮的默认内容是“Submit”,但是同时允许用户覆写为“Save”、“Upload”或别的内容。 你可以在组件模板里的 `<slot>` 标签内部指定默认的内容来做到这一点。 | button type="submit"> slot>Submitslot> button> | 如果父组件为这个插槽提供了内容,则默认的内容会被替换掉。 ## [编译作用域](https://cn.vuejs.org/v2/guide/components-slots.html#%E7%BC%96%E8%AF%91%E4%BD%9C%E7%94%A8%E5%9F%9F "编译作用域") 当你想在插槽内使用数据时,例如: | navigation-link url="/profile"> Logged in as {{ user.name }} navigation-link> | 该插槽可以访问跟这个模板的其它地方相同的实例属性 (也就是说“作用域”是相同的)。但这个插槽不能访问 `<navigation-link>` 的作用域。例如尝试访问 `url` 是不会工作的。牢记一条准则: > 父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。 ## [作用域插槽](https://cn.vuejs.org/v2/guide/components-slots.html#%E4%BD%9C%E7%94%A8%E5%9F%9F%E6%8F%92%E6%A7%BD "作用域插槽") > 2.1.0+ 新增 有的时候你希望提供的组件带有一个可从子组件获取数据的可复用的插槽。例如一个简单的 `<todo-list>` 组件的模板可能包含了如下代码: | ul> li v-for="todo in todos" v-bind:key="todo.id" > {{ todo.text }} li> ul> | 但是在我们应用的某些部分,我们希望每个独立的待办项渲染出和 `todo.text` 不太一样的东西。这也是作用域插槽的用武之地。 为了让这个特性成为可能,你需要做的全部事情就是将待办项内容包裹在一个 `<slot>` 元素上,然后将所有和其上下文相关的数据传递给这个插槽:在这个例子中,这个数据是 `todo` 对象: | ul> li v-for="todo in todos" v-bind:key="todo.id" > slot v-bind:todo="todo"> {{ todo.text }} slot> li> ul> | 现在当我们使用 `<todo-list>` 组件的时候,我们可以选择为待办项定义一个不一样的 `<template>` 作为替代方案,并且可以通过 `slot-scope` 特性从子组件获取数据: | todo-list v-bind:todos="todos"> template slot-scope="slotProps"> span v-if="slotProps.todo.isComplete">✓span> {{ slotProps.todo.text }} template> todo-list> | > 在 2.5.0+,`slot-scope` 不再限制在 `<template>` 元素上使用,而可以用在插槽内的任何元素或组件上。 ### [解构 `slot-scope`](https://cn.vuejs.org/v2/guide/components-slots.html#%E8%A7%A3%E6%9E%84-slot-scope "解构 slot-scope") 如果一个 JavaScript 表达式在一个函数定义的参数位置有效,那么这个表达式实际上就可以被 `slot-scope` 接受。也就是说你可以在支持的环境下 ([单文件组件](https://cn.vuejs.org/v2/guide/single-file-components.html)或[现代浏览器](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#%E6%B5%8F%E8%A7%88%E5%99%A8%E5%85%BC%E5%AE%B9)),在这些表达式中使用 [ES2015 解构语法](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#%E8%A7%A3%E6%9E%84%E5%AF%B9%E8%B1%A1)。例如: | todo-list v-bind:todos="todos"> template slot-scope="{ todo }"> span v-if="todo.isComplete">✓span> {{ todo.text }} template> todo-list> | 这会使作用域插槽变得更干净一些。 # 动态组件 & 异步组件 > 该页面假设你已经阅读过了[组件基础](https://cn.vuejs.org/v2/guide/components.html)。如果你还对组件不太了解,推荐你先阅读它。 ## [在动态组件上使用 `keep-alive`](https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%9C%A8%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6%E4%B8%8A%E4%BD%BF%E7%94%A8-keep-alive "在动态组件上使用 keep-alive") 我们之前曾经在一个多标签的界面中使用 `is` 特性来切换不同的组件: | component v-bind:is="currentTabComponent">component> | 当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。例如我们来展开说一说这个多标签界面: PostsArchive * Cat Ipsum * Hipster Ipsum * Cupcake Ipsum Click on a blog title to the left to view it. 你会注意到,如果你选择了一篇文章,切换到 *Archive* 标签,然后再切换回 *Posts*,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 `currentTabComponent` 实例。 重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 `<keep-alive>` 元素将其动态组件包裹起来。 | keep-alive> component v-bind:is="currentTabComponent">component> keep-alive> | 来看看修改后的结果: PostsArchive * Cat Ipsum * Hipster Ipsum * Cupcake Ipsum Click on a blog title to the left to view it. 现在这个 *Posts* 标签保持了它的状态 (被选中的文章) 甚至当它未被渲染时也是如此。你可以在[这个 fiddle](https://jsfiddle.net/chrisvfritz/Lp20op9o/) 查阅到完整的代码。 注意这个 `<keep-alive>` 要求被切换到的组件都有自己的名字,不论是通过组件的 `name` 选项还是局部/全局注册。 你可以在 [API 参考文档](https://cn.vuejs.org/v2/api/#keep-alive) 查阅更多关于 `<keep-alive>` 的细节。 ## [异步组件](https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%BC%82%E6%AD%A5%E7%BB%84%E4%BB%B6 "异步组件") 在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如: | Vue.component('async-example', function (resolve, reject) { setTimeout(function () { // 向 `resolve` 回调传递组件定义 resolve({ template: 'I am async!' }) }, 1000) }) | 如你所见,这个工厂函数会收到一个 `resolve` 回调,这个回调函数会在你从服务器得到组件定义的时候被调用。你也可以调用 `reject(reason)` 来表示加载失败。这里的 `setTimeout` 是为了演示用的,如何获取组件取决于你自己。一个推荐的做法是将异步组件和 [webpack 的 code-splitting 功能](https://webpack.js.org/guides/code-splitting/)一起配合使用: | Vue.component('async-webpack-example', function (resolve) { // 这个特殊的 `require` 语法将会告诉 webpack // 自动将你的构建代码切割成多个包,这些包 // 会通过 Ajax 请求加载 require(['./my-async-component'], resolve) }) | 你也可以在工厂函数中返回一个 `Promise`,所以把 webpack 2 和 ES2015 语法加在一起,我们可以写成这样: | Vue.component( 'async-webpack-example', // 这个 `import` 函数会返回一个 `Promise` 对象。 () => import('./my-async-component') ) | 当使用[局部注册](https://cn.vuejs.org/v2/guide/components.html#%E6%9C%AC%E5%9C%B0%E6%B3%A8%E5%86%8C)的时候,你也可以直接提供一个返回 `Promise` 的函数: | new Vue({ // ... components: { 'my-component': () => import('./my-async-component') } }) | 如果你是一个 Browserify 用户同时喜欢使用异步组件,很不幸这个工具的作者[明确表示](https://github.com/substack/node-browserify/issues/58#issuecomment-21978224)异步加载“并不会被 Browserify 支持”,至少官方不会。Browserify 社区已经找到了[一些变通方案](https://github.com/vuejs/vuejs.org/issues/620),这些方案可能会对已存在的复杂应用有帮助。对于其它的场景,我们推荐直接使用 webpack,以拥有内建的被作为第一公民的异步支持。 ### [处理加载状态](https://cn.vuejs.org/v2/guide/components-dynamic-async.html#%E5%A4%84%E7%90%86%E5%8A%A0%E8%BD%BD%E7%8A%B6%E6%80%81 "处理加载状态") > 2.3.0+ 新增 这里的异步组件工厂函数也可以返回一个如下格式的对象: | const AsyncComponent = () => ({ // 需要加载的组件 (应该是一个 `Promise` 对象) component: import('./MyComponent.vue'), // 异步组件加载时使用的组件 loading: LoadingComponent, // 加载失败时使用的组件 error: ErrorComponent, // 展示加载时组件的延时时间。默认值是 200 (毫秒) delay: 200, // 如果提供了超时时间且组件加载也超时了, // 则使用加载失败时使用的组件。默认值是:`Infinity` timeout: 3000 }) | > 注意如果你希望在 [Vue Router](https://github.com/vuejs/vue-router) 的路由组件中使用上述语法的话,你必须使用 Vue Router 2.4.0+ 版本。 > # 处理边界情况 > 该页面假设你已经阅读过了[组件基础](https://cn.vuejs.org/v2/guide/components.html)。如果你还对组件不太了解,推荐你先阅读它。 这里记录的都是和处理边界情况有关的功能,即一些需要对 Vue 的规则做一些小调整的特殊情况。不过注意这些功能都是有劣势或危险的场景的。我们会在每个案例中注明,所以当你使用每个功能的时候请稍加留意。 ## [访问元素 & 组件](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0-amp-%E7%BB%84%E4%BB%B6 "访问元素 & 组件") 在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。 ### [访问根实例](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E8%AE%BF%E9%97%AE%E6%A0%B9%E5%AE%9E%E4%BE%8B "访问根实例") 在每个 `new Vue` 实例的子组件中,其根实例可以通过 `$root` 属性进行访问。例如,在这个根实例中: | // Vue 根实例 new Vue({ data: { foo: 1 }, computed: { bar: function () { /* ... */ } }, methods: { baz: function () { /* ... */ } } }) | 所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。 | // 获取根组件的数据 this.$root.foo // 写入根组件的数据 this.$root.foo = 2 // 访问根组件的计算属性 this.$root.bar // 调用根组件的方法 this.$root.baz() | 对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 [Vuex](https://github.com/vuejs/vuex) 来管理应用的状态。 ### [访问父级组件实例](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E8%AE%BF%E9%97%AE%E7%88%B6%E7%BA%A7%E7%BB%84%E4%BB%B6%E5%AE%9E%E4%BE%8B "访问父级组件实例") 和 `$root` 类似,`$parent` 属性可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。 在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。 另外在一些*可能*适当的时候,你需要特别地共享一些组件库。举个例子,在和 JavaScript API 进行交互而不渲染 HTML 的抽象组件内,诸如这些假设性的 Google 地图组件一样: | google-map> google-map-markers v-bind:places="iceCreamShops">google-map-markers> google-map> | 这个 `<google-map>` 组件可以定义一个 `map` 属性,所有的子组件都需要访问它。在这种情况下 `<google-map-markers>` 可能想要通过类似 `this.$parent.getMap` 的方式访问那个地图,以便为其添加一组标记。你可以在[这里](https://jsfiddle.net/chrisvfritz/ttzutdxh/)查阅这种模式。 请留意,尽管如此,通过这种模式构建出来的那个组件的内部仍然是容易出现问题的。比如,设想一下我们添加一个新的 `<google-map-region>` 组件,当 `<google-map-markers>` 在其内部出现的时候,只会渲染那个区域内的标记: | google-map> google-map-region v-bind:shape="cityBoundaries"> google-map-markers v-bind:places="iceCreamShops">google-map-markers> google-map-region> google-map> | 那么在 `<google-map-markers>` 内部你可能发现自己需要一些类似这样的 hack: | var map = this.$parent.map || this.$parent.$parent.map | 很快它就会失控。这也是我们针对需要向任意更深层级的组件提供上下文信息时推荐[依赖注入](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5)的原因。 ### [访问子组件实例或子元素](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E8%AE%BF%E9%97%AE%E5%AD%90%E7%BB%84%E4%BB%B6%E5%AE%9E%E4%BE%8B%E6%88%96%E5%AD%90%E5%85%83%E7%B4%A0 "访问子组件实例或子元素") 尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 `ref` 特性为这个子组件赋予一个 ID 引用。例如: | base-input ref="usernameInput">base-input> | 现在在你已经定义了这个 `ref` 的组件里,你可以使用: | this.$refs.usernameInput | 来访问这个 `<base-input>` 实例,以便不时之需。比如程序化地从一个父级组件聚焦这个输入框。在刚才那个例子中,该 `<base-input>` 组件也可以使用一个类似的 `ref` 提供对内部这个指定元素的访问,例如: | input ref="input"> | 甚至可以通过其父级组件定义方法: | methods: { // 用来从父级组件聚焦输入框 focus: function () { this.$refs.input.focus() } } | 这样就允许父级组件通过下面的代码聚焦 `<base-input>` 里的输入框: | this.$refs.usernameInput.focus() | 当 `ref` 和 `v-for` 一起使用的时候,你得到的引用将会是一个包含了对应数据源的这些子组件的数组。 `$refs` 只会在组件渲染完成之后生效,并且它们不是响应式的。这只意味着一个直接的子组件封装的“逃生舱”——你应该避免在模板或计算属性中访问 `$refs`。 ### [依赖注入](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5 "依赖注入") 在此之前,在我们描述[访问父级组件实例](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E8%AE%BF%E9%97%AE%E7%88%B6%E7%BA%A7%E7%BB%84%E4%BB%B6%E5%AE%9E%E4%BE%8B)的时候,展示过一个类似这样的例子: | google-map> google-map-region v-bind:shape="cityBoundaries"> google-map-markers v-bind:places="iceCreamShops">google-map-markers> google-map-region> google-map> | 在这个组件里,所有 `<google-map>` 的后代都需要访问一个 `getMap` 方法,以便知道要跟那个地图进行交互。不幸的是,使用 `$parent` 属性无法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:`provide`和 `inject`。 `provide` 选项允许我们指定我们想要提供给后代组件的数据/方法。在这个例子中,就是 `<google-map>` 内部的 `getMap` 方法: | provide: function () { return { getMap: this.getMap } } | 然后在任何后代组件里,我们都可以使用 `inject` 选项来接收指定的我们想要添加在这个实例上的属性: | inject: ['getMap'] | 你可以在[这里](https://jsfiddle.net/chrisvfritz/tdv8dt3s/)看到完整的示例。相比 `$parent` 来说,这个用法可以让我们在*任意*后代组件中访问 `getMap`,而不需要暴露整个 `<google-map>` 实例。这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和 `props` 一样。 实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了: * 祖先组件不需要知道哪些后代组件使用它提供的属性 * 后代组件不需要知道被注入的属性来自哪里 然而,依赖注入还是有负面影响的。它将你的应用以目前的组件组织方式耦合了起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟[使用 `$root`](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E8%AE%BF%E9%97%AE%E6%A0%B9%E5%AE%9E%E4%BE%8B)做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 [Vuex](https://github.com/vuejs/vuex) 这样真正的状态管理方案了。 你可以在 [API 参考文档](https://cn.vuejs.org/v2/api/#provide-inject)学习更多关于依赖注入的知识。 ## [程序化的事件侦听器](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E7%A8%8B%E5%BA%8F%E5%8C%96%E7%9A%84%E4%BA%8B%E4%BB%B6%E4%BE%A6%E5%90%AC%E5%99%A8 "程序化的事件侦听器") 现在,你已经知道了 `$emit` 的用法,它可以被 `v-on` 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法。我们可以: * 通过 `$on(eventName, eventHandler)` 侦听一个事件 * 通过 `$once(eventName, eventHandler)` 一次性侦听一个事件 * 通过 `$off(eventName, eventHandler)` 停止侦听一个事件 你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。它们也可以用于代码组织工具。例如,你可能经常看到这种集成一个第三方库的模式: | // 一次性将这个日期选择器附加到一个输入框上 // 它会被挂载到 DOM 上。 mounted: function () { // Pikaday 是一个第三方日期选择器的库 this.picker = new Pikaday({ field: this.$refs.input, format: 'YYYY-MM-DD' }) }, // 在组件被销毁之前, // 也销毁这个日期选择器。 beforeDestroy: function () { this.picker.destroy() } | 这里有两个潜在的问题: * 它需要在这个组件实例中保存这个 `picker`,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。 * 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。 你应该通过一个程序化的侦听器解决这两个问题: | mounted: function () { var picker = new Pikaday({ field: this.$refs.input, format: 'YYYY-MM-DD' }) this.$once('hook:beforeDestroy', function () { picker.destroy() }) } | 使用了这个策略,我甚至可以让多个输入框元素同时使用不同的 Pikaday,每个新的实例都程序化地在后期清理它自己: | mounted: function () { this.attachDatepicker('startDateInput') this.attachDatepicker('endDateInput') }, methods: { attachDatepicker: function (refName) { var picker = new Pikaday({ field: this.$refs[refName], format: 'YYYY-MM-DD' }) this.$once('hook:beforeDestroy', function () { picker.destroy() }) } } | 查阅[这个 fiddle](https://jsfiddle.net/chrisvfritz/1Leb7up8/) 可以了解到完整的代码。注意,即便如此,如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件。在这个例子中,我们推荐创建一个可复用的 `<input-datepicker>` 组件。 想了解更多程序化侦听器的内容,请查阅[实例方法 / 事件](https://cn.vuejs.org/v2/api/#%E5%AE%9E%E4%BE%8B%E6%96%B9%E6%B3%95-%E4%BA%8B%E4%BB%B6)相关的 API。 注意 Vue 的事件系统不同于浏览器的 [EventTarget API](https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget)。尽管它们工作起来是相似的,但是 `$emit`、`$on`, 和 `$off` 并不是 `dispatchEvent`、`addEventListener` 和 `removeEventListener` 的别名。 ## [循环引用](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8 "循环引用") ### [递归组件](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E9%80%92%E5%BD%92%E7%BB%84%E4%BB%B6 "递归组件") 组件是可以在它们自己的模板中调用自身的。不过它们只能通过 `name` 选项来做这件事: | name: 'unique-name-of-my-component' | 当你使用 `Vue.component` 全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 `name` 选项。 | Vue.component('unique-name-of-my-component', { // ... }) | 稍有不慎,递归组件就可能导致无限循环: | name: 'stack-overflow', template: '' | 类似上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的 (例如使用一个最终会得到 `false` 的 `v-if`)。 ### [组件之间的循环引用](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E7%BB%84%E4%BB%B6%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BE%AA%E7%8E%AF%E5%BC%95%E7%94%A8 "组件之间的循环引用") 假设你需要构建一个文件目录树,像访达或资源管理器那样的。你可能有一个 `<tree-folder>` 组件,模板是这样的: | p> span>{{ folder.name }}span> tree-folder-contents :children="folder.children"/> p> | 还有一个 `<tree-folder-contents>` 组件,模板是这样的: | ul> li v-for="child in children"> tree-folder v-if="child.children" :folder="child"/> span v-else>{{ child.name }}span> li> ul> | 当你仔细观察的时候,你会发现这些组件在渲染树中互为对方的后代*和*祖先——一个悖论!当通过 `Vue.component` 全局注册组件的时候,这个悖论会被自动解开。如果你是这样做的,那么你可以跳过这里。 然而,如果你使用一个*模块系统*依赖/导入组件,例如通过 webpack 或 Browserify,你会遇到一个错误: | Failed to mount component: template or render function not defined. | 为了解释这里发生了什么,我们先把两个组件称为 A 和 B。模块系统发现它需要 A,但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点,在那里“A *反正*是需要 B 的,但是我们不需要先解析 B。” 在我们的例子中,把 `<tree-folder>` 组件设为了那个点。我们知道那个产生悖论的子组件是 `<tree-folder-contents>` 组件,所以我们会等到生命周期钩子 `beforeCreate` 时去注册它: | beforeCreate: function () { this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default } | 或者,在本地注册组件的时候,你可以使用 webpack 的异步 `import`: | components: { TreeFolderContents: () => import('./tree-folder-contents.vue') } | 这样问题就解决了! ## [模板定义的替代品](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E6%A8%A1%E6%9D%BF%E5%AE%9A%E4%B9%89%E7%9A%84%E6%9B%BF%E4%BB%A3%E5%93%81 "模板定义的替代品") ### [内联模板](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E5%86%85%E8%81%94%E6%A8%A1%E6%9D%BF "内联模板") 当 `inline-template` 这个特殊的特性出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。 | my-component inline-template> div> p>These are compiled as the component's own template.p> p>Not parent's transclusion content.p> div> my-component> | 不过,`inline-template` 会让你模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 `template` 选项或 `.vue` 文件里的一个 `<template>` 元素来定义模板。 ### [X-Templates](https://cn.vuejs.org/v2/guide/components-edge-cases.html#X-Templates "X-Templates") 另一个定义模板的方式是在一个 `<script>` 元素中,并为其带上 `text/x-template`的类型,然后通过一个 id 将模板引用过去。例如: | script type="text/x-template" id="hello-world-template"> p>Hello hello hellop> script> | | Vue.component('hello-world', { template: '#hello-world-template' }) | 这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。 ## [控制更新](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E6%8E%A7%E5%88%B6%E6%9B%B4%E6%96%B0 "控制更新") 感谢 Vue 的响应式系统,它始终知道何时进行更新 (如果你用对了的话)。不过还是有一些边界情况,你想要强制更新,尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新。 ### [强制更新](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E5%BC%BA%E5%88%B6%E6%9B%B4%E6%96%B0 "强制更新") 如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。 你可能还没有留意到[数组](https://cn.vuejs.org/v2/guide/list.html#%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)或[对象](https://cn.vuejs.org/v2/guide/list.html#%E5%AF%B9%E8%B1%A1%E6%9B%B4%E6%94%B9%E6%A3%80%E6%B5%8B%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9)的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。 然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 [`$forceUpdate`](https://cn.vuejs.org/v2/api/#vm-forceUpdate) 来做这件事。 ### [通过 `v-once` 创建低开销的静态组件](https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E9%80%9A%E8%BF%87-v-once-%E5%88%9B%E5%BB%BA%E4%BD%8E%E5%BC%80%E9%94%80%E7%9A%84%E9%9D%99%E6%80%81%E7%BB%84%E4%BB%B6 "通过 v-once 创建低开销的静态组件") 渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 `v-once` 特性以确保这些内容只计算一次然后缓存起来,就像这样: | Vue.component('terms-of-service', { template: ` Terms of Service ... a lot of static content ... ` }) | 再说一次,试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 `v-once` 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。 # 进入/离开 & 列表过渡 ## [概述](https://cn.vuejs.org/v2/guide/transitions.html#%E6%A6%82%E8%BF%B0 "概述") Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。 包括以下工具: * 在 CSS 过渡和动画中自动应用 class * 可以配合使用第三方 CSS 动画库,如 Animate.css * 在过渡钩子函数中使用 JavaScript 直接操作 DOM * 可以配合使用第三方 JavaScript 动画库,如 Velocity.js 在这里,我们只会讲到进入、离开和列表的过渡,你也可以看下一节的 [管理过渡状态](https://cn.vuejs.org/v2/guide/transitioning-state.html)。 ## [单元素/组件的过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%8D%95%E5%85%83%E7%B4%A0-%E7%BB%84%E4%BB%B6%E7%9A%84%E8%BF%87%E6%B8%A1 "单元素/组件的过渡") Vue 提供了 `transition` 的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡 * 条件渲染 (使用 `v-if`) * 条件展示 (使用 `v-show`) * 动态组件 * 组件根节点 这里是一个典型的例子: | div id="demo"> button v-on:click="show = !show"> Toggle button> transition name="fade"> p v-if="show">hellop> transition> div> | | new Vue({ el: '#demo', data: { show: true } }) | | .fade-enter-active, .fade-leave-active { transition: opacity .5s; } .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { opacity: 0; } | Toggle hello 当插入或删除包含在 `transition` 组件中的元素时,Vue 将会做以下处理: 1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。 2. 如果过渡组件提供了 [JavaScript 钩子函数](https://cn.vuejs.org/v2/guide/transitions.html#JavaScript-%E9%92%A9%E5%AD%90),这些钩子函数将在恰当的时机被调用。 3. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和 Vue 的 `nextTick` 概念不同) ### [过渡的类名](https://cn.vuejs.org/v2/guide/transitions.html#%E8%BF%87%E6%B8%A1%E7%9A%84%E7%B1%BB%E5%90%8D "过渡的类名") 在进入/离开的过渡中,会有 6 个 class 切换。 1. `v-enter`:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。 2. `v-enter-active`:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。 3. `v-enter-to`: 2.1.8版及以上 定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 `v-enter` 被移除),在过渡/动画完成之后移除。 4. `v-leave`: 定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。 5. `v-leave-active`:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。 6. `v-leave-to`: 2.1.8版及以上 定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 `v-leave` 被删除),在过渡/动画完成之后移除。 ![Transition Diagram](https://cn.vuejs.org/images/transition.png) 对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 `<transition>`,则 `v-` 是这些类名的默认前缀。如果你使用了 `<transition name="my-transition">`,那么 `v-enter` 会替换为 `my-transition-enter`。 `v-enter-active` 和 `v-leave-active` 可以控制进入/离开过渡的不同的缓和曲线,在下面章节会有个示例说明。 ### [CSS 过渡](https://cn.vuejs.org/v2/guide/transitions.html#CSS-%E8%BF%87%E6%B8%A1 "CSS 过渡") 常用的过渡都是使用 CSS 过渡。 下面是一个简单例子: | div id="example-1"> button @click="show = !show"> Toggle render button> transition name="slide-fade"> p v-if="show">hellop> transition> div> | | new Vue({ el: '#example-1', data: { show: true } }) | | /* 可以设置不同的进入和离开动画 */ /* 设置持续时间和动画函数 */ .slide-fade-enter-active { transition: all .3s ease; } .slide-fade-leave-active { transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0); } .slide-fade-enter, .slide-fade-leave-to /* .slide-fade-leave-active for below version 2.1.8 */ { transform: translateX(10px); opacity: 0; } | Toggle render hello ### [CSS 动画](https://cn.vuejs.org/v2/guide/transitions.html#CSS-%E5%8A%A8%E7%94%BB "CSS 动画") CSS 动画用法同 CSS 过渡,区别是在动画中 `v-enter` 类名在节点插入 DOM 后不会立即删除,而是在 `animationend` 事件触发时删除。 示例:(省略了兼容性前缀) | div id="example-2"> button @click="show = !show">Toggle showbutton> transition name="bounce"> p v-if="show">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus.p> transition> div> | | new Vue({ el: '#example-2', data: { show: true } }) | | .bounce-enter-active { animation: bounce-in .5s; } .bounce-leave-active { animation: bounce-in .5s reverse; } @keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.5); } 100% { transform: scale(1); } } | Toggle show Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus. ### [自定义过渡的类名](https://cn.vuejs.org/v2/guide/transitions.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E8%BF%87%E6%B8%A1%E7%9A%84%E7%B1%BB%E5%90%8D "自定义过渡的类名") 我们可以通过以下特性来自定义过渡类名: * `enter-class` * `enter-active-class` * `enter-to-class` (2.1.8+) * `leave-class` * `leave-active-class` * `leave-to-class` (2.1.8+) 他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 [Animate.css](https://daneden.github.io/animate.css/) 结合使用十分有用。 示例: | link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css"> div id="example-3"> button @click="show = !show"> Toggle render button> transition name="custom-classes-transition" enter-active-class="animated tada" leave-active-class="animated bounceOutRight" > p v-if="show">hellop> transition> div> | | new Vue({ el: '#example-3', data: { show: true } }) | Toggle render hello ### [同时使用过渡和动画](https://cn.vuejs.org/v2/guide/transitions.html#%E5%90%8C%E6%97%B6%E4%BD%BF%E7%94%A8%E8%BF%87%E6%B8%A1%E5%92%8C%E5%8A%A8%E7%94%BB "同时使用过渡和动画") Vue 为了知道过渡的完成,必须设置相应的事件监听器。它可以是 `transitionend` 或 `animationend` ,这取决于给元素应用的 CSS 规则。如果你使用其中任何一种,Vue 能自动识别类型并设置监听。 但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如 `animation`很快的被触发并完成了,而 `transition` 效果还没结束。在这种情况中,你就需要使用 `type` 特性并设置 `animation` 或 `transition` 来明确声明你需要 Vue 监听的类型。 ### [显性的过渡持续时间](https://cn.vuejs.org/v2/guide/transitions.html#%E6%98%BE%E6%80%A7%E7%9A%84%E8%BF%87%E6%B8%A1%E6%8C%81%E7%BB%AD%E6%97%B6%E9%97%B4 "显性的过渡持续时间") > 2.2.0 新增 在很多情况下,Vue 可以自动得出过渡效果的完成时机。默认情况下,Vue 会等待其在过渡效果的根元素的第一个 `transitionend` 或 `animationend` 事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果。 在这种情况下你可以用 `<transition>` 组件上的 `duration` 属性定制一个显性的过渡持续时间 (以毫秒计): | transition :duration="1000">...transition> | 你也可以定制进入和移出的持续时间: | transition :duration="{ enter: 500, leave: 800 }">...transition> | ### [JavaScript 钩子](https://cn.vuejs.org/v2/guide/transitions.html#JavaScript-%E9%92%A9%E5%AD%90 "JavaScript 钩子") 可以在属性中声明 JavaScript 钩子 | transition v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:after-enter="afterEnter" v-on:enter-cancelled="enterCancelled" v-on:before-leave="beforeLeave" v-on:leave="leave" v-on:after-leave="afterLeave" v-on:leave-cancelled="leaveCancelled" > transition> | | // ... methods: { // -------- // 进入中 // -------- beforeEnter: function (el) { // ... }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 enter: function (el, done) { // ... done() }, afterEnter: function (el) { // ... }, enterCancelled: function (el) { // ... }, // -------- // 离开时 // -------- beforeLeave: function (el) { // ... }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 leave: function (el, done) { // ... done() }, afterLeave: function (el) { // ... }, // leaveCancelled 只用于 v-show 中 leaveCancelled: function (el) { // ... } } | 这些钩子函数可以结合 CSS `transitions/animations` 使用,也可以单独使用。 当只用 JavaScript 过渡的时候,在 `enter` 和 `leave` 中必须使用 `done` 进行回调。否则,它们将被同步调用,过渡会立即完成。 推荐对于仅使用 JavaScript 过渡的元素添加 `v-bind:css="false"`,Vue 会跳过 CSS 的检测。这也可以避免过渡过程中 CSS 的影响。 一个使用 Velocity.js 的简单例子: | Velocity 和 jQuery.animate 的工作方式类似,也是用来实现 JavaScript 动画的一个很棒的选择 --> script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js">script> div id="example-4"> button @click="show = !show"> Toggle button> transition v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:leave="leave" v-bind:css="false" > p v-if="show"> Demo p> transition> div> | | new Vue({ el: '#example-4', data: { show: false }, methods: { beforeEnter: function (el) { el.style.opacity = 0 el.style.transformOrigin = 'left' }, enter: function (el, done) { Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 }) Velocity(el, { fontSize: '1em' }, { complete: done }) }, leave: function (el, done) { Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { duration: 600 }) Velocity(el, { rotateZ: '100deg' }, { loop: 2 }) Velocity(el, { rotateZ: '45deg', translateY: '30px', translateX: '30px', opacity: 0 }, { complete: done }) } } }) | Toggle ## [初始渲染的过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%88%9D%E5%A7%8B%E6%B8%B2%E6%9F%93%E7%9A%84%E8%BF%87%E6%B8%A1 "初始渲染的过渡") 可以通过 `appear` 特性设置节点在初始渲染的过渡 | transition appear> transition> | 这里默认和进入/离开过渡一样,同样也可以自定义 CSS 类名。 | transition appear appear-class="custom-appear-class" appear-to-class="custom-appear-to-class" (2.1.8+) appear-active-class="custom-appear-active-class" > transition> | 自定义 JavaScript 钩子: | transition appear v-on:before-appear="customBeforeAppearHook" v-on:appear="customAppearHook" v-on:after-appear="customAfterAppearHook" v-on:appear-cancelled="customAppearCancelledHook" > transition> | ## [多个元素的过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%A4%9A%E4%B8%AA%E5%85%83%E7%B4%A0%E7%9A%84%E8%BF%87%E6%B8%A1 "多个元素的过渡") 我们之后讨论[多个组件的过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%A4%9A%E4%B8%AA%E7%BB%84%E4%BB%B6%E7%9A%84%E8%BF%87%E6%B8%A1),对于原生标签可以使用 `v-if`/`v-else` 。最常见的多标签过渡是一个列表和描述这个列表为空消息的元素: | transition> table v-if="items.length > 0"> table> p v-else>Sorry, no items found.p> transition> | 可以这样使用,但是有一点需要注意: 当有相同标签名的元素切换时,需要通过 `key` 特性设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 `<transition>` 组件中的多个元素设置 key 是一个更好的实践。 示例: | transition> button v-if="isEditing" key="save"> Save button> button v-else key="edit"> Edit button> transition> | 在一些场景中,也可以通过给同一个元素的 `key` 特性设置不同的状态来代替 `v-if`和 `v-else`,上面的例子可以重写为: | transition> button v-bind:key="isEditing"> {{ isEditing ? 'Save' : 'Edit' }} button> transition> | 使用多个 `v-if` 的多个元素的过渡可以重写为绑定了动态属性的单个元素过渡。例如: | transition> button v-if="docState === 'saved'" key="saved"> Edit button> button v-if="docState === 'edited'" key="edited"> Save button> button v-if="docState === 'editing'" key="editing"> Cancel button> transition> | 可以重写为: | transition> button v-bind:key="docState"> {{ buttonMessage }} button> transition> | | // ... computed: { buttonMessage: function () { switch (this.docState) { case 'saved': return 'Edit' case 'edited': return 'Save' case 'editing': return 'Cancel' } } } | ### [过渡模式](https://cn.vuejs.org/v2/guide/transitions.html#%E8%BF%87%E6%B8%A1%E6%A8%A1%E5%BC%8F "过渡模式") 这里还有一个问题,试着点击下面的按钮: off 在 “on” 按钮和 “off” 按钮的过渡中,两个按钮都被重绘了,一个离开过渡的时候另一个开始进入过渡。这是 `<transition>` 的默认行为 - 进入和离开同时发生。 在元素绝对定位在彼此之上的时候运行正常: off 然后,我们加上 translate 让它们运动像滑动过渡: off 同时生效的进入和离开的过渡不能满足所有要求,所以 Vue 提供了 过渡模式 * `in-out`:新元素先进行过渡,完成之后当前元素过渡离开。 * `out-in`:当前元素先进行过渡,完成之后新元素过渡进入。 用 `out-in` 重写之前的开关按钮过渡: | transition name="fade" mode="out-in"> transition> | off 只用添加一个简单的特性,就解决了之前的过渡问题而无需任何额外的代码。 `in-out` 模式不是经常用到,但对于一些稍微不同的过渡效果还是有用的。 将之前滑动淡出的例子结合: off 很酷吧? ## [多个组件的过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%A4%9A%E4%B8%AA%E7%BB%84%E4%BB%B6%E7%9A%84%E8%BF%87%E6%B8%A1 "多个组件的过渡") 多个组件的过渡简单很多 - 我们不需要使用 `key` 特性。相反,我们只需要使用[动态组件](https://cn.vuejs.org/v2/guide/components.html#%E5%8A%A8%E6%80%81%E7%BB%84%E4%BB%B6): | transition name="component-fade" mode="out-in"> component v-bind:is="view">component> transition> | | new Vue({ el: '#transition-components-demo', data: { view: 'v-a' }, components: { 'v-a': { template: 'Component A' }, 'v-b': { template: 'Component B' } } }) | | .component-fade-enter-active, .component-fade-leave-active { transition: opacity .3s ease; } .component-fade-enter, .component-fade-leave-to /* .component-fade-leave-active for below version 2.1.8 */ { opacity: 0; } | A B Component A ## [列表过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%88%97%E8%A1%A8%E8%BF%87%E6%B8%A1 "列表过渡") 目前为止,关于过渡我们已经讲到: * 单个节点 * 同一时间渲染多个节点中的一个 那么怎么同时渲染整个列表,比如使用 `v-for` ?在这种场景中,使用 `<transition-group>` 组件。在我们深入例子之前,先了解关于这个组件的几个特点: * 不同于 `<transition>`,它会以一个真实元素呈现:默认为一个 `<span>`。你也可以通过 `tag` 特性更换为其他元素。 * [过渡模式](https://cn.vuejs.org/v2/guide/transitions.html#%E8%BF%87%E6%B8%A1%E6%A8%A1%E5%BC%8F)不可用,因为我们不再相互切换特有的元素。 * 内部元素 总是需要 提供唯一的 `key` 属性值。 ### [列表的进入/离开过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%88%97%E8%A1%A8%E7%9A%84%E8%BF%9B%E5%85%A5-%E7%A6%BB%E5%BC%80%E8%BF%87%E6%B8%A1 "列表的进入/离开过渡") 现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS 类名。 | div id="list-demo" class="demo"> button v-on:click="add">Addbutton> button v-on:click="remove">Removebutton> transition-group name="list" tag="p"> span v-for="item in items" v-bind:key="item" class="list-item"> {{ item }} span> transition-group> div> | | new Vue({ el: '#list-demo', data: { items: [1,2,3,4,5,6,7,8,9], nextNum: 10 }, methods: { randomIndex: function () { return Math.floor(Math.random() * this.items.length) }, add: function () { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove: function () { this.items.splice(this.randomIndex(), 1) }, } }) | | .list-item { display: inline-block; margin-right: 10px; } .list-enter-active, .list-leave-active { transition: all 1s; } .list-enter, .list-leave-to /* .list-leave-active for below version 2.1.8 */ { opacity: 0; transform: translateY(30px); } | Add Remove 123456789 这个例子有个问题,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。 ### [列表的排序过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%88%97%E8%A1%A8%E7%9A%84%E6%8E%92%E5%BA%8F%E8%BF%87%E6%B8%A1 "列表的排序过渡") `<transition-group>` 组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 `v-move` 特性,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 `name` 属性来自定义前缀,也可以通过 `move-class` 属性手动设置。 `v-move` 对于设置过渡的切换时机和过渡曲线非常有用,你会看到如下的例子: | script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js">script> div id="flip-list-demo" class="demo"> button v-on:click="shuffle">Shufflebutton> transition-group name="flip-list" tag="ul"> li v-for="item in items" v-bind:key="item"> {{ item }} li> transition-group> div> | | new Vue({ el: '#flip-list-demo', data: { items: [1,2,3,4,5,6,7,8,9] }, methods: { shuffle: function () { this.items = _.shuffle(this.items) } } }) | | .flip-list-move { transition: transform 1s; } | Shuffle * 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 这个看起来很神奇,内部的实现,Vue 使用了一个叫 [FLIP](https://aerotwist.com/blog/flip-your-animations/) 简单的动画队列 使用 transforms 将元素从之前的位置平滑过渡新的位置。 我们将之前实现的例子和这个技术结合,使我们列表的一切变动都会有动画过渡。 | script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js">script> div id="list-complete-demo" class="demo"> button v-on:click="shuffle">Shufflebutton> button v-on:click="add">Addbutton> button v-on:click="remove">Removebutton> transition-group name="list-complete" tag="p"> span v-for="item in items" v-bind:key="item" class="list-complete-item" > {{ item }} span> transition-group> div> | | new Vue({ el: '#list-complete-demo', data: { items: [1,2,3,4,5,6,7,8,9], nextNum: 10 }, methods: { randomIndex: function () { return Math.floor(Math.random() * this.items.length) }, add: function () { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove: function () { this.items.splice(this.randomIndex(), 1) }, shuffle: function () { this.items = _.shuffle(this.items) } } }) | | .list-complete-item { transition: all 1s; display: inline-block; margin-right: 10px; } .list-complete-enter, .list-complete-leave-to /* .list-complete-leave-active for below version 2.1.8 */ { opacity: 0; transform: translateY(30px); } .list-complete-leave-active { position: absolute; } | Shuffle Add Remove 123456789 需要注意的是使用 FLIP 过渡的元素不能设置为 `display: inline` 。作为替代方案,可以设置为 `display: inline-block` 或者放置于 flex 中 FLIP 动画不仅可以实现单列过渡,多维网格也[同样可以过渡](https://jsfiddle.net/chrisvfritz/sLrhk1bc/): Lazy Sudoku Keep hitting the shuffle button until you win. Shuffle 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 ### [列表的交错过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%88%97%E8%A1%A8%E7%9A%84%E4%BA%A4%E9%94%99%E8%BF%87%E6%B8%A1 "列表的交错过渡") 通过 data 属性与 JavaScript 通信 ,就可以实现列表的交错过渡: | script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js">script> div id="staggered-list-demo"> input v-model="query"> transition-group name="staggered-fade" tag="ul" v-bind:css="false" v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:leave="leave" > li v-for="(item, index) in computedList" v-bind:key="item.msg" v-bind:data-index="index" >{{ item.msg }}li> transition-group> div> | | new Vue({ el: '#staggered-list-demo', data: { query: '', list: [ { msg: 'Bruce Lee' }, { msg: 'Jackie Chan' }, { msg: 'Chuck Norris' }, { msg: 'Jet Li' }, { msg: 'Kung Fury' } ] }, computed: { computedList: function () { var vm = this return this.list.filter(function (item) { return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1 }) } }, methods: { beforeEnter: function (el) { el.style.opacity = 0 el.style.height = 0 }, enter: function (el, done) { var delay = el.dataset.index * 150 setTimeout(function () { Velocity( el, { opacity: 1, height: '1.6em' }, { complete: done } ) }, delay) }, leave: function (el, done) { var delay = el.dataset.index * 150 setTimeout(function () { Velocity( el, { opacity: 0, height: 0 }, { complete: done } ) }, delay) } } }) | * Bruce Lee * Jackie Chan * Chuck Norris * Jet Li * Kung Fury ## [可复用的过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%8F%AF%E5%A4%8D%E7%94%A8%E7%9A%84%E8%BF%87%E6%B8%A1 "可复用的过渡") 过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 `<transition>` 或者 `<transition-group>` 作为根组件,然后将任何子组件放置在其中就可以了。 使用 template 的简单例子: | Vue.component('my-special-transition', { template: '\ name="very-special-transition"\ mode="out-in"\ v-on:before-enter="beforeEnter"\ v-on:after-enter="afterEnter"\ >\ \ \ ', methods: { beforeEnter: function (el) { // ... }, afterEnter: function (el) { // ... } } }) | 函数组件更适合完成这个任务: | Vue.component('my-special-transition', { functional: true, render: function (createElement, context) { var data = { props: { name: 'very-special-transition', mode: 'out-in' }, on: { beforeEnter: function (el) { // ... }, afterEnter: function (el) { // ... } } } return createElement('transition', data, context.children) } }) | ## [动态过渡](https://cn.vuejs.org/v2/guide/transitions.html#%E5%8A%A8%E6%80%81%E8%BF%87%E6%B8%A1 "动态过渡") 在 Vue 中即使是过渡也是数据驱动的!动态过渡最基本的例子是通过 `name` 特性来绑定动态值。 | transition v-bind:name="transitionName"> transition> | 当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画 在不同过渡间切换会非常有用。 所有过渡特性都可以动态绑定,但我们不仅仅只有特性可以利用,还可以通过事件钩子获取上下文中的所有数据,因为事件钩子都是方法。这意味着,根据组件的状态不同,你的 JavaScript 过渡会有不同的表现。 | script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js">script> div id="dynamic-fade-demo" class="demo"> Fade In: input type="range" v-model="fadeInDuration" min="0" v-bind:max="maxFadeDuration"> Fade Out: input type="range" v-model="fadeOutDuration" min="0" v-bind:max="maxFadeDuration"> transition v-bind:css="false" v-on:before-enter="beforeEnter" v-on:enter="enter" v-on:leave="leave" > p v-if="show">hellop> transition> button v-if="stop" v-on:click="stop = false; show = false" >Start animatingbutton> button v-else v-on:click="stop = true" >Stop it!button> div> | | new Vue({ el: '#dynamic-fade-demo', data: { show: true, fadeInDuration: 1000, fadeOutDuration: 1000, maxFadeDuration: 1500, stop: true }, mounted: function () { this.show = false }, methods: { beforeEnter: function (el) { el.style.opacity = 0 }, enter: function (el, done) { var vm = this Velocity(el, { opacity: 1 }, { duration: this.fadeInDuration, complete: function () { done() if (!vm.stop) vm.show = false } } ) }, leave: function (el, done) { var vm = this Velocity(el, { opacity: 0 }, { duration: this.fadeOutDuration, complete: function () { done() vm.show = true } } ) } } }) | Fade In:  Fade Out:  hello Start animating 最后,创建动态过渡的最终方案是组件通过接受 props 来动态修改之前的过渡。一句老话,唯一的限制是你的想象力。 # 状态过渡 Vue 的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。那么对于数据元素本身的动效呢,比如: * 数字和运算 * 颜色的显示 * SVG 节点的位置 * 元素的大小和其他的属性 所有的原始数字都被事先存储起来,可以直接转换到数字。做到这一步,我们就可以结合 Vue 的响应式和组件系统,使用第三方库来实现切换元素的过渡状态。 ## [状态动画与侦听器](https://cn.vuejs.org/v2/guide/transitioning-state.html#%E7%8A%B6%E6%80%81%E5%8A%A8%E7%94%BB%E4%B8%8E%E4%BE%A6%E5%90%AC%E5%99%A8 "状态动画与侦听器") 通过侦听器我们能监听到任何数值属性的数值更新。可能听起来很抽象,所以让我们先来看看使用 [GreenSock](https://greensock.com/) 一个例子: | script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.min.js">script> div id="animated-number-demo"> input v-model.number="number" type="number" step="20"> p>{{ animatedNumber }}p> div> | | new Vue({ el: '#animated-number-demo', data: { number: 0, tweenedNumber: 0 }, computed: { animatedNumber: function() { return this.tweenedNumber.toFixed(0); } }, watch: { number: function(newValue) { TweenLite.to(this.$data, 0.5, { tweenedNumber: newValue }); } } }) | 0 当你把数值更新时,就会触发动画。这个是一个不错的演示,但是对于不能直接像数字一样存储的值,比如 CSS 中的 color 的值,通过下面的例子我们来通过 [Tween.js](https://github.com/tweenjs/tween.js) 和 [Color.js](https://github.com/brehaut/color-js) 实现一个例子: | script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4">script> script src="https://cdn.jsdelivr.net/npm/color-js@1.0.3">script> div id="example-7"> input v-model="colorQuery" v-on:keyup.enter="updateColor" placeholder="Enter a color" > button v-on:click="updateColor">Updatebutton> p>Preview:p> span v-bind:style="{ backgroundColor: tweenedCSSColor }" class="example-7-color-preview" >span> p>{{ tweenedCSSColor }}p> div> | | var Color = net.brehaut.Color new Vue({ el: '#example-7', data: { colorQuery: '', color: { red: 0, green: 0, blue: 0, alpha: 1 }, tweenedColor: {} }, created: function () { this.tweenedColor = Object.assign({}, this.color) }, watch: { color: function () { function animate () { if (TWEEN.update()) { requestAnimationFrame(animate) } } new TWEEN.Tween(this.tweenedColor) .to(this.color, 750) .start() animate() } }, computed: { tweenedCSSColor: function () { return new Color({ red: this.tweenedColor.red, green: this.tweenedColor.green, blue: this.tweenedColor.blue, alpha: this.tweenedColor.alpha }).toCSS() } }, methods: { updateColor: function () { this.color = new Color(this.colorQuery).toRGB() this.colorQuery = '' } } }) | | .example-7-color-preview { display: inline-block; width: 50px; height: 50px; } |  Update Preview: #000000 ## [动态状态过渡](https://cn.vuejs.org/v2/guide/transitioning-state.html#%E5%8A%A8%E6%80%81%E7%8A%B6%E6%80%81%E8%BF%87%E6%B8%A1 "动态状态过渡") 就像 Vue 的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。当你修改一些变量,即使是一个简单的 SVG 多边形也可实现很多难以想象的效果。 Sides: 10Minimum Radius: 50%Update Interval: 500 milliseconds 上述 demo 背后的代码可以通过[这个 fiddle](https://jsfiddle.net/chrisvfritz/65gLu2b6/) 进行详阅。 ## [把过渡放到组件里](https://cn.vuejs.org/v2/guide/transitioning-state.html#%E6%8A%8A%E8%BF%87%E6%B8%A1%E6%94%BE%E5%88%B0%E7%BB%84%E4%BB%B6%E9%87%8C "把过渡放到组件里") 管理太多的状态过渡会很快的增加 Vue 实例或者组件的复杂性,幸好很多的动画可以提取到专用的子组件。 我们来将之前的示例改写一下: | script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4">script> div id="example-8"> input v-model.number="firstNumber" type="number" step="20"> + input v-model.number="secondNumber" type="number" step="20"> = {{ result }} p> animated-integer v-bind:value="firstNumber">animated-integer> + animated-integer v-bind:value="secondNumber">animated-integer> = animated-integer v-bind:value="result">animated-integer> p> div> | | // 这种复杂的补间动画逻辑可以被复用 // 任何整数都可以执行动画 // 组件化使我们的界面十分清晰 // 可以支持更多更复杂的动态过渡 // 策略。 Vue.component('animated-integer', { template: '{{ tweeningValue }}', props: { value: { type: Number, required: true } }, data: function () { return { tweeningValue: 0 } }, watch: { value: function (newValue, oldValue) { this.tween(oldValue, newValue) } }, mounted: function () { this.tween(0, this.value) }, methods: { tween: function (startValue, endValue) { var vm = this function animate () { if (TWEEN.update()) { requestAnimationFrame(animate) } } new TWEEN.Tween({ tweeningValue: startValue }) .to({ tweeningValue: endValue }, 500) .onUpdate(function (object) { vm.tweeningValue = object.tweeningValue.toFixed(0) }) .start() animate() } } }) // 所有的复杂度都已经从 Vue 的主实例中移除! new Vue({ el: '#example-8', data: { firstNumber: 20, secondNumber: 40 }, computed: { result: function () { return this.firstNumber + this.secondNumber } } }) |  +  = 60 20 + 40 = 60 我们能在组件中结合使用这一节讲到各种过渡策略和 Vue [内建的过渡系统](https://cn.vuejs.org/v2/guide/transitions.html)。总之,对于完成各种过渡动效几乎没有阻碍。 ## [赋予设计以生命](https://cn.vuejs.org/v2/guide/transitioning-state.html#%E8%B5%8B%E4%BA%88%E8%AE%BE%E8%AE%A1%E4%BB%A5%E7%94%9F%E5%91%BD "赋予设计以生命") 只要一个动画,就可以带来生命。不幸的是,当设计师创建图标、logo 和吉祥物的时候,他们交付的通常都是图片或静态的 SVG。所以,虽然 GitHub 的章鱼猫、Twitter 的小鸟以及其它许多 logo 类似于生灵,它们看上去实际上并不是活着的。 Vue 可以帮到你。因为 SVG 的本质是数据,我们只需要这些动物兴奋、思考或警戒的样例。然后 Vue 就可以辅助完成这几种状态之间的过渡动画,来制作你的欢迎页面、加载指示、以及更加带有情感的提示。 Sarah Drasner 展示了下面这个 demo,这个 demo 结合了时间和交互相关的状态改变: # 混入 ## [基础](https://cn.vuejs.org/v2/guide/mixins.html#%E5%9F%BA%E7%A1%80 "基础") 混入 (mixins) 是一种分发 Vue 组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。 例子: | // 定义一个混入对象 var myMixin = { created: function () { this.hello() }, methods: { hello: function () { console.log('hello from mixin!') } } } // 定义一个使用混入对象的组件 var Component = Vue.extend({ mixins: [myMixin] }) var component = new Component() // => "hello from mixin!" | ## [选项合并](https://cn.vuejs.org/v2/guide/mixins.html#%E9%80%89%E9%A1%B9%E5%90%88%E5%B9%B6 "选项合并") 当组件和混入对象含有同名选项时,这些选项将以恰当的方式混合。 比如,数据对象在内部会进行浅合并 (一层属性深度),在和组件的数据发生冲突时以组件数据优先。 | var mixin = { data: function () { return { message: 'hello', foo: 'abc' } } } new Vue({ mixins: [mixin], data: function () { return { message: 'goodbye', bar: 'def' } }, created: function () { console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" } } }) | 同名钩子函数将混合为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。 | var mixin = { created: function () { console.log('混入对象的钩子被调用') } } new Vue({ mixins: [mixin], created: function () { console.log('组件钩子被调用') } }) // => "混入对象的钩子被调用" // => "组件钩子被调用" | 值为对象的选项,例如 `methods`, `components` 和 `directives`,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。 | var mixin = { methods: { foo: function () { console.log('foo') }, conflicting: function () { console.log('from mixin') } } } var vm = new Vue({ mixins: [mixin], methods: { bar: function () { console.log('bar') }, conflicting: function () { console.log('from self') } } }) vm.foo() // => "foo" vm.bar() // => "bar" vm.conflicting() // => "from self" | 注意:`Vue.extend()` 也使用同样的策略进行合并。 ## [全局混入](https://cn.vuejs.org/v2/guide/mixins.html#%E5%85%A8%E5%B1%80%E6%B7%B7%E5%85%A5 "全局混入") 也可以全局注册混入对象。注意使用! 一旦使用全局混入对象,将会影响到 所有 之后创建的 Vue 实例。使用恰当时,可以为自定义对象注入处理逻辑。 | // 为自定义的选项 'myOption' 注入一个处理器。 Vue.mixin({ created: function () { var myOption = this.$options.myOption if (myOption) { console.log(myOption) } } }) new Vue({ myOption: 'hello!' }) // => "hello!" | 谨慎使用全局混入对象,因为会影响到每个单独创建的 Vue 实例 (包括第三方模板)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。也可以将其用作 [Plugins](https://cn.vuejs.org/v2/guide/plugins.html) 以避免产生重复应用 ## [自定义选项合并策略](https://cn.vuejs.org/v2/guide/mixins.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E9%80%89%E9%A1%B9%E5%90%88%E5%B9%B6%E7%AD%96%E7%95%A5 "自定义选项合并策略") 自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 `Vue.config.optionMergeStrategies` 添加一个函数: | Vue.config.optionMergeStrategies.myOption = function (toVal, fromVal) { // return mergedVal } | 对于大多数对象选项,可以使用 `methods` 的合并策略: | var strategies = Vue.config.optionMergeStrategies strategies.myOption = strategies.methods | 更多高级的例子可以在 [Vuex](https://github.com/vuejs/vuex) 的 1.x 混入策略里找到: | const merge = Vue.config.optionMergeStrategies.computed Vue.config.optionMergeStrategies.vuex = function (toVal, fromVal) { if (!toVal) return fromVal if (!fromVal) return toVal return { getters: merge(toVal.getters, fromVal.getters), state: merge(toVal.state, fromVal.state), actions: merge(toVal.actions, fromVal.actions) } } | # 自定义指令 ## [简介](https://cn.vuejs.org/v2/guide/custom-directive.html#%E7%AE%80%E4%BB%8B "简介") 除了核心功能默认内置的指令 (`v-model` 和 `v-show`),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下: 当页面加载时,该元素将获得焦点 (注意:`autofocus` 在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。现在让我们用指令来实现这个功能: | // 注册一个全局自定义指令 `v-focus` Vue.directive('focus', { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el) { // 聚焦元素 el.focus() } }) | 如果想注册局部指令,组件中也接受一个 `directives` 的选项: | directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() } } } | 然后你可以在模板中任何元素上使用新的 `v-focus` 属性,如下: | input v-focus> | ## [钩子函数](https://cn.vuejs.org/v2/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0 "钩子函数") 一个指令定义对象可以提供如下几个钩子函数 (均为可选): * `bind`:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。 * `inserted`:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。 * `update`:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。 * `componentUpdated`:指令所在组件的 VNode 及其子 VNode 全部更新后调用。 * `unbind`:只调用一次,指令与元素解绑时调用。 接下来我们来看一下钩子函数的参数 (即 `el`、`binding`、`vnode` 和 `oldVnode`)。 ## [钩子函数参数](https://cn.vuejs.org/v2/guide/custom-directive.html#%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0 "钩子函数参数") 指令钩子函数会被传入以下参数: * `el`:指令所绑定的元素,可以用来直接操作 DOM 。 * `binding`:一个对象,包含以下属性: * `name`:指令名,不包括 `v-` 前缀。 * `value`:指令的绑定值,例如:`v-my-directive="1 + 1"` 中,绑定值为 `2`。 * `oldValue`:指令绑定的前一个值,仅在 `update` 和 `componentUpdated` 钩子中可用。无论值是否改变都可用。 * `expression`:字符串形式的指令表达式。例如 `v-my-directive="1 + 1"`中,表达式为 `"1 + 1"`。 * `arg`:传给指令的参数,可选。例如 `v-my-directive:foo` 中,参数为 `"foo"`。 * `modifiers`:一个包含修饰符的对象。例如:`v-my-directive.foo.bar` 中,修饰符对象为 `{ foo: true, bar: true }`。 * `vnode`:Vue 编译生成的虚拟节点。移步 [VNode API](https://cn.vuejs.org/v2/api/#VNode-%E6%8E%A5%E5%8F%A3) 来了解更多详情。 * `oldVnode`:上一个虚拟节点,仅在 `update` 和 `componentUpdated` 钩子中可用。 除了 `el` 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 [`dataset`](https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/dataset) 来进行。 这是一个使用了这些属性的自定义钩子样例: | div id="hook-arguments-example" v-demo:foo.a.b="message">div> | | Vue.directive('demo', { bind: function (el, binding, vnode) { var s = JSON.stringify el.innerHTML = 'name: ' + s(binding.name) + '' + 'value: ' + s(binding.value) + '' + 'expression: ' + s(binding.expression) + '' + 'argument: ' + s(binding.arg) + '' + 'modifiers: ' + s(binding.modifiers) + '' + 'vnode keys: ' + Object.keys(vnode).join(', ') } }) new Vue({ el: '#hook-arguments-example', data: { message: 'hello!' } }) | name: "demo" value: "hello!" expression: "message" argument: "foo" modifiers: {"a":true,"b":true} vnode keys: tag, data, children, text, elm, ns, context, fnContext, fnOptions, fnScopeId, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, isOnce, asyncFactory, asyncMeta, isAsyncPlaceholder ## [函数简写](https://cn.vuejs.org/v2/guide/custom-directive.html#%E5%87%BD%E6%95%B0%E7%AE%80%E5%86%99 "函数简写") 在很多时候,你可能想在 `bind` 和 `update` 时触发相同行为,而不关心其它的钩子。比如这样写: | Vue.directive('color-swatch', function (el, binding) { el.style.backgroundColor = binding.value }) | ## [对象字面量](https://cn.vuejs.org/v2/guide/custom-directive.html#%E5%AF%B9%E8%B1%A1%E5%AD%97%E9%9D%A2%E9%87%8F "对象字面量") 如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。 | div v-demo="{ color: 'white', text: 'hello!' }">div> | | Vue.directive('demo', function (el, binding) { console.log(binding.value.color) // => "white" console.log(binding.value.text) // => "hello!" }) | # 渲染函数 & JSX ## [基础](https://cn.vuejs.org/v2/guide/render-function.html#%E5%9F%BA%E7%A1%80 "基础") Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力,这时你可以用 render 函数,它比 template 更接近编译器。 让我们深入一个简单的例子,这个例子里 `render` 函数很实用。假设我们要生成锚点标题 (anchored headings): | h1> a name="hello-world" href="#hello-world"> Hello world! a> h1> | 对于上面的 HTML,我们决定这样定义组件接口: | anchored-heading :level="1">Hello world!anchored-heading> | 当我们开始写一个只能通过 `level` prop 动态生成 heading 标签的组件时,你可能很快想到这样实现: | script type="text/x-template" id="anchored-heading-template"> if="level === 1"> slot> h1> else-if="level === 2"> slot> h2> else-if="level === 3"> slot> h3> else-if="level === 4"> slot> h4> else-if="level === 5"> slot> h5> else-if="level === 6"> slot> h6> script> | | Vue.component('anchored-heading', { template: '#anchored-heading-template', props: { level: { type: Number, required: true } } }) | 在这种场景中使用 template 并不是最好的选择:首先代码冗长,为了在不同级别的标题中插入锚点元素,我们需要重复地使用 `<slot></slot>`。 虽然模板在大多数组件中都非常好用,但是在这里它就不是很简洁的了。那么,我们来尝试使用 `render` 函数重写上面的例子: | Vue.component('anchored-heading', { render: function (createElement) { return createElement( 'h' + this.level, // 标签名称 this.$slots.default // 子元素数组 ) }, props: { level: { type: Number, required: true } } }) | 简单清晰很多!简单来说,这样代码精简很多,但是需要非常熟悉 Vue 的实例属性。在这个例子中,你需要知道,向组件中传递不带 `slot` 特性的子元素时,比如 `anchored-heading` 中的 `Hello world!`,这些子元素被存储在组件实例中的 `$slots.default` 中。如果你还不了解,在深入 render 函数之前推荐阅读[实例属性 API](https://cn.vuejs.org/v2/api/#%E5%AE%9E%E4%BE%8B%E5%B1%9E%E6%80%A7)。 ## [节点、树以及虚拟 DOM](https://cn.vuejs.org/v2/guide/render-function.html#%E8%8A%82%E7%82%B9%E3%80%81%E6%A0%91%E4%BB%A5%E5%8F%8A%E8%99%9A%E6%8B%9F-DOM "节点、树以及虚拟 DOM") 在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例: | div> h1>My titleh1> Some text content TODO: 添加标签行 --> div> | 当浏览器读到这些代码时,它会建立一个[“DOM 节点”树](https://javascript.info/dom-nodes)来保持追踪,如同你会画一张家谱树来追踪家庭成员的发展一样。 HTML 的 DOM 节点树如下图所示: ![DOM Tree Visualization](https://cn.vuejs.org/images/dom-tree.png) 每个元素都是一个节点。每片文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。 高效的更新所有这些节点会是比较困难的,不过所幸你不必再手动完成这个工作了。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里: | h1>{{ blogTitle }}h1> | 或者一个渲染函数里: | render: function (createElement) { return createElement('h1', this.blogTitle) } | 在这两种情况下,Vue 都会自动保持页面的更新,即便 `blogTitle` 发生了改变。 ### [虚拟 DOM](https://cn.vuejs.org/v2/guide/render-function.html#%E8%99%9A%E6%8B%9F-DOM "虚拟 DOM") Vue 通过建立一个虚拟 DOM 对真实 DOM 发生的变化保持追踪。请仔细看这行代码: | return createElement('h1', this.blogTitle) | `createElement` 到底会返回什么呢?其实不是一个*实际的* DOM 元素。它更准确的名字可能是 `createNodeDescription`,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,及其子节点。我们把这样的节点描述为“虚拟节点 (Virtual Node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。 ## [`createElement` 参数](https://cn.vuejs.org/v2/guide/render-function.html#createElement-%E5%8F%82%E6%95%B0 "createElement 参数") 接下来你需要熟悉的是如何在 `createElement` 函数中生成模板。这里是 `createElement` 接受的参数: | // @returns {VNode} createElement( // {String | Object | Function} // 一个 HTML 标签字符串,组件选项对象,或者 // 解析上述任何一种的一个 async 异步函数。必需参数。 'div', // {Object} // 一个包含模板相关属性的数据对象 // 你可以在 template 中使用这些特性。可选参数。 { // (详情见下一节) }, // {String | Array} // 子虚拟节点 (VNodes),由 `createElement()` 构建而成, // 也可以使用字符串来生成“文本虚拟节点”。可选参数。 [ '先写一些文字', createElement('h1', '一则头条'), createElement(MyComponent, { props: { someProp: 'foobar' } }) ] ) | ### [深入 data 对象](https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5-data-%E5%AF%B9%E8%B1%A1 "深入 data 对象") 有一点要注意:正如在模板语法中,`v-bind:class` 和 `v-bind:style`,会被特别对待一样,在 VNode 数据对象中,下列属性名是级别最高的字段。该对象也允许你绑定普通的 HTML 特性,就像 DOM 属性一样,比如 `innerHTML` (这会取代 `v-html` 指令)。 | { // 和`v-bind:class`一样的 API // 接收一个字符串、对象或字符串和对象组成的数组 'class': { foo: true, bar: false }, // 和`v-bind:style`一样的 API // 接收一个字符串、对象或对象组成的数组 style: { color: 'red', fontSize: '14px' }, // 普通的 HTML 特性 attrs: { id: 'foo' }, // 组件 props props: { myProp: 'bar' }, // DOM 属性 domProps: { innerHTML: 'baz' }, // 事件监听器基于 `on` // 所以不再支持如 `v-on:keyup.enter` 修饰器 // 需要手动匹配 keyCode。 on: { click: this.clickHandler }, // 仅用于组件,用于监听原生事件,而不是组件内部使用 // `vm.$emit` 触发的事件。 nativeOn: { click: this.nativeClickHandler }, // 自定义指令。注意,你无法对 `binding` 中的 `oldValue` // 赋值,因为 Vue 已经自动为你进行了同步。 directives: [ { name: 'my-custom-directive', value: '2', expression: '1 + 1', arg: 'foo', modifiers: { bar: true } } ], // 作用域插槽格式 // { name: props => VNode | Array } scopedSlots: { default: props => createElement('span', props.text) }, // 如果组件是其他组件的子组件,需为插槽指定名称 slot: 'name-of-slot', // 其他特殊顶层属性 key: 'myKey', ref: 'myRef', // 如果你在渲染函数中向多个元素都应用了相同的 ref 名, // 那么 `$refs.myRef` 会变成一个数组。 refInFor: true } | ### [完整示例](https://cn.vuejs.org/v2/guide/render-function.html#%E5%AE%8C%E6%95%B4%E7%A4%BA%E4%BE%8B "完整示例") 有了这些知识,我们现在可以完成我们最开始想实现的组件: | var getChildrenTextContent = function (children) { return children.map(function (node) { return node.children ? getChildrenTextContent(node.children) : node.text }).join('') } Vue.component('anchored-heading', { render: function (createElement) { // 创建 kebab-case 风格的ID var headingId = getChildrenTextContent(this.$slots.default) .toLowerCase() .replace(/\W+/g, '-') .replace(/(^\-|\-$)/g, '') return createElement( 'h' + this.level, [ createElement('a', { attrs: { name: headingId, href: '#' + headingId } }, this.$slots.default) ] ) }, props: { level: { type: Number, required: true } } }) | ### [约束](https://cn.vuejs.org/v2/guide/render-function.html#%E7%BA%A6%E6%9D%9F "约束") #### [](https://cn.vuejs.org/v2/guide/render-function.html#VNodes-%E5%BF%85%E9%A1%BB%E5%94%AF%E4%B8%80 "VNodes 必须唯一")VNodes 必须唯一 组件树中的所有 VNodes 必须是唯一的。这意味着,下面的 render function 是无效的: | render: function (createElement) { var myParagraphVNode = createElement('p', 'hi') return createElement('div', [ // 错误-重复的 VNodes myParagraphVNode, myParagraphVNode ]) } | 如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这个例子 render 函数完美有效地渲染了 20 个相同的段落: | render: function (createElement) { return createElement('div', Array.apply(null, { length: 20 }).map(function () { return createElement('p', 'hi') }) ) } | ## [使用 JavaScript 代替模板功能](https://cn.vuejs.org/v2/guide/render-function.html#%E4%BD%BF%E7%94%A8-JavaScript-%E4%BB%A3%E6%9B%BF%E6%A8%A1%E6%9D%BF%E5%8A%9F%E8%83%BD "使用 JavaScript 代替模板功能") ### [`v-if` 和 `v-for`](https://cn.vuejs.org/v2/guide/render-function.html#v-if-%E5%92%8C-v-for "v-if 和 v-for") 只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的 render 函数就不会提供专有的替代方法。比如,在 template 中使用的 `v-if` 和 `v-for`: | ul v-if="items.length"> li v-for="item in items">{{ item.name }}li> ul> p v-else>No items found.p> | 这些都会在 render 函数中被 JavaScript 的 `if`/`else` 和 `map` 重写: | props: ['items'], render: function (createElement) { if (this.items.length) { return createElement('ul', this.items.map(function (item) { return createElement('li', item.name) })) } else { return createElement('p', 'No items found.') } } | ### [`v-model`](https://cn.vuejs.org/v2/guide/render-function.html#v-model "v-model") render 函数中没有与 `v-model` 的直接对应 - 你必须自己实现相应的逻辑: | props: ['value'], render: function (createElement) { var self = this return createElement('input', { domProps: { value: self.value }, on: { input: function (event) { self.$emit('input', event.target.value) } } }) } | 这就是深入底层的代价,但与 `v-model` 相比,这可以让你更好地控制交互细节。 ### [事件 & 按键修饰符](https://cn.vuejs.org/v2/guide/render-function.html#%E4%BA%8B%E4%BB%B6-amp-%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6 "事件 & 按键修饰符") 对于 `.passive`、`.capture` 和 `.once`事件修饰符, Vue 提供了相应的前缀可以用于 `on`: | Modifier(s) | Prefix | | --- | --- | | `.passive` | `&` | | `.capture` | `!` | | `.once` | `~` | | `.capture.once` or `.once.capture` | `~!` | 例如: | on: { '!click': this.doThisInCapturingMode, '~keyup': this.doThisOnce, '~!mouseover': this.doThisOnceInCapturingMode } | 对于其他的修饰符,前缀不是很重要,因为你可以在事件处理函数中使用事件方法: | Modifier(s) | Equivalent in Handler | | --- | --- | | `.stop` | `event.stopPropagation()` | | `.prevent` | `event.preventDefault()` | | `.self` | `if (event.target !== event.currentTarget) return` | | Keys: `.enter`, `.13` | `if (event.keyCode !== 13) return` (change `13` to [another key code](http://keycode.info/) for other key modifiers) | | Modifiers Keys: `.ctrl`, `.alt`, `.shift`, `.meta` | `if (!event.ctrlKey) return` (change `ctrlKey` to `altKey`, `shiftKey`, or `metaKey`, respectively) | 这里是一个使用所有修饰符的例子: | on: { keyup: function (event) { // 如果触发事件的元素不是事件绑定的元素 // 则返回 if (event.target !== event.currentTarget) return // 如果按下去的不是 enter 键或者 // 没有同时按下 shift 键 // 则返回 if (!event.shiftKey || event.keyCode !== 13) return // 阻止 事件冒泡 event.stopPropagation() // 阻止该元素默认的 keyup 事件 event.preventDefault() // ... } } | ### [插槽](https://cn.vuejs.org/v2/guide/render-function.html#%E6%8F%92%E6%A7%BD "插槽") 你可以通过 [`this.$slots`](https://cn.vuejs.org/v2/api/#vm-slots) 访问静态插槽的内容,得到的是一个 VNodes 数组: | render: function (createElement) { // `` return createElement('div', this.$slots.default) } | 也可以通过 [`this.$scopedSlots`](https://cn.vuejs.org/v2/api/#vm-scopedSlots) 访问作用域插槽,得到的是一个返回 VNodes 的函数: | props: ['message'], render: function (createElement) { // `` return createElement('div', [ this.$scopedSlots.default({ text: this.message }) ]) } | 如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 `scopedSlots` 域: | render: function (createElement) { return createElement('div', [ createElement('child', { // 在数据对象中传递 `scopedSlots` // 格式:{ name: props => VNode | Array } scopedSlots: { default: function (props) { return createElement('span', props.text) } } }) ]) } | ## [JSX](https://cn.vuejs.org/v2/guide/render-function.html#JSX "JSX") 如果你写了很多 `render` 函数,可能会觉得下面这样的代码写起来很痛苦: | createElement( 'anchored-heading', { props: { level: 1 } }, [ createElement('span', 'Hello'), ' world!' ] ) | 特别是模板如此简单的情况下: | anchored-heading :level="1"> span>Hellospan> world! anchored-heading> | 这就是为什么会有一个 [Babel 插件](https://github.com/vuejs/babel-plugin-transform-vue-jsx),用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。 | import AnchoredHeading from './AnchoredHeading.vue' new Vue({ el: '#demo', render: function (h) { return ( 1}> Hello/span> world! AnchoredHeading> ) } }) | 将 `h` 作为 `createElement` 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的,如果在作用域中 `h` 失去作用,在应用中会触发报错。 更多关于 JSX 映射到 JavaScript,阅读 [使用文档](https://github.com/vuejs/babel-plugin-transform-vue-jsx#usage)。 ## [函数式组件](https://cn.vuejs.org/v2/guide/render-function.html#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BB%84%E4%BB%B6 "函数式组件") 之前创建的锚点标题组件是比较简单,没有管理或者监听任何传递给他的状态,也没有生命周期方法。它只是一个接收参数的函数。 在这个例子中,我们标记组件为 `functional`,这意味它是无状态 (没有[响应式数据](https://cn.vuejs.org/v2/api/#%E9%80%89%E9%A1%B9-%E6%95%B0%E6%8D%AE)),无实例 (没有 `this` 上下文)。 一个函数式组件就像这样: | Vue.component('my-component', { functional: true, // Props 可选 props: { // ... }, // 为了弥补缺少的实例 // 提供第二个参数作为上下文 render: function (createElement, context) { // ... } }) | > 注意:在 2.3.0 之前的版本中,如果一个函数式组件想要接受 props,则 `props`选项是必须的。在 2.3.0 或以上的版本中,你可以省略 `props` 选项,所有组件上的特性都会被自动解析为 props。 在 2.5.0 及以上版本中,如果你使用了[单文件组件](https://cn.vuejs.org/v2/guide/single-file-components.html),那么基于模板的函数式组件可以这样声明: | template functional> template> | 组件需要的一切都是通过上下文传递,包括: * `props`:提供所有 prop 的对象 * `children`: VNode 子节点的数组 * `slots`: 返回所有插槽的对象的函数 * `data`:传递给组件的[数据对象](https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5-data-%E5%AF%B9%E8%B1%A1),作为 `createElement` 的第二个参数传入组件 * `parent`:对父组件的引用 * `listeners`: (2.3.0+) 一个包含了所有在父组件上注册的事件侦听器的对象。这只是一个指向 `data.on` 的别名。 * `injections`: (2.3.0+) 如果使用了 [`inject`](https://cn.vuejs.org/v2/api/#provide-inject) 选项,则该对象包含了应当被注入的属性。 在添加 `functional: true` 之后,锚点标题组件的 render 函数之间简单更新增加 `context` 参数,`this.$slots.default` 更新为 `context.children`,之后`this.level` 更新为 `context.props.level`。 因为函数式组件只是一个函数,所以渲染开销也低很多。然而,对持久化实例的缺乏也意味着函数式组件不会出现在 [Vue devtools](https://github.com/vuejs/vue-devtools) 的组件树里。 在作为包装组件时它们也同样非常有用,比如,当你需要做这些时: * 程序化地在多个组件中选择一个 * 在将 children, props, data 传递给子组件之前操作它们。 下面是一个依赖传入 props 的值的 `smart-list` 组件例子,它能代表更多具体的组件: | var EmptyList = { /* ... */ } var TableList = { /* ... */ } var OrderedList = { /* ... */ } var UnorderedList = { /* ... */ } Vue.component('smart-list', { functional: true, props: { items: { type: Array, required: true }, isOrdered: Boolean }, render: function (createElement, context) { function appropriateListComponent () { var items = context.props.items if (items.length === 0) return EmptyList if (typeof items[0] === 'object') return TableList if (context.props.isOrdered) return OrderedList return UnorderedList } return createElement( appropriateListComponent(), context.data, context.children ) } }) | ### [向子元素或子组件传递特性和事件](https://cn.vuejs.org/v2/guide/render-function.html#%E5%90%91%E5%AD%90%E5%85%83%E7%B4%A0%E6%88%96%E5%AD%90%E7%BB%84%E4%BB%B6%E4%BC%A0%E9%80%92%E7%89%B9%E6%80%A7%E5%92%8C%E4%BA%8B%E4%BB%B6 "向子元素或子组件传递特性和事件") 在普通组件中,没有被定义为 prop 的特性会自动添加到组件的根元素上,将现有的同名特性替换或与其[智能合并](https://cn.vuejs.org/v2/guide/class-and-style.html)。 然而函数式组件要求你显式定义该行为: | Vue.component('my-functional-button', { functional: true, render: function (createElement, context) { // 完全透明的传入任何特性、事件监听器、子结点等。 return createElement('button', context.data, context.children) } }) | 向 `createElement` 通过传入 `context.data` 作为第二个参数,我们就把 `my-functional-button` 上面所有的特性和事件监听器都传递下去了。事实上这是非常透明的,那些事件甚至并不要求 `.native` 修饰符。 如果你使用基于模板的函数式组件,那么你还需要手动添加特性和监听器。因为我们可以访问到其独立的上下文内容,所以我们可以使用 `data.attrs` 传递任何 HTML 特性,也可以使用 `listeners` *(即 `data.on` 的别名)* 传递任何事件监听器。 | template functional> button class="btn btn-primary" v-bind="data.attrs" v-on="listeners" > slot/> button> template> | ### [`slots()` 和 `children` 对比](https://cn.vuejs.org/v2/guide/render-function.html#slots-%E5%92%8C-children-%E5%AF%B9%E6%AF%94 "slots() 和 children 对比") 你可能想知道为什么同时需要 `slots()` 和 `children`。`slots().default` 不是和 `children` 类似的吗?在一些场景中,是这样,但是如果是函数式组件和下面这样的 children 呢? | my-functional-component> p slot="foo"> first p> p>secondp> my-functional-component> | 对于这个组件,`children` 会给你两个段落标签,而 `slots().default` 只会传递第二个匿名段落标签,`slots().foo` 会传递第一个具名段落标签。同时拥有 `children`和 `slots()` ,因此你可以选择让组件通过 `slot()` 系统分发或者简单的通过 `children` 接收,让其他组件去处理。 ## [模板编译](https://cn.vuejs.org/v2/guide/render-function.html#%E6%A8%A1%E6%9D%BF%E7%BC%96%E8%AF%91 "模板编译") 你可能有兴趣知道,Vue 的模板实际是编译成了 render 函数。这是一个实现细节,通常不需要关心,但如果你想看看模板的功能是怎样被编译的,你会发现会非常有趣。下面是一个使用 `Vue.compile` 来实时编译模板字符串的简单 demo: render: ~~~ function anonymous( ) { with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])} } ~~~ staticRenderFns: ~~~ _m(0): function anonymous( ) { with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])} } ~~~ # 插件 插件通常会为 Vue 添加全局功能。插件的范围没有限制——一般有下面几种: 1. 添加全局方法或者属性,如: [vue-custom-element](https://github.com/karol-f/vue-custom-element) 2. 添加全局资源:指令/过滤器/过渡等,如 [vue-touch](https://github.com/vuejs/vue-touch) 3. 通过全局 mixin 方法添加一些组件选项,如: [vue-router](https://github.com/vuejs/vue-router) 4. 添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。 5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能,如 [vue-router](https://github.com/vuejs/vue-router) ## [使用插件](https://cn.vuejs.org/v2/guide/plugins.html#%E4%BD%BF%E7%94%A8%E6%8F%92%E4%BB%B6 "使用插件") 通过全局方法 `Vue.use()` 使用插件。它需要在你调用 `new Vue()` 启动应用之前完成: | // 调用 `MyPlugin.install(Vue)` Vue.use(MyPlugin) new Vue({ //... options }) | 也可以传入一个选项对象: | Vue.use(MyPlugin, { someOption: true }) | `Vue.use` 会自动阻止多次注册相同插件,届时只会注册一次该插件。 Vue.js 官方提供的一些插件 (例如 `vue-router`) 在检测到 `Vue` 是可访问的全局变量时会自动调用 `Vue.use()`。然而在例如 CommonJS 的模块环境中,你应该始终显式地调用 `Vue.use()`: | // 用 Browserify 或 webpack 提供的 CommonJS 模块环境时 var Vue = require('vue') var VueRouter = require('vue-router') // 不要忘了调用此方法 Vue.use(VueRouter) | [awesome-vue](https://github.com/vuejs/awesome-vue#components--libraries) 集合了来自社区贡献的数以千计的插件和库。 ## [开发插件](https://cn.vuejs.org/v2/guide/plugins.html#%E5%BC%80%E5%8F%91%E6%8F%92%E4%BB%B6 "开发插件") Vue.js 的插件应该有一个公开方法 `install`。这个方法的第一个参数是 `Vue` 构造器,第二个参数是一个可选的选项对象: | MyPlugin.install = function (Vue, options) { // 1\. 添加全局方法或属性 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.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 `v-bind` 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示: | {{ message | capitalize }} 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) }) new Vue({ // ... }) | 下面这个例子用到了 `capitalize` 过滤器: John 过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,`capitalize` 过滤器函数将会收到 `message` 的值作为第一个参数。 过滤器可以串联: | {{ message | filterA | filterB }} | 在这个例子中,`filterA` 被定义为接收单个参数的过滤器函数,表达式 `message` 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 `filterB`,将 `filterA` 的结果传递到 `filterB` 中。 过滤器是 JavaScript 函数,因此可以接收参数: | {{ message | filterA('arg1', arg2) }} | 这里,`filterA` 被定义为接收三个参数的过滤器函数。其中 `message` 的值作为第一个参数,普通字符串 `'arg1'` 作为第二个参数,表达式 `arg2` 的值作为第三个参数。 # 生产环境部署 ## [开启生产环境模式](https://cn.vuejs.org/v2/guide/deployment.html#%E5%BC%80%E5%90%AF%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E6%A8%A1%E5%BC%8F "开启生产环境模式") 开发环境下,Vue 会提供很多警告来帮你对付常见的错误与陷阱。而在生产环境下,这些警告语句却没有用,反而会增加应用的体积。此外,有些警告检查还有一些小的运行时开销,这在生产环境模式下是可以避免的。 ### [不使用构建工具](https://cn.vuejs.org/v2/guide/deployment.html#%E4%B8%8D%E4%BD%BF%E7%94%A8%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7 "不使用构建工具") 如果用 Vue 完整独立版本,即直接用 `<script>` 元素引入 Vue 而不提前进行构建,请记得在生产环境下使用压缩后的版本 (`vue.min.js`)。两种版本都可以在[安装指导](https://cn.vuejs.org/v2/guide/installation.html#%E7%9B%B4%E6%8E%A5%E7%94%A8-lt-script-gt-%E5%BC%95%E5%85%A5)中找到。 ### [使用构建工具](https://cn.vuejs.org/v2/guide/deployment.html#%E4%BD%BF%E7%94%A8%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7 "使用构建工具") 当使用 webpack 或 Browserify 类似的构建工具时,Vue 源码会根据 `process.env.NODE_ENV` 决定是否启用生产环境模式,默认情况为开发环境模式。在 webpack 与 Browserify 中都有方法来覆盖此变量,以启用 Vue 的生产环境模式,同时在构建过程中警告语句也会被压缩工具去除。这些所有 `vue-cli` 模板中都预先配置好了,但了解一下怎样配置会更好。 #### [](https://cn.vuejs.org/v2/guide/deployment.html#webpack "webpack")webpack 在 webpack 4+ 中,你可以使用 `mode` 选项: | module.exports = { mode: 'production' } | 但是在 webpack 3 及其更低版本中,你需要使用 [DefinePlugin](https://webpack.js.org/plugins/define-plugin/): | var webpack = require('webpack') module.exports = { // ... plugins: [ // ... new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production') }) ] } | #### [](https://cn.vuejs.org/v2/guide/deployment.html#Browserify "Browserify")Browserify * 在运行打包命令时将 `NODE_ENV` 设置为 `"production"`。这等于告诉 `vueify` 避免引入热重载和开发相关的代码。 * 对打包后的文件进行一次全局的 [envify](https://github.com/hughsk/envify) 转换。这使得压缩工具能清除掉 Vue 源码中所有用环境变量条件包裹起来的警告语句。例如: | NODE_ENV=production browserify -g envify -e main.js | uglifyjs -c -m > build.js | * 或者在 Gulp 中使用 [envify](https://github.com/hughsk/envify): | // 使用 envify 的自定义模块来定制环境变量 var envify = require('envify/custom') browserify(browserifyOptions) .transform(vueify) .transform( // 必填项,以处理 node_modules 里的文件 { global: true }, envify({ NODE_ENV: 'production' }) ) .bundle() | * 或者配合 Grunt 和 [grunt-browserify](https://github.com/jmreidy/grunt-browserify) 使用 [envify](https://github.com/hughsk/envify): | // 使用 envify 自定义模块指定环境变量 var envify = require('envify/custom') browserify: { dist: { options: { // 该函数用来调整 grunt-browserify 的默认指令 configure: b => b .transform('vueify') .transform( // 用来处理 `node_modules` 文件 { global: true }, envify({ NODE_ENV: 'production' }) ) .bundle() } } } | #### [](https://cn.vuejs.org/v2/guide/deployment.html#Rollup "Rollup")Rollup 使用 [rollup-plugin-replace](https://github.com/rollup/rollup-plugin-replace): | const replace = require('rollup-plugin-replace') rollup({ // ... plugins: [ replace({ 'process.env.NODE_ENV': JSON.stringify( 'production' ) }) ] }).then(...) | ## [模板预编译](https://cn.vuejs.org/v2/guide/deployment.html#%E6%A8%A1%E6%9D%BF%E9%A2%84%E7%BC%96%E8%AF%91 "模板预编译") 当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。 预编译模板最简单的方式就是使用[单文件组件](https://cn.vuejs.org/v2/guide/single-file-components.html)——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。 如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 [vue-template-loader](https://github.com/ktsn/vue-template-loader),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数。 ## [提取组件的 CSS](https://cn.vuejs.org/v2/guide/deployment.html#%E6%8F%90%E5%8F%96%E7%BB%84%E4%BB%B6%E7%9A%84-CSS "提取组件的 CSS") 当使用单文件组件时,组件内的 CSS 会以 `<style>` 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段“无样式内容闪烁 (fouc)”。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存。 查阅这个构建工具各自的文档来了解更多: * [webpack + vue-loader](https://vue-loader.vuejs.org/zh-cn/configurations/extract-css.html) (`vue-cli` 的 webpack 模板已经预先配置好) * [Browserify + vueify](https://github.com/vuejs/vueify#css-extraction) * [Rollup + rollup-plugin-vue](https://vuejs.github.io/rollup-plugin-vue/#/en/2.3/?id=custom-handler) ## [跟踪运行时错误](https://cn.vuejs.org/v2/guide/deployment.html#%E8%B7%9F%E8%B8%AA%E8%BF%90%E8%A1%8C%E6%97%B6%E9%94%99%E8%AF%AF "跟踪运行时错误") 如果在组件渲染时出现运行错误,错误将会被传递至全局 `Vue.config.errorHandler`配置函数 (如果已设置)。利用这个钩子函数来配合错误跟踪服务是个不错的主意。比如 [Sentry](https://sentry.io/),它为 Vue 提供了[官方集成](https://sentry.io/for/vue/)。 # 单文件组件 ## [介绍](https://cn.vuejs.org/v2/guide/single-file-components.html#%E4%BB%8B%E7%BB%8D "介绍") 在很多 Vue 项目中,我们使用 `Vue.component` 来定义全局组件,紧接着用 `new Vue({ el: '#container '})` 在每个页面内指定一个容器元素。 这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显: * 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复 * 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 `\` * 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏 * 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器,如 Pug (formerly Jade) 和 Babel 文件扩展名为 `.vue` 的 single-file components(单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpack 或 Browserify 等构建工具。 这是一个文件名为 `Hello.vue` 的简单实例: [![单文件组件的示例 (点击查看文本版的代码)](https://cn.vuejs.org/images/vue-component.png)](https://gist.github.com/chrisvfritz/e2b6a6110e0829d78fa4aedf7cf6b235) 现在我们获得: * [完整语法高亮](https://github.com/vuejs/awesome-vue#source-code-editing) * [CommonJS 模块](https://webpack.js.org/concepts/modules/#what-is-a-webpack-module) * [组件作用域的 CSS](https://vue-loader.vuejs.org/zh-cn/features/scoped-css.html) 正如我们说过的,我们可以使用预处理器来构建简洁和功能更丰富的组件,比如 Pug,Babel (with ES2015 modules),和 Stylus。 [![带预处理器的单文件组件的示例 (点击查看文本版的代码)](https://cn.vuejs.org/images/vue-component-with-preprocessors.png)](https://gist.github.com/chrisvfritz/1c9f2daea9bc078dcb47e9a82e5f7587) 这些特定的语言只是例子,你可以只是简单地使用 Babel,TypeScript,SCSS,PostCSS - 或者其他任何能够帮助你提高生产力的预处理器。如果搭配 `vue-loader` 使用 webpack,它也是把 CSS Modules 当作第一公民来对待的。 ### [怎么看待关注点分离?](https://cn.vuejs.org/v2/guide/single-file-components.html#%E6%80%8E%E4%B9%88%E7%9C%8B%E5%BE%85%E5%85%B3%E6%B3%A8%E7%82%B9%E5%88%86%E7%A6%BB%EF%BC%9F "怎么看待关注点分离?") 一个重要的事情值得注意,关注点分离不等于文件类型分离。在现代 UI 开发中,我们已经发现相比于把代码库分离成三个大的层次并将其相互交织起来,把它们划分为松散耦合的组件再将其组合起来更合理一些。在一个组件里,其模板、逻辑和样式是内部耦合的,并且把他们搭配在一起实际上使得组件更加内聚且更可维护。 即便你不喜欢单文件组件,你仍然可以把 JavaScript、CSS 分离成独立的文件然后做到热重载和预编译。 | template> div>This will be pre-compileddiv> template> script src="./my-component.js">script> style src="./my-component.css">style> | ## [起步](https://cn.vuejs.org/v2/guide/single-file-components.html#%E8%B5%B7%E6%AD%A5 "起步") ### [例子沙箱](https://cn.vuejs.org/v2/guide/single-file-components.html#%E4%BE%8B%E5%AD%90%E6%B2%99%E7%AE%B1 "例子沙箱") 如果你希望深入了解并开始使用单文件组件,请来 CodeSandbox [看看这个简单的 todo 应用](https://codesandbox.io/s/o29j95wx9)。 ### [针对刚接触 JavaScript 模块开发系统的用户](https://cn.vuejs.org/v2/guide/single-file-components.html#%E9%92%88%E5%AF%B9%E5%88%9A%E6%8E%A5%E8%A7%A6-JavaScript-%E6%A8%A1%E5%9D%97%E5%BC%80%E5%8F%91%E7%B3%BB%E7%BB%9F%E7%9A%84%E7%94%A8%E6%88%B7 "针对刚接触 JavaScript 模块开发系统的用户") 有了 `.vue` 组件,我们就进入了高级 JavaScript 应用领域。如果你没有准备好的话,意味着还需要学会使用一些附加的工具: * Node Package Manager (NPM):阅读 [Getting Started guide](https://docs.npmjs.com/getting-started/what-is-npm) 直到 *10: Uninstalling global packages*章节。 * Modern JavaScript with ES2015/16:阅读 Babel 的 [Learn ES2015 guide](https://babeljs.io/docs/learn-es2015/)。你不需要立刻记住每一个方法,但是你可以保留这个页面以便后期参考。 在你花一天时间了解这些资源之后,我们建议你参考 [webpack](https://github.com/vuejs-templates/webpack) 模板。只要遵循指示,你就能很快地运行一个用到 `.vue` 组件,ES2015 和热重载 (hot-reloading) 的 Vue 项目! 想学习更多 webpack 的知识,请移步[它们的官方文档](https://webpack.js.org/configuration/)以及 [webpack learning academy](https://webpack.academy/p/the-core-concepts)。在 webpack 中,每个模块被打包到 bundle 之前都由一个相应的“loader”来转换,Vue 也提供 [vue-loader](https://github.com/vuejs/vue-loader) 插件来执行 `.vue` 单文件组件 的转换。 ### [针对高级用户](https://cn.vuejs.org/v2/guide/single-file-components.html#%E9%92%88%E5%AF%B9%E9%AB%98%E7%BA%A7%E7%94%A8%E6%88%B7 "针对高级用户") 无论你更钟情 webpack 或是 Browserify,我们为简单的和更复杂的项目都提供了一些文档模板。我们建议浏览 [github.com/vuejs-templates](https://github.com/vuejs-templates),找到你需要的部分,然后参考 README 中的说明,使用 [vue-cli](https://github.com/vuejs/vue-cli) 工具生成新的项目。 模板中使用 [webpack](https://webpack.js.org/),一个模块加载器加载多个模块然后构建成最终应用。为了进一步了解 webpack,可以看 [官方介绍视频](https://www.youtube.com/watch?v=WQue1AN93YU)。如果你有基础,可以看 [在 Egghead.io 上的 webpack 进阶教程](https://egghead.io/courses/using-webpack-for-production-javascript-applications)。# 单元测试 ## [配置和工具](https://cn.vuejs.org/v2/guide/unit-testing.html#%E9%85%8D%E7%BD%AE%E5%92%8C%E5%B7%A5%E5%85%B7 "配置和工具") 任何兼容基于模块的构建系统都可以正常使用,但如果你需要一个具体的建议,可以使用 [Karma](https://karma-runner.github.io/) 进行自动化测试。它有很多社区版的插件,包括对 [Webpack](https://github.com/webpack/karma-webpack) 和 [Browserify](https://github.com/Nikku/karma-browserify) 的支持。更多详细的安装步骤,请参考各项目的安装文档,通过这些 Karma 配置的例子可以快速帮助你上手 ([Webpack](https://github.com/vuejs-templates/webpack/blob/master/template/test/unit/karma.conf.js) 配置,[Browserify](https://github.com/vuejs-templates/browserify/blob/master/template/karma.conf.js) 配置)。 ## [简单的断言](https://cn.vuejs.org/v2/guide/unit-testing.html#%E7%AE%80%E5%8D%95%E7%9A%84%E6%96%AD%E8%A8%80 "简单的断言") 你不必为了可测性在组件中做任何特殊的操作,导出原始设置就可以了: | template> span>{{ message }}span> template> script> export default { data () { return { message: 'hello!' } }, created () { this.message = 'bye!' } } script> | 然后随着 Vue 导入组件的选项,你可以使用许多常见的断言: | // 导入 Vue.js 和组件,进行测试 import Vue from 'vue' import MyComponent from 'path/to/MyComponent.vue' // 这里是一些 Jasmine 2.0 的测试,你也可以使用你喜欢的任何断言库或测试工具。 describe('MyComponent', () => { // 检查原始组件选项 it('has a created hook', () => { expect(typeof MyComponent.created).toBe('function') }) // 评估原始组件选项中的函数的结果 it('sets the correct default data', () => { expect(typeof MyComponent.data).toBe('function') const defaultData = MyComponent.data() expect(defaultData.message).toBe('hello!') }) // 检查 mount 中的组件实例 it('correctly sets the message when created', () => { const vm = new Vue(MyComponent).$mount() expect(vm.message).toBe('bye!') }) // 创建一个实例并检查渲染输出 it('renders the correct message', () => { const Constructor = Vue.extend(MyComponent) const vm = new Constructor().$mount() expect(vm.$el.textContent).toBe('bye!') }) }) | ## [编写可被测试的组件](https://cn.vuejs.org/v2/guide/unit-testing.html#%E7%BC%96%E5%86%99%E5%8F%AF%E8%A2%AB%E6%B5%8B%E8%AF%95%E7%9A%84%E7%BB%84%E4%BB%B6 "编写可被测试的组件") 很多组件的渲染输出由它的 props 决定。事实上,如果一个组件的渲染输出完全取决于它的 props,那么它会让测试变得简单,就好像断言不同参数的纯函数的返回值。看下面这个例子: | template> p>{{ msg }}p> template> script> export default { props: ['msg'] } script> | 你可以在不同的 props 中,通过 `propsData` 选项断言它的渲染输出: | import Vue from 'vue' import MyComponent from './MyComponent.vue' // 挂载元素并返回已渲染的文本的工具函数 function getRenderedText (Component, propsData) { const Constructor = Vue.extend(Component) const vm = new Constructor({ propsData: propsData }).$mount() return vm.$el.textContent } describe('MyComponent', () => { it('renders correctly with different props', () => { expect(getRenderedText(MyComponent, { msg: 'Hello' })).toBe('Hello') expect(getRenderedText(MyComponent, { msg: 'Bye' })).toBe('Bye') }) }) | ## [断言异步更新](https://cn.vuejs.org/v2/guide/unit-testing.html#%E6%96%AD%E8%A8%80%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0 "断言异步更新") 由于 Vue 进行 [异步更新 DOM](https://cn.vuejs.org/v2/guide/reactivity.html#%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E9%98%9F%E5%88%97) 的情况,一些依赖 DOM 更新结果的断言必须在 `Vue.nextTick` 回调中进行: | // 在状态更新后检查生成的 HTML it('updates the rendered message when vm.message updates', done => { const vm = new Vue(MyComponent).$mount() vm.message = 'foo' // 在状态改变后和断言 DOM 更新前等待一刻 Vue.nextTick(() => { expect(vm.$el.textContent).toBe('foo') done() }) }) | 我们计划做一个通用的测试工具集,让不同策略的渲染输出 (例如忽略子组件的基本渲染) 和断言变得更简单。 关于更深入的 Vue 单元测试的内容,请移步 [vue-test-utils](https://vue-test-utils.vuejs.org/zh/) 以及我们关于 [Vue 组件的单元测试](https://cn.vuejs.org/v2/cookbook/unit-testing-vue-components.html)的 cookbook 文章。 # TypeScript 支持 在 Vue 2.5.0 中,我们大大改进了类型声明以更好地使用默认的基于对象的 API。同时此版本也引入了一些其它变化,需要开发者作出相应的升级。阅读[博客文章](https://medium.com/the-vue-point/upcoming-typescript-changes-in-vue-2-5-e9bd7e2ecf08)了解更多详情。 ## [发布为 NPM 包的官方声明文件](https://cn.vuejs.org/v2/guide/typescript.html#%E5%8F%91%E5%B8%83%E4%B8%BA-NPM-%E5%8C%85%E7%9A%84%E5%AE%98%E6%96%B9%E5%A3%B0%E6%98%8E%E6%96%87%E4%BB%B6 "发布为 NPM 包的官方声明文件") 静态类型系统能帮助你有效防止许多潜在的运行时错误,而且随着你的应用日渐丰满会更加显著。这就是为什么 Vue 不仅仅为 Vue core 提供了针对 [TypeScript](https://www.typescriptlang.org/) 的[官方类型声明](https://github.com/vuejs/vue/tree/dev/types),还为 [Vue Router](https://github.com/vuejs/vue-router/tree/dev/types) 和 [Vuex](https://github.com/vuejs/vuex/tree/dev/types) 也提供了相应的声明文件。 而且,我们已经把它们[发布到了 NPM](https://cdn.jsdelivr.net/npm/vue/types/),最新版本的 TypeScript 也知道该如何自己从 NPM 包里解析类型声明。这意味着只要你成功地通过 NPM 安装了,就不再需要任何额外的工具辅助,即可在 Vue 中使用 TypeScript 了。 ## [推荐配置](https://cn.vuejs.org/v2/guide/typescript.html#%E6%8E%A8%E8%8D%90%E9%85%8D%E7%BD%AE "推荐配置") | // tsconfig.json { "compilerOptions": { // 与 Vue 的浏览器支持保持一致 "target": "es5", // 这可以对 `this` 上的数据属性进行更严格的推断 "strict": true, // 如果使用 webpack 2+ 或 rollup,可以利用 tree-shake: "module": "es2015", "moduleResolution": "node" } } | 注意你需要引入 `strict: true` (或者至少 `noImplicitThis: true`,这是 `strict`模式的一部分) 以利用组件方法中 `this` 的类型检查,否则它会始终被看作 `any` 类型。 参阅 [TypeScript 编译器选项文档 (英)](https://www.typescriptlang.org/docs/handbook/compiler-options.html) 了解更多。 ## [开发工具链](https://cn.vuejs.org/v2/guide/typescript.html#%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7%E9%93%BE "开发工具链") ### [工程创建](https://cn.vuejs.org/v2/guide/typescript.html#%E5%B7%A5%E7%A8%8B%E5%88%9B%E5%BB%BA "工程创建") [Vue CLI 3](https://github.com/vuejs/vue-cli) 可以使用 TypeScript 生成新工程。创建方式: | # 1\. 如果没有安装 Vue CLI 就先安装 npm install --global @vue/cli # 2\. 创建一个新工程,并选择 "Manually select features (手动选择特性)" 选项 vue create my-project-name | ### [编辑器支持](https://cn.vuejs.org/v2/guide/typescript.html#%E7%BC%96%E8%BE%91%E5%99%A8%E6%94%AF%E6%8C%81 "编辑器支持") 要使用 TypeScript 开发 Vue 应用程序,我们强烈建议您使用 [Visual Studio Code](https://code.visualstudio.com/),它为 TypeScript 提供了极好的“开箱即用”支持。如果你正在使用[单文件组件](https://cn.vuejs.org/v2/guide/single-file-components.html) (SFC), 可以安装提供 SFC 支持以及其他更多实用功能的 [Vetur 插件](https://github.com/vuejs/vetur)。 [WebStorm](https://www.jetbrains.com/webstorm/) 同样为 TypeScript 和 Vue 提供了“开箱即用”的支持。 ## [基本用法](https://cn.vuejs.org/v2/guide/typescript.html#%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95 "基本用法") 要让 TypeScript 正确推断 Vue 组件选项中的类型,您需要使用 `Vue.component` 或 `Vue.extend` 定义组件: | import Vue from 'vue' const Component = Vue.extend({ // 类型推断已启用 }) const Component = { // 这里不会有类型推断, // 因为TypeScript不能确认这是Vue组件的选项 } | ## [基于类的 Vue 组件](https://cn.vuejs.org/v2/guide/typescript.html#%E5%9F%BA%E4%BA%8E%E7%B1%BB%E7%9A%84-Vue-%E7%BB%84%E4%BB%B6 "基于类的 Vue 组件") 如果您在声明组件时更喜欢基于类的 API,则可以使用官方维护的 [vue-class-component](https://github.com/vuejs/vue-class-component) 装饰器: | import Vue from 'vue' import Component from 'vue-class-component' // @Component 修饰符注明了此类为一个 Vue 组件 @Component({ // 所有的组件选项都可以放在这里 template: 'Click!' }) export default class MyComponent extends Vue { // 初始数据可以直接声明为实例的属性 message: string = 'Hello!' // 组件方法也可以直接声明为实例的方法 onClick (): void { window.alert(this.message) } } | ## [增强类型以配合插件使用](https://cn.vuejs.org/v2/guide/typescript.html#%E5%A2%9E%E5%BC%BA%E7%B1%BB%E5%9E%8B%E4%BB%A5%E9%85%8D%E5%90%88%E6%8F%92%E4%BB%B6%E4%BD%BF%E7%94%A8 "增强类型以配合插件使用") 插件可以增加 Vue 的全局/实例属性和组件选项。在这些情况下,在 TypeScript 中制作插件需要类型声明。庆幸的是,TypeScript 有一个特性来补充现有的类型,叫做[模块补充 (module augmentation)](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation)。 例如,声明一个 `string` 类型的实例属性 `$myProperty`: | // 1\. 确保在声明补充的类型之前导入 'vue' import Vue from 'vue' // 2\. 定制一个文件,设置你想要补充的类型 // 在 types/vue.d.ts 里 Vue 有构造函数类型 declare module 'vue/types/vue' { // 3\. 声明为 Vue 补充的东西 interface Vue { $myProperty: string } } | 在你的项目中包含了上述作为声明文件的代码之后 (像 `my-property.d.ts`),你就可以在 Vue 实例上使用 `$myProperty` 了。 | var vm = new Vue() console.log(vm.$myProperty) // 将会顺利编译通过 | 你也可以声明额外的属性和组件选项: | import Vue from 'vue' declare module 'vue/types/vue' { // 可以使用 `VueConstructor` 接口 // 来声明全局属性 interface VueConstructor { $myGlobal: string } } // ComponentOptions 声明于 types/options.d.ts 之中 declare module 'vue/types/options' { interface ComponentOptionsextends Vue> { myOption?: string } } | 上述的声明允许下面的代码顺利编译通过: | // 全局属性 console.log(Vue.$myGlobal) // 额外的组件选项 var vm = new Vue({ myOption: 'Hello' }) | ## [标注返回值](https://cn.vuejs.org/v2/guide/typescript.html#%E6%A0%87%E6%B3%A8%E8%BF%94%E5%9B%9E%E5%80%BC "标注返回值") 因为 Vue 的声明文件天生就具有循环性,TypeScript 可能在推断某个方法的类型的时候存在困难。因此,你可能需要在 `render` 或 `computed` 里的方法上标注返回值。 | import Vue, { VNode } from 'vue' const Component = Vue.extend({ data () { return { msg: 'Hello' } }, methods: { // 需要标注有 `this` 参与运算的返回值类型 greet (): string { return this.msg + ' world' } }, computed: { // 需要标注 greeting(): string { return this.greet() + '!' } }, // `createElement` 是可推导的,但是 `render` 需要返回值类型 render (createElement): VNode { return createElement('div', this.greeting) } }) | 如果你发现类型推导或成员补齐不工作了,标注某个方法也许可以帮助你解决这个问题。使用 `--noImplicitAny` 选项将会帮助你找到这些未标注的方法。 # 路由 ## [官方路由](https://cn.vuejs.org/v2/guide/routing.html#%E5%AE%98%E6%96%B9%E8%B7%AF%E7%94%B1 "官方路由") 对于大多数单页面应用,都推荐使用官方支持的 [vue-router 库](https://github.com/vuejs/vue-router)。更多细节可以看 [vue-router 文档](https://router.vuejs.org/)。 ## [从零开始简单的路由](https://cn.vuejs.org/v2/guide/routing.html#%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E7%AE%80%E5%8D%95%E7%9A%84%E8%B7%AF%E7%94%B1 "从零开始简单的路由") 如果只需要非常简单的路由而不需要引入整个路由库,可以动态渲染一个页面级的组件像这样: | const NotFound = { template: 'Page not found' } const Home = { template: 'home page' } const About = { template: 'about page' } const routes = { '/': Home, '/about': About } new Vue({ el: '#app', data: { currentRoute: window.location.pathname }, computed: { ViewComponent () { return routes[this.currentRoute] || NotFound } }, render (h) { return h(this.ViewComponent) } }) | 结合 HTML5 History API,你可以建立一个非常基本但功能齐全的客户端路由器。可以直接看[实例应用](https://github.com/chrisvfritz/vue-2.0-simple-routing-example) ## [整合第三方路由](https://cn.vuejs.org/v2/guide/routing.html#%E6%95%B4%E5%90%88%E7%AC%AC%E4%B8%89%E6%96%B9%E8%B7%AF%E7%94%B1 "整合第三方路由") 如果有非常喜欢的第三方路由,如 [Page.js](https://github.com/visionmedia/page.js) 或者 [Director](https://github.com/flatiron/director),整合[很简单](https://github.com/chrisvfritz/vue-2.0-simple-routing-example/compare/master...pagejs)。这有个用了 Page.js 的[复杂示例](https://github.com/chrisvfritz/vue-2.0-simple-routing-example/tree/pagejs)。 # 状态管理 ## [类 Flux 状态管理的官方实现](https://cn.vuejs.org/v2/guide/state-management.html#%E7%B1%BB-Flux-%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86%E7%9A%84%E5%AE%98%E6%96%B9%E5%AE%9E%E7%8E%B0 "类 Flux 状态管理的官方实现") 由于状态零散地分布在许多组件和组件之间的交互中,大型应用复杂度也经常逐渐增长。为了解决这个问题,Vue 提供 [vuex](https://github.com/vuejs/vuex):我们有受到 Elm 启发的状态管理库。vuex 甚至集成到 [vue-devtools](https://github.com/vuejs/vue-devtools),无需配置即可进行[时光旅行调试](https://raw.githubusercontent.com/vuejs/vue-devtools/master/media/demo.gif)。 ### [React 的开发者请参考以下信息](https://cn.vuejs.org/v2/guide/state-management.html#React-%E7%9A%84%E5%BC%80%E5%8F%91%E8%80%85%E8%AF%B7%E5%8F%82%E8%80%83%E4%BB%A5%E4%B8%8B%E4%BF%A1%E6%81%AF "React 的开发者请参考以下信息") 如果你是来自 React 的开发者,你可能会对 Vuex 和 [Redux](https://github.com/reactjs/redux) 间的差异表示关注,Redux 是 React 生态环境中最流行的 Flux 实现。Redux 事实上无法感知视图层,所以它能够轻松的通过一些[简单绑定](https://yarnpkg.com/en/packages?q=redux%20vue&p=1)和 Vue 一起使用。Vuex 区别在于它是一个专门为 Vue 应用所设计。这使得它能够更好地和 Vue 进行整合,同时提供简洁的 API 和改善过的开发体验。 ## [简单状态管理起步使用](https://cn.vuejs.org/v2/guide/state-management.html#%E7%AE%80%E5%8D%95%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86%E8%B5%B7%E6%AD%A5%E4%BD%BF%E7%94%A8 "简单状态管理起步使用") 经常被忽略的是,Vue 应用中原始`数据`对象的实际来源 - 当访问数据对象时,一个 Vue 实例只是简单的代理访问。所以,如果你有一处需要被多个实例间共享的状态,可以简单地通过维护一份数据来实现共享: | const sourceOfTruth = {} const vmA = new Vue({ data: sourceOfTruth }) const vmB = new Vue({ data: sourceOfTruth }) | 现在当 `sourceOfTruth` 发生变化,`vmA` 和 `vmB` 都将自动的更新引用它们的视图。子组件们的每个实例也会通过 `this.$root.$data` 去访问。现在我们有了唯一的数据来源,但是,调试将会变为噩梦。任何时间,我们应用中的任何部分,在任何数据改变后,都不会留下变更过的记录。 为了解决这个问题,我们采用一个简单的 store 模式: | var store = { debug: true, state: { message: 'Hello!' }, setMessageAction (newValue) { if (this.debug) console.log('setMessageAction triggered with', newValue) this.state.message = newValue }, clearMessageAction () { if (this.debug) console.log('clearMessageAction triggered') this.state.message = '' } } | 需要注意,所有 store 中 state 的改变,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的 mutation 将会发生,以及它们是如何被触发。当错误出现时,我们现在也会有一个 log 记录 bug 之前发生了什么。 此外,每个实例/组件仍然可以拥有和管理自己的私有状态: | var vmA = new Vue({ data: { privateState: {}, sharedState: store.state } }) var vmB = new Vue({ data: { privateState: {}, sharedState: store.state } }) | ![状态管理](https://cn.vuejs.org/images/state.png) 重要的是,注意你不应该在 action 中 替换原始的状态对象 - 组件和 store 需要引用同一个共享对象,mutation 才能够被观察 接着我们继续延伸约定,组件不允许直接修改属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,我们最终达成了 [Flux](https://facebook.github.io/flux/) 架构。这样约定的好处是,我们能够记录所有 store 中发生的 state 改变,同时实现能做到记录变更 (mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具。 说了一圈其实又回到了[vuex](https://github.com/vuejs/vuex),如果你已经读到这儿,或许可以去尝试一下! # 服务端渲染 ## [SSR 完全指南](https://cn.vuejs.org/v2/guide/ssr.html#SSR-%E5%AE%8C%E5%85%A8%E6%8C%87%E5%8D%97 "SSR 完全指南") 在 2.3 发布后我们发布了一份完整的构建 Vue 服务端渲染应用的指南。这份指南非常深入,适合已经熟悉 Vue, webpack 和 Node.js 开发的开发者阅读。请移步 [ssr.vuejs.org](https://ssr.vuejs.org/zh/)。 ## [Nuxt.js](https://cn.vuejs.org/v2/guide/ssr.html#Nuxt-js "Nuxt.js") 从头搭建一个服务端渲染的应用是相当复杂的。幸运的是,我们有一个优秀的社区项目 [Nuxt.js](https://nuxtjs.org/) 让这一切变得非常简单。Nuxt 是一个基于 Vue 生态的更高层的框架,为开发服务端渲染的 Vue 应用提供了极其便利的开发体验。更酷的是,你甚至可以用它来做为静态站生成器。推荐尝试。