LuckyJing

LuckyJing 查看完整档案

杭州编辑  |  填写毕业院校阿里云  |  前端开发工程师 编辑 www.luckyjing.com/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

LuckyJing 赞了文章 · 2018-03-20

我的阿里之路+Java面经考点

我的2017是忙碌的一年,从年初备战实习春招,年三十都在死磕JDK源码,三月份经历了阿里五次面试,四月顺利收到实习offer。然后五月怀着忐忑的心情开始了蚂蚁金服的实习。八月,又经历了两轮面试,总算拿到转正offer。到此为止,我总算可以尽情地享受最后的校园时光了。
希望我的经历与感悟能帮助到大家。

我的读研经历

阿里是我读研阶段的一个重要目标,刚上研一的时候我便立下Flag,计划了读研三年要做的事情、要学的东西。翻翻我的博客,居然还能找到当时写的读研计划(想想时间过的真的好快呀)。阿里在我心中一直是技术者的朝圣地,聚集了一群技术狂热分子,为了提升一点点的性能可以废寝忘食。在立下这个flag后,我便把我要去阿里的想法告诉了身边的所有人。我不想给自己留有后路,既然牛逼吹出去了,那就得落实到行动上。

我当时的计划是这样的,研一玩命做项目,毕竟写代码是一项孰能生巧的技术,多写代码自然能加深对技术的理解。事实也确实证明了这一点。我大四毕业的那个暑假,第一次接触《深入理解JVM虚拟机》,当时看完之后是一脸懵逼的,所有的汉字都认识,但连成一条句子后咋就不明白呢?然后怀着这种一脸懵逼的状态,强行把这本书看完了,但由于理解的不够深刻,很快就忘记了。然后经过一年疯狂做项目,当我再抱起这本书时,我对这本书的感觉有了180度的转变,通俗易懂啊。我怀疑自己当时为什么这么傻。而在这一年之内,我一共做了四个项目。所以说,写代码能潜移默化地提升你对技术的理解程度。

记得我们学校考研结果公布是在三月底,得知我录取了,短暂的高兴了一下,第二天便去了导师实验室,参与到项目中去。因为我心理很清楚,我的目标是阿里。
刚去实验室的时候感觉自己完全是一只小白,师兄们开会讨论的东西我竟然一个字都听不懂,刚开始分到的工作也很难顺利的完成。但我是一个天生不服的人,我越是不懂,我越是要征服。(PS:追女生也一样,当时看上了一个高冷型女神。越是高冷,我越要拿下……省略一万字……最后成功了)。我渐渐跟上了师兄们的步伐,从第二个项目开始,我一直是项目负责人了。
我给自己定的方向是Java,而实验室的项目五花八门,Qt、C++、图像图形……,但我心里清楚,这些项目可以拓宽我的视野,可以锻炼我的学习能力、思维方式,但要深入理解的技术还是Java。所以我利用项目空窗期,看了很多Java方面的书籍,文末我列了一份清单供大家参考。

研一做了一年项目之后,感觉自己解决问题的能力、快速上手新技术的能力有所提升,但做项目一味求快、一味赶时间,我对很多技术的理解仅停留表面。所以更多时候我感觉自己是在搬砖,很难写出高质量的代码。当时我决定,研二的上半学期一定要好好稳固基础,深入理解技术背后的原理,放慢做项目的节奏,不一味地盲目求快。

然而研二因为种种原因,我去了一家创业公司实习。既然我无法选择,那我就拥抱变化。既来之则安之,我希望自己所花的时间都有所收获。所以我全身心投入到工作中去。公司安排我做前端,虽然这并不是我想干的事情,但当前别无选择,既然做那就得做好。我花了一个月的时间怒刷前端知识,学习Angular、React,了解它们的设计思想,学习JS背后的原理,也看到了前端目前的发展状况。前端的技术迭代速度很快,但技术背后的原理都是类似的,所以抓住技术背后的本质才是最重要的。
这段实习一直持续到了今年一月,我心理清楚,二月底三月初阿里的实习招聘就要开始,所以我挤出中午休息时间、晚上下班后的时间、周末的时间,复习我之前积累的东西。

三月三号,人生的第一次面试,在看到杭州的电话时,那一刻的心情既忐忑又兴奋。在此之前,我并不清楚自己的水平在所有应届生中处于一个怎样的层次,在电话接通之前我已经做好了充分的心理准备,想象了各种被面试官吊打的画面。第一次面试持续了54分钟,面试官似乎破有耐心,整个面试过程相谈甚欢。那一次的面试给了我巨大的信心,因为我清楚,我离我的目标更近了一步。
在接下来的一个月中,我陆陆续续接到了五次面试电话,每一次面试都是一次学习的机会,能发现自己的不足,在与面试官交流的过程中也能了解到最新的技术、最佳的实现方法。

四月十四,记得那天刚替导师给本科生上完算法课,晚饭过后便收到的阿里的offer。当时的喜悦之情溢于言表,这么久的努力没有白费。但我心里也清楚,阿里的实习转正率向来很低,要想通过实习留下来也不是一件容易的事。所以在收到阿里offer之后的那晚,我重新规划了接下来的学习计划,将我的短期目标更新成:拿下阿里转正offer。

5.20,一个美好的日子,我怀着憧憬、忐忑、兴奋的心情走进了支付宝大楼,开始了我三个月的实习生涯。这三个月的实习让我学到了很多,我看到了每天的进步,也看到了自己的不足。这100天经历了太多的事情,有太多的体会,有空我再单独写一篇实习期间的感悟与大家分享。

9.1晚上11点,我的状态从『面试中』变成了『待跟进offer』,我会心一笑,我达成了人生第一阶段的目标。那一刻我没有太多的激动,在经过了2017年8个月的持久作战之后,这个offer早已在我的意料之中。

第二天是周六,这个周末我给自己放了两天的假。虽然在杭州已经呆了102天,虽然支付宝大楼就在西湖边,但每天两点一线的工作,我并没有去过西湖。那一晚我的基友得知我收到offer后便立即买票,坐了一夜的火车来杭州为我庆祝。也托他的福,在这个周末我第一次游览了这座美丽的城市。

最后的校园时光,我给自己定了一个新的目标,这个目标也许充满了挑战、充满了艰辛与困难,但我想如果能达成,我的人生将会有很大的改变。在人生的路上,我还在奋斗。希望与各位共勉。

一些建议

在面试阿里的过程中,博客帮了我很大的忙。并不是说有博客在面试中会加分,而是写博客的过程能加深你对知识的理解,而且回顾起来也特别高效(毕竟是自己写的东西)。

我从研一开始便养成了写博客的习惯,现在先过头来,这两年我一共写了185篇文章,有22W的访问量,也很荣幸称为专家博主。我的博客现在已经成为我一笔最宝贵的财富,记录了我技术的整个体系结构,也记录了学习技术的心路历程。

可能很多同学都有写博客的经历,但能长期坚持的少之又少。我想告诉大家,任何事情都贵在坚持。只要坚持21天就能养成习惯。有的时候对这件事缺乏兴趣了很难再持续下去,但想想自己的目标,咬咬牙坚持下去,养成习惯你就赢了。

我的博客一般都是看完一本书后的总结,我会把一本书中最核心的东西,按照我的理解把它们记录下来。每一本书我都会看三遍,第一遍是快速浏览,了解整本书的体系结构、作者的行文思路,知道书中重要的、不重要的内容,也就是要建立起全局观。第二遍我会精读,把那些我认为重要的内容挑选出来着重阅读、反复理解、吃透。第三遍阅读,往往会冒出新的理解,我时常有这种『书读百遍,其义自见』的感觉。三遍读完后,我便按照自己的理解,把核心内容写成博客。由于是自己的写的东西,所以在复习的时候只要花上三五分钟通读一遍,所有的知识点又被唤醒。


Java面试考点梳理

本文是根据我的面试经验,为大家整理Java程序猿面试所需的知识体系。

第一部分:计算机基础

互联网大厂都相当重视程序员的基本功,也就是计算机基础知识。一个程序员能走多远、爬多高,很大程度上取决于基本功是否扎实。对于应届生而言,大都比较缺乏实战项目经验,虽然会有一定的项目经验,但这些课程设计、实验室项目的质量与公司实际的项目有着巨大的差距。因此,基础知识便成为面试考量的一大重点,而且基础扎实的程序员可塑性比较高,做什么都能比较容易快速上手。

计算机基础包含如下几门课程,相信计算机专业的同学肯定都已经学过。但互联网公司面试的考点可能和你们期末考试的考点有一些不同,我都做了整理。

1. 计算机网络

大学课程中的计算机网络一般都按照OSI七层参考模型介绍,然而由于互联网公司的特性,他们更加关注日常开发所涉及到的传输层和应用层,所以需要重点掌握传输层和应用层中所涉及到的所有知识点。

【考点】

  • 传输层的作用
  • 传输层复用和分用的含义
  • 传输层和网络层的区别
  • UDP协议的特点
  • UDP协议的报文结构
  • TCP协议的特点
  • TCP协议的报文结构
  • TCP三次握手过程
  • TCP四次挥手过程
  • TCP可靠传输是如何实现的
  • 停止等待协议
  • 滑动窗口协议
  • TCP的流量控制
  • TCP拥塞控制
  • HTTP协议

    • HTTP工作流程
    • HTTP请求格式
    • HTTP 1.1中的8种请求方式
    • HTTP响应格式
    • HTTP中重要的请求头和响应头字段
    • HTTP常用状态码及其含义
  • HTTPS协议

    • HTTPS协议与HTTP协议的区别
    • HTTPS协议的工作流程

【资料整理】

2. 数据结构

熟练掌握数据结构是程序员最最最基本的素养,在实际开发中选择合适的数据结构将极大影响程序的效率。面试官一般并不会直接问数据结构的问题,而是通过出一些包含数据结构的算法题来考察你对数据结构的理解程度以及在实际项目中是否能够灵活应用。你可以通过刷算法题来提升这部分能力,推荐《剑指offer》和《程序员面试金典》(注意是金典!)。很多公司的算法题库都选自这两本书。

当然,刷这两本书的目的并不是让你死记硬背题目,题目千变万化,面试官可以随意改变。刷算法题最重要的是培养解决问题的思路和解决实际问题的能力。在刷题的过程中要多多总结,再次强调,切忌死记硬背!

3. 算法

和数据结构一样,算法一般也通过具体的算法题来考察,你也可以通过刷《剑指offer》和《程序员面试金典》中的算法题来提高这方面的技能。但在刷这些算法题之前,你需要掌握几类基础的算法,并理解他们解决问题的思路(这才是最为关键的)。这些算法我已经在下面整理。

4. 操作系统

【考点】

  • 操作系统的四个特性。
  • 操作系统的主要功能。
  • 进程的有哪几种状态,状态转换图,及导致转换的事件。
  • 进程与线程的区别。
  • 进程通信的几种方式。
  • 进程同步的几种方式
  • 用户态和核心态的区别。
  • 死锁的概念,导致死锁的原因。
  • 导致死锁的四个必要条件。
  • 处理死锁的四个方式。
  • 预防死锁的方法、避免死锁的方法。
  • 进程调度算法。
  • 内存连续分配方式采用的几种算法及各自优劣。
  • 基本分页储存管理方式。
  • 基本分段储存管理方式。
  • 分段分页方式的比较各自优缺点。
  • 几种页面置换算法,会算所需换页数
  • 虚拟内存的定义及实现方式。

【资料整理】

5. 数据库

【考点】

  • 什么是索引?
  • 索引的分类
  • 索引的优缺点分析
  • 何时需要使用索引?何时无需使用索引?
  • 什么是事务?
  • 事务的四大特性
  • 数据库三大范式
  • 数据库有哪些表连接?

【资料整理】

第二部分:Java

作为一名合格的Java程序员,仅了解如何使用Java是远远不够的。你能够熟练使用Java只能说明你已经成为一名合格的码农,能够利用Java实现某些功能。而公司作为盈利机构,需要用最少的资源实现效益最大化,这就需要程序员具备高质量代码的能力,而能否写出高质量代码取决于你对技术背后原理的理解程度。只有在理解Java背后的原理,你才能根据Java的特性,写出更加高效的代码。这在实际业务中是非常有价值的事情。互联网大厂服务海量用户,更加注重系统的性能,也更加注重程序员对原理的理解。

关于Java的基础知识和如何使用,这里我就不提了,随便一本Java书籍都有详细的介绍。这里我整理了Java原理性的知识点,这些知识点将会成为你面试的加分项。

1. Java虚拟机

【考点】

  • Java虚拟机内存模型特点和作用

    • 程序计数器
    • Java虚拟机栈
    • 本地方法区
    • 方法区
  • 对象创建过程
  • 对象访问过程
  • 对象的内存结构
  • 垃圾收集算法
  • 如何判定哪些对象需要回收?
  • 对象内存分配策略
  • 分配担保机制
  • 垃圾收集器的比较
  • Class文件结构
  • 类加载的时机
  • 类加载过程
  • 双亲委派模型

【知识点资源整理】

2. Java并发编程

【考点】

  • 什么是死锁?如何避免死锁?
  • 什么是重排序?
  • volatile有哪些特性?
  • 什么是内存可见性?
  • volatile为什么能够保证内存可见性?
  • 中断机制
  • 线程通信有哪些方式?
  • 线程池的作用?
  • ThreadPoolExecutor如何使用?
  • 如何设置线程池的大小?
  • 如何保证线程安全?
  • JDK 1.6哪些对锁做了哪些优化?

【知识点资源整理】

3. Java 容器考点及资料整理

4. Java IO 考点及资料整理

5. Java其他知识点汇总

查看原文

赞 131 收藏 440 评论 11

LuckyJing 关注了用户 · 2018-03-15

JSCON简时空 @jscon

个人博客:http://boycgit.github.io/

微博:http://weibo.com/271111536

微信公众号:JSCON简时空 (微信号: iJSCON)

everything is never too later,neither too early

关注 166

LuckyJing 赞了文章 · 2018-03-15

【用故事解读 MobX源码(一)】 autorun

================前言===================

=======================================

A. Story Time

1、 场景


场景
一位名为 张三 的银行用户账户情况为:

  • 账户存款为 3(万元)
  • 信用借贷为 2(万元)

你作为警署最高长官,在一起金融犯罪中认定 张三 为金融犯罪嫌疑犯,想自动化跟踪这位用户的储蓄情况,比如他一旦银行存款有变更就打印出他当前的账户存款


为了实现这个任务,你下发命令给你的执行官(MobX):

var bankUser = mobx.observable({
  name: '张三',
  income: 3,
  debit: 2
});

mobx.autorun(() => {
  console.log('张三的账户存款:', bankUser.income);
});

start

执行官拿着这几行代码开始部署警力来完成你下发的指令,并将这次行动命名为 A计划 (是不是很酷???)。你所要做的,就是等执行官 MobX 执行行动部署完毕之后,坐在办公室里一边惬意地喝着咖啡,一边在电脑上观察张三账户的存款变化。

执行官部署完毕后,首先会立即打印出 张三的账户存款: 3

income

后续张三的账户存款有更改的话,会 自动执行该部署方案,控制台里就自动打印其存款;

// 更改账户存款
bankUser.income = 4;
bankUser.income = 10;

autorun

income

是不是很神奇很自动化?

2、 部署方案

作为警署最高长官,你不必事必躬亲过问执行官(MobX)部署的细节,只要等着要结果就可以。

而作为执行官(MobX),你得知道 A计划 中部署方案的每一步细节。下面我们来一探究竟执行官 MobX 到底是如何部署 A计划 的。

2.1、 组织架构

执行官(MobX) 拥有一套成熟的运作机构组织支撑任务的执行。为了执行这项任务,涉及到 2 类职员和 1 个数据情报室:

  • 观察员:其工作职责是观察并监督嫌疑人特定信息,比如这里,监视张三的收入(income)属性,当这项特征有变更的时候,及时向上级汇报(并执行特定的操作);

observer

  • 探长:一方面负责管理划归给他的 观察员,整合观察员反馈的资讯;另一方面接受 MobX 执行官交给他的任务,在 适当的时机 执行这项任务(此任务是打印张三的存款);

tanzhang

  • 此外还会架设一个 数据情报室,方便执行官 MobX、探长和观察员们 互相通过情报室进行数据信息的交换。

room

具体组织架构关系图如下:

structor

按照组织架构,执行官 MobX 分解计划细节并安排人员如下:

1.明确此次任务是 当张三账户存款变更时,打印其存款

() => {
  console.log('张三的账户存款:', bankUser.income);
}

2.将任务指派给执行组中的探长 R1
3.派遣观察组中的观察员 O1 监察张三账户的 bankUser.income 属性
4.探长 R1 任务中所需的“张三的账户存款” 数值必须从观察员 O1 那儿获取;
5.同时架设数据情报室,方便信息交换;

2.2、 部署细节

人员安排完毕,执行官拿出一份 部署方案书,一声令下 “各位就按这套方案执行任务吧!”;

在部署方案中下达之后,机构各组成员各司其职,开始有条不紊地开始运作,具体操作时序图如下所示:

detail

