1. 前言
大家好,我是若川。为了能帮助到更多对源码感兴趣、想学会看源码、提升自己前端技术能力的同学。我倾力持续组织了一年每周大家一起学习200行左右的源码共读活动,感兴趣的可以了解参与。
想学源码,极力推荐关注我写的专栏(目前是掘金专栏关注人数第一,3.6K+人)《学习源码整体架构系列》 包含jQuery
、underscore
、lodash
、vuex
、sentry
、axios
、redux
、koa
、vue-devtools
、vuex4
、koa-compose
、vue 3.2 发布
、vue-this
、create-vue
、玩具vite
等 20 余篇源码文章。
2. stepper 步进器
感兴趣的小伙伴,可以克隆我的仓库调试学习 git clone https://github.com/lxchuan12/vant-weapp-analysis.git。
我们开发微信小程序时经常会使用到 stepper
步进器组件。本文就来分析 vant-weapp
stepper
步进器源码实现。
相比于原生 JS
等源码。我们或许更应该学习,正在使用的组件库的源码,因为有助于帮助我们写业务和写自己的组件。
stepper 步进器文档
stepper 图
stepper 组件
如何开发一个微信小程序组件,可以参考官方文档。微信小程序自定义组件 文档
看完本文,你将学到:
1. 学会如何通过调试看源码
2. 如何写一个微信小程序的组件
3. 学会开发一个 stepper 步进器组件
复制代码
3. 克隆项目 && 调试
git clone https://github.com/vant-ui/vant-weapp.git
# 也可以克隆我的仓库
git clone https://github.com/lxchuan12/vant-weapp-analysis.git
cd vant-weapp-analysis/vant-weapp
yarn install
yarn run dev
复制代码
由于 yarn run dev
没有压缩代码,本文就基于运行 dev
后没有压缩的代码进行讲述。
打开微信开发者工具,把 vant-weapp/example
目录添加进去就可以预览示例了。注意:如果没有自己的 appid
,可以选择测试号。
添加编译模式,启动页面为 pages/stepper/index
,即可单独调试该页面。
关于更多 JS
调试,之前文章写过,新手向:前端程序员必学基本技能——调试JS代码,这里就不赘述。
前端容易忽略的 debugger 调试技巧
附上一张图。
debugger
我们找到对应的文件,查看下组件源码的整体结构。
4. 整体结构
4.1 组件的 wxml 结构
wxml
部分相对简单清晰,这里就不过多讲述。
<!-- vant-weapp/example/dist/stepper/index.wxml -->
<wxs src="../wxs/utils.wxs" module="utils" />
<wxs src="./index.wxs" module="computed" />
<view class="{{ utils.bem('stepper', [theme]) }} custom-class">
<view
wx:if="{{ showMinus }}"
data-type="minus"
style="{{ computed.buttonStyle({ buttonSize }) }}"
class="minus-class {{ utils.bem('stepper__minus', { disabled: disabled || disableMinus || currentValue <= min }) }}"
hover-class="van-stepper__minus--hover"
hover-stay-time="70"
bind:tap="onTap"
bind:touchstart="onTouchStart"
bind:touchend="onTouchEnd"
>
<slot name="minus" />
</view>
<input
always-embed="{{ false }}"
type="{{ integer ? 'number' : 'digit' }}"
class="input-class {{ utils.bem('stepper__input', { disabled: disabled || disableInput }) }}"
style="{{ computed.inputStyle({ buttonSize, inputWidth }) }}"
value="{{ currentValue }}"
focus="{{ focus }}"
disabled="{{ disabled || disableInput }}"
always-embed="{{ alwaysEmbed }}"
bindinput="onInput"
bind:focus="onFocus"
bind:blur="onBlur"
/>
<view
wx:if="{{ showPlus }}"
data-type="plus"
style="{{ computed.buttonStyle({ buttonSize }) }}"
class="plus-class {{ utils.bem('stepper__plus', { disabled: disabled || disablePlus || currentValue >= max }) }}"
hover-class="van-stepper__plus--hover"
hover-stay-time="70"
bind:tap="onTap"
bind:touchstart="onTouchStart"
bind:touchend="onTouchEnd"
>
<slot name="plus" />
</view>
</view>
复制代码
4.2 组件的 JS 结构
// vant-weapp/example/dist/stepper/index.js
import { VantComponent } from '../common/component';
// 不等于 undefined 也不等于 null
// export function isDef(value) {
// return value !== undefined && value !== null;
// }
import { isDef } from '../common/validator';
// 长按开始时间
const LONG_PRESS_START_TIME = 600;
// 长按定时器
const LONG_PRESS_INTERVAL = 200;
// add num and avoid float number
// 为了解决类似 0.1 + 0.2 !== 0.3 的问题
// 0.1 + 0.2 === 0.30000000000000004
function add(num1, num2) {
const cardinal = Math.pow(10, 10);
return Math.round((num1 + num2) * cardinal) / cardinal;
}
// 判断两个字符串相等
function equal(value1, value2) {
return String(value1) === String(value2);
}
debugger;
VantComponent({
field: true,
classes: ['input-class', 'plus-class', 'minus-class'],
// 代码省略 props、created、methods 函数中若干内容
props: {
value: {
type: null,
observer: 'observeValue',
},
},
data: {
currentValue: '',
},
created() {
this.setData({
currentValue: this.format(this.data.value),
});
},
methods: {
},
});
复制代码
4.3 VantComponent 组件
我们可以在 vant-weapp/example/dist/stepper/index.js
文件的 VantComponent({})
上方加上 debugger;
调试源码。按进入函数按钮。
// vant-weapp/example/dist/stepper/index.js
debugger;
VantComponent({})
复制代码
调试如图所示:
debugger
// vant-weapp/example/dist/common/component.js
import { basic } from '../mixins/basic';
function mapKeys(source, target, map) {
Object.keys(map).forEach((key) => {
if (source[key]) {
target[map[key]] = source[key];
}
});
}
function VantComponent(vantOptions) {
const options = {};
// Vue的写法转换成小程序写法
mapKeys(vantOptions, options, {
data: 'data',
props: 'properties',
mixins: 'behaviors',
methods: 'methods',
beforeCreate: 'created',
created: 'attached',
mounted: 'ready',
destroyed: 'detached',
classes: 'externalClasses',
});
// add default externalClasses
// 外部样式类,也就是为什么我们可以定义 custom-class 修改样式
options.externalClasses = options.externalClasses || [];
options.externalClasses.push('custom-class');
// add default behaviors
// behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins” 或 “traits”。
// 每个 behavior 可以包含一组属性、数据、生命周期函数和方法。
// 组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。
options.behaviors = options.behaviors || [];
options.behaviors.push(basic);
// add relations
// 添加关系 类似 ul li 组件 List => ListItem 组件
const { relation } = vantOptions;
if (relation) {
options.relations = relation.relations;
options.behaviors.push(relation.mixin);
}
// 添加内置的 behavior 参考链接
// https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/behaviors.html#wx-form-field
// https://developers.weixin.qq.com/miniprogram/dev/component/form.html
// map field to form-field behavior
if (vantOptions.field) {
options.behaviors.push('wx://form-field');
}
// add default options
// 参考:https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html
options.options = {
// 在组件定义时的选项中启用多 slot 支持
multipleSlots: true,
addGlobalClass: true,
};
Component(options);
}
export { VantComponent };
复制代码
4.3.1 Behavior 如同 mixins
// vant-weapp/example/dist/mixins/basic.js
export const basic = Behavior({
methods: {
$emit(name, detail, options) {
this.triggerEvent(name, detail, options);
},
set(data) {
this.setData(data);
return new Promise((resolve) => wx.nextTick(resolve));
},
},
});
复制代码
看完了 VantComponent
函数。再来看看组件的初始化。
5. 组件初始化
VantComponent({
field: true,
classes: ['input-class', 'plus-class', 'minus-class'],
// 代码省略 props、data、created、methods 函数中内容
props: {
value: {
type: null,
observer: 'observeValue',
},
integer: {
type: Boolean,
observer: 'check',
},
decimalLength: {
type: Number,
value: null,
observer: 'check',
},
},
data: {
currentValue: '',
},
created() {
this.setData({
currentValue: this.format(this.data.value),
});
},
methods: {
// 观测 value,如果 value、currentValue 两者不相等,以格式化后的 value 值赋值给 currentValue
observeValue() {
const { value, currentValue } = this.data;
if (!equal(value, currentValue)) {
this.setData({ currentValue: this.format(value) });
}
},
// 观测格式化后的 currentValue,如果两者不相等,以 value 值赋值给 currentValue
check() {
const val = this.format(this.data.currentValue);
if (!equal(val, this.data.currentValue)) {
this.setData({ currentValue: val });
}
},
// filter illegal characters
// 格式化 value
filter(value) {
value = String(value).replace(/[^0-9.-]/g, '');
if (this.data.integer && value.indexOf('.') !== -1) {
value = value.split('.')[0];
}
return value;
},
// 限制 value 的范围
// limit value range
format(value) {
value = this.filter(value);
// 处理范围 最大值和最小值
// format range
value = value === '' ? 0 : +value;
value = Math.max(Math.min(this.data.max, value), this.data.min);
// 格式化小数位数
// format decimal
if (isDef(this.data.decimalLength)) {
value = value.toFixed(this.data.decimalLength);
}
return value;
},
},
});
复制代码
接着我们继续调试加减号基础功能。
6. 点击加号/减号
6.1 onTap 函数
<view
bind:tap="onTap"
></view>
复制代码
我们可以在 onTap
函数处断点,或者加上 debugger
。
onTap(event) {
debugger;
// data-type="minus"
const { type } = event.currentTarget.dataset;
this.type = type;
this.onChange();
},
复制代码
再在 onChange
函数断点,点击进入函数按钮操作。接着我们来看 onChange
函数实现。
6.2 onChange 函数
onChange() {
const { type } = this;
// 超出了,派发超过 overlimit 事件
if (this.isDisabled(type)) {
this.$emit('overlimit', type);
return;
}
// 差值 step 步长
const diff = type === 'minus' ? -this.data.step : +this.data.step;
// 格式化
const value = this.format(add(+this.data.currentValue, diff));
this.emitChange(value);
this.$emit(type);
},
复制代码
6.3 isDisabled 函数
最大最小值比较等。
isDisabled(type) {
const { disabled, disablePlus, disableMinus, currentValue, max, min, } = this.data;
if (type === 'plus') {
return disabled || disablePlus || currentValue >= max;
}
return disabled || disableMinus || currentValue <= min;
},
复制代码
6.4 emitChange 函数
emitChange(value) {
if (!this.data.asyncChange) {
this.setData({ currentValue: value });
}
this.$emit('change', value);
},
复制代码
如果不是异步,则直接赋值给 currentValue
。并且派发 change
事件。
断点调试在 $emit
函数。点击开发者工具的进入函数按钮。
6.5 $emit 函数
在上文提到过 Behavior
basic
。类似于 vue
中的 $emit
。
$emit(name, detail, options) {
this.triggerEvent(name, detail, options);
},
复制代码
组件间通信与事件 文档
<!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 -->
<component-tag-name bindmyevent="onMyEvent" />
<!-- 或者可以写成 -->
<component-tag-name bind:myevent="onMyEvent" />
复制代码
<!-- 在自定义组件中 -->
<button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>
复制代码
Component({
properties: {},
methods: {
onTap: function(){
var myEventDetail = {} // detail对象,提供给事件监听函数
var myEventOption = {} // 触发事件的选项
this.triggerEvent('myevent', myEventDetail, myEventOption)
}
}
})
复制代码
我们接着看输入框输入。
7. 输入框输入 onInput 函数
<input
bindinput="onInput"
/>
复制代码
onInput(event) {
debugger;
const { value = '' } = event.detail || {};
// allow input to be empty
if (value === '') {
return;
}
let formatted = this.filter(value);
// 限制最大的小数位
// limit max decimal length
if (isDef(this.data.decimalLength) && formatted.indexOf('.') !== -1) {
const pair = formatted.split('.');
formatted = `${pair[0]}.${pair[1].slice(0, this.data.decimalLength)}`;
}
this.emitChange(formatted);
},
复制代码
8. 输入框聚焦/失焦
<input
bind:focus="onFocus"
bind:blur="onBlur"
/>
复制代码
focus
、blur
事件
onFocus(event) {
this.$emit('focus', event.detail);
},
onBlur(event) {
const value = this.format(event.detail.value);
this.emitChange(value);
this.$emit('blur', Object.assign(Object.assign({}, event.detail), { value }));
},
复制代码
9. 长按加号/减号 累计功能
<view
bind:touchstart="onTouchStart"
bind:touchend="onTouchEnd"
></view>
复制代码
const LONG_PRESS_START_TIME = 600;
const LONG_PRESS_INTERVAL = 200;
longPressStep() {
this.longPressTimer = setTimeout(() => {
this.onChange();
this.longPressStep();
}, LONG_PRESS_INTERVAL);
},
onTouchStart(event) {
debugger;
// 如果不支持长按,默认支持
if (!this.data.longPress) {
return;
}
// 清除定时间
clearTimeout(this.longPressTimer);
const { type } = event.currentTarget.dataset;
this.type = type;
this.isLongPress = false;
this.longPressTimer = setTimeout(() => {
this.isLongPress = true;
this.onChange();
this.longPressStep();
}, LONG_PRESS_START_TIME);
},
onTouchEnd() {
if (!this.data.longPress) {
return;
}
// 长按结束,清除定时器
clearTimeout(this.longPressTimer);
},
复制代码
10. 总结
行文至此,就基本接近尾声了。我们从 vant-weapp
常用的 stepper
步进器组件源码出发。整体源码并不长。
我们通过调试方法,分析了整体结构,VantComponent
函数组件的实现,还有加号减号的功能基本实现,input 输入功能、聚焦失焦、还有长按累计的功能等。
或许我们自己实现,可能就写的一团糟。所以,相比于原生 JS
等源码。我们或许更应该学习,正在使用的组件库的源码,因为有助于帮助我们写业务和写自己的组件。
不过大多时候,学习源码或许是重要但不紧急的事情。除了公司项目外,我们可以多尝试学习开源项目的源码,从而贡献自己的代码,拥抱开源,会让自己更上一层楼。
感兴趣的小伙伴,可以克隆我的仓库调试学习 git clone https://github.com/lxchuan12/vant-weapp-analysis.git。
如果看完有收获,欢迎点赞、评论、分享支持。你的支持和肯定,是我写作的动力~
最后可以持续关注我 @若川。欢迎与我交流,参加由@若川视野发起的,每周大家一起学习200行左右的源码共读活动,共同进步。
评论