- 自定义组件发生Expect END descriptor...的坑!
今天打算把一个表格做成自定义组件,结果发现了一个大坑!!貌似官方文档上并没有对这里进行说明,所以希望能帮到遇到同样问题的同学。本人小白,技术能力不是很强,望说错的地方大神多多指点~ 下面进入正题 -------------------------------------<华丽的分割线>---------------------------------------------- 首先明确,我们在wxml中作条件判断的时候常用的是标签,所以循环表格组件的时候,我们是这样做的: [代码]<[代码][代码]block[代码] [代码]wx:if="{{goodDetail[attributeBoList].length > 0}}">[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"table"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-th"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"th"[代码][代码]>{{item[attributeModel][displayName]}}[代码]view[代码][代码]>[代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-tr"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList][0][attributeValueModelList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"index"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-td"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"idx"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"td"[代码][代码]>{{item[attributeValueModelList][index][attributeVal]}}[代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码]<[代码][代码]block[代码] [代码]wx:else>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]text[代码][代码]>此套餐还没有详细信息哦~[代码]text[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码]那这时候我想把class="table"的那一段打成组件。所以,坑开始了。 按照正常的流程,各种数据传递,引用之类的全部操作完毕,组件wxml为: [代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"table"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-th"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"th"[代码][代码]>[代码][代码] [代码][代码]{{item[attributeModel][displayName]}}[代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-tr"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList][0][attributeValueModelList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"index"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-td"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"idx"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"td"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]{{item[attributeValueModelList][index][attributeVal]}}[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码] 页面wxml为: [代码]<[代码][代码]block[代码] [代码]wx:if="{{goodDetail[attributeBoList].length > 0}}">[代码] [代码] [代码][代码]<[代码][代码]nstable[代码] [代码]goodDetail[代码][代码]=[代码][代码]"{{goodDetail}}"[代码] [代码]attributeBoList[代码][代码]=[代码][代码]"attributeBoList"[代码] [代码]attributeModel[代码][代码]=[代码][代码]"attributeModel"[代码] [代码]displayName[代码][代码]=[代码][代码]"displayName"[代码] [代码]attributeValueModelList[代码][代码]=[代码][代码]"attributeValueModelList"[代码] [代码]attributeVal[代码][代码]=[代码][代码]"attributeVal"[代码] [代码]tableTip[代码][代码]=[代码][代码]"{{tableTip}}"[代码][代码]>[代码]nstable[代码][代码]>[代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码]<[代码][代码]block[代码] [代码]wx:else>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]text[代码][代码]>此套餐还没有详细信息哦~[代码]text[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码] 结果我每次加载表格,都会报错: Expect END descriptor with depth 0 but get another 经过一次次排查,发现竟然是不能直接在block标签内引用自定义组件! 所以当我把block标签一起打入组件之后: [代码]<[代码][代码]block[代码] [代码]wx:if="{{goodDetail[attributeBoList].length > 0}}">[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"table"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-th"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"th"[代码][代码]>[代码][代码] [代码][代码]{{item[attributeModel][displayName]}}[代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-tr"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList][0][attributeValueModelList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"index"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-td"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"idx"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"td"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]{{item[attributeValueModelList][index][attributeVal]}}[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码]<[代码][代码]block[代码] [代码]wx:else>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]text[代码][代码]>{{tableTip}}[代码]text[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码]ok~~~~一切正常!所以这个坑给大家提个醒,希望能帮助大家解决这种问题,好了老板要过来查岗了,溜了溜了...
2018-04-04 - 小程序button去除边框问题
在网上搜了好多,但都是没有解决。所以来求助。 点击抽奖因为要获取用户手机号,所以用了button。但是修改了好多,发现一直存在黑色的边框。 上代码 html [代码]<[代码][代码]button[代码] [代码]class[代码][代码]=[代码][代码]"chou"[代码] [代码]style[代码][代码]=[代码][代码]'background-image:url(https://xxx.cn/playbtn.png);'[代码][代码]plain[代码][代码]=[代码][代码]'true'[代码] [代码]bindtap[代码][代码]=[代码][代码]"{{isclick}}"[代码] [代码]open-type[代码][代码]=[代码][代码]"start"[代码] [代码]bindgetphonenumber[代码][代码]=[代码][代码]"start"[代码] [代码]></[代码][代码]button[代码][代码]>[代码]css [代码].chou {[代码][代码] [代码][代码]position[代码][代码]: [代码][代码]absolute[代码][代码];[代码][代码] [代码][代码]top[代码][代码]: [代码][代码]50%[代码][代码];[代码][代码] [代码][代码]left[代码][代码]: [代码][代码]50%[代码][代码];[代码][代码] [代码][代码]transform: translate([代码][代码]-50%[代码][代码], [代码][代码]-50%[代码][代码]);[代码][代码] [代码][代码]width[代码][代码]: [代码][代码]180[代码][代码]rpx;[代码][代码] [代码][代码]height[代码][代码]: [代码][代码]180[代码][代码]rpx;[代码][代码] [代码][代码]z-index[代码][代码]: [代码][代码]10[代码][代码];[代码][代码] [代码][代码]margin[代码][代码]: [代码][代码]auto[代码][代码];[代码][代码] [代码][代码]padding[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]background-[代码][代码]size[代码][代码]: [代码][代码]180[代码][代码]rpx [代码][代码]180[代码][代码]rpx;[代码][代码] [代码][代码]background-repeat[代码][代码]: [代码][代码]no-repeat[代码][代码];[代码][代码] [代码][代码]border[代码][代码]: [代码][代码]none[代码][代码];[代码][代码]}[代码][代码]button::before {[代码][代码] [代码][代码]border[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]border[代码][代码]: [代码][代码]none[代码][代码];[代码][代码]}[代码][代码]button::after {[代码][代码] [代码][代码]border[代码][代码]: [代码][代码]0[代码][代码];[代码][代码] [代码][代码]border[代码][代码]: [代码][代码]none[代码][代码];[代码][代码]}[代码][图片]
2018-11-27 - 小程序的Animation.export方法有bug
- 当前 Bug 的表现(可附上截图) - 预期表现 - 复现路径 - 提供一个最简复现 Demo 文档中写到 export 方法每次调用后会清掉之前的动画操作。 可实际使用下来发现 代码: console.log(animation.top(200).step().export()) console.log(animation.scale(0, 0).step().export()) console.log(animation.export()) console.log(animation.step().export()) 输出结果: [图片] 也就是说,当export之后,再次export返回的动画是没有动画数据的,但是只要再次调用step了之后,之前调用过,这次没有覆盖掉的动画数据就都回来了. 实测开发者工具和iphone都有该现象并表现一致 如果是本人的使用方法不对也请告知,谢谢
2019-02-27 - 如何在微信内外部浏览器唤起小程序
微信外部浏览器唤起微信小程序目的:通过发送短信召回流失用户。官方文档地址https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html步骤一该API我们主要用到的配置如下:jump_wxa:跳转到的目标小程序信息。该对象内包含两个字段。path:通过scheme码进入的小程序页面路径,必须是已经发布的小程序存在的页面,不可携带queryquery:通过scheme码进入小程序时的query。步骤二需要注意的是,query必传,然而如果我们需要跳转的页面不需要参数,也需要配置上query,默认我们填写的是from=sms, 用于统计是从短信渠道来的。步骤三与后端约定access_token 通过前端传递appKey进行区分,传哪个微信小程序的appKey 就生成对应小程序的access_token。步骤四支持设置scheme的过期时间,默认永久,我们的应用场景很少,暂不详说。步骤五代码里操作如下,由后端聚合参数信息。let postData = { appKey: 'QTSHE_MINI_APP', path: 'pages/partdetails/partdetails', query: 'partJobId=123456' || 'a=1' } this.$axios.post('xxx', postData).then((res) => { if (res.success) { const url = res.data.openlink // 将scheme转为我们平台的短链接,否则在安卓手机上无法打开微信小程序,会默认打开浏览器搜索。 this.$axios.get(`xxx`, {url}).then((res) => { if (res.success) { this.shortLink = res.data // 将weixin://xxxx 转换为 https://b.qtshe.com/xxx } }) } else { this.$Message.error('获取失败,请稍后重试') } }).catch((err) => { console.log(err) this.$Message.error('获取失败,请稍后重试') }) 微信内部浏览器唤起小程序官方文档地址https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html步骤一首先需要登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”,未配置会导致wx.config签名失败,导致无法使用开放标签。步骤二在需要调用JS接口的页面引入如下JS文件:https://res.wx.qq.com/open/js/jweixin-1.6.0.js,1.6.0版本内才增加了上述标签,低于该版本的都无法显示。步骤三通过config接口注入权限验证配置并申请所需开放标签, 在wx.config里增加openTagList标签,内置两个开放标签 [代码]wx-open-launch-app[代码] 微信h5唤起本地已经安装的app,以及 [代码]wx-open-launch-weapp[代码] 微信h5唤起小程序,操作如下:wx.config({ debug: false, appId: window.g_info.wx_appid, // 全局变量appId timestamp: data.data.timestamp, // 调用微信接口返回的 nonceStr: data.data.nonceStr, // 调用微信接口返回的 signature: data.data.signature, // 调用微信接口返回的 jsApiList: ['openLocation', 'getLocation'], // 这里需要任意填写api,不能为空 openTagList: [ 'wx-open-launch-app', 'wx-open-launch-weapp' ] }) 注意点对于Vue等视图框架,为了避免template标签冲突的问题,可使用[代码]<script type="text/wxtag-template"></script>[代码]进行代替,来包裹插槽模版和样式。页面中与布局和定位相关的样式,如[代码]position: fixed; top -100;[代码]等,尽量不要写在插槽模版的节点中,请声明在标签或其父节点上;对于有CSP要求的页面,需要添加白名单frame-src https://*.qq.com webcompt:,才能在页面中正常使用开放标签。// Vue里操作代码如下,如果需要写样式,最好是外部包一个div进行样式编写,内部的内容宽高100%即可,调试样式可使用微信开发者工具的网页模式: .quick-btn { // 这里写样式 width: 3.43rem; height: 0.96rem; display: flex; img { width: 100%; display: block; } } <div class="quick-btn"> <img src="https://qiniu-image.qtshe.com/20220317quick-apply.png" alt="" /> </div> 示例效果[图片] 为了方便运营同学,做了个工具给他们使用。 [图片] 2023年4.11号新版本后的改版方案: 首先做一个落地页:https://b.qtshe.com/1DF43E 代码如下: import toast from 'toast' export default { created() { this.init() }, methods: { // 获取微信scheme地址,并且主动跳转一次 init() { this.$axios.post(`https://xxx/你们的接口地址/user/device/scheme`, this.$route.query).then(res => { if (res.success) { window.location.href = res.data } else { toast(res.msg) } }).catch(() => { toast('服务器错误,请稍后重试') }) } } } 原理:通过地址栏配置需要跳转的参数,h5页面拿到参数后通过接口转换为weixin://xxxx 开头的scheme协议地址,并且主动location.href一次。这样能保证用户每次打开都是新的scheme地址,针对一天50w数量上限的问题,可尝试同一个用户在固定时间内相同的参数,返回相同的scheme地址。 以上限制只存在于除微信外的外部浏览器,微信容器里没有以上的限制。 2023年12月18日后方案: 官方公告如下: https://developers.weixin.qq.com/community/develop/doc/00024e32cbc36055c0c0a34b066401 ,没有外部浏览器、微信环境的限制,可以被多人打开。 使用方式通过明文拼接: weixin://dl/business/?appid=*APPID*&path=*PATH*&query=*QUERY*&env_version=*ENV_VERSION* 示例: // query需要encode编码,ENV_VERSION为控制打开开发版、体验版、线上版本 weixin://dl/business/?appid=wxa7b0b022472e55e6&path=pages/partdetails/partdetails&query=partJobId%3D123456 通过明文 URL Scheme 拉起的小程序(页面)必须要提前在「小程序管理后台 -> 设置 -> 隐私与安全 -> 明文 scheme 拉起此小程序」中进行声明; 小程序:配置能够通过明文 scheme 进入的小程序页面[图片] 这一点很烦很烦,希望能支持下 默认可打开所有的,不然每次都得去配置。
08-07 - 微信小程序动画实现方案
微信小程序动画实现方案 背景 基于商城业务需求背景下,实现加购动画;具体要求如下: 动画落点不准 体验优化,动画先行,每点击一次需要触发一次动画 减少动画掉帧,卡;避免动画延迟,解决加购卡顿问题 基于以上诉求,对微信小程序各种动画实现方案做了简单的对比分析 动画落点不准确 最开始实现方案,就是在dom ready 的时候去获取元素落点,缓存下来,后续复用改落点即可; [代码] onReady() { // 获取落点坐标 this.getBubblePos('#end-point'); }, [代码] 然而在商城当前业务中,这种方案,获取到的坐标频繁出现较大偏差。基于现状,延迟获取落点坐标,出现以下方案(timeout 和 nextTick 都尝试了,依然没能解决问题,于是综合了一下) [代码] onReady() { // 获取落点坐标 this.$nextTick(() => { setTimeout(() => { this.getBubblePos('#end-point'); }, xxxx); }) }, [代码] [代码]timeout[代码]方案尝试了不同的延迟时间,发现在1s内,依旧会出现 获取到的位置不准确,初步判断跟加购动画落点所在的结算条的条件渲染有关。timeout 解决不了问题。 最终在x大佬的指导下,参考了其他平台的实现方案,采取了点击时获取落点坐标,解决了该问题; [代码] async startAnimation(e) { //点击时 获取落点 并 缓存 await this.setStartPos(`#end-point`); // 开始动画 this.useAnimateApi(0); }, [代码] 快速点击下能连续触发,多个动画并存 需要有多个动画元素,保证快速点击的时候,动画不延迟,并且每次点击都能触发一个动画元素执行动画;动画元素要求能购复用,减少冗余dom 元素; 业务现状: 现有加购按钮跟落点所在的结算条位于不同的组件内;点击时获取到点击位置的坐标,然后需要将动画元素从点击位置 移动到落点; 解决方案: 每个页面初始化 [代码]n[代码] (n=10)个元素隐藏在 页面内部 css 隐藏到页面某个区域;为了回收复用动画元素 用一个队列来维护当前可使用动画元素; [代码] <!-- css 元素2个元素实现 --> <block wx:for="{{transition}}" wx:key="id"> <view class="bubblebox bubblebox_{{index}}" style="{{item.parent}}"> <view class="bubble bubble_{{index}}" bind:transitionend="transitionEnd" data-index="{{index}}" style="{{item.child}}"></view> </view> </block> [代码] [代码]// 维护动画元素队列,动画执行完成之后 回收当前动画元素 transitionEnd(e) { console.log(e); // 监听动画结束时间 清除动画 const { index = 0 } = e.currentTarget.dataset || {}; this.data.queue.push(Number(index)); this.data.transition.splice(index, 1, { id: index, parent: '', child: '' }); this.setData({ transition: this.data.transition, }); }, [代码] 掉帧 卡顿问题 针对该问题,对各个实现方案做了个对比 Note: 商城加购动画 最终实现方案采用 css transition 实现 [代码]wx.createAnimation[代码] [代码] useCreateAnimation(index) { const { x: x1, y: y1 } = this.data.startPos; const { x: x2, y: y2 } = this.data.dropPos; // 1. 移动到 起点 // duration 不能为0 最小为1 this.animation.translate(x1, y1).opacity(1).step({ duration: 1, delay: 0 }); // const parent = this.animation.export(); // // 2. 起点移动到 落点 this.animation.translate(x2, y2).step({ duration: 1000, delay: 1 }); // // // 3. 隐藏气泡 回到起点 this.animation.opacity(0).translate(0, 0).step({ duration: 1, delay: 1000 }); const child = this.animation.export(); this.data.transition.splice(index, 1, { id: index, parent: '', child }); this.setData({ transition: this.data.transition, }); }, [代码] [代码]this.animate[代码] 从小程序基础库 2.9.0 开始 [代码] useAnimateApi(index) { const { x: x1, y: y1 } = this.data.startPos; const { x: x2, y: y2 } = this.data.dropPos; console.log(`useAnimateApi`, index, x1, y1, x2, y2); // 2. 上 下 移动动画 this.animate(`#bubble_${index}`, [{ left: `${x1}px`, top: `${y1}px`, opacity: 1 }, { translate: [`${x2 - x1}px`, `${y2 - y1}px`] }], 500, () => { console.log('animationEnd'); this.clearAnimation(`#bubble_${index}`, () => {}); this.animationEnd(index); }); }, [代码] [代码]css3 transition[代码] [代码] useCssAnimate(index) { const { x: x1, y: y1 } = this.data.startPos; const { x: x2, y: y2 } = this.data.dropPos; const parent = `transition: transform 0ms 0ms linear;transform: translate(${x1}px,${y1}px)`; const child = `transition:opacity 0s 0s,transform 2s 0ms linear;transform: translate(${x2 - x1}px,${y2 - y1}px);opacity:1;`; this.data.transition.splice(index, 1, { id: index, parent, child }); this.setData({ transition: this.data.transition, }); }, [代码] [代码]wxs[代码] 相应事件 小程序双引擎设计,setData 是影响性能的关键因素,wxs 相应事件能减少 setData 次数,有助于提高动画性能; [代码]<wxs module="utils"> var transition = function(e, ownerInstance) { var index = 0 var parent = ownerInstance.selectComponent('#bubblebox_1_'+index) // 返回组件的实例 var child = ownerInstance.selectComponent('#bubble_1_'+index) // 返回组件的实例 var x1 = 300 var y1 =50 var x2 = 50 var y2 = 278 console.log(parent,child) parent.setStyle({ transition: 'transform 0ms 0ms linear', transform: 'translate(300px,50px)' }) child.setStyle({ transition:'opacity 0s 0s,transform 2s 0ms linear', transform: 'translate(-250px,238px);opacity:1' }) return false // 不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault } module.exports = { transition:transition } [代码] </details> [代码]css3 animation[代码] 待尝试,动画生成keyframes 如何绑定到 dom 上? [代码]css transition[代码] 细节实现 分析当前动画元素过程 css 元素从隐藏的位置(fixed到左上角,(0,0))移动到起点,并可见; [代码]transition:opacity 0s 0s,top 0s 0s,left 0s 0s,transform 2s 0ms linear; transform: translate(1px,1px);opacity:1; top:100px; left:100px; [代码] 通过top,left 移动元素到起点,并且 通过opacity 控制元素展示出来;执行 translate 动画 利用[代码]transition[代码] 可以同时为不同的 [代码]动画属性[代码] 指定不同的 [代码]duration[代码] [代码]timeFunction[代码] [代码]delay[代码] 动画结束 移除动画元素绑定的 style,动画元素回到起点;动画结束,维护可使用的动画元素队列;监听动画结束事件,回收当前元素入队。 [代码] <view bind:transitionend="transitionEnd"></view> [代码] [代码] transitionEnd(e) { console.log(e); const { index = 0 } = e.currentTarget.dataset || {}; // 动画元素 入队 this.data.queue.push(Number(index)); // 监听动画结束时间 清除动画 this.data.transition.splice(index, 1, { id: index, parent: '', child: '' }); this.setData({ transition: this.data.transition, }); }, [代码] 可以用1层dom,为何用2层dom ? 虽然元素已经脱离了文档流,top left 并不会触发 重绘 引起性能问题;但是不能充分利用css3 硬件加速的能力。 完美解决??? 该方案对 dom性能有较大提高,但是消耗的内存占用也不容小觑。目前商城业务高度复杂,内存占本来就高的情况下,改方案依旧会在内存占用过高的情况下[代码]闪现[代码],过渡态消失的情况。 抛物线方案 该方案 必须使用2层view元素实现 抛物线方案,改方案初步实现后在小程序里面不同机型上 效果差异较大,动画方案回退为直线; 具体步骤: 移动到起点 父级元素 向左边移动 同时子级元素先向上移动指定距离,再向下(时间函数控制曲线斜率) 回收元素 参考: [代码]// 1. 抛物线 const parent = `transition: top 0s 0s, left 0s 0s, opacity 0s 0s, visibility 0s 0s, transform 500ms 0ms ease-in;transform: translateX(${ x2 - x1 }px);left:${x1}px;top:${y1}px;opacity:1;visibility:visible;` const child = `transition: transform 300ms 200ms ease-in, margin-top 200ms 0ms ease-in-out,opacity 0ms 501ms linear,visibility 0ms 501ms linear;margin-top: -66px;transform: translateY(${ y2 - y1 + 66 }px);opacity:0;visibility:hidden;` [代码] Refer weixin miniprogram wxs 响应事件 浏览器的重绘和回流(Repaint & Reflow)
2021-12-13 - 死磕 36 个 JS 手写题(搞懂后,提升真的大)
为什么要写这类文章 作为一个程序员,代码能力毋庸置疑是非常非常重要的,就像现在为什么大厂面试基本都问什么 API 怎么实现可见其重要性。我想说的是居然手写这么重要,那我们就必须掌握它,所以文章标题用了死磕,一点也不过分,也希望不被认为是标题党。 作为一个普通前端,我是真的写不出 Promise A+ 规范,但是没关系,我们可以站在巨人的肩膀上,要相信我们现在要走的路,前人都走过,所以可以找找现在社区已经存在的那些优秀的文章,比如工业聚大佬写的 100 行代码实现 Promises/A+ 规范,找到这些文章后不是收藏夹吃灰,得找个时间踏踏实实的学,一行一行的磨,直到搞懂为止。我现在就是这么干的。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi 能收获什么 这篇文章总体上分为 2 类手写题,前半部分可以归纳为是常见需求,后半部分则是对现有技术的实现; 对常用的需求进行手写实现,比如数据类型判断函数、深拷贝等可以直接用于往后的项目中,提高了项目开发效率;对现有关键字和 API 的实现,可能需要用到别的知识或 API,比如在写 forEach 的时候用到了无符号位右移的操作,平时都不怎么能够接触到这玩意,现在遇到了就可以顺手把它掌握了。所以手写这些实现能够潜移默化的扩展并巩固自己的 JS 基础;通过写各种测试用例,你会知道各种 API 的边界情况,比如 Promise.all, 你得考虑到传入参数的各种情况,从而加深了对它们的理解及使用; 阅读的时候需要做什么 阅读的时候,你需要把每行代码都看懂,知道它在干什么,为什么要这么写,能写得更好嘛?比如在写图片懒加载的时候,一般我们都是根据当前元素的位置和视口进行判断是否要加载这张图片,普通程序员写到这就差不多完成了。而大佬程序员则是会多考虑一些细节的东西,比如性能如何更优?代码如何更精简?比如 yeyan1996 写的图片懒加载就多考虑了 2 点:比如图片全部加载完成的时候得把事件监听给移除;比如加载完一张图片的时候,得把当前 img 从 imgList 里移除,起到优化内存的作用。 除了读通代码之外,还可以打开 Chrome 的 Script snippet 去写测试用例跑跑代码,做到更好的理解以及使用。 在看了几篇以及写了很多测试用例的前提下,尝试自己手写实现,看看自己到底掌握了多少。条条大路通罗马,你还能有别的方式实现嘛?或者你能写得比别人更好嘛? 好了,还楞着干啥,开始干活。 数据类型判断 typeof 可以正确识别:Undefined、Boolean、Number、String、Symbol、Function 等类型的数据,但是对于其他的都会认为是 object,比如 Null、Date 等,所以通过 typeof 来判断数据类型会不准确。但是可以使用 Object.prototype.toString 实现。 function typeOf(obj) { let res = Object.prototype.toString.call(obj).split(' ')[1] res = res.substring(0, res.length - 1).toLowerCase() return res } typeOf([]) // 'array' typeOf({}) // 'object' typeOf(new Date) // 'date' 继承 原型链继承function Animal() { this.colors = ['black', 'white'] } Animal.prototype.getColor = function() { return this.colors } function Dog() {} Dog.prototype = new Animal() let dog1 = new Dog() dog1.colors.push('brown') let dog2 = new Dog() console.log(dog2.colors) // ['black', 'white', 'brown'] 原型链继承存在的问题: 问题1:原型中包含的引用类型属性将被所有实例共享;问题2:子类在实例化的时候不能给父类构造函数传参; 借用构造函数实现继承 function Animal(name) { this.name = name this.getName = function() { return this.name } } function Dog(name) { Animal.call(this, name) } Dog.prototype = new Animal() 借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。 组合继承 组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。 function Animal(name) { this.name = name this.colors = ['black', 'white'] } Animal.prototype.getName = function() { return this.name } function Dog(name, age) { Animal.call(this, name) this.age = age } Dog.prototype = new Animal() Dog.prototype.constructor = Dog let dog1 = new Dog('奶昔', 2) dog1.colors.push('brown') let dog2 = new Dog('哈赤', 1) console.log(dog2) // { name: "哈赤", colors: ["black", "white"], age: 1 } 寄生式组合继承 组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。 所以解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数 F 获取父类原型的副本。 寄生式组合继承写法上和组合继承基本类似,区别是如下这里: - Dog.prototype = new Animal() - Dog.prototype.constructor = Dog + function F() {} + F.prototype = Animal.prototype + let f = new F() + f.constructor = Dog + Dog.prototype = f 稍微封装下上面添加的代码后: function object(o) { function F() {} F.prototype = o return new F() } function inheritPrototype(child, parent) { let prototype = object(parent.prototype) prototype.constructor = child child.prototype = prototype } inheritPrototype(Dog, Animal) 如果你嫌弃上面的代码太多了,还可以基于组合继承的代码改成最简单的寄生式组合继承: - Dog.prototype = new Animal() - Dog.prototype.constructor = Dog + Dog.prototype = Object.create(Animal.prototype) + Dog.prototype.constructor = Dog class 实现继承 class Animal { constructor(name) { this.name = name } getName() { return this.name } } class Dog extends Animal { constructor(name, age) { super(name) this.age = age } } 数组去重 ES5 实现: function unique(arr) { var res = arr.filter(function(item, index, array) { return array.indexOf(item) === index }) return res } ES6 实现: var unique = arr => [...new Set(arr)] 数组扁平化 数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层: [1, [2, [3]]].flat(2) // [1, 2, 3] 现在就是要实现 flat 这种效果。 ES5 实现:递归。 function flatten(arr) { var result = []; for (var i = 0, len = arr.length; i < len; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])) } else { result.push(arr[i]) } } return result; } ES6 实现: function flatten(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } 深浅拷贝 浅拷贝:只考虑对象类型。 function shallowCopy(obj) { if (typeof obj !== 'object') return let newObj = obj instanceof Array ? [] : {} for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key] } } return newObj } 简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数。 function deepClone(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key]; } } return newObj; } 复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。 const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null; function deepClone(target, map = new WeakMap()) { if (map.get(target)) { return target; } // 获取当前值的构造函数:获取它的类型 let constructor = target.constructor; // 检测当前对象target是否与正则、日期格式对象匹配 if (/^(RegExp|Date)$/i.test(constructor.name)) { // 创建一个新的特殊对象(正则类/日期类)的实例 return new constructor(target); } if (isObject(target)) { map.set(target, true); // 为循环引用的对象做标记 const cloneTarget = Array.isArray(target) ? [] : {}; for (let prop in target) { if (target.hasOwnProperty(prop)) { cloneTarget[prop] = deepClone(target[prop], map); } } return cloneTarget; } else { return target; } } 事件总线(发布订阅模式) class EventEmitter { constructor() { this.cache = {} } on(name, fn) { if (this.cache[name]) { this.cache[name].push(fn) } else { this.cache[name] = [fn] } } off(name, fn) { let tasks = this.cache[name] if (tasks) { const index = tasks.findIndex(f => f === fn || f.callback === fn) if (index >= 0) { tasks.splice(index, 1) } } } emit(name, once = false, ...args) { if (this.cache[name]) { // 创建副本,如果回调函数内继续注册相同事件,会造成死循环 let tasks = this.cache[name].slice() for (let fn of tasks) { fn(...args) } if (once) { delete this.cache[name] } } } } // 测试 let eventBus = new EventEmitter() let fn1 = function(name, age) { console.log(`${name} ${age}`) } let fn2 = function(name, age) { console.log(`hello, ${name} ${age}`) } eventBus.on('aaa', fn1) eventBus.on('aaa', fn2) eventBus.emit('aaa', false, '布兰', 12) // '布兰 12' // 'hello, 布兰 12' 解析 URL 参数为对象 function parseParam(url) { const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来 const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中 let paramsObj = {}; // 将 params 存到对象中 paramsArr.forEach(param => { if (/=/.test(param)) { // 处理有 value 的参数 let [key, val] = param.split('='); // 分割 key 和 value val = decodeURIComponent(val); // 解码 val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字 if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值 paramsObj[key] = [].concat(paramsObj[key], val); } else { // 如果对象没有这个 key,创建 key 并设置值 paramsObj[key] = val; } } else { // 处理没有 value 的参数 paramsObj[param] = true; } }) return paramsObj; } 字符串模板 function render(template, data) { const reg = /\{\{(\w+)\}\}/; // 模板字符串正则 if (reg.test(template)) { // 判断模板里是否有模板字符串 const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段 template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 return render(template, data); // 递归的渲染并返回渲染后的结构 } return template; // 如果模板没有模板字符串直接返回 } 测试: let template = '我是{{name}},年龄{{age}},性别{{sex}}'; let person = { name: '布兰', age: 12 } render(template, person); // 我是布兰,年龄12,性别undefined 图片懒加载 与普通的图片懒加载不同,如下这个多做了 2 个精心处理: 图片全部加载完成后移除事件监听;加载完的图片,从 imgList 移除; let imgList = [...document.querySelectorAll('img')] let length = imgList.length const imgLazyLoad = function() { let count = 0 return function() { let deleteIndexList = [] imgList.forEach((img, index) => { let rect = img.getBoundingClientRect() if (rect.top < window.innerHeight) { img.src = img.dataset.src deleteIndexList.push(index) count++ if (count === length) { document.removeEventListener('scroll', imgLazyLoad) } } }) imgList = imgList.filter((img, index) => !deleteIndexList.includes(index)) } } // 这里最好加上防抖处理 document.addEventListener('scroll', imgLazyLoad) 参考:深入理解 Promise 函数防抖 触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。 简单版:函数内部支持使用 this 和 event 对象; function debounce(func, wait) { var timeout; return function () { var context = this; var args = arguments; clearTimeout(timeout) timeout = setTimeout(function(){ func.apply(context, args) }, wait); } } 使用: var node = document.getElementById('layout') function getUserAction(e) { console.log(this, e) // 分别打印:node 这个节点 和 MouseEvent node.innerHTML = count++; }; node.onmousemove = debounce(getUserAction, 1000) 最终版:除了支持 this 和 event 外,还支持以下功能: 支持立即执行;函数可能有返回值;支持取消功能; function debounce(func, wait, immediate) { var timeout, result; var debounced = function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) result = func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } return result; }; debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; return debounced; } 使用: var setUseAction = debounce(getUserAction, 10000, true); // 使用防抖 node.onmousemove = setUseAction // 取消防抖 setUseAction.cancel() 参考:JavaScript专题之跟着underscore学防抖 函数节流 触发高频事件,且 N 秒内只执行一次。 简单版:使用时间戳来实现,立即执行一次,然后每 N 秒执行一次。 function throttle(func, wait) { var context, args; var previous = 0; return function() { var now = +new Date(); context = this; args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } } } 最终版:支持取消节流;另外通过传入第三个参数,options.leading 来表示是否可以立即执行一次,opitons.trailing 表示结束调用的时候是否还要执行一次,默认都是 true。 注意设置的时候不能同时将 leading 或 trailing 设置为 false。 function throttle(func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : new Date().getTime(); timeout = null; func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function() { var now = new Date().getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } }; throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = null; } return throttled; } 节流的使用就不拿代码举例了,参考防抖的写就行。 参考:JavaScript专题之跟着 underscore 学节流 函数柯里化 什么叫函数柯里化?其实就是将使用多个参数的函数转换成一系列使用一个参数的函数的技术。还不懂?来举个例子。 function add(a, b, c) { return a + b + c } add(1, 2, 3) let addCurry = curry(add) addCurry(1)(2)(3) 现在就是要实现 curry 这个函数,使函数从一次调用传入多个参数变成多次调用每次传一个参数。 function curry(fn) { let judge = (...args) => { if (args.length == fn.length) return fn(...args) return (...arg) => judge(...args, ...arg) } return judge } 偏函数 什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。举个例子: function add(a, b, c) { return a + b + c } let partialAdd = partial(add, 1) partialAdd(2, 3) 发现没有,其实偏函数和函数柯里化有点像,所以根据函数柯里化的实现,能够能很快写出偏函数的实现: function partial(fn, ...args) { return (...arg) => { return fn(...args, ...arg) } } 如上这个功能比较简单,现在我们希望偏函数能和柯里化一样能实现占位功能,比如: function clg(a, b, c) { console.log(a, b, c) } let partialClg = partial(clg, '_', 2) partialClg(1, 3) // 依次打印:1, 2, 3 [代码]_[代码] 占的位其实就是 1 的位置。相当于:partial(clg, 1, 2),然后 partialClg(3)。明白了原理,我们就来写实现: function partial(fn, ...args) { return (...arg) => { args[index] = return fn(...args, ...arg) } } JSONP JSONP 核心原理:script 标签不受同源策略约束,所以可以用来进行跨域请求,优点是兼容性好,但是只能用于 GET 请求; const jsonp = ({ url, params, callbackName }) => { const generateUrl = () => { let dataSrc = '' for (let key in params) { if (params.hasOwnProperty(key)) { dataSrc += `${key}=${params[key]}&` } } dataSrc += `callback=${callbackName}` return `${url}?${dataSrc}` } return new Promise((resolve, reject) => { const scriptEle = document.createElement('script') scriptEle.src = generateUrl() document.body.appendChild(scriptEle) window[callbackName] = data => { resolve(data) document.removeChild(scriptEle) } }) } AJAX const getJSON = function(url) { return new Promise((resolve, reject) => { const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp'); xhr.open('GET', url, false); xhr.setRequestHeader('Accept', 'application/json'); xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; if (xhr.status === 200 || xhr.status === 304) { resolve(xhr.responseText); } else { reject(new Error(xhr.responseText)); } } xhr.send(); }) } 实现数组原型方法 forEach Array.prototype.forEach2 = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) // this 就是当前的数组 const len = O.length >>> 0 // 后面有解释 let k = 0 while (k < len) { if (k in O) { callback.call(thisArg, O[k], k, O); } k++; } } 参考:forEach#polyfill O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型。感兴趣可以阅读 something >>> 0是什么意思?。 map 基于 forEach 的实现能够很容易写出 map 的实现: - Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.map2 = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) const len = O.length >>> 0 - let k = 0 + let k = 0, res = [] while (k < len) { if (k in O) { - callback.call(thisArg, O[k], k, O); + res[k] = callback.call(thisArg, O[k], k, O); } k++; } + return res } filter 同样,基于 forEach 的实现能够很容易写出 filter 的实现: - Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.filter2 = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) const len = O.length >>> 0 - let k = 0 + let k = 0, res = [] while (k < len) { if (k in O) { - callback.call(thisArg, O[k], k, O); + if (callback.call(thisArg, O[k], k, O)) { + res.push(O[k]) + } } k++; } + return res } some 同样,基于 forEach 的实现能够很容易写出 some 的实现: - Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.some2 = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) const len = O.length >>> 0 let k = 0 while (k < len) { if (k in O) { - callback.call(thisArg, O[k], k, O); + if (callback.call(thisArg, O[k], k, O)) { + return true + } } k++; } + return false } reduce Array.prototype.reduce2 = function(callback, initialValue) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) const len = O.length >>> 0 let k = 0, acc if (arguments.length > 1) { acc = initialValue } else { // 没传入初始值的时候,取数组中第一个非 empty 的值为初始值 while (k < len && !(k in O)) { k++ } if (k > len) { throw new TypeError( 'Reduce of empty array with no initial value' ); } acc = O[k++] } while (k < len) { if (k in O) { acc = callback(acc, O[k], k, O) } k++ } return acc } 实现函数原型方法 call 使用一个指定的 this 值和一个或多个参数来调用一个函数。 实现要点: this 可能传入 null;传入不固定个数的参数;函数可能有返回值; Function.prototype.call2 = function (context) { var context = context || window; context.fn = this; var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } var result = eval('context.fn(' + args +')'); delete context.fn return result; } apply apply 和 call 一样,唯一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个数组。 实现要点: this 可能传入 null;传入一个数组;函数可能有返回值; Function.prototype.apply2 = function (context, arr) { var context = context || window; context.fn = this; var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push('arr[' + i + ']'); } result = eval('context.fn(' + args + ')') } delete context.fn return result; } bind bind 方法会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。 实现要点: bind() 除了 this 外,还可传入多个参数;bing 创建的新函数可能传入多个参数;新函数可能被当做构造函数调用;函数可能有返回值; Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fNOP = function () {}; var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); } fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; } 实现 new 关键字 new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。 实现要点: new 会产生一个新对象;新对象需要能够访问到构造函数的属性,所以需要重新指定它的原型;构造函数可能会显示返回; function objectFactory() { var obj = new Object() Constructor = [].shift.call(arguments); obj.__proto__ = Constructor.prototype; var ret = Constructor.apply(obj, arguments); // ret || obj 这里这么写考虑了构造函数显示返回 null 的情况 return typeof ret === 'object' ? ret || obj : obj; }; 使用: function person(name, age) { this.name = name this.age = age } let p = objectFactory(person, '布兰', 12) console.log(p) // { name: '布兰', age: 12 } 实现 instanceof 关键字 instanceof 就是判断构造函数的 prototype 属性是否出现在实例的原型链上。 function instanceOf(left, right) { let proto = left.__proto__ while (true) { if (proto === null) return false if (proto === right.prototype) { return true } proto = proto.__proto__ } } 上面的 left.proto 这种写法可以换成 Object.getPrototypeOf(left)。 实现 Object.create Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 Object.create2 = function(proto, propertyObject = undefined) { if (typeof proto !== 'object' && typeof proto !== 'function') { throw new TypeError('Object prototype may only be an Object or null.') if (propertyObject == null) { new TypeError('Cannot convert undefined or null to object') } function F() {} F.prototype = proto const obj = new F() if (propertyObject != undefined) { Object.defineProperties(obj, propertyObject) } if (proto === null) { // 创建一个没有原型对象的对象,Object.create(null) obj.__proto__ = null } return obj } 实现 Object.assign Object.assign2 = function(target, ...source) { if (target == null) { throw new TypeError('Cannot convert undefined or null to object') } let ret = Object(target) source.forEach(function(obj) { if (obj != null) { for (let key in obj) { if (obj.hasOwnProperty(key)) { ret[key] = obj[key] } } } }) return ret } 实现 JSON.stringify JSON.stringify([, replacer [, space]) 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串。此处模拟实现,不考虑可选的第二个参数 replacer 和第三个参数 space,如果对这两个参数的作用还不了解,建议阅读 MDN 文档。 1.基本数据类型: undefined 转换之后仍是 undefined(类型也是 undefined)boolean 值转换之后是字符串 "false"/"true"number 类型(除了 NaN 和 Infinity)转换之后是字符串类型的数值symbol 转换之后是 undefinednull 转换之后是字符串 "null"string 转换之后仍是stringNaN 和 Infinity 转换之后是字符串 "null"2.函数类型:转换之后是 undefined 3.如果是对象类型(非函数) 如果是一个数组:如果属性值中出现了 undefined、任意的函数以及 symbol,转换成字符串 "null" ;如果是 RegExp 对象:返回 {} (类型是 string);如果是 Date 对象,返回 Date 的 toJSON 字符串值;如果是普通对象; 如果有 toJSON() 方法,那么序列化 toJSON() 的返回值。 如果属性值中出现了 undefined、任意的函数以及 symbol 值,忽略。 所有以 symbol 为属性键的属性都会被完全忽略掉。 4.对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi function jsonStringify(data) { let dataType = typeof data; if (dataType !== 'object') { let result = data; //data 可能是 string/number/null/undefined/boolean if (Number.isNaN(data) || data === Infinity) { //NaN 和 Infinity 序列化返回 "null" result = "null"; } else if (dataType === 'function' || dataType === 'undefined' || dataType === 'symbol') { //function 、undefined 、symbol 序列化返回 undefined return undefined; } else if (dataType === 'string') { result = '"' + data + '"'; } //boolean 返回 String() return String(result); } else if (dataType === 'object') { if (data === null) { return "null" } else if (data.toJSON && typeof data.toJSON === 'function') { return jsonStringify(data.toJSON()); } else if (data instanceof Array) { let result = []; //如果是数组 //toJSON 方法可以存在于原型链中 data.forEach((item, index) => { if (typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') { result[index] = "null"; } else { result[index] = jsonStringify(item); } }); result = "[" + result + "]"; return result.replace(/'/g, '"'); } else { //普通对象 /** * 循环引用抛错(暂未检测,循环引用时,堆栈溢出) * symbol key 忽略 * undefined、函数、symbol 为属性值,被忽略 */ let result = []; Object.keys(data).forEach((item, index) => { if (typeof item !== 'symbol') { //key 如果是symbol对象,忽略 if (data[item] !== undefined && typeof data[item] !== 'function' && typeof data[item] !== 'symbol') { //键值如果是 undefined、函数、symbol 为属性值,忽略 result.push('"' + item + '"' + ":" + jsonStringify(data[item])); } } }); return ("{" + result + "}").replace(/'/g, '"'); } } } 参考:实现 JSON.stringify 实现 JSON.parse 介绍 2 种方法实现: eval 实现;new Function 实现; eval 实现 第一种方式最简单,也最直观,就是直接调用 eval,代码如下: var json = '{"a":"1", "b":2}'; var obj = eval("(" + json + ")"); // obj 就是 json 反序列化之后得到的对象 但是直接调用 eval 会存在安全问题,如果数据中可能不是 json 数据,而是可执行的 JavaScript 代码,那很可能会造成 XSS 攻击。因此,在调用 eval 之前,需要对数据进行校验。 var rx_one = /^[\],:{}\s]*$/; var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; var rx_four = /(?:^|:|,)(?:\s*\[)+/g; if ( rx_one.test( json.replace(rx_two, "@") .replace(rx_three, "]") .replace(rx_four, "") ) ) { var obj = eval("(" +json + ")"); } 参考:JSON.parse 三种实现方式 new Function 实现 Function 与 eval 有相同的字符串参数特性。 var json = '{"name":"小姐姐", "age":20}'; var obj = (new Function('return ' + json))(); 实现 Promise 实现 Promise 需要完全读懂 Promise A+ 规范,不过从总体的实现上看,有如下几个点需要考虑到: then 需要支持链式调用,所以得返回一个新的 Promise;处理异步问题,所以得先用 onResolvedCallbacks 和 onRejectedCallbacks 分别把成功和失败的回调存起来;为了让链式调用正常进行下去,需要判断 onFulfilled 和 onRejected 的类型;onFulfilled 和 onRejected 需要被异步调用,这里用 setTimeout 模拟异步;处理 Promise 的 resolve; const PENDING = 'pending'; const FULFILLED = 'fulfilled'; const REJECTED = 'rejected'; class Promise { constructor(executor) { this.status = PENDING; this.value = undefined; this.reason = undefined; this.onResolvedCallbacks = []; this.onRejectedCallbacks = []; let resolve = (value) = > { if (this.status === PENDING) { this.status = FULFILLED; this.value = value; this.onResolvedCallbacks.forEach((fn) = > fn()); } }; let reject = (reason) = > { if (this.status === PENDING) { this.status = REJECTED; this.reason = reason; this.onRejectedCallbacks.forEach((fn) = > fn()); } }; try { executor(resolve, reject); } catch (error) { reject(error); } } then(onFulfilled, onRejected) { // 解决 onFufilled,onRejected 没有传值的问题 onFulfilled = typeof onFulfilled === "function" ? onFulfilled : (v) = > v; // 因为错误的值要让后面访问到,所以这里也要抛出错误,不然会在之后 then 的 resolve 中捕获 onRejected = typeof onRejected === "function" ? onRejected : (err) = > { throw err; }; // 每次调用 then 都返回一个新的 promise let promise2 = new Promise((resolve, reject) = > { if (this.status === FULFILLED) { //Promise/A+ 2.2.4 --- setTimeout setTimeout(() = > { try { let x = onFulfilled(this.value); // x可能是一个proimise resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === REJECTED) { //Promise/A+ 2.2.3 setTimeout(() = > { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); } if (this.status === PENDING) { this.onResolvedCallbacks.push(() = > { setTimeout(() = > { try { let x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); this.onRejectedCallbacks.push(() = > { setTimeout(() = > { try { let x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }, 0); }); } }); return promise2; } } const resolvePromise = (promise2, x, resolve, reject) = > { // 自己等待自己完成是错误的实现,用一个类型错误,结束掉 promise Promise/A+ 2.3.1 if (promise2 === x) { return reject( new TypeError("Chaining cycle detected for promise #")); } // Promise/A+ 2.3.3.3.3 只能调用一次 let called; // 后续的条件要严格判断 保证代码能和别的库一起使用 if ((typeof x === "object" && x != null) || typeof x === "function") { try { // 为了判断 resolve 过的就不用再 reject 了(比如 reject 和 resolve 同时调用的时候) Promise/A+ 2.3.3.1 let then = x.then; if (typeof then === "function") { // 不要写成 x.then,直接 then.call 就可以了 因为 x.then 会再次取值,Object.defineProperty Promise/A+ 2.3.3.3 then.call( x, (y) = > { // 根据 promise 的状态决定是成功还是失败 if (called) return; called = true; // 递归解析的过程(因为可能 promise 中还有 promise) Promise/A+ 2.3.3.3.1 resolvePromise(promise2, y, resolve, reject); }, (r) = > { // 只要失败就失败 Promise/A+ 2.3.3.3.2 if (called) return; called = true; reject(r); }); } else { // 如果 x.then 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.3.4 resolve(x); } } catch (e) { // Promise/A+ 2.3.3.2 if (called) return; called = true; reject(e); } } else { // 如果 x 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.4 resolve(x); } }; Promise 写完之后可以通过 promises-aplus-tests 这个包对我们写的代码进行测试,看是否符合 A+ 规范。不过测试前还得加一段代码: // promise.js // 这里是上面写的 Promise 全部代码 Promise.defer = Promise.deferred = function () { let dfd = {} dfd.promise = new Promise((resolve,reject)=>{ dfd.resolve = resolve; dfd.reject = reject; }); return dfd; } module.exports = Promise; 全局安装: npm i promises-aplus-tests -g 终端下执行验证命令: promises-aplus-tests promise.js 上面写的代码可以顺利通过全部 872 个测试用例。 参考: BAT前端经典面试问题:史上最最最详细的手写Promise教程100 行代码实现 Promises/A+ 规范 Promise.resolve Promsie.resolve(value) 可以将任何值转成值为 value 状态是 fulfilled 的 Promise,但如果传入的值本身是 Promise 则会原样返回它。 Promise.resolve = function(value) { // 如果是 Promsie,则直接输出它 if(value instanceof Promise){ return value } return new Promise(resolve => resolve(value)) } 参考:深入理解 Promise Promise.reject 和 Promise.resolve() 类似,Promise.reject() 会实例化一个 rejected 状态的 Promise。但与 Promise.resolve() 不同的是,如果给 Promise.reject() 传递一个 Promise 对象,则这个对象会成为新 Promise 的值。 Promise.reject = function(reason) { return new Promise((resolve, reject) => reject(reason)) } Promise.all Promise.all 的规则是这样的: 传入的所有 Promsie 都是 fulfilled,则返回由他们的值组成的,状态为 fulfilled 的新 Promise;只要有一个 Promise 是 rejected,则返回 rejected 状态的新 Promsie,且它的值是第一个 rejected 的 Promise 的值;只要有一个 Promise 是 pending,则返回一个 pending 状态的新 Promise; Promise.all = function(promiseArr) { let index = 0, result = [] return new Promise((resolve, reject) => { promiseArr.forEach((p, i) => { Promise.resolve(p).then(val => { index++ result[i] = val if (index === promiseArr.length) { resolve(result) } }, err => { reject(err) }) }) }) } Promise.race Promise.race 会返回一个由所有可迭代实例中第一个 fulfilled 或 rejected 的实例包装后的新实例。 Promise.race = function(promiseArr) { return new Promise((resolve, reject) => { promiseArr.forEach(p => { Promise.resolve(p).then(val => { resolve(val) }, err => { rejecte(err) }) }) }) } Promise.allSettled Promise.allSettled 的规则是这样: 所有 Promise 的状态都变化了,那么新返回一个状态是 fulfilled 的 Promise,且它的值是一个数组,数组的每项由所有 Promise 的值和状态组成的对象;如果有一个是 pending 的 Promise,则返回一个状态是 pending 的新实例; Promise.allSettled = function(promiseArr) { let result = [] return new Promise((resolve, reject) => { promiseArr.forEach((p, i) => { Promise.resolve(p).then(val => { result.push({ status: 'fulfilled', value: val }) if (result.length === promiseArr.length) { resolve(result) } }, err => { result.push({ status: 'rejected', reason: err }) if (result.length === promiseArr.length) { resolve(result) } }) }) }) } Promise.any Promise.any 的规则是这样: 空数组或者所有 Promise 都是 rejected,则返回状态是 rejected 的新 Promsie,且值为 AggregateError 的错误;只要有一个是 fulfilled 状态的,则返回第一个是 fulfilled 的新实例;其他情况都会返回一个 pending 的新实例; Promise.any = function(promiseArr) { let index = 0 return new Promise((resolve, reject) => { if (promiseArr.length === 0) return promiseArr.forEach((p, i) => { Promise.resolve(p).then(val => { resolve(val) }, err => { index++ if (index === promiseArr.length) { reject(new AggregateError('All promises were rejected')) } }) }) }) } 后话 能看到这里的对代码都是真爱了,毕竟代码这玩意看起来是真的很枯燥,但是如果看懂了后,就会像打游戏赢了一样开心,而且这玩意会上瘾,当你通关了越多的关卡后,你的能力就会拔高一个层次。用标题的话来说就是:搞懂后,提升真的大。加油吧💪,干饭人 [图片] 噢不,代码人。 作者:大海我来了 感兴趣的小伙伴点击链接,了解详情~ http://github.crmeb.net/u/yi
2021-04-02 - 小程序1rpx边框不完美解决方案
在小程序开发中,1rpx边框随处可见, 像上图UI给的设计稿,如果只是简单使用[代码]border: 1rpx solid red;[代码]的话,在不同的机型上会有不同的表现 [图片] 表现IOS 机型上[图片] Android机型上[图片] 由图片可以看出, IOS机型上会有边框缺失(然而经常出现缺不能稳定复现), 而Android机型上边框比较粗 原因上面这两种表现形式很难联系到一起 首先先看IOS边框缺失的问题,借鉴网络上前辈们的经验 当父元素的高度为奇数,容易出现上下边框缺失,同理宽度为奇数,容易出现左右边框缺失解决办法是在边框内部添加一个1rpx的元素或者伪元素, 撑开内部使父元素的宽高是偶数。 然而我们发现这种方案在Iphone 6等2倍屏可以生效, 但放在如Iphone X等3倍屏下面就很飘了, 还是经常会出现边框缺失的情况, 这种情况下再去把父元素改为2和3共同的倍数就非常不现实了。 再回过头看导致边框缺失的具体原因是啥。 在这之前需要了解下高分屏的物理像素和虚拟像素的概念 简单来说物理像素是设备的实际像素 虚拟像素是设备的坐标点, 可以简单理解为css像素 而rpx类似rem,渲染后实际转换成px之后可能存在小数,在不同的设备上多多少少会存在渲染的问题。而1rpx的问题就更加明显,因为不足1个物理像素的话,在IOS会进行四舍五入,而安卓好像统一向上取整,这也是上面两种设备表现不同的原因。 解决方法我们采用的方法是采用translate:scale(0.5)的方法对边框进行缩放 具体的代码如下 .border1rpx, .border1rpx_before{ position: relative; border-width: 0rpx !important; padding: 0.5rpx; z-index: 0; } .border1rpx::after, .border1rpx_before::before{ content: ""; border-style: inherit; border-color: inherit; border-radius: inherit; box-sizing: border-box !important; position: absolute; border-width: 2rpx !important; left: 0; top: 0; width: 200% !important; height: 200% !important; transform-origin: 0 0; transform: scale(0.5) !important; z-index: -1; } .border1rpx-full { margin: -1rpx; } 给.border1rpx的元素设置边框宽度为0给::after伪元素宽高为两倍,边框设置2rpx,边框其他样式继承元素的设置然后再缩放0.5来达到边框为1rpx的效果 用法基础用法给相应的元素添加border1rpx的class即可, (.borde1rpx说:我们不生产边框,只是边框的搬运工,要显示边框样式的话还需要在元素上自行设置) 圆角边框圆角边框需要自行设置相应伪元素::before 或 ::after的border-raduis值为预期的2倍, 如原本想要设置10rpx的圆角,需要设置[代码].xxx::after{border-raduis: 20rpx;}[代码] 边框内部填充由于设计原因,目标元素会留1rpx的padding用于显示伪元素的边框,如果内部元素是填充的,正常会看到填充元素和目标元素有小部分间隙,此时需要给填充元素添加.border1rpx_full来解决 注意点此方案默认使用::after伪元素实现边框,如果目标元素的after被占用(如iconfont),请使用[代码].border1rpx_before[代码]如单独设置边框(如上边框), [代码]border: 1rpx solid red;border-width: 1rpx 0 0 0;[代码]不能被正确继承,请使用简写[代码]border-top: 1rpx solid red;[代码]由于设计原因,目标元素请最少设置1rpx的padding用于显示边框,(上面的样式已经有了默认的padding,不写也可以, 只是不要用padding:0覆盖)请自行测试点击功能是否正常,防止层级关系导致元素区域被伪元素覆盖
2020-07-23 - 非常简单的长列表(无限上拉触底加载 onReachBottom)实现方案
我们知道小程序针对长列表有两个硬杠杠,一旦越界直接给白屏: 1、setData的数组数据不能超过1M。 2、DOM数不能太多,具体数据未知。 官方这样处理也不无道理,太长了本身性能确实也有问题。所以长列表一定要人为干预处理,不处理一直上拉加载肯定是不行的。 目前主流的处理方法有三种: 1、二维数组,就是把数据改为二维的,每一个分页数据作为一个一维数组的元素。这样处理,只解决了setData的问题,DOM的问题并未解决。并且把本来是一维的数据强行二维化,在很多逻辑处理上变得复杂。 2、官方提供了一个扩展组件recycle-view,但它要求item等高,存在局限性。https://developers.weixin.qq.com/miniprogram/dev/extended/component-plus/recycle-view.html 3、自行搭建骨架屏,类似于方案2的自研版本,根据自己的实际需要编写代码。实现成本非常高。 实现方案如下(太简单了,不提供代码示例): 1、思路:只保留最新的n页数据进行setData,每新加载一页数据,就舍弃最前面一页的数据。同时把第1页的数据保存起来,监听onPageScroll,如果发现用户拉回到了页面顶部,则舍弃所有数据,把第1页的数据setData回来。 2、举个例子:假设n=5,那么当加载了第6页数据时,第6页数据合并到数组尾部,把数组头部的第1页数据去掉,让setData的数据始终保持5页。这里有个细节要处理好,就是去除数据的时候先setData一次,新数据加载合并后,再setData一次,这样可以保证用户的scrollTop不会走位,停留在最新一条数据那个位置。 3、这个方案也存在一些弊端,看你实际项目中能否接受,主要有两个问题:a、用户如果倒着往回逐条浏览,体验是不连续的,因为中间一段我们已经舍弃掉了,如果拉到页面顶部时,将会出现直接回到第一条数据。b、去除数据的setData操作时,存在一定程度的闪屏现象。(针对问题a,应该可以解决,无非就是把数据再逐页塞回来,而不是像我的方案简单粗暴的回到第一页数据,如果项目有需要可以自行尝试。) 4、适用范围:比较适合信息处理类应用,比如后台管理系统。这类应用,往往头几屏内容就能找到信息,或者借助搜索,比较少会拉很多屏。而且往往处理完毕时是直接回到顶部的,不会逐条翻回去。所以这类应用只要保证不出白屏,一些小概率场景下存在一些几乎可以忽略的体验小瑕疵可以接受。信息浏览类应用,比如新闻应用,往往都是长列表浏览,小瑕疵就不一定能接受。
2020-10-28 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11