对时序图中的关键点做一下解释:

  1. 执行官 MobX 先将探长 R1 信息注册到中心情报室;(有些情况下,比如侦破大案要案时需要多位探长协作,将存在多位探长同时待命的情况;当然此次任务中,只有探长 R1 在待命)

    1. 中心情报室给执行官 MobX 返回所有待命探长列表(此例中仅有探长 R1);
    2. 执行官 MobX 挨个让每位待命探长按以下步骤操作:

      • 3.1. 探长出发做任务时,记得给中心情报室通告一声,好比上班“打卡”操作。(记录事务序号,好让中心情报室知晓案情复杂度;有些案件中将有很多探长同时执行任务)
      • 3.2 探长 R1 开始监督并执行 MobX 交给他的任务(“打印张三的存款”)
      • 3.3 首先在数据情报室中“注册”,将自己设置成 正在执勤人员;(这一步很重要)
      • 3.4 随后真正进入执行任务的状态
      • 3.5 在执行任务的时候,发现需要张三的存款(income)这个数值,可这个数值探长 R1 不能直接获取,必须通过观察员 O1 取得,于是通过专用通讯机和观察员 O1 取得联系,请求获取要张三的存款(income
      • 3.6 观察员 O1 发现有人通过专用通讯机请求张三的存款(income),就开始如下操作:

        • 3.6.1 将自己的信息 经过 数据情报室,然后传达给请求方;只有上级(不一定是探长,有可能是其他的上级领导)才能通过这个专用通讯机发消息给观察员;
        • 3.6.2 数据情报室 将该信息同步给 正在执勤人员 —— 即探长 R1
        • 3.6.3 同时将张三的存款(income)返回给请求方;(该消息不用经过 数据情报室
      • 3.7 此时探长拥有两份信息:任务所需要的张三的存款(income),以及观察员 O1 的相关信息;因时间紧,执行任务优先级高,探长 R1 先拿着张三的存款(income)数据,先做完任务。(至于观察员 O1 的信息 先临时保存 ,方便后续处理);
      • 3.8 等到任务执行完了,探长松了一口气,汇总并整理临时保存的观察员信息;在这一阶段,探长 R1 才和 观察员 O1 互相建立牢固的关系(可以理解为,互留联系方式,可随时联系得上对方),也是在这个时候,观察员 O1 才知晓其上级领导是探长 01
      • 3.9 此后,探长 R1 发消息告知中心情报室,削去此次事务(说明事务执行完了),好比下班 “打卡”操作。
    3. 至此 A 计划部署完毕

上述时序图中有些地方需要强调一下:

  1. 张三的存款(income)只归观察员 O1 监视,探长 R1 所获取的张三的存款只能通过观察员 O1 间接获取到,探长不能越权去直接获取;
  2. 探长 R1 和观察员 O1 建立关系并非一步到位,是 分两个阶段 的:

    • 第一阶段(对应上述步骤中 3.6.2)是在执行任务期间,仅仅是建立短暂的 单向关系即,此时探长 R1 知晓观察员 O1 的情况,但反过来,但观察员 O1 并不知晓探长 R1 ;
    • 第二阶段(对应上述步骤中 3.8)在任务执行完,收尾阶段的时候,探长才有时间梳理、整合任务期间的探员信息(因为任务中涉及到有可能多个观察员,当然此次任务中只有 1 个),那时候才有时间 慢慢地 和各位探员互相交换信息,建立 明确且牢固 的关系;

2.3、 任务执行自动化

作为警署最高长官的你,拿着这份部署方案,眉头紧锁:“我说执行官 ,就为了区区获取张三的存款这么件事儿,耗费那么多人力资源,值么?直接获取 bankUser.income 不就行了?!”

“emm...,这所做的努力,图的是普适性和 自动化响应。”执行官 MobX 淡然自如,不紧不慢徐徐道来,“有了上面那套机制,一方面每当张三的存款变更后,就会 自动化执行上述部署方案的过程;另一方面很方便扩展,后续针对其他监察,只需要在此部署方案中稍加改动就可以,所需要的高级功能都是基于这个方案做延伸。真正做到了 ’部署一次,全自动化执行‘ 的目的。“

随后,执行官 MobX 给出一张当张三存款发生变化之时,此机构的运作时序图;

auto

"的确,小机构靠人力运作,大机构才靠制度运转。那就先试运行这份部署计划,看它能否经受得起时间的考验吧。" 警署最高长官拍拍执行官 MobX 的肩膀,若有所思地踱步出了办公室。

(此节完。未完待续)

B. Source Code Time

上面讲那么久的故事,是为了给讲源码做铺垫。

接下来将会贴 MobX 源码相关的代码,稍显枯燥,我只能尽量用通俗易懂的话来分析讲解。

先罗列本文故事中人物与 MobX 源码概念映射关系:

故事人物MobX 源码解释
警署最高长官(无)MobX 用户,没错,就是你
执行官 MobXMobX整个 MobX 运行环境
A计划autorun官方文档 -mobx.autorun方法
探长reaction官方文档 - Reaction 响应对象
观察员observable官方文档 - Observable 对象
数据情报室globalstateMobX 运行环境中的 ”全局变量“,不同对象通过它进行数据传递通信,十分重要;(但这其实在一定程度上破坏了内聚性,给源码阅读、程序 debug 造成一定的难度)

reflect

本文的重点是讲解 A 计划所对应的 autorun 的源码,先从整体上对 MobX 的运行有个大致了解,而所涉及到的 ReactionObservable 等细节概念后续章节再做展开,这里仅仅大致提及其部分功能和属性;

1、下达的命令

回到故事的最开始,你给 MobX 下达的命令如下:

var bankUser = mobx.observable({
  name: '张三',
  income: 3,
  debit: 2
});

mobx.autorun(() => {
  console.log('张三的账户存款:', bankUser.income);
});

只有两条语句,第一条语句是创建观察员,第二条语句是执行 A 计划(内含委派探长、架设情报局等工作)

我们挨个细分讲解。

1.1、第一条语句:创建观察员 - Observable

第一条语句:

const bankUser = mobx.observable({
  name: '张三',
  income: 3,
  debit: 2
})

我们调用 mobx.observable 的时候,就创建了 Observable 对象,对象的所有属性都将被拷贝至一个克隆对象并将克隆对象转变成可观察的。

因此这一行代码执行后, nameincomedebit 这三个属性都变成可观察的;

若以故事场景来叙述中,执行官 MobX 在部署的时候委派了 3 位探员,分别监视这 3 个属性;而故事中交给探长任务中仅仅涉及了那位监视 income 属性的观察员 O1;(所以另外两位探员都还在休息)

在这里可以看到 惰性求值 的思想的应用,只有在 必要的时候 启用 所观察对象,粒度细,有利于性能提升;

o1

之所以只有 1 位观察员,是因为由于上级下达的具体任务内容是:

() => {
  console.log('张三的账户存款:', bankUser.income);
}

看看任务中出现 bankUser.income 而并没有出现 bankUser.debitbankUser.name,说明这个任务只 牵连 探员O1,而和其他探员无关。

注:本文暂时先不分析 mobx.observable 的源码,留待后续专门的一章来分析;迫不及待的读者,可以先阅读网上其他源码文章,比如:Mobx 源码解读(二) Observable

观察员有两个非常重要的行为特征:

  • 当有人请求观察员所监控的值(比如income)时,会触发 MobX 所提供的 reportObserved 方法;
  • 当观察员所监控的值(比如income)发生变化时,会触发 MobX 所提供的 propagateChanged 方法;

这里留一个印象,本文后续在适当的时机再讲解这两个方法是在什么时候触发的;

1.2、第二条语句:A 计划的实施 - autorun

第二条语句:

mobx.autorun(() => {
  console.log('张三的账户存款:', bankUser.income);
});

这里出现的 MobX 中的 mobx.autorun 方法对应故事中的 整个A计划的实施

autorun

autorun 的直观含义就是 响应式函数 —— 响应观察值的变化而自动执行指定的函数。

我们看一下其源码:

附源码位置:autorun

autorun

从这里可以看出 autorun 大致的脉络如下:

首先创建 Reaction 类型对象new Reaction 操作可以理解为创建探长 R1 ;

探长对应的类是 Reaction,其关键特征是 监督并控制任务的执行

reaction

本文的下一节将详细介绍探长们的 "生活日常",此处先放一放。

其次分配任务。源码中所涉及到的 view() 方法 就是具体的任务内容,即上述故事中的 打印张三账户存款 这项任务:

() => {
  console.log('张三的账户存款:', bankUser.income);
}

③ 最后,立即执行一次部署方案

代码中的 reaction.schedule() 表示让探长 R1 立即执行执行一次部署任务,执行的结果是完成人员部署,并让探长 R1 打印了一次张三账户存款;(同时和观察员 O1 建立关系)

现在你应该会理解官方文档中的那句 ”使用 autorun 时,所提供的函数总是立即被触发一次“ 话了。

clipboard.png

看一下 schedule 方法:

clipboard.png

看上去很简单,不到 5 行代码做了两件事情:
① 将探长入列;
② 让队列中的 所有探长(当然,在我们的示例中仅仅只有 1 名探长)都执行 runReaction 方法

对应时序图中所标注的 1、2 两部分:

clipboard.png

所谓的 部署(schedule) 就是敦促 各位探长执行 runReaction 方法

第二条语句从整体上看就这样了。

接下来就让我们来详细分析探长的 runReaction 的方法,在该方法中 探长将联动观察员、数据情报室一起在部署方案中发挥监督、自动化响应功能

2、每位探长的生活日常

任务的执行全靠探长,不过探长的存在常常是 依赖观察员 的,这是因为在任务过程中,如果想要获取所监视的张三的存款(income),必须通过观察员获取,自身是没有权力绕过观察员直接获取的哦

每位探长的任务执行流大致如下:

clipboard.png
主流程大致只有 4 步:
① 开始执行(runReaction
② 判断是否执行(shouldCompute
③ 执行任务(onInvalidate)
④ 结束

这些基就是每位探长的生活的总体了。下面我们挑其中的第 ① 、 ③ 步来讲解。

其实图中另外有一个很重要的 shouldCompute 判断方法步骤,根据这个方法探长可以自行判断 是否执行任务,并非所有的任务都需要执行,这一步的作用是优化 MobX 执行效率。该方法源码内容先略过,后续章节再展开。

2.1、开始执行 - runReaction

clipboard.png

该函数比较简单,主要是为执行任务 ”做一些准备“,给任务营造氛围。用 startBatch() 开头,用 endBatch() 结尾,中间隔着 onInvalidate

startBatch()endBatch() 这两个方法一定是成对出现,用于影响 globalStateinBatch 属性,表明开启/关闭 一层新的事务,可以理解为 上下班打卡 操作。

只不过 startBatch() 是 ”上班打卡“,对应时序图(3.1) 部分:

clipboard.png

endBatch() 相当于 “下班打卡”,不过稍微复杂一些,包含一些 收尾 操作,对应时序图(3.9)部分:
clipboard.png

我们继续看隔在中间的 onInvalidate 方法。?

2.2、执行任务 - onInvalidate

此阶段是流程中最重要的阶段。

你翻看源码,将会发现此方法 onInvalidate 是 Reaction 类的一个属性,且在初始化 Reaction 时传入到构造函数中的,这样做的目的是方便做扩展。

所以,autorun 方法本质就是一种预定义好的 Reaction —— 你可以依葫芦画瓢,将自定义 onInvalidate 方法传给 Reaction 来实现自己的 计划任务(什么 Z计划啊、阿波罗计划啊,名字都起好了,就差实现了!!....);

回过头来,在刚才所述的 autorun 源码中找到 Reaction 类初始化部分:

const reaction = new Reaction(name, function() {
    this.track(reactionRunner)
})

可以看到 onInvalidate 方法就是:

function() {
    this.track(reactionRunner)
}

这就不难理解 onInvalidate 实际执行的是 reaction.track 方法。

继续跟踪源码,会发现该 onInvalidate 阶段主要是由 3 个很重要的子流程所构成:

这 3 个函数并非是并行关系,而是嵌套关系,后者是嵌套在前者内执行的:

clipboard.png

题外话:是不是很像 Koa 的 洋葱圈模型 ??

2.2.1、track

track

track 方法内容也简单,和刚才所说的 runReaction 方法类似 —— 也是用 startBatch() 开头,用 endBatch() 结尾,中间隔着 trackDerivedFunction

所以在这个案例中,整个部署阶段是执行 两次startBatch()endBatch() 的;在往后复杂的操作中,执行的次数有可能更多。

我们都知道数据库中的事务概念,其表示一组原子性的操作。Mobx 则借鉴了 事务 这个概念,它实现比较简单,就是通过 成对 使用 startBatchendBatch 来开始和结束一个事务,用于批量处理 Reaction 的执行,避免不必要的重新计算。

因此到目前这一步,MobX 程序正处在 第二层 事务中。

clipboard.png

MobX 暴露了 transaction 这一底层 API 供用户调用,让用户能够实现一些较为高级的应用,具体可参考 官方文档 - Transaction(事务) 章节获取更多信息。

接下来继续看隔在中间的 trackDerivedFunction 方法。?

2.2.2、trackDerivedFunction

clipboard.png

我们总算到了探长 真正执行任务 的步骤了,之前讲的所有流程都是为了这个函数服务的。

该环节的第 1 条语句:

globalState.trackingDerivation = derivation;

对应时序图(3.3):

clipboard.png

作用是将 derivation (此处等同于 reaction 对象)挂载到 ”全局变量“ globalStatetrackingDerivation 属性上,这样其他对象就能获取到该 derivation 对象的数据了。这好比将探长在数据情报室中注册为 正在执勤人员,后续观察员 O1 会向数据情报室索取 正在执勤人员 人,然后将自身信息输送给他 —— 从结果上看,就相当于 观察员 O1 直接和 探长 R1 汇报;(之所以要经由数据情报室,是因为在执行任务时候,有可能其他工种的人也需要 正在执勤人员 的信息)

该环节的第 2 条语句:

result = f.call(context); // 效果等同于 result = console.log('张三的账户存款:', bankUser.income);

对应时序图(3.4):

clipboard.png

没错,就是本次部署的 终极目的 —— 打印张三账户存款!

MobX 将真正的目的执行之前里三层外三层地包裹其他操作,是为了将任务的运行情况控制在自己营造的环境氛围中。为什么这么做呢?

这么做是基于一个前提,该前提是:所运行的任务 MobX 它无法控制(警署长官今天下达 A 命令,明天下达 B 命令,控制不了)

所以 MobX 就将任务的执行笼罩在自己所营造的氛围中,改变不了任务实体,我改变环境总行了吧?!!

由于环境是自己营造的,MobX 可以为所欲为,在环境中穿插各种因素:探长、观察员、数据情报室等等(后续还有其他角色),这样就将任务的运行尽最大可能地控制在这套所创造的体系中 —— 孙猴子不也翻不出如来佛的五指山么?

虽然更改不了任务内容,不过 MobX 实际在任务中安插观察员 O1 了,所以呢,当探长在执行任务时,将触发时序图中 (3.5)(3.6)两步反应:

clipboard.png

复杂么?也还好,(3.6)是由 (3.5)触发的,(3.5)对应的操作是:探长 R1 想要获取的张三 income 属性。
(所以,划重点,敲黑板!! 如果任务中不涉及到 income 这项属性,那么就不会有 (3.5)的操作,也就没有 (3.6)什么事)

由于探长 R1 所执行的任务中用到 bankUser.income 变量,这里的 . 符号其实就是 get() 操作;一旦涉及到 get() 操作,监督这个 income 属性的观察员 O1 就会执行 reportObserved 方法。 该 reportObserved 方法对应的源码如下:

clipboard.png

那么多行代码,我们主要关注其中操作影响到探长(derivation)中的操作:

    1. 更新探长的 lastAccessedBy 属性(事务 id),这个是为了避免重复操作而设置的
    1. 更新探长的 newObserving 属性,将探员信息推入到该队列中(对应时序图 (3.6.2)操作),这个比较重要,后续探长和观察员更新依赖关系就靠这个属性了

随后,任务执行完(时序图(3.7))后,探长就开始着手更新和观察员 O1 的关联关系了。?

2.2.3、bindDependencies

探长 R1 整理和观察员的关系是在时序图 (3.8)处:
clipboard.png

两者依赖更新的算法在参考文章Mobx 源码解读(四) Reaction 中有详细的注解,推荐阅读。这里也做一下简单介绍。

该函数的目的,是用 derivation.newObserving 去更新 derivation.observing 属性:

  • derivation.newObserving 就是刚才在所述时序图 (3.6.2)操作是生成的
  • 执行完之后 derivation.newObserving 会置空,而 derivation.observing 属性获得更新,该属性反映的 探长观察员 之间最新的关联关系;

依赖更新肯定需要遍历,由于涉及到探长、观察员两个维度的数组,朴素算法的时间复杂度将是 O(n^2),而 MobX 中使用 3 次遍历 + diffValue 属性的辅助将复杂度降到了 O(n)。? ?

下面我用示例来展现这 3 次遍历过程。

2.2.3.1、先看一下整体的 input / output

假设在执行 bindDependencies 函数之前, derivation.observing 已有 2 个元素,derivation.newObserving 有 5 个对象(由于 A、B 各重复一次,实际只有 3 个不同的对象 A、B、C),经过 bindDependencies 函数后 derivation.observing 将获得更新,如下所示:

clipboard.png

2.2.3.2、第一次循环:newObserving 数组去重

第一次循环遍历 newObserving,利用 diffValue 进行去重,一次遍历就完成了(这种 数组去重算法 可以添加到面试题库中了??)。注意其中 diffValue 改变情况:

clipboard.png

由于 A 对象(引用)既在 observing 数组也在 newObserving 数组中,当改变 newObserving 中 A 元素的 diffValue 值的时候,observing 数组 A 属性也自然跟着改变

这次遍历后,所有 最新的依赖diffValue 值都是 1 了哦,而且去除了所有重复的依赖。

2.2.3.3、第二次循环:去除observing 数组陈旧关联

接下去第二次遍历针对 observing 数组,做了两件事:

  • 如果对象的 diffValue 值为 0 (为 0 说明不在 newObserving 数组中,是陈旧的关联),则调用 removeObserver 去除该关联;因此这次遍历之后会删除 observing 数组中 D 对象
  • observing 数组中剩余对象的 diffValue 值变成 0;

clipboard.png

这一次遍历之后,去除了所有陈旧的依赖,且遗留下来的对象的 diffValue 值都是 0 了。

2.2.3.4、第三次循环:将新增依赖添加到 observing

第二次遍历针对 newObserving 数组,做了一件事:

  • 如果 diffValue 为 1,说明是新增的依赖,调用 addObserver 新增依赖,并将 diffValue 置为 0

clipboard.png

这最后一次遍历,observing 数组关联都是最新,且 diffValue 都是 0 ,为下一次的 bindDependencies 做好了准备。

至此,A计划部署方案(autorun 源码)就讲完了。 A 计划执行后,探长 R1 完成上级下达的任务,同时也和观察员 O1 建立起明确且牢固的依赖

3、响应观察值的变化 - propagateChanged

一旦张三存款发生变化,那么一定会被观察员 O1 监视到,请问此时观察员会怎么做?

或许有人会说,观察员 O1 然后上报给探长 R1 ,然后让探长 R1 再执行一次打印任务

从最终结果角度去理解,上面的陈述其实没毛病,的确是观察员 O1 驱动探长 R1 再打印一次

但若从执行过程角度去看,以上陈述是 错误的! ?

观察员 O1 监视到变化之后,的确通知探长 R1了,但探长并非直接执行任务,而是通知 MobX 再按照 A 计划部署方案执行一遍!;(不得不感慨,这是多么死板地执行机制)

源码中是怎么体现的呢?

上面提及到过,当观察员所监控的值(比如income)发生变化时,会触发 MobX 所提供的 propagateChanged 方法。

propagateChanged 对应的源码如下:

clipboard.png

代码很简单,即遍历观察员的上级们,让他们调用 onBecomeStale() 方法 。该观察员有可能不止对一位上级(上级也不一定只有探长)负责,每位上级的 onBecomeStale() 是不一样的。(当然,此故事中观察员 O1 只有 1 位上级 —— 探长 R1)

我们看一下探长这类上级所定义的 onBecomeStale

onBecomeStale() {
        this.schedule()
    }

简单明了,就是直接 再一次执行部署方案。如此简单朴素,真正做到了 “一视同仁” —— 无论什么任务,一旦部署就绪,任何观察员反馈情况有变(比如张三账户余额发生变动了),探长都是让 MobX 重新执行一遍部署方案,并不会直接执行任务,反正部署方案中有探长执行任务的步骤嘛。??

所谓的流程化、设计模式,都多多少少在一定程度上约束个体行为(丧失了一部分灵活性),而取得整体上的普适性和可扩展性

现在再回过头来看刚才官方文档截图中的第二句话:"然后每次它的依赖关系改变时会再次被触发"

clipboard.png

它所表达的意思其实就是:当张三余额发生变化的时候,将 自动触发 上述的 A 计划部署方案。

4、小测试

问:下列代码中 message.title = "Hello world" 为何不会触发 autorun 再次执行?

const message = observable({ title: "hello" })

autorun(() => {
    console.log(message)
})

// 不会触发重新运行
message.title = "Hello world"

其实上述问题来自官方的一个问题,若无思路的,可以先参考官方文档 常见陷阱: console.log。如果能从源码角度回答这个问题,则说明已经理解本节所讲的 autorun 的知识点了

5、小结

此篇是开篇,所阐述的概念仅仅占 MobX 体系冰山一角。

clipboard.png

故事中还还有很多问题,比如:

  1. 如何成为一名合格的探员、观察员?(用程序员的话讲,就是有哪些属性和方法)
  2. 数据情报室到底还存有哪些关键信息?
  3. 组织机构中是否还有其他组、成员?
  4. 多个探长、观察员情况下,这套部署方案又是如何的呢?
  5. ....

以上问题的答案,读者可能已经知道,那些还不知道的可以自己留作自己思考;在后续章节,我也会在适当的时机解答上述中的问题;
(也欢迎大家提问,将有可能采纳编入后续的故事叙述中来解答)

后续的章节中,将继续介绍 ComputedValueActionAtomDerivationSpy 等,正是这些功能角色使得 MobX 有着强大的自动化能力,合理运用了惰性求值、函数式编程等编程范式,使 MobX 在复杂交互应用中大放异彩;

参考文章

罗列本文所参考的文章,感谢他们所给予的帮助:

下面的是我的公众号二维码图片,欢迎关注。
个人微信公众号

查看原文

赞 91 收藏 78 评论 18

LuckyJing 赞了文章 · 2018-03-15

2018年腾讯前端一面总结(面向2019届学生)

前言

腾讯一面,相比阿里一面来说,腾讯一面先给打电话预定时间,这也给了我们这些面试者去准备的时间。但是也正是因为这种确定性,也有在等待电话的时候的心情的忐忑。

背景

我是一名大三学生,大一在学校acm集训队,后来转向学习java,又去开发Android,在期间,学会怎么去解决一些编程遇到的问题以及灵活运用github。在大二寒假的时候,开始接触学习前端,如今已经一年了,一开始是做百度前端技术学院的任务,学习了html和css,以及参考别人优秀的代码。

js是通过红宝石,js高级教程第三版开始入门学习的,这本书里面的基础知识很精髓,那时候我也很有耐心的去看完了,虽然说可能实践还是跟不上理论,因为后面做的项目基本都是用框架去做项目,而导致对于基础知识的实践比较少。

接下来,我们进入正题吧

腾讯一面

首先,接到电话的时候,由于之前心情的忐忑,情绪还是有点兴奋的,以期待的口气向面试官问好,面试官也问问好之后就开始进入面试题了。

你先简短的介绍一下自己

在这里,我就简短的介绍了自己的学校专业,应聘的岗位,以及是怎么走向学习前端的道路,也就和我写的背景差不多。

你是怎么学习前端的?

开放性问题,我就回答了,一开始是在百度前端技术学院,后来看js高级教程第三版,接下来就是做项目,接触一些框架,然后,就开始学习框架,以及个人对于框架的一些看法。

这里面试官很好,也跟我讨论了一下学习的好不好,以及学习js基础的话,不单单要看红宝石(js高级教程第三版),也要看看js的犀牛书(js权威指南)比较好一点。

有了解js的数据类型吗?说一说

ES5的基本数据类型,Undefined,Null,Number,String,Boolean。引用类型,Object
。ES6新增了数据类型Symbol,表示独一无二的值。

有了解js的事件吗?

在这里,我不清楚是js的事件流还是js的事件方法,经过再次询问之后,是叫我讲述一下js的事件流。然后我就着这个图回答了一遍。

image

一个完整的JS事件流是从window开始,最后回到window的一个过程

事件流被分为三个阶段(1~ 5)捕获过程、(5~ 6)事件触发过程、(6~ 10)冒泡过程

移动端的触摸事件有了解不?

在这里,我回答说我之前的项目经验基本都是PC端的所以不了解,但是面试官建议我去了解一下,毕竟基础知识一定要扎实,才会在前端的路上走的更远。所以,我就红宝石了解了一下,这里也介绍给大家吧。

触摸事件指的是指的是用户将手指放在屏幕上,在屏幕上滑动到将手指从屏幕移开触发的事件,具体来说,有以下触摸事件的产生。

  • touchstart: 当手指触摸屏幕时候触发;
  • touchmove: 当手指在屏幕上滑动的时候连续触发;可以调用阻止默认事件preventDefault()阻止屏幕滚动;
  • touchend: 手指离开屏幕时触发;
  • touchcancel: 系统停止跟踪触摸时触发;

以上的这些时间都会冒泡,而且都可以取消冒泡,而且,对于以上事件也提供了和鼠标事件中常用的属性:bubble,cancelable,view,clientX,clientY,screenX,screenY,detail,altKey,shiftKey,ctrKey和metaKey。

除了上面这些属性外,触摸事件还提供了下面这些属性:

  • touches: 跟踪返回Touch对象的数组;
  • targetTouchs: 特定事件目标的Touch对象的数组;
  • changeTouchs: 上次触摸以来改变了的Touch对象的数组;

每个Touch对象包含一下的属性

  • clientX: 触摸目标在浏览器中的x坐标
  • clientY: 触摸目标在浏览器中的y坐标
  • identifier: 标识触摸的唯一ID。
  • pageX: 触摸目标在当前DOM中的x坐标
  • pageY: 触摸目标在当前DOM中的y坐标
  • screenX: 触摸目标在屏幕中的x坐标
  • screenY: 触摸目标在屏幕中的y坐标
  • target: 触摸的DOM节点目标。

具体的例子,大家可以再在网上搜索一下。我就不带大家敲了。

说一下页面加载过程,就是输入url到加载出页面

这里发生了:

  1. 输入地址
  2. 浏览器查找域名的 IP 地址
  3. 这一步包括 DNS 具体的查找过程,包括:浏览器缓存->系统缓存->路由器缓存...
  4. 浏览器向 web 服务器发送一个 HTTP 请求
  5. 服务器的永久重定向响应(从 http://example.comhttp://www.example.com
  6. 浏览器跟踪重定向地址
  7. 服务器处理请求
  8. 服务器返回一个 HTTP 响应
  9. 浏览器显示 HTML
  10. 浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)
  11. 浏览器发送异步请求

这里腾讯面试官还问了我对状态码的了解,并问了一个304状态码的意思,大家想了解可以自行百度一下咯。

说一下路由器的缓存

因为上面回答了缓存,路由器的缓存,由于本人学习计网也久忘了,就老实回答面试官不清楚,面试官也建议再去了解了解。这里我了解了,也给大家分享一下吧。

通俗点说,每个路由器根据所在网络的不同,都有自己的路由表,在工作时会选择相应的路径。为什么要有路由器缓存呢,这个也是为了发送数据,因为路由器最高层一般都是网络层,网络层一般都是传送数据包,数据包又是经过应用层向下传送之后送来的一部分文件数据,如果我们没有缓存的话,那么,每次都会查找传送到达方的ip地址就会很费力。

做项目中有遇到跨域吗?跨域的原理是什么?

这里,我就老实回答有了,因为如果真的做过项目的话,确实会遇到跨域一般。但是我一般是用vue做项目,然后解决跨域又是用webpack里面配置的poxyTable进行解决跨域,相当于用代理工具,然后面试官又问了问我代理是怎么样有了解原理吗,以及跨域的原理,以及浏览器跨域的原因。

那这里我就简单解说一下跨域的原理吧.

首先,什么是跨域?跨域就是它是由浏览器的同源策略造成的,是浏览器施加的安全限制。

所谓同源是指,域名,协议,端口均相同,不明白可以看看下面的例子:

http://www.123.com/index.html 调用 http://www.123.com/server.php (非跨域)

http://www.123.com/index.html 调用 http://www.456.com/server.php (主域名不同:123/456,跨域)

http://abc.123.com/index.html 调用 http://def.123.com/server.php (子域名不同:abc/def,跨域)

http://www.123.com:8080/index.html 调用 http://www.123.com:8081/server.php (端口不同:8080/8081,跨域)

http://www.123.com/index.html 调用 https://www.123.com/server.php (协议不同:http/https,跨域)

为什么要实现跨域?防止CSRF攻击,我看了一篇文章关于CSRF的还不错,大家有兴趣也可以看看这篇文章

你是怎么解决跨域的?说说Cors解决的方法,和你用的jsonp的解决的原理

我回复面试官,我是用jsonp解决跨域的,然后面试官叫我说明一下jsonp跨域的原理,等我说完之后,面试官又跟我说了一下其实Cors方法更好,大家如果想理解可以看看阮一峰的跨域资源共享CORS详解和而对于jsonp跨域的工作原理,网上有很多,我就不举例子了,我建议可以直接在github上面看看源代码就可以理解。

有了解作用域吗?怎么预防作用域污染

其实网上有很多作用域的文章,参差不齐,个人觉得的话,作用域无非就是js当做对于function函数声明会提升到其他声明语句前执行,以及对于某个{}里面的作用域如果找不到某个属性,就会在该{}上下文当中查找属性,如果还找不到的话,进而类似。

作用域污染,无非就是闭包了吧,个人理解。

说说闭包

网上关于闭包的说明实在是太多,但是很多又讲的不明不白的,标题党太多,这里我建议还是直接看看js高级教程第三版里面的书本吧。

其实闭包也就是指有权访问另一个函数作用域的函数而已。常用的创建闭包的方法就是在函数内部创建另一个函数。

function a(){
    var a;
    // ...
    return function(){
        // 这里可以引用a函数里面的作用域,也就是可以使用a
        // 而且a函数作用域无法使用这里的值。
    }
}
了解前端的缓存吗?缓存分为几类?

前端的缓存无非就Cookie,LocalStorge,SessionStorge这三个吧。

个人就简单的介绍一下这三个吧,更详细可以自己去百度百度一下。

Cookie,存储容量小,仅仅4kb左右,在网络请求的时候可以发送,不建议存储重要数据,因为会被网络诈骗就是把本地的cookie发到别人的服务器上,进而获取你的账户密码。

LocalStorge,SessionStorge都是本地缓存的主要用的,两者的用法都很简单,都有各自的Set和get方法。主要的区别就是LocalStorge是一种持久化的存储方式,也就是说如果不手动清除,数据就永远不会过期。而SessionStorge关闭浏览器就清空数据。

怎么才算一个好的前端开发者

这里的话,其实我的回答大家可能不是很在乎哈哈,就不说了,大家根据自己的理解去回答就好了。

你还有什么问题想问吗

这个问题,其实我想到了,因为我看过鹅厂wo谈会,在那里其实我就知道可能会问这个问题,而我也问了我想知道的答案,就是面试官对于我之前的回答给一个评价。

我碰到的面试官很好,他给我的评价说了很久,不管怎么说有机会和这些大牛聊聊天真的是知识层面又上升了吧。面试官给我的总结就是,基础知识可以再多去学习,不用太着急学习框架,市面上的框架千变万化,只有基础知识比较好才能够学习的更好,而且需要多学习一下性能优化,网络,安全这方面,因为在大公司里面,其实重要的东西并不是你能够做的多好看,而是你的安全性那些做的好不好,一不小心信息泄露了,那就会导致很多无法想象的事情,(这里我特建议大家看看图解http协议吧链接:https://pan.baidu.com/s/1Cvtt... 密码:u35q)

然后,建议我就是基础知识学更多一些,把知识都规范体系化,这样以后碰到问题了就会一下子就知道是哪方面的问题,直接去解决。这里面试官给我从以下几个方面进行说明,我也真的是收获颇多。

  • js基础
  • 计算机网络
  • 性能优化
  • 开发技巧
  • 移动端知识
  • 安全性问题

总结

不管怎么说,不管结果好坏,跟腾讯大牛的聊天都是值得积累的过程,不要想象的很紧张吧,这也是自己能够收获知识的时候,我是这样觉得的,哈哈,如果觉得写得还行,帮忙点个赞吧。

查看原文

赞 196 收藏 510 评论 29

LuckyJing 赞了文章 · 2018-02-05

ES6 换种思路处理数据

Handle javascript data structures with map/reduce

看完本文,希望可以写出更加漂亮、简洁、函数式的代码?

reduce

reduce 可以用来汇总数据

const customer = [
  {id: 1, count: 2},
  {id: 2, count: 89},
  {id: 3, count: 1}
];
const totalCount = customer.reduce((total, item) =>
  total + item.count,
  0 // total 的初始值
);
// now totalCount = 92

把一个对象数组变成一个以数组中各个对象的 id 为属性名,对象本身为属性值的对象。haoduoshipin

let products = [
  {
    id: '123',
    name: '苹果'
  },
  {
    id: '345',
    name: '橘子'
  }
];

const productsById = products.reduce(
  (obj, product) => {
    obj[product.id] = product
    return obj
  },
  {}
);

console.log('result', productsById);

map

map 可以理解为是数组的转换器,依次对数组中的每个元素做变换进而得到一个新的数组。

const integers = [1, 2, 3, 4, 6, 7];
const twoXIntegers = integers.map(i => i*2);
// twoXIntegers are now [2, 4, 6, 8, 12, 14]
// integers数组并不会受到影响

find?

筛选出数组中的个别元素

const posts = [
  {id: 1, title: 'Title 1'},
  {id: 2, title: 'Title 2'},
];
// find the title of post whose id is 1
const title = posts.find(p => p.id === 1).title;

唉~ 使用了半年的 es6才发现有这么好用的东西,译者傻缺还像下面这么写过呢

const posts = [
  {id: 1, title: 'Title 1'},
  {id: 2, title: 'Title 2'},
];

const title = posts.filter(item => item.id === 1)[0].title;

filter

筛选出数组中某些符合条件的元素组成新的数组

const integers = [1, 2, 3, 4, 6, 7];
const evenIntegers = integers.filter(i => i % 2 === 0);
// evenIntegers are [2, 4, 6]

请大家自行思考下filterfind的区别

数组concat

const arr1 = [1, 2, 3, 4, 5];
const arr2 = [6, 7, 8, 9, 0];
const arrTarget = [...arr1, ...arr2];
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]

对象操作

function operation(query, option = {}) {
    const param = {...query, ...option};
    // ....
    return param;
}
let opt = {startTime: 123455555, endTime: 113345555};
let q = {name: '一步', age: 'xxx'};
operation(q, opt);
// {name: "一步", age: "xxx", startTime: 123455555, endTime: 113345555}

对象是引用传参的,所以函数内部应该尽可能的保证传入的参数不受到污染。

为对象动态地添加字段

const dynamicKey = 'wearsSpectacles';
const user = {name: 'Shivek Khurana'};
const updatedUser = {...user, [dynamicKey]: true};
// updatedUser is {name: 'Shivek Khurana', wearsSpectacles: true}

将对象转换为query字符串?

const params = {color: 'red', minPrice: 8000, maxPrice: 10000};
const query = '?' + Object.keys(params)
  .map(k =>
    encodeURIComponent(k) + '=' + encodeURIComponent(params[k])
  )
  .join('&')
;
// encodeURIComponent encodes special characters like spaces, hashes
// query is now "color=red&minPrice=8000&maxPrice=10000"

得到对象数组的元素 index

const posts = [
  {id: 13, title: 'Title 221'},
  {id: 5, title: 'Title 102'},
  {id: 131, title: 'Title 18'},
  {id: 55, title: 'Title 234'}
];
// to find index of element with id 131
const requiredIndex = posts.map(p => p.id).indexOf(131);

更加优雅的写法呱呱呱提供

const posts = [
  {id: 13, title: 'Title 221'},
  {id: 5, title: 'Title 102'},
  {id: 131, title: 'Title 18'},
  {id: 55, title: 'Title 234'}
];
const index = posts.findIndex(p => p.id === 131)

删除对象的某个字段

const user = { name: 'Shivek Khurana', age: 23, password: 'SantaCl@use' };
const userWithoutPassword = Object.keys(user)
    .filter(key => key !== 'password')
    .map(key => ({[key]: user[key]}))
    .reduce((accumulator, current) => ({ ...accumulator, ...current }), {});

这里我认为原作者有点为了函数式编程而函数式了,下面是我的解决方案:

const user = {name: 'Shivek Khurana', age: 23, password: 'SantaCl@use'};
const newUser = {...user};
delete newUser.password;
// {name: "Shivek Khurana", age: 23}

更现代的写法YiHzo提供: ?????

const user = {name: 'Shivek Khurana', age: 23, password: 'SantaCl@use'};
// 利用对象的解构,取出非password的所有字段
const {password, ...newUser} = user

以上代码片段的共同原则:不改变原数据。希望大家的代码都可以尽可能的简洁,可维护?。

【开发环境推荐】Cloud Studio 是基于浏览器的集成式开发环境,支持绝大部分编程语言,包括 HTML5、PHP、Python、Java、Ruby、C/C++、.NET 小程序等等,无需下载安装程序,一键切换开发环境。 Cloud Studio提供了完整的 Linux 环境,并且支持自定义域名指向,动态计算资源调整,可以完成各种应用的开发编译与部署。
查看原文

赞 46 收藏 128 评论 18

LuckyJing 赞了文章 · 2018-02-05

React怎么判断什么时候该重新渲染组件?

React因为他的性能而著名。因为他有一个虚拟DOM层并且只有在需要时才更新真实DOM。即使是同样地信息这也比一直直接更新DOM要快很多。但是,React的智能仅此而已(目前为止),我们的任务是知道React的预期行为以及限制,这样我们才不会意外损失性能。

我们需要关注的一方面是React如何决定什么时候重新渲染组件。不是重新渲染DOM节点,只是调用render方法来改变虚拟DOM。我们可以通过告诉React什么时候需要渲染什么时候不需要渲染来帮助React。让我们依次来看看这些。

1. 组件的状态发生改变

只有在组件的state变化时才会出发组件的重新渲染。状态的改变可以因为props的改变,或者直接通过setState方法改变。组件获得新的状态然后React决定是否应该重新渲染组件。不幸的是,React难以置信简单地将默认行为设计为每次都重新渲染。

组件改变?重新渲染。父组件改变?重新渲染。一部分没有导致视图改变的props改变?重新渲染。

class Todo extends React.Component {

    componentDidMount() {
        setInterval(() => {
            this.setState(() => {
                console.log('setting state');
                return { unseen: "does not display" }
            });
        }, 1000);
    }

    render() {
        console.log('render called');
        return (<div>...</div>);
    }
}

在这个(非常刻意的)例子中,Todo将会每秒重新渲染依次,即使render方法根本没有使用unseen。事实上,unseen值甚至都不改变。你可以在CodePen里查看这个例子的实际版本。

好吧,但是每次都重新渲染没有什么帮助。

我的意思是,我非常感谢React的细心谨慎。如果状态改变但是组件没有正确渲染的话更糟。权衡之下,每次都重新渲染绝对是一个安全的选择。

但是重新渲染的时间成本看起来非常昂贵(例子里非常夸张地表现了出来)。

是的,在不必要的时候重新渲染会浪费循环并且不是一个好的想好。但是,React不能知道什么时候可以安全的跳过重新渲染,所以React无论是否重要每次都重新渲染。

我们如何告诉React跳过重新渲染?

那就是第二点要说的内容。

2. shouldComponentUpdate方法

shouldComponentUpdate方法默认返回true,这就是导致每次更新都重新渲染的原因。但是你可以在需要优化性能时重写这个方法来让React更智能。比起让React每次都重新渲染,你可以告诉React你什么时候不像触发重新渲染。

当React将要渲染组件时他会执行shouldComponentUpdate方法来看它是否返回true(组件应该更新,也就是重新渲染)。所以你需要重写shouldComponentUpdate方法让它根据情况返回true或者false来告诉React什么时候重新渲染什么时候跳过重新渲染。

当你使用shouldComponentUpdate方法你需要考虑哪些数据对与重新渲染重要。让我们回到这个例子。

正如你所看到的,我们只想在titledone属性改变的时候重新渲染Todo。我们不关心unseen是否改变,所以我没有把它包含在shouldComponentUpdate方法中。

当React渲染Todo组件(通过setState触发)他会首先检查状态是否改变(通过propsstate)。假设状态改变了(因为我们显式地调用了setState所以这会发生)React会检查TodoshouldComponentUpdate方法。React会根据shouldComponentUpdate方法返回值为true或者false来决定从哪里渲染。

更新后的代码仍然会每秒调用一次setState但是render只有在第一次加载时(或者titledone属性改变后)才会调用。你可以在这里看到。

看起来有很多工作去做。

是的,这个例子非常冗长因为有两个属性(titledone)需要关注并且只有一个可以忽略(unseen)。根据你的数据可能仅检查一个或两个属性并且忽略其他会更有意义。

重要提示

当子组件的的state变化时, 返回false并不能阻止它们重渲染。

– Facebook的React文档

这作用于子组件的状态而不是他们的props。所以如果一个子组件内部管理了一些他自己的状态(使用他自己的setState),这仍然会更新。但是如果父组件的shouldComponentUpdate方法返回了false就不会传递更新后的props给他的子组件,所以子组件不会重渲染,即使他们的props变化了。

额外内容:简单性能测试

编写并且在shouldComponentUpdate方法中运行计算的时间成本可能会很昂贵,所以你需要确保值得做。在写shouldComponentUpdate方法前你可以测试React一个周期默认会消耗多少时间。有了这个信息做参考,在做性能优化时你可以做一个不盲目的决定。

使用React的性能工具去发现浪费的周期:

Perf.start()
// Do the render
Perf.stop()
Perf.printWasted()

哪一个组件浪费了很多渲染周期?你怎么通过shouldComponentUpdate方法让他们更智能?试着使用性能测试工具来比较他们的性能。

查看原文

赞 7 收藏 13 评论 3

LuckyJing 赞了文章 · 2018-02-05

React16.2的fiber架构

React16真是一天一改,如果现在不看,以后也很难看懂了。

在React16中,虽然也是通过JSX编译得到一个虚拟DOM对象,但对这些虚拟DOM对象的再加工则是经过翻天覆地的变化。我们需要追根溯底,看它是怎么一步步转换过来的。我们先不看什么组件render,先找到ReactDOM.render。在ReactDOM的源码里,有三个类似的东西:

//by 司徒正美, 加群:370262116 一起研究React与anujs
// https://github.com/RubyLouvre/anu 欢迎加star

ReactDOM= {
 hydrate: function (element, container, callback) {
    //新API,代替render
    return renderSubtreeIntoContainer(null, element, container, true, callback);
  },
  render: function (element, container, callback) {
    //React15的重要API,逐渐退出舞台
    return renderSubtreeIntoContainer(null, element, container, false, callback);
  },
  unstable_renderSubtreeIntoContainer: function (parentComponent, element, containerNode, callback) {
    //用于生成子树,废弃
    return renderSubtreeIntoContainer(parentComponent, element, containerNode, false, callback);
  }
}

我们看renderSubtreeIntoContainer,这是一个内部API

//by 司徒正美, 加群:370262116 一起研究React与anujs

function renderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {

  var root = container._reactRootContainer;
  if (!root) {
    //如果是第一次对这个元素进行渲染,那么它会清空元素的内部
    var shouldHydrate = forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
    // First clear any existing content.
    if (!shouldHydrate) {
      var warned = false;
      var rootSibling = void 0;
      while (rootSibling = container.lastChild) {
        container.removeChild(rootSibling);
      }
    }

    var newRoot = DOMRenderer.createContainer(container, shouldHydrate);
    //创建一个HostRoot对象,是Fiber对象的一种
    root = container._reactRootContainer = newRoot;
    
    // Initial mount should not be batched.
    DOMRenderer.unbatchedUpdates(function () {
     //对newRoot对象进行更新
      DOMRenderer.updateContainer(children, newRoot, parentComponent, callback);
    });
  } else {
    //对root对象进行更新
    DOMRenderer.updateContainer(children, root, parentComponent, callback);
  }
  return DOMRenderer.getPublicRootInstance(root);
}

看一下DOMRenderer.createContainer是怎么创建root对象的。

首先DOMRenderer这个对象是由一个叫reactReconciler的方法生成,需要传入一个对象,将一些东西注进去。最后产生一个对象,里面就有createContainer这个方法

// containerInfo就是ReactDOM.render(<div/>, containerInfo)的第二个对象,换言之是一个元素节点
createContainer: function (containerInfo, hydrate) {
   return createFiberRoot(containerInfo, hydrate);
},

再看createFiberRoot是怎么将一个真实DOM变成一个Fiber对象

//by 司徒正美, 加群:370262116 一起研究React与anujs

function createFiberRoot(containerInfo, hydrate) {
  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  var uninitializedFiber = createHostRootFiber();
  var root = {
    current: uninitializedFiber,
    containerInfo: containerInfo,
    pendingChildren: null,
    remainingExpirationTime: NoWork,
    isReadyForCommit: false,
    finishedWork: null,
    context: null,
    pendingContext: null,
    hydrate: hydrate,
    nextScheduledRoot: null
  };
  uninitializedFiber.stateNode = root;

  return root;
}

function createHostRootFiber() {
  var fiber = createFiber(HostRoot, null, NoContext);
  return fiber;
}

var createFiber = function (tag, key, internalContextTag) {
  return new FiberNode(tag, key, internalContextTag);
};


function FiberNode(tag, key, internalContextTag) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this['return'] = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = null;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;

  this.internalContextTag = internalContextTag;

  // Effects
  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  this.expirationTime = NoWork;

  this.alternate = null;


}

所有Fiber对象都是FiberNode的实例,它有许多种类型,通过tag来标识。

内部有许多方法来生成Fiber对象

  • createFiberFromElement (type为类,无状态函数,元素标签名)
  • createFiberFromFragment (type为React.Fragment)
  • createFiberFromText (在JSX中表现为字符串,数字)
  • createFiberFromHostInstanceForDeletion
  • createFiberFromCall
  • createFiberFromReturn
  • createFiberFromPortal (createPortal就会产生该类型)
  • createFiberRoot (用于ReactDOM.render的根节点)

createFiberRoot就是创建了一个普通对象,里面有一个current属性引用fiber对象,有一个containerInfo属性引用刚才的DOM节点,然后fiber对象有一个stateNode引用刚才的普通对象。在React15中,stateNode应该是一个组件实例或真实DOM,可能单纯是为了对齐,就创建一个普通对象。 最后返回普通对象。

我们先不看 DOMRenderer.unbatchedUpdates,直接看DOMRenderer.updateContainer。

//children就是ReactDOM的第一个参数,children通常表示一个数组,但是现在它泛指各种虚拟DOM了,第二个对象就是刚才提到的普通对象,我们可以称它为根组件,parentComponent为之前的根组件,现在它为null
 DOMRenderer.updateContainer(children, newRoot, parentComponent, callback);

updateContainer的源码也很简单,就是获得上下文对象,决定它是叫context还是pendingContext,最后丢给scheduleTopLevelUpdate

//by 司徒正美, 加群:370262116 一起研究React与anujs

 updateContainer: function (element, container, parentComponent, callback) {
      var current = container.current;//createFiberRoot中创建的fiber对象
      var context = getContextForSubtree(parentComponent);
      if (container.context === null) {
        container.context = context;
      } else {
        container.pendingContext = context;
      }
      // 原传名为 children, newRoot, parentComponent, callback
      // newRoot.fiber, children, callback
      scheduleTopLevelUpdate(current, element, callback);
    },

getContextForSubtree的实现

//by 司徒正美, 加群:370262116 一起研究React与anujs

function getContextForSubtree(parentComponent) {
  if (!parentComponent) {
    return emptyObject_1;
  }

  var fiber = get(parentComponent);
  var parentContext = findCurrentUnmaskedContext(fiber);
  return isContextProvider(fiber) ? processChildContext(fiber, parentContext) : parentContext;
}
//isContextConsumer与isContextProvider是两个全新的概念,
// 从原上下文中抽取一部分出来
function isContextConsumer(fiber) {
  return fiber.tag === ClassComponent && fiber.type.contextTypes != null;
}
//isContextProvider,产生一个新的上下文
function isContextProvider(fiber) {
  return fiber.tag === ClassComponent && fiber.type.childContextTypes != null;
}

function _processChildContext(currentContext) {
    var Component = this._currentElement.type;
    var inst = this._instance;
    var childContext;
    if (inst.getChildContext) {
       childContext = inst.getChildContext();
    }
    
    if (childContext) {
        return _assign({}, currentContext, childContext);
    }
    return currentContext;
}

function findCurrentUnmaskedContext(fiber) {
 
  var node = fiber;
  while (node.tag !== HostRoot) {
    if (isContextProvider(node)) {
      return node.stateNode.__reactInternalMemoizedMergedChildContext;
    }
    var parent = node['return'];
    node = parent;
  }
  return node.stateNode.context;
}

因为我们的parentComponent一开始不存在,于是返回一个空对象。注意,这个空对象是重复使用的,不是每次返回一个新的空对象,这是一个很好的优化。

scheduleTopLevelUpdate是将用户的传参封装成一个update对象, update对象有partialState对象,它就是相当于React15中 的setState的第一个state传参。但现在partialState中竟然把children放进去了。

//by 司徒正美, 加群:370262116 一起研究React与anujs

function scheduleTopLevelUpdate(current, element, callback) {
    // // newRoot.fiber, children, callback

    callback = callback === undefined ? null : callback;
    var expirationTime = void 0;
    // Check if the top-level element is an async wrapper component. If so,
    // treat updates to the root as async. This is a bit weird but lets us
    // avoid a separate `renderAsync` API.
    if (enableAsyncSubtreeAPI && element != null && element.type != null && element.type.prototype != null && element.type.prototype.unstable_isAsyncReactComponent === true) {
      expirationTime = computeAsyncExpiration();
    } else {
      expirationTime = computeExpirationForFiber(current);//计算过时时间
    }

    var update = {
      expirationTime: expirationTime,//过时时间
      partialState: { element: element },//!!!!神奇
      callback: callback,
      isReplace: false,
      isForced: false,
      nextCallback: null,
      next: null
    };
    insertUpdateIntoFiber(current, update);//创建一个列队
    scheduleWork(current, expirationTime);//执行列队
  }

列队是一个链表

//by 司徒正美, 加群:370262116 一起研究React与anujs
// https://github.com/RubyLouvre/anu 欢迎加star

function insertUpdateIntoFiber(fiber, update) {
  // We'll have at least one and at most two distinct update queues.
  var alternateFiber = fiber.alternate;
  var queue1 = fiber.updateQueue;
  if (queue1 === null) {
    // TODO: We don't know what the base state will be until we begin work.
    // It depends on which fiber is the next current. Initialize with an empty
    // base state, then set to the memoizedState when rendering. Not super
    // happy with this approach.
    queue1 = fiber.updateQueue = createUpdateQueue(null);
  }

  var queue2 = void 0;
  if (alternateFiber !== null) {
    queue2 = alternateFiber.updateQueue;
    if (queue2 === null) {
      queue2 = alternateFiber.updateQueue = createUpdateQueue(null);
    }
  } else {
    queue2 = null;
  }
  queue2 = queue2 !== queue1 ? queue2 : null;

  // If there's only one queue, add the update to that queue and exit.
  if (queue2 === null) {
    insertUpdateIntoQueue(queue1, update);
    return;
  }

  // If either queue is empty, we need to add to both queues.
  if (queue1.last === null || queue2.last === null) {
    insertUpdateIntoQueue(queue1, update);
    insertUpdateIntoQueue(queue2, update);
    return;
  }

  // If both lists are not empty, the last update is the same for both lists
  // because of structural sharing. So, we should only append to one of
  // the lists.
  insertUpdateIntoQueue(queue1, update);
  // But we still need to update the `last` pointer of queue2.
  queue2.last = update;
}

function insertUpdateIntoQueue(queue, update) {
  // Append the update to the end of the list.
  if (queue.last === null) {
    // Queue is empty
    queue.first = queue.last = update;
  } else {
    queue.last.next = update;
    queue.last = update;
  }
  if (queue.expirationTime === NoWork || queue.expirationTime > update.expirationTime) {
    queue.expirationTime = update.expirationTime;
  }
}

scheduleWork是执行虚拟DOM(fiber树)的更新。 scheduleWork,requestWork, performWork是三部曲。

//by 司徒正美, 加群:370262116 一起研究React与anujs

function scheduleWork(fiber, expirationTime) {
    return scheduleWorkImpl(fiber, expirationTime, false);
  }

  function checkRootNeedsClearing(root, fiber, expirationTime) {
    if (!isWorking && root === nextRoot && expirationTime < nextRenderExpirationTime) {
      // Restart the root from the top.
      if (nextUnitOfWork !== null) {
        // This is an interruption. (Used for performance tracking.)
        interruptedBy = fiber;
      }
      nextRoot = null;
      nextUnitOfWork = null;
      nextRenderExpirationTime = NoWork;
    }
  }

  function scheduleWorkImpl(fiber, expirationTime, isErrorRecovery) {
    recordScheduleUpdate();


    var node = fiber;
    while (node !== null) {
      // Walk the parent path to the root and update each node's
      // expiration time.
      if (node.expirationTime === NoWork || node.expirationTime > expirationTime) {
        node.expirationTime = expirationTime;
      }
      if (node.alternate !== null) {
        if (node.alternate.expirationTime === NoWork || node.alternate.expirationTime > expirationTime) {
          node.alternate.expirationTime = expirationTime;
        }
      }
      if (node['return'] === null) {
        if (node.tag === HostRoot) {
          var root = node.stateNode;

          checkRootNeedsClearing(root, fiber, expirationTime);
          requestWork(root, expirationTime);
          checkRootNeedsClearing(root, fiber, expirationTime);
        } else {

          return;
        }
      }
      node = node['return'];
    }
  }


function requestWork(root, expirationTime) {
    if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
      invariant_1(false, 'Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.');
    }

    // Add the root to the schedule.
    // Check if this root is already part of the schedule.
    if (root.nextScheduledRoot === null) {
      // This root is not already scheduled. Add it.
      root.remainingExpirationTime = expirationTime;
      if (lastScheduledRoot === null) {
        firstScheduledRoot = lastScheduledRoot = root;
        root.nextScheduledRoot = root;
      } else {
        lastScheduledRoot.nextScheduledRoot = root;
        lastScheduledRoot = root;
        lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
      }
    } else {
      // This root is already scheduled, but its priority may have increased.
      var remainingExpirationTime = root.remainingExpirationTime;
      if (remainingExpirationTime === NoWork || expirationTime < remainingExpirationTime) {
        // Update the priority.
        root.remainingExpirationTime = expirationTime;
      }
    }

    if (isRendering) {
      // Prevent reentrancy. Remaining work will be scheduled at the end of
      // the currently rendering batch.
      return;
    }

    if (isBatchingUpdates) {
      // Flush work at the end of the batch.
      if (isUnbatchingUpdates) {
        // unless we're inside unbatchedUpdates, in which case we should
        // flush it now.
        nextFlushedRoot = root;
        nextFlushedExpirationTime = Sync;
        performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
      }
      return;
    }

    // TODO: Get rid of Sync and use current time?
    if (expirationTime === Sync) {
      performWork(Sync, null);
    } else {
      scheduleCallbackWithExpiration(expirationTime);
    }
  }

 function performWork(minExpirationTime, dl) {
    deadline = dl;

    // Keep working on roots until there's no more work, or until the we reach
    // the deadline.
    findHighestPriorityRoot();

    if (enableUserTimingAPI && deadline !== null) {
      var didExpire = nextFlushedExpirationTime < recalculateCurrentTime();
      stopRequestCallbackTimer(didExpire);
    }

    while (nextFlushedRoot !== null && nextFlushedExpirationTime !== NoWork && (minExpirationTime === NoWork || nextFlushedExpirationTime <= minExpirationTime) && !deadlineDidExpire) {
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime);
      // Find the next highest priority work.
      findHighestPriorityRoot();
    }

    // We're done flushing work. Either we ran out of time in this callback,
    // or there's no more work left with sufficient priority.

    // If we're inside a callback, set this to false since we just completed it.
    if (deadline !== null) {
      callbackExpirationTime = NoWork;
      callbackID = -1;
    }
    // If there's work left over, schedule a new callback.
    if (nextFlushedExpirationTime !== NoWork) {
      scheduleCallbackWithExpiration(nextFlushedExpirationTime);
    }

    // Clean-up.
    deadline = null;
    deadlineDidExpire = false;
    nestedUpdateCount = 0;

    if (hasUnhandledError) {
      var _error4 = unhandledError;
      unhandledError = null;
      hasUnhandledError = false;
      throw _error4;
    }
  }

function performWorkOnRoot(root, expirationTime) {
    !!isRendering ? invariant_1(false, 'performWorkOnRoot was called recursively. This error is likely caused by a bug in React. Please file an issue.') : void 0;

    isRendering = true;

    // Check if this is async work or sync/expired work.
    // TODO: Pass current time as argument to renderRoot, commitRoot
    if (expirationTime <= recalculateCurrentTime()) {
      // Flush sync work.
      var finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // This root is already complete. We can commit it.
        root.finishedWork = null;
        root.remainingExpirationTime = commitRoot(finishedWork);
      } else {
        root.finishedWork = null;
        finishedWork = renderRoot(root, expirationTime);
        if (finishedWork !== null) {
          // We've completed the root. Commit it.
          root.remainingExpirationTime = commitRoot(finishedWork);
        }
      }
    } else {
      // Flush async work.
      var _finishedWork = root.finishedWork;
      if (_finishedWork !== null) {
        // This root is already complete. We can commit it.
        root.finishedWork = null;
        root.remainingExpirationTime = commitRoot(_finishedWork);
      } else {
        root.finishedWork = null;
        _finishedWork = renderRoot(root, expirationTime);
        if (_finishedWork !== null) {
          // We've completed the root. Check the deadline one more time
          // before committing.
          if (!shouldYield()) {
            // Still time left. Commit the root.
            root.remainingExpirationTime = commitRoot(_finishedWork);
          } else {
            // There's no time left. Mark this root as complete. We'll come
            // back and commit it later.
            root.finishedWork = _finishedWork;
          }
        }
      }
    }

   isRendering = false;
}
//用于调整渲染顺序,高优先级的组件先执行
function findHighestPriorityRoot() {
    var highestPriorityWork = NoWork;
    var highestPriorityRoot = null;

    if (lastScheduledRoot !== null) {
      var previousScheduledRoot = lastScheduledRoot;
      var root = firstScheduledRoot;
      while (root !== null) {
        var remainingExpirationTime = root.remainingExpirationTime;
        if (remainingExpirationTime === NoWork) {
          // This root no longer has work. Remove it from the scheduler.

          // TODO: This check is redudant, but Flow is confused by the branch
          // below where we set lastScheduledRoot to null, even though we break
          // from the loop right after.
          !(previousScheduledRoot !== null && lastScheduledRoot !== null) ? invariant_1(false, 'Should have a previous and last root. This error is likely caused by a bug in React. Please file an issue.') : void 0;
          if (root === root.nextScheduledRoot) {
            // This is the only root in the list.
            root.nextScheduledRoot = null;
            firstScheduledRoot = lastScheduledRoot = null;
            break;
          } else if (root === firstScheduledRoot) {
            // This is the first root in the list.
            var next = root.nextScheduledRoot;
            firstScheduledRoot = next;
            lastScheduledRoot.nextScheduledRoot = next;
            root.nextScheduledRoot = null;
          } else if (root === lastScheduledRoot) {
            // This is the last root in the list.
            lastScheduledRoot = previousScheduledRoot;
            lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
            root.nextScheduledRoot = null;
            break;
          } else {
            previousScheduledRoot.nextScheduledRoot = root.nextScheduledRoot;
            root.nextScheduledRoot = null;
          }
          root = previousScheduledRoot.nextScheduledRoot;
        } else {
          if (highestPriorityWork === NoWork || remainingExpirationTime < highestPriorityWork) {
            // Update the priority, if it's higher
            highestPriorityWork = remainingExpirationTime;
            highestPriorityRoot = root;
          }
          if (root === lastScheduledRoot) {
            break;
          }
          previousScheduledRoot = root;
          root = root.nextScheduledRoot;
        }
      }
    }

    // If the next root is the same as the previous root, this is a nested
    // update. To prevent an infinite loop, increment the nested update count.
    var previousFlushedRoot = nextFlushedRoot;
    if (previousFlushedRoot !== null && previousFlushedRoot === highestPriorityRoot) {
      nestedUpdateCount++;
    } else {
      // Reset whenever we switch roots.
      nestedUpdateCount = 0;
    }
    nextFlushedRoot = highestPriorityRoot;
    nextFlushedExpirationTime = highestPriorityWork;
  }

