一.⼩程序和H5的区别是什么呢?

  • 运⾏环境:⼩程序基于微信,⽽h5的运行环境是浏览器,所以小程序中没有DOM和BOM的相关API,jquey和一些NPM(2.2.1版本前)包都不能在小程序中使用。小程序可以调用宿主环境/微信提供的微信客户端的能力,这就使得小程序比普通网页拥有更多的能力(扫一扫、摇一摇)。
  • 系统权限:⼩程序能获得更多的系统权限,如⽹络通信状态、数据缓存能⼒等;
  • 渲染机制:⼩程序的逻辑层和渲染层是分开的,⽽h5页面UI渲染跟javascript的脚本执行都在一个单线程中,互斥,所以h5页面中长时间的脚本运行可能会导致页面失去响应。

⼩程序每次更改data里的数据都得setData、没有像Vue便利的watch监听、不能操作Dom,对于杂乱性场景不太好,之前不支撑npm,不支撑sass,less预编译处理言语。

其实,⼩程序开发过程中我们⾯对的是IOS和Android微信客户端和辅助开发的小程序开发者工具,根据官方文档,这三大运行环境也是有所区别的:

二.⼩程序⽣命周期

关于⼩程序的⽣命周期,可以分为两个部分来理解:应⽤⽣命周期和⻚⾯⽣命周期。

1)应⽤的⽣命周期:

  1. 用户首次打开小程序,触发 onLaunch(全局只触发一次)。
  2. 小程序初始化完成后,触发onShow方法,监听小程序显示。
  3. 小程序从前台进入后台,触发 onHide方法。
  4. 小程序从后台进入前台显示,触发 onShow方法。
  5. 小程序后台运行一定时间,或系统资源占用过高,会被销毁。

另外还有:
onError,程序报错调用。
onPageNotFound ,如果页面不存在了。

前台、后台定义:
当用户点击左上角关闭,或者按了设备 Home 键离开微信,小程序并没有直接销毁,而是进入了后台;当再次进入微信或再次打开小程序,又会从后台进入前台。

2)⻚⾯⽣命周期:

  1. 小程序注册完成后,加载页面,触发onLoad方法。
  2. 页面载入后触发onShow方法,显示页面。
  3. 首次显示页面,会触发onReady方法,渲染页面元素和样式,一个页面只会调用一次。
  4. 当小程序后台运行或跳转到其他页面时,触发onHide方法。
  5. 当小程序有后台进入到前台运行或重新进入页面时,触发onShow方法。
  6. 当使用重定向方法wx.redirectTo(OBJECT)或关闭当前页返回上一页wx.navigateBack(),触发onUnload
 onLoad: function(options) { // 页面加载完毕触发 },onUnload: function() { // 页面卸载触发 },onShow: function() { // 页面开始渲染触发 },onHide: function() { // 页面切换到后台触发 },onReady: function() { // 页面渲染完毕触发 },onPullDownRefresh: function() { // 页面下拉刷新触发},onReachBottom: function() { // 页面上滑触底触发 },onShareAppMessage: function () {// 页面点击转发触发,需要return一个object,只有定义了此方法,才会有转发的功能return {title: "分享的页面", // 分享页面的标题path: "/pages/logs/logs" // 分享的页面的路径}},onPageScroll: function(options) { // 页面滚动触发 }

onLoad和onShow的区别:
①onLoad先于onShow执行
②onshow在每次打开页面都会加载数据,可以用于数据在需要刷新的环境下(整个生命周期里,可执行多次)
onload只是在第一次进入页面会刷新数据,从二级页面回来不会重新加载数据 (整个生命周期里,只执行一次)
③获取参数并且只请求一次的事件放在 onLoad 里。
当前页面需要时时刷数据的请求多次的事件放在 onShow 里。

3)应用生命周期影响页面生命周期

  1. 小程序初始化完成后,页面首次加载触发onLoad,只会触发一次。
  2. 当小程序进入到后台,先执行页面onHide方法再执行应用onHide方法。
  3. 当小程序从后台进入到前台,先执行应用onShow方法再执行页面onShow方法。

三.页面跳转-路由

(1)wx.navigateTo (可带参)
(2)wx.navigateBack
(3)wx.redirectTo
(4)wx.switchTab
(5)wx.reLaunch

1、wx.navigateTo:保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面。使用 wx.navigateBack 可以返回到原页面。小程序中页面栈最多十层。(可带参)
2、wx.navigateBack :关闭当前页面,返回上一页面或多级页面。可通过 getCurrentPages 获取当前的页面栈,决定需要返回几层。(没有url属性,不可带参)
3、wx.redirectTo:关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面。(可带参)
4、wx.switchTab:跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面(不可带参)
5、wx.reLaunch:关闭所有页面,打开到应用内的某个页面(可带参)

总结:switchTab这种导航方式,不能带参,也就是说不能通过url进行传参,解决方法就是如果要跳转到tab页面,使用reLaunch方法

(1)wx.navigateTo

只能打开非tabBar页面
描述:
用于保留当前页面、跳转到应用内的某个页面,使用 wx.navigateBack可以返回到原页面。对于页面不是特别多的小程序,通常推荐使用 wx.navigateTo进行跳转, 以便返回原页面,以提高加载速度。当页面特别多时,则不推荐使用。

案例:
wx.navigateTo({ url: ‘pageD’ }) 可以往当前页面栈多推入一个 pageD,此时页面栈变成 [ pageA, pageB, pageC, pageD ]

拓展:
可通过 getCurrentPages() 获取当前的页面栈
wx.navigateTo验证页面栈最大10层限制

(2)wx.navigateBack

只能打开非tabBar页面
描述:关闭当前页面,返回上一页面或多级页面。可通过getCurrentPages() 获取当前的页面栈,决定需要返回几层。

拓展:
①wx.navigateBack可以结合delta参数实现多级退回。
api的参数只有delta,表示返回的页面数。若 delta的取值大于现有可返回页面数时,则返回到用户进入小程序的第一个页面。若不填delta的值时,就默认其为1(注意,默认值并非取0),即返回上一页面。

返回上一页:

backTo(){wx.navigator({delta:1})
}
//或
backTo(){wx.navigator({})
}

②结合页面栈返回pageA基层页面栈首页

//返回页面栈pageA基层页面
backTo(){const page = getCurrentPages();//获取当前页面栈wx.navigator({delta:page.length - 1})
}

注意:若关闭多层页面栈,是从最新页面往旧页面依次关闭,关到基层为止。

(3)wx.redirectTo

只能打开非tabBar页面
描述:关闭当前页面,跳转到应用内的某个页面。页面重定向,将页面重新定向到一个目标页面,并不能返回到上一个页面。

注意:wx.redirectTo和wx.navigateTo的区别。
wx.redirectTo()用于关闭当前页面,跳转到应用内的某个页面。当页面过多时,被保留页面会挤占微信分配给小程序的内存,或是达到微信所限制的10层页面栈。这时应该考虑选择 wx.redirectTo。

优缺点:这样的跳转可以避免跳转前页面占据运行内存,但返回时页面需要重新加载,增加了返回页面的显示时间。

(4)wx.switchTab

只能跳转到带有tab的页面
描述:Tab切换,跳转的页面必须是app.json 中 tabBar 配置的页面。跳转到tabBar页面同时关闭其他非tabBar页面

(5)wx. reLaunch

描述:关闭所有页面,打开到应用内的某个页面。即重新启动, 可以打开任意页面。
wx.reLaunch()与 wx.redirectTo()的用途基本相同, 只是 wx.reLaunch()先关闭了内存中所有保留的页面,再跳转到目标页面(此时页面栈变为[pageA]仅仅1层)。

小结:

  • wx.navigateTo为打开新页面,会增加页面栈大小,直到页面栈大小为10
  • wx.navigateBack为页面回退,会减少页面栈大小,直到页面栈大小为1
  • wx.redirectTo为页面重定向,不会增加页面栈大小
  • wx.switchTab为跳转tabBar页面专用API
  • wx. reLaunch为重新启动,关闭所有页面,可以打开任意页面
  • 跳转tabBar页面:对于跳转到 tab bar 的页面,最好选择 wx.switchTab(),它会先关闭所有非 tab bar 的页面。其次,也可以选择 wx.reLaunch(),它也能实现从非 tab bar 跳转到 tab bar,或在 tab bar 间跳转,效果等同 wx.switchTab()。使用其他跳转 API 来跳转到 tab bar,则会跳转失败。

四.什么是页面栈?

首先先来了解一下微信小程序的运行环境:
小程序的运行环境分成渲染层和逻辑层,其中 WXML 模板和 WXSS 样式工作在渲染层,JS 脚本工作在逻辑层。
小程序的渲染层和逻辑层分别由2个线程管理:渲染层的界面使用了WebView 进行渲染;逻辑层采用JsCore线程运行JS脚本。一个小程序存在多个界面,所以渲染层存在多个WebView线程,这两个线程的通信会经由微信客户端做中转,逻辑层发送网络请求也经由Native转发,小程序的通信模型下图所示。

我们可以看到,一个页面使用一个 WebView 线程进行渲染。如果打开10个页面,则会开启 10 个 WebView 线程,此时内存中的十个webView线程我们称之为页面栈。当然小程序也会对这块内存做限制,目前页面栈的限制是不能超过十条。在小程序中页面的路由是小程序框架本身控制的我们不要去手动管理, 小程序框架通过一个页面栈的设计来管理所有的界面,当发生路由跳转时,页面栈就会做出相应的变化,在小程序页面中通过 getCurrentPages() 就可以获取到当前的页面栈。

五.页面路由触发方式及页面生命周期函数的对应关系

案例:以 A、B 页面为 Tabbar 页面,C 是从 A 页面打开的页面,D 页面是从 C 页面打开的页面

六.页面传值

1. 跨页面传值

1)用 navigator标签传值或 wx.navigator

注:后面为你要传递的值
一个参数:detail页面的 onload方法内接收

<navigator url="../detail/detail?good_id=16"></navigator>
onLoad:function(options){this.setData({good_id:options.good_id})
}

如果需要传多个参数, 用 & 链接即可

<navigator url="../detail/detail?good_id=16&good_price=1.88&good_name=哈哈哈"></navigator>
onLoad:function(options){this.setData({good_id: options.good_id,good_price: options.good_price,good_name: options.good_name})
}

如果要传 数组, 字典等复杂类型, 要先用 JSON.stringify() 转成字符串传递.

2)用getCurrentPages(); 获取栈中全部页面的, 然后把数据写入相应页面

onshow() 方法中接受, 因为再次返回只执行onshow()方法.

在onShow里面把数据渲染到页面上

这样就把 address 传递并接受了

注 :
①这里 这个 route 是一个属性,在最新的小程序中,已经可以用 route 这个属性替换,即图中的
arr[arr.length - 2].route 与 arr[arr.length - 2].route 等效。
②这个方法适合 往后面传值(即已经存在的页面), 这样才能在栈中找到并主动写入数据, 且 一定要在 onshow() 方法中接受, 因为再次返回只执行onshow()方法.

3)使用缓存 wx.setStorage/wx.getStorage(异步接口)等, 小程序中对写入本地数据 封装了很多方法, 各有侧重



注:这里还有一个同步 wx.setStorageSync和wx.getStorageSync,使用方法一样。看到这里大家应该注意到,结尾有Sync的为同步

同异步区别:
1、同步方法会堵塞当前任务,直到该同步的方法处理结束。
2、异步方法不会塞当前任务。

4)把 数据声明为全局变量

在 app.js 中定义全局变量

globalData: {userInfo: null,globalName:"lhs"}

在其他页面可以取到全局变量

//在其他页面的js文件里
let app = getApp();
console.log(app.globalData.globalName)

2.页内传值

1)设置id的方法标识跳转后传递后的参数


bindtap定义的点击方法swiperTap : function(e)中获取

let id = e.currentTarget.id;

2)设置 data-xxx 的方法来标识要传递的值


注 : 这里 data-index="{{index}}"里的 {{index}}是有效的, 在用wx-for 渲染视图层时, index 代表点击的下标. 在bindtap定义的点击方法 swiperTap : function(e) 中获取, 即

let index = e.currentTarget.dataset.index;

3)form表单和input输入框

form表单:

input输入框:


获取input的值:
1、获取input输入的值 wxml代码如下

  <input bindinput="bindModel" type="text" class="input" placeholder="请输入兑换码" value='{{exchangeCode}}' data-model="exchageCode"/>

2、js代码

 data: {exchangeCode:"",},bindModel(e) {let key = e.target.dataset.model;this.setData({[key]: e.detail.value});},

七.⼩程序架构 - 双线程模型

1.两个线程

双线程就是渲染层(wxml、wxss)和逻辑层(js)的分离:

  • 渲染层:WXML 模块和 WXSS 样式运行于渲染层,渲染层使用 WebView 线程进行渲染;因为一个小程序往往不只有一个页面,一个小程序通常会有多个页面,所以就会使用多个 WebView 的线程
  • 逻辑层: js 脚本(app.js/home.js)等运行于逻辑层,逻辑层使用 JsCore 运行js 脚本

1)页面渲染的具体流程

逻辑层把数据变化通知到视图层,触发视图层页面更新;视图层把触发的事件通知到逻辑层,进行业务处理。
在渲染层,运行环境会把WXML转化对应的JS对象,在逻辑层发生数据变变更的时候,我们需要通过运行环境提供的setData方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的Ui界面

2)小程序的数据驱动基本原理:

WXML可以先转成JS对象,然后再渲染出真正的DOM树。
通过setData把msg数据从“Hello World”变成“Goodbye”,产生的JS对象对应的节点就会发生变化,此时可以对比前后两个JS对象变化的部分,然后把这个差异应用到原来的DOM树上,从而达到更新UI的目的,这就是“数据驱动”的原理

2.设计的原因

这样设计的原因是,为了管控和安全,防止开发者使用一些浏览器提供的,诸如跳转页面,操作DOM,动态执行脚本的开放性接口。
于是微信小程序通过提供一个沙箱环境来执行开发者们的js代码来解决。这个沙箱环境只提供纯js的解析环境,没有任何浏览器相关接口。

3.如何动态更改界面

把开发者的js逻辑代码放到单独的线程去运行,但在WebView线程里,开发者就没法直接操作DOM。
那如何实现动态更改界面呢?
逻辑层和渲染层的通信会由Native(微信客户端)做中转,逻辑层发送网络请求也经由Native转发。这说明可以把DOM的更新通过简单的数据通信来实现。
Virtual DOM 过程:用 JS 对象模拟 DOM 树 -> 比较两棵虚拟 DOM 树的差异 -> 把差异应用到真正的 DOM 树上。

双线程模型是小程序框架五业界大多数前端 Web框架不同之处,基于这个模型,可以更好地管控以及提供更安全的环境。

八.小程序的基础库

小程序的基础库是 JavaScript 编写的,它可以被注入到渲染层和逻辑层运行。主要用于:
①在渲染层,提供各类组件来组建界面的元素
②在逻辑层,提供各类 API 来处理各种逻辑
③处理数据绑定、组件系统、事件系统、通信系统等一系列框架逻辑

由于小程序的渲染层和逻辑层是两个线程管理,两个线程各自注入了基础库。小程序的基础库不会被打包在某个小程序的代码包里边,它会被提前内置在微信客户端。这样可以:
①降低业务小程序的代码包大小
②可以单独修复基础库中的 Bug,无需修改到业务小程序的代码包

Exparser 框架
Exparser 是微信小程序的组件组织框架,内置在小程序基础库中,为小程序的各种组件提供基础的支持。小程序内的所有组件,包括内置组件和自定义组件,都由 Exparser 组织管理。Exparser 特点包括:
①基于 Shadow DOM 模型:模型上与 WebComponents 的 ShadowDOM 高度相似,但不依赖浏览器的原生支持,也没有其他依赖库;实现时,还针对性地增加了其他API以支持小程序组件编程。
②可在纯JS环境中运行:这意味着逻辑层也具有一定的组件树组织能力。
③高效轻量:性能表现好,在组件实例极多的环境下表现尤其优异,同时代码尺寸也较小。

九.运⾏机制与更新机制

1.运⾏机制

小程序发动会有两种状况,一种是「冷发动」,一种是「热发动」

  • 热启动:假如⽤户已经打开过某⼩程序,然后在⼀定时间内再次打开该⼩程序,此时⽆需重新启动,只需将后台态的⼩程序切换到前台,这个过程就是热启动;
  • 冷启动:⽤户⾸次打开或⼩程序被微信主动销毁后再次打开的情况,此时⼩程序需要重新加载启动,即冷启动。
  • 销毁:小程序进入后台后,(5分钟内)没有立即销毁,5分钟后可能会被销毁,如果小程序 占用系统资源过高,持续占用资源,会被系统销毁或被微信的客户端主动的回收.

小程序没有重启的概念。

2.更新机制

小程序冷发动时假设发现有新版本,将会异步下载新版本的代码包,并一起用客户端本地的包进行发动,即新版本的小程序需求等下一次冷发动才会应用上。
假设需求马上应用最新版本,能够运用wx.getUpdateManagerAPI进行处理。

十.小程序框架的对比


小结:

  • 原生小程序也是有一些新的技术和语法,
  • 对于公司有vue技术储备的,可以用wepympvuewepy相对更有历史一些,更成熟,mpvue很久没有在维护了,不太推荐
  • 有react技术储备的,可以用taro

十一.原生小程序和wepy不同的数据绑定方式

1.原生小程序的数据绑定方式

原生小程序通过Page提供的setData方法来绑定数据,如:

this.setData({title: 'this is title'});

2.wepy数据绑定方式

WePY使用脏数据检查对setData进行封装,在函数运行周期结束时执行脏数据检查,一来可以不用关心页面多次setData是否会有性能上的问题,二来可以更加简洁去修改数据实现绑定,不用重复去写setData方法。

1)简单更新:

this.title = 'this is title';

2)异步更新

需注意的是,在异步函数中更新数据的时候,必须手动调用$apply方法,才会触发脏数据检查流程的运行。如:

setTimeout(() => {this.title = 'this is title';this.$apply();
}, 3000);

3)标识

在执行脏数据检查时,会通过this.$$phase标识当前检查状态,并且会保证在并发的流程当中,只会有一个脏数据检查流程在运行

4)微信小程序之 this.data和this.setData{()}区别

  • this.setData({})不仅会改变数据,而且还会改变视图。用于将数据从逻辑层发送到视图层(异步),同时改变对应的this.data的值(同步)
  • this.data会造成数据会变,但是视图并不会更新

