Vue与React都鼓励组件化应用,即将应用分拆成一个个功能明确的模块,每个模块之间可以通过合适的方式互相联系,但各自的实现略有不同。以下谈谈我的理解,如有不对,欢迎指正
在Vue组件中,有几个观念和React相差比较大,我觉得主要有以下这几点:
Vue组件分为全局注册和局部注册,在react中都是通过import相应组件,然后模版中引用
props是可以动态变化的,子组件也实时更新,在react中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过props来更改视图
子组件一般要显示地调用props选项来声明它期待获得的数据。而在react中不必需,另两者都有props校验机制
每个Vue实例都实现了事件接口,方便父子组件通信,小型项目中不需要引入状态管理机制,而react必需自己实现
使用插槽分发内容,使得可以混合父组件的内容与子组件自己的模板
多了指令系统,让模版可以实现更丰富的功能,而React只能使用JSX语法
Vue增加的语法糖computed和watch,而在React中需要自己写一套逻辑来实现
0x01 组件的注册
1.Vue
Vue组件注册可分为全局注册和局部注册
全局注册
需在初始化根实例之前注册组件
//html
<div id="example">
<my-component></my-component>
</div>
// 注册
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
// 创建根实例
new Vue({
el: '#example'
})
- 局部注册
通过Vue 实例/组件的实例选项 components 注册仅在其作用域中可用的组件。 这种封装也适用于其它可注册的 Vue 功能,比如指令。
var Child = {
template: '<div>A custom component!</div>'
}
new Vue({
// ...
components: {
// <my-component> 将只在父组件模板中可用
'my-component': Child
}
})
2. React
React组件没有全局注册和局部注册的概念,官方推荐以ES6的class来创建组件,调用通过import导入组件实例
import React from "react";
class Demo extends React.Component {
constructor(props) {
super(props);
// 设置 initial state
this.state = {
text: props.initialValue || 'placeholder'
};
// ES6 类中函数必须手动绑定,也可在调用的时候绑定,或者通过箭头函数
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({
text: event.target.value
});
}
render() {
return (
<div>
Type something:
<input onChange={this.handleChange}
value={this.state.text} />
</div>
);
}
}
export default Demo;
0x02 Props
## 1.VueVue中的props更灵活,对于class和Style特性,采用合并的策略,并且需要在子组件中显示声明props,相同的地方是都有props验证,单项prop数据流。
- 显示声明props
子组件要显式地用 props 选项声明它预期的数据,对于 非 prop 特性,可以直接传入组件,而不需要定义相应的 prop。
Vue.component('child', {
// 在 JavaScript 中使用 camelCase
props: ['myMessage'],
template: '<span>{{ myMessage }}</span>'
})
//父组件,
<!-- 在 HTML 中使用 kebab-case || React使用JSX语法,则不存在此问题-->
<child my-message="hello!"></child>
- 动态Prop
通过v-bind 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
2.React
Reac的props更多的相对state而言,只有props无state的组件叫无状态组件,即在组件的定义中可以只有一个render方法,无生命周期概念,组件不用实例化。
- 对应于Vue的动态prop,React的实现要复杂些
父组件更新子组件的props,在子组件接收到新的props时, 通过在componentWillReceiveProps()生命周期中执行this.setState()来更新视图,但不会引起第二次渲染。
componentWillReceiveProps(nextProps) {
if(this.props.text !== nextProps.text) {
this.setState({
text: nextProps.text
});
}
}
0x03 自定义事件
每个 Vue 实例都实现了事件接口,而在React中需借助第三方插件,比如fbemitter
- Vue中父子组件通信
使用 v-on绑定自定义事件,在子组件通过this.$emit(eventName) 触发事件
// 父组件模版
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
//子组件代码
Vue.component('button-counter', {
template: '<button v-on:click="incrementCounter">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
incrementCounter: function () {
this.counter += 1
this.$emit('increment')
}
},
})
//父组件实例
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
})
- React父子组件通信
React实例没有事件接口,一般会通过引入一个第三方插件来实现,但是父子组件的通信可以通过props来实现,在父组件中传递callback的prop形式,然后在子组件中触发此回调
//子组件
class Child extends Component {
handle (e) {
//回调函数传递参数给父组件
this.props.onChange(e.target.value);
}
render(){
return(
<input type="text" onChange={this.handle.bind(this)}>
)
}
}
//父组件
class Parent extends Component{
constructor(props){
super(props);
this.handleChildChange=this.handleChildChange.bind(this);
}
handleChildChange(value){
if(value){
this.setState({value:value});
}
}
render() {
return (
< Child onChange={this.handleChildChange} ></Child>
)
}
}
0x04 插槽分发内容
在React不存在插槽分发的概念,如果之前学过Angular,那就比较熟悉了,因React不存在slot元素,所以此节只讲述Vue的相关API。
单个插槽
除非子组件模板包含至少一个 插口,否则父组件的内容将会被丢弃。当子组件模板只有一个没有属性的插槽时,父组件传入的整个内容片段将插入到插槽所在的 DOM 位置,并替换掉插槽标签本身。
具名插槽
元素可以用一个特殊的特性 name 来进一步配置如何分发内容。多个插槽可以有不同的名字。具名插槽将匹配内容片段中有对应 slot 特性的元素。 同时也可以有一个默认插槽。
//app-layout 组件
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
//父组件模板
<app-layout>
<h1 slot="header">这里可能是一个页面标题</h1>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
<p slot="footer">这里有一些联系信息</p>
</app-layout>
//渲染结果
<div class="container">
<header>
<h1>这里可能是一个页面标题</h1>
</header>
<main>
<p>主要内容的一个段落。</p>
<p>另一个主要段落。</p>
</main>
<footer>
<p>这里有一些联系信息</p>
</footer>
</div>
- 作用域插槽
作用域插槽是一种特殊类型的插槽,用作一个 (能被传递数据的) 可重用模板,来代替已经渲染好的元素。
//子组件
<div class="child">
<slot text="hello from child"></slot>
</div>
//父组件
<div class="parent">
<child>
<template slot-scope="props">
<span>hello from parent</span>
<span>{{ props.text }}</span>
</template>
</child>
</div>
//渲染结果
<div class="parent">
<div class="child">
<span>hello from parent</span>
<span>hello from child</span>
</div>
</div>
0x05 指令
Vue有丰富的指令,但也有副作用即属性#kebabCase#得转成#kebab-case#写法,而React使用的jsx,本质还是在js上下文,所以不需要转换,对于JSX语法参考此文章
Vue指
是带有 v- 前缀的特殊属性。指令属性的值预期是**单个 JavaScript 表达式** (v-for 是例外情况)。指令的职责是,当表达式的值改变时,将其产生的连带影响,**响应式地作用于 DOM**。- v-model
在表单控件或者组件上创建双向绑定
<input v-model="something">
//等于以下的语法糖
<input
v-bind:value="something"
v-on:input="something = $event.target.value">
所以要让组件的 v-model 生效,需要在组件中声明如下:
1. 接受一个 value prop
2. 在有新的值时触发 input 事件并将新值作为参数(this.$emit(‘input’, value))
- v-for
尽可能在使用 v-for 时提供 key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
<div v-for="item in items" :key="item.id">
<!-- 内容 -->
</div>
- v-bind
动态地绑定一个或多个特性,或一个组件 prop 到表达式。简写” : “
//带破折号的key值需添加双引号,不然报错,故所有的对象key值都可以写成带引号
:class="{ 'market-no-tag': marketNoTag }"
v-show VS v-if
- v-if 是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。 同时惰性渲染,只有值变更才会开始渲染。
- v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
- 一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
其他指令参考Vue文档
0x06 Vue的computed和watch
不应该使用箭头函数来定义computed和watch
- 对于任何复杂逻辑,都应当使用计算属性,尽量不要在模版中进行js运算
<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('')
}
}
})
- 计算属性缓存 vs 方法
计算属性是 基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。而方法在重新渲染时将 总会执行。
<p>Reversed message: "{{ reversedMessage() }}"</p>
methods: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
- 计算属性也有set方法
computed: {
fullName: {
// getter
get: function () {
...
},
// setter
set: function (newValue) {
...
}
}
}
- watch
Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
var vm = new Vue({
data: {
a: 1,
},
watch: {
a: function (val, oldVal) {
console.log('new: %s, old: %s', val, oldVal)
},
}
})
0x07结束语
Vue提供了更多的语法糖来让开发更便利,比如props的动态实时更新、双向的数据绑定、指令系统,实例的事件接口等。而React的中心思想即状态驱动视图的更改,所有UI层的变更都尽量通过setState来触发, 通信方式通过UIAction的行为来实现清晰的单向数据流。