这只是一部分更新逻辑, 简直没完没了,下次继续,添上流程图,回忆一下本文学到的东西

clipboard.png

查看原文

赞 26 收藏 32 评论 1

LuckyJing 赞了文章 · 2018-01-11

精益 React 学习指南 (Lean React)- 1.4 React 组件生命周期和方法

书籍完整目录

1.4 React 组件生命周期

图片描述

官方文档

1.4.1 组件

React 中组件有自己的生命周期方法,简单理解可以为组件从 出生(实例化) -> 激活 -> 销毁 生命周期 hook。通过这些 hook 方法可以自定义组件的特性。 除此之外,还可以设置一些额外的规格配置。

图片描述

这些生命周期方法都可以在调用 React.createClass 的参数对象中传入, 我们已经使用过了一些方法:

  • render

  • getInitialState

  • getDefaultProps

  • propTypes

1.4.2 mixins

类型:array mixins

mixins 可以理解为 React 的插件列表,通过这种模式在不同组件之间共享方法数据或者行为只需共享 mixin 就行,mixins 内定义的生命周期方法在组件的生命周期内都会被调用。

可能的一些疑问:

  • Q1. 如果组件已经定义了某个生命周期方法, mixin 内也定义了该方法,那么 mixin 内会被调用还是 组件的会被调用?

  • Q2. 多个插件都定义了相同生命周期的方法呢?

  • Q3. 那如果多个插件定义了 getInitialState 这种配置方法呢,有何影响?