总结一下就是:this.data与this.setData的关系就是this.setData里面存储的是this.data的副本,而界面是从this.setData里面托管的this.data的副本取数据的。所以我们更改this.data并不会直接更新界面,因为这个时候的this.setData里面的副本还是没有更新前的。

十二.setData

1.setData 工作原理

小程序的视图层目前使用 WebView 作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebView 和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。
而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。

2.常见的 setData 操作错误

  1. 频繁的去 setData。
    在我们分析过的一些案例里,部分小程序会非常频繁(毫秒级)的去setData,其导致了两个后果:Android 下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为 JS 线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层;
    渲染有出现延时,由于 WebView 的 JS 线程一直处于忙碌状态,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不实时;

  2. 每次 setData 都传递大量新数据。
    由setData的底层实现可知,我们的数据传输实际是一次 evaluateJavascript 脚本过程,当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程,

  3. 后台态页面进行 setData。
    当页面进入后台态(用户不可见),不应该继续去进行setData,后台态页面的渲染用户是无法感受的,另外后台态页面去setData也会抢占前台页面的执行。

图片资源:
目前图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面。

图片对内存的影响:
在 iOS 上,小程序的页面是由多个 WKWebView 组成的,在系统内存紧张时,会回收掉一部分 WKWebView。从过去我们分析的案例来看,大图片和长列表图片的使用会引起 WKWebView 的回收

图片对页面切换的影响:
除了内存问题外,大图片也会造成页面切换的卡顿。我们分析过的案例中,有一部分小程序会在页面中引用大图片,在页面后退切换中会出现掉帧卡顿的情况。当前我们建议开发者尽量减少使用大图片资源

代码包大小的优化:
小程序一开始时代码包限制为 1MB,但我们收到了很多反馈说代码包大小不够用,经过评估后我们放开了这个限制,增加到 2MB 。代码包上限的增加对于开发者来说,能够实现更丰富的功能,但对于用户来说,也增加了下载流量和本地空间的占用。开发者在实现业务逻辑同时也有必要尽量减少代码包的大小,因为代码包大小直接影响到下载速度,从而影响用户的首次打开体验。除了代码自身的重构优化外,还可以从这两方面着手优化代码大小:

控制代码包内图片资源:
小程序代码包经过编译后,会放在微信的 CDN 上供用户下载,CDN 开启了 GZIP 压缩,所以用户下载的是压缩后的 GZIP 包,其大小比代码包原体积会更小。 但我们分析数据发现,不同小程序之间的代码包压缩比差异也挺大的,部分可以达到 30%,而部分只有 80%,而造成这部分差异的一个原因,就是图片资源的使用。GZIP 对基于文本资源的压缩效果最好,在压缩较大文件时往往可高达 70%-80% 的压缩率,而如果对已经压缩的资源(例如大多数的图片格式)则效果甚微。

及时清理没有使用到的代码和资源:
在日常开发的时候,我们可能引入了一些新的库文件,而过了一段时间后,由于各种原因又不再使用这个库了,我们常常会只是去掉了代码里的引用,而忘记删掉这类库文件了。目前小程序打包是会将工程下所有文件都打入代码包内,也就是说,这些没有被实际使用到的库文件和资源也会被打入到代码包里,从而影响到整体代码包的大小。

十三.性能优化

主要的优化策略可以归纳为三点:

  • 精简代码,降低WXML结构和JS代码的复杂性;
  • 合理使用setData调用,减少setData次数和数据量;
  • 必要时使用分包优化,小程序分包总结

十四. 如何做到侦听数据改变?

监听器的原理,是将data中需监听的数据写在watch对象中,并给其提供一个方法,当被监听的数据的值改变时,调用该方法。​​
我们需要用到Javascript中的Object.defineProperty()方法,来手动劫持对象的getter/setter,从而实现给对象赋值时(调用setter),执行watch对象中相对应的函数,达到监听效果。

Object.defineProperty()方法,会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

十五.多次setData时候,通信的次数是一次还是多次?

如果次数过多,应该页面会被卡死,数据可能成功,但页面出不来。
建议:把所有要 setData 的数据都存放进对象里面,等执行的差不多的时候再一次性通过 setData 方法把数据更新过去

  • 在一次渲染周期内,收到多次setData的话,只会渲染一次
  • jscore -> native -> webview

JsCore全称为JavaScriptCore ,是一款 JavaScript 引擎,通常会被叫做虚拟机
WebView也就是我们熟悉的“网络视图”,能加载并显示网页,可以将其视为一个浏览器。

十六. 如何优化小程序数据通信,提升页面性能

  • 减少setData的调用,合并多个setData
  • 与界面渲染无关的数据最好不要设置在data
  • 有些数据不在页面中展示,包含复杂数据结构或者超长字符串,则不应该使用setData来设置这些数据

十七.小程序性能优化的几点实践技巧

1)存在setData的数据过大,如何解决?

我们的功能里面有个滚动到底部加载的功能,优化前我们的做法是这样的


<!--只阐述逻辑,非真实代码-->// 1: 初始一个list,存储列表数据
data = startList
// 2: 监听滚动事件,滚动到底部获取新数据,并追加到list尾部,最后重新setData
onReachBottom:()=>{const {list} = this.datafetchNewData().then((res)=>{list.push(res.list);this.setData({list})}

这样会导致list的数据会越来越大,每次setData的数据就会越来越多,因而每次页面重新渲染的节点就会越来越多,从而导致滚动到后面,加载越来越慢。而且小程序本身对数据大小也有限制,不能超过1M。

怎么解决呢?
小程序setData里面的key支持数据路径的写法,比如

let o = obj;
this.setData({'o.属性':value
})或者
let a = array;
this.setData({'array[0].text':value
})

所以我们可以通过数据路径的写法,来将数据分批的传输到视图层中,减少一次性setData的数据大小。具体写法如下

// 1.通过一个二维数组来存储数据
let feedList = [[array]];// 2.维护一个页面变量值,加载完一次数据page++
let page = 1// 3.页面每次滚动到底部,通过数据路径更新数据
onReachBottom:()=>{fetchNewData().then((newVal)=>{this.setData({['feedList[' + (page - 1) + ']']: newVal,})}
}
// 4.最终我们的数据是[[array1],[array2]]这样的格式,然后通过wx:for遍历渲染数据

2)存在短时间内发起太多图片请求,如何解决?

图片懒加载
微信提供了IntersectionObserver对象。IntersectionObserver 对象,用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。
通过这个api我们不用再主动去监听元素位置了,在页面渲染一开始,通过这个api指明需要监听的元素,系统会自动去监听了元素位置。

intersectionRatio值大于0,说明元素出现在视图中了,重新setData数据,显示图片组件。


let data = list;<img class="img-{{index}}" wx:for="{{data}}"></img>data.forEach((item,index)=>{this.createIntersectionObserver().relativeToViewport.observe(`.img-${index}`,res=>{if (res.intersectionRatio > 0){this.setData({item.imgShow:true})}})

3)存在图片太大而显示区域过小

CDN图片处理

对于页面里面的图片,最好都把图片存储在cdn服务器上。一个是能充分利用cdn缓存来加快请求速度,另外一个就是cdn上能够将图片进行一定的处理,比如裁剪。我就是通过cdn来响应图片处理,然后请求图片时告诉cdn服务器需要什么要的尺寸图片,由cdn服务器响应对应尺寸图片。

4)key值在列表渲染中的作用

key值在列表渲染的时候,能够提升列表渲染性能。

小程序的页面是渲染:
①将wxml结构的文档构建成一个vdom虚拟数
②页面有新的交互,产生新的vdom数,然后与旧数进行比较,看哪里有变化了,做对应的修改(删除、移动、更新值)等操作
③最后再将vdom渲染成真实的页面结构

key值的作用就在第二步,当数据改变触发渲染层重新渲染的时候,会校正带有 key 的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。

十八. 还有其他哪些地方是可以放置无关数据

全局变量,常量,本地缓存,存于后端数据等

十九. 为什么data设置长字符串,不显示也会影像页面性能

setData的时候会调evaluateJavascript这个函数解析传入的数据,会带来webView中js引擎资源的占用

1.举一反三

问题:后台返回数据中有可能包含了大量的无用数据,数据量如果过大时候会对小程序渲染界面有影响吗?
回答:有。

2.了解下setData 背后的工作原理:

程序的视图层目前使用 WebView作为渲染载体,而逻辑层是由独立的 JavascriptCore 作为运行环境。在架构上,WebViewJavascriptCore都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluateJavascript所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立环境。

而 evaluateJavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间。

所以最好是新建一个tempData,将要的数据取出来之后再setData这个tempData,以此来提高微信小程序的页面渲染速度,提升微信小程序运行效率,优化微信小程序的用户体验。

二十. wepy 如何做数据绑定优化的

  • wepy内部实现了一个脏数据检查机制,函数执行完成之后 -> data-check
  • newValue 和 oldValue做比较,如果有变化,就会加入到readyToSet的队列中,最后统一做一个setData
  • 同一时间只允许一个脏值检查流程进行

二十一.wepy在小程序性能调优

1.预先加载和预查询数据

传统H5在启动时,page1.html 只会加载 page1.html 的页面与逻辑代码,当page1.html 跳转至 page2.html 时,page1 所有的 Javascript 数据将会从内存中消失。page1 与 page2 之间的数据通信只能通过 URL 参数传递或者浏览器的 cookie,localStorge 存储处理。

小程序在启动时,会直接加载所有页面逻辑代码进内存,即便 page2 可能都不会被使用。在 page1 跳转至 page2 时,page1 的逻辑代码 Javascript 数据也不会从内存中消失。page2 甚至可以直接访问 page1 中的数据。

对于上述问题,WePY 中封装了两种概念去解决:
扩展了生命周期,添加了onPrefetch事件,会在 redirect 之时被主动调用。同时给onLoad事件添加了一个参数,用于接收预加载或者是预查询的数据:

// params
// data.from: 来源页面,page1
// data.prefetch: 预查询数据
// data.preload: 预加载数据
onLoad (params, data) {}

1)预加载数据

用于 page1 主动传递数据给 page2,比如 page2 需要加载一份耗时很长的数据。我可以在 page1 闲时先加载好,进入 page2 时直接就可以使用。
预加载数据示例:

// page1.wpy 预先加载 page2 需要的数据。methods: {tap () {this.$redirect('./page2');}
},
onLoad () {setTimeout(() => {this.$preload('list', api.getBigList())}, 3000)
}// page2.wpy 直接从参数中拿到 page1 中预先加载的数据
onLoad (params, data) {data.preload.list.then((list) => render(list));
}

2)预查询数据

用于避免于 redirecting 延时,在跳转时调用 page2 预查询。
预查询数据示例:

// page1.wpy 使用封装的 redirect 方法跳转时,会调用 page2 的 onPrefetch 方法
methods: {tap () {this.$redirect('./page2');}
}// page2.wpy 直接从参数中拿到 onPrefetch 中返回的数据
onPrefetch () {return api.getBigList();
}
onLoad (params, data) {data.prefetch.then((list) => render(list));
}

2.数据绑定

优化了setData的数据绑定,不管中间进行了什么操作,在运行结束时执行一次脏检查,对需要设置的数据进行setData操作。
注意:在开发过程中,一定要避免同一流程内多次 setData 操作,否则非常的损耗性能。

二十二.wepy中异步数据如何更新

  • $apply()
setTimeout(() => {this.label = 'label';this.$apply();
})

二十三文件分布

wepy 主文件 .wpy => style + template + script

二十四.网络层 - 拦截器 (原生wx.request)

但wepy有一套自建的api拦截器

// config + success + fail + complete
this.intercept('request', {config() {},success() {}
})

针对拦截请求的相关面试题

  1. 项目中如何拦截请求做预处理(额外增加请求参数、增加时间戳)
  2. 请求返回后如何对数据进行加工(判断超时、返回参数增加)

二十五.结构层 - mixin混合

wepy实现了同vue一样,可以复用抽离的方式:

  • 默认式混合 - data/components
  1. page和mixin都定义了参数a, 看page
  2. mixin中定义了,page中未定义的变量b,看mixin
  • 兼容式混合 - methods
  1. 先响应组件本身的事件,再相应mixin中的事件

相关面试题考察:(事件)响应顺序是如何的?
mixin事件的响应顺序和vue是相反的。vue:先执行mixin函数,再执行组件本身函数;而兼容性混合中,先响应了组件本身,在响应混合事件。

二十六.结构层 - 组件

wepy的组件化 < = > js原生模块化

  • bindtap = “handleClick” 模板a和模板b同样都绑定了click方法,不利于后期维护
a.wpy- data: {d: d}- methods: m()b.wpy- data: {d: d}- methods: m()// 编译后
a.wxml + a.wxss + a.js- data: $a.$d- methods: $a.$m()b.wxml + b.wxss + b.js- data: $b.$d- methods: $b.$m()
  • import引入,components
  • 实现了循环module
  • computed, watcher, props, $broadcast, $emit, slot

相关面试题:

  1. wepy如何实现的组件化(如上)
  2. wepy组件有何特殊性 or wepy组件化过程中框架的不足
  • wepy中的组件都是 静态组件 => 组件Id唯一标识一个组件实例 => 在同一个页面中无法独立引用多个相同id或者相同名字的组件
// wepy中的错误示范
<template><view><comp></comp></view><view><comp></comp></view>
</template>
<script>import comp from './comp';// ……
</script>// 正确示范
<template><view><comp></comp></view><view><newComp></newComp></view>
</template>
<script>import comp from './comp';// ……// {//    comp: comp,//    newComp: comp// }
</script>
  1. list循环渲染 or repeat循环有何不足
  • 不支持在repeat中使用props、computed、watch等
// error
// list.wpy
<view>{{ item.name }}</view>// index.wpy
<repeat for="{{ list }}"><list :item.sync="item">
</repeat>// ok
// list.wpy
<repeat for="{{ list }}"><view>{{ item.name }}</view>
</repeat>// index.wpy
<list :item.sync="item">// 原因还是静态组件

二十七.wepy源码分析

  1. 编译流程
// 入口
// wepy-cli/src/compile.js

适配器 - adapter
node => newNode

  1. 数据同步实际代码的实现
components.js
wepy.js$$phase - 是否有脏值检查正在运行
$apply - 应用更新(1. 通过phase做状态检查 2. 调用$digest做值统一更新)
$digest - 遍历originData,做脏值对比,如果值有更新,放到readyToSet。循环之后统一调用setData

二十八.登录鉴权

1.web:

登录通过输入用户名和密码返回token,后面用Token去请求其他接口

2.小程序:

  • 静默登录:新用户,无感知
  • 用户登录

1)静默登录

参考文档:微信小程序登录授权详解

①通信流程

三者之间的通信:小程序(前端),dev_svr本地开发的服务,wx_svr第三方微信的服务

  1. 小程序端直接调wx.login()接口,获取code他就是这次登录的标识码,把和这个code传给本地的svr。需要注意的是,这个code只能获取一次,如果后面超时的话,只能重新后面的交互请求,这个 code不能重新获取;
  2. 本地的服务端将code + appId + appSecret与微信的服务端交互;
  3. 微信的服务端会返回session_key + openId给本地的svr标识当前通话的独立性
    openId:用户在当前微信小程序下的唯一标识
    session_key :本次会话的密钥
  4. 本地svr根据收到的code + session_key + openId与本地业务的登录态token做关联,并且将生成的token传给前端。
  5. 前端根据获取到的token存入storage缓存,并且在后面的请求中带上token

静默登录失败了是访客态,成功时游客态

②code 换取 session_key

接口地址:
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
请求参数:

返回参数:

③wx.checkSession 检测当前用户登录态是否有效

后续用户重新进入小程序,调用wx.checksession()监测登录状态,如失效,则重新发起登录流程。

wx.checkSession({success: function(){//session 未过期,并且在本生命周期一直有效},fail: function(){//登录态过期wx.login() //重新登录....}
})

2)用户登录

①两种登录

分为两种:一个是微信授权登录(手机号登录),另一个是密码登陆
手机号登录的getPhoneNumber()方法是必须绑定在Button按钮上才能触发

用户登录了是会员态

②授权 wx.getSetting

用户可以在小程序设置界面(右上角 - 关于 - 右上角 - 设置)中控制对该小程序的授权状态。
开发者可以使用 wx.getSetting 获取 用户当前的授权状态。
object参数说明:

// 先查询一下用户是否授权了 "scope.record" 这个 scope
wx.getSetting({success(res) {if (!res.authSetting['scope.record']) {wx.authorize({scope: 'scope.record',success() {// 用户已经同意小程序使用录音功能,后续调用 wx.startRecord 接口不会弹窗询问wx.startRecord()}})}}
})

③获取用户信息getUserInfo和getPhoneNumber

wx.getUserInfo
获取用户信息,withCredentials 为 true 时需要先调用 wx.login 接口。
success返回参数说明:

注:当 withCredentials 为 true 时,要求此前有调用过 wx.login 且登录态尚未过期,此时返回的数据会包含 encryptedData, iv 等敏感信息;当 withCredentials 为 false 时,不要求有登录态,返回的数据不包含 encryptedData, iv 等敏感信息。
userInfo参数说明:

示例代码:

wx.getUserInfo({success: function(res) {var userInfo = res.userInfo
var nickName = userInfo.nickName
var avatarUrl = userInfo.avatarUrl
var gender = userInfo.gender //性别 0:未知、1:男、2:女
var province = userInfo.province
var city = userInfo.city
var country = userInfo.country
}
})

getPhoneNumber
因为需要用户主动触发才能发起获取手机号接口,所以该功能不由 API 来调用,需用 组件的点击来触发。
使用步骤:
需要将 组件 open-type 的值设置为 getPhoneNumber。当用户点击并同意之后,可以通过 bindgetphonenumber 事件回调获取到微信服务器返回的加密数据, 然后在第三方服务端结合 session_key 以及 app_id 进行解密获取手机号。

在回调中调用 wx.login 登录,可能会刷新登录态。此时服务器使用 code 换取的 sessionKey 不是加密时使用的 sessionKey,导致解密失败。建议开发者提前进行 login;或者在回调中先使用 checkSession 进行登录态检查,避免 login 刷新登录态。

代码示例:

<button open-type="getPhoneNumber" bindgetphonenumber="getPhoneNumber"> </button>
Page({getPhoneNumber: function(e) {console.log(e.detail.errMsg)
console.log(e.detail.iv)
console.log(e.detail.encryptedData)
}
})

返回参数说明:

encryptedData 解密后为以下 json 结构,包含信息:

getUserInfogetUserProfile的区别?

微信官方阉割了 wx.getUserInfo 接口,前端需要使用新的接口 wx.getUserProfile 替代

不推荐使用wx.getUserInfo<button open-type="getUserInfo"/>
a.将不再弹出弹窗
b.不可获取用户个人信息(头像、昵称、性别与地区),将直接获取 匿名数据(包括userInfoencryptedData中的用户个人信息)
c.获取加密后的 openID 与 unionID 数据的能力不做调整。
新增并推荐使用 wx.getUserProfile
a.产生 点击事件(例如 button 上 bindtap 的回调中)后才可调用
b.每次请求都会弹出授权窗口
c.用户同意后返回 userInfo
d. 返回的加密数据中 不包含 openId 和 unionId 字段!这一点很重要!
使用 wx.getUserProfile的两个前提条件:
1.开发者工具版本不低于 1.05.2103022
2.基础库版本不低于 2.10.4

所以说:
①使用wx.login()获取unionid和code
②使用getUserProfile弹出信息个人授权,获取用户信息

开发调用的流程有何区别?

二十九.小程序中h5的缓存

三十.支付宝小程序 & 微信小程序

(一)、 app.json

(1) 小程序的通用设置:状态栏、导航条、标题、窗口背景色

  • 微信
  window: {"backgroundTextStyle": "light",// ……}
  • 支付宝
  window: {"default": "light",// ……}

(2)tabBar

  • 区别
  tabBar: {// 支付宝items: [],// 微信list: []}

(二)、pages

(1)文件名

  • 支付宝: axml + acss
  • 微信: wxml + wxss

(2)视图页面axml、wxml

a. 事件

  • 支付宝 onTap\catchTap
  • 微信 bindtap\catchtouchstart

b. 列表的渲染

  • 支付宝
  a:for="{{list}}"key="item-{{index}}" index="index"
  • 微信
  wx:for="{{list}}"wx:key="key" wx-for-item="item"

c. 条件渲染

  • 支付宝
  a:ifa:elsea:esleif
  • 微信
  wx:ifwx:elsewx:esleif

(三)、组件的不同

1. showToast

  • 支付宝
  my.showToast({})
  • 微信
  wx.showToast({})

2. showLoading

  my.showLoading({})
  • 微信
  wx.showLoading({})

3. request网络请求

  • 支付宝
my.httpRequest({url: '',method: '',data: {},header: '',dataType: '',success: function() {},fail: function() {}
})
  • 微信
wx.request({url: '',method: '',data: {},header: '',dataType: '',success: function() {},fail: function() {}
})

4. 支付

  • 支付宝
  my.tradePay({tradeNO: '47983279478923797057247185',success: res => {},fail: res => {}})
  • 微信
  wx.requstPayment({package: 'pre_pay_id',signType: 'MD5',paySign: '',success: res => {},fail: res => {}})

5. 获取code

  • 支付宝
my.getAuthCode({success() {}
})
  • 微信
wx.login({success() {}
})

三十一.坑

1. 自定义tabbar在页面存在下拉更新(scrollview)的时候,页面被下拉,tabbar也会跟着下拉。

  • 方案: 提前沟通

2. require在小程序中不支持绝对路径,只能用相对路径去选取’…/…/…/utils/tool.js’

  • 方案:
    全局的js封装:
  App({require: function($uri) {return require($url);}})

组件中使用:

  const Api = app.require('utils/tool.js');

利用require返回uri带上/

3. 组件引用资源路径不能解析特殊字符或汉字

  • 规范文件命名

4. {{}}模板中不能执行特殊方法,只能处理简单的四则运算

  const money = 345678;<view>{{ money }}</view>

期望: ‘34万元’
vue是这样: {{ money | moneyFilter }}
小程序是这样:利用wxs的format

.wxs

  const fnToFixed = function(num) {return num.toFixed(2);}module.exports = {fnToFixed}
//引入<wxs src='../../../xxx.wxs' module="filters">//使用<view>{{ filters.fnToFixed(money) }}</view>

5.wxs无法使用new Date()

  • 方案: getDate()

6.setData过程中需要注意对象覆盖

错误示范:
这个方法会把b里面的其他属性都去掉了。

  data: {a: '1',b: {c: 2,d: 3}}this.setData({b: {c: 4}});

正确示范:
先提取出来或者用wx-update-data

  const { b } = this.data;b.c = 4;this.setData({ b });// wx-update-data

7. IOS的date不支持2020-06-26格式,必须要转成2020/06/26

8. wx接口不promise

微信里面的接口像wx.login()这样的方法,我们获取他的结果都是通过回调获取他的信息

  // wx-promise-pro
  • 安装Promise wx-promise-pro
npm i -S wx-promise-pro
import { promisifyAll } from 'wx-promise-pro';
// before
wx.showLoading({success: res => {wx.request({// ……})}
})
// after
wx.pro.showLoading({title: 'loading',mask: true
}).then(() => {consol.log('I am in promise');
})
  • 问题: 请你自己手写封装一个promise化的接口
wx.request({url: '',data: (),success: res => {}
})
// 实现
wx.pro.request({url: '',data: ()
}).then(res => {}).catch(res => {})
  • 做法:
//一种写法
function request(opt) {return new Promise( (resolve, reject) => {wx.request({...opt,success: res => {resolve(res);},fail: err => {reject(err);}})} )
}
//更加抽象的另一种写法
function promisify(api) {return (opt = {}) => {return new Promise((resolve, reject) => {api({...opt,success: resolve,fail: reject})})}
}
promisify(wx.request)(opt);

引申:promise的定义

    1. promise有哪些状态: pending、fulfilled、rejected
    1. 状态流转规律:状态只能从pending => rejected 或 pending => fulfilled
    1. new Promise时,需要传入的参数是:executor()且立即执行
    1. promise默认状态是pending
    1. promise中成功状态value:undefined、thenable、promise
    1. 失败状态reason
    1. promise中的then方法,onFulfilled、onRejected
    1. onFulfilled 参数 value
    1. onRejected 参数 reason

手写promise

class Promise {}

9. app入口

1)可以通过getApp来获取全局的app()里面的全局变量

var app = getApp()
app.getUserInfo()

a. 如果在App()内部函数中,如果要获取全局getApp,直接用this即可
b. 获取getApp之后,可不可以拿到生命周期?可以,但是禁止操作,调用的目的是为了获取全局变量,不要修改

2) page()

a. getCurrentPage()获取当前page,不要修改页面栈,不要在app.onLaunch时候调用,page还没生成

3)微信小程序eventChannel页面间事件通信通道

可以运用在父子组件或点击下一个页面传值回上一个页面
一、当前页面跳转下一页注册监听 events,监听被打开页面的回调

wx.navigateTo({url: 'home?id=1',<!-- events 监听被打开页面发送到当前页面的数据 -->events: {<!-- 给指定事件添加监视器,获取被打开页面传回当前页面的数据 --><!-- 被打开页面进行回调 -->accessDataForm: function(data) {console.log(data)},homeEvent: function(data) {console.log(data)}...},success: function(res) {<!-- 通过eventChannel向被打开页面传值 -->res.eventChannel.emit('accData', { data: 'id_number' })}
})

二、被打开的页面调用

Page({onLoad: function(option){<!-- 获取事件对象 -->const eventChannel = this.getOpenerEventChannel()<!-- 通知上一页,传回参数,响应函数 --><!-- 改变上一页监听的数据时调用 -->eventChannel.emit('accessDataForm', {data: 'id_number'});eventChannel.emit('homeEvent', {data: 'id_number'});<!-- 监听accData事件,获取上一页面通过eventChannel传到当前页面的数据 -->eventChannel.on('accData', function(data) {console.log(data)})}
})

10. 属性名不要使用data开头。dataXyz, data-xyz dataset节点来处理

属性名应避免以 data 开头,即不要命名成dataXyz这样的形式,因为在 WXML 中,data-xyz=""会被作为节点 dataset 来处理,而不是组件属性。

部分内容参考:https://blog.csdn.net/weixin_33994444/article/details/91361960

wx-wx-wx-wx-wx-wx-wx-wx-wx-wx-wx相关推荐

  1. python wxpython wx.grid动态增加行_wxPython控件学习之wx.FlexGridSizer

    FlexGridSizer是GridSizer的一个更灵活的版本.它与标准的GridSizer几乎相同,除了下面3点例外: 1.每行和每列可以有各自的尺寸. 2.默认情况下,当尺寸调整时,它行和列整体 ...

  2. 列表渲染 wx:key 的作用、条件渲染 wx:if 与 hidden 的区别

    这是微信小程序踩坑系列的第三篇,想要了解更多关于微信小程序开发的那些事,欢迎关注我的<微信小程序>专栏. 前言 开发微信小程序离不开"页面渲染",对于初学者来说很难理解 ...

  3. wxpython安装失败_在Windows XP上安装wxPython后,“导入wx”失败

    我下载并安装了这个版本的wxPython以便与我的Python 2.6安装一起使用: 当我运行Python并尝试导入wx时,出现以下错误:C:\Program Files\Console2>py ...

  4. 微信小程序之wx:if视图层的条件渲染

    示例: wxml:使用view <!--index.wxml--> <button bindtap="EventHandle">按钮</button& ...

  5. 小程序-wx:for

    wx:for (列表渲染) index默认数组下标 item默认数组当前项的变量名 数组是对象的形式,单纯写{{item}},结果是[object object]的形式,必须加对象名,并且对象名基本设 ...

  6. python fonttool_Python wx.Font方法代码示例

    本文整理汇总了Python中wx.Font方法的典型用法代码示例.如果您正苦于以下问题:Python wx.Font方法的具体用法?Python wx.Font怎么用?Python wx.Font使用 ...

  7. wxWidgets:显示如何从 DLL 使用 wx 的示例

    wxWidgets:显示如何从 DLL 使用 wx 的示例 wxWidgets:显示如何从 DLL 使用 wx 的示例 my_dll.h my_dll.cpp wxWidgets:显示如何从 DLL ...

  8. wx:for-item循环数组失败

    如果将wx:for = "{{list}}"换成wx:for-items="{{list}}":运行科一看到和wx:for = "{{list}}&q ...

  9. python wx提示框字体_使用wxStyledTextCtrl实现代码提示

    wxStyledTextCtrl是wxPython对流行的Scintilla的包装,Scintilla的网站(http://www.scintilla.org/), wxStyledTextCtrl是 ...

  10. python wx模块下choice列表框值怎么更新_wx python

    一.静态文本控件 wx.StaticText(parent, id, label, pos=wx.DefaultPosition,    size=wx.DefaultSize, style=0, n ...

最新文章

  1. javascript模拟sleep
  2. Android 第九课 常用控件-------ListView
  3. android与php使用base64加密的字符串结果不一样解决方法
  4. ZZULIOJ 1121: 电梯
  5. Java项目案例大全
  6. 漫步数学分析八——集合边界
  7. 《跟菜鸟学Cisco UC部署实战》-上线了(线下培训班开班,见百度云)
  8. 如何将物理服务器转换成基于的Vmware ESXi虚拟服务器
  9. Spring4学习笔记 - Bean的生命周期
  10. networkx绘制人物关系网络图
  11. 已经有些跑偏的“学术会议文化”!
  12. 修图教程:为照片增加云雾效果
  13. 人脸活体检测、红外人脸数据集下载
  14. 拉勾课程--性能优化记录
  15. 单片机毕业设计 自动浇花灌溉系统设计
  16. 面向对象程序设计php,php面向对象的程序设计
  17. 彻底卸载并重装Anaconda环境与Python的方法
  18. CGAL官方软件包-算数和代数 1. Algebraic Foundations
  19. matlab合理分配席位_走进民航一线——首都机场机位分配员的24小时
  20. 决策曲线 Decision Curve

热门文章

  1. 中断深入-->休眠唤醒(通用)
  2. latex插入图片之后图片后面的文字跑到前面来了怎么办
  3. ICLR2020国际会议焦点论文(Spotlight Paper)列表(内含论文源码)
  4. C语言 12个球称3次 找出其中一个坏球
  5. *寒假水105——Reduced ID Numbers
  6. 被资本和巨头炒上风口的无人便利店,会成为下一个无人货架吗?
  7. ViewPager 添加广告页面小圆点指示器效果
  8. 爆糗的买单,看谁脸皮厚
  9. 外包岗退退退!坚决不能选的三点理由:简历有污点,稳定性极差,福利待遇差!...
  10. 广州麦仑 全面亮相2022身份识别技术大会及第十七届SDS