一、灵活的Vue
1. 高效的追踪机制
Vue通过Object.defineProperty
定义数据的存取描述符(set和get),来追踪数据变化。
在Vue组件初始化时,data下的属性会被循环遍历添加上set和get,直到所有的子孙属性都被添加完。当data的某一属性值发生了变化(执行set)时,它就会向订阅了该数据的组件发布通知,去更新组件。
在set里,组件可以订阅该数据变化的通知。
set和get叫对象的存取描述符,与存取描述符对应的叫数据描述符。二者不能共存,默认情况下,是数据描述符。如下:
Vue在初始化的时候,会删掉数据描述符,加上存取描述符。这种方式追踪数据变化,比React更高效。React用比较引用来追踪数据变化的方式。在setState的时候,React会用新的state直接替换掉旧state。
即便不改变数据的数值属性,只要数据的引用地址发生变化了,React组件也会触发更新。如果不加处理,React组件会多很多次更新。如下面,每次执行handleClick,即便没有改变数据的数值,(但引用地址发生变化了),组件也会重新渲染。这个渲染是没必要的。
为了避免组件的这种没意义的渲染,React推出了PureComponent(纯组件),帮助开发者做一层数据的浅比较,仅引用地址发生变化的,就没必要重新渲染了。开发的时候,还可以拦截shouldComponentUpdate生命周期钩子,显式地干预是否需要重新渲染组件。在得到通知数据变化时,React和Vue都采用了vDOM diff算法,各自去更新组件。在追踪数据变化上,Vue比React更加高效。
2. 令人尴尬的v-model
Vue无法检测对象属性的增删变化和数组索引长度的变化,在定义object类型的数据时,一般把它下面的属性也定义了。但是,JS是一门动态语言,可以不预先定义对象,并且任意地操作对象的属性。结合v-model使用时,object类型数据的属性也可以不用预先定义。
例如,在表单型的组件里,一个常见的行为,不给formData定义title、type等这些属性,v-model会自动响应用户输入,并添加属性到formData上。
<template>
<div>
<input v-model="formData.title" placeholder="请输入标题" />
<select v-model="formData.type">
<option value="1">类别</option>
</select>
</div>
</template>
data() {
return {
formData: {}
}
}
复制代码
props是组件间自上向下通信数据的方式,子组件逆向修改props,Vue会警告应当避免直接操作props,用data或者computed属性代替,修改值也不会被传递到父组件。
然而,借助v-model,object类型props值,也可以由子组件传递给父组件。简单类型的props值仍旧无法修改。
原来,v-model内部检测到绑定的是对象的key时(indexOf('.') > 0),会调用set方法,更新属性到对象上,并给组件添加了订阅事件。
Vue1.0采用的是双向数据绑定,Vue2.0采用的是单向数据流,即只能父对子通信,子对父通信要用回调函数的方式。 Vue3.0以前无法检测到数据属性的增删变化,常用Vue.prototype.$set和JSON.parse(JSON.stringify)去纠正这一点。未来Vue3.0将用proxy代替
Object.defineProperty
监听数据变化,proxy将直接监听对象,而不是监听属性,它可以检测到数组和对象的增删变化。
3. 作用域问题
JS开发有三大难点,原型、闭包和作用域。在es6 module的帮助下,原型和闭包的坑已经不多了,剩下的作用域成了最常遇到的坑。
在React当中,给组件添加事件必须要修正函数的执行作用域。像下面这样,onClick是定义在全局作用域上的,它的this就是undefined。因为js是静态作用域语言,它作用域是定义时确定的。在这里,要拿到React组件实例内部的state,所有bind函数的作用域到组件实例上。
在每一个事件函数上,不厌其烦地绑定函数的作用域,这在Vue里是根本不care的。Vue会在组件实例化的时候,把methods里的函数作用域都绑定到组件实例上 CSS的作用域问题,也是困扰前端开发者的大问题。命名冲突的className会带来意想不到的惊喜,采用提升权重的方式,可以提高选择器的竞争力,但这会让项目更加臃肿,CSS管理混乱不堪,维护开发非常麻烦。Vue采用类似shadow DOM的方式对CSS进行封装,添加了scoped属性后,CSS只作用在组件内部,组件之间的CSS不会互相影响。
React采用css in js、css module、style-component的方式封装CSS,都没有Vue好用。
React的css in js写法
4. Vue的灵活性
相比React,Vue和JS这门语言契合度是最高的,它没有科班化的数据流管理,没有刀耕火种的JS编写方式。
Vue的追踪数据机制、v-model双向绑定和js/css作用域,都很巧妙的利用JS作为一门动态语言的优势。这使Vue成为一门非常容易上手的技术框架,在快节奏、频繁的迭代的开发需求中占有一席之地。我在商业产品部一年多一共开发和维护了9个项目,包括1个react、1个angular和7个vue项目。Vue非常容易上手,有些简单的需求,就让后端同学代劳了。
一直以来,都有一个争议点,Vue适合做小型项目,React适合开发大型项目。以前,Vue饱受诟病的是数据流管理,实际上,现在Vue2和React已经相差不大了,借助v-model和vuex,Vue甚至比React更胜一筹。未来Vue3会用Typescript写,构建项目将会更加稳健。
二. 5种数据流管理方式
1. props + emit回调
react和vue都在用的组件通信方式之一,简约又简单。
依赖组件父子关系。
如下,子组件实例化过程中,如果发现父组件订阅了子组件的事件,就会把订阅的事件添加到events列表里,以此允许开发者来发布事件,即$emit事件。
添加订阅者到event队列。 如果是祖孙级组件和兄弟级组件,它们之间的通信就需要很多个emit回调在组件之间传递。这种通信方式会组件耦合性太强,程序稳定性降低。2. props + eventBus
适用于所有组件,不依赖组件之间的嵌套关系。
在EventBus里,实例化一个Vue实例作为一个观察者,所有的组件都作为订阅者。
// EventBus.js
import Vue from 'vue';
export default new Vue();
复制代码
// 添加订阅者
eventBus.$on('reset-preview', this.closeHandler);
// 发布通知
eventBus.$emit('preview');
复制代码
Vue内部实现了on和emit两个方法,on方法添加订阅者到队列里,emit在事件变化时,发布通知给订阅者。
EventBus虽然不依赖组件嵌套关系,但是数据流向是随意的,对于复杂的业务需求,难以支撑。所以需要一个中心化的观察者,观察数据变化,自上而下组件响应数据变化,自下而上更新数据变化到观察者中心。
emit回调和eventBus的区别是什么?
emit回调是强调组件关系,父组件是订阅者,子组件是发布者。 eventBus不关心组件关系,eventBus实例化的一个实例是发布者,组件都是订阅者。
3. vuex
优点:
- 解决所有祖孙级和父子级组件嵌套的数据流问题
- 缓存数据,减少http请求次数
- 单向数据流,数据流向更清晰
- 减少props和回调函数,组件之间解耦
使用vuex需要注意的点
- 与后台约束性强的数据,不宜写到vuex里。
vuex和vue一样都是通过劫持setter/getter追踪数据变化的,所以vuex也不能检测到array和object的增删。
例如表单型的数据,需要增加字段,向后台提交。vuex无法处理属性增删,$store.commit到store里,store里的数据不会更新。
- 在数据生命周期结束时,清空store里缓存的数据。
例如,一个复杂类型的数据,从服务端请求出来存到vuex里,然后在多个组件之间传递,在数据处理结束之后,又提交给服务端。此时,应该清除vuex保存的数据,以便在下一次打开页面时,vuex里的数据是干净的。
一般,在组件的created生命周期检查vuex缓存是否存在,没有缓存,则重新拉数据。在组件生命周期结束前,重置store,清除数据缓存。
什么情况下适合用vuex呢?
- 在需要缓存数据的时候
- emit实在解决不了时候
只有在用React实在解决不了的时候,才用Redux。--------Redux作者
过度使用vuex,将使项目变得臃肿,组件之间耦合度增加。
4. route
传统型,适用于各页面之间的数据传递。对于一些需要粘贴url让其他人访问的需求,需要在router里加上必须参数,而不能在vuex里。
比如筛选列表页,媒体流量桶管理页。这种页面结构相同,种类又众多。不能拆分成多个页面,但又需要独立的页面展示效果,适合把数据保存在route里。
使用router传参时,需要注意:
- 组件生命周期钩子(确保必需的route参数) 1.1 对于依赖router保存数据的页面,注意在vue组件生命周期钩子里加校验 1.2 父子组件的渲染顺序是,父created ---> 子created ---> 子mounted ---> 父mounted,所以在created生命周期里校验route参数,确保组件render以后有正确的数据显示。
5. 组件实例方法调用
用于嵌套的表单型组件
比如创建父任务时,父任务表单里同时又可以添加子任务,子认为有分别可以添加不同的任务奖励规则。 比如创建订单时,可以添加广告组、广告计划、广告创意,广告创意里可以添加各类素材。 这些嵌套复杂的表单型组件,比较适合用这个。
三. 组件拆分原则
1. 善用slot,开闭规则。
修改封闭,扩展开放。
2. 功能拆分,单一职责规则
功能组件还是ui组件?简化使用。 比如pagnition处理total < 1时,不显示分页器 比如筛选列表组件,保留UI部分,把提交按钮用slot写进去 功能组件,比如upload组件
3. 最少知道原则
尽可能减少外部依赖 组件内的功能点,对外界的依赖越少越好。 越简单越好。 参考设计的最高境界,Kiss规则。
四. 一些注意事项
1. 利用$next获取组件渲染之后的dom
2. v-for的key
3. 覆写复杂类型的props
4. ajax的封装
创建axios的实例对ajax的封装,减少处理回调的代码量。直接修改axios的拦截器会污染整个工程ajax调用规则。对于多次import进来的文件,webpack只会打包一次。