插件模式并非继承的模式,对于问题 1、2 的答案是一样的,都会被调用,调用顺序为 mixins 数组中的顺序。

  • A1: 都会被调用

  • A2: 都会被调用

  • A3: React 会对返回结果做智能的合并,所有插件的 getInitialState 都会生效,前提条件是它们返回的字段不冲突,如果发生字段冲突,React 会提示报错。 同理如果是非 组件的规格方法,出于共享目的的一些方法在多个 mixin 中也不能冲突。

eg:

var MyMixin1 = {
    componentDidMount: function() {
        console.log('auto do something when component did mount');
    }
};

var MyMixin2 = {
    someMethod: function() {
        console.log('doSomething');
    }
};

var MyComponnet = React.createClass({
    mixins: [MyMixin1, MyMixin2],
    componentDidMount: function() {
        // 调用 mixin1 共享的方法
        this.someMethod();
    }
});

更多 mixins 的使用会在第三章中讲解。

1.4.3 statics

类型:object statics

statics 可以定义组件的类方法

eg:

var MyComponent = React.createClass({
  statics: {
    customMethod: function(foo) {
      return foo === 'bar';
    }
  }
});

MyComponent.customMethod('bar');  // true

React 的组件是 OOP 的思维,MyComponent 是一个 class,class 分为类方法和实例方法,实例方法可以访问 this, 然而类方法不能,所以我们不能在 Class 中返回状态或者属性。

