注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Android-EventBus修改纪实

背景 笔者在使用 EventBus 的过程中发现有时只能收到最后一次的粘性 Event ,导致业务逻辑出现混乱,下面是笔者的使用示例: // Event.java public final class Event { private final in...
继续阅读 »

背景


笔者在使用 EventBus 的过程中发现有时只能收到最后一次的粘性 Event ,导致业务逻辑出现混乱,下面是笔者的使用示例:


// Event.java
public final class Event {

private final int code;

public Event(int code) {
this.code = code;
}

public int getCode() {
return code;
}
}

// Example.java
public class Example {

// 调用多次
public void test(int code) {
EventBus.getDefault().postSticky(new Event(code));
}

// 调用多次 `test(int code)` 后再注册订阅者
public void register() {
EventBus.getDefault().register(this);
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEvent(Event event) {
// 发现只能收到最后一次的粘性事件
System.out.println(event.getCode());
}
}

所以去查看了 EventBus 的源码,接下来我们分析下 EventBus 发送粘性事件的流程。


分析


粘性事件



以下源码基于 EventBus 3.3.1 版本



下面是发送粘性事件的源码:


private final Map, Object> stickyEvents;

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

postSticky 代码比较简单,首先对 stickyEvents 进行加锁,接下来把 event 事件的 Class 对象作为 Key,event 事件本身作为 value 放进 Map 中,其中stickyEvents 是 Map 对象,实例是 ConcurrentHashMap, 其 Key 和 Value 的泛型形参分别是 ClassObject, 它的作用就是用来存储粘性事件;然后调用 post(event) 把粘性事件当作普通事件发送一下。


首先我们看下最后为什么要调用下 post(event)


虽然 post(evnet) 上面有注释,简单翻译下:"在放进 Map 后应该再发送一次,以防止订阅者想立即删除此事件",读完注释后,可能还是不太明白,这里笔者认为:在前面存储完粘性事件后,这里调用 post 把粘性事件当作普通事件发送出去,或许是因为现在已经有注册的粘性事件订阅者,此时把已经注册的粘性事件订阅者当作普通事件的订阅者,这样已经注册的粘性事件订阅者可以立即收到相应的事件,只是此时事件不再是粘性的。


postSticky 中我们并没有看到粘性事件是在哪里发送的,想一想我们使用粘性事件的目的是什么?当注册订阅者时可以收到之前发送的事件,这样来看,粘性事件的发送是在注册订阅者时,下面是注册订阅者的源码,删除了一些无关代码:


public void register(Object subscriber) {

// 省略无关代码

Class subscriberClass = subscriber.getClass();

// 查找订阅者所有的Event接收方法
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

register 代码也比较简单,首先通过订阅者的 Class 对象查找订阅者所有的Event事件接收方法,然后对 EventBus 对象加锁,遍历所有的Event事件接收方法 subscriberMethods 调用 subscribe 方法,以下是 subscribe 方法源码:


// Key 为 Event Class 对象,Value 为存储 Event 的订阅者和接收 Event 方法对象的集合 
private final Map, CopyOnWriteArrayList> subscriptionsByEventType;

// Key 为订阅者对象,Value 为订阅者中的 Event Class对象集合
private final Map>> typesBySubscriber;

// Must be called in synchronized block
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// Event Class对象
Class eventType = subscriberMethod.eventType;

// 订阅者和接收 Event 方法对象
Subscription newSubscription = new Subscription(subscriber, subscriberMethod);

// 根据 Event Class对象,获取订阅者和接收 Event 方法对象的集合
CopyOnWriteArrayList subscriptions = subscriptionsByEventType.get(eventType);

// 判断订阅者和接收 Event 方法对象是否为空
if (subscriptions == null) {
subscriptions = new CopyOnWriteArrayList<>();
subscriptionsByEventType.put(eventType, subscriptions);
} else {
// 判断是否已经包含了新的订阅者和接收 Event 方法对象,若是包含则认为是重复注册
if (subscriptions.contains(newSubscription)) {
throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
+ eventType);
}
}

// 这里是按优先级排序插入到集合中
int size = subscriptions.size();
for (int i = 0; i <= size; i++) {
if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
subscriptions.add(i, newSubscription);
break;
}
}

// 这里是把 Event Class对象添加进对应订阅者的 Event Class对象集合中
List> subscribedEvents = typesBySubscriber.get(subscriber);
if (subscribedEvents == null) {
subscribedEvents = new ArrayList<>();
typesBySubscriber.put(subscriber, subscribedEvents);
}

// 上面已经判断了是否重复注册,所以这里直接添加
subscribedEvents.add(eventType);

// 接下来就是粘性事件的发送逻辑了
// 判断 Event 接收方法是否可以处理粘性事件
if (subscriberMethod.sticky) {
// 这里判断是否考虑 Event 事件类的继承关系,默认为 Ture
if (eventInheritance) {
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
}

在上面的源码中,增加了不少注释有助于我们读懂源码,在源码的最后就是粘性事件的发送逻辑了,其中有两个分支,其中一个分支根据 Event 事件的继承关系发送事件,另外一个分支根据接收 Event 方法中的 Event Class 对象从 stickyEvents 中直接查找粘性事件,最后两个分支殊途同归,都调用了 checkPostStickyEventToSubscription 方法:


private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
if (stickyEvent != null) {
// If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
// --> Strange corner case, which we don't take care of here.
postToSubscription(newSubscription, stickyEvent, isMainThread());
}
}

checkPostStickyEventToSubscription 方法很简单,对粘性事件做下判空处理,继续调用 postToSubscription 方法,传入订阅者与接收 Event 方法对象,粘性事件和是否是主线程布尔值:


private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
switch (subscription.subscriberMethod.threadMode) {
case POSTING:
invokeSubscriber(subscription, event);
break;
case MAIN:
if (isMainThread) {
invokeSubscriber(subscription, event);
} else {
mainThreadPoster.enqueue(subscription, event);
}
break;
case MAIN_ORDERED:
if (mainThreadPoster != null) {
mainThreadPoster.enqueue(subscription, event);
} else {
// temporary: technically not correct as poster not decoupled from subscriber
invokeSubscriber(subscription, event);
}
break;A
case BACKGROUND:
if (isMainThread) {
backgroundPoster.enqueue(subscription, event);
} else {
invokeSubscriber(subscription, event);
}
break;
case ASYNC:
asyncPoster.enqueue(subscription, event);
break;
default:
throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
}
}

postToSubscription 方法比较长,但是比较好理解,就是根据接收 Event 方法上的 @Subscribe 注解中传入的线程模型进行事件的分发,具体的事件分发流程,有空再分析,本文就先不分析了,现在我们只需知道最后都会调用 invokeSubscriber(Subscription subscription, Object event) 方法即可:


void invokeSubscriber(Subscription subscription, Object event) {
try {
// 反射调用 Event 接收方法传入 Event 事件
subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
} catch (InvocationTargetException e) {
handleSubscriberException(subscription, event, e.getCause());
} catch (IllegalAccessException e) {
throw new IllegalStateException("Unexpected exception", e);
}
}

终于在 invokeSubscriber 方法中找到调用 Event 接收方法的地方了,原来 EventBus 最后是通过反射调用 Event 接收方法并传入相应 Event 事件的。


分析完 Event 事件的发送流程,好像没有发现为什么有时收不到粘性事件。


我们回过头来再看下笔者的使用示例,为了方便查看,下面贴出使用示例代码:


// Example.java
public class Example {

// 调用多次
public void test(int code) {
EventBus.getDefault().postSticky(new Event(code));
}

// 调用多次 `test(int code)` 后再注册订阅者
public void register() {
EventBus.getDefault().register(this);
}

@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEvent(Event event) {
// 发现只能收到最后一次的粘性事件
System.out.println(event.getCode());
}
}

可能细心的读者已经发现 test 方法调用了,问题应该出在 postSticky 方法中,让我们再次查看 postSticky 方法:


private final Map, Object> stickyEvents;

public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

根据前面分析 postSticky 方法的结果,stickyEvents 用于存储粘性事件,它是个 Map 结构,而 stickyEvents 的 Key 正是 Event 的 Class 对象,根据 Map 结构的存储原理:如果存在相同的 Key,则覆盖 Value 的值,而 stickyEvents 的 Value 正是 Event 本身。


终于真相大白,多次调用 test 方法发送粘性事件,EventBus 只会存储最后一次的粘性事件。


小结


EventBus 针对同一个粘性 Event 事件只会存储最后一次发送的粘性事件。


EventBus 的上述实现可能是因为多次发送同一个粘性事件,则认为之前的事件是过期事件应该抛弃,因此只传递最新的粘性事件。


EventBus 的这种实现无法满足笔者的业务逻辑需求,笔者希望多次发送的粘性事件,订阅者都能接收到,而不是只接收最新的粘性事件,可以理解为粘性事件必达订阅者,下面让我们修改 EventBus 的源码来满足需求吧。


修改


上一节我们分析了粘性事件的发送流程,为了满足粘性事件必达的需求,基于现有粘性事件流程,我们可以仿照粘性事件的发送来提供一个发送必达消息的方法。


Subscribe


首先我们定义 Event 接收方法可以接收粘性事件是在 @Subscribesticky = true , 所以我们可以修改 Subscribe 注解,增加粘性事件必达的方法:


@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
ThreadMode threadMode() default ThreadMode.POSTING;

/**
* If true, delivers the most recent sticky event (posted with
* {@link EventBus#postSticky(Object)}) to this subscriber (if event available).
*/

boolean sticky() default false;

// 增加消息必达的方法
boolean rendezvous() default false;

/** Subscriber priority to influence the order of event delivery.
* Within the same delivery thread ({@link ThreadMode}), higher priority subscribers will receive events before
* others with a lower priority. The default priority is 0. Note: the priority does *NOT* affect the order of
* delivery among subscribers with different {@link ThreadMode}s! */

int priority() default 0;
}

rendezvous 以为约会、约定的意思,可以理解为不见不散,在这里它有两层作用,其一是标记方法可以接收粘性事件,其二是标记方法接收的事件是必达的。


findSubscriberMethods


接下来就需要解析 rendezvous 了,我们先看看 sticky 是如何解析的,在上一节我们分析了 register 方法,方便查看,下面再贴出 register 方法源码:


public void register(Object subscriber) {

// 省略无关代码

Class subscriberClass = subscriber.getClass();

// 查找订阅者所有的Event接收方法
List subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
synchronized (this) {
for (SubscriberMethod subscriberMethod : subscriberMethods) {
subscribe(subscriber, subscriberMethod);
}
}
}

上一节分析中,我们没有分析查找订阅者中所有的 Event 接收方法 findSubscriberMethods ,接下来我们分析下在 findSubscriberMethods 方法是如何查找 Event 接收方法的:


List findSubscriberMethods(Class subscriberClass) {
// 先从缓存中查找
List subscriberMethods = METHOD_CACHE.get(subscriberClass);
if (subscriberMethods != null) {
return subscriberMethods;
}

// 是否忽略生成索引,默认为False,所以这里走else分支
if (ignoreGeneratedIndex) {
subscriberMethods = findUsingReflection(subscriberClass);
} else {
// 查找Event接收方法
subscriberMethods = findUsingInfo(subscriberClass);
}

// 如果订阅者和订阅者父类中没有Event接收方法则抛出异常
if (subscriberMethods.isEmpty()) {
throw new EventBusException("Subscriber " + subscriberClass
+ " and its super classes have no public methods with the @Subscribe annotation");
} else {
// 添加进缓存中
METHOD_CACHE.put(subscriberClass, subscriberMethods);
return subscriberMethods;
}
}

调用 findSubscriberMethods 方法需要传入订阅者 Class 对象,通过笔者在源码中增加的注释分析发现默认调用 findUsingInfo 方法查找 Event 接收方法,我们继续跟踪 findUsingInfo 方法:


private List findUsingInfo(Class subscriberClass) {
// FindState对订阅者Class对象和Event接收方法进行了一层封装
FindState findState = prepareFindState();
findState.initForSubscriber(subscriberClass); // ①
while (findState.clazz != null) {

// 查找订阅者信息,包含订阅者Class对象、 订阅者父类、Event接收方法等
findState.subscriberInfo = getSubscriberInfo(findState); // ②

// 在 ① initForSubscriber中会把subscriberInfo置为null,
// 在 ② getSubscriberInfo中没有Index对象,
// 所以第一次时这里会走else分支
if (findState.subscriberInfo != null) {
SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
for (SubscriberMethod subscriberMethod : array) {
if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
findState.subscriberMethods.add(subscriberMethod);
}
}
} else {
// 查找Event接收方法
findUsingReflectionInSingleClass(findState);
}

// 查找父类的Event接收方法
findState.moveToSuperclass();
}

// 通过findState返回Event接收方法,并回收findState
return getMethodsAndRelease(findState);
}

根据笔者在源码中的注释分析,在 findUsingInfo 方法中使用「享元模式」对 FindState 进行回收利用,避免创建大量临时的 FindState 对象占用内存,最后再次调用 findUsingReflectionInSingleClass 方法查找 Event 接收方法,看方法名字应该是使用反射查找,findUsingReflectionInSingleClass 源码较长,删减一些不关心的代码:


private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
// 通过反射获取当前类中声明的所有方法
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// 删减不关心的代码
}

// 遍历所有方法
for (Method method : methods) {

// 获取方法的修饰符
int modifiers = method.getModifiers();

// 判断方法是否是public的;是否是抽象方法,是否是静态方法,是否是桥接方法,是否是合成方法
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {

// 获取方法的形参Class对象数组
Class[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {

// 获取方法上的Subscribe注解
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class eventType = parameterTypes[0];

// 检测是否已经添加了相同签名的方法,考虑子类复写父类方法的情况
if (findState.checkAdd(method, eventType)) {

// 获取注解的参数
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky(),

// 这里我们添加rendezvous参数 ①
subscribeAnnotation.rendezvous()));
}
}
}
// 删减不关心的代码
}
// 删减不关心的代码
}
}

findUsingReflectionInSingleClass 方法中通过反射获取订阅者中声明的所有方法,然后遍历所有方法:



  1. 首先判断方法的修饰符是否符合,

  2. 其次判断方法是否只有一个形参,

  3. 再次判断方法是否有 Subscribe 注解,

  4. 然后检测是否已经添加了相同签名的方法,主要是考虑子类复写父类方法这种情况,

  5. 最后获取 Subscribe 注解的参数,在这里我们解析 rendezvous,封装进 SubscriberMethod 中。


SubscriberMethod 中增加 rendezvous 字段,删除不关心的代码:


public class SubscriberMethod {
final Method method;
final ThreadMode threadMode;
final Class eventType;
final int priority;
final boolean sticky;

// 增加 `rendezvous` 字段
final boolean rendezvous;
/** Used for efficient comparison */
String methodString;

public SubscriberMethod(Method method, Class eventType, ThreadMode threadMode,
int priority, boolean sticky,

// 增加 `rendezvous` 形参
boolean rendezvous) {
this.method = method;
this.threadMode = threadMode;
this.eventType = eventType;
this.priority = priority;
this.sticky = sticky;
this.rendezvous = rendezvous;
}
}

postRendezvous


好的,rendezvous 已经解析出来了,接下来我们对外提供发送必达事件的接口:


// 选择List存储必达事件,使用Pair封装必达事件的Key和Value
private final List, Object>> rendezvousEvents;

public void postRendezvous(Object event) {
synchronized (rendezvousEvents) {
rendezvousEvents.add(Pair.create(event.getClass(), event));
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}

上面的源码,我们通过仿照 postSticky 方法实现了 postRendezvous 方法,在 postSticky 方法中使用 Map 存储粘性事件,不过我们在 postRendezvous 方法中使用 List 存储必达事件,保证必达事件不会因为 Key 相同而被覆盖丢失,最后也是调用 post 方法尝试先发送一次必达事件。


register


在上一节中我们分析了粘性事件是在 register 中调用 subscribe 方法进行发送的,这里我们仿照粘性事件的发送逻辑,实现必达事件的发送逻辑,我们可以在 subscribe 方法最后增加发送必达事件的逻辑,以下源码省略了一些不关心的代码:


private final List, Object>> rendezvousEvents;

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
// 省略不关心的代码

// 粘性事件发送逻辑
if (subscriberMethod.sticky) {
if (eventInheritance) {
Set, Object>> entries = stickyEvents.entrySet();
for (Map.Entry, Object> entry : entries) {
Class candidateEventType = entry.getKey();
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = entry.getValue();
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object stickyEvent = stickyEvents.get(eventType);
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}

// 新增必达事件发送逻辑
// 判断方法是否可以接收必达事件
if (subscriberMethod.rendezvous) {
if (eventInheritance) {
for (Pair, Object> next : rendezvousEvents) {
Class candidateEventType = next.first;
if (eventType.isAssignableFrom(candidateEventType)) {
Object stickyEvent = next.second;
checkPostStickyEventToSubscription(newSubscription, stickyEvent);
}
}
} else {
Object rendezvousEvent = getRendezvousEvent(eventType);
if (rendezvousEvent != null) {
checkPostStickyEventToSubscription(newSubscription, rendezvousEvent);
}
}
}
}

subscribe 方法中,我们通过仿照粘性事件的发送逻辑增加了必达事件的发送:



  1. 首先判断 Event 接收方法是否可以接收必达事件

  2. 其次考虑 Event 必达事件的继承关系,

  3. 最后两个分支都调用 checkPostStickyEventToSubscription 方法发送必达事件


happy~


总结


使用第三方库时,发现问题不要慌张,带着问题去查看源码总有一番收获,这也告诫我们在使用第三库时最好先搞明白它的实现原理,遇到问题时不至于束手无策。


通过分析 EventBus 的源码,我们有以下收获:



  1. 明白了我们注册订阅者时 EventBus 做了哪些事情

  2. 知晓了我们发送粘性事件时,EventBus 是如何处理及何时发送粘性事件的

  3. 了解到 EventBus 是通过反射调用 Event 事件的接收方法

  4. 学习了 EventBus 中的一些优化点,比如对 FindState 使用「享元模式」避免创建大量临时对象占用内存

  5. 进一步了解到对并发的处理


通过以上收获,我们成功修改 EventBus 源码实现了我们必达事件的需求。


到这里我们已经完成了必达事件的发送,不过我们还剩下获取必达事件,移除必达事件没有实现,最后 EventBus 中还有单元测试 module,我们还没有针对 rendezvous 编写单元测试,读者有兴趣的话,可以自己试着实现。


希望可以帮到你~


作者:guodongAndroid
链接:https://juejin.cn/post/7104107150678917133
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

入职东北国企做程序员一个月,感受如何?

工作环境 我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。 人 目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职...
继续阅读 »

工作环境


我新入职的公司是哈尔滨的一家国企下的二级子公司,新成立的研发公司,目前还处于蓬勃发展的阶段,业务水准也算的上是不错了。



目前人数100多个,但是却五脏俱全。单说研发部门,从产品,UI,研发,测试,运维,甚至运营人员都很完善,人员只需要根据自己的职责去负责自己的事情就好。


办公环境可以分为两个环境,分别是“职能部门”和“研发部门”:


* 职能部门比较正式,工位、装修以及员工着装都比较正规。
* 研发部门较为随意一些,无论是工位还是桌椅什么的,有些东拼西凑的感觉,但是整体还是可以接受。

另外可能是因为国企的原因,所有的工位都是大隔断那种,如果换成现在公司常见的大通桌,估计人数还能多做十好几个,毕竟我刚来的时候还没有正式工位坐呢。



相比于在其他公司上班,可能在这最大的体会就是不用考虑吃什么。公司有食堂,提供午饭,菜不能选,但是每天四菜一汤,加水果或酸奶。相比于每天纠结的选择外卖,我对这个很满意。


晚上如果加班的话,公司会统一订餐,大概一餐的费用也在20至30块之间吧,当然也没法选择吃什么,有啥吃啥被。


早餐为什么最后说,因为公司的早餐在早上八点之前供应,八点半上班。。。有点难受啊。


幸好公司提供简单的零食,面包、火腿肠、泡面等等,虽然偶尔会被大家抢空,但是总比没有强吧。



上家公司离我家只有1公里的距离,所以从回到哈尔滨也没有买车,每天不行上班,还挺惬意的。


现在不行了,新公司距离家里有十好几公里,当然我也暂时没有选择买车,地铁出行,快捷方便,还省心,唯一的缺点就是要走个1.5公里吧。


在晚上八点之后打车可以报销的,但是只能是网约车,可能是出租车的票,粘贴太过麻烦了吧。反正我是不打车,因为我嫌报销麻烦。


工具


啥是工具呢,对程序员来说就是电脑了,公司提供电脑,也可以自己买电脑进行报销,还是很人性化地。


公司的会议室设施还是不错的,各种投屏等等,比较先进,完全摒弃了传统的投影仪等等,这还让我对公司有种另眼相看的感觉。


还提供显示器什么的,自己申请就好了。


入职感受


我面试的岗位是java开发,常规的java框架使用起来都没有问题。面试过程还是比较简单的,主要是常用的一些组件,简单的实现原理等等,所以顺利通过了。


但是比较遗憾的公司给我砍了一些,定位的职级也不是很高。说实话我还是有点难受的,毕竟整个面试过程,和我对个人的能力认知还是比较清楚地。


但是当我入职后我明白了,这里毕竟是哈尔滨,收入和年龄还是有很大的关系的。部门内有好几位大哥,想想也就释然了,在其位谋其政吧,他们的工作确实我我接下来要做的繁琐。希望日后能够慢慢的升职加薪吧。


总体来说,东北人还是敞亮,有事直接提,工作也没啥拐弯抹角的,干就完了。我才刚来公司第一天,就给我把工作安排上了,一点不拿我当外人啊


工作感受


既然谈到工作了,就展开说说。


我第一天到公司,找了个临时工位,领导们各种git账号、禅道账号就给我创建好,一个项目扔给我,本月中期要求做完。。我当时内心的想法真的是:东北人果然是好相处啊,整的跟老同事似的。我能怎么办,干就完了啊。


项目还是很简单的,常规的springboot + mybatis + vue2的小项目,大概也没到月中期,一个礼拜就完事了。


比较让我惊喜的是部署的环节。居然使用的是devops工具KubeSphere。我只说这一句你们可能不理解,这是我在哈尔滨的第三家公司,从来没有一家公司说使用过k8s,甚至相关的devops工具。只能说是哈尔滨软件行业的云化程度还是太低了。唯一在上家公司的jenkins还是因为我想偷懒搭建的。


不过运维相关的内容都把握在运维人员手里,所以想要料及并且掌握先关的知识还是要自己私下去学习的。


项目其实都是小项目,以web端和app为主,基本都是前后端分离的单体架构。唯一我接触到的微服务架构应该就是公司的中台,提供统一的权限配置和登录认证,整体还是很不错的。


虽然公司的项目很多,工作看起来很忙碌,但实际还是比较轻松愉快的,我还能应付自如。每天晚上为了蹭一顿晚饭,通常会加班到七点半。用晚上这个时间更更文,也挺好的。


从体来说,是我比较喜欢的工作节奏。


个人分析


我是一个不太安定的人,长期干一件事会让我比较容易失去兴趣,还是挺享受刚换工作时,这段适应环境的感觉。也有可能更喜欢这种有一定挑战的感觉。


和上一家公司相比,这家公司在公司的时间明显多出很多,也没有那么悠闲了,但是我却觉得这更适合我,毕竟我是一个闲不住的人,安逸的环境让我感到格外的焦虑,忙碌的生活会让自己感到生活很充实。


记得之前的文章说过自己的身体健健的不太好,但是最近不知道是上班的路程变远,导致运动量的增加,之前不适的症状似乎都小时了。真闲出病来了




既来之,则安之,时刻提醒自己再努力点,阳光总在风雨后。


作者:我犟不过你
链接:https://juejin.cn/post/7125627005407592462
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一位 98 年程序员离职后

今天不写技术文了,写点轻松的。 我自己都没讨好自己,何苦要来讨好你呢? 开篇 本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、...
继续阅读 »

今天不写技术文了,写点轻松的。


img-16593196478205dc10966293f6e5e3f0be6d9ff93705f.jpg



我自己都没讨好自己,何苦要来讨好你呢?



开篇


本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、跑路的,这些消息可以说不绝于耳。但本人最终还是选择离职休整下。


得益于父辈们的努力,在房价还不到当前房价一半的时候出手了,让弱小的我在这座城市有瓦遮头,不必为三餐奔波。既没有房贷,也没有车贷,一人吃饱,全家不饿的我没有了这方面的顾虑,也有了底气做出这个选择。


img-165932129544253f44bc0a87ca13a950b2ff14f24ccd2.jpg



希望明天还能看到那朵软萌萌的云,因为它好像你



说下离职后我都干了些什么吧,给各位列一下,说不定能找到知音。



  • 刷力扣

  • 整理下之前的东西

  • 健身

  • 写作

  • 品鉴周董的新歌

  • 看综艺

  • ...


上面的这些东西不分先后,一直都在做。


刷力扣


刷力扣其实很早就开始了,每天登录力扣有一个积分,完成每日一题有 10 积分,到现在坚持了有差不多两年了,战绩如下:


QQ截图20220801104732.png



大部分是中等



光看题目量不算少,但其实大部分困难和部分中等都是 cv 之后通过的,不装了,摊牌了。刷题过程也不艰难,就一句话,简单题重拳出击,困难题唯唯诺诺。有些人会觉得算法没有必要,因为平时的工作就用不到。但我觉得算法最重要的是锻炼人的思维,思维很重要,它能够指导一个人思考问题的轨迹和方向。虽然有时刷题时会感觉自己活着就是凑数的,没必要灰心,真的,因为你的判断是对的。


整理下之前的东西


之前在工作时也积累一些东西,但没有做整理,所以趁着这段时间整理下,看下能不能发现一些新东西。个人觉得一直处于一种忙碌的状态并不一定是好事,这有点像吃东西时狼吞虎咽,容易噎着。


健身


这件事是坚持的最久的一件事,从高一一直到现在。高一时上映的速 7,被强森和郭达在办公室的那段打戏吸引,当时觉得男人就应该这样。于是从最简单的俯卧撑、引体开始,一点点的朝自己的目标努力。但这过程走了很多弯路,比如训练的方式不对,太急于求成、吃的没跟上、休息没跟上,导致很长一段时间都处于瓶颈期,一直在原地踏步。这种不上不下的感觉真的不好受,也想过放弃,但已经戒不掉了。图就不发了,担心被喷。胸、背、腿、腰、手、腹肌都有练,腹肌不多不少,正好 6 块。至于身材,我个人觉得还行,至少不差,也被人说过身材好,同性异性都有。


QQ截图20220801201459.png



被同性说



写作


这个貌似是在去年开始的,但中断了挺长一段时间,就想着在空窗期重新捡起来。至于最终能开出什么样的花,也没想过,就觉得写比不写强。读者感兴趣的可以看我之前写过的文章,主页:
鄙人的主页


img-1659357885349240b678eb24aca42039c30c16b002044.jpg



对待生活,不必说的太多,你同样可以给它一个惊喜



品鉴周董的新歌


本人虽说不是周董的粉丝,但以前总想着能在晴天里给千里之外的她带一壶爷爷泡的茶,面对面安静的坐着,她的笑容甜甜的,我也对着她傻笑。院子里是一片花海,散发着迷迭香。


她送我来到渡口,她的倒影在满是桃花的粉色海洋里若隐若现。船夫摇着桨,背对着我,哼着她唱过的红颜如霜突然来了句:这是最伟大的作品。可谁让夜晚留不住错过的烟火,活该我到现在还在流浪。


看综艺


综艺平时也是我解压的一种方式,最近把跑男第十季追完了,几位 mc 都是各有特点。不过最喜欢的还是新加入的白鹿,人美,很搞笑,魔性的笑声让人很容易记住她。


magazine-unlock-01-2.3.2022080201-7432B64DE5C9B11.jpg



你问我:我对你有多重要,我回答:太阳你知道吧



总结


可能有人看了之后会觉得有点躺平的趋势,但其实并没有。本人还是很爱折腾的,也希望能多认识点圈子以外的人,多认识点有趣的人,多认识点志同道合的人。有些人会觉得程序员很闷,不爱说话,天天就对着电脑。可能有部分人是这样的,但我不是,因为我是一个不走寻常路的程序员,而且我深知只有跳出圈子,才能打破认知。by the way,本人对数字化转型行业挺感兴趣的,有读者从事或者了解的话,可以大胆私信我啊。


作者:对方正在输入
来源:juejin.cn/post/7127653600532103198
收起阅读 »

记一次不规范使用key引发的惨案

web
前言 平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题,直到有一天,我遇到一个这样的需求 场景 在一个下单界面,我需要去商品列表选商品,然后在下单界面遍历显示所选商品,要求后选的...
继续阅读 »

前言


平时在使用v-for的时候,一般会要求传入key,有没有像我一样的小伙伴,为了省心,直接传索引index,貌似也没有遇到过什么问题,直到有一天,我遇到一个这样的需求


场景


在一个下单界面,我需要去商品列表选商品,然后在下单界面遍历显示所选商品,要求后选的排在前面,而且选好商品之后,需要在下单界面给每个商品选择发货地,发货地列表是通过商品id去接口取的,我的代码长这样:



  • 下单界面调用商品组件


// 这里每次选了商品都是从前插入:list.value = [...newList, ...list.value]
<Goods
v-for="(item, index) in list"
:key="index"
:goods="item">
</Goods>


  • 商品组件内部调用发货地组件


<SendAddress
v-model="address"
:product-no="goods.productNo"
placeholder="请选择发货地"
@update:model-value="updateValue"></SendAddress>


  • 发货地组件内部获取发货地址列表


onMounted(async () => {
getList()
})
const getList = async () => {
const postData = {
productInfo: props.productNo,
}
}

上述代码运行结果是,每次获取地址用的都是最开始选的那个商品的信息,百思不得其解啊,最后说服产品,不要倒序了,问题解决


解决过程


后来在研究前进刷新后退缓存时,关注到了组件的key,详细了解后才知其中来头


企业微信截图_16813558431830.png



重点:根据key复用或者更新,也就是key没有变化,就是复用,变化了在更新挂载,而onMounted是在挂载完成后执行,没有挂载的元素,就不会走onMounted



回到上述问题,当我们每次从前面插入数据,key的变化逻辑是这样的


结论


企业微信截图_16813564053499.png



最开始选中的商品key从1变成了2,最近选的是0。


而0和1是本来就存在的,只会更新数据,不会重新挂载,只有最开始选的那个商品key是全新的,会重新挂载,重新走onMounted。


所以每次选择数据后,拿去获取地址列表的商品信息都是第一个的



解决以上问题,把key改成item.productNo就解决了


作者:赖皮喵
来源:juejin.cn/post/7221357811287834680
收起阅读 »

1.6kB 搞定懒加载、无限滚动、精准曝光

web
上文提到有很多类库在用 IntersectionObserver 实现懒加载,但更精准的描述是,IntersectionObserver 提供了一种异步观察目标元素与根元素(窗口或指定父元素)的交叉状态的能力,这项能力不仅能用来做懒加载,还可以提供无限滚动,精...
继续阅读 »

上文提到有很多类库在用 IntersectionObserver 实现懒加载,但更精准的描述是,IntersectionObserver 提供了一种异步观察目标元素与根元素(窗口或指定父元素)的交叉状态的能力,这项能力不仅能用来做懒加载,还可以提供无限滚动,精准曝光的功能。


1. IntersectionObserver 基础介绍


不管我们使用哪个类库,都需要了解 IntersectionObserver 的基本原理,下面是一个简单的例子



import React, { useEffect } from "react";
import "./page.css";

const Page1 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;

useEffect(() => {
const io = new IntersectionObserver((entries) => {
console.log(entries[0].intersectionRatio);
});

const footer = document.querySelector(".footer");

if (footer) {
io.observe(footer);
}

return () => {
io.disconnect();
};
}, []);

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer">被观察的元素</div>
</div>

);
};

export default Page1;

如上例,可以了解到以下几点知识




  1. new 一个 IntersectionObserver 对象,下称 io,需传入一个函数,下称 callbackcallback 的入参 entries 代表了正在被观察的元素数组,数组的每一项都拥有属性 intersectionRatio ,代表了被观察的元素与根元素可视区域的交叉比例,。




  2. 使用 ioobserve 方法来添加你想观察的元素,可以多次调用添加多个,




  3. 使用 iodisconnect 方法来销毁观测




使用上方的代码,可以完成对元素最基本的观察。如上方 gif 操作,在控制台可得到以下结果 ,




  • 进入页面时,callback 被调用了一次:intersectionRatio 为 0

  • 滚动到可视区,再次调用:intersectionRatio > 0

  • 滚动出可视区,再次调用:intersectionRatio 为 0

  • 滚动到可视区,再次调用:intersectionRatio > 0


而懒加载,无限滚动,精准曝光是如何基于这个 api 去实现的呢,如果直接去写,当然也能实现,但是会有些繁琐,下面引入本篇文章的主角:react-intersection-observer 类库,先看看这个类库的基本介绍吧。


2. react-intersection-observer 基础介绍


这个类库在全局维护了一个 IntersectionObserver 实例(如果只有一个根元素,那全局仅有一个实例,实际上代码中维护了一个实例的 Map,此处简单表述),并提供了一个名为 useInViewhooks 方便我们了解到被观测的元素的观测状态。与上面相同的例子,他的写法如下:


import React, { useEffect } from "react";
import { useInView } from 'react-intersection-observer';
import "./page.css";

const Page2 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const { ref } = useInView({
onChange: (inView, entry) => {
console.log(entry.intersectionRatio);
}
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer" ref={ref}>被观察的元素</div>
</div>

);
};

export default Page2;

如上例,使用更少的代码,就实现了相同的功能,而且带来了一些好处



  • 不用自己维护 IntersectionObserver 实例,既不用关心创建,也不用关心销毁

  • 不用控制被观察的元素到底是 entries 内的第几个,观察事件都会在相应绑定的 onChange 中进行回调


以上仅为基本使用,实战中需求是更为复杂的,所以这个类库也提供了一系列属性,方便大家的使用:



利用上面这些配置项,我们可以实现以下功能


3. 实战用例


3.1. 懒加载


import React from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

interface Props {
width: number;
height: number;
src: string;
}

const LazyImage = ({ width, height, src, ...rest }: Props) => {
const { ref, inView } = useInView({
triggerOnce: true,
root: document.querySelector('.scroll-container'),
rootMargin: `0px 0px ${window.innerHeight}px 0px`,
onChange: (inView, entry) => {
console.log('info', inView, entry.intersectionRatio);
}
});

return (
<div
ref={ref}
style={{
position: "relative",
paddingBottom: `${(height / width) * 100}%`,
background: "#2a4b7a",
}}
>

{inView ? (
<img
{...rest}
src={src}
width={width}
height={height}
style={{ position: "absolute", width: "100%", height: "100%", left: 0, top: 0 }}
/>

) : null}
</div>

);
};

const Page3 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<LazyImage width={750} height={200} src={"https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4acf97e7dc944bf8ad5719b2b42f026~tplv-k3u1fbpfcp-watermark.image?"} />
</div>

);
};

export default Page3;

懒加载中我们需要用到几个额外的属性:




  • triggerOnce :只触发一次




  • root:默认为文档视口(如果被观察的元素,父/祖元素中有 overflow: scroll,需要指定为该元素)




  • rootMarginrootmargin




    • 同 css 上右下左写法,需要带单位,可简写('200px 0px')




    • 正值代表观察区域增大,负值代表观察区域缩小






在图片懒加载中,因为通常不可能等到元素被滚动到了可视区域,才开始加载图片,所以需要调整 rootMargin ,可以写为,rootMargin: `0px 0px ${window.innerHeight}px 0px ,这样图片可以提前一屏进行加载。


同样懒加载不需要不可见的时候回收掉相应的 dom ,所以只需要触发一次,设置 triggerOncetrue 即可。


3.2. 无限滚动



import React, { useState } from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

const Page4 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const [datas, setDatas] = useState([1, 1, 1]);
const { ref } = useInView({
onChange: (inView, entry) => {
console.log("inView", inView);
if (inView) {
setDatas((prevDatas) => [...prevDatas, ...new Array(3).fill(1)]);
}
},
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
{datas.map((item, index) => {
return (
<div key={index + 1} className="placeholder">
第{index + 1}个元素
</div>
);
})}
<div className="load-more" ref={ref}></div>
</div>

);
};

export default Page4;

无限滚动主要依赖在 onChange 中对 inView 进行判断,我们可以添加一个高度为0的元素,名为 load-more ,当页面滚动到最下方时,该元素的 onChange 会被触发,通过对 inViewtrue 的判断后,加载后续的数据。同理,真正的无限滚动也需要提前加载(在观察内写异步请求等),也可以设置相应的 rootMargin ,让无限滚动更丝滑。


3.3. 精准曝光



import React from "react";
import { useInView } from "react-intersection-observer";
import "./page.css";

const Page5 = (props: { handleShowTypeChange: (type: number) => void }) => {
const { handleShowTypeChange } = props;
const { ref } = useInView({
threshold: 0.5,
delay: 500,
onChange: (inView, entry) => {
if (inView) {
console.log("元素需要上报曝光事件", entry.intersectionRatio);
}
},
});

return (
<div className="scroll-container">
<button className="btn" onClick={() => handleShowTypeChange(0)}>
返回
</button>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="placeholder">其他元素</div>
<div className="footer" ref={ref}>
需要精准曝光的元素
</div>
</div>

);
};

export default Page5;

精准曝光也是很常见的业务需求,通常此类需求会要求元素的露出比例和最小停留时长。



  • 对露出比例要求的原因:因为有可能元素的有效信息并未展示,只是露出了一点点头,一般业务上会要求露出比例大于一半。

  • 对停留时长要求的原因:有可能用户快速划过,比如小说看到了很啰嗦的章节快速滑动,直接看后面结果,如果不加停留时长,中间快速滑动的区域也会曝光,与实际想要的不符。


类库恰好提供了下面两个属性方便大家的使用



  • threshold: 观察元素露出比例,取值范围 0~1,默认值 0

  • delay: 延迟通知元素露出(如果延迟后元素未达标,则不会触发onChange),取值单位毫秒,非必填。


使用上面两个属性,就可以轻松实现业务需求。


3.4. 官方示例


示例,官方示例中还有很多对属性的应用,比如 threshold 传入数组,skiptrack-visibility ,大家可自行体验。


总结


以上就是对 IntersectionObserver 以及 react-intersection-observer 的介绍了,希望能对大家有所帮助,文中录制的示例完整项目可以从此处获取。


作者:windyrain
来源:juejin.cn/post/7220309530910851130
收起阅读 »

字节都在用的代码自动生成

web
背景 如果有一份接口定义,前端和后端都能基于此生成相应端的代码,不仅能降低前后端沟通成本,而且还能提升研发效率。 字节内部的 RPC 定义主要基于 thrift 实现,thrift 定义了数据结构和函数,那么是否可以用来作为接口定义提供给前端使用呢?如果可以作...
继续阅读 »

背景


如果有一份接口定义,前端和后端都能基于此生成相应端的代码,不仅能降低前后端沟通成本,而且还能提升研发效率。


字节内部的 RPC 定义主要基于 thrift 实现,thrift 定义了数据结构和函数,那么是否可以用来作为接口定义提供给前端使用呢?如果可以作为接口定义,是不是也可以通过接口定义自动生成请求接口的代码呢?答案是肯定的,字节内部已经衍生出了多个基于 thrift 的代码生成工具,本篇文章主要介绍如何通过 thrift 生成前端接口调用的代码。


接口定义


接口定义,顾名思义就是用来定义接口的语言,由于字节内部广泛使用的 thrift 基本上满足接口定义的要求,所以我们不妨直接把 thrift 当成接口定义。


thrift 是一种跨语言的远程过程调用 (RPC) 框架,如果你对 Typescript 比较熟悉的话,那它的结构看起来应该很简单,看个例子:


namespace go namesapce

// 请求的结构体
struct GetRandomRequest {
1: optional i32 min,
2: optional i32 max,
3: optional string extra
}

// 响应的结构体
struct GetRandomResponse {
1: optional i64 random_num
}

// 定义服务
service RandomService {
GetRandomResponse GetRandom (1: GetRandomRequest req)
}

示例中的 service 可以看成是一组函数,每个函数可以看成是一个接口。我们都知道,对于 restful 接口,还需要定义接口路径(比如 /getUserInfo)和参数(query 参数、body 参数等),我们可以通过 thrift 注解来表示这些附加信息。


namespace go namesapce

struct GetRandomRequest {
1: optional i32 min (api.source = "query"),
2: optional i32 max (api.source =
"query"),
3: optional string extra (api.source = "body"),
}

struct GetRandomResponse
{
1: optional i64 random_num,
}

// Service
service RandomService {
GetRandomResponse GetRandom (1: GetRandomRequest req) (api.get = "/api/get-random"),
}

api.source 用来指定参数的位置,query 表示是 query 参数,body 表示 body 参数;api.get="/api/get-random" 表示接口路径是 /api/get-random,请求方法是 GET;


生成 Typescript


上面我们已经有了接口定义,那么对应的 Typescript 应该就呼之欲出了,一起来看代码:


interface GetRandomRequest {
min: number;
max: number;
extra: string;
}

interface GetRandomResponse {
random_num: number;
}

async function GetRandom(req: GetRandomRequest): Promise<GetRandomResponse> {
return request<GetRandomResponse>({
url: '/api/get-random',
method: 'GET',
query: {
min: req.min,
max: req.max,
},
body: {
extra: req.extra,
}
});
}


生成 Typescript 后,我们无需关心生成的代码长什么样,直接调用 GetRandom 即可。


架构设计


要实现基于 thrift 生成代码,最核心的架构如下:


image.png
因为 thrift 的内容我们不能直接拿来用,需要转化成中间代码(IR),这里的中间代码通常是 json、AST 或者自定义的 DSL。如果中间代码是 json,可能的结构如下:


{
name: 'GetRandom',
method: 'get',
path: '/api/get-random',
req_schema: {
query_params: [
{
name: 'min',
type: 'int',
optional: true,
},
{
name: 'max',
type: 'int',
optional: true,
}
],
body_params: [
{
name: 'extra',
type: 'string',
optional: true,
}
],
header_params: [],
},
resp_schema: {
header_params: [],
body_params: [],
}
}

为了保持架构的开放性,我们在核心链路上插入了 PrePlugin 和 PostPlugin,其中 PrePlugin 决定了 thrift 如何转化成 IR,PostPlugin 决定 IR 如何生成目标代码。


这里之所以是「目标代码」而不是「Typescript 代码」,是因为我希望不同的 PostPlugin 可以产生不同的目标代码,比如可以通过 TSPostPlugin 生成 Typescript 代码,通过 GoPostPlugin 生成 go 语言的代码。


总结


代码生成这块的内容还有很多可以探索的地方,比如如何解析 thrift?是找第三方功能生成 AST 还是通过 pegjs 解析成自定义的 DSL?多文件联编如何处理、字段名 case 如何转换、运行时类型校验、生成的代码如何与 useRequest 或 ReactQuery 集成等。


thrift 其实可以看成接口定义的具体实现,如果 thrift 不满足你的业务场景,也可以自己实现一套类似的接口定义语言;接口定义作为前后端的约定,可以降低前后端的沟通成本;代码生成,可以提升前端代码的质量和研发效率。


如果本文对你有启发,欢迎点赞、关注、留言交流。


作者:探险家火焱
来源:juejin.cn/post/7220054775298359351
收起阅读 »

前端怎么样限制用户截图?

web
做后台系统,或者版权比较重视的项目时,产品经常会提出这样的需求:能不能禁止用户截图?有经验的开发不会直接拒绝产品,而是进行引导。 先了解初始需求是什么?是内容数据过于敏感,严禁泄漏。还是内容泄漏后,需要溯源追责。不同的需求需要的方案也不同。来看看就限制用户截图...
继续阅读 »

做后台系统,或者版权比较重视的项目时,产品经常会提出这样的需求:能不能禁止用户截图?有经验的开发不会直接拒绝产品,而是进行引导。


先了解初始需求是什么?是内容数据过于敏感,严禁泄漏。还是内容泄漏后,需要溯源追责。不同的需求需要的方案也不同。来看看就限制用户截图,有哪些脑洞?


有哪些脑洞


v站和某乎上的大佬给出了不少脑洞,我又加了点思路。


1.基础方案,阻止右键保存和拖拽。


这个方案是最基础,当前可只能阻拦一些小白用户。如果是浏览器,分分钟调出控制台,直接找到图片url。还可以直接ctrl+p,进入打印模式,直接保存下来再裁减。


2.失焦后加遮罩层


这个方案有点意思,看敏感信息时,必须鼠标点在某个按钮上,照片才完整显示。如果失去焦点图片显示不完整或者直接遮罩盖住。


3.高速动态马赛克


这个方案是可行的,并且在一些网站已经得到了应用,在视频或者图片上随机插像素点,动态跑来跑去,对客户来说,每一时刻屏幕上显示的都是完整的图像,靠用户的视觉残留看图或者视频。即时手机拍照也拍不完全。实际应用需要优化的点还是挺多的。比如用手机录像就可以看到完整内容,只是增加了截图成本。


下面是一个知乎上的方案效果。(原地址):


image.png


正经需求vs方案


其实限制用户截图这个方案本身就不合理,除非整个设备都是定制的,在软件上阉割截图功能。为了这个需求添加更复杂的功能对于一些安全性没那么高的需求来说,有点本末倒置了。


下面聊聊正经方案:


1.对于后台系统敏感数据或者图片,主要是担心泄漏出去,可以采用斜45度七彩水印,想要完全去掉几乎不可能,就是观感比较差。


2.对于图片版权,可以使用现在主流的盲水印,之前看过腾讯云提供的服务,当然成本比较高,如果版权需求较大,使用起来效果比较好。


3.视频方案,tiktok下载下来的时候会有一个水印跑来跑去,当然这个是经过处理过的视频,非原画,画质损耗也比较高。Netflix等视频网站采用的是服务端权限控制,走的视频流,每次播放下载加密视频,同时获得短期许可,得到许可后在本地解密并播放,一旦停止播放后许可失效。


总之,除了类似于Android提供的截图API等底层功能,其他的功能实现都不完美。即使是底层控制了,一样可以拍照录像,没有完美的方案。不过还是可以做的相对安全。


你还有什么新思路吗?有的话咱评论区见,欢迎点赞收藏关注,感谢!


作者:正经程序员
来源:juejin.cn/post/7127829348689674253
收起阅读 »

Vue 实现接口进度条

web
前端在向后端请求信息时,常常需要等待一定的时间才能得到返回结果。为了提高用户体验,可以通过实现一个接口进度条函数来增加页面的交互性和视觉效果。 接口响应快 - 效果 接口响应慢 - 效果 实现思路 首先定义一个进度条组件来渲染页面展示效果,组件包含进度条背...
继续阅读 »

cover.png


前端在向后端请求信息时,常常需要等待一定的时间才能得到返回结果。为了提高用户体验,可以通过实现一个接口进度条函数来增加页面的交互性和视觉效果。



接口响应快 - 效果



接口响应慢 - 效果


实现思路


首先定义一个进度条组件来渲染页面展示效果,组件包含进度条背景、进度长度、以及进度数字,同时还要设置数据绑定相关属性,如进度条当前的百分比、动画执行状态、以及完成状态等。在请求数据的过程中,需要添加监听函数来监测数据请求的过程变化,并更新组件相应的属性和界面元素。


代码实现


下面是使用 Vue 实现一个接口进度条的栗子:


<template>
<div class="progress-bar">
<div class="bg"></div>
<div class="bar" :style="{ width: progress + '%' }"></div>
<div class="label">{{ progress }}%</div>
</div>
</template>

<script>
export default {
data() {
return {
progress: 0,
isPlaying: false,
isCompleted: false
}
},
mounted() {
this.start();
},
methods: {
start() {
this.isPlaying = true;
this.animateProgress(90)
.then(() => {
if (!this.isCompleted) {
this.animateProgress(100);
}
})
.catch((error) => {
console.error('Progress error', error);
});
},
animateProgress(target) {
return new Promise((resolve, reject) => {
let start = this.progress;
const end = target;
const duration = (target - start) * 150;

const doAnimation = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);

this.progress = start + ((end - start) * progress);

if (progress === 1) {
resolve();
} else if (this.isCompleted) {
resolve();
} else {
requestAnimationFrame(doAnimation);
}
};

const startTime = Date.now();
requestAnimationFrame(doAnimation);
});
},
finish() {
this.isCompleted = true;
this.progress = 100;
}
}
};
</script>

<style scoped>
.progress-bar {
position: relative;
height: 8px;
margin: 10px 0;
}
.bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #ccc;
border-radius: 5px;
}
.bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
border-radius: 5px;
background-color: #409eff;
transition: width 0.5s;
}
.label {
position: absolute;
top: -20px;
left: calc(100% + 5px);
color: #333;
font-size: 12px;
}
</style>

首先定义了三个数据属性用于控制动画的播放和完成状态,分别是进度条当前比例 progress、动画播放状态 isPlaying、动画完成状态 isCompleted。在组件初始化的过程中,调用了 start 方法来启动进度条动画效果。在该方法内部,使用 Promise 来从 0% 到 90% 的百分比向相应位置移动,并在到达该位置时停止。


判断当前是否完成,如果没有完成则再次调用 animateProgress(100) ,并在进度加载期间检查是否有数据返回。若存在,则停止前半段动画,并使用1秒钟将进度条填充至100%。


下面讲解一下如何在请求数据的过程中添加监听函数:


import axios from 'axios';
import ProgressBar from './ProgressBar.vue';

const progressBar = new Vue(ProgressBar).$mount();
document.body.appendChild(progressBar.$el);

在这个代码片段中,使用了 Axios 拦截器来监听请求的过程。在请求开始之前,向页面添加进度条组件,之后将该组件挂载到页面中,并且将其元素追加到 HTML 的 <body> 标记尾部。


接下来,通过 onDownloadProgress 监听函数来监测下载进度的变化。如果加载完成则移除进度条组件。同时,也可以实现针对使用不同 API 的 ajax 请求设定不同的进度条,以达到更佳的用户体验效果。


axios.interceptors.request.use((config) => {    
const progressBar = new Vue(ProgressBar).$mount();
document.body.appendChild(progressBar.$el);

config.onDownloadProgress = (event) => {
if (event.lengthComputable) {
progressBar.progress = parseInt((event.loaded / event.total) * 100, 10);
if (progressBar.progress === 100) {
progressBar.finish();
setTimeout(() => {
document.body.removeChild(progressBar.$el);
}, 500);
}
}
};

return config;
}, (error) => {
return Promise.reject(error);
});

参数注入


为了能够灵活地调整接口进度条效果,可以使用参数注入来控制动画速度和完成时间的设定。在 animateProgress 函数中,使用传参来设置百分比范围和动画播放速度,从而得到不同进度条和播放时间的效果。


animateProgress(target, duration) {
return new Promise((resolve, reject) => {
let start = this.progress;
const end = target;

const doAnimation = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);

this.progress = start + ((end - start) * progress);

if (progress === 1) {
resolve();
} else if (this.isCompleted) {
resolve();
} else {
requestAnimationFrame(doAnimation);
}
};

const startTime = Date.now();
requestAnimationFrame(doAnimation);
});
}

...

this.animateProgress(90, 1000)
.then(() => {
if (!this.isCompleted) {
this.animateProgress(100, 500);
}
})
...

在这个栗子中,将 duration 参数添加到 animateProgress 函数内部,并使用该参数来设置动画速度和完成时间。在第一个调用函数的时候,将异步进度条的播放时间设为 1000ms,从而得到速度较慢、完成时间较长的进度条效果。在第二个调用函数时,将进度条完成时间缩短为 500ms,并获得由此带来的更快动画效果。


总结


实现一个接口进度条函数可以提高网站性能和用户体验效果,同时也可以让页面更加交互性和生动有趣。在栗子中,使用了 Vue 框架来构建动画组件,使用了 Axios 拦截器来监听请求进度,使用了参数注入来控制动画速度和完成时间。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7225417805855916087
收起阅读 »

Js中异步代码挂起怎么解决?

web
从下面代码引入问题 function a() { console.log('aa'); } function b() { setTimeout(() => { //异步代码 console.log('bb'); ...
继续阅读 »

从下面代码引入问题


function a() {
console.log('aa');
}

function b() {
setTimeout(() => { //异步代码
console.log('bb');
}, 1000)
}

function c() {
console.log('cc');
}

a()
b()
c()

上述代码的执行结果为先打印'aa',再打印'cc',等一秒后再打印'bb'。哎?我们是不是就有疑问了,我们明显是先调用的函数a,再调用的函数b,最后调用的函数c,为什么函数b的打印结果最后才出来呢?这里我们要清楚的是函数b中定义了一个计时器,执行此代码是需要时间的,属于异步代码,当浏览器执行到此代码时,会先将此程序挂起,继续往下执行,最后才会执行异步代码。那要怎么解决此类问题呢?一个方法是将其他函数体内也定义一个计时器,这样也就会按顺序调用了,但是这样太不优雅了;还一个方法是函数c作为参数传入函数b,在函数b中执行掉,这样也不优雅。es6出来后就可以使用promise来解决此问题了。


js是一种单线程语言


什么是单线程?


我们可以理解为一次只能完成一个任务,如果有其他任务进来,那就需要排队了,一个任务完成了接着下一个任务。



因为js是一种单线程语言,任务是按顺序执行的,但是有时我们有多个任务同时执行的需求,这就需要异步编程的思想。



什么是异步?


当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情。


什么是异步模式调用? 


前一个任务执行完,调用回调函数而不是进行后一个任务。后一个任务不等前一个任务结束就执行,任务排列顺序与执行顺序无关。


什么是回调函数?


把函数当作参数传入另一个函数中,不会立即执行,当需要用这个函数时,再回调运行()这个函数。



以前是通过回调函数实现异步的,但是回调用多了会出现回调地狱,导致爆栈。



举个用回调函数来解决异步代码挂起问题


<body>
<div class="box">
<audio src="" id="audio" controls></audio> </audio>
</div>
<script>
//ajax
let url = ''
function getSong(cb) {
$.ajax({
url: ' 数据地址',
dataType: 'json',
success(res) {
console.log(res);
url = res[0].url
cb()
}
})
}
getSong(playSong)

function playSong() {
let audio = document.getElementById('audio')
window.addEventListener('click', () => {
audio.src = url
window.onclick = function () {
audio.play()
}
})
}

</script>
</body>

代码中用ajax向后端获取数据,这是需要时间的,属于异步代码,当我们分开调用这两个函数,函数getSong中的异步代码会出现挂起状态,导致函数playSong中的url获取不到值,会出现报错的情况,运用回调函数可以很好地解决这个问题。


Promise的使用


先执行一段代码


function xq() {

setTimeout(() => {
console.log('老王');
}, 2000)

}


function marry() {
setTimeout(() => {
console.log('老王结婚了');
}, 1000)
}


function baby() {
setTimeout(() => {
console.log('小王出生了');
}, 500)
}
xq()
marry()
baby()

结果为


1.png


???这是不是有点违背了道德,只能说老王是个渣男。这时候我们就需要使用promise对象来调整一下顺序了。


function xq() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('老王去相亲');
resolve('ok')
}, 2000)
})
}


function marry() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('老王结婚了');
resolve('ok')
}, 1000)
})
}


function baby() {
setTimeout(() => {
console.log('小王出生了');
}, 500)
}


// xq().then(() => {
// marry().then(() => {
// baby()
// })
// })
xq()
.then(marry)
.then(baby)
// xq().then(marry)
// baby()

在这里我们可以理解为老王相亲的时候疯狂对相亲对象promise,才有了后面的步入婚姻的殿堂,结婚后想生个娃也要对妻子疯狂的promise,才有了后面的小王出生了。


老王长叹了一口气,终于通过promise挽回了形象。


小结


Js异步编程方法不只这两种,还有比如事件监听,发布/订阅,生成器函数 Generator/yield等。需要我们一起去探索研究,毕竟‘学无止境’。


作者:一拾九
来源:juejin.cn/post/7225257817345884221
收起阅读 »

😈当一个摆子前端太闲的时候会做什么

国破山河在,城春草木深。 ——杜甫·春望 今日周一,在下与诸位同道中人一起来讨论一个话题:当一个前端空闲的时候会做些什么。 🤯是独自深耕论坛,钻研学术? 👯还是三两闲聊打趣,坐而论道? 💆‍♂️亦或是闭目养神,神游天地? 作为一名优秀的(摆子、摸鱼、切图....
继续阅读 »

国破山河在,城春草木深。 ——杜甫·春望



今日周一,在下与诸位同道中人一起来讨论一个话题:当一个前端空闲的时候会做些什么


🤯是独自深耕论坛,钻研学术?


👯还是三两闲聊打趣,坐而论道?


💆‍♂️亦或是闭目养神,神游天地?




作为一名优秀的(摆子、摸鱼、切图...)前端开发者,在下在空闲时间最喜欢做的还是钻研(混)前端技术(工作量)。


新的一周,新的开始,上篇文章中有同学批评在下说不够“玩”,那么这周就“简单”画一个鼠标精灵再交予各位“玩一玩”吧。



说明一下:在下说的玩,是写一遍嗷


温馨提示:文章较长,图片较多,不耐看的同学可以先去文末玩一玩在下的“大眼”,不满足了再去创造属于各位自己的鼠标精灵



以下是这周“玩具”的简单介绍:



  • 名称:大眼

  • 生辰:发文时间的昨天(2022-08-15)

  • 性别:随意

  • 情绪:发怒/常态

  • 状态:休眠/工作中

  • 简介:没啥特别的,大眼就干一件事,就是盯着你的鼠标,以防你找不到鼠标了。不过大眼有起床气,而且非常懒散,容易犯困。


大眼生活照:


image.png


接下来请各位跟随在下的节奏,一步一步把自己的“大眼”创造出来。


👀 画“大眼”先画圆


老话说“画人先画骨”,同样画大眼也得先画它的骨,嗯......也就是个圆,没错,就是个普通的圆


在下的笔法还是老套路,先给他一个容器。


<div class="eyeSocket"></div>

给大眼容器添加一些必要的样式


body {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #111;
}
.eyeSocket {
position: absolute; // 浮动居中
left: calc(50% - 75px);
top: calc(50% - 75px);
width: 150px; // 固定宽度
aspect-ratio: 1; // 长宽比 1:1 如果浏览器不支持该属性,换成 height: 150px 也一样
border-radius: 50%;
border: 4px solid rgb(41, 104, 217);
z-index: 1;
}

效果:


image.png


然后就是另外两个圆和一些阴影效果,由于另外两个圆没有特殊的动效,所以在下使用两个伪元素来实现


.eyeSocket::before,
.eyeSocket::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); // 居中
border-radius: 50%;
box-sizing: border-box; // css3盒子模型
}
.eyeSocket::before {
width: calc(100% + 20px);
height: calc(100% + 20px);
border: 6px solid #02ffff;
}
.eyeSocket::after {
width: 100%;
height: 100%;
border: 4px solid rgb(35, 22, 140);
box-shadow: inset 0px 0px 30px rgb(35, 22, 140);
}

效果:


image.png


👀 画龙需点睛


大眼的眼球画好了,之后就需要给它点上眼睛,喜欢什么样的眼睛因人而异,在下就选择这种分割线来作为大眼的眼仁。


为了方便做一些过渡效果,在下使用echarts来完成这个眼仁。


首先在下需要各位通过任何方式引入echarts库,然后给眼仁一个容器,并初始化echarts画布。


<div class="eyeSocket">
<div id="eyeball"></div>
</div>

#eyeball {
width: 100%;
height: 100%;
}

// 画眼球
let eyeball = document.getElementById('eyeball'); // 获取eyeball元素
let eyeballChart = echarts.init(eyeball); // 初始化画布
function getEyeballChart() {
eyeballChart.setOption({
series: [
{
type: 'gauge', // 使用仪表盘类型
radius: '-20%', // 采用负数是为了让分割线从内向外延伸
clockwise: false,
startAngle: '0', // 起始角度
endAngle: '270', // 结束角度
splitNumber: 3, // 分割数量,会将270度分割为3份,所以有四根线
detail: false,
axisLine: {
show: false,
},
axisTick: false,
splitLine: {
show: true,
length: 12, // 分割线长度
lineStyle: {
shadowBlur: 20, // 阴影渐变
shadowColor: 'rgb(0, 238, 255)', // 阴影颜色
shadowOffsetY: '0',
color: 'rgb(0, 238, 255)', // 分割线颜色
width: 4, // 分割线宽度
}
},
axisLabel: false
},
{
type: 'gauge',
radius: '-20%',
clockwise: false,
startAngle: '45', // 倾斜45度
endAngle: '315',
splitNumber: 3,
detail: false,
axisLine: {
show: false,
},
axisTick: false,
splitLine: {
show: true,
length: 12,
lineStyle: {
shadowBlur: 20,
shadowColor: 'rgb(0, 238, 255)',
shadowOffsetY: '0',
color: 'rgb(0, 238, 255)',
width: 4,
}
},
axisLabel: false
}
]
})
}
getEyeballChart();

效果:


image.png


眼仁就这么轻轻松松的画好了,对于常用echarts的同学可以说是轻而易举,对吧。


同时一个静态的大眼也创建完毕,接下来就要给大眼赋予生命了。



再次提醒:长文,而且代码量多,建议抽思路看即可。



✨ 生命仪式:休眠状态


赋予生命是神圣的,她需要一个过程,所以在下从最简单的开始——休眠状态


在下给大眼设计的休眠状态,就是闭着眼睛睡觉,其实不露出眼仁同时有节奏的呼吸(缩放)罢了,相比于整个生命仪式来说,还是比较简单的,只需要修改大眼外框的大小即可。


呼吸

这里在下采用的是css转换+动画的方式


<div class="eyeSocket eyeSocketSleeping">
<div id="eyeball"></div>
</div>

/* ...其他样式 */
.eyeSocketSleeping {
animation: sleeping 6s infinite;
}

@keyframes sleeping {
0% {
transform: scale(1);
}

50% {
transform: scale(1.2);
}

100% {
transform: scale(1);
}
}

sleeping.gif


闭眼

搞定了呼吸,但是睁着眼睛怎么睡得着?


所以接下来在下要帮助大眼把眼睛闭上,这时候咱们前面给眼睛设置负数radius的好处就来了(其实是在下设计好的),因为分割线是从内向外延伸的,所以此时只需要慢慢减小分割线的高度,即可实现眼睛慢慢缩小的效果,即在下给大眼设计的闭眼效果。


实现的效果是:大眼慢慢闭上眼睛(分割线缩小至0),然后开始呼吸


直接上代码


<div class="eyeSocket" id='bigEye'> // 去掉 eyeSocketSleeping 样式,添加id
<div id="eyeball"></div>
</div>

let bigEye = document.getElementById('bigEye'); // 获取元素
// ...其他代码
let leftRotSize = 0; // 旋转角度
let ballSize = 12; // 眼睛尺寸
let rotTimer; // 定时器

function getEyeballChart() {
eyeballChart.setOption({
series: [
{
startAngle: `${0 + leftRotSize * 5}`, // 加为逆时针旋转,乘5表示速度为leftRotSize的倍
endAngle: `${270 + leftRotSize * 5}`, // 即变为每10微秒移动0.5度,1234678同理
// ...其他
splitLine: {
length: ballSize, // 分割线高度设置为眼球尺寸变量
},
},
{
startAngle: `${45 + leftRotSize * 5}`,
endAngle: `${315 + leftRotSize * 5}`,
// ...其他
splitLine: {
length: ballSize, // 同上
}
},
}
]
})
}
// 休眠
function toSleep() {
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart()
if (ballSize > 0) {
ballSize -= 0.1; // 当眼球存在时慢慢减小
} else {
bigEye.className = 'eyeSocket eyeSocketSleeping'; // 眼球消失后添加呼吸
}
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1); // 旋转,
}, 10);
}
getEyeballChart();
toSleep()


旋转实现原理:(看过在下第一篇动效的同学对旋转的实现原理应该不陌生)


修改每个圈的起始角度(startAngle)和结束角度(endAngle),并不断刷新视图,


增加度数为逆时针旋转,减去度数为顺时针旋转



如此一来就实现了眼睛缩小消失,然后开始呼吸的过程,同时咱们的大眼也进入了生命仪式之休眠状态(乱入的鼠标有点烦);


tosleep.gif


✨ 生命仪式:起床气状态


在下相信,在座(站?蹲?)的各位同僚身边或者自身都存在一些小毛病,譬如咱们的大眼,它不但懒,喜欢睡觉,甚至叫醒它还会生气,通俗讲就是有起床气


心理学上说有种说法是:情绪会让你接近生命的本真


生命不就是情绪的结合嘛,没有情绪怎么能称之为生命的呢?


在设计之前我们还有点准备工作,就是让大眼先处于休眠状态


<div class="eyeSocket eyeSocketSleeping" id='bigEye'> // 添加休眠
<div id="eyeball"></div>
</div>

// ...其他代码
let ballSize = 0; // 初始眼球尺寸为0
// ...其他代码
// getEyeballChart(); // 把这两行删掉
// toSleep() // 把这两行删掉

唤醒

然后我们需要唤醒大眼,所以首先我们需要添加唤醒动作——点击事件;


let bigEye = document.getElementById('bigEye'); // 获取元素
// ...其他代码
let leftRotSize = 0;
let ballSize = 0;
let rotTimer;
let isSleep = true; // 是否处于休眠状态
// 添加点击事件,当处于休眠状态时执行唤醒方法
bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})
// 唤醒
function clickToWeakup() {
isSleep = false; // 修改状态
bigEye.className = 'eyeSocket'; // 清除休眠状态
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart()
ballSize <= 12 && (ballSize += 0.1);
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
}, 10);
}

这样点一下大眼它就苏醒了过来:


toWeakup.gif


生气

但是!


这是一个没有情绪的大眼,而在下需要的是一个有起床气的大眼,所以这样的大眼咱们不要!


退格←...退格←...退格←...退格←...退格←...退格←......


......


慢点慢点,也不是全都不要了,咱们只需要修改一下他唤醒以后的操作,给他添加上起床气不就行了?


接着来吧:


首先我们把代表了大眼常态的蓝色系抽离出来,使用css变量代替,然后再苏醒后给他添加成代表生气的红色系


body {
width: 100vw;
height: 100vh;
overflow: hidden;
background-color: #111;
perspective: 1000px;
--c-eyeSocket: rgb(41, 104, 217);
--c-eyeSocket-outer: #02ffff;
--c-eyeSocket-outer-shadow: transparent;
--c-eyeSocket-inner: rgb(35, 22, 140);
}
.eyeSocket {
/* 其他属性 */
border: 4px solid var(--c-eyeSocket);
box-shadow: 0px 0px 50px var(--c-eyeSocket-outer-shadow); /* 当生气时添加红色外发光,常态则保持透明 */
transition: border 0.5s ease-in-out, box-shadow 0.5s ease-in-out; /* 添加过渡效果 */
}
.eyeSocket::before,
.eyeSocket::after {
/* 其他属性 */
transition: all 0.5s ease-in-out; /* 添加过渡效果 */
}
.eyeSocket::before {
/* 其他属性 */
border: 6px solid var(--c-eyeSocket-outer);
}
.eyeSocket::after {
/* 其他属性 */
border: 4px solid var(--c-eyeSocket-inner);
box-shadow: inset 0px 0px 30px var(--c-eyeSocket-inner);
}

// ...其他代码
let ballColor = 'transparent'; // 默认透明,其实默认是啥都无所谓,反正看不见

function getEyeballChart() {
eyeballChart.setOption({
series: [
{
// ...其他
splitLine: {
// ...其他
lineStyle: {
// ...其他
shadowColor: ballColor, // 把眼睛的眼影颜色设为变量控制
color: ballColor,
}
},
},
{
// ...其他
splitLine: {
// ...其他
lineStyle: {
// ...其他
shadowColor: ballColor,
color: ballColor,
}
}
},
}
]
})
}
// 生气模式
function setAngry() {
// 通过js修改body的css变量
document.body.style.setProperty('--c-eyeSocket', 'rgb(255,187,255)');
document.body.style.setProperty('--c-eyeSocket-outer', 'rgb(238,85,135)');
document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'rgb(255, 60, 86)');
document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(208,14,74)');
ballColor = 'rgb(208,14,74)';
}
// 常态模式
function setNormal() {
document.body.style.setProperty('--c-eyeSocket', 'rgb(41, 104, 217)');
document.body.style.setProperty('--c-eyeSocket-outer', '#02ffff');
document.body.style.setProperty('--c-eyeSocket-outer-shadow', 'transparent');
document.body.style.setProperty('--c-eyeSocket-inner', 'rgb(35, 22, 140)');
ballColor = 'rgb(0,238,255)';
}
// 唤醒
function clickToWeakup() {
isSleep = false;
bigEye.className = 'eyeSocket';
setAngry(); // 设置为生气模式
clearInterval(rotTimer);
rotTimer = setInterval(() => {
getEyeballChart()
ballSize <= 50 && (ballSize += 1);
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.5);
}, 10);
}
// 点击
bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

大眼生气长这样:


angry.gif


更生气

不知道在座(站?蹲擦?)各位是如何看待,但是在下看来,大眼这样好像还不够生气。


没错还不够生气,如何让大眼起来更生气呢,生气到发火如何?


嗦干酒干!


在下这里采用的是svg滤镜的方法,svg滤镜的属性和使用方法非常繁多,在下使用得也不是很娴熟,本文中在下就不赘述了,网上冲浪有许多技术大牛讲的非常好,希望各位勉励自己。emmmm......然后来教会在下,记得给在下留言文章地址


在下使用的是feTurbulence来形成噪声,然后用feDisplacementMap替换来给大眼添加粒子效果,因为feDisplacementMap会混合掉元素,所以在下需要给大眼新增一个大眼替身来代替大眼被融合。


创建大眼替身


<div class="filter"> // 添加滤镜的元素
<div class="eyeSocket" id='eyeFilter'> // 大眼替身
</div>
</div>

.filter {
width: 100%;
height: 100%;
}
.eyeSocket,
.filter .eyeSocket { /* 给替身加上相同的样式 */
/* ...原属性 */
}

image.png


融合


<div class="filter">
<div class="eyeSocket" id='eyeFilter'>
</div>
</div>
<!-- Svg滤镜 -->
<svg width="0">
<filter id='filter'>
<feTurbulence baseFrequency="1">
<animate id="animate1" attributeName="baseFrequency" dur="1s" from="0.5" to="0.55" begin="0s;animate1.end">
</animate>
<animate id="animate2" attributeName="baseFrequency" dur="1s" from="0.55" to="0.5" begin="animate2.end">
</animate>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" scale="50" xChannelSelector="R" yChannelSelector="B" />
</filter>
</svg>

.filter {
width: 100%;
height: 100%;
filter: url('#filter'); /* 开启滤镜 */
}

copy.gif


芜湖~果然献祭了一只“大眼”出来的效果看起来确实还不错哈?确实看起来酷炫多了,不愧是**“献祭”**啊!


真眼出现


既然粒子效果已经产生,咱们的真实大眼也就不需要躲躲藏藏了,该站出来获取这粒子“光环”了!


大眼:哈!


fire.gif


额......


其实......


也挺好看的嘛,不是吗?毕竟不是献祭的真正的大眼,毕竟是个替身,效果没有本体好也是很正常的对吧。



本质上是因为feDisplacementMap设置了scale属性的原因。


feDisplacementMap其实就是一个位置替换滤镜,通过就是改变元素和图形的像素位置的进行重新映射,然后替换一个新的位置,形成一个新的图形。


scale就是替换公式计算后偏移值相乘的比例,影响着图形的偏移量和呈现的效果。



但是话虽如此,咱这个光环不能真的就这么戴着呀,咱们还需要对光环的位置进行一些微调。


.filter .eyeSocket {
left: calc(50% - 92px);
top: calc(50% - 92px);
}

goodfire.gif


看看,看看!这不就顺眼多了吗,献祭了替身,所以尺寸都是非常契合的,而且共用了样式,所以当大眼生气的时候,光环也会跟着生气。


这下光环也有了,看起来的确比之前更生气了。


但是我们还需要对大眼做一些细微的调整,因为大眼在常规状态下并不需要这个光环,睡着的时候光环在旁边“滋啦滋啦”不吵的慌么,所以我们还需要把常态下的大眼光环给消除掉。


在下采用的是不透明度opacity来控制,当大眼处于生气状态时,光环为不透明;处于常规状态时光环透明不可见。


.filter .eyeSocket {
opacity: 0; // 默认状态下不透明度为0
left: calc(50% - 92px);
top: calc(50% - 92px);
transition: all 0.5s ease-in-out; // 添加过渡效果,值得注意的是不能丢了原本的过渡效果,所以这里使用all
}

// ...其他代码
let eyeFilter = document.getElementById('eyeFilter'); // 获取元素
// 唤醒
function clickToWeakup() {
eyeFilter.style.opacity = '1'; // 不透明度设为1
// ...其他
}
deathEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

这样设置完,一个更生气的大眼就这样出现了:


moreAngry.gif


更更生气


不知看到这样发火的大眼,各位是不是已经满足于此。


但是在下认为不,在下觉得一个真正足够生气的大眼,不只局限于自己生气,还需要找人发泄!!


所以在下还给大眼添加了一些大眼找人的动效(当然是找不到的,它这么笨)。


其实就是让大眼左右旋转,通过css转换来实现。


<div class="eyeSocket eyeSocketSleeping" id='bigEye'>
<div id="eyeball"></div>
</div>
<div class="filter">
<div class="eyeSocket" id='eyeFilter'>
</div>
</div>
<!-- Svg滤镜 -->
<svg width="0">
...
</svg>

/* ...其他样式 */
body {
/* ...其他属性 */
perspective: 1000px;
}
.eyeSocketLooking {
animation: lookAround 2.5s; // 添加动画,只播放一次
}
/* 环视动画 */
@keyframes lookAround {
0% {
transform: translateX(0) rotateY(0);
}

10% {
transform: translateX(0) rotateY(0);
}

40% {
transform: translateX(-70px) rotateY(-30deg);
}

80% {
transform: translateX(70px) rotateY(30deg);
}

100% {
transform: translateX(0) rotateY(0);
}
}

// ...其他代码
let bigEye = document.getElementById('bigEye'); // 获取元素
let eyeFilter = document.getElementById('eyeFilter');

// 唤醒
function clickToWeakup() {
// ...其他代码
eyeFilter.className = bigEye.className = 'eyeSocket eyeSocketLooking'; // 同时给大眼和光环添加环视动画
}

bigEye.addEventListener('click', () => {
if (!isSleep) return;
clickToWeakup();
})

看看大眼在找什么?


lookaround.gif



向左看时,Y轴偏移量为-70px,同时按Y轴旋转-30°


向右看时,Y轴偏移量为70px,同时Y轴旋转30°



✨ 生命仪式:自我调整状态


这个状态非常好理解,大眼虽然有起床气,但是也仅限于起床对吧,总不能一直让它生气,气坏了咋办,带着情绪工作,效果也不好不是吗。


所以我们还需要给它一点时间,让它自我调整一下,恢复成正常状态。


这个自我调整状态就是一个从生气状态变回常态的过程,在这个过程中,大眼需要将生气状态的红色系切换为常态的蓝色系,同时红眼也会慢慢褪去恢复正常。


其实这个自我调整状态还是属于唤醒状态中,只是需要放在起床气状态之后。


这里在下采纳了上文中有位同学给的建议,监听动画结束事件webkitAnimationEnd,然后将自我调整放在动画结束以后。


同时这里也有两个步骤:



  1. 退出起床气状态

  2. 变回常态


为了保证两个步骤的先后顺序,可以使用Promise来实现。不懂Promise的同学可以先去学习一下,在下也讲不清楚哈哈哈哈。


// ...其他代码
bigEye.addEventListener('webkitAnimationEnd', () => { // 监听动画结束事件
new Promise(res => {
clearInterval(rotTimer); // 清除定时器
rotTimer = setInterval(() => {
getEyeballChart(); // 更新视图
ballSize > 0 && (ballSize -= 0.5); // 眼球尺寸减小
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
if (ballSize === 0) { // 当眼球尺寸为0时,将Promise标记为resolved,然后执行后面的代码
clearInterval(rotTimer);
res();
}
}, 10);
}).then(() => {
eyeFilter.style.opacity = '0'; // 清除光环
eyeFilter.className = bigEye.className = 'eyeSocket'; // 清除环视动画
setNormal(); // 设置常态样式
rotTimer = setInterval(() => {
getEyeballChart();
ballSize <= 12 && (ballSize += 0.1); // 眼球尺寸缓慢增加
leftRotSize === 360 ? (leftRotSize = 0) : (leftRotSize += 0.1);
}, 10);
})
})

添加了这样一个监听事件后,咱们的大眼就已经具备了自我调整的能力了:


back.gif


✨ 生命仪式:工作状态


接下来就到了大眼重中之重的环节,也就是大眼的工作状态


在下给大眼的工作非常简单,就是单纯的盯住在下的鼠标,如果各位想给各自的大眼一些其他的功能,可以自己发挥。


盯住鼠标,不只是说说而已,那么怎么样才能让大眼表现出他已经盯住了呢?


在下的思路是:



  1. 以大眼的位置为原点建立一个直角坐标系

  2. 然后通过监听鼠标移动事件,获取鼠标所在位置,计算出鼠标处于大眼坐标系的坐标。

  3. 将整个视口背景以X轴和Y轴进行等分成无数个旋转角度,通过鼠标坐标的数值和正负来调整大眼眼框和眼睛的Y轴和Z轴旋转,从而达到盯住鼠标的目的。


好的,咱们理清思路,接下来就是付诸于行动。


// ...其他代码
// 工作
function focusOnMouse(e) {
{
// 视口尺寸,获取到整个视口的大小
let clientWidth = document.body.clientWidth;
let clientHeight = document.body.clientHeight;
// 原点,即bigEye中心位置,页面中心
let origin = [clientWidth / 2, clientHeight / 2];
// 鼠标坐标
let mouseCoords = [e.clientX - origin[0], origin[1] - e.clientY];
// 旋转角度
let eyeXDeg = mouseCoords[1] / clientHeight * 80; // 这里的80代表的是最上下边缘大眼X轴旋转角度
let eyeYDeg = mouseCoords[0] / clientWidth * 60;
bigEye.style.transform = `rotateY(${eyeYDeg}deg) rotateX(${eyeXDeg}deg)`;
eyeball.style.transform = `translate(${eyeYDeg / 1.5}px, ${-eyeXDeg / 1.5}px)`;
}
}


注意: 如果觉得旋转角度不够大,可以调整代码中的8060,最大可以到180,也就是完全朝向,但是由于大眼终归是一个平面生物,如果旋转度数过大,就很容易穿帮,如果旋转角度为180,大眼就会在某个方向完全消失看不见(因为大眼没有厚度,这个也许是可以优化的点),所以个人喜好调整吧。



咱们来看看大眼工作时的飒爽英姿:


watching.gif


✨ 生命仪式:懒惰状态


顾名思义,懒惰状态就是......懒惰状态。


在下给大眼设计的懒惰状态就是当在下的鼠标超过30秒没有移动时,大眼就会进入休眠状态


所以生命仪式的最后收尾其实非常的轻松,没有大量的代码,只需要添加一个定时器,然后修改休眠状态的代码,将大眼的所有参数初始化即可。


// ...其他代码
let sleepTimer; // 休眠定时器

// 休眠
function toSleep() {
// ...其他操作
document.body.removeEventListener('mousemove', focusOnMouse); // 移除鼠标移动事件
bigEye.style.transform = `rotateY(0deg) rotateX(0deg)`; // 大眼归位
eyeball.style.transform = `translate(0px, 0px)`; // 眼睛归位
}
// 工作
function focusOnMouse(e) {
// ...其他操作
// 设置休眠
if (sleepTimer) clearTimeout(sleepTimer); // 如果休眠定时器已经被设置,则清除休眠定时器
sleepTimer = setTimeout(() => { // 重新计时
toSleep();
}, 30000);
}

感谢上次掘金官方的提醒,在下把线上代码贴在这,在下文笔较差,看不下去的同学可以直接过来玩一玩,感兴趣再去创建自己的大眼。(没有点运行的不要来问我为什么出不来!!!)



如果自己在码上掘金动手的同学记得不要忘记添加echarts资源


image.png


💐 结语


好家伙,原来再写一遍大眼会这么累,这次是真真正正的“玩”了一天,有功夫的各位同僚也可以去玩一玩,于在下的基础上进行完善,创造出属于各位自己的大眼。当然如果有一些比较好玩的动效也可以留言告知在下,当下次混工作量时在下可以有东西写。


就这样!


image.png


作者:Urias
来源:juejin.cn/post/7132409301380890660
收起阅读 »

IDEA用上这十大插件绝对舒服

在本文中,我们将介绍 10 多个最好的 IntelliJ IDEA 插件,以提高工作效率并在更短的时间内完成更多工作。如果将这些插件合并到您的工作流程中,您将能够更有效地应对开发挑战。 1、TabNine TabNine 是一个 IntelliJ IDEA 插...
继续阅读 »

在本文中,我们将介绍 10 多个最好的 IntelliJ IDEA 插件,以提高工作效率并在更短的时间内完成更多工作。如果将这些插件合并到您的工作流程中,您将能够更有效地应对开发挑战。


1、TabNine


TabNine 是一个 IntelliJ IDEA 插件,可以为 Java 和 JavaScript 开发人员的代码提供 AI 建议。它分析来自数百万个开源项目的代码,并提供相关且准确的代码片段,以帮助开发人员更快、更准确地编写代码。

使用 TabNine 的众多优势包括:



  1. 有效的代码提示。

  2. 支持大量编程语言。

  3. 为主流编辑器和IDE提供帮助。

  4. 使用机器学习,记住你经常写的代码,并提供极其详细的提示。


地址:plugins.jetbrains.com/plugin/1279…



2、RestfulToolkit


RestfulToolkit 提供了与 RESTful API 交互的有用工具。开发人员可以使用此插件直接从 IDE 轻松测试、调试和管理 RESTful API 请求,从而提高他们的整体效率和生产力。


该插件与 HTTP Client、REST Assured 等流行工具集成,使其成为 RESTful API 开发的完整解决方案。


地址:plugins.jetbrains.com/plugin/1029…


3、MyBatisCodeHelperPro


MyBatisCodeHelperPro 在使用 MyBatis 框架时提高了开发人员的工作效率。它包括代码生成和实时模板,使编写和管理 MyBatis 代码更加容易,节省时间和精力。



此外,该插件支持数据库架构同步和 SQL 文件生成,提高开发效率。



地址:plugins.jetbrains.com/plugin/9837…
dehelperpro


4、CodeGlance


CodeGlance 为开发人员提供了代码右侧添加了简明概览,使他们更容易浏览和理解代码。

地址:plugins.jetbrains.com/plugin/7275…



可以看到在上图右侧区域有一个代码概览区域,并且可以上下滑动。


5、GenerateAllSetter


GenerateAllSetter 有助于为类中的所有属性生成 setter 方法。这可以在编写代码时节省时间和精力,同时也降低了出错的可能性。



地址:plugins.jetbrains.com/plugin/9360…



6、Lombok


Lombok:一个自动生成样板代码的 Java 库。



Project Lombok 是一个 java 库,可自动插入您的编辑器和构建工具,为您的 java 增添趣味。永远不要再写另一个 getter 或 equals 方法,通过一个注解,您的类就有一个功能齐全的构建器,自动化您的日志变量,等等。



地址:projectlombok.org/

需要注意的就是在使用了在 IDEA 中使用 Lombok 插件记得启用 Enable annotation processing


7、Rainbow Brackets


该插件为代码的方括号和圆括号着色,从而更容易区分不同级别的代码块。


地址:plugins.jetbrains.com/plugin/1008…


可以看到添加彩色方括号后,代码可读性有所提高。


8、GitToolBox


它包括许多额外的功能和快捷方式,使开发人员更容易使用 Git。使用 GitToolBox 的众多优点包括:



  1. GitToolBox 在 IntelliJ IDEA 上下文菜单中添加了几个快速操作,允许您在不离开 IDE 的情况下执行常见的 Git 任务。

  2. Git 控制台:该插件向 IntelliJ IDEA 添加了一个 Git 控制台,允许您在 IDE 中使用 Git。

  3. GitToolBox包含了几个解决合并冲突的工具,可以更容易地解决冲突并保持你的代码库是最新的。

  4. Git stash management:该插件添加了几个用于管理Git stashes的工具,使保存和重新应用代码更改变得更加容易。


地址:plugins.jetbrains.com/plugin/7499…


9、Maven Helper


Maven Helper 提供了一种更方便的方式来处理 Maven 项目。


Maven Helper 是一个帮助开发人员完成 Maven 构建过程的工具。该插件包括用于管理依赖项、插件和配置文件的功能,例如查看、分析和解决冲突以及运行和调试特定 Maven 目标的能力。


这可以通过减少花在手动配置和故障排除任务上的时间,使开发人员有时间进行编码和创新,从而提高生产力。


地址:plugins.jetbrains.com/plugin/7179…


10、Sonarlint


Sonarlint 是一个代码质量检测工具,集成了 SonarQube 以动态检测和修复代码质量问题。


Sonarlint 提供实时反馈和建议,帮助开发人员提高代码质量。它集成了 SonarQube 代码分析平台,允许开发人员直接在他们的 IDE 中查看代码质量问题。


这通过在潜在问题到达构建和测试阶段之前检测它们来节省时间并提高效率。 Sonarlint 还可以帮助开发人员遵守最佳实践和编码标准,从而生成更易于维护和更健壮的代码。


地址:plugins.jetbrains.com/plugin/7973…



作者:wayn
链接:https://juejin.cn/post/7225989135999434789
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

初学后端,如何做好表结构设计?

前言 最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计? 大家关心的问题阳哥必须整理出来,希望对大家有帮助。 先说结论 这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从...
继续阅读 »

前言


最近有不少前端和测试转Go的朋友在私信我:如何做好表结构设计?


大家关心的问题阳哥必须整理出来,希望对大家有帮助。


先说结论


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度。


4个方面


设计数据库表结构需要考虑到以下4个方面:




  1. 数据库范式:通常情况下,我们希望表的数据符合某种范式,这可以保证数据的完整性和一致性。例如,第一范式要求表的每个属性都是原子性的,第二范式要求每个非主键属性完全依赖于主键,第三范式要求每个非主键属性不依赖于其他非主键属性。




  2. 实体关系模型(ER模型):我们需要先根据实际情况画出实体关系模型,然后再将其转化为数据库表结构。实体关系模型通常包括实体、属性、关系等要素,我们需要将它们转化为表的形式。




  3. 数据库性能:我们需要考虑到数据库的性能问题,包括表的大小、索引的使用、查询语句的优化等。




  4. 数据库安全:我们需要考虑到数据库的安全问题,包括表的权限、用户角色的设置等。




设计原则


在设计数据库表结构时,可以参考以下几个优雅的设计原则:




  1. 简单明了:表结构应该简单明了,避免过度复杂化。




  2. 一致性:表结构应该保持一致性,例如命名规范、数据类型等。




  3. 规范化:尽可能将表规范化,避免数据冗余和不一致性。




  4. 性能:表结构应该考虑到性能问题,例如使用适当的索引、避免全表扫描等。




  5. 安全:表结构应该考虑到安全问题,例如合理设置权限、避免SQL注入等。




  6. 扩展性:表结构应该具有一定的扩展性,例如预留字段、可扩展的关系等。




最后,需要提醒的是,优雅的数据库表结构需要在实践中不断迭代和优化,不断满足实际需求和新的挑战。



下面举个示例让大家更好的理解如何设计表结构,如何引入内存,有哪些优化思路:



问题描述



如上图所示,红框中的视频筛选标签,应该怎么设计数据库表结构?除了前台筛选,还想支持在管理后台灵活配置这些筛选标签。


这是一个很好的应用场景,大家可以先自己想一下。不要着急看我的方案。


需求分析



  1. 可以根据红框的标签筛选视频

  2. 其中综合标签比较特殊,和类型、地区、年份、演员等不一样



  • 综合是根据业务逻辑取值,并不需要入库

  • 类型、地区、年份、演员等需要入库



  1. 设计表结构时要考虑到:



  • 方便获取标签信息,方便把标签信息缓存处理

  • 方便根据标签筛选视频,方便我们写后续的业务逻辑


设计思路



  1. 综合标签可以写到配置文件中(或者写在前端),这些信息不需要灵活配置,所以不需要保存到数据库中

  2. 类型、地区、年份、演员都设计单独的表

  3. 视频表中设计标签表的外键,方便视频列表筛选取值

  4. 标签信息写入缓存,提高接口响应速度

  5. 类型、地区、年份、演员表也要支持对数据排序,方便后期管理维护


表结构设计


视频表































字段注释
id视频主键id
type_id类型表外键id
area_id地区表外键id
year_id年份外键id
actor_id演员外键id

其他和视频直接相关的字段(比如名称)我就省略不写了


类型表























字段注释
id类型主键id
name类型名称
sort排序字段

地区表























字段注释
id类型主键id
name类型名称
sort排序字段

年份表























字段注释
id类型主键id
name类型名称
sort排序字段

原以为年份字段不需要排序,要么是年份正序排列,要么是年份倒序排列,所以不需要sort字段。


仔细看了看需求,还有“10年代”还是需要灵活配置的呀~


演员表























字段注释
id类型主键id
name类型名称
sort排序字段

表结构设计完了,别忘了缓存


缓存策略


首先这些不会频繁更新的筛选条件建议使用缓存:




  1. 比较常用的就是redis缓存

  2. 再进阶一点,如果你使用docker,可以把这些配置信息写入docker容器所在物理机的内存中,而不用请求其他节点的redis,进一步降低网络传输带来的耗时损耗

  3. 筛选条件这类配置信息,客户端和服务端可以约定一个更新缓存的机制,客户端直接缓存配置信息,进一步提高性能


列表数据自动缓存


目前很多框架都是支持自动缓存处理的,比如goframe和go-zero


goframe


可以使用ORM链式操作-查询缓存


示例代码:


package main

import (
"time"

"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
)

func main() {
var (
db = g.DB()
ctx = gctx.New()
)

// 开启调试模式,以便于记录所有执行的SQL
db.SetDebug(true)

// 写入测试数据
_, err := g.Model("user").Ctx(ctx).Data(g.Map{
"name": "xxx",
"site": "https://xxx.org",
}).Insert()

// 执行2次查询并将查询结果缓存1小时,并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}

// 执行更新操作,并清理指定名称的查询缓存
_, err = g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: -1,
Name: "vip-user",
Force: false,
}).Data(gdb.Map{"name": "smith"}).Where("uid", 1).Update()
if err != nil {
g.Log().Fatal(ctx, err)
}

// 再次执行查询,启用查询缓存特性
r, _ := g.Model("user").Ctx(ctx).Cache(gdb.CacheOption{
Duration: time.Hour,
Name: "vip-user",
Force: false,
}).Where("uid", 1).One()
g.Log().Debug(ctx, r.Map())
}
复制代码

go-zero


DB缓存机制


go-zero缓存设计之持久层缓存


官方都做了详细的介绍,不作为本文的重点。


讨论


我的方案也在我的技术交流群里引起了大家的讨论,也和大家分享一下:


Q1 冗余设计和一致性问题



提问: 一个表里做了这么多外键,如果我要查各自的名称,势必要关联4张表,对于这种存在多外键关联的这种表,要不要做冗余呢(直接在主表里冗余各自的名称字段)?要是保证一致性的话,就势必会影响性能,如果做冗余的话,又无法保证一致性



回答:


你看文章的上下文应该知道,文章想解决的是视频列表筛选问题。


你提到的这个场景是在视频详情信息中,如果要展示这些外键的名称怎么设计更好。


我的建议是这样的:



  1. 根据需求可以做适当冗余,比如你的主表信息量不大,配置信息修改后同步修改冗余字段的成本并不高。

  2. 或者像我文章中写的不做冗余设计,但是会把外键信息缓存,业务查询从缓存中取值。

  3. 或者将视频详情的查询结果整体进行缓存


还是看具体需求,如果这些筛选信息不变化或者不需要手工管理,甚至不需要设计表,直接写死在代码的配置文件中也可以。进一步降低DB压力,提高性能。


Q2 why设计外键?



提问:为什么要设计外键关联?直接写到视频表中不就行了?这么设计的意义在哪里?



回答:



  1. 关键问题是想解决管理后台灵活配置

  2. 如果没有这个需求,我们可以直接把筛选条件以配置文件的方式写死在程序中,降低复杂度。

  3. 站在我的角度:这个功能的筛选条件变化并不会很大,所以很懂你的意思。也建议像我2.中的方案去做,去和产品经理拉扯喽~


总结


这篇文章介绍了设计数据库表结构应该考虑的4个方面,还有优雅设计的6个原则,举了一个例子分享了我的设计思路,为了提高性能我们也要从多方面考虑缓存问题。


收获最大的还是和大家的交流讨论,总结一下:



  1. 首先,一定要先搞清楚业务需求。比如我的例子中,如果不需要灵活设置,完全可以写到配置文件中,并不需要单独设计外键。主表中直接保存各种筛选标签名称(注意维护的问题,要考虑到数据一致性)

  2. 数据库表结构设计一定考虑数据量和并发量,我的例子中如果数据量小,可以适当做冗余设计,降低业务复杂度



本文抛砖引玉,欢迎大家留言交流。


作者:王中阳Go
链接:https://juejin.cn/post/7212828749128876092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为什么要使用Docker和容器

1981年出版的一本书《Nailing Jelly to a Tree》描述了软件是“模糊的,难以把握的”。这在1981年是真实的,四十年后也同样如此。无论是你购买的应用程序还是自己构建的应用程序,软件部署、管理和运行仍然很困难。 Docker容器提供了一种把...
继续阅读 »


1981年出版的一本书《Nailing Jelly to a Tree》描述了软件是“模糊的,难以把握的”。这在1981年是真实的,四十年后也同样如此。无论是你购买的应用程序还是自己构建的应用程序,软件部署、管理和运行仍然很困难。


Docker容器提供了一种把握软件的方法。你可以使用Docker将应用程序封装起来,以便处理其部署和运行时的问题,如:如何在网络上公开它,如何管理其对存储、内存和I/O的使用,如何控制访问权限等等。这些问题都在应用程序本身之外处理,并以一种在所有“容器化”应用程序中保持一致的方式处理。你可以在任何安装了Docker运行时的兼容操作系统主机(Linux或Windows)上运行Docker容器。


除了这种方便的封装、隔离、可移植性和控制之外,Docker还提供了许多其他好处。Docker容器很小(几兆字节),启动速度很快,具有自己内置的版本控制和组件重用机制,可以通过公共Docker Hub或私有仓库轻松共享。


Docker容器也是不可变的,这既有安全性又有操作上的好处。对容器的任何更改都必须部署为一个全新的、版本不同的容器。


在本文中,我将探讨Docker容器如何使构建和部署软件更容易,容器解决的问题,何时容器才是正确的解决方案,何时不是。




在Docker容器之前


多年来,企业软件通常是部署在“裸机”上(即安装在具有对底层硬件完全控制的操作系统上)或虚拟机上(即安装在与其他“客户”操作系统共享底层硬件的操作系统上)。自然地,安装在裸机上使得软件难以移动和更新,这两个限制使得IT难以敏捷地响应业务需求的变化。


然后,虚拟化出现了。虚拟化平台(也称为“虚拟机管理程序”)允许多个虚拟机共享单个物理系统,每个虚拟机以隔离的方式模拟整个系统的行为,包括其自己的操作系统、存储和I/O。IT现在可以更有效地响应业务需求的变化,因为虚拟机可以克隆、复制、迁移和启动或关闭以满足需求或节约资源。



虚拟机也有助于降低成本,因为更多的虚拟机可以合并到更少的物理机器上。运行旧应用程序的遗留系统可以转换为虚拟机,并进行物理退役以节省更多的资金。


但是虚拟机仍然存在一些问题。虚拟机很大(千兆字节),每个虚拟机都包含一个完整的操作系统。只有很多虚拟化应用程序可以合并到单个系统上。分配虚拟机仍然需要相当长的时间。最后,虚拟机的可移植性有限。在某个点之后,虚拟机无法提供快速移动的企业所需的速度、敏捷性和节省成本。




Docker容器的好处


容器的工作方式有点像虚拟机,但更加具体和细粒度。它们将单个应用程序及其依赖项(应用程序运行所需的所有外部软件库)与底层操作系统和其他容器隔离开来。


所有容器化的应用程序共享一个公共操作系统(Linux或Windows),但它们彼此之间与整个系统隔离开来。操作系统提供所需的隔离机制,使这种隔离发生。Docker将这些机制包装在一个方便的接口。


Docker容器的好处在许多地方体现。以下是一些Docker和容器的主要优势:


1、Docker 可以更有效地利用系统资源


容器化应用程序的实例使用的内存比虚拟机少得多,它们启动和停止更快,并且可以在它们的主机硬件上更密集地打包。所有这些都意味着 IT 开支更少。


成本节省将根据所使用的应用程序和它们可能的资源密集程度而异,但容器无疑比虚拟机更有效率。还可以节省软件许可证的成本,因为您需要更少的操作系统实例来运行相同的工作负载。


2、Docker 可以加快软件交付周期


企业软件必须快速响应各种不断变化的情况。这意味着需要轻松扩展以满足需求,并且需要轻松更新以添加业务所需的新功能。


Docker容器可以轻松地将具有新业务功能的新版软件快速投入生产,并在需要时快速回滚到以前的版本。它们还可以更轻松地实施蓝/绿部署等策略。


3、Docker 可以实现应用程序的可移植性


在防火墙后面运行企业应用程序很重要,为了保持紧密和安全; 或者在公共云中,以便于公众访问和高弹性的资源。因为Docker容器封装了应用程序运行所需的所有内容(并且只包含那些内容),所以它们允许应用程序在环境之间轻松穿梭。任何安装了Docker运行时的主机,无论是开发人员的笔记本电脑还是公共云实例,都可以运行Docker容器。


4、Docker 在微服务架构中表现出色


Docker 容器是轻量级、可移植和自包含的,使得更容易按照前瞻性的思路构建软件,这样您就不会试图用昨天的开发方法来解决明天的问题。


容器使得实现微服务等软件模式更加容易,其中应用程序由许多松散耦合的组件构成。通过将传统的“单块式”应用程序分解为单独的服务,微服务允许业务应用程序的不同部分可以分别进行扩展、修改和维护——如果符合业务需求,可以由不同的团队在不同的时间表上进行。


容器不是实现微服务的必要条件,但它们非常适合微服务方法和敏捷开发流程。




容器并不是万能的


需要记住的是,与任何软件技术一样,容器并不是万能的。Docker 容器本身不能解决所有问题。


特别是以下几点:


1、Docker 无法解决软件的安全问题


容器中的软件默认情况下可能比在裸机上运行的软件更安全,但这就像说锁着门的房子比开着门的房子更安全一样。这并没有说明社区的状况、诱人偷盗的贵重物品的可见存在、居住在那里的人的日常生活等等。容器可以为应用程序添加一层安全性,但只能作为在上下文中保护应用程序的一般计划的一部分。


2、Docker 不能神奇地将应用程序变成微服务


如果将现有的应用程序容器化,可以减少其资源消耗并使其更容易部署。但它并不会自动更改应用程序的设计或其与其他应用程序的交互方式。这些好处只能通过开发人员的时间和努力来实现,而不仅仅是将所有内容移动到容器中的命令。


如果将传统的单块式或面向服务的应用程序放入容器中,最终得到的是一个老旧的应用程序在容器中运行。这对你的工作没有任何帮助。


容器本身没有组合微服务式应用程序的机制。需要更高级别的编排来实现这一点。Kubernetes 是这种编排系统的最常见示例。Docker swarm 模式也可以用于管理多个 Docker 主机上的许多 Docker 容器。


3、Docker 不是虚拟机的替代品


容器的一个误解是它们使虚拟机过时了。许多以前在虚拟机中运行的应用程序可以移动到容器中,但这并不意味着所有应用程序都可以或应该这样做。例如,如果你在一个有严格监管要求的行业中,可能无法将容器替换为虚拟机,因为虚拟机提供的隔离性比容器更强。


作者:Squids数据库云服务提供商
链接:https://juejin.cn/post/7226153074078416933
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一位 98 年程序员离职后

今天不写技术文了,写点轻松的。 我自己都没讨好自己,何苦要来讨好你呢? 开篇 本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、...
继续阅读 »

今天不写技术文了,写点轻松的。


img-16593196478205dc10966293f6e5e3f0be6d9ff93705f.jpg



我自己都没讨好自己,何苦要来讨好你呢?



开篇


本人最近的情况已经在题目中交待完了。为啥离职呢?职场上就那些事,离职和入职时一样安静,跟几个聊得来的人互道祝福就可以了。其实在此之前,也了解到今年行情不是很好。裁员的、降薪的、跑路的,这些消息可以说不绝于耳。但本人最终还是选择离职休整下。


得益于父辈们的努力,在房价还不到当前房价一半的时候出手了,让弱小的我在这座城市有瓦遮头,不必为三餐奔波。既没有房贷,也没有车贷,一人吃饱,全家不饿的我没有了这方面的顾虑,也有了底气做出这个选择。


img-165932129544253f44bc0a87ca13a950b2ff14f24ccd2.jpg



希望明天还能看到那朵软萌萌的云,因为它好像你



说下离职后我都干了些什么吧,给各位列一下,说不定能找到知音。



  • 刷力扣

  • 整理下之前的东西

  • 健身

  • 写作

  • 品鉴周董的新歌

  • 看综艺

  • ...


上面的这些东西不分先后,一直都在做。


刷力扣


刷力扣其实很早就开始了,每天登录力扣有一个积分,完成每日一题有 10 积分,到现在坚持了有差不多两年了,战绩如下:


QQ截图20220801104732.png



大部分是中等



光看题目量不算少,但其实大部分困难和部分中等都是 cv 之后通过的,不装了,摊牌了。刷题过程也不艰难,就一句话,简单题重拳出击,困难题唯唯诺诺。有些人会觉得算法没有必要,因为平时的工作就用不到。但我觉得算法最重要的是锻炼人的思维,思维很重要,它能够指导一个人思考问题的轨迹和方向。虽然有时刷题时会感觉自己活着就是凑数的,没必要灰心,真的,因为你的判断是对的。


整理下之前的东西


之前在工作时也积累一些东西,但没有做整理,所以趁着这段时间整理下,看下能不能发现一些新东西。个人觉得一直处于一种忙碌的状态并不一定是好事,这有点像吃东西时狼吞虎咽,容易噎着。


健身


这件事是坚持的最久的一件事,从高一一直到现在。高一时上映的速 7,被强森和郭达在办公室的那段打戏吸引,当时觉得男人就应该这样。于是从最简单的俯卧撑、引体开始,一点点的朝自己的目标努力。但这过程走了很多弯路,比如训练的方式不对,太急于求成、吃的没跟上、休息没跟上,导致很长一段时间都处于瓶颈期,一直在原地踏步。这种不上不下的感觉真的不好受,也想过放弃,但已经戒不掉了。图就不发了,担心被喷。胸、背、腿、腰、手、腹肌都有练,腹肌不多不少,正好 6 块。至于身材,我个人觉得还行,至少不差,也被人说过身材好,同性异性都有。


QQ截图20220801201459.png



被同性说



写作


这个貌似是在去年开始的,但中断了挺长一段时间,就想着在空窗期重新捡起来。至于最终能开出什么样的花,也没想过,就觉得写比不写强。读者感兴趣的可以看我之前写过的文章,主页:
鄙人的主页


img-1659357885349240b678eb24aca42039c30c16b002044.jpg



对待生活,不必说的太多,你同样可以给它一个惊喜



品鉴周董的新歌


本人虽说不是周董的粉丝,但以前总想着能在晴天里给千里之外的她带一壶爷爷泡的茶,面对面安静的坐着,她的笑容甜甜的,我也对着她傻笑。院子里是一片花海,散发着迷迭香。


她送我来到渡口,她的倒影在满是桃花的粉色海洋里若隐若现。船夫摇着桨,背对着我,哼着她唱过的红颜如霜突然来了句:这是最伟大的作品。可谁让夜晚留不住错过的烟火,活该我到现在还在流浪。


看综艺


综艺平时也是我解压的一种方式,最近把跑男第十季追完了,几位 mc 都是各有特点。不过最喜欢的还是新加入的白鹿,人美,很搞笑,魔性的笑声让人很容易记住她。


magazine-unlock-01-2.3.2022080201-7432B64DE5C9B11.jpg



你问我:我对你有多重要,我回答:太阳你知道吧



总结


可能有人看了之后会觉得有点躺平的趋势,但其实并没有。本人还是很爱折腾的,也希望能多认识点圈子以外的人,多认识点有趣的人,多认识点志同道合的人。有些人会觉得程序员很闷,不爱说话,天天就对着电脑。可能有部分人是这样的,但我不是,因为我是一个不走寻常路的程序员,而且我深知只有跳出圈子,才能打破认知。by the way,本人对数字化转型行业挺感兴趣的,有读者从事或者了解的话,可以大胆私信我啊。


作者:对方正在输入
链接:https://juejin.cn/post/7127653600532103198
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

快到 35 的"大龄"程序员

大家好,这是一篇快到 35 岁的“大龄”程序员的自我介绍,希望能够借此认识更多同道者。 我叫黎清龙,广东人,2012年本科毕业,2014年研究生毕业,是浙江大学软件工程学院本硕连读(呸,就是不想考研选择了保研)。第一份正式工作经历是在腾讯,CSIG 在线教育部...
继续阅读 »

大家好,这是一篇快到 35 岁的“大龄”程序员的自我介绍,希望能够借此认识更多同道者。


我叫黎清龙,广东人,2012年本科毕业,2014年研究生毕业,是浙江大学软件工程学院本硕连读(呸,就是不想考研选择了保研)。第一份正式工作经历是在腾讯,CSIG 在线教育部,做前端开发,也是 IMWeb 团队的一员,先后做过腾讯课堂和企鹅辅导业务,2020年正式任命 leader,管理差不多10人左右的前端开发团队;2022年3月,因(cheng)为(xu)某(yuan)些(dou)原(zhi)因(dao),加入虾皮金融产品部,现负责消费贷业务+催收业务的前端开发和管理工作。


我的自我介绍完了,如果大家不想浪费更多时间深入了解我的话,知道以上信息已经足够了,为了大家的脑细胞着想,提供给大家 3 个不用思考的快捷选项:



  1. 对我不感兴趣,可以左上角关闭页面(我可以对天发誓,这绝对不是相亲贴);

  2. 觉得可以交个朋友,给自己保留一个未来有惊喜的可能,可以关注我的公众号或者加我微信;

  3. 还想听我唠唠嗑的,欢迎继续看下去呀,一定满足大家的好奇心。




感谢你能够继续看下去。我想了很久,怎么样才能不至于让我的自我介绍写成流水账,但是,当我想了更久的时间之后,我发现,我想把这份流水账写出来更难,因为,很多的经历我都不记得了,我只能把我的记忆片段写下来,拼凑出我的职业生涯。好记性不如烂笔头,我觉得本文我可以永远留存并持续迭代,直到我的职业生涯结束的时候,可以用来回顾我的人生,也不失一桩美事。我也推荐大家这样做。


我的前端之路的伊始


我的第一份进入企业的工作是在2011年,大三实习,在杭州阿里,阿里妈妈广告部门(部门全称已经不记得了),后台开发,你没有看错,是的,我是后台开发,那会儿我还不知道前端,大学课程也没有一门是教前端的。


我对于阿里的印象,绝不是现在的"味儿"。我对阿里最大的印象还停留在当初那个时代,有三:



  1. 江湖气派,店小二文化,随性,直来直往,互相接受度非常高,我是非常喜欢这个文化的,当时阿里实习是不能拥有花名的,这是我职业生涯最大的遗憾之一,我还很清楚记得,当时我曾经查过,好像还没有人取名曹操,不过也是我的异想天开,因为即使我转正,我也没有那个资格取这个花名。

  2. 开放,真得非常开放,当我在新人入职欢迎聚餐中,脱到只剩裤衩的时候,我相信我那会一定是完全理解了开放这个词了。虽然直到现在回忆起来,还会有点不适,但是,当经历了那次聚餐之后,隐隐中,我会潜意识地得觉得,好像自己没什么是不可以“坦诚相待”的。

  3. 倒立文化,换个角度思考,我自认为我完全做到了,当我换个角度思考我的职业的时候,我走上了前端之路。。。


虽然在我拿到转正 offer 的时候,还是毅然决然选择保研(其实是被父母逼的)并转前端,但是我还是觉得,我在阿里的大半年实习期间,是我整个开发生涯中成长最快的时期,在那里,我学到了太多太多,以至于到现在我的开发习惯还会保留当时的一些痕迹:




  • 当我碰到需要服务运维的场景,我一定是首选 bash 脚本,然后是 python,最后才是 js,基本不会是 js 的,因为没什么是前两者做不到的。定时任务,文件锁,离线计算,文本处理等等,到现在我还记忆犹新。




  • 记不清写了多少 Map Reduce 了,但是当时,我真得被 Hadoop 的设计原理给深深的吸引到了,原来,大数据处理&分析,分布式计算和分布式文件系统有这么多的挑战,但它的解决方案又是这么的精简,直到现在,我仍然坚信,任何架构设计,一定是精简的,当我跟别人讨论架构的时候,如果他讲不清楚,或者表达非常复杂的时候,我就知道,不是设计有问题,就是表达有问题,我们还有更优的方案。天地良心,当时实习的时候,我真的是非常认真的做后台开发的,当时我还啃下了那本大象书呢,现在想想也觉得不容易,当年我是真喜欢看书呀。




  • 架构设计非常“好玩” ,在当时,阿里内部有非常多的技术分享,我常常去听我自己喜欢的分享,让我的技术视野得到了非常大的增长,比如:



    • 中文的分词要比英文的分词要难很多,最终发现,自然语言处理不是我的菜;

    • 推荐系统的结果是竞速的,当时真的有想入职百度,去学习搜索引擎的冲动;

    • 秒杀的多重降级、动态降级,各种“砍砍砍”,非常有意思。


    在当时,我学到的一个最重要的知识是,任何架构设计都是因地制宜的,不能墨守成规




在实习转正答辩的时候,最后问我的未来规划的时候,我的回答更多是偏架构设计和 UI 相关,现在回想起来都会觉得搞笑,当时我一度以为是转正失败了,但是没想到阿里开放到这都给我发了 offer,真得很感激我的老领导,但也觉得很对不起他们,因为我真的不想淹没在数据的海洋里,我更喜欢开发一些"看得着,摸得到"的东西,我会觉得做这个更有意思,所以,我选择了前端。


一波三折的腾讯梦


先说说为什么想去腾讯吧,因为我是广东人,父母都在深圳,都希望我回深圳,当时深圳不用多说,大公司就腾讯了,所以,我在实习和毕业的选择上一直都非常明确,就是深圳腾讯,但是我自己都没想到我回深圳是这么的坎坷。


研一找实习的时候,我第一次面试腾讯挂了,当时是电话面试,我记得是早上,很突然接到了面试电话,然后突然开始面试,我完全没有准备,很自然地就挂了,跟我同一个项目的做 web 服务的同学拿到腾讯的实习 offer 了,当时心理还有点不平衡,但是后面我也很快拿到新的 offer 了。


插一段题外话,当时我跟另外两个同学一起跟着导师外包项目,项目也挺有意思的,因为我们是嵌入式方向的实验室,所以我们做的是一个实时监控系统,有个同学主要负责传感器和网络编程,另外一个同学主要负责 web 后台服务,我负责前端页面(extjs),我们的项目是给一家医院做冰柜的温度实时监控系统,在冰柜中放入温度传感器,然后不断把冰柜的温度数据通过各个局域网网络节点传输器一路传到中心服务器中,然后中心服务负责存储并分析数据,落库并返回数据到前端,展示实时监控页面并添加告警功能。整个系统非常有意思,通过这个项目,我深深地感受到物联网的魅力,软硬件结合的威力。这还只是单向的,如果可以做到双向,再加上智能化,那基本就可以取代人的工作了,实际上,现在很多的无人XXX系统,他们的本质都是这个,现在互联网环境这么差,哪天干不下去了,换个行业,做物联网+虚拟+AI,做现实虚拟,实业升级事业,也是大有可为的。


回归正题,在腾讯突然面挂之后,我就开始认真复习,专门找前端的实习工作,然后很快就找到了网易的教育部门的前端开发 offer,这段经历我印象最深刻的是当时那批前端的笔试当中,我是最高分的,面试也没怎么问就拿到 offer 了,果然有笔试就是好呀,妥妥我的强项。或者是因为我有这段经历,所以后面我才会被分配到腾讯做教育吧。。。


在网易,我做的是网易云课堂和网易公开课相关的前端工作,在网易的实习过程中,我的前端基础和实践不断加强,三剑客,前端组件库,前端基础库,模块化,构建,浏览器兼容处理等等,基础技术收获很多,但是大的方面上,没什么特别的收获,就像网易的公司文化一样,没什么特别的感受,至今都没留下什么。在网易,印象最深的两个点就是:



  • 除了游戏,万般皆下品,主要靠情怀。其实这点跟在腾讯做教育也差不多;

  • 网易的伙食真的是互联网第一,不存在之一。


研二找工作的时候,我研究了腾讯的校招路演,发现有以下问题:



  • 杭州算是最后一站那种,时间很晚,到我们这边黄花菜都凉了;

  • 杭州离上海很近,过来招聘的团队应该基本都是上海的;

  • 像我这样的杭州毕业生不去阿里想去腾讯的奇葩真得不多了。


因此,我决定跑去上海参加校园招聘。当年校招我只面了百度跟腾讯,当时校园招聘都是统一笔试,面试,我记得百度是去他们上海分公司内部面试的,面了 2 轮就到 hr 了,还能留下记忆的是当时 2 面面试官对我的阿里经历很感兴趣,问了非常多,我当时就懵了,你们不是招前端的么。


然后是腾讯的面试,在一家 5 星级酒店的房间面的,当时进去就问我,能不能接受 web 服务研发岗位,我当时第一反应就是,你有无搞错呀!?但是机敏如我,肯定是立刻回答可以接受的,虽然这是一个随时都可以被废弃的万金油 api 岗位,但是它胜在可上可下,呸,是可前可后,啊呸,是可前端可后台,必须难不倒我呀,然后就是很无聊的面试,问了一些简单的前端题,了解了一下实习项目,最后做了一道智力题就结束了,相比百度的面试,有点看不过去了。最后问了我填的志愿是深圳的岗位,问我服不服从调剂,我说只想看深圳岗位,让我一度以为我又挂了,不过最后还是顺利进到 hr 的房间。。。面试,随便瞎聊,最后确认我只想回深圳,并表示可以给我争取调剂。


在回杭州的火车上,我知道百度的 offer 基本稳了,不过是上海的,腾讯的 offer 还是内心忐忑,实在是腾讯的面试有点“敷衍”了,那会儿我都在思考怎么忽悠我爸妈先在上海工作2年再回深圳了。不过没过2天,就收到了腾讯的 offer,是深圳易迅的前端开发岗位,当时在上海招聘的 90% 都是易迅(腾讯收购)的招聘,也很感谢当时帮我调剂的面试官跟 hr 了。兴奋的我在跟百度 hr 电话的时候就直接拒掉了百度 offer,现在回想起来,还真有点轻率了。


很快,我就决定提前到腾讯实习,当我坐在回深圳的火车时,看到了一则新闻:腾讯决定出售整个 ECC 给京东置换京东股份,并和京东开启战略合作。我不太记得我回家那天是什么心情,我只记得我办理入职手续的时候,窗外的天空是没有太阳的。我甚至都没认识全我的团队,因为当时所有工作都暂停了,那会儿,不是开大会,就是漫长的等待,现在想想,还挺像现在经历这场寒冬的我们一样,迷茫,忐忑,甚至有点慌张。


我加入了应届生群,在联名信上“签名”,在论坛上堆楼,终于,高层听到了我们的声音,跟京东友好协商之后,给予了我们这届应届生自主选择权 —— 是去京东还是留在腾讯,待遇不变。毫不犹豫地,我选择了腾讯。


写到这里,我还是很感慨,我的腾讯梦还真是一波三折,除了幸运还是幸运,或许因为在这件事情上花光了我前半生积攒的运气,以至于直到到现在所有的年会我都是阳光普照,深圳车牌摇号还遥遥无期,但是,我的腾讯之路还是开启了。。。


我职业生涯中最大的幸运 —— IMWeb 团队


多动动脑子


刚转来 IMWeb 团队,我接到的第一个任务是做一个爬虫,要爬淘宝教育的课程和购课数据。这不是很简单吗,之前做过呀,殊不知噩梦即将开始...


不到半天我就写好了,包括邮件模板,也自测好了,正式启动,美滋滋去喝杯茶,回来就能交差了。当我摸鱼回来一看,咦,脚本停了,接口报错,被限频了。于是我进入了疯狂调试模式,添加重试逻辑,不断调整请求频率策略,最终祭出终极策略,3分钟请求1次,这下不会被限频了吧,在稳定跑了1个小时没问题之后,我安心的下班回家了。


第二天到公司,数据跑完了,完美。于是,我做了最后的数据校对和计算调整,然后调通自动发送邮件的逻辑,再次执行。当我美滋滋地再次摸鱼回来,发现脚本又停了,这次是新的错误,没有错误信息,就是 5xx,黑人问号啊,于是各种调试各种排查,最终得出一个结论,ip 被拉进黑名单了。


好家伙,算你狠。于是我上网各种研究代理,不管免费付费,能用就是好代理,再次调整策略,申请十多个账号轮流爬,光荣牺牲了一批又一批的 ip 之后,我还是败下阵来。那个时候,我觉得我的人生都是黑暗的,我的面前立着一座大山,我怎么样都翻不过去。


当老大咨询进度的时候,我并没有得到任何安慰和建议,而是一句“多动动脑子”。


我已经忘记当时的我是什么心情,被打击成什么样了。也已经忘记了一周后是怎样完成任务的。我只记得,之后我只花了半天时间就爬了网易云课堂和慕课网的数据,他们就是毫不设防的裸......奔。


任性如我


对于我们程序员来说,碰到的最棘手的问题中,无法复现的问题肯定名列前茅。


有一次需求发布,现网验证的时候发现了一个问题,在本地和 test 环境都复现不了,live 打断点也复现不了,真是绝了,打断点没问题,不打断点有问题,我大概能猜到问题,但是需要打印一些日志来定位最终问题,可是只能在 live 才有效,先不说 live 构建会自动删掉 console.log 语句,执行一次 live 部署非常慢,如果要折腾几次来调试,那半天都解决不了问题了。


急性子的我肯定受不了这种折磨,所以我选择了直接登录现网服务器改代码调试。先把压缩文件 down 下来,本地格式化,找到对应位置添加 console.log,然后传回服务器覆盖文件,禁用 cdn 资源,直接在现网复现排查问题。几分钟不到就确定问题,然后修改代码重新部署一次过完成最终需求发布。整个过程行云流水,但是我内心慌得一比,这要是出问题被发现,那后果不敢想象。


还有好几次的 Node 服务问题,我也是直接现网调试,其实 Node 服务才是最适合这么做的场景,但是,我并不是推荐大家这样做。再到后面,我行我素的我越来越能够理解流程机制的用意和作用,现在踏上管理岗位,我更希望小伙伴们是严格遵照流程规范来工作,但我的内心深处,还是住着一个不羁的我


“万恶的” owner


“清龙,这个需求就由你来当 owner 吧。”


“owner?要做什么?”


“就是这个需求的负责人,看看需求进度有没有问题,发布别延期就行”


“好”


【需求开发中...】


“清龙,现在需求进度怎样?有没有风险?”


“我这边没问题,我问一下后台同学看看”


“你可以每天下班前收集一下大家的进展,然后在群里同步哈”


“好”


【需求测试中...】


“清龙,需求测得怎么样啦?”


“......(这不应该问测试吗)应该问题不大,我这边的 bug 都处理完了,我找测试跟进一下测试进度哈”


“可以每天下班前找测试对齐一下测试的整体进度,让测试在群里同步哈”


“好”


【需求发布中...】


“清龙,需求发得怎么样啦?”


“后台发完了,前端正在发,问题不大”


“牛呀,一定要做好现网验证,发布完成记得要在群里周知哈”


“好~”


自从团队推行 owner 机制,工作量是噌噌噌地往上涨,但是工作能力也有很大的提升。


怎么说呢,这是毁誉参半的机制,重点在于每个人怎么看待这个事情,它可以是卷、分担压力的借口;它也可以是培养新人,锻炼项目管理能力,提升沟通协调能力的最佳实践机会。


我眼中的 IMWeb 团队


它是综合的。我们团队涉猎的领域非常广,移动端,pc 端,后台均有涉猎,正因如此,我们有非常好的土壤茁壮成长,尝试各种新技术。在很早的时候,我就在数据接口低代码平台落地 GraphQL,实现了基于mysql 的 GraphQL 的 node 版本,不说业界,在公司内肯定是领先的。在公司成长的过程中,我们团队也在成长,在前端工程化上也有很多的实践和成果。后面腾讯搞 Oteam,我们团队也多有贡献。


它是着眼于业务的。 我们团队推崇做产品的主人翁,坚持不懈地以技术手段助力业务发展。我们做的所有项目都是为了业务服务,为了整个团队服务。我们团队是专业的,没有钻技术的牛角尖,更多地是扎根于业务,一切以实际出发,更多以落地与实践为主。但我们团队的业务并不是很出彩,属于半公益的教育,至今我仍然唏嘘不已,只能感叹时运不济,现在回过头来细品,再厉害的技术,没有好的业务相辅相成,也是无法一直走下去,业务是王道啊。


它是被信任与敢于信任的。作为前端团队,能够有那么大的空间来施展身手,这足以说明我们团队是受到领导的充分信任的,我们团队也非常努力来对得起这份信任。而团队也非常信任团队里的每一个人,会给予很多的试错机会和时间,就看我们有没有耐心,主动与坚持了。


在一个已经建立了一定文化的团队是幸福的,它是需要细品的,但很多人都不愿意去感受。这两年,我过得很难受,不知变通地我一直守着这份坚持,与已经被潜移默化的团队文化对抗,最终只是落得个遍体鳞伤。但是我并不后悔,反而很庆幸,因为最后我找到了自己内心的真相,一直以来,我觉得是 IMWeb 团队造就了我,其实,我所依恋的一直都是它的价值观与文化,而我愿意一直为之践行。


我的管理之路


我正式任命是在 2020年上半年,但实际上,我在 2018 年下半年就从腾讯课堂调到了企鹅辅导,从一组调到三组,并开始做一些团队管理的工作。整体而言我的管理经验成长的非常缓慢,这是我自己的结论。


首先,我的角色转变比较缓慢。经常看到小伙伴们做事情太“慢”,我都忍不住要自己上,或者直接告诉他们答案,我知道这很不好,但是初期的我就是忍不住,我感觉我的管理之路就是憋气之路,最后总结就是,在大方向上,我要站出来,但是具体实施层面,我要当个隐身人,这对我来说,非常难受。


其次,我是主猫头鹰次考拉的重事风格,不太擅长管理小伙伴的情绪还有激励,沟通和语言艺术真是我需要投入一生去学习锻炼的课程。另外,我有一个最大的问题就是不喜欢冲突,直接导致我不太擅长争取资源,这会让我觉得很对不起小伙伴们,这点也是我离开腾讯最大的原因吧。感觉我比较适合增量市场,在存量市场这点真的是致命的,不过专心搞好业务不挺好吗,何苦浪费时间在这些地方。


作者:潜龙在渊灬
链接:https://juejin.cn/post/7189612924116140092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

对话大环境严峻之下选择跨行跨岗的小姐姐

前阵子,今年3月份的时候,参加了一个线下行业交流会,里面各行各业的人士都会讲述自己的从业情况以及成果,当时状态有些不舒服加上有些拘谨没能记得很多人,人和事件都是选择性的记忆。坐在我附近的一位女孩子,笑起来很让人舒服且友好,大脑可能就选择性地记忆了。 她讲述自己...
继续阅读 »

前阵子,今年3月份的时候,参加了一个线下行业交流会,里面各行各业的人士都会讲述自己的从业情况以及成果,当时状态有些不舒服加上有些拘谨没能记得很多人,人和事件都是选择性的记忆。坐在我附近的一位女孩子,笑起来很让人舒服且友好,大脑可能就选择性地记忆了。


她讲述自己行业的时候,没感觉到丝毫的怯场,反而将自己的所处的情况以及计划都理得很清晰。为什么我会在意这些,大概是自身所不具备的看到别人具备的特质则显得格外的在意。从她描述的状况和语气来看,她已经解除了自己的困境,话里透露着对以后的走向多了些坚定和勇气。据后来了解到是咨询了行业大佬V姐,V姐将她的现状和想法做了解读和引导,大概是能如此吧。


她硕士毕业,去了一家央企工作,从事金融相关的行业,日常需要对接国内外的业务,目前该行业是天花板,自己处于事业的瓶颈期,也在离职过程中。听到这里或许觉得,今年大环境差,就业严峻,能苟着就苟着,况且还是央企,养老铁饭碗啊。是的,我也是这样想的,今年杭州上半年大肆裁员,海投简历多半已读不回,看多了it从业者在社交平台贩卖焦虑。但此刻的她,已经分析过自身的处境才能做出这一举动。毕竟相比于环境怎么样,时间其实更加宝贵,财富的积累更能决定你所做的和做想的是否值得。所以我是很支持她,结束之后和她交流了一番,并表示将来能够有更多机会联动。


一段时间后,周三的下午,正在码bug中,突然收到了她的消息,从她的话里提取出来就是她成功裸辞跳槽,还跨行跨岗,心里暗暗称强,终于能联动上了。那个下午疯狂码代码中,回了一句手头忙得晚点,觉得交流是在微信里的,可以推迟到晚上交流。当她回复预约线下当面聊的时候,我内心其实是拒绝的,像我这种比较腼腆内向的男孩子,一向没太多线下交流的机会,除非早期体验生活接外快硬着头皮上,还硬是拉上了同学陪衬,且这次还是女孩子,啊,好为难啊。


图片


当然,这个弱点一直被我重视,今年上半年都在为这个弱点做了大量的功课。每次都会和内心的自己和解,尝试和周围的人交流,小区的人,店家老板,租友,其他行业的人,再者就是周末跑出去摆摊和阿姨大叔一块聊天,聊聊他们女儿的情况......


所以,这次当然也和自己和解了,决定赴约。再次见面像极了朋友重逢的感觉,或许在职场被领导或甲方压迫久了,这次没有压迫感,久违了,唯独怕自己不知道专不专业的知识能不能够帮助到她。


见面聊了很多,发现跨行跨岗位,能够看出给她带来了很多的压力。本身专业学的就不是这个,现在从事网络安全相关的岗位,不仅要把这些计算机相关知识消化,还需要把公司提供的产品都要有所掌握,光说她工作内容下来我统计她叹了两三次气,我赶忙安慰慢慢来,一切都会好的,她遂即回我她很喜欢这份工作,累是确实是累,但是很有意义,是带着笑着说出来的。


期间也吐露过以后成熟了打算和自己的挚友出来单干,这么敏感重要的信息我当然很在意并予以支持和鼓励,这充分的表达了她对于自身情况和职业规划是清晰的。


对于自己的人生或者职业规划,有一个清晰的目标是很重要的,至少我认为很重要,“大方向不变,小动作可伸缩”,但凡你敢想敢做,就能有无限可能,不管概率大不大,有经验包就是一个值。


或许有股鸡汤的味道,但我身边接触的大佬,有互联网行业的,做自媒体的,做副业的,总结就是他们从不会把网络上频繁出现的“润”,“躺平”,“摆烂”之类慰藉心灵的词摆在嘴上,反而他们做事非常的雷厉风行,不拖拉,很明白自己的现状,以及自己想要干嘛。总比自己状况都摸不清楚,自己本职的工作都没搞明白,一边想“润”,一边好高骛远强得多。


和她交流之后,给我的感觉就是做事雷厉风行,学习能力强的那种,非但没有被跨行跨业给恐惧到,反而越发的有精神。期间问了我一些市面上主流比较热门的内容,例如云服务与本地机房,防火墙的原理,堡垒机的原理和作用,系统架构以及服务治理等等。开始是不适应的,可能内心戏比较多,我讲得很生硬岂不尴尬?或者旁边人觉得我这桌好怪,大晚上不好好吃东西讲些莫名其妙的话题?想刀一个人心是藏不住的,想帮一个人的心也是藏不住的,我尽可能地调度我那单核脑容量,同时组织人话讲述出去,专不专业我不知道,但是我知道有些比较主观,有些没能覆盖全面,讲的过程中感觉不到周围有人的存在。


结束之后,虽然打滴回去的路上还在讨论网络安全案例图,我不知道司机师傅会不会懵逼,但我觉得讨论这些很有趣,哈哈。期待下次的联动。


图片
▲图/ Ariel 拍摄提供


这一次的联动,从她身上也学习到了很多。她对于一个新环境的适应能力和学习能力是很强的,做事的风格雷厉风行,遇到难题会选择非常有效的方式。上一个做事雷厉风行,从不拖拉让我印象深刻的大佬还是去年认识,成功转型go开发,很遗憾的就是因为很多因素没能一起共事。


相反,我在往期文章多次提到的挚友鑫仔,学的建筑行业,近期询问他的状况,因为大环境恶劣,破坏了自己在学校规划好的一切,从深圳跑回到家乡二三线城市,选择了一家低于自己预期但实属无奈的设计院工作。他日常焦虑,对于过来人的经验看待他的状态和举动像是以第三视角在看他,于是对他进行了言传身教,希望对他有所帮助,能够再次恢复挥斥方遒的热血青年。


不知不觉,文章看多了,写得多了,身边各行各业的人接触多了,越发觉得什么最重要,要像什么样的人靠齐,真正做到知行合一是能带来多大的益处。


作者:桑小榆呀
链接:https://juejin.cn/post/7118992017175838750
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用Snackbar代替Toast

用Snackbar代替Toast Toast是远古的安卓原生组件,在不方便打印日志的时候,Toast可以直观的看出来看出来日志;更多的Toast作为一种提示,默认的Toast提示小,区域不明显,且不美观,虽然可以通过自定义Toast解决。这篇文章讲的是Sna...
继续阅读 »

用Snackbar代替Toast



Toast是远古的安卓原生组件,在不方便打印日志的时候,Toast可以直观的看出来看出来日志;更多的Toast作为一种提示,默认的Toast提示小,区域不明显,且不美观,虽然可以通过自定义Toast解决。这篇文章讲的是Snackbar。



认识Snackbar


我是在看某个三方库源码的时候发现它的,在以往的开发经验中,它出现的频率并不高。我在某个三方库中发现,在两个项目中,提示的表现却不一样,代码怎么会骗人呢。于是认真的看这个三方库的源码,直到看到下面的判断。



static {

try {

Class.forName("android.support.design.widget.Snackbar");

Class.forName("android.support.design.widget.BottomSheetDialog");

HAS_DESIGN_LIB = true;

} catch (Throwable ignore) {

HAS_DESIGN_LIB = false;

}

}

protected AbsAgentWebUIController create() {

return HAS_DESIGN_LIB ? new DefaultDesignUIController() : new DefaultUIController();

}

这里的意思是说,如果项目中有SnackbarBottomSheetDialog库的引用,则使用DefaultDesignUIController的UI,也就是Snackbar的提示;没有的话,则使用DefaultUIController的UI,也就是默认的Toast提示。


实际上其实谷歌在Android 5.0的时候就推出了Snackbar,它是Material Design中的一个控件。


实践 Snackbar


下图为ToastSnackbar的展示效果


image.png



  • 简单的提示


Snackbar的基本用法和Toast类似



Snackbar.make(findViewById(R.id.root), "这是一条提示", Snackbar.LENGTH_LONG).show();


  • 带有Action的提示



Snackbar snackbar = Snackbar.make(view, "这是一个提示", Snackbar.LENGTH_INDEFINITE);

snackbar.setAction("取消", new View.OnClickListener() {

@Override

public void onClick(View v) {

Toast.makeText(MainActivity.this,"点击取消",Toast.LENGTH_SHORT).show();

}

});

snackbar.show();

这里只能设置一个Action,不然旧的会被替代掉。




  1. Snackbar.LENGTH_INDEFINITE:代表无限期的显示,一直显示,点击按钮才可以隐藏




  2. Snackbar.LENGTH_LONG:长时间提示




  3. Snackbar.LENGTH_SHORT:短时间提示





  • 修改样式


更改Snackbar的背景颜色



snackbar.getView().setBackgroundColor(Color.parseColor("#0000ff"));

更改Action提示的颜色



snackbar.setActionTextColor(Color.parseColor("#ffffff"));

更改padding的距离



snackbar.getView().setPadding(50, 50, 50 , 50);

操作文字,比如添加图片、更改文字内容、更改文字颜色,更改文字大小等。


虽然Snackbar没有提供给我们直接操作文字样式的方法,但我们可以通过findViewById获取这个文字,然后就像操作TextView一样去操作它就可以了。


怎么获取TextView?



TextView textView = snackbar.getView().findViewById(R.id.snackbar_text);

snackbar_text id来自Snackbar的源码。获取ID的时候编辑器可能会报错提示,实际上是可以运行的。



@NonNull

public static Snackbar make(@NonNull View view, @NonNull CharSequence text, int duration) {

ViewGroup parent = findSuitableParent(view);

if (parent == null) {

throw new IllegalArgumentException("No suitable parent found from the given view. Please provide a valid view.");

} else {

LayoutInflater inflater = LayoutInflater.from(parent.getContext());

SnackbarContentLayout content = (SnackbarContentLayout)inflater.inflate(hasSnackbarButtonStyleAttr(parent.getContext()) ? layout.mtrl_layout_snackbar_include : layout.design_layout_snackbar_include, parent, false);

Snackbar snackbar = new Snackbar(parent, content, content);

snackbar.setText(text);

snackbar.setDuration(duration);

return snackbar;

}

}


<view

xmlns:android="http://schemas.android.com/apk/res/android"

class="android.support.design.widget.SnackbarContentLayout"

android:layout_width="match_parent"

android:layout_height="wrap_content"

android:layout_gravity="bottom"

android:theme="@style/ThemeOverlay.AppCompat.Dark">

<TextView

android:id="@+id/snackbar_text"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_weight="1"

android:layout_gravity="center_vertical|left|start"

android:paddingTop="@dimen/design_snackbar_padding_vertical"

android:paddingBottom="@dimen/design_snackbar_padding_vertical"

android:paddingLeft="@dimen/design_snackbar_padding_horizontal"

android:paddingRight="@dimen/design_snackbar_padding_horizontal"

android:ellipsize="end"

android:maxLines="@integer/design_snackbar_text_max_lines"

android:textAlignment="viewStart"

android:textAppearance="?attr/textAppearanceBody2"/>

<Button

android:id="@+id/snackbar_action"

style="?attr/snackbarButtonStyle"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginStart="@dimen/design_snackbar_extra_spacing_horizontal"

android:layout_marginLeft="@dimen/design_snackbar_extra_spacing_horizontal"

android:layout_gravity="center_vertical|right|end"

android:minWidth="48dp"

android:visibility="gone"/>

</view>

作者:Android唐浮
链接:https://juejin.cn/post/7103803312910106660
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

定位都得集成第三方?Android原生定位服务LocationManager不行吗?

前言 现在的应用,几乎每一个 App 都存在定位的逻辑,方便更好的推荐产品或服务,获取当前设备的经纬度是必备的功能了。有些 App 还是以LBS(基于位置服务)为基础来实现的,比如美团,饿了吗,不获取到位置都无法使用的。 有些同学觉得不就是获取到经纬度么,An...
继续阅读 »

前言


现在的应用,几乎每一个 App 都存在定位的逻辑,方便更好的推荐产品或服务,获取当前设备的经纬度是必备的功能了。有些 App 还是以LBS(基于位置服务)为基础来实现的,比如美团,饿了吗,不获取到位置都无法使用的。


有些同学觉得不就是获取到经纬度么,Android 自带的就有位置服务 LocationManager ,我们无需引入第三方服务,就可以很方便的实现定位逻辑。


确实 LocationManager 的使用很简单,获取经纬度很方便,我们就无需第三方的服务了吗? 或者说 LocationManager 有没有坑呢?兼容性问题怎么样?获取不到位置有没有什么兜底策略?


一、LocationManager的使用


由于是Android的系统服务,直接 getSystemService 可以获取到


LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);

一般获取位置有两种方式 NetWork 与 GPS 。我们可以指定方式,也可以让系统自动提供最好的方式。


// 获取所有可用的位置提供器
List<String> providerList = locationManager.getProviders(true);
// 可以指定优先GPS,再次网络定位
if (providerList.contains(LocationManager.GPS_PROVIDER)) {
provider = LocationManager.GPS_PROVIDER;
} else if (providerList.contains(LocationManager.NETWORK_PROVIDER)) {
provider = LocationManager.NETWORK_PROVIDER;
} else {
// 当没有可用的位置提供器时,弹出Toast提示用户
return;
}

当然我更推荐由系统提供,当我的设备在室内的时候就会以网络的定位提供,当设备在室外的时候就可以提供GPS定位。


 String provider = locationManager.getBestProvider(criteria, true);

我们可以实现一个定位的Service实现这个逻辑


/**
* 获取定位服务
*/
public class LocationService extends Service {

private LocationManager lm;
private MyLocationListener listener;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@SuppressLint("MissingPermission")
@Override
public void onCreate() {
super.onCreate();

lm = (LocationManager) getSystemService(LOCATION_SERVICE);
listener = new MyLocationListener();

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setAltitudeRequired(false);//不要求海拔
criteria.setBearingRequired(false);//不要求方位
criteria.setCostAllowed(true);//允许有花费
criteria.setPowerRequirement(Criteria.POWER_LOW);//低功耗

String provider = lm.getBestProvider(criteria, true);

YYLogUtils.w("定位的provider:" + provider);

Location location = lm.getLastKnownLocation(provider);

YYLogUtils.w("" + location);

if (location != null) {
//不为空,显示地理位置经纬度
String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("getLastKnownLocation:" + longitude + "-" + latitude);

stopSelf();

}

//第二个参数是间隔时间 第三个参数是间隔多少距离,这里我试过了不同的各种组合,能获取到位置就是能,不能获取就是不能
lm.requestLocationUpdates(provider, 3000, 10, listener);
}

class MyLocationListener implements LocationListener {
// 位置改变时获取经纬度
@Override
public void onLocationChanged(Location location) {

String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("onLocationChanged:" + longitude + "-" + latitude);


stopSelf(); // 获取到经纬度以后,停止该service
}

// 状态改变时
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
YYLogUtils.w("onStatusChanged - provider:"+provider +" status:"+status);
}

// 提供者可以使用时
@Override
public void onProviderEnabled(String provider) {
YYLogUtils.w("GPS开启了");
}

// 提供者不可以使用时
@Override
public void onProviderDisabled(String provider) {
YYLogUtils.w("GPS关闭了");
}

}

@Override
public void onDestroy() {
super.onDestroy();
lm.removeUpdates(listener); // 停止所有的定位服务
}

}

使用:定义并动态申请权限之后即可开启服务



fun testLocation() {

extRequestPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) {

startService(Intent(mActivity, LocationService::class.java))

}

}

这样我们启动这个服务就可以获取到当前的经纬度,只是获取一次,大家如果想再后台持续定位,那么实现的方式就不同了,我们服务要设置为前台服务,并且需要额外申请后台定位权限。


话说回来,这么使用就一定能获取到经纬度吗?有没有兼容性问题


Android 5.0 Oppo



Android 6.0 Oppo海外版



Android 7.0 华为



Android 11 三星海外版



Android 12 vivo



目前测试不多,也能发现问题,特别是一些低版本,老系统的手机就可能无法获取位置,应该是系统的问题,这种服务跟网络没关系,开不开代理都是一样的。


并且随着测试系统的变高,越来越完善,提供的最好定位方式还出现混合定位 fused 的选项。


那是不是6.0的Oppo手机太老了,不支持定位了?并不是,百度定位可以获取到位置的。



既然只使用 LocationManager 有风险,有可能无法获取到位置,那怎么办?


二、混合定位


其实目前百度,高度的定位Api的服务SDK也不算大,相比地图导航等比较重的功能,定位的SDK很小了,并且目前都支持海外的定位服务。并且定位服务是免费的哦。


既然 LocationManager 有可能获取不到位置,那我们就加入第三方定位服务,比如百度定位。我们同时使用 LocationManager 和百度定位,哪个先成功就用哪一个。(如果LocationManager可用的话,它的定位比百度定位更快的)


完整代码如下:


@SuppressLint("MissingPermission")
public class LocationService extends Service {

private LocationManager lm;
private MyLocationListener listener;
private LocationClient mBDLocationClient = null;
private MyBDLocationListener mBDLocationListener;

@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();

createNativeLocation();

createBDLocation();
}

/**
* 第三方百度定位服务
*/
private void createBDLocation() {
mBDLocationClient = new LocationClient(UIUtils.getContext());
mBDLocationListener = new MyBDLocationListener();
//声明LocationClient类
mBDLocationClient.registerLocationListener(mBDLocationListener);
//配置百度定位的选项
LocationClientOption option = new LocationClientOption();
option.setLocationMode(LocationClientOption.LocationMode.Battery_Saving);
option.setCoorType("WGS84");
option.setScanSpan(10000);
option.setIsNeedAddress(true);
option.setOpenGps(true);
option.SetIgnoreCacheException(false);
option.setWifiCacheTimeOut(5 * 60 * 1000);
option.setEnableSimulateGps(false);
mBDLocationClient.setLocOption(option);
//开启百度定位
mBDLocationClient.start();
}

/**
* 原生的定位服务
*/
private void createNativeLocation() {

lm = (LocationManager) getSystemService(LOCATION_SERVICE);
listener = new MyLocationListener();

Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_COARSE);
criteria.setAltitudeRequired(false);//不要求海拔
criteria.setBearingRequired(false);//不要求方位
criteria.setCostAllowed(true);//允许有花费
criteria.setPowerRequirement(Criteria.POWER_LOW);//低功耗

String provider = lm.getBestProvider(criteria, true);

YYLogUtils.w("定位的provider:" + provider);

Location location = lm.getLastKnownLocation(provider);

YYLogUtils.w("" + location);

if (location != null) {
//不为空,显示地理位置经纬度
String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("getLastKnownLocation:" + longitude + "-" + latitude);

stopSelf();

}

lm.requestLocationUpdates(provider, 3000, 10, listener);
}

class MyLocationListener implements LocationListener {
// 位置改变时获取经纬度
@Override
public void onLocationChanged(Location location) {

String longitude = "Longitude:" + location.getLongitude();
String latitude = "Latitude:" + location.getLatitude();

YYLogUtils.w("onLocationChanged:" + longitude + "-" + latitude);


stopSelf(); // 获取到经纬度以后,停止该service
}

// 状态改变时
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
YYLogUtils.w("onStatusChanged - provider:" + provider + " status:" + status);
}

// 提供者可以使用时
@Override
public void onProviderEnabled(String provider) {
YYLogUtils.w("GPS开启了");
}

// 提供者不可以使用时
@Override
public void onProviderDisabled(String provider) {
YYLogUtils.w("GPS关闭了");
}

}


/**
* 百度定位的监听
*/
class MyBDLocationListener extends BDAbstractLocationListener {

@Override
public void onReceiveLocation(BDLocation location) {

double latitude = location.getLatitude(); //获取纬度信息
double longitude = location.getLongitude(); //获取经度信息


YYLogUtils.w("百度的监听 latitude:" + latitude);
YYLogUtils.w("百度的监听 longitude:" + longitude);

YYLogUtils.w("onBaiduLocationChanged:" + longitude + "-" + latitude);

stopSelf(); // 获取到经纬度以后,停止该service
}
}

@Override
public void onDestroy() {
super.onDestroy();
// 停止所有的定位服务
lm.removeUpdates(listener);

mBDLocationClient.stop();
mBDLocationClient.unregisterLocationListener(mBDLocationListener);
}

}

其实逻辑都是很简单的,并且省略了不少回调通信的逻辑,这里只涉及到定位的逻辑,别的逻辑我就尽量不涉及到。


百度定位服务的API申请与初始化请自行完善,这里只是简单的使用。并且坐标系统一为国际坐标,如果需要转gcj02的坐标系,可以网上找个工具类,或者看我之前的文章


获取到位置之后,如何Service与Activity通信,就由大家自由发挥了,有兴趣的可以看我之前的文章


总结


所以说Android原生定位服务 LocationManager 还是有问题啊,低版本的设备可能不行,高版本的Android系统又很行,兼容性有问题!让人又爱又恨。


很羡慕iOS的定位服务,真的好用,我们 Android 的定位服务真是拉跨,居然还有兼容性问题。


我们使用第三方定位服务和自己的 LocationManager 并发获取位置,这样可以增加容错率。是比较好用的,为什么要加上 LocationManager 呢?我直接单独用第三方的定位服务不香吗?可以是可以,但是如果设备支持 LocationManager 的话,它会更快一点,体验更好。


好了,我如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。


作者:newki
链接:https://juejin.cn/post/7131910145574961182
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android案例手册 - 仅一个文件的展开收缩LinearLayout

👉实践过程 Hello,大家好,小空这两天又开始造Android方面的文章啦,哈哈,总是在Android和Unity中来回横跳。 前两天我们刚讲解了LinearLayout,那么今天我们自定义一个可展开收缩的LinearLayout。 仅一个文件(Java版或...
继续阅读 »

👉实践过程


Hello,大家好,小空这两天又开始造Android方面的文章啦,哈哈,总是在Android和Unity中来回横跳。


前两天我们刚讲解了LinearLayout,那么今天我们自定义一个可展开收缩的LinearLayout。


仅一个文件(Java版或Kotlin版),随时复制随时用。
先看效果图


可展开的LinearLayout-仅一个文件复制即用.gif


默认展示两个子item,当点击“显示更多”的时候展开所有的子View,当点击“收起内容”的时候除了前两个其他的都隐藏。


😜使用


我们先来看看使用方式:


<cn.phototocartoonstudy.ExpandableLinearLayout
    android:id="@+id/idExpandableLinearLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="芝麻粒儿" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="https://juejin.cn/user/4265760844943479" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="CSDN" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="https://zhima.blog.csdn.net/" />
 
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="4dp"
        android:text="Android/Unity技术" />
</cn.phototocartoonstudy.ExpandableLinearLayout>

直接布局中用即可,或者动态代码添加:


@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_framelayout);
ExpandableLinearLayout idExpandableLinearLayout=findViewById(R.id.idExpandableLinearLayout);
for (int i = 0; i < 4; i++) {
TextView  txtViewTip = new TextView(this);
txtViewTip.setText("芝麻粒儿添加更多内容"+i);
LinearLayout.LayoutParams layoutParamsBottomTxt = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
txtViewTip.setLayoutParams(layoutParamsBottomTxt);
idExpandableLinearLayout.addView(txtViewTip);
}
}

😜实现


说完了使用,我们就来说说实现,前面学完LinearLayout后知道该控件使用了wrap_content,如果子View使用隐藏GONE的形式,则高度自动变化,页面布局中和该控件对其的其他控件也会自动变化。


所以,当子View的个数小于设置的默认个数,则不用添加底部,如果子View个数大于默认显示个数,则在最后动态添加一个View,当点击展开和隐藏的时候,其他多余的控件进行GONE和VISIBLE的控制即可。


我们再为控件增加点其他方法:




  1. 修改当隐藏的时候默认展示的条目




  2. 可修改展开和收起的控件文本




  3. 可修改展开和收起控件的字体大小和颜色




  4. 其他功能自己看着加吧




public void outUseMethodChangeDefaultItemCount(int intDefaultItemCount) {
this.intDefaultItemCount = intDefaultItemCount;
}
public void outUseMethodChangeExpandText(String strExpandText) {
this.strExpandText = strExpandText;
}
public void outUseMethodChangeHideText(String strHideText) {
this.strHideText = strHideText;
}
public void outUseMethodChangeExpandHideTextSize(float fontTextSize) {
this.fontTextSize = fontTextSize;
}
public void outUseMethodChangeExpandHideTextColor(@ColorInt int intTextColor) {
this.intTextColor = intTextColor;
}

Java版


/**
* Created by akitaka on 2022-08-11.
*
* @author akitaka
* @filename ExpandableLinearLayout
*/

public class JavaExpandableLinearLayout extends LinearLayout implements View.OnClickListener {

private TextView txtViewTip;
/**
* 是否是展开状态,默认是隐藏
*/
private boolean isExpand = false;
private boolean boolHasBottom = false;

private int intDefaultItemCount = 2;
/**
* 待展开显示的文字
*/
private String strExpandText = "显示更多";
/**
* 待隐藏显示的文字
*/
private String strHideText = "收起内容";
private float fontTextSize;
private int intTextColor;

public void outUseMethodChangeDefaultItemCount(int intDefaultItemCount) {
this.intDefaultItemCount = intDefaultItemCount;
}

public void outUseMethodChangeExpandText(String strExpandText) {
this.strExpandText = strExpandText;
}

public void outUseMethodChangeHideText(String strHideText) {
this.strHideText = strHideText;
}
public void outUseMethodChangeExpandHideTextSize(float fontTextSize) {
this.fontTextSize = fontTextSize;
}
public void outUseMethodChangeExpandHideTextColor(@ColorInt int intTextColor) {
this.intTextColor = intTextColor;
}
public JavaExpandableLinearLayout(Context context) {
this(context, null);
}

public JavaExpandableLinearLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public JavaExpandableLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//设置垂直方向
setOrientation(VERTICAL);
}

@Override
public void setOrientation(int orientation) {
if (LinearLayout.HORIZONTAL == orientation) {
throw new IllegalArgumentException("ExpandableLinearLayout只支持垂直布局");
}
super.setOrientation(orientation);
}


@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int childCount = getChildCount();
justToAddBottom(childCount);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}


/**
* 判断是否要添加底部
*/
private void justToAddBottom(int childCount) {
if (childCount > intDefaultItemCount && !boolHasBottom) {
boolHasBottom = true;
//要使用默认底部,并且还没有底部
LinearLayout linearLayoutBottom = new LinearLayout(getContext());
LinearLayout.LayoutParams layoutParamsBottom = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
linearLayoutBottom.setLayoutParams(layoutParamsBottom);
linearLayoutBottom.setGravity(Gravity.CENTER);
txtViewTip = new TextView(getContext());
txtViewTip.setText("展开更多");
txtViewTip.setTextSize(fontTextSize);
txtViewTip.setTextColor(intTextColor);
LinearLayout.LayoutParams layoutParamsBottomTxt = new LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
txtViewTip.setLayoutParams(layoutParamsBottomTxt);
//设置个边距
layoutParamsBottomTxt.setMargins(0, 10, 0, 10);
linearLayoutBottom.addView(txtViewTip);
linearLayoutBottom.setOnClickListener(this);
//添加底部
addView(linearLayoutBottom);
hide();
Log.e("TAG", "justToAddBottom: zou l zhe ");
}
}

/**
* 刷新UI
*/
private void refreshView(View view) {
int childCount = getChildCount();
if (childCount > intDefaultItemCount) {
if (childCount - intDefaultItemCount == 1) {
//刚超过默认,判断是否要添加底部
justToAddBottom(childCount);
}
//大于默认数目的先隐藏
view.setVisibility(GONE);
}
}

/**
* 展开
*/
private void expand() {
for (int i = intDefaultItemCount; i < getChildCount(); i++) {
//从默认显示条目位置以下的都显示出来
View view = getChildAt(i);
view.setVisibility(VISIBLE);
}
}

/**
* 收起
*/
private void hide() {
int endIndex = getChildCount() - 1;
for (int i = intDefaultItemCount; i < endIndex; i++) {
//从默认显示条目位置以下的都隐藏
View view = getChildAt(i);
view.setVisibility(GONE);
}
}

@Override
public void onClick(View v) {
outUseMethodToggle();
}

/**
* 外部也可调用 展开或关闭
*/
public void outUseMethodToggle() {
if (isExpand) {
hide();
txtViewTip.setText(strExpandText);
} else {
expand();
txtViewTip.setText(strHideText);
}
isExpand = !isExpand;
}

/**
* 外部可随时添加子view
*/
public void outUseMethodAddItem(View view) {
int childCount = getChildCount();
//插在底部之前
addView(view, childCount - 1);
refreshView(view);
}
}

Kotlin版


/**
* Created by akitaka on 2022-08-11.
* @author akitaka
* @filename KotlinExpandableLinearLayout
*/
class KotlinExpandableLinearLayout :LinearLayout, View.OnClickListener {
private var txtViewTip: TextView? = null
constructor(context: Context?) :this(context,null)
constructor(context: Context?, attrs: AttributeSet?) :this(context,attrs,0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
init {
//设置垂直方向
orientation = VERTICAL
}
/**
* 是否是展开状态,默认是隐藏
*/
private var isExpand = false

private var intDefaultItemCount = 2
private var boolHasBottom = false

/**
* 待展开显示的文字
*/
private var strExpandText = "显示更多"

/**
* 待隐藏显示的文字
*/
private var strHideText = "收起内容"
private var fontTextSize = 0f
private var intTextColor = 0

fun outUseMethodChangeDefaultItemCount(intDefaultItemCount: Int) {
this.intDefaultItemCount = intDefaultItemCount
}

fun outUseMethodChangeExpandText(strExpandText: String) {
this.strExpandText = strExpandText
}

fun outUseMethodChangeHideText(strHideText: String) {
this.strHideText = strHideText
}

fun outUseMethodChangeExpandHideTextSize(fontTextSize: Float) {
this.fontTextSize = fontTextSize
}

fun outUseMethodChangeExpandHideTextColor(@ColorInt intTextColor: Int) {
this.intTextColor = intTextColor
}

override fun setOrientation(orientation: Int) {
require(HORIZONTAL != orientation) { "ExpandableLinearLayout只支持垂直布局" }
super.setOrientation(orientation)
}


override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val childCount = childCount
justToAddBottom(childCount)
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}


/**
* 判断是否要添加底部
*/
private fun justToAddBottom(childCount: Int) {
if (childCount > intDefaultItemCount && !boolHasBottom) {
boolHasBottom = true
//要使用默认底部,并且还没有底部
val linearLayoutBottom = LinearLayout(context)
val layoutParamsBottom = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
linearLayoutBottom.layoutParams = layoutParamsBottom
linearLayoutBottom.gravity = Gravity.CENTER
txtViewTip = TextView(context)
txtViewTip!!.text = "展开更多"
txtViewTip!!.textSize = fontTextSize
txtViewTip!!.setTextColor(intTextColor)
val layoutParamsBottomTxt = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
txtViewTip!!.layoutParams = layoutParamsBottomTxt
//设置个边距
layoutParamsBottomTxt.setMargins(0, 10, 0, 10)
linearLayoutBottom.addView(txtViewTip)
linearLayoutBottom.setOnClickListener(this)
//添加底部
addView(linearLayoutBottom)
hide()
}
}

/**
* 刷新UI
*/
private fun refreshView(view: View) {
val childCount = childCount
if (childCount > intDefaultItemCount) {
if (childCount - intDefaultItemCount == 1) {
//刚超过默认,判断是否要添加底部
justToAddBottom(childCount)
}
//大于默认数目的先隐藏
view.setVisibility(GONE)
}
}

/**
* 展开
*/
private fun expand() {
for (i in intDefaultItemCount until childCount) {
//从默认显示条目位置以下的都显示出来
val view: View = getChildAt(i)
view.setVisibility(VISIBLE)
}
}

/**
* 收起
*/
private fun hide() {
val endIndex = childCount - 1
for (i in intDefaultItemCount until endIndex) {
//从默认显示条目位置以下的都隐藏
val view: View = getChildAt(i)
view.setVisibility(GONE)
}
}

override fun onClick(v: View?) {
outUseMethodToggle()
}

/**
* 外部也可调用 展开或关闭
*/
fun outUseMethodToggle() {
if (isExpand) {
hide()
txtViewTip!!.text = strExpandText
} else {
expand()
txtViewTip!!.text = strHideText
}
isExpand = !isExpand
}

/**
* 外部可随时添加子view
*/
fun outUseMethodAddItem(view: View) {
val childCount = childCount
//插在底部之前
addView(view, childCount - 1)
refreshView(view)
}
}


📢作者:小空和小芝中的小空


作者:芝麻粒儿
链接:https://juejin.cn/post/7130639529123250213
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

JS令人头疼的类型转换

web
前言 JS中的类型转换常常被人诟病,因为javascript属于弱类型语言,它对于类型的语言没有强制的限定,这对于我们来说是头疼的。不同的类型之间的运算需要先对数据的类型进行转换,在日常开发中我们经常会用到。 数据类型 基本数据类型 Number(数字) S...
继续阅读 »

前言


JS中的类型转换常常被人诟病,因为javascript属于弱类型语言,它对于类型的语言没有强制的限定,这对于我们来说是头疼的。不同的类型之间的运算需要先对数据的类型进行转换,在日常开发中我们经常会用到。


数据类型


基本数据类型



  • Number(数字)

  • String(字符串)

  • Boolean(布尔)

  • Null

  • Undefined

  • Symbol(ES6)


引用数据类型



  • object{}

  • array[]

  • function(){}

  • date()


由于JS中拥有动态类型,在定义的时候不用指定数据类型,赋值的时候可以将任意类型赋给同一个变量,例如:let a = 1; a = '1'


类型转换


什么是类型转换?


简单来说就是将值从一种数据类型转换为另一种数据类型的过程。


分为哪几种?


根据转换的特点分为:显式类型转换(强制转换)和隐式类型转换(自动转换)。


显示类型转换(强制转换)


通过Boolean()——原始值转布尔,Number()——原始值转数字,String()——原始值转字符来进行强制类型转换。这里的转换规则可以直接查看Js官方文档:Annotated ES5


1.png


2.png


我们从文档中可以知道当我们想进行强制类型转换时,js会自动会帮我们使用ToString(value),ToNumber(value)进行转换。


//原始值转布尔
console.log(Boolean('123'));
console.log(Boolean(123));
console.log(Boolean(null));
console.log(Boolean(undefined));
console.log(Boolean(true));

//原始值转数字
console.log(Number('123'));
console.log(Number(123));
console.log(Number(null));
console.log(Number(undefined));
console.log(Number(true));

//原始值转字符串
console.log(String('123'));
console.log(String(123));
console.log(String(null));
console.log(String(undefined));
console.log(String(true));

结果为:


3.png


对象转字符串,数字


通过调用特殊的对象转换方法来完成,在js中有两个方法来执行转换,这两个方法所有的对象都具备,就是用来把对象转换为原始值的。这两个方法分别为toString(),valueOf(),这两个方法对象的构造函数原型上就有,其目的就是要有办法把对象转换为原始类型。


对象转字符串


toString()方法除了Null和Undefined其他的数据类型都具有此方法。通常情况下toString()和String()效果一样。



4.png


我们在文档中重点关注对象转字符串,上图中对象转字符串有两个步骤,先是执行自带的ToPrimitive(obj,String),再返回执行结果,分以下几步:


1.判断obj是否为基本类型,是则返回


2.调用对象自带的toString方法,如果能得到一个原始类型,则返回


3.调用对象自带的valueOf方法,如果能得到一个原始类型,则返回


4.报错


对象转数字

对象转数字的话也同样是有两个步骤:先是执行自带的ToPrimitive(obj,Number),再返回执行结果,分以下几步:


1.判断obj是否为基本类型,是则返回


2.调用对象自带的valueOf方法,如果能得到一个原始类型,则返回


3.调用对象自带的toString方法,如果能得到一个原始类型,则返回


4.报错


隐式类型转换



  • 当 + 运算作为一元操作符时,会自动调用ToNumber()处理该值。(相当于Number())


例如:console.log(+'123');结果为数字123。
console.log(+[]);结果为0,因为对象[]转换为了0。



  • 当 + 运算作为二元操作符,例(a + b)


1.lprim = ToPrimitive(v1)


2.rprim = ToPrimitive(v2)


3.如果lprim是字符串或者rprim是字符串,则返回ToString(lprim)和ToStringrprim()的拼接结果


4.返回ToNumber(lprim) + ToNumber(rprim)


结语


js类型转换规则,相当于历史事件,是已经规定好的,弄清楚它,能更好地和面试官侃侃而谈。最后感谢各位的观看。


作者:一拾九
来源:juejin.cn/post/7224518612161593402
收起阅读 »

节流 你会手写吗?

web
节流 在各大面试题中,频繁出现的老油条,节流。 啥叫节流呢? 节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器。 在间隔一段时间执行一次回调的场景有: 1...
继续阅读 »

节流


在各大面试题中,频繁出现的老油条,节流。


啥叫节流呢?


节流(throttle):每次触发定时器后,直到这个定时器结束之前无法再次触发。一般用于可预知的用户行为的优化,比如为scroll事件的回调函数添加定时器。


在间隔一段时间执行一次回调的场景有:


1.滚动加载,加载更多或滚到底部监听

2.搜索框,搜索联想功能

简单来说就是,一段时间内重复触发,按一定频率(1s、3s、5s)执行,可配置一开始就执行一次。


如果还不懂,就直接上我们的例子。我们可以看到当我们滑动屏幕的时候,会频繁运行打印这个函数。


image.png
当我们进行节流后,给它设置一个时间,那么他就只会在该时间后


image.png


上代码


其中fn代表将要运行的函数,delay代表函数触发的时间间隔。


整个代码思路,
timer=null,
flag=false, 默认刚开始不运行
设置一个定时器,
等到delay时间到了,就会开始运行这个函数fn。如果在delay之前,发生了滚动等事件,因为已经
flag = true,只会return 不会运行这个函数fn。只有等带delay到了时间,才会运行函数。


定时器实现的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数。


js
let count = 0;
function throttle(fn, delay) {
let timer = null // 把变量放函数里面,避免全局污染
let flag = false
let that = this
return function () {
if (flag) return
flag = true
let arg = arguments // 此处为fn函数的参数
timer = setTimeout(() => {
fn.apply(that, arg)
flag = false
}, delay)
}
}
function test(a, b) {
console.log(a, b)
}
let throttleTest = throttle(test, 1000)
// 测试函数
function test1() {
console.log('普通test:', count++)
}

window.addEventListener('scroll', (e) => {
// test1() // 不用节流的话,页面滚动一直在调用
throttleTest(1, 2) // 加上节流,即使页面一直滚动,也只会每隔一秒执行一次test函数
})

作者:Mr-Wang-Y-P
来源:juejin.cn/post/7222984001769488443
收起阅读 »

上传的图片怎么满足我们的审美的呢?开始玩弄css的一些 特别属性 object-fit

web
今天开始玩弄css的一些比较冷门但是可能比较实用的属性 object-fit。 首先让我们先看看一张效果图 这两张自拍,你个人觉得哪张比较好看,不用想都知道第一张好看啦,我们肯定希望我们上传的图片都是以第一种图片当头像啊,而不是第二种扁扁的。那么这样的效果是怎...
继续阅读 »

今天开始玩弄css的一些比较冷门但是可能比较实用的属性 object-fit。


首先让我们先看看一张效果图


这两张自拍,你个人觉得哪张比较好看,不用想都知道第一张好看啦,我们肯定希望我们上传的图片都是以第一种图片当头像啊,而不是第二种扁扁的。那么这样的效果是怎么实现的呢?
image.png


诀窍 object-fit


相信大多数人都对这个属性比较陌生吧。没咋看过这个属性吧!它有啥用呢?


我来给大家介绍介绍这个属性。
object-fit是一个CSS属性,用于控制图片或视频等替换元素的尺寸和位置,以使其适合其容器。


默认情况下,替换元素的大小取决于其本身的大小,而不是其容器的大小。这可能会导致替换元素与其容器不匹配,或者在缩放容器时无法应用正确的比例。


使用object-fit属性,可以指定替换元素如何调整其大小以适应其容器。它有以下几个值:



  • fill:默认值,替换元素会拉伸以填充容器,可能会导致元素的宽高比发生变化。

  • contain:替换元素会缩放以适应容器,保持其宽高比,可能会留有空白区域。

  • cover:替换元素会缩放以填充容器,保持其宽高比,可能会被裁剪。

  • none:替换元素将保持其本来的尺寸和宽高比,可能会溢出容器。

  • scale-down:替换元素会根据容器的大小进行缩放,但不会超过其原始大小,可能会留有空白区域。


看完还是好晕,不如直接看代码和效果图



注释解释了代码中每个部分的作用:



  • object-fit: cover将图像填充到容器中,保持比例不变。

  • border-radius: 50%将图像的四个角设置为圆角,使其呈现圆形。

  • width: 340pxheight: 340px设置图像的宽度和高度。

  • border: 1px solid #ccc设置图像周围的边框。


容器1和容器2具有相同的样式,但容器1使用了object-fit属性,而容器2没有。这样,我们可以比较两者之间的区别,看看object-fit如何影响图像的呈现方式。


html
<!DOCTYPE html>
<html>

<head>
<title>object-fit示例</title>
<style>
/* 容器1样式 */
.container1 img {
object-fit: cover; /* 图像填充容器,保持比例不变 */
border-radius: 50%; /* 圆角 */
width: 340px;
height: 340px;
border: 1px solid #ccc;
}

/* 容器2样式 */
.container2 img {
border-radius: 50%; /* 圆角 */
width: 340px;
height: 340px;
border: 1px solid #ccc;
}
</style>
</head>

<body>
<h2>自拍照 object-fit</h2>
<!-- 容器1 -->
<div class="container1">
<img src="https://ts1.cn.mm.bing.net/th/id/R-C.e62783996335efecfb15e445205cc5f6?rik=2Z0xpGAe3tn1kQ&riu=http%3a%2f%2fwww.xgsy188.com%2fuploadfile%2f20131151461663843.jpg&ehk=rKGrd9FbAQUFWicdL8Omt%2bFaMw%2f09v2obcuVTAWca4w%3d&risl=&pid=ImgRaw&r=0" alt="自拍照">
</div>

<h2>自拍照2</h2>
<!-- 容器2 -->
<div class="container2">
<img src="https://ts1.cn.mm.bing.net/th/id/R-C.e62783996335efecfb15e445205cc5f6?rik=2Z0xpGAe3tn1kQ&riu=http%3a%2f%2fwww.xgsy188.com%2fuploadfile%2f20131151461663843.jpg&ehk=rKGrd9FbAQUFWicdL8Omt%2bFaMw%2f09v2obcuVTAWca4w%3d&risl=&pid=ImgRaw&r=0" alt="自拍照">
</div>
</body>

</html>


作者:Mr-Wang-Y-P
来源:juejin.cn/post/7223767530981326885
收起阅读 »

这些数组reduce的妙用,你都会吗?

web
reduce 是 JavaScript 数组对象上的一个高阶函数,它可以用来迭代数组中的所有元素,并返回一个单一的值。其常用的语法为: array.reduce(callback[, initialValue]) 其中,callback 是一个回调函数,它接...
继续阅读 »

reduce 是 JavaScript 数组对象上的一个高阶函数,它可以用来迭代数组中的所有元素,并返回一个单一的值。其常用的语法为:
array.reduce(callback[, initialValue])



其中,callback 是一个回调函数,它接受四个参数:累加器(初始值或上一次回调函数的返回值)、当前元素、当前索引、操作的数组本身。initialValue 是一个可选的初始值,如果提供了该值,则作为累加器的初始值,否则累加器的初始值为数组的第一个元素。
reduce 函数会从数组的第一个元素开始,依次对数组中的每个元素执行回调函数。回调函数的返回值将成为下一次回调函数的第一个参数(累加器)。最后,reduce 函数返回最终的累加结果。
以下是一个简单的 reduce
示例,用于计算数组中所有元素的和:


const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue);
console.log(sum); // 15

在上面的代码中,reduce 函数从数组的第一个元素开始,计算累加值,返回最终的累加结果 15。
除了数组的求和,reduce 函数还可以用于其他各种用途,如数组求平均数、最大值、最小值等。此外,reduce 函数还可以与 map、filter、forEach 等函数组合使用,实现更加复杂的数据操作。


当然,以下是一些 reduce 的实际应用案例,帮助你更好地理解它的用法:


计算数组的平均数


const arr = [1, 2, 3, 4, 5];
const average = arr.reduce((accumulator, currentValue, index, array) => {
accumulator += currentValue;
if (index === array.length - 1) {
return accumulator / array.length;
} else {
return accumulator;
}
});
console.log(average); // 3

求数组的最大值


const arr = [1, 2, 3, 4, 5];
const max = arr.reduce((accumulator, currentValue) => Math.max(accumulator, currentValue));
console.log(max); // 5

求数组的最小值


const arr = [1, 2, 3, 4, 5];
const min = arr.reduce((accumulator, currentValue) => Math.min(accumulator, currentValue));
console.log(min); // 1

数组去重


const arr = [1, 2, 3, 3, 4, 4, 5];
const uniqueArr = arr.reduce((accumulator, currentValue) => {
if (!accumulator.includes(currentValue)) {
accumulator.push(currentValue);
}
return accumulator;
}, []);
console.log(uniqueArr); // [1, 2, 3, 4, 5]

计算数组中每个元素出现的次数


const arr = [1, 2, 3, 3, 4, 4, 5];
const countMap = arr.reduce((accumulator, currentValue) => {
if (!accumulator[currentValue]) {
accumulator[currentValue] = 1;
} else {
accumulator[currentValue]++;
}
return accumulator;
}, {});
console.log(countMap); // {1: 1, 2: 1, 3: 2, 4: 2, 5: 1}

实现数组分组


const arr = [1, 2, 3, 4, 5];
const result = arr.reduce((accumulator, currentValue) => {
if (currentValue % 2 === 0) {
accumulator.even.push(currentValue);
} else {
accumulator.odd.push(currentValue);
}
return accumulator;
}, { even: [], odd: [] });
console.log(result); // {even: [2, 4], odd: [1, 3, 5]}

计算数组中连续递增数字的长度


const arr = [1, 2, 3, 5, 6, 7, 8, 9];
const result = arr.reduce((accumulator, currentValue, index, array) => {
if (index === 0 || currentValue !== array[index - 1] + 1) {
accumulator.push([currentValue]);
} else {
accumulator[accumulator.length - 1].push(currentValue);
}
return accumulator;
}, []);
const maxLength = result.reduce((accumulator, currentValue) => Math.max(accumulator, currentValue.length), 0);
console.log(maxLength); // 5

计算对象数组的属性总和


const arr = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 },
];
const result = arr.reduce((accumulator, currentValue) => accumulator + currentValue.age, 0);
console.log(result); // 90

将对象数组转换为键值对对象


const arr = [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
{ name: 'Charlie', age: 35 },
];
const result = arr.reduce((accumulator, currentValue) => {
accumulator[currentValue.name] = currentValue.age;
return accumulator;
}, {});
console.log(result); // {Alice: 25, Bob: 30, Charlie: 35}

计算数组中出现次数最多的元素


const arr = [1, 2, 3, 4, 4, 4, 5, 5, 6, 6, 6, 6];
const result = arr.reduce((accumulator, currentValue) => {
accumulator[currentValue] = (accumulator[currentValue] || 0) + 1;
return accumulator;
}, {});
const maxCount = Math.max(...Object.values(result));
const mostFrequent = Object.keys(result).filter(key => result[key] === maxCount).map(Number);
console.log(mostFrequent); // [6]

实现 Promise 串行执行


const promise1 = () => Promise.resolve('one');
const promise2 = (input) => Promise.resolve(input + ' two');
const promise3 = (input) => Promise.resolve(input + ' three');

const promises = [promise1, promise2, promise3];
const result = promises.reduce((accumulator, currentValue) => {
return accumulator.then(currentValue);
}, Promise.resolve('start'));
result.then(console.log); // 'one two three'

对象属性值求和


const obj = {
a: 1,
b: 2,
c: 3
};
const result = Object.values(obj).reduce((accumulator, currentValue) => accumulator + currentValue);
console.log(result); // 6

按属性对数组分组


const arr = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Mary' },
{ id: 3, name: 'Bob' },
{ id: 4, name: 'Mary' }
];
const result = arr.reduce((accumulator, currentValue) => {
const key = currentValue.name;
if (!accumulator[key]) {
accumulator[key] = [];
}
accumulator[key].push(currentValue);
return accumulator;
}, {});
console.log(result);
/*
{
John: [{ id: 1, name: 'John' }],
Mary: [
{ id: 2, name: 'Mary' },
{ id: 4, name: 'Mary' }
]
,
Bob: [{ id: 3, name: 'Bob' }]
}
*/

扁平化数组


// 如果你有一个嵌套的数组,可以使用reduce将其扁平化成一个一维数组。例如:
const nestedArray = [[1, 2], [3, 4], [5, 6]];
const flattenedArray = nestedArray.reduce((acc, curr) => acc.concat(curr), []);
console.log(flattenedArray); // [1, 2, 3, 4, 5, 6]

合并对象


// 可以使用reduce将多个对象合并成一个对象。例如:
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const obj3 = { e: 5, f: 6 };
const mergedObj = [obj1, obj2, obj3].reduce((acc, curr) => Object.assign(acc, curr), {});
console.log(mergedObj); // {a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}
复制代码
作者:思学堂
来源:juejin.cn/post/7223278436893163581
>
收起阅读 »

都这么多年了,作为一个前端的你是不是连Symbol都不会用

web
Symbol是JavaScript中的原始数据类型之一,它表示一个唯一的、不可变的值,通常用作对象属性的键值。由于Symbol值是唯一的,因此可以防止对象属性被意外地覆盖或修改。以下是Symbol的方法和属性整理: 属性 Symbol.length Symbo...
继续阅读 »

Symbol是JavaScript中的原始数据类型之一,它表示一个唯一的、不可变的值,通常用作对象属性的键值。由于Symbol值是唯一的,因此可以防止对象属性被意外地覆盖或修改。以下是Symbol的方法和属性整理:


属性


Symbol.length


Symbol构造函数的length属性值为0。


示例代码:


console.log(Symbol.length); // 0

方法


Symbol.for()


Symbol.for()方法会根据给定的字符串key,返回一个已经存在的symbol值。如果不存在,则会创建一个新的Symbol值并将其注册到全局Symbol注册表中。


示例代码:


const symbol1 = Symbol.for('foo');
const symbol2 = Symbol.for('foo');

console.log(symbol1 === symbol2); // true

使用场景: 当我们需要使用一个全局唯一的Symbol值时,可以使用Symbol.for()方法来获取或创建该值。例如,在多个模块之间共享某个Symbol值时,我们可以使用Symbol.for()来确保获取到的Symbol值是唯一的。


Symbol.keyFor()


Symbol.keyFor()方法会返回一个已经存在的Symbol值的key。如果给定的Symbol值不存在于全局Symbol注册表中,则返回undefined。


示例代码:


const symbol1 = Symbol.for('foo');
const key1 = Symbol.keyFor(symbol1);

const symbol2 = Symbol('bar');
const key2 = Symbol.keyFor(symbol2);

console.log(key1); // 'foo'
console.log(key2); // undefined

使用场景: 当我们需要获取一个全局唯一的Symbol值的key时,可以使用Symbol.keyFor()方法。但需要注意的是,只有在该Symbol值被注册到全局Symbol注册表中时,才能使用Symbol.keyFor()方法获取到其key。


Symbol()


Symbol()函数会返回一个新的、唯一的Symbol值。可以使用可选参数description来为Symbol值添加一个描述信息。


示例代码:


const symbol1 = Symbol('foo');
const symbol2 = Symbol('foo');

console.log(symbol1 === symbol2); // false

使用场景: 当我们需要使用一个唯一的Symbol值时,可以使用Symbol()函数来创建该值。通常情况下,我们会将Symbol值用作对象属性的键值,以确保该属性不会被意外地覆盖或修改。


Symbol.prototype.toString()


Symbol.prototype.toString()方法会返回Symbol值的字符串表示形式,该表示形式包含Symbol()函数创建时指定的描述信息。


示例代码:


const symbol = Symbol('foo');

console.log(symbol.toString()); // 'Symbol(foo)'

使用场景: 当我们需要将一个Symbol值转换成字符串时,可以使用Symbol.prototype.toString()方法。


Symbol.prototype.valueOf()


Symbol.prototype.valueOf()方法会返回Symbol值本身。


示例代码:


const symbol = Symbol('foo');

console.log(symbol.valueOf()); // Symbol(foo)

使用场景: 当我们需要获取一个Symbol值本身时,可以使用Symbol.prototype.valueOf()方法。


Symbol.iterator


Symbol.iterator是一个预定义好的Symbol值,表示对象的默认迭代器方法。该方法返回一个迭代器对象,可以用于遍历该对象的所有可遍历属性。


示例代码:


const obj = { a: 1, b: 2 };

for (const key of Object.keys(obj)) {
console.log(key);
}
// Output:
// 'a'
// 'b'

for (const key of Object.getOwnPropertyNames(obj)) {
console.log(key);
}
// Output:
// 'a'
// 'b'

for (const key of Object.getOwnPropertySymbols(obj)) {
console.log(key);
}
// Output:
// No output

obj[Symbol.iterator] = function* () {
for (const key of Object.keys(this)) {
yield key;
}
}

for (const key of obj) {
console.log(key);
}
// Output:
// 'a'
// 'b'

使用场景: 当我们需要自定义一个对象的迭代行为时,可以通过定义Symbol.iterator属性来实现。例如,对于自定义的数据结构,我们可以定义它的Symbol.iterator方法以便能够使用for...of语句进行遍历。


Symbol.hasInstance


Symbol.hasInstance是一个预定义好的Symbol值,用于定义对象的 instanceof 操作符行为。当一个对象的原型链中存在Symbol.hasInstance方法时,该对象可以被instanceof运算符使用。


示例代码:


class Foo {
static [Symbol.hasInstance](obj) {
return obj instanceof Array;
}
}

console.log([] instanceof Foo); // true
console.log({} instanceof Foo); // false

使用场景: 当我们需要自定义一个对象的 instanceof 行为时,可以通过定义Symbol.hasInstance方法来实现。


Symbol.isConcatSpreadable


Symbol.isConcatSpreadable是一个预定义好的Symbol值,用于定义对象在使用concat()方法时的展开行为。如果一个对象的Symbol.isConcatSpreadable属性为false,则在调用concat()方法时,该对象不会被展开。


示例代码:


const arr1 = [1, 2];
const arr2 = [3, 4];
const obj = { length: 2, 0: 5, 1: 6, [Symbol.isConcatSpreadable]: false };

console.log(arr1.concat(arr2)); // [1, 2, 3, 4]
console.log(arr1.concat(obj)); // [1, 2, { length: 2, 0: 5, 1: 6, [Symbol(Symbol.isConcatSpreadable)]: false }]

使用场景: 当我们需要自定义一个对象在使用concat()方法时的展开行为时,可以通过定义Symbol.isConcatSpreadable属性来实现。


Symbol.toPrimitive


Symbol.toPrimitive是一个预定义好的Symbol值,用于定义对象在被强制类型转换时的行为。如果一个对象定义了Symbol.toPrimitive方法,则在将该对象转换为原始值时,会调用该方法。


示例代码:


const obj = {
valueOf() {
return 1;
},
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return 2;
} else if (hint === 'string') {
return 'foo';
} else {
return 'default';
}
}
};

console.log(+obj); // 2
console.log(`${obj}`); // 'foo'
console.log(obj + ''); // 'default'


使用场景: 当我们需要自定义一个对象在被强制类型转换时的行为时,可以通过定义Symbol.toPrimitive方法来实现。


Symbol.toStringTag


Symbol.toStringTag是一个预定义好的Symbol值,用于定义对象在调用Object.prototype.toString()方法时返回的字符串。如果一个对象定义了Symbol.toStringTag属性,则在调用该对象的toString()方法时,会返回该属性对应的字符串。


示例代码:


class Foo {
get [Symbol.toStringTag]() {
return 'Bar';
}
}

console.log(Object.prototype.toString.call(new Foo())); // '[object Bar]'

使用场景: 当我们需要自定义一个对象在调用Object.prototype.toString()方法时返回的字符串时,可以通过定义Symbol.toStringTag属性来实现。


Symbol.species


Symbol.species是一个预定义好的Symbol值,用于定义派生对象的构造函数。如果一个对象定义了Symbol.species属性,则在调用该对象的派生方法(如Array.prototype.map())时,返回的新对象会使用该属性指定的构造函数。


示例代码:


class MyArray extends Array {
static get [Symbol.species]() {
return Array;
}
}

const myArr = new MyArray(1, 2, 3);
const arr = myArr.map(x => x * 2);

console.log(arr instanceof MyArray); // false
console.log(arr instanceof Array); // true

使用场景: 当我们需要自定义一个派生对象的构造函数时,可以通过定义Symbol.species属性来实现。


Symbol.match


Symbol.match是一个预定义好的Symbol值,用于定义对象在调用String.prototype.match()方法时的行为。如果一个对象定义了Symbol.match方法,则在调用该对象的match()方法时,会调用该方法进行匹配。


示例代码:


class Foo {
[Symbol.match](str) {
return str.indexOf('foo') !== -1;
}
}

console.log('foobar'.match(new Foo())); // true
console.log('barbaz'.match(new Foo())); // false

使用场景: 当我们需要自定义一个对象在调用String.prototype.match()方法时的行为时,可以通过定义Symbol.match方法来实现。


Symbol.replace


Symbol.replace是一个预定义好的Symbol值,用于定义对象在调用String.prototype.replace()方法时的行为。如果一个对象定义了Symbol.replace方法,则在调用该对象的replace()方法时,会调用该方法进行替换。


示例代码:


class Foo {
[Symbol.replace](str, replacement) {
return str.replace('foo', replacement);
}
}

console.log('foobar'.replace(new Foo(), 'baz')); // 'bazbar'
console.log('barbaz'.replace(new Foo(), 'baz')); // 'barbaz'

使用场景: 当我们需要自定义一个对象在调用String.prototype.replace()方法时的行为时,可以通过定义Symbol.replace方法来实现。


Symbol.search


Symbol.search是一个预定义好的Symbol值,用于定义对象在调用String.prototype.search()方法时的行为。如果一个对象定义了Symbol.search


class Foo {
[Symbol.search](str) {
return str.indexOf('foo');
}
}

console.log('foobar'.search(new Foo())); // 0
console.log('barbaz'.search(new Foo())); // -1


使用场景: 当我们需要自定义一个对象在调用String.prototype.search()方法时的行为时,可以通过定义Symbol.search方法来实现。


Symbol.split


Symbol.split是一个预定义好的Symbol值,用于定义对象在调用String.prototype.split()方法时的行为。如果一个对象定义了Symbol.split方法,则在调用该对象的split()方法时,会调用该方法进行分割。


示例代码:


class Foo {
[Symbol.split](str) {
return str.split(' ');
}
}

console.log('foo bar baz'.split(new Foo())); // ['foo', 'bar', 'baz']
console.log('foobarbaz'.split(new Foo())); // ['foobarbaz']

使用场景: 当我们需要自定义一个对象在调用String.prototype.split()方法时的行为时,可以通过定义Symbol.split方法来实现。


Symbol.iterator


Symbol.iterator是一个预定义好的Symbol值,用于定义对象在被遍历时的行为。如果一个对象定义了Symbol.iterator方法,则可以使用for...of循环、扩展运算符等方式来遍历该对象。


示例代码:


class Foo {
constructor() {
this.items = ['foo', 'bar', 'baz'];
}

*[Symbol.iterator]() {
for (const item of this.items) {
yield item;
}
}
}

const foo = new Foo();

for (const item of foo) {
console.log(item);
}

// 'foo'
// 'bar'
// 'baz'

使用场景: 当我们需要自定义一个对象在被遍历时的行为时,可以通过定义Symbol.iterator方法来实现。比如,我们可以通过实现Symbol.iterator方法来支持自定义数据结构的遍历。


Symbol.toPrimitive


Symbol.toPrimitive是一个预定义好的Symbol值,用于定义对象在被强制类型转换时的行为。如果一个对象定义了Symbol.toPrimitive方法,则可以通过调用该方法来进行强制类型转换。


示例代码:


const obj = {
valueOf() {
return 1;
},
[Symbol.toPrimitive](hint) {
if (hint === 'default') {
return 'default';
} else if (hint === 'number') {
return 2;
} else {
return 'foo';
}
}
};

console.log(+obj); // 2
console.log(`${obj}`); // 'foo'
console.log(obj + ''); // 'default'

使用场景: 当我们需要自定义一个对象在被强制类型转换时的行为时,可以通过定义Symbol.toPrimitive方法来实现。


Symbol.toStringTag


Symbol.toStringTag是一个预定义好的Symbol值,用于定义对象在调用Object.prototype.toString()方法时返回的字符串。如果一个对象定义了Symbol.toStringTag属性,则在调用该对象的toString()方法时,会返回该属性对应的字符串。


示例代码:


class Foo {
get [Symbol.toStringTag]() {
return 'Bar';
}
}

console.log(Object.prototype.toString.call(new Foo())); // '[object Bar]'

使用场景: 当我们需要自定义一个对象在调用Object.prototype.toString()方法时返回的字符串时,可以通过定义Symbol.toStringTag属性来实现。这样做有助于我们更清晰地表示对象的类型。


Symbol.unscopables


Symbol.unscopables是一个预定义好的Symbol值,用于定义对象在使用with语句时的行为。如果一个对象定义了Symbol.unscopables属性,则在使用with语句时,该对象的指定属性将不会被绑定到with语句的环境中。


示例代码:


const obj = {
a: 1,
b: 2,
c: 3,
[Symbol.unscopables]: {
c: true
}
};

with (obj) {
console.log(a); // 1
console.log(b); // 2
console.log(c); // ReferenceError: c is not defined
}

使用场景: 由于with语句会带来一些安全性问题和性能问题,因此在实际开发中不建议使用。但是,如果确实需要使用with语句,可以通过定义Symbol.unscopables属性来避免某些属性被误绑定到with语句的环境中。


Symbol.hasInstance


Symbol.hasInstance是一个预定义好的Symbol值,用于定义对象在调用instanceof运算符时的行为。如果一个对象定义了Symbol.hasInstance方法,则在调用该对象的instanceof运算符时,会调用该方法来判断目标对象是否为该对象的实例。


示例代码:


class Foo {
static [Symbol.hasInstance](obj) {
return Array.isArray(obj);
}
}

console.log([] instanceof Foo); // true
console.log({} instanceof Foo); // false

使用场景: 当我们需要自定义一个对象在调用instanceof运算符时的行为时,可以通过定义Symbol.hasInstance方法来实现。比如,我们可以通过实现Symbol.hasInstance方法来支持自定义数据类型的判断。


总结


Symbol是ES6中新增的一种基本数据类型,用于表示独一无二的值。Symbol值在语言层面上解决了属性名冲突的问题,可以作为对象的属性名使用,并且不会被意外覆盖。除此之外,Symbol还具有以下特点:



  • Symbol值是唯一的,每个Symbol值都是独一无二的,即使是通过相同的描述字符串创建的Symbol值,也不会相等;

  • Symbol值可以作为对象的属性名使用,并且不会被意外覆盖;

  • Symbol值可以作为私有属性来使用,因为无法通过对象外部访问对象中的Symbol属性;

  • Symbol值可以被用作常量,因为它们是唯一的;

  • Symbol值可以用于定义迭代器、类型转换规则、私有属性、元编程等高级功能。


在使用Symbol时需要注意以下几点:



  • Symbol值不能使用new运算符创建;

  • Symbol值可以通过描述字符串来创建,但是描述字符串并不是Symbol值的唯一标识符;

  • Symbol属性在使用时需要用[]来访问,不能使用.运算符;

  • 同一对象中的多个Symbol属性是独立的,它们之间不会互相影响。


总之,Symbol是一个非常有用的数据类型,在JavaScript中具有非常广泛的应用。使用Symbol可以有效地避免属性名冲突问题,并且可以为对象提供一些高级功能。熟练掌握Symbol,有助于我们写出更加健壮、高效和可维护的Jav

作者:布衣1983
来源:juejin.cn/post/7226193000496463928
aScript代码。

收起阅读 »

实现tabs圆角及反圆角效果

web
直接上最终效果 基本页面结构 <div class="tab-list"> <div v-for="tab in tabList" :key="tab.id" ...
继续阅读 »

直接上最终效果


image.png


image.png


基本页面结构


      <div class="tab-list">
<div
v-for="tab in tabList"
:key="tab.id"
class="tab-item"
:class="activeTab === tab.id ? 'tab-selected' : ''"
@click="onTab(tab.id)"
>

<image :src="tab.icon" class="tab-icon" />
<div>{{ tab.label }}</div>
</div>
</div>

  $tab-height: 52px;
$tab-bgcolor: #e2e8f8

.tab-list {
display: flex;
border-radius: 12px 12px 0 0;
background-color: $tab-bgcolor;

.tab-item {
flex: 1;
height: $tab-height;
display: flex;
justify-content: center;
align-items: center;
font-size: 15px;
opacity: 0.65;
color: $primary-color;
font-weight: 600;
position: relative;
}

.tab-icon {
width: 17px;
height: 17px;
margin-right: 4px;
}

.tab-selected {
opacity: 1;
background: #ffffff;
}

}


image.png


image.png


添加上半两个圆角


这个很简单


    .tab-selected {
opacity: 1;
background: #ffffff;
// 新增
border-radius: 12px 12px 0 0;
}

image.png


添加下半两个反圆角


加两个辅助的伪元素


    .tab-selected::before {
content: '';
position: absolute;
left: -12px;
bottom: 0;
width: 12px;
height: $tab-height;
background: red;
border-radius: 0 0 12px 0;
}
.tab-selected::after {
content: '';
position: absolute;
right: -12px;
bottom: 0;
width: 12px;
height: $tab-height;
background: red;
border-radius: 0 0 0 12px;
}

image.png


image.png


再添加box-shadow


    .tab-selected {
opacity: 1;
background: #ffffff;
border-radius: 12px 12px 0 0;
// 新装置
box-shadow: 12px 12px 0 0 blue, -12px 12px 0 0 blue;
}

image.png


image.png


到这个就差不多可以收尾了,把伪元素的背景色改为tabs的背景色


    .tab-selected::before {
content: '';
position: absolute;
left: -12px;
bottom: 0;
width: 12px;
height: $tab-height;
background: #e2e8f8; // 修改
border-radius: 0 0 12px 0;
}
.tab-selected::after {
content: '';
position: absolute;
right: -12px;
bottom: 0;
width: 12px;
height: $tab-height;
background: #e2e8f8; // 修改
border-radius: 0 0 0 12px;
}

image.png


再处理下box-shadow


    .tab-selected {
opacity: 1;
background: #ffffff;
border-radius: 12px 12px 0 0;
// box-shadow: 12px 12px 0 0 blue, -12px 12px 0 0 blue;
box-shadow: 12px 12px 0 0 #ffffff, -12px 12px 0 0 #ffffff;
}

完美


image.png


但是两边的还会有问题


image.png


image.png


父级元素overflow:hidden即可


.tab-list {
display: flex;
position: relative;
z-index: 2;
border-radius: 12px 12px 0 0;
background-color: #e2e8f8;
overflow: hidden; // 新增
}

收工


参考



CSS3 实现双圆角 Tab 菜单



相关知识点回顾


box-shadow




  1. x轴偏移 右为正

  2. y轴偏移 下为正

  3. 模糊半径

  4. 阴影大小

  5. 颜色

  6. 位置 inset



border-radius


先记得下面这个图


image.png



  • 一个值的时候设置1/2/3/4

  • 两个值的时候设置 1/32/4

  • 三个值的时候设置12/4, 3

  • 四个值就简单了1234


border-radius 如果需要设置某个角的圆角边框,可以使用下面四个



  1. border-top-left-radius;

  2. border-top-right-radius;

  3. border-bottom-left-radius;

  4. border-bottom-right-radius;


又要画一个图了,上面四个属性,又可以设置一个值或者两个值


第一个值是水平半径,第二个是垂直半径。如果省略第二个值,它是从第一个复制


image.png


image.png


当然border-radius也可以分别设置水平半径 垂直半径



border-radius: 10px / 20px 30px 40px 50px; 水平半径都为10px, 但四个角的垂直半径分别设置



image.png



border-radius: 50px 10px / 20px;



image.png


下期预告


曲线圆角tabs


image.png


传送门


作者:feng_cc
来源:juejin.cn/post/7224311569777934392
收起阅读 »

用Flutter写一个单例

在Flutter中创建单例可以使用Dart语言中的静态变量和工厂方法的组合来实现。下面是一个示例代码: class MySingleton { // 静态变量 static final MySingleton _singleton = MySingle...
继续阅读 »

在Flutter中创建单例可以使用Dart语言中的静态变量和工厂方法的组合来实现。下面是一个示例代码:


class MySingleton {
// 静态变量
static final MySingleton _singleton = MySingleton._internal();

// 工厂方法
factory MySingleton() {
return _singleton;
}

// 私有构造函数
MySingleton._internal();

// 其他方法
void doSomething() {
print("Doing something...");
}
}

在上面的代码中,MySingleton类有一个私有的构造函数,这意味着它不能直接实例化。


相反,它使用一个静态变量 _singleton 来存储唯一的实例,并使用一个工厂方法来获取该实例。因此,当您需要引用该单例时,您只需调用 MySingleton() 方法,就可以得到唯一的实例。


要使用该单例,只需调用 MySingleton() 方法,并调用其公共方法,如 doSomething()


MySingleton mySingleton = MySingleton();
mySingleton.doSomething();

Flutter单例模式可以在以下场景中使用:



  1. 网络请求:在网络请求过程中,您可能只需要一个单例来管理所有的HTTP客户端和连接。使用单例模式可以确保只有一个实例在整个应用程序中被创建和使用,这样可以节约系统资源并避免重复创建相同的实例。

  2. 数据库操作:在应用程序中,您可能需要与数据库进行交互。使用单例模式,您可以确保只需要一个单例来管理数据库连接并执行所有数据库操作。

  3. 状态管理:在Flutter中,您可以使用单例模式来管理应用程序状态。您可以创建一个具有全局作用域的单例,以存储和管理应用程序中的状态,并确保在整个应用程序中只有一个实例在使用。

  4. 全局管理:在某些情况下,您可能需要在整个应用程序中共享某些对象或数据。使用单例模式,您可以创建一个具有全局作用域的单例来存储这些对象和数据,并确保在整个应用程序中只有一个实例在使用。 在这些场景中,使用单例模式可以简化代码并提高应用程序性能,避免了创建多个重复的对象的开销。

作者:早起的年轻人
链接:https://juejin.cn/post/7230416597012463677
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Flutter list 数组排序

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。 sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。 以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序: List<...
继续阅读 »

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。
sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。
以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序:


List<int> numbers = [1, 3, 2, 5, 4];
// 升序排序
numbers.sort((a, b) => a.compareTo(b));
print(numbers); // 输出:[1, 2, 3, 4, 5]

// 降序排序
numbers.sort((a, b) => b.compareTo(a));
print(numbers); // 输出:[5, 4, 3, 2, 1]

在上述代码中,我们使用了sort()方法将数字列表按照升序和降序进行了排序。


在比较函数中,我们使用了 compareTo() 方法来比较两个数字对象。


如果想按照其他字段进行排序,只需将比较函数中的a和b替换为您想要排序的字段即可。




以下是示例代码,假设您有一个包含Person对象的列表,可以按照Person的年龄字段进行排序:


class Person {
String name;
int age;

Person({this.name, this.age});
}

List<Person> persons = [
Person(name: "John", age: 30),
Person(name: "Jane", age: 20),
Person(name: "Bob", age: 25),
];

// 按照年龄字段进行排序
persons.sort((a, b) => a.age.compareTo(b.age));

// 输出排序后的列表
print(persons);

在上述代码中,我们使用了sort()函数将Person对象列表按照年龄字段进行排序。
在该示例中,我们使用了compareTo()函数来比较Person对象的年龄字段,并按照升序排序。


作者:早起的年轻人
链接:https://juejin.cn/post/7230420475494137913
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

No Focused Window ANR是怎样产生的

ANR
之前我们讲过因为事件没有得到及时处理,引起的ANR问题。但这只是Input Dispatching Timeout中的一种情况,还有一种情况,在我们应用中出现的也很常见,就是No Focused Window ANR,这个又是在哪些情况下产生的呢? 由之前的文...
继续阅读 »

之前我们讲过因为事件没有得到及时处理,引起的ANR问题。但这只是Input Dispatching Timeout中的一种情况,还有一种情况,在我们应用中出现的也很常见,就是No Focused Window ANR,这个又是在哪些情况下产生的呢?


由之前的文章,我们知道,点击事件都是由InputDispatcher来分发的,我们直接来看InputDispatcher的源码。


No Focused Window ANR如何产生


如果是Key事件,或Motion事件,都需要找到焦点窗口取处理,都会调用到findFocusedWindowTargetsLocked()。


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
const EventEntry& entry,
std::vector<InputTarget>& inputTargets,
nsecs_t* nextWakeupTime) {
std::string reason;

int32_t displayId = getTargetDisplayId(entry);
// mFocusedWindowHandlesByDisplay在setInputWindowsLocked()里赋值
sp<InputWindowHandle> focusedWindowHandle =
getValueByKey(mFocusedWindowHandlesByDisplay, displayId);
// mFocusedApplicationHandlesByDisplay在setFocusedApplication()里赋值
sp<InputApplicationHandle> focusedApplicationHandle =
getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);

// focusedWindowHandle和focusedApplicationHandle都为空时表示当前无窗口,该事件会被丢弃,不会执行dispatchEventLocked
// 一般出现两个都为空的场景,是在窗口切换的过程,此时不处理事件注入
if (focusedWindowHandle == nullptr && focusedApplicationHandle == nullptr) {
return INPUT_EVENT_INJECTION_FAILED;
}

// focusedWindowHandle为空但focusedApplicationHandle不为空时开始ANR检查
if (focusedWindowHandle == nullptr && focusedApplicationHandle != nullptr) {
// 默认mNoFocusedWindowTimeoutTime没有值,第一次检查ANR会走下面这个流程
if (!mNoFocusedWindowTimeoutTime.has_value()) {
// DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5s * HwTimeoutMultiplier();
// 默认input dispatch timeout时间时5s
const nsecs_t timeout = focusedApplicationHandle->getDispatchingTimeout(
DEFAULT_INPUT_DISPATCHING_TIMEOUT.count());
// 给mNoFocusedWindowTimeoutTime赋值,触发ANR时会检查这个值是否为空,不为空才触发ANR
mNoFocusedWindowTimeoutTime = currentTime + timeout;
// 把当前的focusedApplicationHandle赋值给mAwaitedFocusedApplication,触发ANR时会检查这个值是否为空,不为空才触发ANR
mAwaitedFocusedApplication = focusedApplicationHandle;
mAwaitedApplicationDisplayId = displayId;
*nextWakeupTime = *mNoFocusedWindowTimeoutTime;
// 返回INPUT_EVENT_INJECTION_PENDING表示dispatchKeyLocked()或者dispatchMotionLocked()为false
return INPUT_EVENT_INJECTION_PENDING;
} else if (currentTime > *mNoFocusedWindowTimeoutTime) {
// Already raised ANR. Drop the event
return INPUT_EVENT_INJECTION_FAILED;
} else {
// Still waiting for the focused window
return INPUT_EVENT_INJECTION_PENDING;
}
}

// 如果走到这个流程,说明没有ANR,清空mNoFocusedWindowTimeoutTime和mAwaitedFocusedApplication
resetNoFocusedWindowTimeoutLocked();
return INPUT_EVENT_INJECTION_SUCCEEDED;
}

主要逻辑:



  • 如果focusedWindowHandle和focusedApplicationHandle都为null,一般发生在窗口切换的时候,返回INPUT_EVENT_INJECTION_FAILED,直接drop事件,不做处理

  • 如果focusedWindowHandle为null,focusedApplicationHandle不为null,返回INPUT_EVENT_INJECTION_PENDING,在nextWakeupTime之后唤醒,检查是否发生ANR

    • mNoFocusedWindowTimeoutTime:记录no focused window timeout的时间

    • mAwaitedFocusedApplication:记录focusedApplicationHandle

    • nextWakeupTime: 下次唤醒pollInner的时间




接下来看看检查ANR的逻辑:


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
nsecs_t InputDispatcher::processAnrsLocked() {
const nsecs_t currentTime = now();
nsecs_t nextAnrCheck = LONG_LONG_MAX;
// 在findFocusedWindowTargetsLocked()中,如果focusedWindowHandle为空,focusedApplicationHandle不为空,以下条件就会满足
if (mNoFocusedWindowTimeoutTime.has_value() && mAwaitedFocusedApplication != nullptr) {
// mNoFocusedWindowTimeoutTime为检查时间+5s,如果currentTime大于等于mNoFocusedWindowTimeoutTime,表示超时
if (currentTime >= *mNoFocusedWindowTimeoutTime) {
// 触发ANR流程,此处触发的ANR类型是xxx does not have a focused window
processNoFocusedWindowAnrLocked();
// 清空mAwaitedFocusedApplication,下次就不会再走ANR流程
mAwaitedFocusedApplication.clear();
mNoFocusedWindowTimeoutTime = std::nullopt;
return LONG_LONG_MIN;
} else {
// Keep waiting
const nsecs_t millisRemaining = ns2ms(*mNoFocusedWindowTimeoutTime - currentTime);
ALOGW("Still no focused window. Will drop the event in %" PRId64 "ms", millisRemaining);
// 还没有超时,更新检查时间
nextAnrCheck = *mNoFocusedWindowTimeoutTime;
}
}
....
// 如果走到这个流程,ANR类型是xxx is not responding. Waited xxx ms for xxx
// 这个地方,focusedWindowHandle和focusedApplicationHandle都是不为空的场景
onAnrLocked(*connection);
return LONG_LONG_MIN;
}

主要流程:



  • 如果mNoFocusedWindowTimeoutTime有值,且mAwaitedFocusedApplication不为空

    • 超时:调用processNoFocusedWindowAnrLocked触发ANR

    • 未超时:更新检查时间



  • 继续检查input事件是否超时,如果超时,则调用onAnrLocked触发ANR


接下来,我们看看processNoFocusedAnrLocked的流程:


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
void InputDispatcher::processNoFocusedWindowAnrLocked() {
// 在触发ANR前,再获取一次当前的focusedApplication
sp<InputApplicationHandle> focusedApplication =
getValueByKey(mFocusedApplicationHandlesByDisplay, mAwaitedApplicationDisplayId);
// 检查触发ANR时的条件是focusedApplication不为空
// 如果此时focusedApplication为空,或者focusedApplication不等于前一个mAwaitedFocusedApplication表示已经切换application focus,取消触发ANR
if (focusedApplication == nullptr ||
focusedApplication->getApplicationToken() !=
mAwaitedFocusedApplication->getApplicationToken()) {
return; // The focused application has changed.
}
// 在触发ANR前,再获取一次当前的focusedWindowHandle
const sp<InputWindowHandle>& focusedWindowHandle =
getFocusedWindowHandleLocked(mAwaitedApplicationDisplayId);
// 检查触发ANR时focusedWindowHandle为空,如果此时focusedWindowHandle不为空,取消触发ANR
if (focusedWindowHandle != nullptr) {
return; // We now have a focused window. No need for ANR.
}
// 通过前面的判断,还是无法拦截,说明该ANR无可避免,最终触发ANR
// 早期代码没有前面一系列的判断,是直接触发的ANR,会在性能较差的场景下出现误判
onAnrLocked(mAwaitedFocusedApplication);
}

主要流程:



  • 在这个方法里面,再次检查focusedApplication

    • 如果当前focusedApplication为空,或者和之前记录的mAwaitedFocusedApplication不一致,则说明窗口已经切换,不需要报ANR



  • 再次检查focusedWindow是否未空

    • 如果不为空,则不需要报ANR



  • 检查都通过之后,才会调用onAnrLocked,报no Focused Window ANR


focusedApplication设置流程


// frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
void InputDispatcher::setFocusedApplication(
int32_t displayId, const sp<InputApplicationHandle>& inputApplicationHandle) {
{ // acquire lock
std::scoped_lock _l(mLock);
// 获取当前的focusedApplicationHandle
sp<InputApplicationHandle> oldFocusedApplicationHandle =
getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);
// 如果当前的focusedApplicationHandle跟触发ANR是的focusedApplicationHandle是一样且
// 新的focusedApplicationHandle跟旧的不一样,说明focusedApplicationHandle有更新
// 需要重置ANR计时
if (oldFocusedApplicationHandle == mAwaitedFocusedApplication &&
inputApplicationHandle != oldFocusedApplicationHandle) {
// 重置ANR计时
resetNoFocusedWindowTimeoutLocked();
}

if (inputApplicationHandle != nullptr && inputApplicationHandle->updateInfo()) {
if (oldFocusedApplicationHandle != inputApplicationHandle) {
// 赋值新的inputApplicationHandle到mFocusedApplicationHandlesByDisplay,在findFocusedWindowTargetsLocked()时用到
mFocusedApplicationHandlesByDisplay[displayId] = inputApplicationHandle;
}
} else if (oldFocusedApplicationHandle != nullptr) {
// 如果inputApplicationHandle为空,oldFocusedApplicationHandle不为空,需要清除oldFocusedApplicationHandle
oldFocusedApplicationHandle.clear();
// 走到这个流程会出现findFocusedWindowTargetsLocked()中focusedApplicationHandle为空
mFocusedApplicationHandlesByDisplay.erase(displayId);
}
} // release lock

// Wake up poll loop since it may need to make new input dispatching choices.
mLooper->wake();
}

主要流程:



  • 如果inputApplicationHandle与oldFocusedApplication,则要重置ANR计时

  • 如果inputApplicationHandle不为空,则更新map中的值

  • 如果inputApplicationHandle为空,则清除oldFocusedApplication


这个方法,是从AMS调过来的,主要流程如下图:
image.png


focusedWindow设置流程


// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
// 当VSYNC信号来了之后,会调用到SurfaceFlinger的onMessageInvalidate()方法
// SurfaceFlinger::onMessageInvalidate()
// ==> SurfaceFlinger: updateInputFlinger()
// ==> SurfaceFlinger: updateInputWindowInfo()
// ==> InputManager::setInputWindows()
// ==> InputDispatcher::setInputWindows()
// ==> InputDispatcher::setInputWindowsLocked()
void InputDispatcher::setInputWindowsLocked(
const std::vector<sp<InputWindowHandle>>& inputWindowHandles, int32_t displayId) {

// ......
const std::vector<sp<InputWindowHandle>> oldWindowHandles = getWindowHandlesLocked(displayId);
// 更新mWindowHandlesByDisplay这个map,然后通过getWindowHandlesLocked()找newFocusedWindowHandle
updateWindowHandlesForDisplayLocked(inputWindowHandles, displayId);

sp<InputWindowHandle> newFocusedWindowHandle = nullptr;
bool foundHoveredWindow = false;
// 在mWindowHandlesByDisplay这个map里面找newFocusedWindowHandle
for (const sp<InputWindowHandle>& windowHandle : getWindowHandlesLocked(displayId)) {
// newFocusedWindowHandle要不为空,windowHandle具备focusable和visible属性
if (!newFocusedWindowHandle && windowHandle->getInfo()->hasFocus &&
windowHandle->getInfo()->visible) {
// 给newFocusedWindowHandle赋值,最后这个值存到mFocusedWindowHandlesByDisplay这个map
newFocusedWindowHandle = windowHandle;
}
if (windowHandle == mLastHoverWindowHandle) {
foundHoveredWindow = true;
}
}

if (!foundHoveredWindow) {
mLastHoverWindowHandle = nullptr;
}

// 在mFocusedWindowHandlesByDisplay这个map里找当前的焦点窗口
sp<InputWindowHandle> oldFocusedWindowHandle =
getValueByKey(mFocusedWindowHandlesByDisplay, displayId);

// 判断oldFocusedWindowHandle是否等于newFocusedWindowHandle,如果相等则不走focus change流程
if (!haveSameToken(oldFocusedWindowHandle, newFocusedWindowHandle)) {
// 如果当前的焦点窗口不为空,需要从mFocusedWindowHandlesByDisplay移除掉
if (oldFocusedWindowHandle != nullptr) {
sp<InputChannel> focusedInputChannel =
getInputChannelLocked(oldFocusedWindowHandle->getToken());
if (focusedInputChannel != nullptr) {
CancelationOptions options(CancelationOptions::CANCEL_NON_POINTER_EVENTS,
"focus left window");
synthesizeCancelationEventsForInputChannelLocked(focusedInputChannel, options);
// 新建一个FocusEntry加入到mInboundQueue去dispatch
enqueueFocusEventLocked(*oldFocusedWindowHandle, false /*hasFocus*/);
}
// oldFocusedWindowHandle不为空时需要移除旧的
mFocusedWindowHandlesByDisplay.erase(displayId);
}
// 走到这个流程,如果oldFocusedWindowHandle不为空,newFocusedWindowHandle为空,那么在findFocusedWindowTargetsLocked()中的focusedWindowHandle为空
// 如果newFocusedWindowHandle不为空,更新mFocusedWindowHandlesByDisplay
if (newFocusedWindowHandle != nullptr) {
// 更新mFocusedWindowHandlesByDisplay,在findFocusedWindowTargetsLocked()时用到
mFocusedWindowHandlesByDisplay[displayId] = newFocusedWindowHandle;
// 新建一个FocusEntry加入到mInboundQueue去dispatch
enqueueFocusEventLocked(*newFocusedWindowHandle, true /*hasFocus*/);
}

if (mFocusedDisplayId == displayId) {
// 添加focusChanged到mCommandQueue,在dispatchOnce时会执行
onFocusChangedLocked(oldFocusedWindowHandle, newFocusedWindowHandle);
}
}

// ......
}

这个方法,是从WMS调过来的,主要流程如下图:
image.png


ANR可能的原因



  1. 设置focusedApplication和focusedWindow中间时间差太长,在这个时间差内发生了ANR



  • 设置focusedApplication发生在resumeTopActivity,也就是am_set_resumed_activity的时候。

  • 设置focusedWindow发生在onResume结束后,也就是调用WMS的addView添加完窗口之后。


在这个过程中,很有很多的生命周期流程,包括前一个Activity的onPause,Applicaiton的attachBaseContext, onCreate, Activity的onCreate,onStart,onResume。所有方法加起来耗时不能超过5s,否则很容易发生ANR。



  1. window被设置成了no_focusable,无法响应焦点。



  • 如果误将一个window设置成no_focusable,则窗口无法成为focusedWindow,也可能导致ANR的发生。

  • 不过这种情况一般比较少出现。

作者:尹学姐
链接:https://juejin.cn/post/7224839996402122809
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一线城市or回老家

前言 哈喽~还在纠结是继续一线城市干着,还是回老家发展吗?先带大家回顾一下我工作的经历 19年还在大四的时候,我就去了上海,干起了前端,敲起了代码,刚开始干啥啥不行,整个流程一脸懵逼,过需求、开发、对接口、提测…… 过需求嘛,不就是听一听pm怎么讲; 开发嘛...
继续阅读 »

前言


哈喽~还在纠结是继续一线城市干着,还是回老家发展吗?先带大家回顾一下我工作的经历


19年还在大四的时候,我就去了上海,干起了前端,敲起了代码,刚开始干啥啥不行,整个流程一脸懵逼,过需求、开发、对接口、提测……



  • 过需求嘛,不就是听一听pm怎么讲;

  • 开发嘛,就自己慢慢开发;

  • 对接口嘛,等着后端给呗,慢慢对;

  • 过测试用例嘛,就听听测试怎么讲呗;

  • 提测嘛,主要测试干,有bug我改改呗~


no!现在回想起当时的这些心里所想,简直是大错特错啦!经历过很多事情后,我来给你们整理个干货:



  • 过需求,很重要,不只是听pm说,自己还要审视需求,从技术的角度,让技术实现起来简单,又能满足产品需求,否则最后坑的还是自己,吭哧吭哧的去实现pm提出来的奇奇怪怪的需求~

  • 开发,不单单要开发,还要提前预估好时间,安排好自己的计划,有什么问题,要 delay了,都要提前跟pm说,否则 最后难办的还是你自己,熬大夜的还是自己。。提前跟他们说,提前要时间,提前规划好,我就是不加班的那个崽!

  • 对接口,也不仅仅是对接口,要提前跟后端要接口文档,否则你都不会想到你的后端怎么能给你跟你开发差别这么大的数据格式,尤其是陌生的后端,别问我咋知道的,说多了都是泪o(╥﹏╥)o

  • 过测试用例也要给我好好听,谁知道测试那个货看完需求文档后,理解的跟你开发的有啥区别,没准完全不一样,这时候你要给予反驳,问pm到底是啥,否则你会收到很多奇奇怪怪,每个字都认识,但是结合起来无法理解的bug。。一样也别问我咋知道的0.0

  • 提测,一定一定要自测,确保主体流程通顺,否则被打下来的话,是piapia的被测试打脸


从摆烂到涨知识


经历过初期的摧残之后,我进入到了摆烂期,因为什么都熟悉了,给东西也能做出来了,就日常摆烂了,直到跳槽去了另一家比较新型的互联网公司,接触了好多之前没接触的,干一天学到的东西是上家公司干一年也可能学不到的。


之前每次发包,是自己吭哧吭哧远程服务器,贴个包,现在是Jenkins自动化部署,一点就好;


之前没开发过微信小程序、没用react写过项目,现在天天是uniapp开发的微小和react+hooks的后管……


总之,就感觉学了好多东西,每天都在学习。


2022 放飞


直到2022年三月,上海疫情的到来,开启了在家办公,身边同事也被辞了好多。


2022年6月复工,又开始了正常去公司上班,但是任务很少,几乎没再学到东西了,每天上班就是再跟同事扯皮子。


2022年9月我也被辞了,公司因自己发展原因,辞退了我,然后就计划回老家了,给外面也干了三年了,决定在走之前玩一把,就去了杭州、去了好多之前想去的的地方



回老家安安稳稳


因为自己还养了两只猫,我自己还晕车,总之就很艰难的在2022年九月中旬回了老家,又休息了半个多月,开始找工作,老家的工作真的很不好找,boss、智联都被翻烂了,全聊过了,而且薪资也很低,简直是比之前的一半都低,面试也根本不像一线城市一样那么难,好多还不是双休,就这样艰难的挑挑拣拣,在十月中旬,我入职了,过起了躺平的日子,从来不加班,九点到,六点跑,双休,技术上有很多之前没接触过的,但不怕,慢慢整呗,而且我身兼数职,虽然是前端,但可以帮忙做icon图标,还可以当当测试。


跟之前比,难免有技术上和管理规范上的落差感,但是回老家后的生活相当充实和真实,每天都能吃的爸爸妈妈做的饭,走从小走过的路,虽然钱不多,但是真的幸福感+真实感上升了好多。


2023年我就希望可以安安稳稳,平平安安过着简单的小日子,只要自己觉得快乐就好。


还在犹豫在一线城市打拼还是回老家的友友们,你们也可以看看我的经历,来判断哟,我个人觉得如果不打算在一线城市买房安家的,早点回老家挺好的,安安稳稳,愿大家也可以过自己觉得舒服的日子哟~


作者:wenLi
链接:https://juejin.cn/post/7187315339846713399
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android自定义控件之虚拟摇杆(遥控飞机)

前言 之前在开发项目中,有一个功能是,设计一个虚拟摇杆,操作大疆无人机飞行,在实现过程中感觉比较锻炼自定义View的能力,在此记录一下,本文中摇杆代码从项目中抽取出来重新实现,如下是程序运行图: 功能分析 本次自定义View功能需求如下: 1.摇杆绘制 自定...
继续阅读 »

前言


之前在开发项目中,有一个功能是,设计一个虚拟摇杆,操作大疆无人机飞行,在实现过程中感觉比较锻炼自定义View的能力,在此记录一下,本文中摇杆代码从项目中抽取出来重新实现,如下是程序运行图:
虚拟摇杆.gif


功能分析


本次自定义View功能需求如下:

1.摇杆绘制

自定义View绘制摇杆大小圆,手指移动时只改变小圆位置,当手指触摸点在大圆外时,小圆圆心在大圆边缘上,并且绘制一条蓝色弧线,绘制度数为小圆圆心位置向两侧延伸45度(一般UI设计的时候,会给特定的圆弧形图片,如果显示图片就需要将图片移动到小圆圆心位置,之后根据手指触摸点与大圆圆心夹角来旋转图片,目前没有找到类似的圆弧图片,后期看能不能找到类似的)。

2.摇杆移动数据返回

返回摇杆移动产生的数据,根据这些数据控制飞行图片移动。在这里我返回的是飞机图片x,y坐标应该改变的值。这个值具体如何获得,在下面代码实现中讲解。

3.飞机图片移动

飞机图片移动相对简单,只需要在接收到摇杆数据的时候,修改飞机图片绘制位置,并重绘即可,需要注意的地方是摇杆移动飞机超出View边界该怎么处理。


代码实现


摇杆绘制和摇杆移动数据返回,通过自定义的RockerView内实现,飞机图片移动,通过自定义的FlyView实现,上述功能在RockerView和FlyView代码实现里面介绍。


摇杆(RockerView)


我们可以先从摇杆如何绘制开始。


首先从RockerView开头声明一些绘制需要一些变量,比如画笔,圆心坐标,手指触摸点坐标,圆半径等变量。


在init()方法内对画笔样式,颜色,View默认宽高等数据进行设置。


在onMeasure()方法内获取View的宽高模式,该方法简单可以概况为,宽高有具体值或者为match_parent。宽高设置为MeasureSpec.getSize()方法获取的数据,之后宽高值取两者中最小值,当宽高值在xml设置为wrap_content时,宽高取默认值,之后在方法末尾通过setMeasuredDimension()设置宽高。


在onLayout()方法内,对绘制圆等图像用到的变量进行赋值,例如,大圆圆心xy值,小圆圆心xy值,大小圆半径,绘制蓝色圆弧矩形,RockerView宽高等数据。


之后是onDraw()方法,在该方法内绘制大小圆,蓝色圆弧等图案。只不过蓝色圆弧需要加上判断条件来控制是否绘制。


手指触摸时绘制小圆位置改变,则需要重写onTouchEvent()方法,当手指按下或移动时,需要更新手指触摸点坐标,并判断手指触摸点是否超出大圆,超出大圆时,需要计算小圆圆心位置,并且还需要计算手指触摸点与圆心连线和x正半轴形成的夹角。并且通过接口返回摇杆移动的数据,飞机图片根据这些数据来移动。


绘制代码简单介绍如上,下面对View内一些需要注意地方进行介绍。如果看到完整代码,里面有一个自定义方法是initAngle(),该方法代码如下:


/** 计算夹角度数,并实现小圆圆心最多至大圆边上 */
private void initAngle() {
radian = Math.atan2((touchY - bigCenterY), (touchX - bigCenterX));
angle = (float) (radian * (180 / Math.PI));//范围-180-180
isBigCircleOut = false;
if (bigCenterX != -1 && bigCenterY != -1) {//大圆中心xy已赋值
double rxr = (double) Math.pow(touchX - bigCenterX, 2) + Math.pow(touchY - bigCenterY, 2);
distance = Math.sqrt(rxr);//手点击点距离大圆圆心距离
smallCenterX = touchX;
smallCenterY = touchY;
if (distance > bigRadius) {//距离大于半圆半径时,固定小圆圆心在大圆边缘上
smallCenterX = (int) (bigRadius / distance * (touchX - bigCenterX)) + bigCenterX;
smallCenterY = (int) (bigRadius / distance * (touchY - bigCenterY)) + bigCenterX;
isBigCircleOut = true;
}
}
}

这个方法用在onTouchEvent()方法的手指按下与移动事件中应用,这个方法前两行代码是计算手指触摸点与圆心连线和x正半轴形成的夹角取值,夹角取值范围如下图所示。
图片.png
代码先通过Math.atan2(y,x)方法获取手指触摸点与圆心连线和x正半轴之间的弧度制,获取弧度后通过(float) (radian * (180 / Math.PI))获取对应的度数,这里特别注意下Math.atan2(y,x)方法是y值在前,x在后。

此外这个方法还计算了手指触摸点与大圆圆心距离,以及判断手指触摸点是否在大圆外,以及在大圆外时,获取在大圆边缘上的小圆圆心的xy值。


在计算小圆圆心的坐标需要了解一个地方是,view实现过程中使用的坐标系是屏幕坐标系,屏幕坐标系是以View左上角为原点,原点左边是x的正半轴,原点下面是y正半轴,屏幕坐标系和数学坐标系是不一样。小圆圆心坐标获取原理,是根据三角形的相似原理获取,小圆圆心的坐标获取原理如下图所示:


图片.png
在上图中可以看到小圆y坐标的获取,小圆x坐标获取与y获取类似。可以直接把公式套进去。关于摇杆绘制的内容,至此差不多完成了,下面来处理返回摇杆移动数据的功能。


返回摇杆移动数据是通过自定义接口实现的。在触摸事件返回摇杆移动数据的事件有手指按下与移动。我们代码可以写为下面的形式(下面代码是伪代码)。


@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//返回摇杆移动数据的方法
break;
case MotionEvent.ACTION_UP:
...
break;
}
postInvalidate();
return true;
}

如果按照上面代码写法我们会发现,当我们手指按下不动的时候或者手指按下移动一会后手指不动,是不会触发ACTION_MOVE事件的,不触发这个事件,我们就无法返回摇杆移动的数据,进而无法控制飞机改变位置。效果图如下


虚拟摇杆_按下不移动的问题.gif
解决这个问题,需要使用Handler和Runnable,在Runnable的run方法内,实现接口方法,并调用自身。getFlyOffset()是传递摇杆移动数据的方法,代码如下:


private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (isStart){
getFlyOffset();
mHandler.postDelayed(this,drawTime);
}
}
};

之后在手指按下与点击事件里面,先判断Handler有没有开始,若isStart为true,则isStart改为false,并移除mRunnable,之后isStart改为true,延迟16ms执行mRunnable,当手指抬起时,若Handler状态为开始,则修改状态为false并移除mRunnable,这样就解决了手指按下不移动时,传递摇杆数据,相关代码如下:


@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
...
initAngle();
getFlyOffset();
if (isStart) {
isStart = false;
mHandler.removeCallbacks(mRunnable);
}
isStart = true;
mHandler.postDelayed(mRunnable,drawTime);
break;
case MotionEvent.ACTION_UP:
...
if (isStart) {
mHandler.removeCallbacks(mRunnable);//有问题
isStart = false;
}
break;
}
postInvalidate();
return true;
}

至此摇杆相关功能介绍完毕,RockerView完整代码如下:


public class RockerView extends View {
private final int VELOCITY = 40;//飞机速度

private Paint smallCirclePaint;//小圆画笔
private Paint bigCirclePaint;//大圆画笔
private Paint sideCirclePaint;//大圆边框画笔
private Paint arcPaint;//圆弧画布
private int smallCenterX = -1, smallCenterY = -1;//绘制小圆圆心 x,y坐标
private int bigCenterX = -1,bigCenterY = -1;//绘制大圆圆心 x,y坐标
private int touchX = -1, touchY = -1;//触摸点 x,y坐标
private float bigRadiusProportion = 69F / 110F;//大圆半径占view一半宽度的比例 用于获取大圆半径
private float smallRadiusProportion = 4F / 11F;//小圆半径占view一半宽度的比例
private float bigRadius = -1;//大圆半径
private float smallRadius = -1;//小圆半径
private double distance = -1; //手指按压点与大圆圆心的距离
private double radian = -1;//弧度
private float angle = -1;//度数 -180~180
private int viewHeight,viewWidth;
private int defaultViewHeight, defaultViewWidth;
private RectF arcRect = new RectF();//绘制蓝色圆弧用到矩形
private int drawArcAngle = 90;//圆弧绘制度数
private int arcOffsetAngle = -45;//圆弧偏移度数
private int drawTime = 16;//告诉flyView重绘的时间间隔 这里是16ms一次
private boolean isBigCircleOut = false;//触摸点在大圆外

private boolean isStart = false;
private Handler mHandler = new Handler();
private Runnable mRunnable = new Runnable() {
@Override
public void run() {
if (isStart){
getFlyOffset();
mHandler.postDelayed(this,drawTime);
}
}
};

public RockerView(Context context) {
super(context);
init(context);
}

public RockerView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}

public RockerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}


private void init(Context context) {
defaultViewWidth = DensityUtil.dp2px(context,220);
defaultViewHeight = DensityUtil.dp2px(context,220);

bigCirclePaint = new Paint();
bigCirclePaint.setStyle(Paint.Style.FILL);
bigCirclePaint.setStrokeWidth(5);
bigCirclePaint.setColor(Color.parseColor("#1AFFFFFF"));
bigCirclePaint.setAntiAlias(true);

smallCirclePaint = new Paint();
smallCirclePaint.setStyle(Paint.Style.FILL);
smallCirclePaint.setStrokeWidth(5);
smallCirclePaint.setColor(Color.parseColor("#4DFFFFFF"));
smallCirclePaint.setAntiAlias(true);

sideCirclePaint = new Paint();
sideCirclePaint.setStyle(Paint.Style.STROKE);
sideCirclePaint.setStrokeWidth(DensityUtil.dp2px(context, 1));
sideCirclePaint.setColor(Color.parseColor("#33FFFFFF"));
sideCirclePaint.setAntiAlias(true);

arcPaint = new Paint();
arcPaint.setColor(Color.parseColor("#FF5DA9FF"));
arcPaint.setStyle(Paint.Style.STROKE);
arcPaint.setStrokeWidth(5);
arcPaint.setAntiAlias(true);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取视图的宽高的测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width,height;
if (widthMode == MeasureSpec.EXACTLY){
width = widthSize;
}else {
width = defaultViewWidth;
}

if (heightMode == MeasureSpec.EXACTLY){
height = heightSize;
}else {
height = defaultViewHeight;
}
width = Math.min(width,height);
height = width;
//设置视图的宽度和高度
setMeasuredDimension(width,height);
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
bigCenterX = getWidth() / 2;
bigCenterY = getHeight() / 2;
smallCenterX = bigCenterX;
smallCenterY = bigCenterY;

bigRadius = bigRadiusProportion * Math.min(bigCenterX, bigCenterY);
smallRadius = smallRadiusProportion * Math.min(bigCenterX, bigCenterY);

arcRect.set(bigCenterX-bigRadius,bigCenterY-bigRadius,bigCenterX+bigRadius,bigCenterY+bigRadius);
viewHeight = getHeight();
viewWidth = getWidth();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(bigCenterX, bigCenterY, bigRadius, bigCirclePaint);
canvas.drawCircle(smallCenterX, smallCenterY, smallRadius, smallCirclePaint);
canvas.drawCircle(bigCenterX, bigCenterY, bigRadius, sideCirclePaint);

if (isBigCircleOut) {
canvas.drawArc(arcRect,angle+arcOffsetAngle,drawArcAngle,false,arcPaint);
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
touchX = (int) event.getX();
touchY = (int) event.getY();
initAngle();
getFlyOffset();
if (isStart) {
isStart = false;
mHandler.removeCallbacks(mRunnable);
}
isStart = true;
mHandler.postDelayed(mRunnable,drawTime);
break;
case MotionEvent.ACTION_UP:
smallCenterX = bigCenterX;
smallCenterY = bigCenterY;
isBigCircleOut = false;
if (isStart) {
mHandler.removeCallbacks(mRunnable);//有问题
isStart = false;
}
break;
}
postInvalidate();
return true;
}

/** 计算夹角度数,并实现小圆圆心最多至大圆边上 */
private void initAngle() {
radian = Math.atan2((touchY - bigCenterY), (touchX - bigCenterX));
angle = (float) (radian * (180 / Math.PI));//范围-180-180
isBigCircleOut = false;
if (bigCenterX != -1 && bigCenterY != -1) {//大圆中心xy已赋值
double rxr = (double) Math.pow(touchX - bigCenterX, 2) + Math.pow(touchY - bigCenterY, 2);
distance = Math.sqrt(rxr);//手点击点距离大圆圆心距离
smallCenterX = touchX;
smallCenterY = touchY;
if (distance > bigRadius) {//距离大于半圆半径时,固定小圆圆心在大圆边缘上
smallCenterX = (int) (bigRadius / distance * (touchX - bigCenterX)) + bigCenterX;
smallCenterY = (int) (bigRadius / distance * (touchY - bigCenterY)) + bigCenterX;
isBigCircleOut = true;
}
}
}

/** 获取飞行偏移量 */
private void getFlyOffset() {
float x = (smallCenterX - bigCenterX) * 1.0f / viewWidth * VELOCITY;
float y = (smallCenterY - bigCenterY) * 1.0f / viewHeight * VELOCITY;
onRockerListener.getDate(this, x, y);
}

/**
* pX,pY为手指按点坐标减view的坐标
*/
public interface OnRockerListener {
public void getDate(RockerView rocker, final float pX, final float pY);
}
private OnRockerListener onRockerListener;
public void getDate(final OnRockerListener onRockerListener) {
this.onRockerListener = onRockerListener;
}
}

飞机(FlyView)


飞机图片移动相对简单,实现原理是在自定义View里面,通过改变绘制图片方法(drawBitmap()方法)里的left,top值来模拟飞机移动。FlyView实现代码如下:


public class FlyView extends View {
private Paint mPaint;
private Bitmap mBitmap;
private int viewHeight, viewWidth;
private int imgHeight, imgWidth;
private int left, top;

public FlyView(Context context) {
super(context);
init(context);
}

public FlyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}

public FlyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}

void init(Context context) {
mPaint = new Paint();
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.mipmap.fly);
imgHeight = mBitmap.getHeight();
imgWidth = mBitmap.getWidth();
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewHeight = h;
viewWidth = w;
left = w / 2 - imgHeight / 2;
top = h / 2 - imgWidth / 2;
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(mBitmap, left, top, mPaint);
}

/** 移动图片 */
public void move(float x, float y) {
left += x;
top += y;
if (left < 0) {
left = 0;
}else if (left > viewWidth - imgWidth) {
left = viewWidth - imgWidth;
}

if (top < 0) {
top = 0;
} else if (top > viewHeight - imgHeight) {
top = viewHeight - imgHeight;
}
postInvalidate();
}
}

在Activity或者Fragment里面对View设置代码(kotlin)如下:


binding.viewRocker.getDate { _, pX, pY ->
binding.viewFly.move(pX, pY)
}

飞机图片如下:


fly.png


总结


摇杆整体实现没有太复杂的逻辑,比较容易混的地方,可能是屏幕坐标系和数学坐标系能不能转过弯来。印象中好像可以通过Matrix将坐标变换,但一时间想不起来怎么实现,后面了解下Matrix相关内容。

关于虚拟摇杆实现有很多方式,我写的这个不是最优的方式,虚拟摇杆有些需求没有接触到,在代码实现中可能比较简单,小伙伴们看到文章不足的地方,可以留言告诉我,一起学习交流下。


项目地址: GitHub


作者:卤肉拌面
链接:https://juejin.cn/post/7225079705992822842
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

我受精了,Git log竟然还有这种用法!

Git
前言 当你使用Git进行版本控制时,经常需要查看提交历史记录。Git提供了git log命令来查看版本历史记录。 在本文中,我们将介绍如何使用git log命令来查看Git提交历史记录。 查看提交历史记录 通过在命令行中输入以下命令来查看提交历史记录: git...
继续阅读 »

前言


当你使用Git进行版本控制时,经常需要查看提交历史记录。Git提供了git log命令来查看版本历史记录。


在本文中,我们将介绍如何使用git log命令来查看Git提交历史记录。


查看提交历史记录


通过在命令行中输入以下命令来查看提交历史记录:


git log

这将显示最新的提交历史记录,包括提交ID、作者、提交日期、提交消息等。例如:


commit 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 22:21:48 2023 -0500

Added new feature

commit b3f3f066e75a7d3352898c9eddf23baa7265f5b5
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 17:32:16 2023 -0500

Fixed bug

commit 0498b3d96b2732e36e7d41501274c327a38188aa
Author: Zhangsan <Zhangsan@example.com>
Date: Fri Apr 23 14:01:11 2023 -0500

Updated documentation

显示分支图


如果你想查看分支的提交历史记录,你可以使用git log --graph命令来显示一个分支图。分支图可以帮助你更好地理解分支之间的关系和合并历史。


例如:


git log --graph

这将显示一个分支图,其中每个提交都显示为一个节点,不同的分支用不同的线表示。你可以在分支图中看到合并提交和分支之间的关系。


例如:


* commit da32d1d7e7f22ec59330e6b8c51def819b951aec
| Author: Zhangsan <Zhangsan@example.com>
| Date: Wed Apr 12 15:28:40 2023 +0800
|
| feat:xxx
|
* commit 8fdc0a9838d45d9e027740e7a448822bb8431969
|\ Merge: e22ce87ae d80ce707b
| | Author: Zhangsan <Zhangsan@example.com>
| | Date: Wed Apr 12 13:08:17 2023 +0800
| |
| | Merge branch 'xxx' into xxx
| |
| * commit d80ce707b72e1231c18a4843e62175a7a430e3c3
| | Author: Zhangsan <Zhangsan@example.com>
| | Date: Tue Apr 11 19:36:48 2023 +0800
| |
| | xxxx
| |

格式化输出


git log命令还支持格式化输出,你可以使用--pretty选项来指定输出的格式。例如,以下命令将以一种类似于JSON的格式输出提交记录:


git log --pretty=format:'{%n "commit": "%H",%n "author": "%an <%ae>",%n "date": "%ad",%n "message": "%f"%n},' --no-merges

这将输出每个提交的哈希值、作者、提交日期和提交消息。


例如:


{
"commit": "a8c4b34ab5e4d844dc741e105913266502d82dcd",
"author": "Zhangsan <Zhangsan@example.com>",
"date": "Sun Apr 16 16:32:20 2023 +0800",
"message": "feat-resize"
},
{
"commit": "f23b4e61633033b9db5a3c87afc5f523cf5e583e",
"author": "Zhangsan <Zhangsan@example.com>",
"date": "Sat Apr 15 15:32:25 2023 +0800",
"message": "feat"
}

你也可以使用一些预定义的格式来输出,例如--pretty=oneline将每个提交压缩成一行,只包含哈希值和提交消息。


例如:


a3fe1d136ab9587db19d9f8073fd491ead892f4a feat:xxxx
84738075dd00f1e0712f139c23c276b7559fd0d9 feat:xxxx
a8c4b34ab5e4d844dc741e105913266502d82dcd feat:xxxx
f23b4e61633033b9db5a3c87afc5f523cf5e583e feat:xxxx

查看详细信息


默认情况下,git log命令只显示每个提交的基本信息。但是,你可以通过添加--stat选项来显示每个提交所做的更改数量和文件列表。


例如:


git log --stat

这将显示每个提交所做的更改数量和文件列表


例如:


commit 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 22:21:48 2023 -0500

Added new feature

somefile.txt | 1 +
1 file changed, 1 insertion(+)

commit b3f3f066e75a7d3352898c9eddf23baa7265f5b5
Author: Zhangsan <Zhangsan@example.com>
Date: Sat Apr 24 17:32:16 2023 -0500

Fixed bug

somefile.txt | 1 -
1 file changed, 1 deletion(-)

commit 049

查看某个提交的详细信息


除了git log命令,我们还可以使用git show


如果你想查看某个提交的详细信息,可以使用git show <commit>命令。


例如:


git show 6d883ef

这将显示提交6d883ef的详细信息,包括提交消息、作者、提交日期和更改的文件。


查看某分支记录


有时候你可能只想查看某个特定分支历史记录。这可以使用git log <branch>命令。


例如,如果你只想查看main分支的历史记录,你可以输入以下命令:


git log main

显示指定文件的提交历史记录


如果你只想查看某个文件的提交历史记录,你可以使用git log <filename>命令。


例如:


git log somefile.txt

这将显示与该文件相关的所有提交历史记录。


显示指定作者的提交历史记录


如果你只想查看某个作者的提交历史记录,你可以使用git log --author=<author>命令。例如:


例如:


git log --author="Zhangsan"

这将显示所有由Zhangsan提交的历史记录。


显示指定时间段的提交记录


如果你指向查看某个时间范围内的提交历史记录、你可以使用git log --after,git log --before命令。


例如:


git log --after="2023-04-01" --before="2023-04-02"

这将显示出,2023-04-01 到 2023-04-02之间的提交记录



  • --after 会筛选出指定时间之后的提交记录

  • --before 会筛选出指定时间之前的提交记录。


还有一些快捷命令:


git log --after="yesterday" //显示昨天的记录
git log --after="today" //显示今天的
git log --before="10 day ago" // 显示10天前的提交记录
git log --after="1 week ago" //显示最近一周的提交录
git log --after="1 month ago" //显示最近一个月的提交率

限制输出的历史记录数量


例如,要查看最近的5个提交,你可以使用以下命令:


git log -5

搜索历史记录


git log命令还可以用于搜索历史记录。例如,如果你只想查看提交消息中包含关键字“bug”或“fix”的历史记录,可以使用以下命令:


git log --grep=bug --grep=fix

这将显示所有提交消息中包含关键字“bug”或“fix”的提交记录。


commit 27ad72addeba005d7194132789a22820d994b0a9
Author: Zhangsan <Zhangsan@example.com>
Date: Thu Apr 13 11:17:13 2023 +0800

fix:还原local环境配置

commit 8369c45344640b3b7215de957446d7ee13a48019
Author: Zhangsan <Zhangsan@example.com>
Date: Mon Apr 10 11:02:47 2023 +0800

fix:获取文件

显示带有内容变更的提交日志


如果你想查看带有内容变更的提交日志,可以使用git log -p,能清楚的看到每次提交的内容变化。
非常有用的一个命令;


例如:


git log -p

这将显示每个提交与它的父提交之间的差异。


diff --git a/xxxx.tsx b/xxxx.tsx
index 7f796c934..87b365426 100644
--- a/xxx.tsx
+++ b/xxx.tsx

我们也可以知道某个具体的提交的差异,可以使用git log -p <commit>命令


显示提交的差异


如果你想查看某个提交与上一个提交之间的差异,可以使用git log -p <commit>命令。例如:


git log -p 6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429

这将显示提交6d883ef6d4d6fa4c2ee59f6ca8121d1a925dc429与它的父提交之间的差异。


显示当前分支的合并情况


如果你想查看当前分支的合并情况,可以使用git log --merges命令。例如:


git log --merges

commit 2f3f4c45a7be3509fff6496c9de6d13ef0964c9d
Merge: 8369c4534 4103a08bf
Author: xxx <xxx@xx.com>
Date: Mon Apr 10 11:03:55 2023 +0800

Merge branch 'dev/feature1' into dev/dev

commit 14b40421ef54c875b8f8f0cfc297bcdc3960b9be
Merge: 30e36edbb 48bb05ede
Author: xxx <xxx@xx.com>
Date: Mon Apr 10 00:34:09 2023 +0800

Merge branch 'dev/feature1' into dev/dev

总结


以上是更多关于git log命令的使用教程、示例,希望对你有所帮助。


作者:前端后腿哥
链接:https://juejin.cn/post/7225902885544165436
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

普通专科生的十年

前言 Hello,大家好,我是 Sunday。 其实我想写这个博客已经很久了,总是提起笔然后又放下,总觉得自己的经历普普通通,实在是没有什么值得分享的地方。 我从2013年 毕业,转眼间,时间来到了 2023 年的 4 月份。“前端已死,java 已亡”、“专...
继续阅读 »

前言


Hello,大家好,我是 Sunday


其实我想写这个博客已经很久了,总是提起笔然后又放下,总觉得自己的经历普普通通,实在是没有什么值得分享的地方。


我从2013年 毕业,转眼间,时间来到了 2023 年的 4 月份“前端已死,java 已亡”、“专科生是找不到工作的” 类似的言论开始充斥着各个平台。仿佛互联网已经到了耄耋之年,尽显疲态。


未来如何,没有人会知道。我们只谈过去,暂且不谈未来。


我也算是经历过 “互联网红利期” 的人,回想当年拿着一份简历,一天就可以收到 10 多个面试电话。对比现在刚毕业的同学而言,真的是幸福太多了。


因为工作的原因,我会接触到很多的前端初学者(以大专为主),能够感受到他们的焦虑,也能够感受到他们的迷茫。


我能够做到感同身受,可能因为,我也有过类似的经历吧~~


我的十年


2010 年,高考失利的我进入了济南一家普通的专科院校。


image-20230417202445036.png


在这里,我的脑海中没有 “努力、专注、奋斗、反思” 等词汇,只有 “游戏、恋爱” 这两个关键字。


混了三年,2013 年我从学院毕业。


可能是因为我运气比较好,也可能因为那个时候专科还有点价值。我很幸运的进入到了一家国企做检测员,薪资是 1200 块钱。


80.jpeg


如果一切没有变化,那么我可能会在这里一直干下去,过着混吃等死,没心没肺的生活。


让我人生轨迹发生改变的是:一次和我对面的一位老大哥的聊天。



这位老大哥当时是 三十五六岁,每个月到手是 3000 块钱出头,有两个孩子。因为父母身体不好,所以他老婆只能全职在家。


他们一家四口人,每个月只有这三千多块钱的收入,和别人合租了一个卧室,一家四口就挤在那个小小的卧室里面。


我当时就突然间有了一个想法,十年之后,我也会是这个样子吗?



因为当时身边有很多朋友在做这一行,所以我就从一个 hello world 都不知道怎么写的白中白开始,自学软件开发。


我开始尝试在工作之余自学 java,并且特别好运的在三个月之后拿到了一个 offer,薪资是 1600 块钱。从此开始了我的软件开发之路。对了,这个时候是 2014 年。


后来我又陆陆续续自学了 Android开发、IOS 开发、以及现在的前端开发


3221681782093_.pic.jpg


这让我养成了看书的习惯,所以才有了现在 小破站 的《一个小时阅读一本书》系列。


image-20230417195637739.png


随着不断的学习和工作,我开始可以在各个平台产出一些东西。


比如:在 GitChat 的上的付费专栏


image.png


比如:在慕课网上的技术视频


image-20230417201023839.png


长期的自学,让我深知自学之艰难,持续的产出,也让我喜欢上了教学的工作。


所以 2019年 10 月 我入职了黑马程序员,成为了一名前端老师,并且一直工作到了现在。


image-20230418091825852.png


2013 年 开始,到现在的 2023 年,正好十年的时间。


十年。


我从之前的一个 160斤 的山东小伙


DSC_08760.jpg


变成了现在 210斤 的山东大汉。


3211681781608_.pic.jpg


收入,也从原先的 1200 块钱,到现在可以保证一个家庭体面的生活。


十年,我没有变成那位老大哥的样子,而是可以做着自己喜欢的工作,同时能够帮助着更多希望学习技术的人。


仔细想想,好像也不错~~~~~~~


结语


这篇博客我在半个月之前就已经写好了,只是一直没有发出来。


今天也不是什么特殊的日子,想着发就发了吧。


以此来致我的十年~~


作者:LGD_Sunday
链接:https://juejin.cn/post/7223642909793878053
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

无聊的分享:点击EditText以外区域隐藏软键盘

1.前言 当我们在使用一个应用的搜索功能或者任何带有输入功能的控件时,如果想取消输入往往会点击外部空间,这个时候系统的软键盘就会自动收起,并且输入框也会清楚焦点,这样看上去很自然,其实使用EditText控件是没有这种效果的,本文章就如何实现上述效果提供一点小...
继续阅读 »

1.前言


当我们在使用一个应用的搜索功能或者任何带有输入功能的控件时,如果想取消输入往往会点击外部空间,这个时候系统的软键盘就会自动收起,并且输入框也会清楚焦点,这样看上去很自然,其实使用EditText控件是没有这种效果的,本文章就如何实现上述效果提供一点小小的思路。


2.如何实现


当我们在Activity单纯的添加一个EditText时,点击吊起软键盘,这个时候再点击EditText外部区域会是这个样子的:



会发现,无论我们怎么点击外部区域软键盘都不会收起。所以要达到点击外部区域收起键盘效果需要我们自己添加方法去隐藏键盘:


重写dispatchTouchEvent


override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
   ev?.let {
       if (it.action == MotionEvent.ACTION_DOWN) {
           //如果现在取得焦点的View为EditText则进入判断
           currentFocus?.let { view ->
               if (view is EditText) {
                   if (!isInSide(view, ev) && isSoftInPutDisplayed()) {
                       hideSoftInPut(view)
                  }
              }
          }
      }
  }
   return super.dispatchTouchEvent(ev)
}

在Activity 中重写dispatchTouchEvent,对ACTION_DOWN事件做处理,使用getCurrentFocus()方法拿到当前获取焦点的View,判断其是否为EditText,若为EditText,则看当前软键盘是否展示(isSoftInPutDisplayed)并且点击坐标是否在EditText的外部区域(isInSide),满足条件则隐藏软键盘(hideSoftInPut)。


判断点击坐标是否在EditText内部


//判断点击坐标是否在EditText内部
private fun isInSide(currentFocus: View, ev: MotionEvent): Boolean {
   val location = intArrayOf(0, 0)
//获取当前EditText坐标
   currentFocus.getLocationInWindow(location)
//上下左右
   val left = location[0]
   val top = location[1]
   val right = left + currentFocus.width
   val bottom = top + currentFocus.height
//点击坐标是否在其内部
   return (ev.x >= left && ev.x <= right && ev.y > top && ev.y < bottom)
}

定义一个数组location存储当前EditText坐标,计算出其边界,再用点击坐标(ev.x,ev.y)和边界做比较最终得出点击坐标是否在其内部。


来判断软键盘是否展示


private fun isSoftInPutDisplayed(): Boolean {
   return ViewCompat.getRootWindowInsets(window.decorView)
       ?.isVisible(WindowInsetsCompat.Type.ime()) ?: false
}

使用
WindowInsetsCompat类来判断当前状态下软键盘是否展示,WindowInsetsCompat是AndroidX库中的一个类,用于处理窗口插入(WindowInsets)的辅助类,可用于帮助开发者处理设备的系统UI变化,如状态栏、导航栏、软键盘等,给ViewCompat.getRootWindowInsets传入decorView拿到其实例,利用isVisible方法判断软键盘(WindowInsetsCompat.Type.ime())是否显示。


隐藏软键盘


private fun hideSoftInPut(currentFocus: View) {
   currentFocus.let {
    //清除焦点
       it.clearFocus()
    //关闭软键盘
       val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
       imm.hideSoftInputFromWindow(it.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
  }
}

首先要清除当前EditText的焦点,防止出现键盘收起但是焦点还在的情况:



最后是获取系统Service隐藏当前的键盘。


来看看最终的效果吧:



3.结尾


以上就是关于点击EditText外部区域隐藏软键盘并且清除焦点的实现方法,当然这只是其中的一种方式,如有不足请在评论区或私信指出,如果你们有更多的实现方法也欢迎在论区或私信留言捏❤️❤️


作者:Otaku_尻男
链接:https://juejin.cn/post/7226248402798936119
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

这一年半的时间我都做了些什么

介绍 也很久没有在这儿更新文章了,主要自己也不知道写什么。这篇文章不讲技术,主要讲一下我在这最近一年多时间的生活、工作上的一些经历和感受。 2021 在 2021 年 9 月,我通过两个星期的准备和面试,拿到几家公司的 offer。通过一些自己的硬性要求...
继续阅读 »

介绍



  • 也很久没有在这儿更新文章了,主要自己也不知道写什么。这篇文章不讲技术,主要讲一下我在这最近一年多时间的生活、工作上的一些经历和感受。


2021



  • 20219 月,我通过两个星期的准备和面试,拿到几家公司的 offer。通过一些自己的硬性要求(上下班时间、距离、薪资待遇...),最后选择了一家做低代码平台的公司,就在我和 HR 商谈到双方都满意准备定入职时间时,我收到了另一家公司的面试。本来这边我基本上已经确定了,可以选择推脱掉,但是那边说面试流程很快,让我考虑一下。就这样,我在回复后的当天晚上收到了初试并面试通过,随后不到 10 分钟的时间里接到了二面也通过了,就这样不到一个小时,我直接到了 HR 的终面,通过并拿到了 offer

  • 通过 HR 和面试官,我了解到了这家公司是做中老年教育的,有 APP、小程序、H5 ,用的技术也是比较前沿的新技术,通过自己的考虑,对比自己缺失的技术方向,我选择了后者。

  • 简单说一下谈的一些内容:

    • 上班时间为早上弹性 10:00

    • 每年 2 次调薪;

    • 年终奖 2 个月,也就是 14 薪,我看的招聘是 16 薪,但 HR 说的之前大小周的是,我去的时候刚好取消了大小周;

    • 社保全额 12%、公积金全额 5%

    • 节假日有节日福利;

    • 晚上 6 点有晚餐;

    • 加班到 10 点可以打车;

    • 每月最后一天发当月工资。



  • 看到这里,我相信很多人都觉得这是一个不错的工作,当时我也是这么想的,所以我怀着学习深造、技术沉淀、薪资待遇啥的都还不错的想法就去了。

  • 在去的第一天,我遇到了我大学的同班同学,可能是我去太早了,还没什么人,巧的是刚好是他给我开的门;这天除了办理入职,剩下的时间就是在熟悉代码,还是和其他公司入职没有什么差异;第二天拿到电脑,我就开始了在这的搬砖之路;前几天每天我都还都是比较正常的上下班,直到我接了第一个需求,每个需求都有排期,你需要自己在需求评讲的时候去思考涉及的改动、可能遇到的问题、是否对其他地方有影响以及需要的开发时间。由于对项目不熟悉、需求的变动等原因,导致好几天的加班,有时候到 21:00 点,有时候 22:00 甚至更晚。当我以为熟悉后下一个需求就好了,然而理想总是丰满的,这个状态并没有什么改变,反而越来越离谱。
    image.png

  • 需求一个接一个的来,到我手上的基本上都是大需求,前一个需求还没有提测或者刚提测就要进入下一个需求,边开发新需求边修改测试提的 bug。上线发布的时候加班是最多的,最开始那一两个月,加班太晚(00:00 及以后)还有夜宵,还有半天到一天的调休,招我进来的 HR 走了之后就什么都没有了。

  • 在快放春节的前几天,运营还临时加一个春节活动的需求,春节这种节日要做什么不知道提前规划,到要放假的前几天才通知。没出意外,我和另一个测试是公司最后走的。

  • 这年的中秋节是什么都没有,春节有一副对联、福字还有一支像钢笔的中性笔,到处都是公司的大而显眼的 logo


2022



  • 很多人可能很奇怪,不是 14 薪吗?上文怎么没有提到呢?是没有还是没发呢?

  • 为什么 2021 的年终奖的事情放在 2022 里说,年终奖是发了的,发的时间却是 4 月底,而在发年终奖前,还做了一个考核表让自己填去年在各个方面的表现,然后直系领导再打分。本以为只是简单走个过场,没想到事实却并非如此,年终奖有分级 A、B、C、D 分别占 5%、10%、75%、10%,对应系数 1.5、1.2、1、0D 是强制比例且必须被淘汰。如果都是强制比例那也还好,但却只有 D 是,A、B 的据我小道消息了解,普通员工中就只有一个 B,至于领导层是否占了名额就不得而知了。在整个过程中,没有一个考核标准,在 2021 年,基本上所有研发都在努力付出,都加班不少,结果还有强制比例的 D,前后端各有一个。前端的 D 还给到了我们意想不到的人头上,一个将三端使用的项目 0-1 搭建起来的人,后端则是忽悠说你是 21 年最后一天来的,得 D也无所谓(最开始没有说 D 强制被淘汰,名额确定后才约谈的,这我也是后来才知道的)。
    image.png

  • 从年终奖下来以后,前端走了 4 个人,其他的人也是居安思危,既然这次能这么做,那么也有可能有下次。

  • 说一下我为什么没有走:

    • 我才来不到半年时间;

    • 我当时和女朋友准备结婚;

    • 当时的行情也不是很好。



  • 综上,我继续在这里待着,但我也不再像从前那样自己老是加班,每天非的把某个东西做完或者做到某种程度再走。需求也不再没做完一个,领导让接下一个就屁颠屁颠去接(挣表现),领导之前开早会的时候说:xx(我)手上的事情够多了,下一个就换个人来。到过需求的时候,领导:这个还是让xx(我)来吧。我不知道我是该开心还是怎么样,让我来是因为认可我的能力、我对这块比较熟,还是其他什么原因呢?后来我知道了,需求是做不完的,我们当时就 4 个前端,产品却有 5-6个,永远有需求在等待评审开发。

  • 后面,我开始了每天 10 点,晚上 8 点前上下班,需求按时完成,但基本上不会再加班,除非遇到什么棘手的问题,一直没解决。

  • 说的年终奖后要普调薪资,最终结果是调了公积金的比例由 5% 调到了 12%,因为刚好买了房子,想着也还能接受。

  • 然后后面开始搞 OKR ,结果搞了 2 次,不知道什么原因没有继续搞了。

  • 5.20 号我领结婚证了,领证后我有一段时间不想让班,抵触的情绪异常高涨,就是去公司就觉得人没有精神、呆呆的。于是我就请假准备休息一下,我花光了我所有假,包括婚假和年假,我觉得还不够还请了 2 天事假,连续请了 2 周。由于假期太长,领导让老板审批,于是我就给老板发了飞书消息,老板也同意了,我给HR看了一下,让后我就开始了休假,结果后面我发现我的请假是在我已经休假后的 2-3 天才通过的。我当时走的时候就只有 HR 那儿没有通过,我还单独给他说了。

  • 8 月的婚礼由于疫情,推出到了 9 月底,最后也是圆满的完成了。

  • 这年的节假日只有中秋节有一个水杯、背包、飞盘,四个糕点饼,其他节日就什么都没有了,年会也没有。离谱的是,年前一个月左右有次聚餐,后来被称之为年会。团建看什么时候想起来,又一次去运动馆,自己吃中午饭后出发,完了没有晚饭自己回家。
    image.png

  • 日期老婆构图时写错啦,哈哈哈哈!!!


2023



  • 年初开始打卡了,没有邮件通知上下班打卡时间,但是显示的是早 108,领导说的是晚9,如果部门时长过低,会追溯到个人。然后我开始了早 107,可能很多人会说,你这不是明目张胆对着干吗?其实我自己也想了很多,我觉得我不应该只有工作,我应该还有生活。还有就是我老婆怀孕了,所以我需要更多的时间,而不是说只有工作,更不说是自己工作得不是很舒服的工作。

  • 公司也实施了一些控制成本的方案,降低晚餐标准 25 -> 18,降低打车额度 150 -> 100,...

  • 慢慢的,大家不约而同的都在等 4 月,想看今年又会有怎么样的年终考核方式,不出所料,还是去年那一套,自评加领导评。没有任何的依据,还说:你完成工作只是及格,想要高分,就得做出重大贡献啥的。

  • 我去年从做产品需求迭代,转到了内部系统基建,完成了桌面端直播开播的从 0-1 的第一版,开发内部新的应用 4 个,维护多个老的内部应用的前后端项目,这样的工作是否合格呢?答案是否定的。

  • 4 月底,结果出来了,我不出意外在被裁名单里,有人肯定会疑惑为什么没有说比例和系数?因为没有意义,这次明说比例 10%,结果裁了快一半,有的整个部门从前后端测试到产品运营都裁了。还增加了一个部门系数,听没被裁的说的是整个都是 0.5,意思就是 14 薪的 1 个月,16 薪的 2 个月。

  • 我自己都还好,有两个来了不到一个月的实习生也没能幸免,刚出来就经历这样的事情换谁也不好受。只能说往好的方面想吧!


总结


工作



  • 在这里确实学到了不少东西,就技术而言,对跨端、nodejs 后台、数据库、桌面端等一些问题的解决方案,有自己想的,也有学习别人的。

  • 职场相处,都说程序员的职场比较简单,确实相对其他的行业,这儿并没有那么多,但偶尔还是会遇到一些让你无法接受、理解、相处的人,但大部分人都是很好相处的,至少我是这么认为的,在这儿我也遇到一些很好的同事,也是朋友。

  • 当你要去一个公司的时候,不要光看表面的以及面试官和 HR 给你说的,你可以去一些贴吧、论坛、脉脉等上面去看这家公司的在职人员或离职人员的评论或看法。我相信不会有人无缘无辜去只说一家公司不好的方面,一家公司肯定有好有坏,每个人看重的点不一样,所以需要自己去权衡。

  • 像年终奖这种,如果合同没有固定写几个月,公司有很多的方法去卡。如果有一个考核标准,我觉得大家都能接受,你做了什么、成绩怎么样、出了哪些问题等等,总的有个衡量标准去量化它。


生活


  • 完成了自己的一件人生大事(结婚);

  • 有了属于自己房子(虽然还没交房);

  • 老婆在今年怀孕了(健康的兔宝宝)。


最后



  • 希望大家接好运,工作顺心、生活开心。

  • 有什么问题欢迎大家评论区留言交流。


作者:小小小十七
来源:juejin.cn/post/7229667871606374457
收起阅读 »

一个28岁程序员入行自述和感受

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。 自我叙述 我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,...
继续阅读 »

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。


自我叙述


我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,我不可能如此普通,自己的一生不应该泯然众生,平凡平庸的度过。尤其是干我们it这一行业的,都有一个自己的程序员梦,梦想着,真的能够用 “代码改变世界”


入行回顾



你们还记得自己是什么时候,入行it行业的吗



我今年已经28岁了,想起来入行,还挺久远的,应该是2016入行的,我也算是半路出家的,中间有过武术梦 歌唱梦 但是电脑什么上学那会就喜欢玩,当然是指游戏,




武术梦




来讲讲我得第一个·梦,武术梦,可能是从小受到武打演员动作电视剧的影响,尤其那个时候,成龙大哥的电影,一直再放,我觉得学武术是很酷的一件事情,尤其那会上小学,还是初中我的体育还是非常好的,


然后我们家那个时候电视还是黑白的,电视机。哈哈哈😀电视台就那么几个,放来放去,有一个台一直重复放成龙电影,还有广告, 都是 学武术就到 xxxx学校, 我被洗脑了吧


于是真的让我爸,打电话质询了一下,可是好像他们这种武术学校都是托管式的,封闭式学习,听说很苦,,,,当然这不是重点,重点每年学费非常的贵,en~,于是乎我的这个梦想终止了,。。




歌唱梦




为啥会有唱歌想法,你猜对了,是被那个时候的好声音给影响了,那个时候好声音是真的很火,看的时候我一度以为我也可以上好声音,去当歌手然后出道,当明星,什么的。


不过不经历打击,怎么会知道自己的下线在哪里呢


我小学换了两到三个学校,到初中,再到高中,你们还记得自己读高中那会吗,高中是有专业选择的,入学军训完以后。


我们代班主任,和我们说有三个专业方向可以选择,艺术类,分美术,和唱歌,然后是文化类,然后艺术类就业考大学分数会低很多,然后一系列原因,哈哈哈,我就选择了歌唱班。


我最好伙伴他选择了,美术类就是素描。这里我挺后悔没有选择 美术类。


到了歌唱班,第一课就是到专业课有钢琴的教室,老是要测试每个同学的,音色和音高,音域
然后各自上台表演自己的拿手的一首歌,。我当时测试时候就是跟着老师的弹的钢琴键瞎唱,


表演的歌曲是张雨生《大海》 也就唱了高潮那么几句。。 😀现在想起来还很羞耻,那是我第一次在那么多人面前唱歌,


后面开始上课老师说我当时分班时候音色什么还不错,但学到后面,我是音准不太行,我发现。再加上我自己的从小感觉好像有点自卑敏感人格,到现在把,我唱歌,就越来越差,


当然我们也有乐理。和钢琴课,我就想主助攻乐理和钢琴,


但是我很天真


乐理很难学习,都是文科知识需要背诵,但是他也要有视唱,也就是唱谱子,duo,re,mi,fa,suo,la,xi,duo。。等,我发现我也学不进去


后面我又开始去学钢琴,但是钢琴好像需要一定童子功,不然可能很难学出来,于是我每天早上6点钟起来,晚上吃完饭就去钢琴教师抢占位置, 还得把门堵着怕人笑话,打扰我,


结果你们也猜到了,音乐方面天赋很重要,然后就是性格上面表演上面,要放得开,可是我第一年勉强撑过去了,后面第二年,专业课越来越多了,我感觉我越来越自卑~,然后成绩就越来越差,老师也就没太重视,嗯~好不容撑到了第二年下半年,放暑假,


但是老师布置任务暑假要自己去外面练钢琴,来了之后要考试,我还花钱去外面上了声乐课钢琴课,哎,我感觉就是浪费钱,,,,,因为没什么效果,性格缺陷加上天赋不行,基本没效果,那段时间我也很痛苦的,因为越来越感觉根本容入不进去班级体,尤其是后面高二,了专业课很多大部分是前面老师带着发生开嗓,后面自由练习,我也不好意思,不想练习,所以
到后面,高二下学习我就转学了,,,,


当然我们班转学的,不止我一个,还有一个转学的 和我一个寝室的,他是因为音高上不去,转到了文科班, 还有一个是挺有天赋,我挺羡慕的,但是人家挺喜欢学习,不喜欢唱歌什么,就申请转到了,文科班。 不过她转到文科班,没多久也不太好,后面好像退学了,,我一直想打听他的消息,都在也没打听到了




玩电脑




我对电脑的组装非常感兴趣,喜欢研究电脑系统怎么装,笔记本拆装,台式机拆装,我会拿我自己的的笔记本来做实验,自己给自己配台式机,自己给自己笔记本增加配置,哈哈哈哈。对这些都爱不释手。



这还是我很早时候,自己一点一点比价,然后去那种太平洋电脑城,电脑一条街,那种地去找人配置的。想想那时候配置这个电脑还挺激动,这是人生的第一台自己全部从零开始组装配的电脑,


本来打算,后面去电脑城上班,开一个笔记本维修,电脑装配的门面的,(因为自己研究了很多笔记本系统,电脑组装),可是好像听电脑城的人说,电脑组装什么的已经不赚钱了,没什么价格利润,都是透明的而且更新迭代非常的快,电脑城这种店铺也越来越少了,都不干了,没有新人再去干这个了,于是乎我的第一份工作失业 半道崩殂了,哈哈哈哈还没有开始就结束了。




学it




后面我又报名自学了,it编程,《xxx鸟》 但是学it我学起来,好像挺快的,挺有感觉的,入学前一个星期,要等班人数到齐才能开班,我们先来的就自己学习打字了,我每天都和寝室人,一起去打字,我感觉那段时间我过得挺开心和充实的,


后面我们觉得自带寝室不好,环境差,于是就几个人一起,搬出去住了,一起学习时候有一个年级26了,我和他关系还蛮好的,不过现在也没什么联系了,,,


学习时候,每次做项目时候我都是组长,那个时候原来是有成就感的,嗯,学习it好像改变了,我学唱歌那个时候,一些自卑性格,可能是遇到了一个好的老师吧


当然后面就顺利毕业,然后找到了工作了,,,


直到现在我还在it行业里


嗯~还想往下面写一点什么,,,下一篇分享一下我入门感受和经历吧


关注公众号,程序员三时 希望给你带来一点启发和帮助


作者:程序员三时
来源:juejin.cn/post/7230351646798643255
收起阅读 »

被问了无数次的函数防抖与函数节流,这次你应该学会了吧

web
前言 本篇文章内容,或许早已是烂大街的解读文章。不过参加几场面试下来发现,不少伙伴们还是似懂非懂地栽倒在(~面试官~)深意的笑容之下,权当温故知新吧。 文章从防抖、节流的原理说起再结合实际开发的场景,分别逐步实现完整的防抖和节流函数。 函数防抖 原理:当持续...
继续阅读 »

前言


本篇文章内容,或许早已是烂大街的解读文章。不过参加几场面试下来发现,不少伙伴们还是似懂非懂地栽倒在(~面试官~)深意的笑容之下,权当温故知新吧。


文章从防抖、节流的原理说起再结合实际开发的场景,分别逐步实现完整的防抖和节流函数。


函数防抖



  • 原理:当持续触发一个事件时,在n秒内,事件没有再次触发,此时才会执行回调;如果n秒内,又触发了事件,就重新计时

  • 适用场景:



    • search远程搜索框:防止用户不断输入过程中,不断请求资源,n秒内只发送1次,用防抖来节约资源

    • 按钮提交场景,比如点赞,表单提交等,防止多次提交

    • 监听resize触发时, 不断的调整浏览器窗口大小会不断触发resize,使用防抖可以让其只执行一次



  • 辅助理解:在你坐电梯时,当一直有人进电梯(连续触发),电梯门不会关闭,在一定时间间隔内没有人进入(停止连续触发)才会关闭。


下面我们先实现一个简单的防抖函数,请看栗子:


// 简单防抖函数
const debounce = (fn, delay) => {
  let timer;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    timer = setTimeout(function () {
      fn.call(context, ...args);
      //等同于上一句 fn.apply(context, args)
    }, delay);
  };
};

// 请求接口方法
const ajax = (e) => {
  console.log(`send ajax ${new Date()} ${e.target.value}`);
};

// 绑定监听事件
const noneDebounce = document.getElementsByClassName("none_debounce")[0];
const debounceInput = document.getElementsByClassName("debounce")[0];

noneDebounce.addEventListener("keyup", ajax);
debounceInput.addEventListener("keyup"debounce(ajax, 500));

运行效果如下:图片


点击这里,试试效果点击demo


可以很清晰的看到,当我们频繁输入时, 不使用节流就会不断的发送数据请求,但是使用节流后,只有当你在指定间隔时间内没有输入,才会执行发送数据请求的函数。


上面有个注意点:



  • this指向问题,在定时器中如果使用箭头函数()=>{fn.call(this, ...args)} 与上面代码效果一样, 原因时箭头函数的this是**「继承父执行上下文里面的this」**


关于防抖函数的疑问:



  1. 为什么要使用 fn.apply(context, args), 而不是直接调用 fn(args)


如果我们不使用防抖函数debounce时, 在ajax函数中打印this的值为dom节点:


<input class="debounce" type="text">

在使用debounce函数后,如果我们不使用fn.apply(context, args)修改this的指向, this就会指向window(ES6下为undefined)



  1. 为什么要传入arguments参数


我们同样与未使用防抖函数的场景进行对比


const ajax = (e) =>{
    console.log(e)
}


  1. 怎么给ajax函数传参


有的小伙伴就说了, 你的ajax只能接受绑定事件的参数,不是我想要的,我还要传入其他参数,so easy!


const sendAjax = debounce(ajax, 1000)
sendAjax(参数1, 参数2,...)

因为sendAjax 其实就是debounce中return的函数, 所以你传入什么参数,最后都给了fn


在未使用时,调用ajax函数对打印一个KeyboardEvent对象


image.png


使用debounce函数时,如果不传入arguments, ajax中的参数打印为undefined,所以我们需要将接收到的参数,传递给fn


函数防抖的理解:




我个人的理解其实和平时上电梯的原理一样:当一直有人进电梯时(连续触发),电梯门不会关闭,在一定时间间隔内没有人进入(停止连续触发)才会关闭。




从上面的例子,对防抖有了初步的认识,但是在实际开发中,需求往往要更加的复杂,比如我们要提交一个表单按钮,为了防止用户多次提交表单,可以使用节流, 但如果使用上面的节流,就会导致用户停止连续点击才会提交,而我们希望让用户点击时,立即提交, 等到n秒后,才可以重新提交。


对上面的代码进行改造,得到立即提交版:


const debounce = (fn, delay, immediate) => {
  let timer;
  return function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    if (immediate) {
      let startNow = !timer;

      timer = setTimeout(function () {
        timer = null;
      }, delay);

      if (startNow) {
        fn.apply(context, args);
      }
    } else {
      timer = setTimeout(function () {
        fn.call(context, ...args);
        //等同于上一句 fn.apply(context, args)
      }, delay);
    }
  };
};

从上面的代码可以看到,通过immediate 参数判断是否是立刻执行。


timer = setTimeout(function () {
    timer = null
}, delay)

立即执行的逻辑中,如果去掉上面这小段代码, 也是立即执行,但是之后就不会再执行提交了,当我们提交失败了怎么办(哭),所以加上上面这段代码,在设定的时间间隔内,将timer设置为null, 过了设定的时间间隔,可以再次触发提交按钮的立即执行,这才是完整的。


这是一个使用立即提交版本的防抖实现的了一个提交按钮demo


目前我们已经实现了包含非立即执行立即执行功能的防抖函数,感兴趣的小伙伴可以和我一起继续探究一下去,完善防抖函数~




做直播功能时,产品的小伙伴给提出这样一个需求:


直播的小窗口可以拖动, 点击小窗口以及拖动时, 显示关闭小窗口按钮,当拖动结束2s后, 隐藏关闭按钮;当点击关闭按钮时, 关闭小窗口




分析需求, 我们可以使用防抖来实现, 用户连续拖动小窗口过程中, 不执行隐藏关闭按钮,拖动结束后2s才执行隐藏关闭按钮;但是点击关闭按钮后,我们希望可以取消防抖, 所以需要继续完善防抖函数, 使其可以被取消。


「可取消版本」


const debounce = (fn, delay, immediate) => {
  let timer, debounced;
  // 修改--
  debounced = function () {
    const context = this;
    const args = arguments;
    clearTimeout(timer);

    if (immediate) {
      let startNow = !timer;

      timer = setTimeout(function () {
        timer = null;
      }, delay);

      if (startNow) {
        fn.apply(context, args);
      }
    } else {
      timer = setTimeout(function () {
        fn.call(context, ...args);
      }, delay);
    }
  };

  // 新增--
  debounced.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return debounced;
};

从上面代码可以看到,修改的地方是将return的函数赋值给debounced对象, 并且给debounced扩展了一个cancel方法, 内部执行了清除定时器timer, 并且将其设置为null; 为什么要将timer设置为null呢? 由于debounce内部形成了闭包, 避免造成内存泄露


上面的需求我写了个小demo, 需要的小伙伴可以看看可取消版本demo, 效果如下所示:图片


这个demo中,在拖拽过程中还可以使用节流,减少页面重新计算位置的次数,在下边学完节流,大家不妨试试


介绍节流原理、区别以及应用


前面学习了防抖,也知道了我们为什么要使用防抖来限制事件触发频率,那我们接下来学习另一种限制的方式节流(throttle)


函数节流



  • 原理:当频繁的触发一个事件,每隔一段时间, 只会执行一次事件。

  • 适用场景:



    • 拖拽场景:固定时间内只执行一次, 防止高频率的的触发位置变动

    • 监听滚动事件:实现触底加载更多功能

    • 屏幕尺寸变化时, 页面内容随之变动,执行相应的逻辑

    • 射击游戏中的mousedown、keydown时间



  • 辅助理解:


下面我们就来实现一个简单的节流函数,由于每隔一段时间执行一次,那么就需要计算时间差,我们有两种方式来计算时间差:一种是使用时间戳,另一种是设置定时器


使用时间戳实现


function throttle(func, delay) {
  let args;
  let lastTime = 0;

  return function () {
    // 获取当前时间
    const currentTime = new Date().getTime();
    args = arguments;
    if (currentTime - lastTime > delay) {
      func.apply(this, args);
      lastTime = currentTime;
    }
  };
}

使用时间搓的方式来实现的思路比较简单,当触发事件时,获取当前时间戳,然后减去之前的时间戳(第一次设置为0), 如果差值大于设置的等待时间, 就执行函数,然后更新上一次执行时间为为当前的时间戳,如果小于设置的等待时间,就不执行。


使用定时器实现


下面我们来看使用定时器实现的方式:与时间戳实现的思路是有差别的, 我们在事件触发时设置一个定时器, 当再次触发事件时, 如果定时器存在,就不执行;等过了设置的等待时间,定时器执行,我们需要在定时器执行时,清空定时器,这样就可以设置下一个定时器了


function throttle1(fn, delay) {
  let timer;
  return function () {
    const context = this;
    let args = arguments;

    if (timer) return;
    timer = setTimeout(function () {
      console.log("hahahhah");
      fn.apply(context, args);

      clearTimeout(timer);
      timer = null;
    }, delay);
  };
}

虽然两种方式都实现了节流, 但是他们达到的效果还是有一点点差别的,第一种实现方式,事件触发时,会立即执行函数,之后每隔指定时间执行,最后一次触发事件,事件函数不一定会执行;假设你将等待时间设置为1s, 当3.2s时停止事件的触发,那么函数只会被执行3次,以后不会再执行。


第二种实现方式,事件触发时,函数不会立即执行, 需要等待指定时间后执行,最后一次事件触发会被执行;同样假设等待时间设置为1s, 在3.2秒是停止事件的触发,但是依然会在第4秒时执行事件函数


总结


对两种实现方式比较得出:



  1. 第一种方式, 事件会立即执行, 第二种方式事件会在n秒后第一次执行

  2. 第一种方式,事件停止触发后,就不会在执行事件函数, 第二种方式停止触发后仍然会再执行一次


接下来我们写一个下拉加载更多的小demo来验证上面两个节流函数:点击查看代码


let state = 0 // 0: 加载已完成  1:加载中  2:没有更多
let page = 1
let list =[{...},{...},{...}]

window.addEventListener('scroll'throttle(scrollEvent, 1000))

function scrollEvent() {
    // 当前窗口高度
    let winHeight =
        document.documentElement.clientHeight || document.body.clientHeight

    // 滚动条滚动的距离
    let scrollTop = Math.max(
        document.body.scrollTop,
        document.documentElement.scrollTop
    )

    // 当前文档高度
    let docHeight = Math.max(
        document.body.scrollHeight,
        document.documentElement.scrollHeight
    )
    console.log('执行滚动')

    if (scrollTop + winHeight >= docHeight - 50) {
        console.log('滚动到底部了!')
        if (state == 1 || state == 2) {
            return
        }
        getMoreList()
    }
}

function getMoreList() {
    state = 1
    tipText.innerHTML = '加载数据中'
    setTimeout(() => {
        renderList()
        page++

        if (page > 5) {
            state = 2
            tipText.innerHTML = '没有更多数据了'
            return
        }
        state = 0
        tipText.innerHTML = ''
    }, 2000)
}

function renderList() {
    // 渲染元素
    ...
}

使用第一种方式效果如下:图片


一开始滚动便会触发滚动事件, 但是在滚动到底部时停止, 不会打印"滚动到底部了"; 这就是由于事件停止触发后,就不会在执行事件函数


使用第二种方式, 为了看到效果,将事件设置为3s, 这样更能直观感受到事件函数是否立即执行:


// window.addEventListener('scroll', throttle(scrollEvent, 1000))
window.addEventListener('scroll'throttle1(scrollEvent, 3000))

图片


一开始滚动事件函数并不会被触发,而是等到3s后才触发;而当我们快速的滚动到底部后停止滚动事件, 最后还是会执行一次


上面的这个例子是为了辅助理解这两种实现不方式的不同。


时间戳 + 定时器实现版本


在实际开发中, 上面两种实现方案都不满足我们的需求,我们希望一开始滚动就立即执行,停止触发的时候也还能执行一次。结合时间搓方式和定时器方式实现如下:


function throttle(fn, delay) {
  let timer, context, args;
  let lastTime = 0;

  return function () {
    context = this;
    args = arguments;

    let currentTime = new Date().getTime();

    if (currentTime - lastTime > delay) {
      // 防止时间戳和定时器重复
      // -----------
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      // -----------
      fn.apply(context, args);
      lastTime = currentTime;
    }
    if (!timer) {
      timer = setTimeout(() => {
        // 更新执行时间, 防止重复执行
        // -----------
        lastTime = new Date().getTime();
        // -----------
        fn.apply(context, args);
        clearTimeout(timer);
        timer = null;
      }, delay);
    }
  };
}

使用演示效果如下:


图片


实现思路是结合两种实现方式,同时避免两种方式重复执行, 所以当调用时间戳执行函数时,需要将定时器清空;当使用到定时器执行函数时,需要增加修改执行记录的时间lastTime


我们可以看到,开始滚动立即会打印页面滚动了,停止滚动后,时间会再执行一次,滚动到底部时停止,也会执行到滚动到底部了


最终完善版


上面的节流函数满足了我们的基本需求, 但是我们可以进一步对节流函数进行优化,使得节流函数可以满足下面三种情况:



  • 事件函数立即执行,并且事件停止后再执行一次(以满足)

  • 事件函数立即执行,但是事件停止后不再执行(待探究)

  • 事件函数不立即执行,但是事件停止后再执行一次(待探究)




注意点:事件函数不立即执行,事件停止不再执行一次 这种情况不能满足,在后面从代码角度会做分析。




我们设置两个参数startlast分别控制是否立即执行与最后是否执行;修改上一版代码, 实现如下:


function throttle(fn, delay, option = {}) {
  let timer, context, args;
  let lastTime = 0;

  return function () {
    context = this;
    args = arguments;

    let currentTime = new Date().getTime();

    // 增加是否立即执行判断
    if (option.start == false && !lastTime) lastTime = currentTime;

    if (currentTime - lastTime > delay) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      fn.apply(context, args);
      lastTime = currentTime;
    }
    // 增加最后是否再执行一次判断
    if (!timer && option.last == true) {
      timer = setTimeout(() => {
        // 确保再次触发事件时, 仍然不立即执行
        lastTime = option.start == false ? 0 : new Date().getTime();
        fn.apply(context, args);
        clearTimeout(timer);
        timer = null;
      }, delay);
    }
  };
}

上面代码就修改了三个地方,一个是立即执行之前增加一个判断:


if (option.start == false && !lastTime) lastTime = currentTime

如果传入参数是非立即执行, 并且lastTime为0, 将当前时间戳赋值给lastTime, 这样就不会进入 if (currentTime - lastTime > delay)


第二个修改地方, 增加最后一次是否执行的判断:


// 原来
// if (!timer) {...}

// 修改后
if (!timer && option.last == true) {
   ...
}

当传入last为true时,才使用定时器计时方式, 反之通过时间戳实现逻辑即可满足


第三个修改的地方, 也是容易被忽视的点, 如果start传入false,last传入true(即不立即执行,但最后还会执行一次), 需要在执行定时器逻辑调用事件函数时, 将lastTime设置为0:


 // 确保再次触发事件时, 仍然不立即执行
lastTime = option.start ==false0 : new Date().getTime()

这里解决的是再次触发事件时, 也能保证不立即执行。


疑问点


相信有的小伙伴会存在疑问,为什么没有讨论不立即执行, 最后一次也不执行的情况呢(即 start为true, last为true), 因为这种情况满足不了。


当最后一次不执行, 也就不会进入到 定时器执行逻辑,也就无法对 lastTime重置为0,所以,当再一次触发事件时,就会立即执行,与我们的需求矛盾了。关于这一点,大家了解即可了。


到这里,我们的节流函数功能就差不多了, 如果有兴趣的小伙伴可以自己实现一下可取消功能, 与防抖函数实现方式一致, 这里就不赘述了。


作者:白哥学前端
来源:juejin.cn/post/7230419964300951613
收起阅读 »

Flutter list 数组排序

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。 sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。 以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序: List<...
继续阅读 »

以使用Dart的 sort() 方法对Flutter中的List进行升序或降序排序。
sort()方法需要传递一个比较函数来指定如何对对象进行比较,并按照您指定的顺序进行排序。
以下是一个示例,假设有一个包含整数的列表,可以按照整数值进行排序:


List<int> numbers = [1, 3, 2, 5, 4];
// 升序排序
numbers.sort((a, b) => a.compareTo(b));
print(numbers); // 输出:[1, 2, 3, 4, 5]

// 降序排序
numbers.sort((a, b) => b.compareTo(a));
print(numbers); // 输出:[5, 4, 3, 2, 1]

在上述代码中,我们使用了sort()方法将数字列表按照升序和降序进行了排序。


在比较函数中,我们使用了 compareTo() 方法来比较两个数字对象。


如果想按照其他字段进行排序,只需将比较函数中的a和b替换为您想要排序的字段即可。




以下是示例代码,假设您有一个包含Person对象的列表,可以按照Person的年龄字段进行排序:


class Person {
String name;
int age;

Person({this.name, this.age});
}

List<Person> persons = [
Person(name: "John", age: 30),
Person(name: "Jane", age: 20),
Person(name: "Bob", age: 25),
];

// 按照年龄字段进行排序
persons.sort((a, b) => a.age.compareTo(b.age));

// 输出排序后的列表
print(persons);

在上述代码中,我们使用了sort()函数将Person对象列表按照年龄字段进行排序。
在该示例中,我们使用了compareTo()函数来比较Person对象的年龄字段,并按照升序排序。



如果您有小程序、APP、公众号、网站相关的需求,您可以通过私信来联系我


如果你有兴趣,可以关注一下我的综合公众号:biglead


作者:早起的年轻人
来源:juejin.cn/post/7230420475494137913

收起阅读 »

跳舞的小人,鼠标跟随事件

web
鼠标跟随事件 在这里,我本来想弄一个灰太狼抓羊的动画效果,就是将我们的鼠标logo替换成一只羊的照片,然后后面跟随着一只狼,设置了cursor: url('./01.gif'), auto这个属性,但是好像没有成功,好像是兼容问题。于是找了一张给会动的gif。...
继续阅读 »

鼠标跟随事件


在这里,我本来想弄一个灰太狼抓羊的动画效果,就是将我们的鼠标logo替换成一只羊的照片,然后后面跟随着一只狼,设置了cursor: url('./01.gif'), auto这个属性,但是好像没有成功,好像是兼容问题。于是找了一张给会动的gif。


实现效果


整体十分简单,主要就是js代码。


01.gif
html里我们就只是放了一张图片


<div class="img"></div>

然后简简单单的给他们设置了一下大小和样式。


  * {
margin: 0;
padding: 0;
}

![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e17598cd683f41fe89fddda68981de97~tplv-k3u1fbpfcp-watermark.image?)
body {
background: rgb(240, 230, 240);
}

.img {
width: 10%;
height: 20%;
position: absolute;
background-image: url('./01.gif');
background-size: cover;
}

看一看主要的js代码


这里我们主要是进行一些基本属性的定义和设置,index可以看成是时间桢或者循环次数。


let img = document.querySelector('.img')
// 定义小图片的旋转角度
let deg = 0
// 定义小图片位于网页左侧的位置
let imgx = 0
// 定义小图片位于网页顶部的位置
let imgy = 0
// 定义小图片x轴的位置
let imgl = 0
// 定义小图片y轴的位置
let imgt = 0
// 定义小图片翻转的角度
let y = 0
// 定义一个计数器
let index = 0

这段代码的作用是根据鼠标在图片上的位置计算图片的旋转角度和翻转。


首先,通过 e.x 获取鼠标在页面上的横坐标,img.offsetLeft 获取图片左边边缘距页面左边边缘的距离,img.clientWidth / 2 获取图片宽度的一半,即图片中心点距图片左边边缘的距离。将这三个值相减,得到鼠标相对于图片中心点的横向偏移量,即 imgx


 imgx = e.x - img.offsetLeft - img.clientWidth / 2

同样地,通过 e.y 获取鼠标在页面上的纵坐标,img.offsetTop 获取图片上边边缘距页面上边边缘的距离,img.clientHeight / 2 获取图片高度的一半,即图片中心点距图片上边边缘的距离。将这三个值相减,得到鼠标相对于图片中心点的纵向偏移量,即 imgy


imgy = e.y - img.offsetTop - img.clientHeight / 2

接下来,根据 imgximgy 的值,使用 Math.atan2 计算出以图片中心为原点的弧度值,并将弧度值转换为角度值,即 deg,这就是图片需要旋转的角度。
最后,将 deg 赋值给 rotate 属性,就可以实现对图片的旋转了。


 deg = 360 * Math.atan(imgy / imgx) / (2 * Math.PI)

然后,通过 index = 0 定义了一个初始值为 0 的变量 index,用于后续的操作。接下来,通过 let x = event.clientX 获取到当前鼠标的水平坐标位置。


    // 每当鼠标移动的时候重置index
index = 0
// 定义当前鼠标的位置
let x = event.clientX

然后,使用 if 判断图片的左边界是否小于当前鼠标的位置,也就是判断鼠标是否在图片的右侧。如果鼠标在图片的右侧,说明图片需要向左翻转,这时候将 y 赋值为 -180,用于后续的样式设置。如果鼠标在图片的左侧,说明图片无需翻转,此时将 y 赋值为 0。最终,将 y 值赋给图片的 rotateY 样式属性,就可以实现对图片的翻转效果了。当 y-180 时,图片将被翻转过来;当 y0 时,图片不会被翻转。


 // 当鼠标的x轴大于图片的时候,就要对着鼠标,所以需要将图片翻转过来
// 否则就不用翻转
if (img.offsetLeft < x) {
y = -180
} else {
y = 0
}


 window.addEventListener('mousemove', function (e) {
// 获取网页左侧距离的图片位置
imgx = e.x - img.offsetLeft - img.clientWidth / 2
// 多去网页顶部距离图片的位置
imgy = e.y - img.offsetTop - img.clientHeight / 2
// 套入公式,定义图片的旋转角度
deg = 360 * Math.atan(imgy / imgx) / (2 * Math.PI)
// 每当鼠标移动的时候重置index
index = 0
// 定义当前鼠标的位置
let x = event.clientX
// 当鼠标的x轴大于图片的时候,就要对着鼠标,所以需要将图片翻转过来
// 否则就不用翻转
if (img.offsetLeft < x) {
y = -180
} else {
y = 0
}
})

接下来就要实现图片跟随了


这段代码的作用是通过 JavaScript 实现对图片的旋转和移动。然后利用了setInterval一直重复


首先,使用 img.style.transform 将旋转角度 deg 和翻转角度 y 应用于元素的 transform 样式。rotateZ 用于实现元素绕 z 轴旋转,而 rotateY 则用于实现元素的翻转效果。注意,这里使用字符串拼接的方式将旋转角度和翻转角度拼接起来,以达到应用两个属性值的效果。


img.style.transform = "rotateZ(" + deg + "deg) rotateY(" + y + "deg)"

接下来,将 index 的值加一,即 index++,表明下一帧需要进行的操作。


index++

然后,使用条件语句 if (index < 50) 对小图片的位置进行调整。在这里,通过不停地调整小图片的位置,实现了小图片沿着鼠标运动的效果。其中,imgl += imgx / 50 用于计算小图片应移动的水平距离,而 imgt += imgy / 50 则用于计算小图片应移动的垂直距离。50 是移动的帧数,可以根据需求进行调整。


// 在这里设置小图片的位置和速度,并判断小图片到达鼠标位置时停止移动 
if (index < 100)
{
imgl += imgx / 100
imgt += imgy / 100
}
img.style.left = imgl + "px"
img.style.top = imgt + "px"

setInterval(() => {
// 设置小图片的旋转和翻转
img.style.transform = "rotateZ(" + deg + "deg) rotateY(" + y + "deg)"
index++
// 在这里设置小图片的位置和速度,并判断小图片到达鼠标位置时停止移动
if (index < 100) {
imgl += imgx / 100
imgt += imgy / 100
}
img.style.left = imgl + "px"
img.style.top = imgt + "px"
}, 10)

源码


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>鼠标跟随</title>
<style>
* {
margin: 0;
padding: 0;
}

body {
background: rgb(240, 230, 240);
}

.img {
width: 10%;
height: 20%;
position: absolute;
background-image: url('./01.gif');
background-size: cover;
}
</style>
</head>

<body>
<div class="img"></div>
</body>
<script>
let img = document.querySelector('.img')
// 定义小图片的旋转角度
let deg = 0
// 定义小图片位于网页左侧的位置
let imgx = 0
// 定义小图片位于网页顶部的位置
let imgy = 0
// 定义小图片x轴的位置
let imgl = 0
// 定义小图片y轴的位置
let imgt = 0
// 定义小图片翻转的角度
let y = 0
// 定义一个计数器
let index = 0

window.addEventListener('mousemove', function (e) {
// 获取网页左侧距离的图片位置
imgx = e.x - img.offsetLeft - img.clientWidth / 2
// 多去网页顶部距离图片的位置
imgy = e.y - img.offsetTop - img.clientHeight / 2
// 套入公式,定义图片的旋转角度
deg = 360 * Math.atan(imgy / imgx) / (2 * Math.PI)
// 每当鼠标移动的时候重置index
index = 0
// 定义当前鼠标的位置
let x = event.clientX
// 当鼠标的x轴大于图片的时候,就要对着鼠标,所以需要将图片翻转过来
// 否则就不用翻转
if (img.offsetLeft < x) {
y = -180
} else {
y = 0
}
})
setInterval(() => {
// 设置小图片的旋转和翻转
img.style.transform = "rotateZ(" + deg + "deg) rotateY(" + y + "deg)"
index++
// 在这里设置小图片的位置和速度,并判断小图片到达鼠标位置时停止移动
if (index < 100) {
imgl += imgx / 100
imgt += imgy / 100
}
img.style.left = imgl + "px"
img.style.top = imgt + "px"
}, 10)
</script>

</html>

作者:Mr-Wang-Y-P
来源:juejin.cn/post/7230457833212280893
收起阅读 »

聊聊我在阿里第一年375&晋升的心得

前言 思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。 绩效 首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。 很多同学都知道阿里的绩效是361制度,也就是30%...
继续阅读 »

前言


思来想去,觉得这个事情除了领导赏识大佬抬爱之外,还是挺不容易的,主观认为有一定的参考价值,然后也是复盘一下,继续完善自己。


绩效


首先晋升的条件就是要有个好绩效,那么我们就先基于绩效这个维度简单聊一下。


很多同学都知道阿里的绩效是361制度,也就是30%的人拿A,60%的人拿B,10%的人拿C,不过在阿里,我们一般不用ABC来表示,除去价值观考核,我们常用的表达是3.75、3.5、3.25,初略的对应关系如下:


361通用阿里
30%A3.75
60%B3.5 ±
10%C3.25

那么,了解了阿里的绩效制度,再来看看绩效里面的门道。


首先,讲一个职场潜规则,「团队的新人是用来背绩效的」,也就是会把差绩效(325)指标分配给新人,因为如果把325给老人,容易产生团队不稳定的因素,而且不得不承认的是,少一个老人比少一个新人的成本更大,另一方面,你作为新人,业务不熟,根基不稳,也不见得能产出多大的价值,所以对于多数新人来说,也只能接受。据我所知,只有极少的公司里面会有「绩效保护」这种政策,而且一般还是针对的应届生,社招来说,还是要看能力的。


其次,基于潜规则,大部分新人都在为保住3.5做努力,只有少数优秀的人可以拿到更好的绩效。而新人拿375在阿里是很少的存在,即使是老人,拿375都是非常不容易的,何况是一个新人。


最后,就是晋升,晋升的前提条件就是满一年且是好绩效,加上现在降本增效的大环境,有的要求连续两年375才行,甚至都不一定有名额,当然,晋升也和团队绩效和级别有关系,但总之,男上加男,凤毛麟角。


个人背景


我是21年8月份入职阿里的,2022年是在阿里的第一个整财年。


之前一直是在小厂摸爬滚打,没有大厂经历,这对于我来说,是劣势,写代码不注重安全性防护,没有code review,没有ab test,没有自动化测试,没有文档沉淀,没有技术分享,纯纯小作坊。更重要的是,小厂和大厂的做事风格流程什么的,真的是千差万别,所以当时的landing对我来说,还是很难受的。但是,我有没有自带的优势呢,也有,写作能力,但是光有写作能力还是不够的,你没东西可写也不行啊。


其实试用期结束之后就差不多进入新的财年了,对于刚刚进入状态的我,也迎来了更大的挑战。过去的一整年有较多的精力都投入在研发效能和安全生产方面,这对于以前纯业务开发的我来说,完全是一个新的领域,好在不负重托,也略有成果。


其实回想过去一年的经历来看,今天的成绩是多维度的结合,比如硬实力和软实力、个人和团队、内部和外部等多个维度,下面将分别介绍一些我个人的心得经验,仅供参考。


沟通能力


这也用说?不就是说话吗,谁不会说?


我看过很多的简历,如果有「自我评价」,几乎都会提到「具备良好的沟通能力」。
可是沟通能力看起来真的有那么简单吗?我认为不是的,实际上我甚至认为它有点难。


在职场经常会有这些问题:



  1. 这个点我没太理解,你能在解释一下吗?

  2. 为什么要这么做?为什么不这么做?

  3. 现在有什么问题?

  4. 你的诉求是什么?

  5. 讲话的时候经常被打断等等...


这些问题你是不是被问到过,或者这么问别人呢。


而这些问题的背后,则反映了沟通的不完整和不够清晰。面对他人的挑战,或向跨部门的同学讲述目标价值时,会沟通的同学会显的游刃有余,而不会沟通的同学则显得捉襟见肘。


沟通方面,其实也包含很多场景。


首先是逻辑要清晰。


面对用户的一线同事,比如销售和客服,他们都是有话术的,话术就是沟通的技巧。


为什么脸皮薄/不懂拒绝的人容易被销售忽悠?


因为销售在跟客户沟通的时候,就是有一套完整的话术,他先讲一,看你反应再决定讲二还是三;当你拒绝了A方案,他还有B方案C方案。一套完整的话术逻辑把你都囊括在里面,最后只能跟着他的思维走,这就是话术的意义。


同样的,在职场,你跟人沟通的时候,不能直说怎么做,解决方案是什么,而背景和问题同样重要,同时讲述问题的时候要尽可能的拆解清楚,避免遗漏,这样不只是让别人更理解你的意思,其实有时候换个视角,解决方案可能有更优的。


逻辑清晰,方案完善,对方就会处于一个比较舒服的状态,有时候能起到事半功倍的效果。你可能会觉得有些麻烦,但如果因为没有表达清楚而导致最后的结果不符合预期,孰轻孰重,应该拎得清的吧?


其次是要分人。


我在之前的面经中提到,自我介绍要分人,技术面试官和HR的关注点一定是不一样的,面对这种不同的出发点,你讲的东西肯定不能是同一套,要有侧重点,你跟HR讲你用了什么牛逼的技术、原理是什么,那不是瞎扯嘛。


这个逻辑在职场也是一样的,你和同事讨论技术、向领导汇报、回答领导的领导问题、跟产品、运营、跨部门沟通,甚至出现故障的时候给客满提供的话术,面对不同的角色、不同的场合,表达出来的意思一定是要经过「翻译」的,多换位思考。


即要把自己的意思传达到,也要让对方get到,这才是有效沟通。


所谓沟通能力,不只是有表达,还要有倾听。


倾听方面,比如当别人给你讲一个事情的时候,你能不能快速理解,能不能get到对方的点,这也很重要。
快速且高效,这是一个双向的过程。这里面会涉及到理解能力,而能理解的基础是基于现有的认知,也就是过往的工作经验、项目经历和知识面,这是一个积累的过程。


当然,也有可能是对方表达的不够清楚,这时候就要不耻下问,把事情搞清楚,搞不清楚就会有不确定性,就是有风险,如果最终的结果不符合预期,那么复盘的时候,委屈你一下,不过分吧😅。


最后是沟通媒介。


我们工作沟通的场景一般都是基于钉钉、微信、飞书之类的即时通讯办公平台,文字表达的好处是,它可以留档记录,方便后期翻阅查看,但是也一定程度在沟通表达的传递上,存在不高效的情况。


那这时候,语音、电话就上场了,如果你发现文字沟通比较费劲的时候,那一定不如直接讲来的更高效。


但是语音、电话就一定能聊的清楚吗,也不见得。


“聊不清楚就当面聊”,为什么当面聊就能比语音、电话聊的清楚?因为当面聊,不只是可以听到语气看到表情肢体动作,更重要的是当面聊的时候,双方一定是专注在这个事情上的,不像语音的时候还可以干其他的事,文字甚至会长时间已读不回,所以讲不清楚的时候,当面聊的效果是最好的。为了弥补留档的缺陷,完事再来个文字版的纪要同步出来,就ok了。


其他。


上面提到逻辑要清晰,要分人,还有倾听能力,和善用沟通媒介。


其实沟通里还包括反应能力,当你被突然问到一个问题的时候,能不能快速流畅的表达清楚,这个也很关键,如果你支支吾吾,反反复复的都没有说清楚,设想一下,其他人会怎么看你,“这人是不是不行啊?”,长此以往,这个信任度就会降低,而一旦打上标签,你就需要很多努力才能证明回来。


还包括争辩能力,比如在故障复盘的时候,能不能有效脱身不被拉下水,能不能大事化小小事化了,也都是沟通的技巧,限于篇幅,不展开了。


学会复盘


复盘是什么?


复盘是棋类术语,指对局完毕后,复演该盘棋的记录,以检查对局中招法的优劣与得失关键。在工作中,复盘是通过外在的表象、客观的结果找出本质,形成成功经验或失败教训,并应用到其他类似事件中,提升面向未来的能力。


所以,复盘不是流水账的记录经过和简单总结,也不是为了表彰罗列成绩,更不是为了甩锅而相互扯皮。找出本质的同时一定要有思考,这个思考可以体现在后续的一些执行事项和未来规划上,总之,就是要让「复盘」变的有意义。


什么是有意义的复盘?


就是你通过这次复盘,能知道哪些错误是不能再犯的,哪些正确的事是可以坚持去做的。会有一种「再来一次结果肯定不一样」的想法,通过有意义的复盘让「不完美」趋向「完美」。


我个人复盘的三个阶段:



  • 回顾:回顾目标、经过、结果的整个流程;

  • 分析:通过主观、客观的视角分析,失败的原因是什么,成功的原因是什么;

  • 转化:把成功经验和失败教训形成方法论,应用到类似事件中;


Owner意识


什么是owner意识?


简单讲就是主人翁精神。认真负责是工作的底线,积极主动是「owner意识」更高一级的要求。


如果你就是怀着只干活的心态,或者躺平的心态,换我是领导,也不认为你能把活做好。因为这种心态就是「做了就行,能用就行」,但有owner意识不一样,这种人做事的时候就会多思考一点、多做一点,这里面最主要的就是主动性,举个栗子,好比写代码,当你想让程序运行的更好的时候,你就会多关注代码编写的鲁棒性,而不是随便写写完成功能就行。


但人性自私,责任感也不是人人都有,更别提主动性了,所以这两种心态的人其实差别很大,有owner意识的人除了本职工作能做好之外,在涉及到跨团队的情况,也能主动打破边界去push,有责任心有担当,往往能受到团队内外的一致好评。


在其位谋其职,我其实并没有特意去固化自己的owner意识,就是怀着一个要把事情做好的心态,跟我个人性格也有关系,现在回想起来,不过是水到渠成而已。



卷不一定就有owner意识,不卷也不代表没有。



向上管理


这个其实我一开始做的并不好,甚至可以说是很差,小公司出身哪需要什么向上管理,活干好就行。


但是现在不一样了,刚入职比较大的一个感受就是,我老板(领导)其实并不太过问我的工作内容,只是偶尔问一下进度。


然而这个「问」,其实也能反应出一些过往不太在意的问题:



  1. 没有及时汇报,等到老板来问的时候其实处于一个被动的局面了,虽然也不会有什么太大的影响,可能多数人也都是这样,但是这不正说明我不够出众吗?

  2. 不确定性,什么进度?有没有遇到问题?这些都是不确定性,老板不喜欢“惊喜”,有困难要说,有风险要提前暴,该有结果的时候没有,老板也很被动,你会留下一个什么印象?


当然,向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道。不要只提问题找老板要解决方案,我会简述问题,评估影响面,还会给出解决方案的Plan A和Plan B,这样老板就是做选择题了,即使方案不够完美,老板指点一下不正是学习的好机会吗。


学会写TODO


为什么写todo?


写todo的习惯其实是在上家公司养成的,因为要写周报,如果不记录每天做了什么,写周报的时候就会时不时的出现遗漏的情况。除了记录当天所做的事情之外,我还列了第二天要做的事情。虽然一直有给自己定规划的习惯,但是颗粒度都没有这么细过。彼时的todo习惯,不仅知道当天做了什么,还规划了第二天做什么,时刻有目标,也不觉得迷茫。


进入阿里之后,虽然没有周报月报这种东西,但是这个习惯也一直保持着,在此之上,我还做了一些改良。



  1. 优先级,公司体量一旦大起来之后,业务线就很多,随之而来的事情就很多,我个人对处理工作是非常反感多线程并发的,特别是需要思考的工作,虽然能并行完成,但完成度不如专注在一件事情上的好,但是有些事情又确实得优先处理,所以就只能根据事情的紧急程度排一下优先级,基本很少有一件事需要从早到晚都投入在里面的,所以抽出身来就可以处理下一件事,所以也不会出现耽误其他事情的情况,当然线上故障除外。

  2. 消失的时间,因为真的是事情又多又杂,时常在下班的时候会有一种瞎忙的感觉,就是「忙了一天,好像什么都没干」,但又确实很忙很累,仿佛陷入一个怪圈。所以后来我就把颗粒度拆的更细,精确到小时,也不是几点钟该做什么,就是把做的事情记录下来,并备注一下投入的时间,比如上午排障答疑投入了两小时,这样到中午吃饭的时候也不至于上午就这样虚度了。让“消失的时间”有迹可循,治愈精神内耗。

  3. 归纳总结,我现在是在语雀以月度为单位记录每天的todo,这样就可以进行月度的总结和复盘,在半年度的OKR总结的时候,还有了归纳总结的来源,有经过、有结果、还有时间线,一目了然,再也不用为写总结发愁了。


总之,写todo的好处除了记录做了什么、要做什么,它还能辅助你把工作安排的更好。


有规划有目标,也不会陷入一个迷茫虚度的状态,属于一个成本非常低而收益却很高的好习惯,不止工作,学习也是如此,生活亦然。


其他方面


适应能力


于我个人来说,工作强度比以前要大很多,慢慢习惯了就行,在大厂里面阿里还不是最卷的,但钱也不是最多的;工作流程方面只是有些不清楚而已,并没有什么门槛,熟悉了就行;还有阿里味儿,确实要学很多新词儿、缩写、甚至潜台词,这没啥说的,还没见到有能独善其身的😅。


适应能力也不是随便说说,有太多的人入职新公司干的怀疑人生、浑身难受而跑路的,抛开公司的问题不说,难道就没有自己的问题吗?🐶


我把适应分为两个阶段,第一个阶段就是适应工作环境,熟悉公司、同事、产品、项目;第二个阶段就是适应之后,要想想如何迎接没有新手光环的挑战,如何去展示自己、提升自己等。


技术能力


夯实自己的技术能力尤为重要,毕竟是吃饭的家伙,是做事拿结果的重要工具和手段。


在大家技术能力的基本面都不会太差的情况下,如何在技术上建立团队影响力,就是需要思考的问题。


要找准自己在团队中的定位,并在这一领域深耕,做到一提这个技术就能想到你的效果,建立技术壁垒。


其实也不只是技术,要学会举一反三,找到自己在团队的差异性,虽然不可替代性在公司离了谁都可以转的说法下有些牵强,但是可以提高自己在团队的重要性和信任度。


信息渠道


要学会拓宽自己的信息渠道,有句话叫,掌握的信息越多,决策就越正确



  • 多看,看别人在做什么,输出什么,规划什么;

  • 多聊,找合作方,相同目标的同事,甚至其他公司的朋友,互通有无;


看完聊完要把对自己有用的信息提炼出来哦。


影响力


内部的话,主要是建立同事间的信任,技术的占比相对要多一些;


外部的话,主要是在合作方那里建立一个靠谱的口碑,如果配合超预期那就更好了,我就是被几个大佬抬了一手,虽然不起决定性作用,但是也很重要。


摆脱固化


跳脱出程序员的固化思维


程序的世界非0即1,程序员的思维都是非常严谨的,这个严谨有时候可能会显得有些“死板”,在商业化的公司里面,很多事情不是能不能的问题,而是要不要的问题。而在这里面,技术大多数都不是第一要素,出发点得是业务视角、用户视角,很多技术难点、卡点,有时候甚至不用技术也能解决。


小结



  • 沟通能力:逻辑要清晰,对象要分人,还有倾听能力,和善用沟通媒介;

  • owner意识:认真负责是工作的底线,积极主动是「owner意识」更高一级的要求;

  • 向上管理:向上管理也不只是向上汇报,也是一个体现个人能力和学习的渠道;

  • 写TODO:辅助工作,治愈内耗,一个成本低而收益高的好习惯;

  • 其他方面:拓宽信息渠道,找到技术方向,简历内部外部的影响力等;



实际上不止这些,今天就到这吧。



最后


当下的市场环境无论是求职还是晋升,都挺难的,都在降本增效,寒气逼人,我能拿到晋升的门票,诚然是实力的体现,但也有运气的成分。没晋升也不一定是你的问题,放平心态,当下保住工作才是最重要的。


哔哔了这么多,可能很多同学道理也都懂,估计就难在知行合一吧...


最后送给大家一句罗翔老师的经典名言:



人生最大的痛苦,就是你知道什么是对的,但却永远做出错误的选择,知道和做到这个巨大的鸿沟,你永远无法跨越。


作者:yechaoa
来源:juejin.cn/post/7230457573719392315

收起阅读 »

React 你是真的骚啊,一个组件就有这么多个设计模式🙄🙄🙄

web
React 真的是太灵活了,写它就感觉像是在写原生 JavaScript 一样,一个功能你可以有多种实现方式,例如你要实现动态样式,只要你愿意去做,你会有很多种解决方案,这可能也就是 React 会比 Vue 相对来说比较难一点的原因,这或许也就是这么喜欢 R...
继续阅读 »

React 真的是太灵活了,写它就感觉像是在写原生 JavaScript 一样,一个功能你可以有多种实现方式,例如你要实现动态样式,只要你愿意去做,你会有很多种解决方案,这可能也就是 React 会比 Vue 相对来说比较难一点的原因,这或许也就是这么喜欢 React 的原因了吧,毕竟它可是我见一个爱一个的技术之一🤣🤣🤣


也正是因为这个原因,在 React 中编写一个组件就给我们编写一个组件提供了多种方式,那么在接下来的文章中我们就来讲解一下这几种组件的设计模式。


Mixin设计模式


在上一篇文章中有讲解到了 JavaScript 中的 Mixin,如果对这个设计模式不太理解的可以通过这篇文章进行学习 来学习一下 JavaScript 中的 Mixin


如何在多个组件之间共享代码,是开发者们在学习 React 是最先问的问题之一,你可以使用组件组合来实现代码重构,你也可以定义一个组件并在其他几个组件中使用它。


如何用组合来解决某个模式并不是显而易见的,React 受函数式编程的影响,但是它进入了由面向对象库主导的领域(hooks 出现以前),为了解决这个问题,React 团队在这加上了 Mixin,它的目标就是当你不确定如何使用组合解决想用的问题时,为你提供一种在组件之间重用代码。


React 最主流构建 Component 的方法是利用 createClass 创建,顾名思义,就是创造一个包含 React 方法 Class 类。


Mixin危害


React 官方文档 Mixins Considered Harmful 中提到了 Mixin 带来的危害,主要有以下几个方面:



  • Mixin 可能会相互依赖,相互耦合,不利于代码维护;

  • 不同的 Mixin 中的方法可能会相互冲突;

  • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性;


装饰器模式


装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上,装饰者使用 @+函数名 形式来修改类的行为。如果你对装饰器不太了解,你可以通过这一篇文章 TS的装饰器你再学不会我可就要报警了哈 进行学习。


现在我们来看看在 React 中怎么使用装饰器,我们现在有这样的一个需求,就是为被装饰的页面或组件设置统一的背景颜色和自定义颜色,完整代码具体如下:


import React, { Component } from "react";

interface Params {
background: string;
size?: number;
}

function Controller(params: Params) {
return function (
WrappedComponent: React.ComponentClass,
): React.ComponentClass {
WrappedComponent.prototype.render = function (): React.ReactNode {
return <div>但使龙城飞将在,不教胡马度阴山</div>;
};

return class Page extends Component {
render(): React.ReactNode {
const { background, size = 16 } = params;
return (
<div style={{ backgroundColor: background, fontSize: size }}>
<WrappedComponent {...this.props}></WrappedComponent>
</div>

);
}
};
};
}

@Controller({ background: "pink", size: 100 })
class App extends Component {
render(): React.ReactNode {
return <div>牛逼</div>;
}
}

export default App;

这段代码的具体输出如下所示:


image.png


在上面的代码中,Controller 装饰器会接收 App 组件,其中 WrappedComponent 就是我们的 App 组件,在这里我们通过修改原型方法 render 将其的返回值修改了,并对其进行了一层包裹。


所以 App 组件在使用了类装饰器,不仅可以修改了原来的 DOM,还对外层多加了一层包裹,理解起来就是接收需要装饰的类为参数,返回一个新的内部类。恰与 HOC 的定义完全一致。所以,可以认为作用在类上的 decorator 语法糖简化了高阶组件的调用。


高阶组件


HOC 高阶组件模式是 React 比较常用的一种包装强化模式之一,你也可以看作 React 对装饰模式的一种实现,高阶组件就是一个函数,并且该函数接收一个组件作为参数,并返回一个新的组件,它是一种设计模式,这种设计模式是由 React 自身的特性产生的结果。


高阶组件主要解决了以下问题,具体如下:



  • 复用逻辑: 高阶组件就像是一个加工 React 组件的工厂,你需要向该工厂提供一个坯子,它可以批量地对你送进来的组件进行加工,包装处理,还可以根据你的需求定制不同的产品;

  • 强化props: 高阶组件返回的组件,可以劫持上一层传过来的 props,染回混入新的 props,来增强组件的功能;

  • 控制渲染: 劫持渲染是 hoc 中的一个特性,在高阶组件中,你可以对原来的组件进行条件渲染,节流渲染,懒加载等功能;


HOC的实现方式


常用的高阶组件有两种方式,它们分别是 正向属性代理反向继承,接下来我们来看看这两者的区别。


正向属性代理


所谓正向属性代理,就是用组件包裹一层代理组件,在代理组件上,我们可以代理所有传入的 props,并且觉得如何渲染。实际上这种方式生成的高阶组件就是原组件的父组件,父组件对子组件进行一系列强化操作,上面那个装饰器的例子就是一个 HOC 正向属性代理的实现方式。


对比原生组件增强的项主要有以下几个方面:



  • 可操作所有传入的props: 可以对其传入的 props 进行条件渲染,例如权限控制等;

  • 可以操作组件的生命周期;

  • 可操作组件的 static 方法,但是需要手动处理,或者引入第三方库;

  • 获取 refs;

  • 抽象 state;


反向继承


反向继承其实是一个函数接收一个组件作为参数传入,并返回了一个继承自该传入的组件的类,并且在该类的 render() 方法中返回 super.render() 方法,能通过 this 访问到源组件的生命周期propsstaterender等,相比属性代理它能操作更多的属性。


两者区别



  • 属性代理是从组合的角度出发,这样有利于从外部操作被包裹的组件,可以操作的对象是 props,或者加一层拦截器或者控制器等;

  • 方向继承则是从继承的角度出发,是从内部去操作被包裹的组件,也就是可以操作组件内部的 state,生命周期,render 函数等;


具体实例代码如下所示:


function Controller(WrapComponent: React.ComponentClass) {
return class extends WrapComponent {
public state: State;
constructor(props: any) {
super(props);
this.state = {
nickname: "moment",
};
}

render(): React.ReactNode {
return super.render();
}
};
}

interface State {
nickname: string;
}

@Controller
class App extends Component {
public state: State = {
nickname: "你小子",
};
render(): React.ReactNode {
return <div>{this.state.nickname}</div>;
}
}

反向继承主要有以下优点:



  • 可以获取组件内部状态,比如 state,props,生命周期事件函数;

  • 操作由 render() 输出的 React 组件;

  • 可以继承静态属性,无需对静态属性和方法进行额外的处理;


反向继承也存在缺点,它和被包装的组件强耦合,需要知道被包装的组件内部的状态,具体是做什么,如果多个反向继承包裹在一起,状态会被覆盖。


HOC的实现


HOC 的实现方式按照上面讲到的两个分类一样,来分别讲解这两者有什么写法。


操作 props


该功能由属性代理实现,它可以对传入组件的 props 进行增加、修改、删除或者根据特定的 props 进行特殊的操作,具体实现代码如下所示:


import React, { Component } from "react";

interface Params {
background: string;
size?: number;
}

function Controller(params: Params) {
return function (
WrappedComponent: React.ComponentClass,
): React.ComponentClass {
WrappedComponent.prototype.render = function (): React.ReactNode {
return <div>但使龙城飞将在,不教胡马度阴山</div>;
};

return class Page extends Component {
render(): React.ReactNode {
const { background, size = 16 } = params;
return (
<div style={{ backgroundColor: background, fontSize: size }}>
<WrappedComponent {...this.props}></WrappedComponent>
</div>

);
}
};
};
}

@Controller({ background: "pink", size: 100 })
class App extends Component {
render(): React.ReactNode {
return <div>牛逼</div>;
}
}

export default App;

抽离state控制组件更新


高阶组件可以将 HOCstate 配合起来,控制业务组件的更新,在下面的代码中,我们将 inputvalue 提取到 HOC 中进行管理,使其变成受控组件,同时不影响它使用 onChange 方法进行一些其他操作,具体代码如下所示:


function Controller(WrappedComponent) {
return class extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "",
};
this.onChange = this.onChange.bind(this);
}

onChange = (event) => {
this.setState({
name: event.target.value,
});
};

render() {
const newProps = {
value: this.state.name,
};
return (
<WrappedComponent
onChange={() =>
this.onChange}
{...this.props}
{...newProps}
/>

);
}
};
}

class App extends React.Component {
render() {
return (
<div>
<h1>{this.props.value}</h1>
<input name="name" {...this.props} />
</div>

);
}
}

export default Controller(App);

获取 Refs 实例


使用高阶组件后,获取到的 ref 实例实际上是最外层的容器组件,而非原组件,但是很多情况下我们需要用到原组件的 ref,我们先来看下面的代码,具体代码如下所示:


function Controller(WrappedComponent) {
return class Page extends React.Component {
render() {
const { ref, ...rest } = this.props;
return <WrappedComponent {...rest} ref={ref} />;
}
};
}

class Input extends React.Component {
render() {
return <input />;
}
}

class App extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
console.log(this.ref);
}
render() {
return <Input ref={this.ref} />;
}
}

export default Controller(App);

image.png


通过查看控制台输出,你会发现获取到的是整个 Input 组件,那么有什么办法可以获取到 input 这个真实的 DOM 呢?


在之前的例子中我们可以通过 props 传递,一层一层传递给 input 原生组件来获取,具体代码如下:


class Input extends React.Component {
render() {
return <input ref={this.props.inputRef} />;
}
}

注意,因为传参不能传 ref,所以这里要修改一下


image.png


当然你也可以利用父组件的回调,具体代码如下:


class App extends React.Component {
constructor(props) {
super(props);
this.ref = React.createRef();
}
componentDidMount() {
console.log(this.ref);
}
render() {
return <Input inputRef={(e) => (this.ref = e)} />;
}
}

最终的代码如下图所示,这里展示了以上两个方法具体代码,如下图所示:


image.png


通过查看浏览器输出,两者都能成功输出原生的 ref 实例


image.png


React 给我们提供了一个 forwardRef 来帮助我们进行 refs 传递,这样我们在高阶组件上获取的 ref 实例就是原组件的 ref 了,而不需要手动传递,我们只需要修改一下 Input 组件代码即可,具体如下:


const Input = React.forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});

image.png


这样我们就获取到了原始组件的 ref 实例啦!


获取原组件的 static 方法


当待处理的组件为 class 组件时,通过属性代理实现的高阶组件可以获取到原组件的 static 方法,具体实现代码如下所示:


function Controller(WrappedComponent) {
return class Page extends React.Component {
componentDidMount() {
WrappedComponent.moment();
}
render() {
const { ref, ...rest } = this.props;
return <WrappedComponent {...rest} ref={ref} />;
}
};
}

class App extends React.Component {
static moment() {
console.log("你好骚啊");
}
render() {
return <div>你小子</div>;
}
}

export default Controller(App);

你好骚啊 正常输出


image.png


反向继承操作 state


因为我们高阶组件继承了传入组件,那么就是能访问到this了,有了 this 我们就能操作和读取 state,也就不用像属性代理那么复杂还要通过 props 回调来操作 state


反向继承的基本实现方法就是原组件继承 Component,再在高阶组件中通过把原组件传参,再生成一个继承自原组件的组件。


image.png


具体实例代码如下所示:


function Controller(WrappedComponent) {
return class Page extends WrappedComponent {
componentDidMount() {
console.log(`组件挂载时 this.state 的状态为`, this.state);
setTimeout(() => {
this.setState({ nickname: "你个叼毛" });
}, 1000);
// this.setState({ nickname: 1 });
}
render() {
return super.render();
}
};
}

class App extends React.Component {
constructor() {
super();
this.state = {
nickname: "你小子",
};
}
render() {
return <h1>{this.state.nickname}</h1>;
}
}

export default Controller(App);

代码具体输出如下图所示,当组件挂载完成之后经过一秒,state 状态发生改变:


image.png


劫持原组件生命周期


因为反向继承方法实现的是高阶组件继承原组件,而返回的新组件属于原组件的子类,子类的实例方法会覆盖父类的,具体实例代码如下所示:


function Controller(WrappedComponent) {
return class Page extends WrappedComponent {
componentDidMount() {
console.log("生命周期方法被劫持啦");
}
render() {
return super.render();
}
};
}

class App extends React.Component {
componentDidMount() {
console.log("原组件");
}
render() {
return <h1>你小子</h1>;
}
}

export default Controller(App);

代码的具体输出如下图所示:


image.png


render props 模式


render props 的核心思想是通过一个函数将组件作为 props 的形式传递给另外一个函数组件。函数的参数由容器组件提供,这样的好处就是将组件的状态提升到外层组件中,具体实例代码如下所示:


const Home = (props) => {
console.log(props);
const { children } = props;
return <div>{children}</div>;
};

const App = () => {
return (
<div>
<Home admin={true}>
<h1>你小子</h1>
<h1>小黑子</h1>
</Home>
</div>

);
};

export default App;

具体的代码运行结果如下图所示:


image.png


虽然这样能实现效果,但是官方说这是一个傻逼行为,因此官方更推荐使用 React 官方提供的 Children 方法,具体实例代码如下所示:


const Home = (props) => {
console.log(props);
const { children } = props;
return <div>{React.Children.map(children, (node) => node)}</div>;
};

const App = () => {
return (
<div>
<Home admin={true}>
<h1>你小子</h1>
<h1>小黑子</h1>
</Home>
</div>

);
};

export default App;

具体更多信息请参考 官方文档


实际上,我们经常使用的 context 就是使用的 render props 模式。


反向状态回传


这个组件的设计模式很叼很骚,就是你可以通过 render props 中的状态,提升到当前组件中也就是把容器组件内的状态,传递给父组件,具体示例代码如下所示:


import React, { useRef, useEffect } from "react";

const Home = (props) => {
console.log(props);
const dom = useRef();
const getDomRef = () => dom.current;
const handleClick = () => {
console.log("小黑子");
};
const { children } = props;
return (
<div ref={dom}>
<div>{children({ getDomRef, handleClick })}</div>
<div>{React.Children.map(children, (node) => node)}</div>
</div>

);
};

const App = () => {
const childRef = useRef(null);
useEffect(() => {
const dom = childRef.current();
dom.style.background = "red";
dom.style.fontSize = "100px";
}, [childRef]);

return (
<div>
<Home admin={true}>
{({ getDomRef, handleClick }) => {
childRef.current = getDomRef;

return <div onClick={handleClick}>你小子</div>;
}}
</Home>
</div>

);
};

export default App;

在运行代码之后,我们首先点击一下 div 元素,具体有如下输出,请看下图:


image.png


你会看到成功的在父组件操作到了子组件的 ref 实例了,还获取到了子组件的 handleClick 函数并成功调用了。


提供者模式


考虑一下这个场景,就好像爷爷要给孙子送吃的,按照之前的例子中,要通过 props 的方式把吃的送到孙子手中,你首先要经过儿子手中,再由儿子传给孙子,那万一儿子偷吃了呢?孙子岂不是饿死了.....


为了解决这个问题,React 提供了 Context 提供者模式,它可以直接跳过儿子直接把吃的送到孙子手上,具体实例代码如下所示:


import React, { createContext, useContext } from "react";

const ThemeContext = createContext({ nickname: "moment" });

const Foo = () => {
const theme = useContext(ThemeContext);
return <h1>{theme.nickname}</h1>;
};

const Home = () => {
const theme = useContext(ThemeContext);
return <h1>{theme.nickname}</h1>;
};

const App = () => {
const theme = useContext(ThemeContext);

return (
<div>
{
<ThemeContext.Provider
value={{
nickname: "你小子",
}}
>

<Foo />
</ThemeContext.Provider>
}
{
<ThemeContext.Provider
value={{
nickname: "首页",
}}
>

<Home />
</ThemeContext.Provider>
}
<div>{theme.nickname}</div>
</div>

);
};

export default App;

代码输出如下图所示:


image.png


到这里本篇文章也就结束了,Hooks 的就不讲啦,在这篇文章中有讲到一点,喜欢的可以看看 如何优雅设地计出不可维护的 React 组件


参考资料



总结


不管是使用高阶组件、render propscontext亦或是 Hooks,它们都有不同的使用场景,不能说哪个好用,哪个不好用,这就要根据到你的业务场景了,最后不得不说,React,你是真的骚啊......


最后希望这篇文章对你有帮助,如果错漏,欢迎留言指出,最后祝大嘎假期快来!


作者:Moment
来源:juejin.cn/post/7230461901356154940
收起阅读 »

别再删到手抽筋!JS中删除数组元素指南

web
作为一名前端开发工程师,我们经常需要在 JavaScript 中操作数组,其中比较常见的操作便是对数组进行元素的添加、删除和修改。在这篇文章中,我会详细介绍JS中所有删除数组元素的方法。 删除数组元素之splice() splice()方法可以向数组任意位置插...
继续阅读 »

cover.png


作为一名前端开发工程师,我们经常需要在 JavaScript 中操作数组,其中比较常见的操作便是对数组进行元素的添加、删除和修改。在这篇文章中,我会详细介绍JS中所有删除数组元素的方法。


删除数组元素之splice()


splice()方法可以向数组任意位置插入或者删除任意数量的元素,同时返回被删除元素组成的一个数组。


const arr = ['a', 'b', 'c', 'd', 'e'];
arr.splice(1, 2);//删除数组下标为1、2的元素
console.log(arr); // ["a", "d", "e"]

通过上述代码,可以看到元素'b'和'c'已被删除,被删除的元素以数组形式返回。需要注意的是,该方法会改变原数组,因此使用时应该谨慎。


删除数组元素之filter()


filter() 方法创建一个新数组,其包含通过所提供函数实现的测试的所有元素。它不会改变原始数组。


const arr = [10, 2, 33, 5];
const newArr = arr.filter(item => item !== 2);//过滤掉值为2的元素
console.log(newArr); //[10, 33, 5]

以上代码展示了如何使用 filter() 方法删除数组内某些元素。其中箭头函数 (item) => item !== 2 表示过滤掉数组元素中值为2的元素。


删除数组元素之pop()


pop() 方法用于删除并返回数组的最后一个元素。


const arr = [1, 2, 3];
const lastItem = arr.pop(); //删除元素3,lastItem为3
console.log(lastItem); //3
console.log(arr); //[1, 2]

通过上述代码可以看到,使用 pop() 方法可以非常容易地删除数组的最后一个元素。


删除数组元素之shift()


shift() 方法用于删除并返回数组的第一个元素。


const arr = [1, 2, 3];
const firstItem = arr.shift(); //删除元素1,firstItem为1
console.log(firstItem); //1
console.log(arr); //[2, 3]

与pop()类似, shift() 方法也是从数组中删除元素。但与 pop() 不同的是,它从数组头部开始删除。


删除数组元素之splice()、slice()和concat()组合操作


刚才已经讲到了 splice()方法的删除功能,现在我们还可以将slice()concat() 结合起来使用进行删除。


let arr = ['a', 'b', 'c', 'd', 'e'];
arr = arr.slice(0, 1).concat(arr.slice(2));//删除数组下标为1的元素
console.log(arr);//["a", "c", "d", "e"]

通过以上代码可以看出,使用 slice() 方法获取要删除的元素前面和后面的元素,最后使用 concat() 将两个数组合并成为一个新的数组。


删除数组元素之使用ES6中的扩展运算符


在ES6中,spread operator扩展运算符是用来展开一个可迭代对象,比如用于函数调用时的展开数组等。


let arr = ['a', 'b', 'c', 'd', 'e'];
arr = [...arr.slice(0, 1), ...arr.slice(2)];//删除数组下标为1的元素
console.log(arr);//["a", "c", "d", "e"]

通过以上代码可以看出,使用ES6中的扩展运算符(...)也可以方便地删除数组内某些元素。


总结


不同方法适用于不同情境,具体的使用应该根据情况而定。总体而言, splice()filter() 是两个最常用的方法,pop()shift() 则适合删除特定位置的元素。而在多种情况下,不同的操作组合也能实现有效删除。至于如何更好地使用这些方法,还需要根据实际情况进行深入应用和理解。


希望本文对你有所帮助,同时也欢迎拓展其他新颖的删除数组元素的方法。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7230460443189690405
收起阅读 »

快速跑通环信IM Android Demo

1、以Android 4.0.0 Demo为例https://www.easemob.com/download/demo(下载其他版本的Demo 可以修改版本号直接下载就可以)https://downloadsdk.easemob.com/downloads/...
继续阅读 »

1、以Android 4.0.0 Demo为例
https://www.easemob.com/download/demo

(下载其他版本的Demo 可以修改版本号直接下载就可以)
https://downloadsdk.easemob.com/downloads/easemob-sdk-4.0.0.zip

运行时遇到以下报错在项目build.gradle中添加运行时遇到以下报错在项目build.gradle中添加

allowInsecureProtocol = true




注意:两个地方都要修改


2、清单文件中换成自己的appkey



3、运行项目(如果不出意外的话是可以正常运行的,如遇到其他报错,把gradle 版本切换成自己可以正常运行项目的版本)



demo为了安全默认的是手机号验证码登录,切换账号密码登录,双击版本号切换



4、从环信console 注册账号(https://console.easemob.com/user/register


5、输入id,密码登录即可大功告成!


附加:环信Demo 中登录逻辑




这里的isTokenFlag参数为boolean类型(true 为id 和token 登录,false 为id 和密码登录)


收起阅读 »

低头做事,抬头看路

前言 博主出来搬砖快4年了,在职场摸爬滚打,经历挺多比较难熬的经历,在这些磨练下不断的成长,平时有空的时候我习惯去思考,当然这个跟我的习惯有关系,以前喜欢幻想各种变身机甲战斗,每个男孩子小时候的梦想。 低头做事,抬头看路 这个应该是我感受比较深的一句话,...
继续阅读 »

前言




博主出来搬砖快4年了,在职场摸爬滚打,经历挺多比较难熬的经历,在这些磨练下不断的成长,平时有空的时候我习惯去思考,当然这个跟我的习惯有关系,以前喜欢幻想各种变身机甲战斗,每个男孩子小时候的梦想。



低头做事,抬头看路



这个应该是我感受比较深的一句话,当然也是属于我自己的东西,比如说某个名人说了一句名言,但是你没有去经历过,没有去深入思考,它对你来说就是一句话,只是被名人光环笼罩着,感觉高大无比,其实对你没有什么帮助的。


接下来,找个凳子坐下,听我慢慢讲述~


image.png


低头做事





  • 干好活


我认为这是普通人最实在的一件事,也是最应该去做的,比如说在社会中有很多收入比较低的人群,他们起早贪黑,很辛苦,但是如果他们不这么做的话,可能连生活都成问题,所以努力做事是一个基础。



  • 运气差的时候


如果你读过《易经》里面会谈到人的运气类似周期变化,潜龙勿用,这里我们不讲玄学的东西,当运气差的时候,没有背景,没有靠山的时候,你再怎么吹多厉害多能干,其实是没有多大用处的,这时适合把嘴闭上,把事干好。



  • 建立信任


当你刚刚进入新的团队的时候,leader怎么会把重任给一个新人接手对吧,其实多做事,做好事,这个是建立信任的基础。如果你上来就喊要造飞机搞火箭,可能别人会觉得你是传销毕业的,不太靠谱。


image.png


我认为它是一种比较实在的做法,也是普通人最基本的生存法则,它叫低头做事。


抬头看路




低头做事作为一个基础,那么抬头看路是一个进阶版,快跑者未必先到,力战者未必能克,如果方向错了,努力事倍功半。这部分讲更多的是方法论,一种思想。



  • 方向


作为程序猿,在业界对他是有标准要求的,之前写过一篇文章是阿里工程师修炼素养里面讲到的,就是技术思维、工程思维、产品思维,但是这不是唯一标准,你可以就某一项特别突出,那一定是个人才。但很多人对自己的职业规划还是模糊的,有几方面:


一个是接触的人、事情比较少,如果跟你打交道的都是小喽喽,那你没有机会从大佬身上去学习东西。即使你没有机会接触公司这些中高层,你可以网上认识各大厂大佬,我之前在一个技术群认识很多技术大牛,至少他们可以为你的问题、你的职业抉择提出他们的想法。



毕竟你走的路,他们已经走过了。



一方面是自己没有意识去归纳,你当前这个阶段要求的能力是什么,下一阶段需要的能力、素质是什么,那下一段的要求从哪里去找呢?从你身边的大佬、业界优秀的人身上去找,甚至招聘的jd里面的要求去找。



有时迷茫是正常的,保持前进的心态,积极向上的精神



image.png



  • 参考、借鉴


在实际工作中,有很多需求其实在业界早已有解决方案了,互联网发展了几十年,你碰到的问题其实很多人都帮你踩了好几遍了,在处理这些需求的时候,需要前期的调研,比如业界有哪些优秀的设计或者思想,为什么要采用这种方式解决,跟你心里想的方案有什么差异。


就拿ddd领域来讲吧,其实这个就是换个皮又出来,高内聚、低耦合这个思想一直都是存在的,比如说各种设计模式,还有各种优化,对那些重复的代码进行抽象、聚拢,这个一直在我们身边只是没有给它定义一套方法论出来,ddd就将它讲清楚,并给了对应demo。


甚至是跨界的思维也可以帮你解决问题,这个需要你的抽象能力,就像今天有个同学在我博客下面评论他对ddd那篇文章看法,因为我项目里面应用的比较少,违背各种它的设计理念,我觉得这是大部分人的思维被技术控死了。


我举了个栗子,一棵苹果树砍成树冠,那它还是不是苹果树?大部人会觉得是的,我也是这个观点。就是你目前有结苹果的能力,或者未来有这个能力,也算苹果树。苹果树苗,人家小时候也是一点一点长起来的,又不是一下子就结果对吧。


DDD领域改造一个道理,它是一个过程,它具备往这个方向发展的能力,但是目前没有必要去做这层优化,你说我项目是不是ddd呢?我觉得算是,只是应用程度没有覆盖那么广,但是理论我们需要去了解的。



  • 机会


前面讲过,低头做事为了建立信任,那么抬头看路可以更多为你创造机会,有段时间我的飞书签名:抓住机会,创造机会。



抓住机会,创造机会



我在上家公司的时候,感受比较深,就是即使你做了很多事,其实成就不高的,原因是你干的活不被上面的人重视,或者不是很重要的活。这时你要去发现机会,抓住它,甚至主动请缨,劳资就能把事情干好,干漂亮了。即使是难度很大,要把首战必胜的信心,认真做事态度表现出来。


还有个栗子,很多高收益的项目,就是你做完之后收获很多,可能是荣誉、奖金、信任、赞赏,其实换个人去做很大几率可以干成,所以并不是你埋头苦干就能出成绩,往往属于你的机会就是那么关键几个,把它做好,做漂亮了就可以了。就像我们以前高考,把会答的题写了就已经超过60%的人了。



现在太多的人想着拿90分,但连5分的题都不屑于去做



查理芒格他们有个投资理念:棒球理论,就是有个研究,棒球在某几个格子的时候是最容易击中的,只要把那几个打好就ok了,现在的社会鼓吹一股浮躁的风气,大家都想赚大钱,拿很多荣誉,这是人性的贪在肆虐。



寻找属于自己的机会



感知能力




这个话题偏玄学一点的,当然里面的内容尽可能往实际的场景去靠,不过于虚,对读者也没有太大的帮助。上面我们讲了低头做事,抬头看路这还不够的,因为这局限于个人,人在一个场里面,或者不同场的嵌套,是会被影响的,大趋势一定比你个人的努力还重要的。(这个我觉得是前面两者做好的基础上去提高,而不是还没学会走,开始学飞)



  • 势能


有句话我听过很多次,“一命二运三风水...”,它对个人来讲有木有用呢?这个需要个人去理解、思考的,前面好几个讲的是机会问题,这个是我们决定不了的,闽南语里面“七分靠打拼,三分天注定”是这个道理。另外有个东西是我们可以去感知的,就是势能。


比如说一家技术公司,它是产品强势还是研发,还是测试,他们之间的势能不一样,会影响职场工作、项目推进,因为这是一个比较大的场。


市场也有势能,这东西就像做菜一样,买菜备料,炖的话可能要炖很久,食物才能入味,最后才能发挥它的威力。市场我们也能找到对应的事件,忽然某个社交app一直讲某件事造势,然后其他人跟着喊,然后就扩大了,最终朝着它想要去的地方发展。



个人能力、努力太单薄了,多关注势




  • 推演


这个跟上面还是有些耦合的内容,推演是依据现有的东西,然后根据自己的经验或者过往的经验进行预判。这个在五子棋的时候,ai机器人进行机器学习训练,打败了很多高手,这就是推演的魅力。


这个能力也会被决策者应用,比如说决策树,主要是列出会出现的场景,以及对应的应对措施,预防一些风险。


推演对于普通人来讲也是一项能力,比如说你能否预测下这个月的成长,今年所达到的水平,或者更长时间5年后你成长的模样,你的能力圈层能到哪里。


总结




低头做事,抬头看路,可能几年后荣誉满满、成就满满,也可能平淡无奇,人生也就那样。人生的意义不是得来衡量的,就像很多有钱人不一定就很幸福,因为他们消费水平也上去了,欲望更大了。


人生是一场体验,你在中间经历了什么,成长了什么,收获了什么,酸甜苦辣咸何尝不就是人生的味道。


image.png


作者:大鸡腿同学
链接:https://juejin.cn/post/7187433862765477947
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

工作这么久了,不想打工的想法越来越强烈?

每次和我的同学,朋友们电话的时候,他们总是能分享给你他所在行业以及领域的知识,并且对于自己目前的职业也是非常稳定的。 怎么说呢,就是一毕业实习或者找到自己的第一份工作,一干就是四五年,甚至更长。 他们虽然也抗拒过公司作息制度,也担忧过自己的薪资涨幅能不能跟上同...
继续阅读 »

每次和我的同学,朋友们电话的时候,他们总是能分享给你他所在行业以及领域的知识,并且对于自己目前的职业也是非常稳定的。


怎么说呢,就是一毕业实习或者找到自己的第一份工作,一干就是四五年,甚至更长。


他们虽然也抗拒过公司作息制度,也担忧过自己的薪资涨幅能不能跟上同批次的同学,但想归想,但身体总是安分的,在一家公司本本分分的工作下去,到现在不是主管经理就是别的重要职务了。



▲图/ 喝了香槟,咱就是深圳人


毕竟,一个员工在一家企业奋斗,始终最重要的品质就是忠诚,对自己的领导,对自己的公司忠诚,其次是你那出色的个人能力。


这是一个职业人的路线。


虽然和他们本质上都是职业人,但是走的路线已经出现了异同。


很有意思的是,我那同学从毕业就开始在一家公司做到现在,对岗位很敬业,对公司非常忠诚,尽管他知道老板喜欢画饼,相关福利一般般,新一线中规中矩。但还是对老板的愿景表示期待。


可能文字描述敬业已经不顶用了,那就实际情况来旁证吧。由于他所在的是乙方公司,凭借自己的敬业,负责的项目加班加点也要完成,获得了不少甲方客户的赞赏,甚至出现过甲方单独打赏几千,有的高达5k到他的私人账户上。


当然,他的老板也看在眼里,公司需要这种人,在公司已经是很难得的品质了。所以一路到现在,当上了前端组的组长,薪资按照当地中位数来算的话,已经是两倍多了。


这样的品质连我也对他表示致敬。但是在国内,这种能力像是被稀释了一般,任劳任怨的人太多太多。


或许出于站在不同的角度,我总是会敲打他工作的诟病,并引导他对于自身的规划和目的清晰点。


身边人出现了这种品质,使得我每一次的离职也好,换城市也好,都会在那么几分钟怀疑自己,怀疑自己的此举做的是不是正确的。每次出现断舍离总是充满内心活动的。



▲图/ 一个地方,最吸引你的地方是什么?


如果不这么做,内心总是备受煎熬,同时伴随着遗憾。


我尝试着计划安分守己在一家企业干到30岁,40岁,甚至退休。


表示内心仍然很难做到,我的内心总是浮现出一股强烈的自主意识,想去做一些事情,去做一些更加有意义的事情。


有这般想法,我归因于混在一群想法各异,活法各异的群体里面。在我认为的世界里面原来还有另一个世界。这种感觉就好像童年玩魂斗罗,听说有水下8关,总是充满着好奇与探索的心是一样的。


就如此次,本以为今年做足了准备和勇气选择裸辞,处于职业空档期的我会做一些具备个人发展力的路线选择。


事实上,我只是换了一个城市而已,还是一个职业码农。


说起去深圳,我甚至都没有做好充足的准备,仅仅是打算去海边游玩,见一见想见的人。


来了之后,一路上接地气的拖鞋短裤短袖装,一排排的大排档,生活充满了烟火气,那就玩几天吧?


显然,一到工作日,在深圳的伙伴也都基本上班,偶尔走走当散散心了,记录记录写几篇散文也好。


玩着玩着脑海就闪过职业人的想法。


投递着简历试试吧。


嗯?待遇还不不错,工作环境也还不错,还包吃还包住?


那就去吧。



▲图/ 工作餐,吃饭就要积极


十天左右,就没有了空档期,直接动身去上班。


在新入职一个月左右,前前后后忙碌压根没有创作的想法,一股脑地栽在工作上。


渐渐一股无形的力压迫在身上。因为所在的一家是车企,每天都能看到豪车劳斯莱斯,保时捷,宾利等等。一般的车都是见不到,基本上都是各式名车。


能够想象随便一台车都能顶一个人几年的薪资。而经常出入的客户有时只需要一句话就能将车拿下。


不清楚别的同事会有作何感想,至少我偶尔会在下班的路上emo一阵子。这人与人的财富差距,得隔几代人才能追上。


入职时,作为新员工学习了企业的文化,整整三天的洗礼。还是能够学习到一家企业的发展路线,每时每刻做出的决策和不同时期做出的战略,不能不让人佩服。


一个普通人进入金碧辉煌的宫殿,小时候能够在电视剧里看到,现在自己也成了当事人,不说表情,可能连内心活动都如出一辙。


但是作为普通人,也该有普通人的活法,稳扎稳打,逐步向上。


总归要扩大自己的眼界,开放自己的内心。


尽管我的现东家富得流油,但心态并不会规避自己,而是不断融入到集体里。学习他们的方式,学习他们对于不同时期的战略和决策。


也要懂得精于计算得失。 很多人其实不是很懂计算。绝大多数人都是在算计自己会失去多少,而不会算会得到多少。


而一般的人也总是在算短期内会失去什么,优秀则总是会算我投入后未来会有什么样的回报,前者在算计今天,目光短浅,而后者则是舍在今天,得在明天,计算的是未来。


精于计算得失的,就懂得什么是投资,不懂的只会投机。对于赚钱,你可以投机,但是对于自己最好还是投资。



▲图/ 天很蓝,是没想到的


我也一样,待稳定了自己的工作,终归还是需要花费时间和经历来投资自己,无论是在技术上,还是在行业圈子建设,又或者是领域摸索,最终还是以自身为主做出一定的成果。


这并不是承诺,而是一种必要做的过程,这也是出现和我的同学朋友走的路线出现不同的原因。


相比于职业,我更加倾向于借助职业,耳濡目染,以身作则,完备自身的空缺知识,投身于自身成果建设以及财富积累的历程。


那,我们拭目以待吧。


作者:桑小榆呀
链接:https://juejin.cn/post/7172755443763118094
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

记一次蛋疼的debug经历(cycle inside)

甲方公司的大部分业务都是做成私有pod的,本人这次接手的也不例外。由于是二次开发,一开始图方便并没有做成pod而是把业务代码全部放测试工程中。昨天做完之后复制一下前面的.podspec,修改了部分信息后pod install,pod完成以为没问题了,谁知一运行...
继续阅读 »

甲方公司的大部分业务都是做成私有pod的,本人这次接手的也不例外。由于是二次开发,一开始图方便并没有做成pod而是把业务代码全部放测试工程中。昨天做完之后复制一下前面的.podspec,修改了部分信息后pod install,pod完成以为没问题了,谁知一运行直接报错:
cycle inside 工程名: building could producce xxx(后面省略)


这个报错内容还是挺详细的,但一开始没有多想,直接谷歌搜一下,stackoverflow确实有类似问题。发现很多回答也是只会叫你清缓存清derive data,还有叫你用老的编译系统,移动build phases的顺序,甚至还有命令行开启swift编译环境的。我移动了build phases的顺序发现没效果。然后在苹果开发者论坛看到一个教程教你解决库的依赖循环


此时就想着:导致依赖循环的原因难道是头文件的引用出了问题?可是OC的#import也不会重复导入,前面也一直没有报错提示。但也没办法,只能死马当作活马医,先排查了公共头文件,发现确实很多.h都直接引用此文件,于是先挨个解耦。运行,报错依旧。于是继续检查清除一些不必要的引用,还是没效果。


不得已只能重新查看报错信息,其实一开始就已经丢到翻译网页上,只是内容太长,一直没有细看。这时发现报错提到pod在生成图片资源的时候打出来一个'\\\\.bundle',和其他组件一对比明显有问题,于是检查.podspec,发现在设置s.resource_bundles的时候,居然是换行的:


s.resource_bundles = {
'xxx
' => ['xxx/Assets/*']
}

并且图片资源里有子文件夹的,也没有加上**/,修改完之后如下


s.resource_bundles = {
'xxx' => ['xxx/Assets/**/*']
}

修改完重新pod install之后,终于运行成功。真不知道前面第一版是怎么集成进去的。。。


从昨天下午发现问题到现在解决,总耗时估计有6-8小时,果然魔鬼都在细节里。以后还是要认真查看报错信息,不要单纯依赖搜索,更加不要指望清缓存


作者:浪大鱼贵
链接:https://juejin.cn/post/7224764099187884090
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Kotlin ?: 语法糖的 一个坑

问题还原 先定义一个函数 /** * @param s 字符串 * @return 参数的 长度如果不为1 返回null */ fun testStr(s: String): String? { return if (s.length == 1...
继续阅读 »

问题还原


先定义一个函数


/**
* @param s 字符串
* @return 参数的 长度如果不为1 返回null
*/
fun testStr(s: String): String? {
return if (s.length == 1) {
"length:1"
} else {
null
}
}

然后定义一个类 里面有个函数调用了上面的方法


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
} ?: println("inner else")

} ?: println("outer else")
}
}

在main函数里面跑一下


fun main() {

val t = Test()
t.testLet()
}

看下运行结果:


image.png


到这里还是好理解的, 但是我如果稍微改一下我们的代码


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
}
} ?: println("outer else")
}
}

这个时候 你认为程序应该打印什么?


我觉得多数人可能会认为 程序只会打印一行
inner let-1


然而你运行一下你就会发现:


image.png


他竟然还多打印了 outer else


这个行为我估计很多kotlin开发者就理解不了了, 因为按照javaer 的习惯, if 和 else 总会配对出现
我们上面的代码, 内部的判断逻辑 没有写else 那就不应该有任何行为啊, 为什么 内部的else 逻辑没写的时候
自动给我走了外部的else逻辑?


我们反编译看一下


image.png


这个反编译的代码是符合 程序运行结果的,虽然 他肯定不符合 多数kotlin开发者的意愿,


我们再把代码修改回去,让我们内部的inner case 也有一个else的逻辑


class Test {
fun testLet() {
testStr("1")?.let {
println("inner let-1")
testStr("11")?.let {
println("inner let-2")
} ?: println("inner else")
} ?: println("outer else")
}
}

再反编译看一下代码:


image.png


这个代码就很符合我们写代码时候的意愿了


到这里我们就可以下一个结论


对于 ?: 这样的语法糖来说 ,如果内部 使用了?. 的写法 而没有写?: 的逻辑,则当内部的代码真的走到else的逻辑时,外部的?: 会默认执行


另外一个有趣的问题


下面的let 为什么会报错?


image.png


这里就有点奇怪了,这个let的写法为什么会报错? 但是run的写法 却是ok的?
而且这个let的写法 在我们第一个小节的代码中也没报错啊? 这到底是咋回事


看下这2个方法的定义


image.png


image.png


最关键就在于 这个let 需要有一个receiver, 而如果这个let 刚才在main函数中的写法就不对了


因为main函数 显然不属于任何一个对象,也不属于任何一个类,let在这个执行环境下 找不到自己的receiver 自然就会编译报错了


但是对于run 来说,这个函数的定义没有receiver的概念,所以可以运行


作者:vivo高启强
链接:https://juejin.cn/post/7229606662289457211
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

轻松便捷,使用Compose的Scaffold搭建一个首页布局

节后第一篇文章,稍微轻松一点,简单介绍下Scaffold的使用。在如今各式各样的app里面,如果我们想要在里面寻找一个共同点,那么就是基本每个app首页都是以底部tab切换页面加上顶部导航栏的形式出现,所以Compose也专门为了这一点设计出自己的便捷式布局-...
继续阅读 »

节后第一篇文章,稍微轻松一点,简单介绍下Scaffold的使用。在如今各式各样的app里面,如果我们想要在里面寻找一个共同点,那么就是基本每个app首页都是以底部tab切换页面加上顶部导航栏的形式出现,所以Compose也专门为了这一点设计出自己的便捷式布局--Scaffold,我们称为脚手架布局,如同字面意思,脚手架布局就是将各种Meterial Design风格的组件组合在一起形成一个具有Meterial Design风格的布局


Scaffold的参数


学习Compose的组件第一步我们肯定是去看看这个组件都支持哪些参数,哪些必传哪些可选,当我们点到Scaffold里面去查看它的参数的时候,会发现它的参数基本囊括了我们熟知的首页所具备的一切元素


image.png

其中有几个参数是专门用来设置Composable组件的,它们分别是



  • topBar:顶部导航栏布局,推荐使用TopAppBar组件

  • bottomBar:底部导航栏,推荐使用BottomAppBar

  • snackBarHost:Snackbars组件

  • floatingActionButton:悬浮按钮

  • drawerContent:侧边栏布局

  • content: Scaffold需要呈现出来的布局


一般一个普通的app首页,使用topBar,bottomBar和content这三个参数就可以搭建出来,我们来尝试下


搭建首页


导航栏


首先创建个Composable函数,在最外层使用Scaffold布局,然后topBar就使用官方推荐的TopAppBar组件


image.png

而这个TopAppBar组件在AppBar.kt文件里面,有两个重载函数,一个是内部布局完全自定义的


image.png

一个是如同脚手架布局一样帮你已经排版好了布局,你只需要往里面添加相应组件就好


image.png

我们这边就选择第二种方式来做我们的导航栏,代码如下


image.png

navigationIcon对应着导航栏上最左边的图标,一般可以放返回键,菜单键,这边使用系统自带的菜单icon,title对应着导航栏上的标题,可以直接放一个文案,也可以放一个布局,actions是一个RowScope,放置在导航栏的右侧,在里面可以添加一些小按钮,使用的也都是系统自带的icon,效果图如下


image.png

很方便是不,想想看这样的一个布局我们用传统xml形式来写需要多少代码量,用我们的脚手架布局几行代码就搞定了,接下来做底部导航栏


底部导航栏


底部导航栏也使用官方推荐的BottomAppBar,它也是个RowScope,所以往里面添加组件就能满足水平布局的效果,而我们每一个tab都可以使用另外一个组件,BottomNavigationItem来表示,它的参数如下所示


image.png

  • selected:是否被选中

  • onClick:点击事件

  • icon:图标组件,通常是个矢量图

  • modifier:操作符

  • enabled:是否可点击

  • label:文案

  • alwaysShowLabel:默认为true,表示文案常驻在图标底下,如果是false,那么只有等item被选中时候文案才会展示出来

  • interactionSource:监听导航栏点击事件

  • selectedContentColor:选中的颜色

  • unselectedContentColor:未被选中的颜色


知道参数以后,我们就可以把底部导航栏添加到布局里面


image.png

selectedIndex记录着我们点击过的下标值,我们可以通过下标值来判断应该展示哪个页面,代码如下


image.png

一个简单的首页切换页面的效果就出来了,效果如下


0503aa1.gif

侧边栏


刚刚我们看到Scaffold的参数里面还有大量drawerxxxx相关的参数,不用猜肯定知道是用来设置侧边栏的,其中drawerContent就是拿来写侧边栏里面的视图,那么我们也简单写个侧边栏布局看看


image.png

代码很简单就是一个头像加一个用户名,运行下代码看看


0504aa1.gif

加了drawerContent以后我们在界面上轻松向右一滑就出来个侧边栏了,能够通过手势直接滑出侧边栏主要是因为Scaffold里面的drawerGesturesEnabled这个参数默认为true,我们如果说哪一天不要侧边栏了,又不想把代码删掉,可以直接把drawerGesturesEnabled设置为false就好了,但是这里有个问题,我们看到drawerxxx参数里面没有用来设置去打开侧边栏的参数,要知道现在应用当中但凡有侧边栏功能的,都会提供一个按钮提示用户去点击展示出侧边栏,还是说Compose里面的侧边栏里面没有这功能?当然不是,我们看下Scaffold里面第二个参数,也就是ScaffoldState,我们看下这个状态类里面都有啥


image.png

总共俩参数,第一个就是DrawerState,一看就知道是拿来设置侧边栏状态的,我们再去DrawerState里面看看


image.png

第一个参数DrawerValue就是拿来设置侧边栏打开还是关闭的,他有两个值,分别是ClosedOpen,第二个参数是用来监听侧边栏开启或者关闭状态的,暂时用不到先忽略,这样我们先在代码当中创建一个ScaffoldState,并且传入一个DrawerState,代码如下


image.png

到了这一步我们知道了如果要通过某个按钮来展示侧边栏,只需要改变drawerStateDrawerValue属性就好,从Closed改变到Open,那怎么变呢?我们使用DrawerState里面提供的animateTo函数


image.png

我们看到animateTo函数里面第一个参数就是目标值,也就是我们需要设置成DrawerValue.Open的地方,第二个参数是个动画参数,因为侧边栏展示出来有个滑动的动画过程,当然如果不需要动画可以使用另一个函数snapTo就可以了,这个函数只需要设置一个targetValue值,另外无论是animateTo还是snapTo,都是挂起函数,所以我们还需要为它提供一个协程作用域,这里直接使用
rememberCoroutineScope函数来创建协程作用域


image.png

现在我们就可以在界面顶部的菜单按钮上设置一个打开侧边栏的动作,代码如下


image.png

代码到这里就结束了,我们看下效果


0504aa2.gif

总结


Scaffold使用起来就相当于我们平时用过的自定义View一样,已经把一些常用的视图和逻辑已经封装在里面,我们开发时候可以使用已经被封装过的组件,也可以自己去写视图,开发起来也是很方便的.


作者:Coffeeee
链接:https://juejin.cn/post/7229150192259465274
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

不是吧不是吧,fdsan 都不知道?

背景 fd 是什么 In Unix and Unix-like computer operating systems, a file descriptor (FD, less frequently fildes) is a process-unique id...
继续阅读 »

背景


fd 是什么



In Unix and Unix-like computer operating systems, a file descriptor (FD, less frequently fildes) is a process-unique identifier (handle) for a file or other input/output resource, such as a pipe or network socket.



fd 通常用作进程内或进程间通信,是进程独有的文件描述符表的索引,简单来说,就是系统内核为每个进程维护了一个 fd table,来记录进程中的fd,通常在android 系统上,每个进程所能最大读写的fd数量是有限的,如果超限,会出现fd 无法创建/读取的问题。


fdsan 是什么


fdsan 全称其实是 file descriptor sanitizer,是一种用于检测和清除进程中未关闭的文件描述符(fd)的工具。它通常用于检测程序中的内存泄漏和文件句柄泄漏等问题。文件描述符是操作系统中用于访问文件、网络套接字和其他I/O设备的机制。在程序中,打开文件或套接字会生成一个文件描述符,如果此文件描述符在使用后未关闭,就会造成文件句柄泄漏,导致程序内存的不断增加。fd sanitizer会扫描进程的文件描述符表,检测未关闭的文件描述符,并将它们关闭,以避免进程内存泄漏。


fdsan in Android


在 Android 上,fdsan(File Descriptor Sanitizer)是自 Android 11 开始引入的一项新功能。fdsan 旨在帮助开发人员诊断和修复 Android 应用程序中的文件描述符泄漏和使用错误。


fdsan 使用 Android Runtime (ART) 虚拟机中的功能来捕获应用程序的文件描述符使用情况。它会跟踪文件描述符的分配和释放,并在文件描述符泄漏或错误使用时发出警告。fdsan 还支持在应用程序崩溃时生成详细的调试信息,以帮助开发人员诊断问题的根本原因。


常见场景



void thread_one() {
int fd = open("/dev/null", O_RDONLY);
close(fd);
close(fd);
}

void thread_two() {
while (true) {
int fd = open("log", O_WRONLY | O_APPEND);
if (write(fd, "foo", 3) != 3) {
err(1, "write failed!");
}
}
}

同时运行上述两个线程,你会发现


thread one                                thread two
open("/dev/null", O_RDONLY) = 123
close(123) = 0
open("log", O_WRONLY | APPEND) = 123
close(123) = 0
write(123, "foo", 3) = -1 (EBADF)
err(1, "write failed!")

断言失败可能是这些错误中最无害的结果:也可能发生静默数据损坏或安全漏洞(例如,当第二个线程正在将用户数据保存到磁盘时,第三个线程进来并打开了一个连接到互联网的套接字)。


检测原理


fdsan 试图通过文件描述符所有权来强制检测或者预防文件描述符管理错误。与大多数内存分配可以通过std::unique_ptr等类型来处理其所有权类似,几乎所有文件描述符都可以与负责关闭它们的唯一所有者相关联。fdsan提供了将文件描述符与所有者相关联的函数;如果有人试图关闭他们不拥有的文件描述符,根据配置,会发出警告或终止进程。


实现这个的方法是提供函数在文件描述符上设置一个64位的关闭标记。标记包括一个8位的类型字节,用于标识所有者的类型(在<android/fdsan.h>中的枚举变量 android_fdsan_owner_type),以及一个56位的值。这个值理想情况下应该是能够唯一标识对象的东西(原生对象的对象地址和Java对象的System.identityHashCode),但是在难以为“所有者”推导出标识符的情况下,即使对于模块中的所有文件描述符都使用相同的值也很有用,因为它会捕捉关闭您的文件描述符的其他代码。


如果已标记标记的文件描述符使用错误的标记或没有标记关闭,我们就知道出了问题,就可以生成诊断信息或终止进程。


在Android Q(11)中,fdsan的全局默认设置为单次警告。可以通过<android/fdsan.h>中的android_fdsan_set_error_level函数在运行时使 fdsan 更加严格或宽松。


fdsan捕捉文件描述符错误的可能性与在您的进程中标记所有者的文件描述符百分比成正比。


常见问题


E fdsan : failed to exchange ownership of file descriptor: fd xxx is owned by ParcelFileDescriptor 0xfffddddd was expected to be unowned

通常情况下,fd 所有权的误用并不会造成闪退,但是由于国内外厂商对 framework 的魔改,目前线上高频出现对应的闪退,为了规避这类情况,我们首先要规范 fd 的使用,特别是所有权的迁移,另外,在操作涉及到 localsocketsharedmemory 时,要慎之又慎,系统会为每个进程记录一份 fd table,会记录每个fd 对应的所有权。如果长时间不释放并且又在不断分配,会出现fd 超限问题,报错提示 cannot open fd


image.png


来看看 java 侧对文件描述符操作的注释



/**
* Create a new ParcelFileDescriptor that is a dup of the existing
* FileDescriptor. This obeys standard POSIX semantics, where the
* new file descriptor shared state such as file position with the
* original file descriptor.
*/
public ParcelFileDescriptor dup() throws IOException {
if (mWrapped != null) {
return mWrapped.dup();
} else {
return dup(getFileDescriptor());
}
}


/**
* Create a new ParcelFileDescriptor from a raw native fd. The new
* ParcelFileDescriptor holds a dup of the original fd passed in here,
* so you must still close that fd as well as the new ParcelFileDescriptor.
*
* @param fd The native fd that the ParcelFileDescriptor should dup.
*
* @return Returns a new ParcelFileDescriptor holding a FileDescriptor
* for a dup of the given fd.
*/
public static ParcelFileDescriptor fromFd(int fd) throws IOException {
final FileDescriptor original = new FileDescriptor();
original.setInt$(fd);

try {
final FileDescriptor dup = new FileDescriptor();
int intfd = Os.fcntlInt(original, (isAtLeastQ() ? F_DUPFD_CLOEXEC : F_DUPFD), 0);
dup.setInt$(intfd);
return new ParcelFileDescriptor(dup);
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
}
}


/**
* Return the native fd int for this ParcelFileDescriptor and detach it from
* the object here. You are now responsible for closing the fd in native
* code.
* <p>
* You should not detach when the original creator of the descriptor is
* expecting a reliable signal through {@link #close()} or
* {@link #closeWithError(String)}.
*
* @see #canDetectErrors()
*/
public int detachFd() {
if (mWrapped != null) {
return mWrapped.detachFd();
} else {
if (mClosed) {
throw new IllegalStateException("Already closed");
}
int fd = IoUtils.acquireRawFd(mFd);
writeCommStatusAndClose(Status.DETACHED, null);
mClosed = true;
mGuard.close();
releaseResources();
return fd;
}

Share两个闪退案例:



  1. fd 超限问题


W zygote64: ashmem_create_region failed for 'indirect ref table': Too many open files

这个时候我们去查看 系统侧对应 fd 情况,可以发现,fd table 中出现了非常多的 socket 且所有者均显示为unix domain socket,很明显是跨进程通信的 socket 未被释放的原因



  1. fd 所有权转移问题


[DEBUG] Read self maps instead! map: 0x0

[]()****#00 pc 00000000000c6144 /apex/com.android.runtime/bin/linker64 (__dl_abort+168)

[]()****#01 pc 00000000000c6114 /apex/com.android.runtime/bin/linker64 (__dl_abort+120)

这个堆栈看得人一头雾水,因为蹦在 linker 里,我们完全不知道发生了什么,但是通过观察我们发现问题日志中都存在如下报错


E fdsan : failed to exchange ownership of file descriptor: fd xxx is owned by ParcelFileDescriptor 0xsssssss was expected to be unowned

根据上述知识,我们有理由怀疑是代码中fd 的操作合法性存在问题,通过细致梳理,我们得出了对应这两类问题的一些action:


所以有以下对应的action:




  • local socket 要及时关闭 connection,避免 fd 超限问题。




  • sharememory 从 进程A 转移到 进程B 时,一定要 detachFd 进行 fd 所有权转移,如果需要在进程 A 内进行缓存,那么 share 给进程B 时需要对 fd 进行 dup 操作后再 detachFd




版本差异


fdsan 在 Android 10 上开始引入,在Android 10 上会持续输出检测结果,在Android 11 及以上,fdsan 检测到错误后会输出错误日志并中止检测,在 Android 9 以下,没有对应的实现。所以,如果你需要在代码中引入fdsan 来进行 fd 校验检测。请参照以下实现:


extern "C" {
void android_fdsan_exchange_owner_tag(int fd,
uint64_t expected_tag,
uint64_t new_tag)
__attribute__((__weak__));
}

void CheckOwnership(uint64_t owner, int fd) {
if (android_fdsan_exchange_owner_tag) {
android_fdsan_exchange_owner_tag(fd, 0, owner);
}
}

作者:faultaddr
链接:https://juejin.cn/post/7225511164121120829
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【直播开发】Android 端实现 WebSocket 通信

前言 在之前的文章中,我们知道了 WebSocket 是一种全双工通信协议。本文将介绍如何在 Android 端使用 WebSocket 进行双向通信。其中包括创建 WebSocket 对象、发送和接收数据等操作。 创建 WebSocket 对象 要使用 We...
继续阅读 »

前言


在之前的文章中,我们知道了 WebSocket 是一种全双工通信协议。本文将介绍如何在 Android 端使用 WebSocket 进行双向通信。其中包括创建 WebSocket 对象、发送和接收数据等操作。


创建 WebSocket 对象


要使用 WebSocket 对象,我们需要先创建一个 WebSocket 客户端对象。在 Android 中,我们可以使用 OkHttp 库来创建 WebSocket 客户端对象。在开始之前,我们先在 Gradle 文件中添加 OkHttp 库的依赖:


implementation 'com.squareup.okhttp3:okhttp:4.9.3'

在代码中创建 WebSocket 客户端对象的示例如下:


import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket

//创建一个 OkHttpClient 对象
val client = OkHttpClient()

//请求体
val request = Request.Builder()
.url("wss://example.com/websocket")
.build()

//通过上面的 client 创建 webSocket
val webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
// WebSocket 连接建立成功
}

override fun onMessage(webSocket: WebSocket, text: String) {
// 收到 WebSocket 服务器发送的文本消息
}

override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
// 收到 WebSocket 服务器发送的二进制消息
}

override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
// WebSocket 连接失败
}

override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
// WebSocket 连接关闭
}
})

在上面的示例中,我们首先创建了一个 OkHttpClient 对象,然后使用 Request.Builder 类构建了一个 WebSocket 请求对象,并指定了 WebSocket 服务器的地址。接着,我们调用 OkHttpClient 对象的 newWebSocket() 方法,传入 WebSocket 请求对象和一个 WebSocketListener 对象,来创建一个 WebSocket 客户端对象。在 WebSocketListener 对象中,我们可以实现 WebSocket 连接建立、收到消息、连接失败、连接关闭等事件的回调函数。


发送和接收数据


WebSocket 客户端对象创建成功后,我们可以通过 send() 方法来向 WebSocket 服务器发送消息。在 WebSocketListener 对象中的 onMessage() 方法中,我们可以接收到 WebSocket 服务器发送的消息。下面是一个发送和接收文本消息的示例:


val message = "Hello, WebSocket!"

webSocket.send(message)

// 在 onMessage() 方法中接收消息
override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "Received message: $text")
}

如果要发送二进制消息,可以使用 send() 同名的另一个重载方法:


val message = ByteString.encodeUtf8("Hello, WebSocket!")

webSocket.send(message)

// 在 onMessage() 方法中接收消息
override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Log.d(TAG, "Received message: ${bytes.utf8()}")
}

关闭 WebSocket 连接


当 WebSocket 连接不再需要时,我们可以调用 WebSocket 对象的 close() 方法来关闭连接,及时释放资源,避免引起内存泄漏。在 WebSocketListener 对象中的 onclose() 方法中,我们可以接收到 WebSocket 关闭事件,可以在该事件中执行一些清理操作。下面是一个关闭 WebSocket 连接的示例:


webSocket.close(NORMAL_CLOSURE_STATUS, "WebSocket connection closed")

// 在 onClosed() 方法中接收关闭事件
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket connection closed: $reason")
}

在上面的示例中,我们调用 WebSocket 对象的 close() 方法来关闭连接,传入一个关闭代码和关闭原因。在 WebSocketListener 对象中的 onClosed() 方法中,我们可以接收到 WebSocket 关闭事件,并处理关闭原因。


完整示例


下面是一个完整的 WebSocket 通信示例,包括创建 WebSocket 对象、发送和接收消息、关闭连接等操作:


import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import okhttp3.*
import okio.ByteString

class MainActivity : AppCompatActivity() {

private lateinit var webSocket: WebSocket

companion object {
private const val TAG = "WebSocketDemo"
private const val NORMAL_CLOSURE_STATUS = 1000
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val client = OkHttpClient()

val request = Request.Builder()
.url("wss://echo.websocket.org")
.build()

webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
webSocket.send("Hello, WebSocket!")
}

override fun onMessage(webSocket: WebSocket, text: String) {
Log.d(TAG, "Received message: $text")
}

override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
Log.d(TAG, "Received message: ${bytes.utf8()}")
}

override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
Log.e(TAG, "WebSocket connection failed", t)
}

override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
Log.d(TAG, "WebSocket connection closed: $reason")
}
})

btn_send.setOnClickListener {
val message = et_message.text.toString()
webSocket.send(message)
}

btn_close.setOnClickListener {
webSocket.close(NORMAL_CLOSURE_STATUS, "WebSocket connection closed")
}
}
}

在上面的示例中,我们在 Activity 的 onCreate() 方法中创建了 WebSocket 客户端对象,并通过按钮的点击事件来发送消息和关闭连接。我们使用了 Echo WebSocket 服务器来测试 WebSocket 通信。在实际开发中,我们可以使用自己的 WebSocket 服务器来进行通信。


总结


WebSocket 是一种全双工通信协议,可以在 Android 应用程序中使用 WebSocket 对象实现双向通信。通过 OkHttp 库,我们可以创建 WebSocket 客户端对象,使用 send() 方法发送消息,使用 WebSocketListener 回调接口处理事件。在实际应用中,我们可以使用 WebSocket 协议来实现实时交互、即时通信等功能,提升 Android 应用程序的用户体验和竞争力。


作者:Quincy_Ye
链接:https://juejin.cn/post/7224015103481200677
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Java、Kotlin不香吗?为什么Flutter要选用Dart作为开发语言?

对于任何想要了解一门新兴技术的开发者来说,语言常常是横亘在学习之路上的第一道障碍,如C/C++之于音视频,Python之于人工智能等,当然也包括Dart之于Flutter。 尤其当你原先从事的是Android开发时,你肯定也曾产生过这样的疑惑: 既然同可以归...
继续阅读 »

对于任何想要了解一门新兴技术的开发者来说,语言常常是横亘在学习之路上的第一道障碍,如C/C++之于音视频,Python之于人工智能等,当然也包括Dart之于Flutter。


尤其当你原先从事的是Android开发时,你肯定也曾产生过这样的疑惑:



既然同可以归到移动开发的范畴,也同属于Google旗下的团队,为什么Flutter不能沿用既有的Java或Kotlin语言来进行开发呢?



通过阅读本文,你的疑惑将得到充分的解答,你不仅能够了解到Flutter团队在选用Dart作为开发语言时的考量,还能充分感受到使用Dart语言进行开发的魅力所在。


照例,先奉上思维导图一张,方便复习:





热重载 (Hot Reload)一直以来都是Flutter对外推广的一大卖点,这是因为,相对于现有的基于原生平台的移动开发流程来讲,热重载在开发效率上确实是一个质的飞跃。



简单讲,热重载允许你在无需重启App的情况下,快速地构建页面、添加功能或修复错误。这个功能很大程度上依赖于Dart语言的一个很突出的特性:


同时支持AOT编译与JIT编译


AOT编译与JIT编译


AOT Compilation(Ahead-of-Time Compilation, 提前编译)是指在程序执行之前,将源代码或中间代码(如Java字节码)转换为可执行的机器码的过程。这么做可以提高程序的执行效率,但也需要更长的编译时间。


JIT Compilation(Just-in-Time Compilation, 即时编译)是指在程序执行期间,将源代码或中间代码转换为可执行的机器码的过程。这么做可以提高程序的灵活性和开发效率,但也会带来一些额外的开销,例如会对程序的初始执行造成一定的延迟。


用比较贴近生活的例子来解释二者之间的区别,就是:



AOT编译就像你在上台演讲之前,把原本全是英文的演讲稿提前翻译成中文,并写在纸上,这样当你上台之后,就可以直接照着译文念出来,而不需要再在现场翻译,演讲过程能更为流畅,但就是要在前期花费更多的时间和精力来准备。




JIT编译就像你在上台演讲之前,不需要做过多的准备,等到上台之后,再在现场将演讲稿上的英文逐句翻译成中文,也可以根据实际情况灵活地调整演讲内容,但就是会增加演讲的难度,遇到语法复杂的句子可能也会有更多的停顿。



可以看到,两种编译方式的应用场景不同,各有优劣,而Dart是为数不多的同时支持这两种编译方式的主流编程语言之一。根据当前所处项目阶段的不同,Dart提供了两种不同的构建模式:开发模式与生产模式。


开发模式与发布模式


在开发模式下,会利用 Dart VM 的 JIT 编译器,在运行时将内核文件转换为机器码,以实现热重载等功能,缩短开发周期。


热重载的流程,可以简单概括为以下几步:




  1. 扫描改动:当我们保存编辑内容或点击热重载按钮时,主机会扫描自上次编译以来的任何有代码改动的文件。




  2. 增量编译:将有代码改动的文件增量编译为内核文件。




  3. 推送更新:将内核文件注入到正在运行的 Dart VM。




  4. 代码合并:使用新的字段和函数更新类。




  5. Widget重建:应用的状态会被保留,并重建 widget 树,以便快速查看更改效果。




而在发布模式下,则会利用 Dart VM 的 AOT 编译器,在运行前将源代码直接转换为机器码,以实现程序的快速启动和更流畅地运行。


这里的“更流畅地运行”指的是在运行时能够更快地响应用户的操作,提供更流畅的用户体验,而不是单指让程序运行得更“快”。


这是因为Dart代码在被转换为机器码后,是可以直接在硬件上运行的,而不需要在运行时进行解释或编译,因此可以减少运行时的开销,提高程序的执行效率。


此外,经 AOT 编译后的代码,会强制执行健全的 Dart 类型系统,并使用快速对象分配和分代垃圾收集器来更好地管理内存。


因此,根据当前所处项目阶段的不同,采用不同的构建模式,Dart语言可以实现两全其美的效果


单线程模型


现如今,几乎所有的智能终端设备都支持多核CPU,为使应用在设备上能有更好的表现,我们常常会启动多个共享内存的线程,来并发执行多个任务。


大多数支持并发运行线程的计算机语言,如我们熟知的Java、Objective-C等,都采用了“抢占”的方式在线程之间进行切换,每个线程都被分配了一个时间片以执行任务,一旦超过了分配的时间,操作系统就会中断当前正在执行的线程,将CPU分配给正在等待队列的下一个线程。


但是,如果是在更新线程共享资源(如内存)期间发生的抢占行为,则可能会引致竞态条件的产生。竞态条件会导致严重的错误,轻则数据丢失,重则应用崩溃,且难以被定位和修复。


修复竞争条件的典型做法就是加锁,但锁本身会导致卡顿,甚至引发死锁等更严重的问题。


那Dart语言又是怎么解决这个问题的呢?


Dart语言采用了名为Isolate的单线程模型,Isolate模型是以操作系统提供的进程和线程等更为底层的原语进行设计的,所以你会发现它既有进程的特征(如:不共享内存),又有线程的特征(如:可处理异步任务)。


正如Isolate这个单词的原意“隔离”一样,在一个Dart应用中,所有的Dart代码都在Isolate内运行,每个Isolate都会有自己的堆内存,从而确保Isolate之间相互隔离,无法互相访问状态。在需要进行通信的场景里,Isolate会使用消息机制。


因为不共享内存,意味着它根本不允许抢占,因此也就无须担心线程的管理以及后台线程的创建等问题。


在一般场景下,我们甚至完全无需关心Isolate,通常一个Dart应用会在主Isolate下执行完所有代码。


虽然是单线程模型,但这并不意味着我们需要以阻塞UI的方式来运行代码,相反,Dart语言提供了包括 async/await 在内的一系列异步工具,可以帮助我们处理大部分的异步任务。关于 async/await 我们后面会有一篇单独的文章讲到,这里先不展开,只需要知道它跟Kotlin的协程有点像就可以了。



如图所示,Dart代码会在readAsString()方法执行非Dart代码时暂停,并在 readAsString()方法返回值后继续执行。


Isolate内部会运行一个消息循环,按照先进先出的模式处理重绘、点击等事件,可以与Android主线程的Looper相对照。



如图所示,在main()方法执行完毕后,事件队列会依次处理每一个事件。


而如果某个同步执行的操作花费了过长的处理时间,可能会导致应用看起来像是失去了响应。



如图所示,由于某个点击事件的同步处理耗时过长,导致其超过了处理两次重绘事件的期望时间间隔,直观的呈现就是界面卡顿。


因此,当我们需要执行消耗CPU的计算密集型工作时,可以将其转移到另外一个Isolate上以避免阻塞事件循环,这样的Isolate我们称之为后台运行对象



如图所示,生成的这个Isolate会执行耗时的计算任务,在结束后退出,并把结果返回。


由于这个Isolate持有自己的内存空间,与主Isolate互相隔离,因此即使阻塞也不会对其他Isolate造成影响。


快速对象分配与分代垃圾回收


在Android中,视图 (View)是构成用户界面的基础块,表示用户可以看到并与之交互的内容。在Flutter中,与之大致对应的概念则是Widget。Widget也是通过多个对象的嵌套组合,来形成一个层次结构关系,共同构建成一棵完整的Widget树。


但两者也不能完全等同。首先,Widget并非视图本身,最终的UI树是由一个个称之为Element的节点构成的;其次,Widget也不会直接绘制任何内容,最终的绘制工作是交由RenderObject完成的。Widget只是一个不可变的临时对象,用于描述在当前状态下视图应该呈现的样子


而所谓的Widget树只是我们描述组件嵌套关系的一种说法,是一种虚拟的结构。但 Element和RenderObject是在运行时实际存在的,如图:



这就好比手机与其规格参数的关系。Widget就像是一台手机的规格参数,是对当前组装成这个手机的真正的硬件配置的描述,当手机的硬件有更新或升级时,重新生成的规格参数也会有所变化。


由于Widget是不可变的,因此,我们无法直接对其更新,而是要通过操作状态来实现。但实际上,当Widget所依赖的状态发生改变时,Flutter框架就会重新创建一棵基于当前最新状态绘制的新的Widget树,对于原先的Widget来说它的生命周期其实已经结束了。


有人可能会对这种抛弃了整棵Widget树并完全重建一棵的做法存有疑问,担心这种行为会导致Flutter频繁创建和销毁大量短暂的Widget对象,给垃圾回收带来了巨大压力,特别对于一些可能由数千个Widget组合而成的复杂页面而言。


实际上这种担心完全没有必要,Dart的快速对象分配与分代垃圾回收足以让它应对这种情况。


快速对象分配


Dart以指针碰撞(Bump Pointer)的形式来完成对象的内存分配。


指针碰撞是指在堆内存中,Dart VM使用一个指针来跟踪下一个可用的内存位置。当需要分配新的内存时,Dart VM会将指针向前移动所需内存大小的距离,从而分配出新的内存空间



这种方式可以快速地分配内存,而不需要查找可用的内存段,并且使内存增长始终保持线性


另外,前面我们提到,由于每个Isolate都有自己的堆内存,彼此隔离,无法互相访问状态,因此可以实现无锁的快速分配。


分代垃圾回收


Dart的垃圾回收器是分代的,主要分为新生代(New Generation)与老年代(Old Generation)。


新生代用于分配生命周期较短的临时对象。其所在的内存空间会被分为两半,一个处于活跃状态,另一个处于非活跃状态,并且任何时候都只使用其中的一半。



新的对象会被分配到活跃的那一半,一旦被填满,垃圾回收器就会从根对象开始,查找所有对象的引用状态。




被引用到的对象会被标记为存活状态,并从活跃的一半复制到非活跃的一半。而没有被引用到的对象会被标记为死亡状态,并在随后的垃圾回收事件中被清除。



最后,这两半内存空间会交换活跃状态,非活跃的一半会再次变成活跃的一半,并且继续重复以上过程。



当对象达到一定的生命周期后,它们会被提升为老年代。此时的垃圾回收策略会分为两个阶段:标记与清除。


首先,在标记阶段,会遍历整个对象图,标记仍在使用的对象。


随后,在清除阶段,会扫描整个内存,回收任何没有被标记的对象,然后清除所有标记。


这种形式的垃圾回收发生频率不高,但有时需要暂停Dart Runtime以支持其运行。


为了最小化地降低垃圾回收事件对于应用程序的影响,垃圾回收器为Flutter引擎提供了钩子,当引擎检测到应用程序处于空闲状态并且没有用户交互时会发出通知,使得垃圾回收器可以在不影响性能的情况下执行回收工作。


另外,同样由于每个Isolate都在自己都独立线程内运行,因此每个Isolate的垃圾回收事件不会影响到其他Isolate的性能。


综上可知,Flutter框架所采用的工作流程,很大程度上依赖于其下层的内存分配器和垃圾回收器对于小型的、短生命周期的对象高效的内存分配和回收,缺少这个机制的语言是无法有效运作的


学习成本低


对于想要转岗Flutter的Android或iOS开发者,Dart语言是很友好的,其语法与Kotlin、Swift等语言都存在一些相似之处。


例如,它们都是面向对象的语言,都支持类、接口、继承、抽象类等概念。绝大多数开发者都拥有面向对象开发的经验,因此可以以极低的学习成本学习Dart语言。


此外,Dart语言也拥有着许多与其他语言相似的优秀的语法特性,可以提高开发人员的生产力,例如:




  • 字符串插值:可以直接在字符串中嵌入变量或表达式,而不需要使用+号相连:
    var name = 'Bob'; print('Hello, $name!');




  • 初始化形式参数:可以在构造函数中直接初始化类的属性,而不需要在函数体中赋值:
    class Point { num x, y; Point(this.x, this.y); }




  • 函数式编程风格:可以利用高阶函数、匿名函数、箭头函数等特性简化代码的结构和逻辑:
    var numbers = [1, 2, 3]; var doubled = numbers.map((n) => n * 2);




Dart团队配合度高


拥有一定工作年限的Android开发者,对于早些年Oracle与Google两家科技公司的Java API版权之争可能还有些许印象。


简单讲就是,Oracle认为Google在Android系统中对Java API的复制使用侵犯了其版权和专利权,这场持续了11年的专利纠纷最终以Google的胜利结束。


相比之下,Dart语言与Flutter之间则没有那么多狗血撕逼的剧情,相反,Flutter与Dart社区展开了密切合作,Dart社区积极投入资源改进Dart语言,以便在Flutter中更易使用。


例如,Flutter在最开始采用Dart语言时,还没有用于生成原生二进制文件的AOT工具链,但在Dart团队为Flutter构建了这些工具后,这个缺失已经不复存在了。


结语


以上,就是我汇总Flutter官网资料及Flutter社区推荐博文的说法之后,总结出的Flutter选用Dart作为开发语言的几大主要原因,希望对于刚入门或想要初步了解Flutter开发的小伙伴们有所帮助。


引用



作者:星际码仔
链接:https://juejin.cn/post/7225629551602565178
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »