vue.js 通过简洁的 API 提供高效的数据绑定和灵活的组件系统. 在前端纷繁复杂的生态中, vue.JS 有幸受到一定程度的关注, 目前在 GitHub 上已经有 5000 + 的 star. 本文将从各方面对 Vue.JS 做一个深入的介绍.
Vue.JS 是我在 2014 年 2 月开源的一个前端开发库, 通过简洁的 API 提供高效的数据绑定和灵活的组件系统. 在前端纷繁复杂的生态中, Vue.JS 有幸受到一定程度的关注, 目前在 GitHub 上已经有 5000 + 的 star. 本文将从各方面对 Vue.JS 做一个深入的介绍.
开发初衷
2013 年末, 我还在 Google Creative Lab 工作. 当时在项目中使用了一段时间的 Angular, 在感叹数据绑定带来生产力提升的同时, 我也感到 Angular 的 API 设计过于繁琐, 使得学习曲线颇为陡峭. 出于对 Angular 数据绑定原理的好奇, 我开始 "造轮子", 自己实现了一个非常粗糙的, 基于依赖收集的数据绑定库. 这就是 Vue.JS 的前身. 同时在实际开发中, 我发现用户界面完全可以用嵌套的组件树来描述, 而一个组件恰恰可以对应 MVVM 中的 ViewModel. 于是我决定将我的数据绑定实验改进成一个真正的开源项目, 其核心思想便是 "数据驱动的组件系统".
MVVM 数据绑定
MVVM 的本质是通过数据绑定链接 View 和 Model, 让数据的变化自动映射为视图的更新. Vue.JS 在数据绑定的 API 设计上借鉴了 Angular 的指令机制: 用户可以通过具有特殊前缀的 html 属性来实现数据绑定, 也可以使用常见的花括号模板插值, 或是在表单元素上使用双向绑定:
- <!-- 指令 -->
- <span v-text="msg">
- </span>
- <!-- 插值 -->
- <span>
- {{msg}}
- </span>
- <!-- 双向绑定 -->
- <input v-model="msg">
插值本质上也是指令, 只是为了方便模板的书写. 在模板的编译过程中, Vue.JS 会为每一处需要动态更新的 DOM 节点创建一个指令对象. 每当一个指令对象观测的数据变化时, 它便会对所绑定的目标节点执行相应的 DOM 操作. 基于指令的数据绑定使得具体的 DOM 操作都被合理地封装在指令定义中, 业务代码只需要涉及模板和对数据状态的操作即可, 这使得应用的开发效率和可维护性都大大提升.
与 Angular 不同的是, Vue.JS 的 API 里并没有繁杂的 module,controller,scope,factory,service 等概念, 一切都是以 "ViewModel 实例" 为基本单位:
- <!-- 模板 -->
- <div id="app">
- {{msg}}
- </div>
- // 原生对象即数据
- var data = {
- msg: 'hello!'
- }
- // 创建一个 ViewModel 实例
- var vm = new Vue({
- // 选择目标元素
- el: '#app',
- // 提供初始数据
- data: data
- })
渲染结果:
- <div id="app">
- hello!
- </div>
在渲染的同时, Vue.JS 也已经完成了数据的动态绑定: 此时如果改动 data.msg 的值, DOM 将自动更新. 是不是非常简单易懂呢? 除此之外, Vue.JS 对自定义指令, 过滤器的 API 也做了大幅的简化, 如果你有 Angular 的开发经验, 上手会非常迅速.
数据观测的实现
Vue.JS 的数据观测实现原理和 Angular 有着本质的不同. 了解 Angular 的读者可能知道, Angular 的数据观测采用的是脏检查 (dirty checking) 机制. 每一个指令都会有一个对应的用来观测数据的对象, 叫做 watcher; 一个作用域中会有很多个 watcher. 每当界面需要更新时, Angular 会遍历当前作用域里的所有 watcher, 对它们一一求值, 然后和之前保存的旧值进行比较. 如果求值的结果变化了, 就触发对应的更新, 这个过程叫做 digest cycle. 脏检查有两个问题:
1. 任何数据变动都意味着当前作用域的每一个 watcher 需要被重新求值, 因此当 watcher 的数量庞大时, 应用的性能就不可避免地受到影响, 并且很难优化.
2. 当数据变动时, 框架并不能主动侦测到变化的发生, 需要手动触发 digest cycle 才能触发相应的 DOM 更新. Angular 通过在 DOM 事件处理函数中自动触发 digest cycle 部分规避了这个问题, 但还是有很多情况需要用户手动进行触发.
Vue.JS 采用的则是基于依赖收集的观测机制. 从原理上来说, 和老牌 MVVM 框架 Knockout 是一样的. 依赖收集的基本原理是:
1. 将原生的数据改造成 "可观察对象". 一个可观察对象可以被取值, 也可以被赋值.
2. 在 watcher 的求值过程中, 每一个被取值的可观察对象都会将当前的 watcher 注册为自己的一个订阅者, 并成为当前 watcher 的一个依赖.
3. 当一个被依赖的可观察对象被赋值时, 它会通知所有订阅自己的 watcher 重新求值, 并触发相应的更新.
4. 依赖收集的优点在于可以精确, 主动地追踪数据的变化, 不存在上述提到的脏检查的两个问题. 但传统的依赖收集实现, 比如 Knockout, 通常需要包裹原生数据来制造可观察对象, 在取值和赋值时需要采用函数调用的形式, 在进行数据操作时写法繁琐, 不够直观; 同时, 对复杂嵌套结构的对象支持也不理想.
Vue.JS 利用了 ES5 的 Object.defineProperty 方法, 直接将原生数据对象的属性改造为 getter 和 setter, 在这两个函数内部实现依赖的收集和触发, 而且完美支持嵌套的对象结构. 对于数组, 则通过包裹数组的可变方法 (比如 push) 来监听数组的变化. 这使得操作 Vue.JS 的数据和操作原生对象几乎没有差别 [注: 在添加 / 删除属性, 或是修改数组特定位置元素时, 需要调用特定的函数, 如 obj.$add(key, value) 才能触发更新. 这是受 ES5 的语言特性所限.], 数据操作的逻辑更为清晰流畅, 和第三方数据同步方案的整合也更为方便.
组件系统
在大型的应用中, 为了分工, 复用和可维护性, 我们不可避免地需要将应用抽象为多个相对独立的模块. 在较为传统的开发模式中, 我们只有在考虑复用时才会将某一部分做成组件; 但实际上, 应用类 UI 完全可以看作是全部由组件树构成的:
因此, 在 Vue.JS 的设计中将组件作为一个核心概念. 可以说, 每一个 Vue.JS 应用都是围绕着组件来开发的.
注册一个 Vue.JS 组件十分简单:
- Vue.component('my-component', {
- // 模板
- template: '<div>{{msg}} {{privateMsg}}</div>',
- // 接受参数
- props: {
- msg: String<br>
- },
- // 私有数据, 需要在函数中返回以避免多个实例共享一个对象
- data: function () {
- return {
- privateMsg: 'component!'
- }
- }
- })
注册之后即可在父组件模板中以自定义元素的形式调用一个子组件:
<my-component msg="hello"></my-component>
渲染结果:
<div>hello component!</div>
Vue.JS 的组件可以理解为预先定义好了行为的 ViewModel 类. 一个组件可以预定义很多选项, 但最核心的是以下几个:
• 模板(template): 模板声明了数据和最终展现给用户的 DOM 之间的映射关系.
• 初始数据(data): 一个组件的初始数据状态. 对于可复用的组件来说, 这通常是私有的状态.
• 接受的外部参数(props): 组件之间通过参数来进行数据的传递和共享. 参数默认是单向绑定(由上至下), 但也可以显式地声明为双向绑定.
• 方法(methods): 对数据的改动操作一般都在组件的方法内进行. 可以通过 v-on 指令将用户输入事件和组件方法进行绑定.
• 生命周期钩子函数(lifecycle hooks): 一个组件会触发多个生命周期钩子函数, 比如 created,attached,destroyed 等等. 在这些钩子函数中, 我们可以封装一些自定义的逻辑. 和传统的 MVC 相比, 可以理解为 Controller 的逻辑被分散到了这些钩子函数中.
• 私有资源(assets):Vue.JS 当中将用户自定义的指令, 过滤器, 组件等统称为资源. 由于全局注册资源容易导致命名冲突, 一个组件可以声明自己的私有资源. 私有资源只有该组件和它的子组件可以调用.
除此之外, 同一颗组件树之内的组件之间还可以通过内建的事件 API 来进行通信. Vue.JS 提供了完善的定义, 复用和嵌套组件的 API, 让开发者可以像搭积木一样用组件拼出整个应用的界面. 这个思路的可行性在 Facebook 开源的 React 当中也得到了印证.
基于构建工具的单文件组件格式
Vue.JS 的核心库只提供基本的 API, 本身在如何组织应用的文件结构上并不做太多约束. 但在构建大型应用时, 推荐使用 webpack+vue-loader 这个组合以使针对组件的开发更高效.
Webpack 是由 Tobias Koppers 开发的一个开源前端模块构建工具. 它的基本功能是将以模块格式书写的多个 JavaScript 文件打包成一个文件, 同时支持 CommonJS 和 AMD 格式. 但让它与众不同的是, 它提供了强大的 loader API 来定义对不同文件格式的预处理逻辑, 从而让我们可以将 CSS, 模板, 甚至是自定义的文件格式当做 JavaScript 模块来使用. Webpack 基于 loader 还可以实现大量高级功能, 比如自动分块打包并按需加载, 对图片资源引用的自动定位, 根据图片大小决定是否用 base64 内联, 开发时的模块热替换等等, 可以说是目前前端构建领域最有竞争力的解决方案之一.
我在 Webpack 的 loader API 基础上开发了 vue-loader 插件, 从而让我们可以用这样的单文件格式 (*.vue) 来书写 Vue 组件:
- <style>
- .my-component h2 {
- color: red;
- }
- </style>
- <template>
- <div class="my-component">
- <h2>Hello from {{msg}}</h2>
- <other-component></other-component>
- </div>
- </template>
- <script>
- // 遵循 CommonJS 模块格式
- var otherComponent = require('./other-component')
- // 导出组件定义
- module.exports = {
- data: function () {
- return {
- msg: 'vue-loader'
- }
- },
- components: {
- 'other-component': otherComponent
- }
- }
- </script>
同时, 还可以在 *.vue 文件中使用其他预处理器, 只需要安装对应的 Webpack loader 即可:
- <style lang="stylus">
- .my-component h2
- color red
- </style>
- <template lang="jade">
- div.my-component
- h2 Hello from {{msg}}
- </template>
- <script lang="babel">
- // 利用 Babel 编译 ES2015
- export default {
- data () {
- return {
- msg: 'Hello from Babel!'
- }
- }
- }
- </script>
这样的组件格式, 把一个组件的模板, 样式, 逻辑三要素整合在同一个文件中, 即方便开发, 也方便复用和维护. 另外, Vue.JS 本身支持对组件的异步加载, 配合 Webpack 的分块打包功能, 可以极其轻松地实现组件的异步按需加载.
其他特性
Vue.JS 还有几个值得一提的特性:
1. 异步批量 DOM 更新: 当大量数据变动时, 所有受到影响的 watcher 会被推送到一个队列中, 并且每个 watcher 只会推进队列一次. 这个队列会在进程的下一个 "tick" 异步执行. 这个机制可以避免同一个数据多次变动产生的多余 DOM 操作, 也可以保证所有的 DOM 写操作在一起执行, 避免 DOM 读写切换可能导致的 layout.
2. 动画系统: Vue.JS 提供了简单却强大的动画系统, 当一个元素的可见性变化时, 用户不仅可以很简单地定义对应的 CSS Transition 或 Animation 效果, 还可以利用丰富的 JavaScript 钩子函数进行更底层的动画处理.
3. 可扩展性: 除了自定义指令, 过滤器和组件, Vue.JS 还提供了灵活的 mixin 机制, 让用户可以在多个组件中复用共同的特性.
与 Web Components 的异同
对 Web Components 有了解的读者看到这里可能会产生疑问: Vue.JS 的组件和 Web Components 的区别在哪里呢? 这里简要地做一下分析.
Web Components 是一套底层规范, 本身并不带有数据绑定, 动画系统等上层功能, 因此更合适的比较对象可能是 Polymer.Polymer 在 API 和功能上和 Vue.JS 比较相似, 但它对 Web Components 的硬性依赖使得它在浏览器支持方面有一定的问题 -- 在不支持 Web Components 规范的浏览器中, 需要加载庞大的 polyfill, 不仅在性能上会有影响, 并且有些功能, 比如 ShadowDOM,polyfill 并没有办法完美支持. 同时, Web Components 规范本身尚未定稿, 一些具体设计上仍存在不小的分歧. 相比之下, Vue.JS 在支持的浏览器中 (IE9+) 没有任何依赖.
除此之外, 在支持 Web Components 的环境中, 我们也可以很简单地利用 Web Components 底层 API 将一个 Vue.JS 组件封装在一个真正的自定义元素中, 从而实现 Vue.JS 组件和其他框架的无缝整合.
总结
在发布之初, Vue.JS 原本是着眼于轻量的嵌入式使用场景. 在今天, Vue.JS 也依然适用于这样的场景. 由于其轻量(22kb min+gzip), 高性能的特点, 对于移动场景也有很好的契合度. 更重要的是, 设计完备的组件系统和配套的构建工具, 插件, 使得 Vue.JS 在保留了其简洁 API 的同时, 也已经完全有能力担当起复杂的大型应用的开发.
为了学习工作与休闲娱乐互不冲突, 现新建圈[码农茶水铺] 用于程序员生活, 爱好, 交友, 求职招聘, 吐槽等话题交流, 希望各位大神工作之余到茶水铺来喝茶聊天. 群号: 582735936
来源: http://www.qdfuns.com/article/51116/5a9f57dbcf69a6522c1d94c06b441c60.html