1.4.4 displayName

类型:string displayName

为了显示调试信息,每个组件都会有一个名称,JSX 在转为 JS 的时候自动的设置 displayName, 如下:

// Input (JSX):
var MyComponent = React.createClass({ });

// Output (JS):
var MyComponent = React.createClass({displayName: "MyComponent", });

当然我们也可以自定义 displayName

1.4.5 生命周期方法

下图描述了整个组件的生命周期,包含的主要几种情况:

  1. 组件被实例化的时候

  2. 组件属性改变的时候

  3. 组件状态被改变的时候

  4. 组件被销毁的时候

图片描述

1.4.6 componentWillMount

void componentWillMount()

条件:第一次渲染阶段在调用 render 方法前会被调用
作用:该方法在整个组件生命周期只会被调用一次,所以可以利用该方法做一些组件内部的初始化工作

1.4.7 componentDidMount

void componentDidMount()

条件:第一次渲染成功过后,组件对应的 DOM 已经添加到页面后调用
作用:这个阶段表示组件对应的 DOM 已经存在,我们可以在这个时候做一些依赖 DOM 的操作或者其他的一些如请求数据,和第三方库整合的操作。如果嵌套了子组件,子组件会比父组件优先渲染,所以这个时候可以获取子组件对应的 DOM。

1.4.8 componentWillReceiveProps(newProps)

void componentWillReceiveProps(
  object nextProps
)

条件: 当组件获取新属性的时候,第一次渲染不会调用
用处: 这个时候可以根据新的属性来修改组件状态
eg:

    componentWillReceiveProps: function(nextProps) {
      this.setState({
        likesIncreasing: nextProps.likeCount > this.props.likeCount
      });
    }

注意: 这个时候虽说是获取新属性,但并不能确定属性一定改变了,例如一个组件被多次渲染到 DOM 中,如下面:

    var Component = React.createClass({
        componentWillReceiveProps: function(nextProps) {
            console.log('componentWillReceiveProps', nextProps.data.bar);
        },
        rener: function() {
            return <div> {this.props.data.bar} </div>
        }
    });

    var container = document.getElementById('container');
    var mydata = {bar: 'drinks'};
    ReactDOM.render(<Component data={mydata} />, container);
    ReactDOM.render(<Component data={mydata} />, container);
    ReactDOM.render(<Component data={mydata} />, container);

结果会输出两次 componentWillReceiveProps,虽然属性数据没有改变,但是仍然会调用 componentWillReceiveProps 方法。

参考 Facebook (A=>B) => (B => A)

1.4.9 shouldComponentUpdate(nextProps, nextState)

boolean shouldComponentUpdate(
  object nextProps, object nextState
)

条件: 接收到新属性或者新状态的时候在 render 前会被调用(除了调用 forceUpdate 和初始化渲染以外)
用处: 该方法让我们有机会决定是否重渲染组件,如果返回 false,那么不会重渲染组件,借此可以优化应用性能(在组件很多的情况)。

1.4.10 componentWillUpdate

void componentWillUpdate(
  object nextProps, object nextState
)

条件:当组件确定要更新,在 render 之前调用
用处:这个时候可以确定一定会更新组件,可以执行更新前的操作
注意:方法中不能使用 setState ,setState 的操作应该在 componentWillReceiveProps 方法中调用

1.4.11 componentDidUpdate

void componentDidUpdate(
  object prevProps, object prevState
)

条件:更新被应用到 DOM 之后
用处:可以执行组件更新过后的操作

1.4.12 生命周期与单向数据流

我们知道 React 的核心模式是单向数据流,这不仅仅是对于组件级别的模式,在组件内部 的生命周期中也是应该符合单向数据的模式。数据从组件的属性流入,再结合组件的状态,流入生命周期方法,直到渲染结束这都应该是一个单向的过程,其间不能随意改变组件的状态。

图片描述

1.4.13 实例练习:通过 mixin 打印出组件生命周期的执行顺序

@todo

查看原文

赞 13 收藏 36 评论 4

LuckyJing 赞了文章 · 2018-01-08

Cookie 在前端中的实践

本篇文章的主题,用 Node.js 搭一个服务,来看看 Cookie 的实际应用场景

环境配置

我们新建一个文件 main.js,并在 main.js 写入以下代码:

const express = require('express')
const app = express()

app.listen(3000, err => {
  if (err) {
    return console.log(err)
  }
  console.log('---- 打开 http://localhost:3000 吧----')
})

app.get('/', (req, res) => {
  res.send('<h1>hello world!</h1>')
})
node main.js

// 一个本地服务就跑起来了,现在打开 http://localhost:3000
// 就可以看到一个大大的 hello world!

Cookie 是怎样工作的

在介绍 Cookie 是什么之前,我们来看看 Cookie 是如何工作的:

1. 首先,我们假设当前域名下还是没有 Cookie 的
2. 接下来,浏览器发送了一个请求给服务器(这个请求是还没带上 Cookie 的)
3. 服务器设置 Cookie 并发送给浏览器(当然也可以不设置)
4. 浏览器将 Cookie 保存下来
5. 接下来,以后的每一次请求,都会带上这些 Cookie,发送给服务器

验证

我们来验证一下

// 修改 main.js

app.get('/', (req, res) => {
  // 服务器接收到请求,在给响应设置一个 Cookie
  // 这个 Cookie 的 name 为 testName
  // value 为 testValue
  res.cookie('testName', 'testValue')
  res.send('<h1>hello world!</h1>')
})

// 保存之后,重启服务器
// node main.js

现在打开 http://localhost:3000

  1. 我们看到 Request Headers 并没有 Cookie 这个字段
  2. 但是 Response Headers 有了 Set-Cookie 这个字段

现在我们刷新一下页面,相当于重新向 http://localhost:3000/ 这个地址发起了一次请求。

现在我们就可以看到 Cookie 字段已经带上了,再刷新几次看 Cookie 也还是在的。

document.cookie

JS 提供了获取 Cookie 的方法:document.cookie,我们先去设置多几个 Cookie。

app.get('/', (req, res) => {
  res.cookie('testName0', 'testValue0')
  res.cookie('testName1', 'testValue1')
  res.cookie('testName2', 'testValue2')
  res.cookie('testName3', 'testValue3')
  res.send('<h1>hello world!</h1>')
})

我们可以看到,Cookie 就是一段字符串。但这个字符串是有格式的,由键值对 key=value 构成,键值对之间由一个分号一个空格隔开。

什么是 Cookie

说了这么多,大家应该知道 Cookie 是什么吧。整理一下有以下几个点:

  • Cookie 就是浏览器储存在用户电脑上的一小段文本文件
  • Cookie 是纯文本格式,不包含任何可执行的代码
  • Cookie 由键值对构成,由分号和空格隔开
  • Cookie 虽然是存储在浏览器,但是通常由服务器端进行设置
  • Cookie 的大小限制在 4kb 左右

Cookie 的属性选项

每个 Cookie 都有一定的属性,如什么时候失效,要发送到哪个域名,哪个路径等等。在设置任一个 Cookie 时都可以设置相关的这些属性,当然也可以不设置,这时会使用这些属性的默认值。

expires / max-age

expires / max-age 都是控制 Cookie 失效时刻的选项。如果没有设置这两个选项,则默认有效期为 session,即会话 Cookie。这种 Cookie 在浏览器关闭后就没有了。

expires

expires 选项用来设置 Cookie 什么时间内有效,expires 其实是 Cookie 失效日期。
expires 必须是 GMT 格式的时间(可以通过 new Date().toGMTString() 或者 new Date().toUTCString() 来获得)

app.get('/', (req, res) => {
  // 这个 Cookie 设置十秒后失效
  res.cookie('testName0', 'testValue0', {
    expires: new Date(Date.now() + 10000)
  })
  // 这个 Cookie 不设置失效时间
  res.cookie('testName1', 'testValue1')
  res.send('<h1>hello world!</h1>')
})

上面的代码服务器设置了两个 Cookie,一个设置了失效刻,另外一个没有设置,也就是默认的失效时刻 session。现在我们重启服务并且刷新一下页面。

现在响应头部已经加上了响应的设置失效时刻的字段了。在控制台输入下面的代码。

console.log(`现在的 cookie 是:${document.cookie}`)
setTimeout(() => {
  console.log(`5 秒后的 cookie 是:${document.cookie}`)
}, 5000)
setTimeout(() => {
  console.log(`10 秒后的 cookie 是:${document.cookie}`)
}, 10000)

所以,Cookie 的失效时刻到了之后,通过 document.cookie 就访问不到这个 Cookie 了,当然以后发送请求也不会再带上这个失效的 Cookie 了。

max-age

expires 是 http/1.0 协议中的选项,在新的 http/1.1 协议中 expires 已经由 max-age 选项代替,两者的作用都是限制 Cookie 的有效时间。expires 的值是一个时间点 (Cookie 失效时刻 = expires),而 max-age 的值是一个以为单位时间段 (Cookie 失效时刻 = 创建时刻 + max-age)

// 设置 max-age,就是设置从 cookie 创建的时刻算起
// 再过多少秒 cookie 就会失效
app.get('/', (req, res) => {
  res.cookie('testName0', 'testValue0', {
    // express 这个参数是以毫秒来做单位的
    // 实际发送给浏览器就会转换为秒
    // 十秒后失效
    maxAge: 10000
  })
  res.cookie('testName1', 'testValue1')
  res.send('<h1>hello world!</h1>')
})

优先级

如果同时设置了 max-age 和 expires,以 max-age 的时间为准。

app.get('/', (req, res) => {
  res.cookie('name0', 'value0')
  res.cookie('name1', 'value1', {
    expires: new Date(Date.now() + 30 * 1000),
    maxAge: 60 * 1000
  })
  res.cookie('name2', 'value2', {
    maxAge: 60 * 1000
  })
  res.send('<h1>hello world!</h1>')
})

domain 和 path

namedomainpath 可以标识一个唯一的 Cookie。domainpath 两个选项共同决定了 Cookie 何时被浏览器自动添加到请求头部中发送出去。具体是什么原理请看 Cookie 的作用域和作用路径 这个章节。

如果没有设置这两个选项,则会使用默认值。domain 的默认值为设置该 Cookie 的网页所在的域名,path 默认值为设置该 Cookie 的网页所在的目录。

secure

secure 选项用来设置 Cookie 只在确保安全的请求中才会发送。当请求是 HTTPS 或者其他安全协议时,包含 secure 选项的 Cookie 才能被保存到浏览器或者发送至服务器。

默认情况下,Cookie 不会带 secure 选项(即为空)。所以默认情况下,不管是 HTTPS 协议还是 HTTP 协议的请求,Cookie 都会被发送至服务端。

httpOnly

这个选项用来设置 Cookie 是否能通过 js 去访问。默认情况下,Cookie 不会带 httpOnly 选项(即为空),客户端是可以通过 js 代码去访问(包括读取、修改、删除等)这个 Cookie 的。当 Cookie 带 httpOnly 选项时,客户端则无法通过 js 代码去访问(包括读取、修改、删除等)这个 Cookie。

看看代码吧,修改 main.js,保存重启服务,刷新页面。

app.get('/', (req, res) => {
  res.cookie('notHttpOnly', 'testValue')
  res.cookie('httpOnlyTest', 'testValue', {
    httpOnly: true
  })
  res.send('<h1>hello world!</h1>')
})

看图,设置了 httpOnly 的 Cookie 多了一个勾。而且通过 document.cookie 无法访问到那个 Cookie。

在客户端是不能通过 js 代码去设置 一个 httpOnly 类型的 Cookie 的,这种类型的 Cookie 只能通过服务端来设置,发送请求的时候,我们看到请求头还是会带上这个设置了 httpOnly 的 Cookie,如下图。

设置 Cookie

明确一点:Cookie 可以由服务端设置,也可以由客户端设置。看到这里相信大家都可以理解了吧。

服务端设置 Cookie

看回刚刚的那张图,我们设置了很多个 Cookie。

  • 一个 Set-Cookie 字段只能设置一个 Cookie,当你要想设置多个 Cookie,需要添加同样多的 Set-Cookie 字段
  • 服务端可以设置 Cookie 的所有选项:expires、domain、path、secure、HttpOnly

客户端设置 Cookie

在网页即客户端中我们也可以通过 js 代码来设置 Cookie。

设置

document.cookie = 'name=value'

可以设置 Cookie 的下列选项:expires、domain、path,各个键值对之间都要用 ;空格 隔开

document.cookie='name=value; expires=Thu, 26 Feb 2116 11:50:25 GMT; domain=sankuai.com; path=/';

secure

只有在 https 协议的网页中,客户端设置 secure 类型的 Cookie 才能成功

HttpOnly

客户端中无法设置 HttpOnly 选项

删除 Cookie

Cookie 的 name、path 和 domain 是唯一标识一个 Cookie 的。我们只要将一个 Cookie 的 max-age 设置为 0,就可以删除一个 Cookie 了。

let removeCookie = (name, path, domain) => {
  document.cookie = `${name}=; path=${path}; domain=${domain}; max-age=0`
}

Cookie 的作用域和作用路径

作用域

在说这个作用域之前,我们先来对域名做一个简单的了解。

子域,是相对父域来说的,指域名中的每一个段。各子域之间用小数点分隔开。放在域名最后的子域称为最高级子域,或称为一级域,在它前面的子域称为二级域。

以下图为例,news.163.comsports.163.com 是子域,163.com 是父域。

当 Cookie 的 domain 为 news.163.com,那么访问 news.163.com 的时候就会带上 Cookie;
当 Cookie 的 domain 为 163.com,那么访问 news.163.comsports.163.com 就会带上 Cookie

作用路径

当 Cookie 的 domain 是相同的情况下,也有是否带上 Cookie 也有一定的规则。

在子路径内可以访问访问到父路径的 Cookie,反过来就不行。

看看例子,还是先修改 main.js

app.get('/parent', (req, res) => {
  res.cookie('parent-name', 'parent-value', {
    path: '/parent'
  })
  res.send('<h1>父路径!</h1>')
})

app.get('/parent/childA', (req, res) => {
  res.cookie('child-name-A', 'child-value-A', {
    path: '/parent/childA'
  })
  res.send('<h1>子路径A!</h1>')
})

app.get('/parent/childB', (req, res) => {
  res.cookie('child-name-B', 'child-value-B', {
    path: '/parent/childB'
  })
  res.send('<h1>子路径B!</h1>')
})
下面这里的 “域” 应该改为路径



参考文章

查看原文

赞 36 收藏 95 评论 4

认证与成就

  • 获得 120 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-08-25
个人主页被 670 人浏览