首页 文章 精选 留言 我的

精选列表

搜索[入门],共9380篇文章
优秀的个人博客,低调大师

Spark入门

http://spark.incubator.apache.org/ http://spark.incubator.apache.org/documentation.html http://ampcamp.berkeley.edu/3/exercises/data-exploration-using-spark.html, 非常好的hand-on exercises 源码分析 http://jerryshao.me/archive.html http://www.cnblogs.com/jerrylead/ 下载 http://spark.apache.org/downloads.html 下载需要的版本,解压就可以 当然想自己编译也可以 编译 当前spark支持Maven编译, http://spark.apache.org/docs/latest/building-with-maven.html 可以简单的这样编译, mvn -DskipTests clean package Spark如果需要通过Hadoop-Client来访问HDFS, 由于不同版本的Hadoop的client协议不同, 所以编译时需要匹配特定的Hadoop版本进行编译,参考上面的链接 同样如果需要用Yarn做资源管理,也需要在编译的时候指明 Quick Start http://spark.apache.org/docs/latest/quick-start.html Spark interactive Spark很方便的一点是,支持命令行的方式 这样可以简单的学习和调试,或者interactively的进行数据分析,很赞 Scala shell (./spark-shell) Python interpreter (./pyspark) 用--help来查看如果使用 (./spark-shell --help) 并且shell不但可以local使用,可以连接实际的集群,进行大规模的数据分析,./bin/spark-shell --master spark://IP:PORT 下面看个简单的例子,统计下README的行数,可以看到在shell中会自动创建SparkContext, sc scala> val textFile = sc.textFile("README.md") textFile: spark.RDD[String] = spark.MappedRDD@2ee9b6e3 scala> textFile.count() // Number of items in this RDD res0: Long = 126 看个稍微复杂点的,里面用到java的math包, 计算文件里面最长的行包含多少单词 scala> import java.lang.Math import java.lang.Math scala> textFile.map(line => line.split(" ").size).reduce((a, b) => Math.max(a, b)) res5: Int = 15 Standalone Applications 可以使用Scala,Java,或Python来写Spark的app 这里描述下用scala是如何做的,Java和Python参考上面的链接, 先写出应用,很简单,统计出文档里面,包含a和b的行数各是多少 /* SimpleApp.scala */ import org.apache.spark.SparkContext import org.apache.spark.SparkContext._ import org.apache.spark.SparkConf object SimpleApp { def main(args: Array[String]) { val logFile = "YOUR_SPARK_HOME/README.md" // Should be some file on your system val conf = new SparkConf().setAppName("Simple Application") val sc = new SparkContext(conf) val logData = sc.textFile(logFile, 2).cache() val numAs = logData.filter(line => line.contains("a")).count() val numBs = logData.filter(line => line.contains("b")).count() println("Lines with a: %s, Lines with b: %s".format(numAs, numBs)) } } 用sbt编译,创建simple.sbt,注意sbt中每行中间要空行来分割 name := "Simple Project" version := "1.0" scalaVersion := "2.10.4" libraryDependencies += "org.apache.spark" %% "spark-core" % "1.1.0" 创建如下的目录结构, # Your directory layout should look like this $ find . . ./simple.sbt ./src ./src/main ./src/main/scala ./src/main/scala/SimpleApp.scala 编译, # Package a jar containing your application $ sbt package ... [info] Packaging {..}/{..}/target/scala-2.10/simple-project_2.10-1.0.jar 提交任务,并得到结果 # Use spark-submit to run your application $ YOUR_SPARK_HOME/bin/spark-submit \ --class "SimpleApp" \ --master local[4] \ target/scala-2.10/simple-project_2.10-1.0.jar ... Lines with a: 46, Lines with b: 23 maven编译Spark应用 参考,http://www.scala-lang.org/old/node/345https://blogs.oracle.com/arungupta/entry/scala_and_maven_getting_started sbt用不惯,也懒的学,连spark都用maven编译,所以介绍如何用maven来编译scala写的spark应用 比较简单Maven本身提供scala的archetype,scala-archetype-simple mvn archetype:generate \ -DarchetypeGroupId=org.scala-tools.archetypes \ -DarchetypeArtifactId=scala-archetype-simple \ -DremoteRepositories=http://scala-tools.org/repo-releases\ -DgroupId=com.xxx \ -DartifactId=sparkapp \ -Dversion=1.0-SNAPSHOT 然后再pom里面加上依赖, <dependency> <groupId>org.apache.spark</groupId> <artifactId>spark-core_2.10</artifactId> <version>1.1.0</version> </dependency> 默认没有maven-assembly-plugin,加上 <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass></mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> 别忘了修改pom里面scala的版本,然后就mvn package,就ok了 Running the Examples 同时Spark也提供了些例子大家可以参考,也可以直接运行,如下 ./bin/run-example SparkPi 得到结果,Pi is roughly 3.14302 也可以看下,SparkPi的实现,很简单, package org.apache.spark.examples import scala.math.random import org.apache.spark._ /** Computes an approximation to pi */ object SparkPi { def main(args: Array[String]) { val conf = new SparkConf().setAppName("Spark Pi") val spark = new SparkContext(conf) val slices = if (args.length > 0) args(0).toInt else 2 val n = 100000 * slices val count = spark.parallelize(1 to n, slices).map { i => val x = random * 2 - 1 val y = random * 2 - 1 if (x*x + y*y < 1) 1 else 0 }.reduce(_ + _) println("Pi is roughly " + 4.0 * count / n) spark.stop() } } Programming Guide http://spark.apache.org/docs/latest/quick-start.html Initializing Spark 开始用Spark, 首先需要创建SparkContext This is done through the following constructor: new SparkContext(master, appName, [sparkHome], [jars]) 1. master参数, 就是指明spark集群的位置url, 支持如下一些格式 local, Run Spark locally with one worker thread (i.e. no parallelism at all). local[K],Run Spark locally with K worker threads (ideally, set this to the number of cores on your machine). spark://HOST:PORT, Connect to the givenSpark standalone clustermaster. The port must be whichever one your master is configured to use, which is 7077 by default. mesos://HOST:PORT,Connect to the givenMesoscluster. The host parameter is the hostname of the Mesos master. The port must be whichever one the master is configured to use, which is 5050 by default. If no master URL is specified, the spark shelldefaults to “local”. 2. appName, 这个很好理解,给个名字 3. 如果你需要部署到分布式集群上, 那么就需要指定后面两个参数,sparkHome, spark在worker上的的安装目录,必须保持一致jars, 所有的jar, 包含你的应用和依赖, spark会部署到所有worker上, 并自动加入到classpath Resilient Distributed Datasets (RDDs) 首先数据如果要在Spark中被处理,首先需要导入成RDD 如何生成RDD有两种方式, Parallelized collections 可以将进程中的数据结构转换为RDD,这样挺方便,你可以把数据从任意源中先读出来,然后转成RDD 但前提是数据不能太大,否则效率会是问题,因为这种case是会把数据copy到各个节点 对于Scala,在Scala collection上面调用SparkContext的parallelize函数, 作用就是把collection分成多个slices, 并分发到各个分布式节点上, 这样便于并行处理 scala> val data = Array(1, 2, 3, 4, 5) scala> val distData = sc.parallelize(data) //会将data, 分成slices, 并分发到各个节点 distData: spark.RDD[Int] = spark.ParallelCollection@10d13e3e 这里slices, 其实spark会根据集群的CPU情况自动分配, 但是你也可以自己设置 sc.parallelize(data, 10) External Datasets 当然更为常见的是,从外部数据集中读取数据,转换为RDD Spark最常用的是从文件读取数据,local,hdfs或s3等 Spark也支持从数据库,如Cassandra, HBase, 甚至是Mysql读取数据(通过jdbcRDD) 先看看使用SparkContext的textFile方法来加载文件, Text file RDDs can be created usingSparkContext’stextFilemethod. This method takes an URI for the file (either a local path on the machine, or ahdfs://,s3n://,kfs://, etc URI)and reads it as a collection of lines. Here is an example invocation: scala> val distFile = sc.textFile("data.txt") distFile: spark.RDD[String] = spark.HadoopRDD@1d4cee08 textFile一样可以指定slices参数, 默认Spark会为每个block file创建一个slice, 你可以选择更多的slices, 但不能比block数少 需要注意的是,对于外部存储,是各个worker独自去读自己的数据的,所以如果用分布式存储没有问题,但如果用local存储就需要保证在每个节点的改目录上都要有这个文件 所以如果不用分布式存储,就使用共享存储,不然比较麻烦,需要自己copy到每个节点 textFile会将文件读出成collection of lines,所以可以for直接遍历每行 并且还支持目录,压缩文件,或通配符的读取,textFile("/my/directory"),textFile("/my/directory/*.txt"), andtextFile("/my/directory/*.gz"). 除了textFile,还支持其他的读取接口, SparkContext.wholeTextFiles,用于读一个目录,返回(filename, content) pairs SequeceFiles, 使用sequenceFile[K, V]接口, K, V表示类型, 应该是Writable的子类SparkContext.hadoopRDDmethod, which takes an arbitraryJobConfand input format class, key class and value class. Set these the same way you would for a Hadoop job with your input source. RDD Operations 现在已经把原始数据导入成RDD,后面就是如果继续处理 Spark对于RDD有两种操作,transformations和actions transformation具有lazy特性,其实就是调用transform的时候,不会真的执行,只会记录下这个操作而已 action,会真正的runjob去执行 下面会列出最常用的的transform和action 这个链接会给出所有操作的例子,不错http://homepage.cs.latrobe.edu.au/zhe/ZhenHeSparkRDDAPIExamples.html Transformations map(func) Return a new distributed dataset formed by passing each element of the source through a functionfunc. filter(func) Return a new dataset formed by selecting those elements of the source on whichfuncreturns true. flatMap(func), map是1->1, flatMap是1->0或多个,所以这个函数返回的是Seq,最终会把所有的Seq合成一个 Similar to map, but each input item can be mapped to 0 or more output items (sofuncshould return a Seq rather than a single item). Example, sc.parallelize(List(1, 2, 3), 2).flatMap(x => List(x, x, x)).collect res85: Array[Int] = Array(1, 1, 1, 2, 2, 2, 3, 3, 3) 为何需要下面两个,当map里面有比较耗时的初始化操作,比如链接和断开数据库,肯定不想每个item都做一遍 所以用mapPartitons就可以一个partition只做一次,输入输出都是iterator 而mapPartitionsWithSplit只是多传入split index而已,也许对于某些场景需要知道mapPartitions(func) Similar to map, but runs separately on each partition (block) of the RDD, sofuncmust be of type Iterator[T] => Iterator[U] when running on an RDD of type T.mapPartitionsWithSplit(func) Similar to mapPartitions, but also providesfuncwith an integer value representing the index of the split, sofuncmust be of type (Int, Iterator[T]) => Iterator[U] when running on an RDD of type T. pipe(command,[envVars]), 以shell命令处理RDD数据 Takes the RDD data of each partition and sends it via stdin to a shell-command. The resulting output of the command is captured and returned as a RDD of string values. Example, val a = sc.parallelize(1 to 9, 3) a.pipe("head -n 1").collect res2: Array[String] = Array(1, 4, 7) sample(withReplacement,fraction,seed), 随机抽样 Sample a fractionfractionof the data, with or without replacement, using a given random number generator seed. withReplacement,是否放回抽样 fraction,比例,0.1表示10% seed,伪随机,相同的seed得到的随机序列是一样的,所以如果不设seed,同一段代码执行两遍得到的随机序列是一样的 example, val a = sc.parallelize(1 to 10000, 3) a.sample(false, 0.1, 0).count res24: Long = 960 集合操作union(otherDataset) Return a new dataset that contains the union of the elements in the source dataset and the argument.intersection(otherDataset) Return a new RDD that contains the intersection of elements in the source dataset and the argument.distinct([numTasks])) Return a new dataset that contains the distinct elements of the source dataset. Example, val c = sc.parallelize(List("Gnu", "Cat", "Rat", "Dog", "Gnu", "Rat"), 2) c.distinct.collect res6: Array[String] = Array(Dog, Gnu, Cat, Rat) 下面的操作都是基于key-value pairs的,而其他的操作大都是不限制数据类型 先看几种生成key-value pairs的函数,KeyBy(func)用于构建key-value pairs,func是用于基于item来构建key,item作为value Example, val a = sc.parallelize(List("dog", "salmon", "salmon", "rat", "elephant"), 3) val b = a.keyBy(_.length) b.collect res26: Array[(Int, String)] = Array((3,dog), (6,salmon), (6,salmon), (3,rat), (8,elephant) Zip Joins two RDDs by combining the i-th of either partition with each other. Examples, val a = sc.parallelize(List("dog", "cat", "owl", "gnu", "ant"), 2) val b = sc.parallelize(1 to a.count.toInt, 2) val c = a.zip(b) c.sortByKey(true).collect res74: Array[(String, Int)] = Array((ant,5), (cat,2), (dog,1), (gnu,4), (owl,3))) cartesian( otherDataset), 笛卡尔积 When called on datasets of types T and U, returns a dataset of (T, U) pairs (all pairs of elements). Example, val x = sc.parallelize(List(1,2,3,4,5)) val y = sc.parallelize(List(6,7,8,9,10)) x.cartesian(y).collect res0: Array[(Int, Int)] = Array((1,6), (1,7), (1,8), (1,9), (1,10), (2,6), (2,7), (2,8), (2,9), (2,10), (3,6), (3,7), (3,8), (3,9), (3,10), (4,6), (5,6), (4,7), (5,7), (4,8), (5,8), (4,9), (4,10), (5,9), (5,10)) 再看看基于key-value pairs的操作, 这类操作要求是key-value pairs,基于PairRDDFunctions类 这类操作往往需要shuffle,如group或aggregate 并且都有个可选参数numTasks,默认就是parent的partition数目,当然也可以指定 groupByKey([numTasks]) When called on a dataset of (K, V) pairs, returns a dataset of (K, Seq[V]) pairs. 如果group的目的是做sum或average,那用reduceByKeyorcombineByKey会有更好的效率 Example,val a = sc.parallelize(List("dog", "tiger", "lion", "cat", "spider", "eagle"), 2) val b = a.keyBy(_.length) b.groupByKey.collect res11: Array[(Int, Seq[String])] = Array((4,ArrayBuffer(lion)), (6,ArrayBuffer(spider)), (3,ArrayBuffer(dog, cat)), (5,ArrayBuffer(tiger, eagle))) reduceByKey(func, [numTasks]) When called on a dataset of (K, V) pairs, returns a dataset of (K, V) pairs where the values for each key are aggregated using the given reduce function. 经典的例子,统计文本词数,val lines = sc.textFile("data.txt") val pairs = lines.map(s => (s, 1)) val counts = pairs.reduceByKey((a, b) => a + b) aggregateByKey(zeroValue)(seqOp,combOp, [numTasks]) When called on a dataset of (K, V) pairs, returns a dataset of (K, U) pairs where the values for each key are aggregated using the given combine functions and a neutral "zero" value. Allows an aggregated value type that is different than the input value type, while avoiding unnecessary allocations. 这个function看着比较难懂, aggreateByKey(zeroValue: U)(seqOp: (U, T)=> U, combOp: (U, U) =>U) 写成这样好懂点, 首先和reduceByKey的不同在于,reduceByKey输入输出都是(K, V) 而aggreateByKey输出是(K, U),可以不同于输入(K, V) 这里需要定义3个东西,类似combineByKey zeroValue: U,初始值,比如空列表{} seqOp: (U, T)=> U, seq操作符,描述如何将T合并入U,比如如何将item合并到列表 combOp: (U, U) =>U,comb操作符,描述如果合并两个U,比如合并两个列表 所以aggreateByKey可以看成更高抽象的,更灵活的reduce或group Example, val z = sc.parallelize(List(1,2,3,4,5,6), 2) z.aggreateByKey(0)(math.max(_, _), _ + _) res40: Int = 9 val z = sc.parallelize(List("a","b","c","d","e","f"),2) z.aggreateByKey("")(_ + _, _+_) res115: String = abcdef sortByKey([ascending], [numTasks]) When called on a dataset of (K, V) pairs where K implements Ordered, returns a dataset of (K, V) pairs sorted by keys in ascending or descending order, as specified in the booleanascendingargument. join(otherDataset, [numTasks]) When called on datasets of type (K, V) and (K, W), returns a dataset of (K, (V, W)) pairs with all pairs of elements for each key. Outer joins are also supported throughleftOuterJoinandrightOuterJoin. Example, val a = sc.parallelize(List("dog", "salmon","elephant"), 3) val b = a.keyBy(_.length) val c = sc.parallelize(List("dog","rabbit","turkey","wolf"), 3) val d = c.keyBy(_.length) b.join(d).collect res0: Array[(Int, (String, String))] = Array((6,(salmon,rabbit)), (6,(salmon,turkey)), (3,(dog,dog))) cogroup(otherDataset, [numTasks]) When called on datasets of type (K, V) and (K, W), returns a dataset of (K, Iterable<V>, Iterable<W>)tuples. This operation is also calledgroupWith. groupByKey, 可以用于group一个rdd里面的数据,cogroup可以同时group多个rdd的数据 Example, val a = sc.parallelize(List(1, 2, 1, 3), 1) val b = a.map((_, "b")) val c = a.map((_, "c")) val d = a.map((_, "d")) b.cogroup(c, d).collect res2: Array[(Int, (Iterable[String], Iterable[String], Iterable[String]))] = Array((1,(CompactBuffer(b, b),CompactBuffer(c, c),CompactBuffer(d, d))), (3,(CompactBuffer(b),CompactBuffer(c),CompactBuffer(d))), (2,(CompactBuffer(b),CompactBuffer(c),CompactBuffer(d)))) 下面的操作用于改变partition的个数, 为何要改变partition个数?因为有些操作会大幅的增加或减少RDD中的数据 比如filter,可以会过滤掉极大部分的数据,所以此时可以用coalesce来减少partition的数目 repartition,不光可以减少,也可以增加partition的数目,而且一定会用reshuffle来随机的balance各个partitioncoalesce(numPartitions) Decrease the number of partitions in the RDD to numPartitions. Useful for running operations more efficiently after filtering down a large dataset. repartition(numPartitions) Reshuffle the data in the RDD randomly to create either more or fewer partitions and balance it across them. This always shuffles all data over the network. Actions reduce(func) Aggregate the elements of the dataset using a functionfunc(which takes two arguments and returns one). The function should be commutative and associative so that it can be computed correctly in parallel. Example, val a = sc.parallelize(1 to 100, 3) a.reduce(_ + _) res41: Int = 5050 collect() Return all the elements of the dataset as an array at the driver program. This is usually useful after a filter or other operation that returns a sufficiently small subset of the data. Example, val c = sc.parallelize(List("Gnu", "Cat", "Rat", "Dog", "Gnu", "Rat"), 2) c.collect res29: Array[String] = Array(Gnu, Cat, Rat, Dog, Gnu, Rat) count() Return the number of elements in the dataset. Example, val c = sc.parallelize(List("Gnu", "Cat", "Rat", "Dog"), 2) c.count res2: Long = 4 first() Return the first element of the dataset (similar to take(1)). Example, val c = sc.parallelize(List("Gnu", "Cat", "Rat", "Dog"), 2) c.first res1: String = Gnu take(n) Return an array with the firstnelements of the dataset. Note that this is currently not executed in parallel. Instead, the driver program computes all the elements. Example, val b = sc.parallelize(List("dog", "cat", "ape", "salmon", "gnu"), 2) b.take(2) res18: Array[String] = Array(dog, cat) takeSample(withReplacement,num,seed) Return an array with a random sample ofnumelements of the dataset, with or without replacement, using the given random number generator seed. 和Sample不同, 是action所以返回的是array而不是RDD 第二个参数num会精确指定抽样数,而不是比例 返回array时,会进行随机排序 saveAsTextFile(path) Write the elements of the dataset as a text file (or set of text files) in a given directory in the local filesystem, HDFS or any other Hadoop-supported file system. Spark will call toString on each element to convert it to a line of text in the file. Example, val a = sc.parallelize(1 to 10000, 3) a.saveAsTextFile("mydata_a") 这个例子会在mydata_a目录下存3个文件,part-00000,part-00001,part-00002 saveAsSequenceFile(path) Write the elements of the dataset as a Hadoop SequenceFile in a given path in the local filesystem, HDFS or any other Hadoop-supported file system. This is only available on RDDs of key-value pairs that either implement Hadoop's Writable interface or are implicitly convertible to Writable (Spark includes conversions for basic types like Int, Double, String, etc). countByKey() Only available on RDDs of type (K, V). Returns a `Map` of (K, Int) pairs with the count of each key. Example, val c = sc.parallelize(List((3, "Gnu"), (3, "Yak"), (5, "Mouse"), (3, "Dog")), 2) c.countByKey res3: scala.collection.Map[Int,Long] = Map(3 -> 3, 5 -> 1) foreach(func) Run a functionfuncon each element of the dataset. This is usually done for side effects such as updating an accumulator variable (see below) or interacting with external storage systems. Example, val c = sc.parallelize(List("cat", "dog", "tiger", "lion", "gnu", "crocodile", "ant", "whale", "dolphin", "spider"), 3) c.foreach(x => println(x + "s are yummy")) lions are yummy gnus are yummy crocodiles are yummy ants are yummy whales are yummy dolphins are yummy spiders are yummy Passing Functions to Spark 使用spark有很多的transform和action作为元操作,很方便 但总有一些特殊的逻辑,需要用比如map引用到数据上面去,那么如果来定义这些function 1. 对于很短的逻辑,直接用Anonymous function syntax,这是最方便的lineLengths.reduce((a, b) => a + b) 2. 当然不可能所有的逻辑都那么短,所以可以使用Static methods in a global singleton object 因为Scala里面没有静态类,所以global singleton object其实就是静态类的概念object MyFunctions { def func1(s: String): String = { ... } } myRdd.map(MyFunctions.func1) 上面两种是推荐的形式,如果你非要用普通类的成员函数,或是函数中用到类成员class MyClass { def func1(s: String): String = { ... } def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(func1) } }class MyClass { val field = "Hello" def doStuff(rdd: RDD[String]): RDD[String] = { rdd.map(x => field + x) } } 那么需要了解,这样整个类对象都需要被send到集群上去执行,比较低效 对于用到成员变量的case,可以用局部变量替换绕过def doStuff(rdd: RDD[String]): RDD[String] = { val field_ = this.field rdd.map(x => field_ + x) } RDD Persistence 在一个stage中,各个transform就是function的调用,中间结果是不会保留的,当然出于方便或避免反复计算,也可以cache某个中间结果 这就是RDD persisitence的目的 可以选择不同的storage level, 如果使用cache(), 就是使用默认的MEMORY_ONLY 当然也可以使用persist()接口, 可以选择不同的level MEMORY_ONLY Store RDD as deserialized Java objects in the JVM. If the RDD does not fit in memory, some partitions will not be cached and will be recomputed on the fly each time they're needed. This is the default level. MEMORY_AND_DISK Store RDD as deserialized Java objects in the JVM. If the RDD does not fit in memory, store the partitions that don't fit on disk, and read them from there when they're needed. MEMORY_ONLY_SER Store RDD asserializedJava objects (one byte array per partition). This is generally more space-efficient than deserialized objects, especially when using afast serializer, but more CPU-intensive to read. MEMORY_AND_DISK_SER Similar to MEMORY_ONLY_SER, but spill partitions that don't fit in memory to disk instead of recomputing them on the fly each time they're needed. DISK_ONLY Store the RDD partitions only on disk. MEMORY_ONLY_2, MEMORY_AND_DISK_2, etc. Same as the levels above, but replicate each partition on two cluster nodes. 如何选择不同的storage level? 尽量选择memory_only 实在memory不够, 可以考虑使用memory_only_ser, 会节省些空间, 但这样的问题是每次使用都需要做反序列化, 所以要用快的序列化库 尽量不要使用disk, 因为一般重新计算也比从disk读快, 除非计算复杂度非常高 一般不需要使用replica, 因为RDD都会通过用重新计算来快速fault recovery, 除非有实时需求, 不忍容忍重新计算的时间 并且,Spark会以least-recently-used (LRU)的方式清理旧的cache,也可以通过RDD.unpersist(),手工清空某个RDD的cache Shared Variables 一般来说, 对于这种分布式架构是很难提供shared variables的, 但是出于方便的要求, Spark提供两种特殊的shared variables, broadcast variables and accumulators Broadcast Variables 通过接口广播read-only variable到每个节点, 更灵活些(和shipping a copy of it with tasks比较) 注意这个值是不可以改变的, 否则就会导致各个节点不一致 Broadcast variables allow the programmer to keep a read-only variable cached on each machine rather than shipping a copy of it with tasks. scala> val broadcastVar = sc.broadcast(Array(1, 2, 3)) //广播数据 broadcastVar: spark.Broadcast[Array[Int]] = spark.Broadcast(b5c40191-a864-4c7d-b9bf-d87e1a4e787c) scala> broadcastVar.value //通过value取到广播的值 res0: Array[Int] = Array(1, 2, 3) Accumulators 用于来实现counter, Spark支持int和double类型的accumulator, 用户可以实现其他类型的 注意的是在各个节点都可以通过+=来增加accumulator, 但只有在driver上可以read accumulator的值 The interpreter session below shows an accumulator being used to add up the elements of an array: scala> val accum = sc.accumulator(0) accum: spark.Accumulator[Int] = 0 scala> sc.parallelize(Array(1, 2, 3, 4)).foreach(x => accum += x) ... 10/09/29 18:41:08 INFO SparkContext: Tasks finished in 0.317106 s scala> accum.value res2: Int = 10 本文章摘自博客园,原文发布日期:2013-11-14

优秀的个人博客,低调大师

以太坊开发入门,完整入门

一个适合区块链新手的以太坊DApp开发教程: http://xc.hubwiz.com/course/5a952991adb3847553d205d1 一个用区块链、星际文件系统(IPFS)、Node.js和MongoDB来构建电商平台: http://xc.hubwiz.com/course/5abbb7acc02e6b6a59171dd6 收集整理了一些免费区块链、以太坊技术开发相关的文件,有需要的可以下载,文件链接: web3.js API官方文档中文版:https://pan.baidu.com/s/1hOV9hEzi7hFxJCL4LTvC6g 以太坊官方文档中文版 :https://pan.baidu.com/s/1ktODJKLMBmkOsi8MPrpIJA 以太坊白皮书中文版 :https://pan.baidu.com/s/1bzAFnzJ35hlQxJ2J4Oj-Ow Solidity的官方文档中文版 :https://pan.baidu.com/s/18yp9XjEqAHpiFm2ZSCygHw Truffle的官方文档中文版 :https://pan.baidu.com/s/1y6SVd7lSLUHK21YF5FzIUQ C#区块链编程指南 :https://pan.baidu.com/s/1sJPLqp1eQqkG7jmxqwn3EA 区块链技术指南: :https://pan.baidu.com/s/13cJxAa80I6iMCczA04CZhg 精通比特币中文版: :https://pan.baidu.com/s/1lz6te3wcQuNJm28rFvBfxg Node.js区块链开发 :https://pan.baidu.com/s/1Ldpn0DvJ5LgLqwix6eWgyg geth使用指南文档中文版 :https://pan.baidu.com/s/1M0WxhmumF_fRqzt_cegnag 以太坊DApp开发环境搭建-Ubuntu : https://pan.baidu.com/s/10qL4q-uKooMehv9X2R1qSA 以太坊DApp开发环境搭建-windows :https://pan.baidu.com/s/1cyYkhIJIFuI2oyxM9Ut0eA 以太坊DApp开发私链搭建-Ubuntu : https://pan.baidu.com/s/1aBOFZT2bCjD2o0EILBWs-g 以太坊DApp开发私链搭建-windows :https://pan.baidu.com/s/10Y6F1cqUltZNN99aJv9kAA 以太坊ganache CLI命令行参数详解:https://pan.baidu.com/s/1lnknFkwenacaeM4asOcBdg 使用truflle和infura部署以太坊合约:https://pan.baidu.com/s/1PTxSVff2vHSVUihYczRRqw IPFS安装部署与开发环境搭建-windows:https://pan.baidu.com/s/1bnhDvqCoOgAqEBZXMtVbRg

优秀的个人博客,低调大师

入门 | egg.js 入门之egg-jwt

小小继续学习,这次学习的内容是egg-jwt 相关。 创建egg项目 这里创建一个egg新项目,这里使用的是ts模式。 npm init egg --type=ts npm install 安装相关的包 这里创建并安装完成以后,需要再次初始化俩包,分别为egg-cors与egg-jwt token 生成的验证包 npm install egg-cors egg-jwt --save 配置相关插件 这里配置相关的插件 import { EggPlugin } from 'egg'; const plugin: EggPlugin = { jwt: { enable: true, package: "egg-jwt" }, cors: { enable: true, package: 'egg-cors', } }; export default plugin; 配置默认配置文件 config.jwt = { secret: "123456"//自定义 token 的加密条件字符串 }; config.security = { csrf: { enable: false, ignoreJSON: true }, domainWhiteList: ['http://localhost:8080'],//允许访问接口的白名单 }; config.cors = { origin:'*', allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH' }; 这里配置完成了相关的默认配置 在根目录声明any类型 这里需要在跟目录声明一个any类型,用于前后端发送相关的字符串参数。 import 'egg'; declare module 'egg' { interface Application { jwt: any; } } 配置相关路由 这里在app/router.ts 创建相关的路由 import { Application } from 'egg'; export default (app: Application) => { const { controller, router, jwt } = app; //正常路由 router.post('/admin/login', controller.admin.login); /* * 这里的第二个对象不再是控制器,而是 jwt 验证对象,第三个地方才是控制器 * 只有在需要验证 token 的路由才需要第二个 是 jwt 否则第二个对象为控制器 **/ router.post('/admin',jwt, controller.admin.index); }; 这里就配置完成了相关的路由。 编写路由对应的控制器 这里编写路由所对应的控制器这个控制器在app/controller/home.ts 目录下 import { Controller } from 'egg'; export default class AdminController extends Controller { // 验证登录并且生成 token public async login() { const { ctx ,app} = this; //获取用户端传递过来的参数 const data = ctx.request.body; // 进行验证 data 数据 登录是否成功 // ......... //成功过后进行一下操作 //生成 token 的方式 const token = app.jwt.sign({ username: data.username, //需要存储的 token 数据 //...... }, app.config.jwt.secret); // 生成的token = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1NjAzNDY5MDN9.B95GqH-fdRpyZIE5g_T0l8RgzNyWOyXepkLiynWqrJg // 返回 token 到前端 ctx.body = token; }; //访问admin数据时进行验证token,并且解析 token 的数据 public async index() { const { ctx ,app} = this; console.log(ctx.state.user); /* * 打印内容为:{ username : 'admin', iat: 1560346903 } * iat 为过期时间,可以单独写中间件验证,这里不做细究 * 除了 iat 之后,其余的为当时存储的数据 **/ ctx.body = {code:0,msg:'验证成功'}; } } 前端请求相匹配 这里只需要在前端的authorization字段里,添加相关的配置信息即可。 axios({ method: 'post', url: 'http://127.0.0.1:7001/admin', data: { username: 'admin', lastName: '123456' }, headers:{ // 切记 token 不要直接发送,要在前面加上 Bearer 字符串和一个空格 'Authorization':`Bearer ${token}` } }).then(res=>{ console.log(res.data) }) 这里就完成了egg.js 结合jwt完成相关的验证 小tips 这里插曲一个小tips,这里使用的是jsonwebtoken。这里使用jsonwebtoken实现token相关认证机制。 安装 这里安装相关的依赖 npm install jsonwebtoken 编写中间件 在middleware文件下新建一个jwt.ts 文件 'use strict' const fs = require('fs') const path = require('path') const jwt = require('jsonwebtoken') //引入jsonwebtoken module.exports = (options, app) => { return async function userInterceptor(ctx, next) { let authToken = ctx.header.authorization // 获取header里的authorization if (authToken) { authToken = authToken.substring(7) const res = verifyToken(authToken) // 解密获取的Token if (res.corpid && res.userid) { // 如果需要限制单端登陆或者使用过程中废止某个token,或者更改token的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效 // 此处使用redis进行保存 const redis_token = await app.redis.get('loginToken').get(res.corpid + res.userid) // 获取保存的token if (authToken === redis_token) { ctx.locals.corpid = res.corpid ctx.locals.userid = res.userid await next() } else { ctx.body = { code: 50012, msg: '您的账号已在其他地方登录' } } } else { ctx.body = { code: 50012, msg: '登录状态已过期' } } } else { ctx.body = { code: 50008, msg: '请登陆后再进行操作' } } } } // 解密,验证 function verifyToken(token) { const cert = fs.readFileSync(path.join(__dirname, '../public/rsa_public_key.pem')) // 公钥,看后面生成方法 let res = '' try { const result = jwt.verify(token, cert, { algorithms: [ 'RS256' ] }) || {} const { exp } = result, current = Math.floor(Date.now() / 1000) if (current <= exp) res = result.data || {} } catch (e) { console.log(e) } return res } 使用中间件 这里在config.default.js中加入如下的配置,实现中间件的开启和配置 // 方法一:在应用中使用中间件 config.middleware = [ 'jwt' ] config.jwt = { enable: true, ignore: [ '/api/v1/test/', '/public/' ], // 哪些请求不需要认证 } // 方法二:router中使用中间件 module.exports = app => { const jwt = app.middleware.jwt(); app.router.get('/api/v1/test/', jwt, app.controller.test.test); }; token生成 这里卸载文件里,用于调用,生成相关的token loginToken(data, expires = 7200) { const exp = Math.floor(Date.now() / 1000) + expires const cert = fs.readFileSync(path.join(__dirname, '../public/rsa_private_key.pem')) // 私钥,看后面生成方法 const token = jwt.sign({ data, exp }, cert, { algorithm: 'RS256' }) return token } 调用相关的生成方法 const token = ctx.helper.loginToken({ corpid: usersData.corpid, userid: usersData.userid }, 7200) // token生成 await app.redis.get('loginToken').set(usersData.corpid + usersData.userid, token, 'ex', 7200) // 保存到redis ctx.body = { data: { token, expires: this.config.login_token_time }, code: 1, msg: '登录成功' } // 返回前端 前端使用 这里前端使用headers,在后面加上相关的空格。 例:axios中 // request拦截器 service.interceptors.request.use(config => { if (store.getters.token) { config.headers['Authorization'] = `Bearer ${getToken()}` } return config }, error => { console.log(error) Promise.reject(error) }) 这里就完成了相关jwt的生成与使用。

优秀的个人博客,低调大师

React 入门

共计 10137 字,阅读时长约 25 分钟 谁都没有看见过风,更不用说你和我了。但是当纸币在飘的时候,我们知道那是风在数钱。 React 影响着我们工作的方方面面,我们每天都在使用它,只窥其表却难以窥其里。正所谓看不如写,本篇文章的目的就是从原理层面探究 React 是如何工作的。 工具— 在写文章之前,为了方便理解,我准备了一个懒人调试仓库 simple_react[1] ,这个仓库将 benchmark 用例(只有两个 ^ ^)和 React 源码共同放在 src 文件夹中,通过 snowpack 进行热更新,可以直接在源码中加入 log 和 debuger 进行调试。当然这里的“源码”并不是真的源码,因为 React 源码中充斥着巨量的 dev 代码和不明确的功能函数,所以我对源码进行了整理,用 typescript 对类型进行了规范,删除了大量和核心流程无关的代码(当然也误删了一些有关的 ^ ^)。 如果你只是希望了解 React 的运行流程而不是写一个可以用的框架的话,那么这个仓库完全可以满足你学习的需要。当然,这个仓库基于 React16.8 ,虽然这个版本并不包括当前的航道模型 Lane 等新特性,但是是我个人认为比较稳定且更适合阅读的一个版本。 (如果希望调试完整的源码,也可以参考 拉取源码[2] 通过 yarn link 来进行 debug) 文章结构— fiber 架构设计及首次渲染流程 事件委托机制 状态的更新 时间片 在了解 React 是如何工作之前,我们应该确保了解几点有关 React 的基础知识。 Why Framework— 首先,我们需要知道使用框架对于开发的意义是什么。如果我们还处于远古时期使用纯 JS 的阶段,每次数据的改变都会引发组件的展示状态改变,因此我们需要去手动的操作 DOM 。如果在某一秒内,数据异步的连续改变了几十次,根据展示逻辑我们也需要连续对 DOM 进行几十次修改。频繁的 DOM 操作对网页性能的影响是很大的,当然,创建 DOM 元素和修改 DOM 元素的属性都不过分消耗性能,主要在于每次将新的 DOM 插入 document 都会导致浏览器重新计算布局属性,以及各个视图层、合并、渲染。所以,这样的代码性能是十分低下的。 可以试想这样一个场景。对于一个前端列表组件而言,当存在 3 条数据的时候展示 3 条,当存在 5 条数据的时候展示 5 条。也就是说 UI 的呈现在某种程度上必然会和数据存在某种逻辑关系。如果 JS 能够感知到关键数据的改变,使用一种高效的方式将 DOM 改写成与数据相对应的状态。那么于开发者而言,就可以专注于业务逻辑和数据的改变,工作效率也会大幅提高。 所以, 框架 最核心的功能之一就是 高效地 达成 UI 层和数据层的统一。 React 哲学— React 本身并不是框架, React 只是一个 JavaScript 库,他的作用是通过组件构建用户界面,属于 MVC 应用中的 View 视图层。React 通过 props 和 state 来简化关键数据的存储,对于一个 react 组件函数而言,在 1 秒内可能被执行很多次。而每一次被执行,数据被注入 JSX , JSX 并不是真实的 DOM ,在 React 中会被转换成 React.createElement(type, props, children) 函数,执行的结果就是 ReactElement 元素 ,也即是 虚拟 DOM ,用来描述在浏览器的某一帧中,组件应该被呈现为什么样子。 Virtual Dom— VirtualDom 并非 React 专属,就像 redux 也可以在非 React 环境下使用一样,它们只是一种设计的思路。 事实上, React 在使用 fiber 架构之前的 Virtual Dom 和 diff 过程要相对直观一些。但是在引入了 fiber 架构之后整个流程变得冗长,如果单纯想了解 VirtualDom 和 diff 过程的原理也可以通过 simple-virtual-dom[3] 这个仓库来学习。 VirtualDom 的本质是利用 JS 变量 对真实 DOM 进行抽象,既然每一次操作 DOM 都可能触发浏览器的重排消耗性能,那么就可以使用 VirtualDom 来缓存当前组件状态,对用户交互和数据的变动进行批次处理,直接计算出每一帧页面应该呈现的最终状态,而这个状态是以 JS 变量 的形式存在于内存中的。所以通过 VirtualDom 既能够保证用户看到的每一帧都响应了数据的变化,又能节约性能保证浏览器不出现卡顿。 第一次渲染 First Render 首先我们应该注意到 React(浏览器环境) 代码的入口 render 函数 ReactDOM.render(<App />, domContainer) 这个 render 过程中, React 需要做到的是根据用户创造的 JSX 语法,构建出一个虚拟的树结构(也就是 ReactElement 和 Fiber )来表示用户 期望中 页面中的元素结构。当然对于这个过程相对并不复杂(误),因为此时的 document 内还是一片虚无。就思路上而言,只需要根据虚拟 DOM 节点生成真实的 DOM 元素然后插入 document ,第一次渲染就算圆满完成。 createReactElement— 通常我们会通过 Babel 将 JSX 转换为一个 JS 执行函数。例如我们在 React 环境下用 JSX 中写了一个标题组件 <h1 className='title'><div>Class Component</div></h1> 那么这个组件被 Babel 转换之后将会是 React.createElement('h1', { className: 'title' }, [React.createElement('div', null, [ 'Class Component' ]]) 传统编译讲究一个 JSON 化,当然 JSX 和 React 也没有什么关系, JSX 只是 React 推荐的一种拓展语法。当然你也可以不用 JSX 直接使用 React.createElement 函数,但是对比上面的两种写法你就也能知道,使用纯 JS 的心智成本会比简明可见的 JSX 高多少。我们可以看出, React.createElement 需要接收 3 个参数,分别是 DOM 元素的标签名,属性对象以及一个子元素数组,返回值则是一个 ReactElement 对象。 事实上, JSX 编译后的 json 结构本身就是一个对象,即使不执行 React.createElement 函数也已经初步可以使用了。那么在这个函数中我们做了什么呢。 一个 ReactElement 元素主要有 5 个关键属性,我们都知道要构建成一个页面需要通过 html 描述元素的类型和结构,通过 style 和 class 去描述元素的样式呈现,通过 js 和绑定事件来触发交互事件和页面更新。 所以最重要的是第一个属性,元素类型 type 。如果这个元素是一个纯 html 标签元素,例如 div ,那么 type 将会是字符串 div ,如果是一个 React 组件,例如 function App() {return ( <div>Hello, World!</div>)} 那么 type 的值将会指向 App 函数,当然 Class 组件 也一样(众所周知 ES6 的 Class 语法本身就是函数以及原型链构成的语法糖) 第二个属性是 props ,我们在 html 标签中写入的大部分属性都会被收集在 props 中,例如 id 、 className 、 style 、 children 、点击事件等等。 第三个第四个属性分别是 key 和 ref ,其中 key 在数组的处理和 diff 过程中有重要作用,而 ref 则是引用标识,在这里就先不做过多介绍。 最后一个属性是 $$typeof ,这个属性会指向 Symbol(React.element) 。作为 React 元素的唯一标识的同时,这个标签也承担了安全方面的功能。我们已经知道了所谓的 ReactElement 其实就是一个 JS 对象。那么如果有用户恶意的向服务端数据库中存入了某个有侵入性功能的 伪 React 对象,在实际渲染过程中被当做页面元素渲染,那么将有可能威胁到用户的安全。而 Symbol 是无法在数据库中被存储的,换句话说, React 所渲染的所有元素,都必须是由 JSX 编译的拥有 Symbol 标识的元素。(如果在低版本不支持 Symbol 的浏览器中,将会使用字符串替代,也就没有这层安排保护了) ok,接下来回到 render 函数。在这个函数中到底发生了什么呢,简单来说就是创建 Root 结构。 enqueueUpdate 从设计者的角度,根据 单一职责原则 和 开闭口原则 需要有与函数体解耦的数据结构来告诉 React 应该怎么操作 fiber 。而不是初次渲染写一套逻辑,第二次渲染写一套逻辑。因此, fiber 上有了更新队列 UpdateQueue 和 更新链表 Update 结构 如果查看一下相关的定义就会发现,更新队列 updateQueue 是多个更新组成的链表结构,而 update 的更新也是一个链表,至于为什么是这样设计,试想在一个 Class Component 的更新函数中连续执行了 3 次 setState ,与其将其作为 3 个更新挂载到组件上,不如提供一种更小粒度的控制方式。一句话概括就是, setState 级别的小更新合并成一个状态更新,组件中的多个状态更新在组件的更新队列中合并,就能够计算出组件的新状态 newState。 对于初次渲染而言,只需要在第一个 fiber 上,挂载一个 update 标识这是一个初次渲染的 fiber 即可。 // 更新根节点export function ScheduleRootUpdate ( current: Fiber, element: ReactElement, expirationTime: number, suspenseConfig: SuspenseConfig | null, callback?: Function) { // 创建一个update实例 const update = createUpdate(expirationTime, suspenseConfig) // 对于作用在根节点上的 react element update.payload = { element } // 将 update 挂载到根 fiber 的 updateQueue 属性上 enqueueUpdate( current, update ) ScheduleWork( current, expirationTime )} Fiber— 作为整个 Fiber 架构 中最核心的设计, Fiber 被设计成了链表结构。 child 指向当前节点的第一个子元素 return 指向当前节点的父元素 sibling 指向同级的下一个兄弟节点 如果是 React16 之前的树状结构,就需要通过 DFS 深度遍历来查找每一个节点。而现在只需要将指针按照 child → sibling → return 的优先级移动,就可以处理所有的节点 这样设计还有一个好处就是在 React 工作的时候只需要使用一个全局变量作为指针在链表中不断移动,如果出现用户输入或其他优先级更高的任务就可以 暂停 当前工作,其他任务结束后只需要根据指针的位置继续向下移动就可以继续之前的工作。指针移动的规律可以归纳为 自顶向下,从左到右 。 康康 fiber 的基本结构 其中 tag fiber 的类型 ,例如函数组件,类组件,原生组件, Portal 等。 type React 元素 类型 详见上方 createElement。 alternate 代表双向缓冲对象(看后面)。 effectTag 代表这个 fiber 在下一次渲染中将会被如何处理。例如只需要插入,那么这个值中会包含 Placement ,如果需要被删除,那么将会包含 Deletion 。 expirationTime 过期时间,过期时间越靠前,就代表这个 fiber 的优先级越高。 firstEffect 和 lastEffect 的类型都和 fiber 一样,同样是链表结构,通过 nextEffect 来连接。代表着即将更新的 fiber 状态 memorizeState 和 memorizeProps 代表在上次渲染中组件的 props 和 state 。如果成功更新,那么新的 pendingProps 和 newState 将会替代这两个变量的值 ref 引用标识 stateNode 代表这个 fiber 节点对应的真实状态 对于原生组件,这个值指向一个 dom 节点(虽然已经被创建了,但不代表就被插入了 document ) 对于类组件,这个值指向对应的类实例 对于函数组件,这个值指向 Null 对于 RootFiber,这个值指向 FiberRoot (如图) 接下来是初次渲染的几个核心步骤,因为是初次渲染,核心任务就是将首屏元素渲染到页面上,所以这个过程将会是同步的。 PrepareFreshStack— 因为笔者是土货没学过英语,百度了下发现是 准备干净的栈 的意思。结合了下流程,可以看出这一步的作用是在真正工作之前做一些准备,例如初始化一些变量,放弃之前未完成的工作,以及最重要的—— 创建双向缓冲变量 WorkInProgress let workInProgress: Fiber | null = null...export function prepareFreshStack ( root: FiberRoot, expirationTime: number) { // 重置根节点的finishWork root.finishedWork = null root.finishedExpirationTime = ExpirationTime.NoWork... if (workInProgress !== null) { // 如果已经存在了WIP,说明存在未完成的任务 // 向上找到它的root fiber let interruptedWork = workInProgress.return while (interruptedWork !== null) { // unwindInterruptedWork // 抹去未完成的任务 unwindInterruptedWork(interruptedWork) interruptedWork = interruptedWork.return } } workInProgressRoot = root // 创建双向缓冲对象 workInProgress = createWorkInProgress(root.current, null, expirationTime) renderExpirationTime = expirationTime workInProgressRootExitStatus = RootExitStatus.RootImcomplete} 双向缓冲变量 WorkInProgress 这里简称 WIP 好了,与之对应的是 current , current 代表的是当前页面上呈现的组件对应的 fiber 节点,你可以将其类比为 git 中的 master 分支,它代表的是已经对外的状态。而 WIP 则代表了一个 pending 的状态,也就是下一帧屏幕将要呈现的状态,就像是从 master 拉出来的一个 feature 分支,我们可以在这个分支上做任意的更改。最终协调完毕,将 WIP 的结果渲染到了页面上,按照页面内容对应 current 的原则, current 将会指向 WIP ,也就是说, WIP 取代了之前的 current ( git 的 master 分支)。 在这之前 current 和 WIP 的 alternate 字段分别指向彼此。 那么 WIP 是如何被创造出来的呢: // 根据已有 fiber 生成一个 workInProgress 节点export function createWorkInProgress ( current: Fiber, pendingProps: any, expirationTime): Fiber { let workInProgress = current.alternate if (workInProgress === null) { // 如果当前fiber没有alternate // tip: 这里使用的是“双缓冲池技术”,因为我们最多需要一棵树的两个实例。 // tip: 我们可以自由的复用未使用的节点 // tip: 这是异步创建的,避免使用额外的对象 // tip: 这同样支持我们释放额外的内存(如果需要的话 workInProgress = createFiber( current.tag, pendingProps, current.key, current.mode ) workInProgress.elementType = current.elementType workInProgress.type = current.type workInProgress.stateNode = current.stateNode workInProgress.alternate = current current.alternate = workInProgress } else { // 我们已经有了一个 WIP workInProgress.pendingProps = pendingProps // 重置 effectTag workInProgress.effectTag = EffectTag.NoEffect // 重置 effect 链表 workInProgress.nextEffect = null workInProgress.firstEffect = null workInProgress.lastEffect = null } 可以看出 WIP 其实就是继承了 current 的核心属性,但是去除了一些副作用和工作记录的 干净 的 fiber。 工作循环 WorkLoop 在工作循环中,将会执行一个 while 语句,每执行一次循环,都会完成对一个 fiber 节点的处理。在 workLoop 模块中有一个指针 workInProgress 指向当前正在处理的 fiber ,它会不断向链表的尾部移动,直到指向的值为 null ,就停止这部分工作, workLoop 的部分也就结束了。 每处理一个 fiber 节点都是一个工作单元,结束了一个工作单元后 React 会进行一次判断,是否需要暂停工作检查有没有更高优先级的用户交互进来。 function workLoopConcurrent() { // 执行工作直到 Scheduler 要求我们 yield while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); }} 跳出条件只有: 所有 fiber 都已经被遍历结束了 当前线程的使用权移交给了外部任务队列 但是我们现在讨论的是第一次渲染,触屏渲染的优先级高于一切,所以并不存在第二个限制条件。 function workLoopSync () { // 只要没有完成reconcile就一直执行 while(workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress as Fiber) }} PerformUnitOfWork & beginWork— 单元工作 performUnitOfWork 的主要工作是通过 beginWork 来完成。beginWork 的核心工作是通过判断 fiber.tag 判断当前的 fiber 代表的是一个类组件、函数组件还是原生组件,并且针对它们做一些特殊处理。这一切都是为了最终步骤:操作真实 DOM 做准备,即通过改变 fiber.effectTag 和 pendingProps 告诉后面的 commitRoot 函数应该对真实 DOM 进行怎样的改写。 switch (workInProgress.tag) { // RootFiber case WorkTag.HostRoot: return updateHostRoot(current as Fiber, workInProgress, renderExpirationTime) // class 组件 case WorkTag.ClassComponent: { const Component = workInProgress.type const resolvedProps = workInProgress.pendingProps return updateClassComponent( current, workInProgress, Component, resolvedProps, renderExpirationTime ) } ...} 此处就以 Class 组件为例,查看一下具体是如何构建的。 之前有提过,对于类组件而言, fiber.stateNode 会指向这个类之前构造过的实例。 // 更新Class组件function updateClassComponent ( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpiration: number) { // 如果这个 class 组件被渲染过,stateNode 会指向类实例 // 否则 stateNode 指向 null const instance = workInProgress.stateNodeif (instance === null) {// 如果没有构造过类实例...} else {// 如果构造过类实例 ...}// 完成 render 的构建,将得到的 react 元素和已有元素进行调和const nextUnitOfWork = finishClassComponent( current, workInProgress, Component, shouldUpdate, false, renderExpiration)return nextUnitOfWork 如果这个 fiber 并没有构建过类实例的话,就会调用它的构建函数,并且将更新器 updater 挂载到这个类实例上。(处理 setState 逻辑用的,事实上所有的类组件实例上的更新器都是同一个对象,后面会提到) if (instance === null) {// 这个 class 第一次渲染 if (current !== null) { // 删除 current 和 WIP 之间的指针 current.alternate = null workInProgress.alternate = null // 插入操作 workInProgress.effectTag |= EffectTag.Placement } // 调用构造函数,创造新的类实例 // 给予类实例的某个指针指向更新器 updater constructClassInstance( workInProgress, Component, nextProps, renderExpiration ) // 将属性挂载到类实例上,并且触发多个生命周期 mountClassInstance( workInProgress, Component, nextProps, renderExpiration )} 如果实例已经存在,就需要对比新旧 props 和 state ,判断是否需要更新组件(万一写了 shouldComponentUpdate 呢)。并且触发一些更新时的生命周期钩子,例如 getDerivedStateFromProps 等等。 else {// 已经 render 过了,更新 shouldUpdate = updateClassInstance( current, workInProgress, Component, nextProps, renderExpiration )} 属性计算完毕后,调用类的 render 函数获取最终的 ReactElement ,打上 Performed 标记,代表这个类在本次渲染中已经执行过了。 // 完成Class组件的构建function finishClassComponent ( current: Fiber | null, workInProgress: Fiber, Component: any, shouldUpdate: boolean, hasContext: boolean, renderExpiration: number) {// 错误 边界捕获 const didCaptureError = false if (!shouldUpdate && !didCaptureError) { if (hasContext) { // 抛出问题 return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpiration ) } } // 实例 const instance = workInProgress.stateNode let nextChildren nextChildren = instance.render() // 标记为已完成 workInProgress.effectTag |= EffectTag.PerformedWork // 开始调和 reconcile reconcileChildren( current, workInProgress, nextChildren, renderExpiration ) return workInProgress.child} 调和过程 如果还记得之前的内容的话,我们在一切工作开始之前只是构建了第一个根节点 fiberRoot 和第一个无意义的空 root ,而在单个元素的调和过程 reconcileSingleElement 中会根据之前 render 得到的 ReactElement 元素构建出对应的 fiber 并且插入到整个 fiber 链表中去。 并且通过 placeSingleChild 给这个 fiber 的 effectTag 打上 Placement 的标签,拥有 Placement 标记后这里的工作就完成了,可以将 fiber 指针移动到下一个节点了。 // 处理对象类型(单个节点)const isObjectType = isObject(newChild) && !isNull(newChild)// 对象if (isObjectType) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { // 在递归调和结束,向上回溯的过程中 // 给这个 fiber 节点打上 Placement 的 Tag return placeSingleChild( reconcileSingleElement( returnFiber, currentFirstChild, newChild, expirationTime ) ) } // 还有 Fragment 等类型 }}// 如果这时子元素是字符串或者数字,按照文字节点来处理// 值得一提的是,如果元素的子元素是纯文字节点// 那么这些文字不会被转换成 fiber// 而是作为父元素的 prop 来处理if (isString(newChild) || isNumber(newChild)) { return placeSingleChild( reconcileSingleTextNode( returnFiber, currentFirstChild, '' + newChild, expirationTime ) )}// 数组if (isArray(newChild)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, expirationTime )} 文章篇幅有限,对于函数组件和原生组件这里就不做过多介绍。假设我们已经完成了对于所有 WIP 的构建和调和过程,对于第一次构建而言,我们需要插入大量的 DOM 结构,但是到现在我们得到的仍然是一些虚拟的 fiber 节点。 所以,在最后一次单元工作 performUnitOfWork 中将会执行 completeWork ,在此之前,我们的单元工作是一步步向尾部的 fiber 节点移动。而在 completeWork 中,我们的工作将是自底向上,根据 fiber 生成真实的 dom 结构,并且在向上的过程中将这些结构拼接成一棵 dom 树。 export function completeWork ( current: Fiber | null, workInProgress: Fiber, renderExpirationTime: number): Fiber | null { // 最新的 props const newProps = workInProgress.pendingProps switch (workInProgress.tag) { ... case WorkTag.HostComponent: { // pop 该 fiber 对应的上下文 popHostContext(workInProgress) // 获取 stack 中的当前 dom const rootContainerInstance = getRootHostContainer() // 原生组件类型 const type = workInProgress.type if (current !== null && workInProgress.stateNode !== null) { // 如果不是初次渲染了,可以尝试对已有的 dom 节点进行更新复用 updateHostComponent( current, workInProgress, type as string, newProps, rootContainerInstance ) } else { if (!newProps) { throw new Error('如果没有newProps,是不合法的') } const currentHostContext = getHostContext() // 创建原生组件 let instance = createInstance( type as string, newProps, rootContainerInstance, currentHostContext, workInProgress ) // 将之前所有已经生成的子 dom 元素装载到 instance 实例中 // 逐步拼接成一颗 dom 树 appendAllChildren(instance, workInProgress, false, false) // fiber 的 stateNode 指向这个 dom 结构 workInProgress.stateNode = instance // feat: 这个函数真的藏得很隐蔽,我不知道这些人是怎么能注释都不提一句的呢→_→ // finalizeInitialChildren 作用是将props中的属性挂载到真实的dom元素中去,结果作为一个判断条件被调用 // 返回一个bool值,代表是否需要auto focus(input, textarea...) if (finalizeInitialChildren(instance, type as string, newProps, rootContainerInstance, currentHostContext)) { markUpdate(workInProgress) } } } } return null} 构建完毕后,我们得到了形如下图,虚拟 dom 和 真实 dom,父元素和子元素之间的关系结构 截止到当前,调和 reconcile 工作已经完成,我们已经进入了准备提交到文档 ready to commit 的状态。其实从进入 completeUnitOfWork 构建开始,后面的过程就已经和时间片,任务调度系统没有关系了,此时一切事件、交互、异步任务都将屏气凝神,聆听接下来 dom 的改变。 // 提交根实例(dom)到浏览器真实容器root中function commitRootImpl ( root: FiberRoot, renderPriorityLevel: ReactPriorityLevel) {... // 因为这次是整个组件树被挂载,所以根 fiber 节点将会作为 fiberRoot 的 finishedWorkconst finishedWork = root.finishedWork ... // effect 链表,即那些将要被插入的原生组件 fiber let firstEffect = finishedWork.firstEffect...let nextEffect = firstEffectwhile (nextEffect !== null) { try { commitMutationEffects(root, renderPriorityLevel) } catch(err) { throw new Error(err) } }} 在 commitMutationEffects 函数之前其实对 effect 链表还进行了另外两次遍历,分别是一些生命周期的处理,例如 getSnapshotBeforeUpdate ,以及一些变量的准备。 // 真正改写文档中dom的函数// 提交fiber effectfunction commitMutationEffects ( root: FiberRoot, renderPriorityLevel: number) { // @question 这个 while 语句似乎是多余的 = = while (nextEffect !== null) { // 当前fiber的tag const effectTag = nextEffect.effectTag // 下方的switch语句只处理 Placement,Deletion 和 Update const primaryEffectTag = effectTag & ( EffectTag.Placement | EffectTag.Update | EffectTag.Deletion | EffectTag.Hydrating ) switch (primaryEffectTag) { case EffectTag.Placement: { // 执行插入 commitPlacement(nextEffect) // effectTag 完成实名制后,要将对应的 effect 去除 nextEffect.effectTag &= ~EffectTag.Placement } case EffectTag.Update: { // 更新现有的 dom 组件 const current = nextEffect.alternate commitWork(current, nextEffect) } } nextEffect = nextEffect.nextEffect }} 截至此刻,第一次渲染的内容已经在屏幕上出现。也就是说,真实 DOM 中的内容不再对应此时的 current fiber ,而是对应着我们操作的 workInProgress fiber ,即函数中的 finishedWork 变量。 // 在 commit Mutation 阶段之后,workInProgress tree 已经是真实 Dom 对应的树了// 所以之前的 tree 仍然是 componentWillUnmount 阶段的状态// 所以此时, workInProgress 代替了 current 成为了新的 currentroot.current = finishedWork 一次点击事件 如果你是一个经常使用 React 的打工人,就会发现 React 中的 event 是“阅后即焚的”。假设这样一段代码: import React, { MouseEvent } from 'react'function TestPersist () {const handleClick = ( event: MouseEvent<HTMLElement, globalThis.MouseEvent>) => { setTimeout(() => console.log('event', event)) }return ( <div onClick={handleClick}>O2</div>)} 如果我们需要异步的获取这次点击事件在屏幕中的位置并且做出相应处理,那么在 setTimeout 中能否达到目的呢。 答案是否定的,因为 React 使用了 事件委托 机制,我们拿到的 event 对象并不是原生的 nativeEvent ,而是被 React 挟持处理过的合成事件 SyntheticEvent ,这一点从 ts 类型中也可以看出, 我们使用的 MouseEvent 是从 React 包中引入的而不是全局的默认事件类型。在 handleClick 函数同步执行完毕的一瞬间,这个 event 就已经在 React 事件池中被销毁了,我们可以跑这个组件康一康。 当然 React 也提供了使用异步事件对象的解决方案,它提供了一个 persist 函数,可以让事件不再进入事件池。(在 React17 中为了解决某些 issue ,已经重写了合成事件机制,事件不再由 document 来代理,官网的说法是合成事件[4]不再由事件池管理,也没有了 persist 函数) 那,为什么要用事件委托呢。还是回到那个经典的命题,渲染 2 个 div 当然横着写竖着写都没关系,如果是 1000 个组件 2000 个点击事件呢。事件委托的收益就是: 简化了事件注册的流程,优化性能。 dom 元素不断在更新,你无法保证下一帧的 div 和上一帧中的 div 在内存中的地址是同一个。既然不是同一个,事件又要全部重新绑定,烦死了(指浏览器)。 ok,言归正传。我们点击事件到底发生了什么呢。首先是在 React 的 render 函数执行之前,在 JS 脚本中就已经自动执行了事件的注入。 事件注入— 事件注入的过程稍微有一点复杂,不光模块之间有顺序,数据也做了不少处理,这里不 po 太详细的代码。可能有人会问为啥不直接写死呢,浏览器的事件不也就那么亿点点。就像 Redux 不是专门为 React 服务的一样, React 也不是专门为浏览器服务的。文章开头也说了 React 只是一个 javascipt 库,它也可以服务 native 端、桌面端甚至各种终端。所以根据底层环境的不同动态的注入事件集也是非常合理的做法。 当然注入过程并不重要,我们需要知道的就是 React 安排了每种事件在 JSX 中的写法和原生事件的对应关系(例如 onClick 和 onclick ),以及事件的优先级。 /* ReactDOM环境 */// DOM 环境的事件 pluginconst DOMEventPluginOrder = [ 'ResponderEventPlugin', 'SimpleEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'BeforeInputEventPlugin',];// 这个文件被引入的时候自动执行 injectEventPluginOrder// 确定 plugin 被注册的顺序,并不是真正引入EventPluginHub.injectEventPluginOrder(DOMEventPluginOrder)// 真正的注入事件内容EventPluginHub.injectEventPluginByName({ SimpleEventPlugin: SimpleEventPlugin}) 这里以 SimpleEventPlugin 为例,点击事件等我们平时常用的事件都属于这个 plugin。 // 事件元组类型type EventTuple = [ DOMTopLevelEventType, // React 中的事件类型 string, // 浏览器中的事件名称 EventPriority // 事件优先级]const eventTuples: EventTuple[] = [ // 离散的事件 // 离散事件一般指的是在浏览器中连续两次触发间隔最少 33ms 的事件(没有依据,我猜的) // 例如你以光速敲打键盘两次,这两个事件的实际触发时间戳仍然会有间隔 [ DOMTopLevelEventTypes.TOP_BLUR, 'blur', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CANCEL, 'cancel', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CHANGE, 'change', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CLICK, 'click', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CLOSE, 'close', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CONTEXT_MENU, 'contextMenu', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_COPY, 'copy', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_CUT, 'cut', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_DOUBLE_CLICK, 'doubleClick', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_AUX_CLICK, 'auxClick', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_FOCUS, 'focus', DiscreteEvent ], [ DOMTopLevelEventTypes.TOP_INPUT, 'input', DiscreteEvent ],...] 那么,这些事件的监听事件是如何被注册的呢。还记得在调和 Class 组件的时候会计算要向浏览器插入什么样的 dom 元素或是要如何更新 dom 元素。在这个过程中会通过 diffProperty 函数对元素的属性进行 diff 对比,其中通过 ListenTo 来添加监听函数 大家都知道,最终被绑定的监听事件一定是被 React 魔改过,然后绑定在 document 上的。 function trapEventForPluginEventSystem ( element: Document | Element | Node, topLevelType: DOMTopLevelEventType, capture: boolean): void {// 生成一个 listener 监听函数 let listener switch (getEventPriority(topLevelType)) { case DiscreteEvent: { listener = dispatchDiscreteEvent.bind( null, topLevelType, EventSystemFlags.PLUGIN_EVENT_SYSTEM ) break } ... default: { listener = dispatchEvent.bind( null, topLevelType, EventSystemFlags.PLUGIN_EVENT_SYSTEM ) } } // @todo 这里用一个getRawEventName转换了一下 // 这个函数就是 →_→ // const getRawEventName = a => a // 虽然这个函数什么都没有做 // 但是它的名字语义化的说明了这一步 // 目的是得到浏览器环境下addEventListener第一个参数的合法名称 const rawEventName = topLevelType // 将捕获事件listener挂载到根节点 // 这两个部分都是为了为了兼容 IE 封装过的 addEventListener if (capture) { // 注册捕获事件 addEventCaptureListener(element, rawEventName, listener) } else { // 注册冒泡事件 addEventBubbleListener(element, rawEventName, listener) }} 大家应该都知道 addEventListener 的第三个参数是控制监听捕获过程 or 冒泡过程的吧 ok,right now,鼠标点了下页面,页面调用了这个函数。开局就一个 nativeEvent 对象,这个函数要做的第一件事就是知道真正被点的那个组件是谁,其实看了一些源码就知道, React 但凡有什么事儿第一个步骤总是找到需要负责的那个 fiber 。 首先,通过 nativeEvent 获取目标 dom 元素也就是 dom.target const nativeEventTarget = getEventTarget(nativeEvent) export default function getEventTarget(nativeEvent) { // 兼容写法 let target = nativeEvent.target || nativeEvent.srcElement || window // Normalize SVG // @todo return target.nodeType === HtmlNodeType.TEXT_NODE ? target.parentNode : target} 那么如何通过 dom 拿到这个 dom 对应的 fiber 呢,事实上, React 会给这个 dom 元素添加一个属性指向它对应的 fiber 。对于这个做法我是有疑问的,这样的映射关系也可以通过维护一个 WeekMap 对象来实现,操作一个 WeakMap 的性能或许会优于操作一个 DOM 的属性,且后者似乎不太优雅,如果你有更好的想法也欢迎在评论区指出。 每当 completeWork 中为 fiber 构造了新的 dom,都会给这个 dom 一个指针来指向它的 fiber // 随机Keyconst randomKey = Math.random().toString(36).slice(2)// 随机Key对应的当前实例的Keyconst internalInstanceKey = '__reactInternalInstance$' + randomKey// Key 对应 render 之后的 propsconst internalEventHandlersKey = '__reactEventHandlers$' + randomKey// 对应实例const internalContianerInstanceKey = '__reactContainer$' + randomKey// 绑定操作export function precacheFiberNode ( hostInst: object, node: Document | Element | Node): void { node[internalInstanceKey] = hostInst}// 读取操作export function getClosestInstanceFromNode (targetNode) { let targetInst = targetNode[internalInstanceKey] // 如果此时没有Key,直接返回null if (targetInst) { return targetInst }// 省略了一部分代码// 如果这个 dom 上面找不到 internalInstanceKey 这个属性 // 就会向上寻找父节点,直到找到一个拥有 internalInstanceKey 属性的 dom 元素 // 这也是为什么这个函数名要叫做 从 node 获取最近的 (fiber) 实例... return null} 此时我们已经拥有了原生事件的对象,以及触发了事件的 dom 以及对应的 fiber ,就可以从 fiber.memorizedProps 中取到我们绑定的 onClick 事件。这些信息已经足够生成一个 React 合成事件 ReactSyntheticEvent 的实例了。 React 声明了一个全局变量 事件队列 eventQueue ,这个队列用来存储某次更新中所有被触发的事件,我们需要让这个点击事件入队。然后触发。 // 事件队列let eventQueue: ReactSyntheticEvent[] | ReactSyntheticEvent | null = nullexport function runEventsInBatch ( events: ReactSyntheticEvent[] | ReactSyntheticEvent | null) { if (events !== null) { // 存在 events 的话,加入事件队列 // react 自己写的合并数组函数 accumulateInto // 或许是 ES3 时期写的吧 eventQueue = accumulateInto<ReactSyntheticEvent>(eventQueue, events) }const processingEventQueue = eventQueue // 执行完毕之后要清空队列 // 虽然已经这些 event 已经被释放了,但还是会被遍历 eventQueue = null if (!processingEventQueue) return// 将这些事件逐个触发 // forEachAccumulated 是 React 自己实现的 foreach forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel)} // 触发一个事件并且立刻将事件释放到事件池中,除非执行了presistentconst executeDispatchesAndRelease = function (event: ReactSyntheticEvent) { if (event) { // 按照次序依次触发和该事件类型绑定的所有 listener executeDispatchesInOrder(event) } // 如果没有执行 persist 持久化 , 立即销毁事件 if (!event.isPersistent()) { (event.constructor as any).release(event) }} 可以看到合成事件的构造函数实例上挂载了一个函数 release ,用来释放事件。我们看一看 SyntheticEvent 的代码,可以发现这里使用了一个事件池的概念 eventPool 。 Object.assign(SyntheticEvent.prototype, {// 模拟原生的 preventDefault 函数 preventDefault: function() { this.defaultPrevented = true; const event = this.nativeEvent; if (!event) { return; } if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } this.isDefaultPrevented = functionThatReturnsTrue; }, // 模拟原生的 stopPropagation stopPropagation: function() { const event = this.nativeEvent; if (!event) { return; } if (event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } this.isPropagationStopped = functionThatReturnsTrue; }, /** * 在每次事件循环之后,所有被 dispatch 过的合成事件都会被释放 * 这个函数能够允许一个引用使用事件不会被 GC 回收 */ persist: function() { this.isPersistent = functionThatReturnsTrue; }, /** * 这个 event 是否会被 GC 回收 */ isPersistent: functionThatReturnsFalse, /** * 销毁实例 * 就是将所有的字段都设置为 null */ destructor: function() { const Interface = this.constructor.Interface; for (const propName in Interface) { this[propName] = null; } this.dispatchConfig = null; this._targetInst = null; this.nativeEvent = null; this.isDefaultPrevented = functionThatReturnsFalse; this.isPropagationStopped = functionThatReturnsFalse; this._dispatchListeners = null; this._dispatchInstances = null; },}); React 在构造函数上直接添加了一个事件池属性,其实就是一个数组,这个数组将被全局共用。每当事件被释放的时候,如果线程池的长度还没有超过规定的大小(默认是 10 ),那么这个被销毁后的事件就会被放进事件池 // 为合成事件构造函数添加静态属性// 事件池为所有实例所共用function addEventPoolingTo (EventConstructor) { EventConstructor.eventPool = [] EventConstructor.getPooled = getPooledEvent EventConstructor.release = releasePooledEvent}// 将事件释放// 事件池有容量的话,放进事件池function releasePooledEvent (event) { const EventConstructor = this event.destructor() if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) { EventConstructor.eventPool.push(event) }} 我们都知道单例模式,就是对于一个类在全局最多只会有一个实例。而这种事件池的设计相当于是 n 例模式,每次事件触发完毕之后,实例都要还给构造函数放进事件池,后面的每次触发都将复用这些干净的实例,从而减少内存方面的开销。 // 需要事件实例的时候直接从事件池中取出function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) { const EventConstructor = this if (EventConstructor.eventPool.length) { // 从事件池中取出最后一个 const instance = EventConstructor.eventPool.pop() EventConstructor.call( instance, dispatchConfig, targetInst, nativeEvent, nativeInst ) return instance } return new EventConstructor ( dispatchConfig, targetInst, nativeEvent, nativeInst )} 如果在短时间内浏览器事件被频繁触发,那么将出现的现象是,之前事件池中的实例都被取出复用,而后续的合成事件对象就只能被老老实实重新创建,结束的时候通过放弃引用来被 V8 引擎的 GC 回收。 回到之前的事件触发,如果不特地将属性名写成 onClickCapture 的话,那么默认将被触发的就会是冒泡过程。这个过程也是 React 模拟的,就是通过 fiber 逐层向上触发的方式,捕获过程也是同理。 我们都知道正常的事件触发流程是: 事件捕获 处于事件 事件冒泡 处于事件 阶段是一个 try-catch 语句,这样即使发生错误也会处于 React 的错误捕获机制当中。我们真正想要执行的函数实体就是在此被触发: export default function invodeGuardedCallbackImpl< A, B, C, D, E, F, Context>( name: string | null, func: (a: A, b: B, c: C, d: D, e: E, f: F) => void, context?: Context, a?: A, b?: B, c?: C, d?: D, e?: E, f?: F,): void { const funcArgs = Array.prototype.slice.call(arguments, 3) try { func.apply(context, funcArgs) } catch (error) { this.onError(error) }} 类与函数 当我们使用类组件或是函数组件的时候,最终目的都是为了得到一份 JSX 来描述我们的页面。那么其中就存在着一个问题—— React 是如何分辨函数组件和类组件的。 虽然在 ES6 中,我们可以轻易的看出 Class 和 函数的区别,但是别忘了,我们实际使用的往往是 babel 编译后的代码,而类就是函数和原型链构成的语法糖。可能大部分人最直接的想法就是,既然类组件继承了 React.Component ,那么应该可以直接使用类类型判断就就行: App instanceof React.Component 当然, React 采用的做法是在原型链上添加一个标识 Component.prototype.isReactComponent = {} 源码中需要判断是否是类组件的时候,就可以直接读取函数的 isReactComponent 属性时,因为在函数(也是对象)自身找不到时,就会向上游原型链逐级查找,直到到达 Object.prototype 对象为止。 为什么 isReactComponent 是一个对象而不是布尔以及为什么不能用 instanceOf [5] 状态的更新 之前我们已经看懂了 React 的事件委托机制,那么不如在一次点击事件中尝试修改组件的状态来更新我们的页面。 首先康康 setState 是如何工作的,我们知道 this.setState 是 React.Component 类中的方法: /*** @description 更新组件state* @param { object | Function } partialState 下个阶段的状态* @param { ?Function } callback 更新完毕之后的回调*/Component.prototype.setState = function (partialState, callback) { if (!( isObject(partialState) || isFunction(partialState) || isNull )) { console.warn('setState的第一个参数应为对象、函数或null') return } this.updater.enqueueSetState(this, partialState, callback, 'setState')} 看起来核心步骤就是触发挂载在实例上的一个 updater 对象。默认的, updater 会是一个展位的空对象,虽然实现了 enqueueSetState 等方法,但是这些方法内部都是空的。 // 我们初始化这个默认的update,真正的updater会被renderer注入this.updater = updater || ReactNoopUpdateQueue export const ReactNoopUpdateQueue = { /** * 检查组件是否已经挂载 */ isMounted: function (publishInstance) { // 初始化ing的组件就别挂载不挂载了 return false }, /** * 强制更新 */ enqueueForceUpdate: function (publishInstance, callback, callerName) { console.warn('enqueueForceUpdate', publishInstance) }, /** * 直接替换整个state,通常用这个或者setState来更新状态 */ enqueueReplaceState: function ( publishInstance, completeState, callback, callerName ) { console.warn('enqueueReplaceState', publishInstance) }, /** * 修改部分state */ enqueueSetState: function ( publishInstance, partialState, callback, callerName ) { console.warn('enqueueSetState', publishInstance) }} 还记得我们在 render 的过程中,是通过执行 Component.render() 来获得一个类组件的实例,当 React 得到了这个实例之后,就会将实例的 updater 替换成真正的 classComponentUpdater : function adoptClassInstance ( workInProgress: Fiber, instance: any): void { instance.updater = classComponentUpdate ...} 刚刚我们触发了这个对象中的 enqueueSetState 函数,那么可以看看实现: const classComponentUpdate = { isMounted, /** * 触发组件状态的更新 * @param inst ReactElement * @param payload any * @param callback 更新结束之后的回调 */ enqueueSetState( inst: ReactElement, payload: any, callback?: Function ) { // ReactElement -> fiber const fiber = getInstance(inst) // 当前时间 const currentTime = requestCurrentTime() // 获取当前 suspense config const suspenseConfig = requestCurrentSuspenseConfig() // 计算当前 fiber 节点的任务过期时间 const expirationTime = computeExpirationForFiber( currentTime, fiber, suspenseConfig ) // 创建一个 update 实例 const update = createUpdate(expirationTime, suspenseConfig) update.payload = payload // 将 update 装载到 fiber 的 queue 中 enqueueUpdate(fiber, update) // 安排任务 ScheduleWork(fiber, expirationTime) }, ...} 显然,这个函数的作用就是获得类组件对应的 fiber ,更新它在任务调度器中的过期时间(领导给了新工作,自然要定新的 Deadline ),然后就是创建一个新的 update 任务装载到 fiber 的任务队列中。最后通过 ScheduleWork (告诉任务调度器来任务了,赶紧干活) 要求从这个 fiber 开始调和,至于调和和更新的步骤我们在第一次渲染中已经有了大致的了解。 顺带提一提 Hooks 中的 useState 。网络上有挺多讲解 hook 实现的文章已经讲得很全面了,我们只需要搞清楚以下几点问题。 Q1. 函数组件不像类组件一样拥有实例,数据存储在哪里 A1. 任何以 ReactElement 为粒度的组件都需要围绕 fiber ,数据存储在 fiber.memorizedState 上 Q2. useState 的实现 A2. 如果你听过了 useState 那么你就应该听过 useReducer ,如果听过 reducer 就应该知道 redux。首先,useState 的本质就是 useReducer 的语法糖。我们都知道构建一个状态库需要一个 reducer ,useState 就是当 reducer 函数为 a => a 时的特殊情况。 function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { return typeof action === 'function' ? action(state) : action}function updateState<S>( initialState: (() => S) | S): [ S, Dispatch<BasicStateAction<S>> ] { return updateReducer<S, (() => S) | S, any>(basicStateReducer, initialState)} Q3. 为什么 Hooks 的顺序和个数不允许改变 A3. 每次执行 Hooks 函数需要取出上一次渲染时数据的最终状态,因为结构是链表而不是一个 Map,所以这些最终状态也会是有序的,所以如果个数和次序改变会导致数据的错乱。 时间调度机制— 虽然今年过期时间 expirationTime 机制已经被淘汰了,但是不管是航道模型还是过期时间,本质上都是任务优先级的不同体现形式。 在探究运行机制之前我们需要知道一个问题就是,为什么时间片的性能会优于同步计算的性能。此处借用司徒正美老师文章[6]中的例子。 实验 1,通过 for 循环一次性向 document 中插入 1000 个节点 function randomHexColor(){ return "#" + ("0000"+ (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);}setTimeout(function() { var k = 0; var root = document.getElementById("root"); for(var i = 0; i < 10000; i++){ k += new Date - 0 ; var el = document.createElement("div"); el.innerHTML = k; root.appendChild(el); el.style.cssText = background:${randomHexColor()};height:40px ; }}, 1000); 实验 2,进行 10 次 setTimeout 分批次操作,每次插入 100 个节点 function randomHexColor() { return "#" + ("0000" + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);}var root = document.getElementById("root");setTimeout(function () { function loop(n) { var k = 0; console.log(n); for (var i = 0; i < 100; i++) { k += new Date - 0; var el = document.createElement("div"); el.innerHTML = k; root.appendChild(el); el.style.cssText = background:${randomHexColor()};height:40px ; } if (n) { setTimeout(function () { loop(n - 1); }, 40); } } loop(100);}, 1000); 相同的结果,第一个实验花费了 1000 ms,而第二个实验仅仅花费了 31.5 ms。 这和 V8 引擎的底层原理有关,我们都知道浏览器是单线程,一次性需要做到 GUI 描绘,事件处理,JS 执行等多个操作时,V8 引擎会优先对代码进行执行,而不会对执行速度进行优化。如果我们稍微给浏览器一些时间,浏览器就能够进行 JIT ,也叫热代码优化。 简单来说, JS 是一种解释型语言,每次执行都需要被编译成字节码才能被运行。但是如果某个函数被多次执行,且参数类型和参数个数始终保持不变。那么这段代码会被识别为 热代码 ,遵循着“万物皆可空间换时间”的原则,这段代码的字节码会被缓存,下次再次运行的时候就会直接被运行而不需要进行耗时的解释操作。也就是 解释器 + 编译器 的模式。 做个比喻来说,我们工作不能一直蛮干,必须要给自己一些时间进行反思和总结,否则工作速度和效率始终是线性的,人也不会有进步。 还记得在 WorkLoop 函数中,每次处理完一个 fiber 都会跳出循环执行一次 shouldYield 函数进行判断,是否应该将执行权交还给浏览器处理用户时间或是渲染。看看这个 shouldYield 函数的代码: // 当前是否应该阻塞 react 的工作function shouldYield (): boolean { // 获取当前的时间点 const currentTime = getCurrentTime() // 检查任务队列中是否有任务需要执行 advanceTimers(currentTime) // 取出任务队列中任务优先级最高的任务 const firstTask = peek(taskQueue) // 以下两种情况需要yield // 1. 当前任务队列中存在任务,且第一个任务的开始时间还没到,且过期时间小于当前任务 // 2. 处于固定的浏览器渲染时间区间 return ( ( currentTask !== null && firstTask !== null && (firstTask as any).startTime <= currentTime && (firstTask as any).expirationTime < currentTask.expirationTime ) // 当前处于时间片的阻塞区间 || shouldYieldToHost() )} 决定一个任务当前是否应该被执行有两个因素。 这个任务是否非执行不可,正所谓一切的不论是不是先问为什么都是耍流氓。如果到期时间还没到,为什么不先把线程空出来留给可能的高优先级任务呢。 如果多个任务都非执行不可,那么任务的优先级是否是当前队列中最高的。 如果一个任务的过期时间已经到了必须执行,那么这个任务就应该处于 待执行队列 taskQueue 中。相反这个任务的过期时间还没到,就可以先放在 延迟列表 中。每一帧结束的时候都会执行 advanceTimer 函数,将一些延迟列表中到期的任务取出,插入待执行队列。 可能是出于最佳实践考虑,待执行队列是一个小根堆结构,而延迟队列是一个有序链表。 回想一下 React 的任务调度要求,当一个新的优先级更高的任务产生,需要能够打断之前的工作并插队。也就是说,React 需要维持一个始终有序的数组数据结构。因此,React 自实现了一个小根堆,但是这个小根堆无需像堆排序的结果一样整体有序,只需要保证每次进行 push 和 pop 操作之后,优先级最高的任务能够到达堆顶。 所以 shouldYield 返回 true 的一个关键条件就是,当前 taskQueue 堆中的堆顶任务的过期时间已经到了,那么就应该暂停工作交出线程使用权。 那么待执行的任务是如何被执行的呢。这里我们需要先了解 MessageChannel[7] 的概念。Message Channel 的实例会拥有两个端口,其中第一个端口为发送信息的端口,第二个端口为接收信息的端口。当接收到信息就可以执行指定的回调函数。 const channel = new MessageChannel()// 发送端const port = channel.port2// 接收端channel.port1.onmessage = performWorkUntilDeadline // 在一定时间内尽可能的处理任务 每当待执行任务队列中有任务的时候,就会通过 Channel 的发送端发送一个空的 message ,当接收端异步地接收到这个信号的时候,就会在一个时间片内尽可能地执行任务。 // 记录任一时间片的结束时刻let deadline = 0// 单位时间切片长度let yieldInterval = 5// 执行任务直到用尽当前时间片空闲时间function performWorkUntilDeadline () { if (scheduledHostCallback !== null) { // 如果有计划任务,那么需要执行 // 当前时间 const currentTime = getCurrentTime() // 在每个时间片之后阻塞(5ms) // deadline 为这一次时间片的结束时间 deadline = currentTime + yieldInterval // 既然能执行这个函数,就代表着还有时间剩余 const hasTimeRemaining = true try { // 将当前阻塞的任务计划执行 const hasMoreWork = scheduledHostCallback( hasTimeRemaining, currentTime ) if (!hasMoreWork) { // 如果没有任务了, 清空数据 isMessageLoopRunning = false scheduledHostCallback = null } else { // 如果还有任务,在当前时间片的结尾发送一个 message event // 接收端接收到的时候就将进入下一个时间片 port.postMessage(null) } } catch (error) { port.postMessage(null) throw(error) } } else { // 压根没有任务,不执行 isMessageLoopRunning = false }} 我们在之前说过,阻塞 WorkLoop 的条件有两个,第一个是任务队列的第一个任务还没到时间,第二个条件就是 shouldYieldToHost 返回 true,也就是处于时间片期间。 // 此时是否是【时间片阻塞】区间export function shouldYieldToHost () { return getCurrentTime() >= deadline} 总结一下,时间调度机制其实就是 fiber 遍历任务 WorkLoop 和调度器中的任务队列争夺线程使用权的过程。不过区别是前者完全是同步的过程,只会在每个 while 的间隙去询问 调度器 :我是否可以继续执行下去。而在调度器拿到线程使用权的每个时间片中,都会尽可能的处理任务队列中的任务。 传统武术讲究点到为止,以上内容,就是这次 React 原理的全部。在文章中我并没有放出大量的代码,只是放出了一些片段用来佐证我对于源码的一些看法和观点,文中的流程只是一个循序思考的过程,如果需要查看更多细节还是应该从源码入手。 当然文中的很多观点带有主观色彩,并不一定就正确,同时我也不认为网络上的其他文章的说法就和 React 被设计时的初衷完全一致,甚至 React 源码中的很多写法也未必完美。不管阅读什么代码,我们都不要神话它,而是应该辩证的去看待它。总的来说,功过 91 开。 前端世界并不需要第二个 React ,我们学习的意义并不是为了证明我们对这个框架有多么了解。而是通过窥探这些顶级工程师的实现思路,去完善我们自己的逻辑体系,从而成为一个更加严谨的人。 参考资料 [1]simple_react: https://github.com/XHFkindergarten/simple_react [2]拉取源码: https://react.iamkasong.com/preparation/source.html#%E6%8B%89%E5%8F%96%E6%BA%90%E7%A0%81 [3]simple-virtual-dom: https://github.com/livoras/simple-virtual-dom [4]合成事件: https://zh-hans.reactjs.org/docs/legacy-event-pooling.html [5]为什么 isReactComponent 是一个对象而不是布尔以及为什么不能用 instanceOf : https://github.com/facebook/react/pull/4663 [6]文章: https://zhuanlan.zhihu.com/p/37095662 [7]MessageChannel: https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel 本文分享自微信公众号 - 凹凸实验室(AOTULabs)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

WidgetKit 入门指北

作者:zvving,iOS 开发者,现就职于字节跳动音乐团队 本主题基于 https://developer.apple.com/videos/play/wwdc2020/10028/ 梳理 概述 WidgetKit 是 WWDC20 备受关注的新特性,支持在 iOS、iPadOS 主屏幕,今日视图以及 macOS 通知栏展示动态信息和个性化内容。 众所周知,Apple 对 iOS 主屏幕的改进一直保持克制,这次引入 Widget 是一次巨大的改动。Widget 如何提供一致的跨平台体验?如何提供实时的个性化内容?如何引入 Widget 同时最大可能减少主屏幕的性能、电量开销?本文作为 WidgetKit 速览,带你快速了解这些内容。 优秀 Widget 的三要素 设计优秀的 Widget,包含如下三个要素 Glanceable 一目了然 Relevant 高相关性 Personalized 个性化 Glanceable 一目了然 Widgets are not mini-apps Widget 并不是(常驻桌面的)微型应用程序,这句话被反复提起。在一闪而过的快速切换应用的主屏幕里,设计交互复杂的应用界面并不能切合用户的需要。一目了然的内容才是用户关心的唯一要素 通过强大的 SwiftUI,你可以快速实现跨平台体验一致的 Widget,同时非常方便的支持 Dynamic Type、DarkMode 等系统特性,提供一目了然、快速响应的桌面体验。 Relevant 高相关性 一般用户每天进入主屏幕的次数超过 90 次,但停留的总时长不过几分钟。用户可不想在主屏幕见到枯燥的加载界面 WidgetKit 会在正确的时机显示即时的内容,避免每个 Widget 启动、加载、渲染的漫长过程。为了达成这一目标,WidgetKit 统一管理所有 Widget,支持预渲染、复用,并提供合适的更新策略,灵活可控的更新时机。 智能叠放 智能叠放是 Widget 与用户高相关性的又一体现。它是支持多个 Widget 叠放的智能组件。用户可以上下滑动手动切换,更巧妙的是:基于用户的使用场景以及开发者的配置,系统也会智能地为用户展示最需要的 Widget。 Personalized 个性化 在天气 widget 中,基于不同地理位置,语言、摄氏度单位,提供个性化的使用体验。同时支持三种尺寸,更小的尺寸展示核心内容,更大的尺寸则展示更多的相关信息,满足不同用户的需要。 快速开发一个 Widget 配置信息 Configuration Static configuration :静态配置,无用户配置项 Intent configuration :支持用户配置及用户意图推测功能(Intents Extension) SupportedFamilies Widget 支持 small、medium、large三种尺寸 ,建议开发者同时提供三种尺寸。 Placeholder UI Placeholder UI 是 Widget 加载中所显示的占位信息,它应该清晰的表达 Widget 所属类型。绝大多数场景下,用户不会看到 Placeholder UI ,只有在修改设备环境配置等个别场景下会碰到。 Widget 交互 Widgets are not mini-apps Widget UI 是无状态的,不支持滚动,不能展示视频和动态图像。 Widget 为用户提供一目了然的内容展示,在用户需要进一步查看或编辑的时候,通过轻点启动 app 完成顺畅的体验。 用户点击 Widget 中的特定专辑后,通过 Deep links,App 启动后快速展示对应专辑,方便用户的进一步操作。 注意:systemSmall 只支持一个 link ,systemMedium,systemLarge 支持多个 WidgetKit 执行原理 开发者通过 SwiftUI 构建 Views,定义 Timelines 为 Views 提供对应时间所需数据。数据变化时,通过 reload 更新数据。 WidgetKit 统一管理多个 Widgets:序列化 Widget Views 和 Timelines 数据,处理预渲染和复用,响应并处理 System reloads、App-driven reloads,为桌面、Widget Gallery 预览等场景提供高效、即时的 widget 体验。 值得一提的是:WidgetKit 会把 Timelines 所定义的 Entries 对应的 Views 结构信息缓存到磁盘,仅在需要的时候实时渲染特定的 View。这使得系统可以在极低电量开销下为众多 Widgets 处理 Timelines 信息。 Views Placeholder UI:(上文已经介绍)Widget 占位 UI Snapshot 以最快速度返回的视图。并非截图,而是真实体验。 大多数I情况下,timeline 第一条数据可以与 snapshot 相同,这样用户在 Widget Gallery 中看到内容和添加到设备时得到的东西相同。 Timelines Timeline 是一系列驱动 Views 的数据集合,WidgetKit 随着时间流逝加载对应的 Widget 数据,配合 Reload 策略灵活支持复杂业务场景的变化。 Reloads Reloads 分两类:System reloads,App-driven reloads。系统会综合判断,确定重新加载 widget 的最佳时间。 System reloads ReloadPolicy:配置 timeline 时定制 reload 策略 atEnd after(date: Date) never Widget 被查看越多,将更多的被 reload 设备环境变化也会触发 reload App-driven reloads 当应用在后台时,后台推送可以触发 reload;当应用在前台时,应用可以主动触发 reload。进而通过 reload 变更全部或特定 timeline entry。 请谨慎的对待 widget reload,仅当相关数据变化需要反映在 widget 中时才触发 relaod,避免无谓的重新加载。 对于后台加载需要网络数据可以通过 Background sessions 完成,相关的系统资源占用将计入 widget 开销,由 WidgetKit 管理对应 background reload 预算。 个性化并理解用户意图 Intents framework Intents framework 一直被应用在 SiriKit 和 Shortcuts,从现在开始 WidgetKit 也将借助 Intents framework 更好的理解用户意图。 驱动理解用户意图的智能堆栈 在智能堆栈小组件中,开发者通过每一个 TimelineEntryRelevance 可以告知 WidgetKit 用户在特定场景下相关性评分和持续时间。WidgetKit 综合评估不同 Widget 评分,分析用户意图,旋转堆栈到用户关心的 widget。 注意:widget 提供的所有 TimelineEntryRelevance 都会作为理解用户意图相关性的参考 Demo 配置 Widget 是很容易的,如下 SampleWidget 代码已经包含大部分 widget 初始配置: 静态 WidgetConfiguration 需要提供 Provider(),PlaceholderView(),entry -> SampleWidgetEntryView 闭包 Provider 中定义 snapshot,构建包含 Entry 集合的 Timeline 最后使用 SwiftUI 实现 PlaceholderView 和 SampleWidgetEntryView 即可 @mainpublicstructSampleWidget:Widget{privateletkind:String="SampleWidget"publicvarbody:someWidgetConfiguration{StaticConfiguration(kind:kind,provider:Provider(),placeholder:PlaceholderView()){entryinSampleWidgetEntryView(entry:entry)}.configurationDisplayName("MyWidget").description("Thisisanexamplewidget.")}}publicstructProvider:TimelineProvider{publicfuncsnapshot(withcontext:Context,completion:@escaping(SimpleEntry)->()){letentry=SimpleEntry(date:Date())completion(entry)}publicfunctimeline(withcontext:Context,completion:@escaping(Timeline<Entry>)->()){letentry=SimpleEntry(date:Date())lettimeline=Timeline(entries:[entry,entry],policy:.atEnd)completion(timeline)}} 总结 终于讲完了,为了理解上的完整性,本文做了很多扩展性的说明,感谢各位读者的耐心阅读;这是 Widget 系列的最后一篇,WidgetKit 的概念很棒,也非常好用!希望大家能真正读懂原理,创造更多优秀的 Widget! 推荐阅读 ✨ Apple Widget:下一个顶级流量入口? ✨ 为 Widgets 构建 SwiftUI 视图 《Widgets 边看边写》第一部分:冒险开始了 《Widgets 边看边写》第二部分:Timelines 的基本使用 《Widgets 边看边写》第三部分:Timelines的进阶使用 关注我们 我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。 支持作者 这篇文章的内容来自于 《WWDC20 内参》。在这里给大家推荐一下这个专栏,专栏目前已经创作了 101 篇文章,只需要 29.9 元。点击【阅读原文】,就可以购买继续阅读 ~ WWDC 内参 系列是由老司机周报、知识小集合以及 SwiftGG 几个技术组织发起的。已经做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并号召一群一线互联网的 iOS 开发者,结合自己的实际开发经验、苹果文档和视频内容做二次创作。 本文分享自微信公众号 - 老司机技术周报(LSJCoding)。如有侵权,请联系 support@oschina.cn 删除。本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

优秀的个人博客,低调大师

Fabric Kafka入门

Hyperledger Fabric推荐Kafa用于生产环境。Kafa是一个分布式、具有水平伸缩能力、崩溃容错能力的日志系统。在Hyperledger Fabric区块链中可以有多个Kafka节点,使用zookeeper进行同步管理。本文将介绍Kfaka的基本工作原理,以及在HyperledgerFabric中使用Kafka和zookeeper实现共识的原理,并通过一个实例剖析Hyperledger Farbic中Kafka共识的达成过程。 如果希望快速掌握Fabric区块链的链码及应用开发,建议访问汇智网的在线互动课程: Fabric区块链Java开发详解 Fabric区块链NodeJs开发详解 一、Kafka工作原理 Kafka本质上是一个消息处理系统,它使用的是经典的发布-订阅模型。消息的消费者订阅特定的主题,以便收到新消息的通知,生产者则负责消息的发布。 当主题的数据规模变得越来越大时,可以拆分为多个分区,Kafka保障在一个分区内的消息是按顺序排列的。 Kafka并不跟踪消费者读取了哪些消息,也不会自动删除已经读取的消息。Kafka会保存消息一段时间,例如一天,或者直到数据规模超过一定的阈值。消费者需要轮询新的消息,这是的他们可以根据自己的需求来定位消息,因此可以重放或重新处理事件。消费者处于不同的消费者分组,对应一个或多个消费者进程。每个分区被分贝给单一的消费者进程,因此同样的消息不会被多次读取。 崩溃容错机制是通过在多个Kafka代理之间复制分区来实现的。因此如果一个代理由于软件或硬件故障挂掉,数据也不会丢失。当然接下来还需要一个领导-跟随机制,领导者持有分区,跟随者则进行分区的复制。当领导者挂掉后,会有某个跟随者转变为新的领导者。 如果一个消费者订阅了某个主体,那么它怎么知道从哪个分区领导者来读取订阅的消息? 答案在于zookeeper服务。 zookeeper是一个分布式key-value存储库,通常用于存储元数据及集群机制的实现。zookeeper允许服务(Kafka代理)的客户端订阅变化并获得实时通知。这就是代理如何确定应当使用哪个分区领导者的原因。zookeeper有超强的故障容错能力,因此Kafka的运行严重依赖于它。 在zookeeper中存储的元数据包括: 消费者分组在每个分区的读取偏移量 访问控制清单,用于访问授权与限制 生产者及消费者配额,每秒最多消息数量 分区领导者及健康信息 二、Hyperledger Fabric中的Kafka 要理解在超级账本Hyperledger Fabric中的Kafka是如何工作的,首先需要理解几个重要的术语: Chain - 指的是一组客户端(通道/channel)可以访问的日志 Channel - 一个通道类似于一个主题,授权的对等节点(peer)可以订阅并且成为通道的成员。 只有通道的成员可以在通道上交易,一个通道中的交易在其他通道中看不到。 OSN - 即排序服务节点(Ordering Service Node),在Fabric中被称为排序节点。排序节点负责: 进行客户鉴权 允许客户端通过一个简单的接口写入或读取通道 执行配置交易的过滤与验证,实现通道的重新配置或创建新的通道 RPC - 即远程过程调用(Remote Procedure Call),是一种用于调用其他机器上的服务而无需了解 通信与实现细节的通信协议,目的是像调用本地函数一样调用网络中其他机器上的函数 广播PRC - 交易提交调用,由排序节点执行 分发RPC - 交易分发请求,当交易由kafka代理处理后,分发给请求节点 注意,虽然在Hyperledger Fabric中Kafka被称为共识(Consensus),但是其核心是交易排序服务以及额外的崩溃容错能力。 在Hyperledger Fabric中的Kafka实际运行逻辑如下: 对于每一条链,都有一个对应的分区 每个链对应一个单一的分区主题 排序节点负责将来自特定链的交易(通过广播RPC接收)中继到对应的分区 排序节点可以读取分区并获得在所有排序节点间达成一致的排序交易列表 一个链中的交易是定时分批处理的,也就是说当一个新的批次的第一个交易进来时,开始计时 当交易达到最大数量时或超时后进行批次切分,生成新的区块 定时交易是另一个交易,由上面描述的定时器生成 每个排序节点为每个链维护一个本地日志,生成的区块保存在本地账本中 交易区块通过分发RPC返回客户端 当发生崩溃时,可以利用不同的排序节点分发区块,因为所有的排序节点都维护有本地日志 三、Hyperledger Fabric Kafka实例解析 考虑下图,假设排序节点OSN0和OSN2时连接到广播客户端,OSN1连接到分发客户端。 OSN0已经有了交易foo,中继到kafka集群 此时OSN2将交易baz广播到集群中 最后,交易bar由OSN0发送到集群中 集群现在有三个交易,可以在图中看到三个交易的在日志中的位置偏移量 客户端发送分发请求,在OSN1的本地日志中,上述三个交易在4#区块里。 因此OSN1将4#区块返回客户端,处理结束 Kakfa的高性能对于Hyperledger Fabric有很大的帮助,多个排序节点通过Kafka实现同步,而Kafka本身并不是排序节点,它只是将排序节点通过流连接起来。虽然Kafka支持崩溃容错,它并不能提供对网络中恶意攻击的保护。需要一种拜占庭容错方案(BFT)才可以对抗恶意的攻击,但是目前在Farbic框架中还有待实现这一机制。 总而言之,在Hyperledger Farbic中,Kafka共识模块是可以用于生产环境的,它可以支持崩溃容错,但无法对抗恶意攻击。 原文:The ABCs of Kafka in Hyperledger Fabric

优秀的个人博客,低调大师

yaml 语言入门

编程免不了要写配置文件,YAML 是专门用来写配置文件的语言,非常简洁和强大,远比 JSON 格式方便。本文介绍 YAML 的语法,以JS-YAML的实现为例。你可以去在线 Demo验证下面的例子。 一、简介 YAML 语言(发音 /ˈjæməl/ )的设计目标,就是方便人类读写。它实质上是一种通用的数据串行化格式。 它的基本语法规则如下。 大小写敏感 使用缩进表示层级关系 缩进时不允许使用Tab键,只允许使用空格。 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可 #表示注释,从这个字符一直到行尾,都会被解析器忽略。 YAML 支持的数据结构有三种。 对象:键值对的集合,又称为映射(mapping)/ 哈希(hashes) / 字典(dictionary) 数组:一组按次序排列的值,又称为序列(sequence) / 列表(list) 纯量(scalars):单个的、不可再分的值 以下分别介绍这三种数据结构。 二、对象 对象的一组键值对,使用冒号结构表示。 animal: pets 转为 JavaScript 如下。 { animal: 'pets' } Yaml 也允许另一种写法,将所有键值对写成一个行内对象。 hash: { name: Steve, foo: bar } 转为 JavaScript 如下。 { hash: { name: 'Steve', foo: 'bar' } } 三、数组 一组连词线开头的行,构成一个数组。 - Cat - Dog - Goldfish 转为 JavaScript 如下。 [ 'Cat', 'Dog', 'Goldfish' ] 数据结构的子成员是一个数组,则可以在该项下面缩进一个空格。 - - Cat - Dog - Goldfish 转为 JavaScript 如下。 [ [ 'Cat', 'Dog', 'Goldfish' ] ] 数组也可以采用行内表示法。 animal: [Cat, Dog] 转为 JavaScript 如下。 { animal: [ 'Cat', 'Dog' ] } 四、复合结构 对象和数组可以结合使用,形成复合结构。 languages: - Ruby - Perl - Python websites: YAML: yaml.org Ruby: ruby-lang.org Python: python.org Perl: use.perl.org 转为 JavaScript 如下。 { languages: [ 'Ruby', 'Perl', 'Python' ], websites: { YAML: 'yaml.org', Ruby: 'ruby-lang.org', Python: 'python.org', Perl: 'use.perl.org' } } 五、纯量 纯量是最基本的、不可再分的值。以下数据类型都属于 JavaScript 的纯量。 字符串 布尔值 整数 浮点数 Null 时间 日期数值直接以字面量的形式表示。 number: 12.30 转为 JavaScript 如下。 { number: 12.30 } 布尔值用true和false表示。 isSet: true 转为 JavaScript 如下。 { isSet: true } null用~表示。 parent: ~ 转为 JavaScript 如下。 { parent: null } 时间采用 ISO8601 格式。 iso8601: 2001-12-14t21:59:43.10-05:00 转为 JavaScript 如下。 { iso8601: new Date('2001-12-14t21:59:43.10-05:00') } 日期采用复合 iso8601 格式的年、月、日表示。 date: 1976-07-31 转为 JavaScript 如下。 { date: new Date('1976-07-31') } YAML 允许使用两个感叹号,强制转换数据类型。 e: !!str 123 f: !!str true 转为 JavaScript 如下。 { e: '123', f: 'true' } 六、字符串 字符串是最常见,也是最复杂的一种数据类型。 字符串默认不使用引号表示。 str: 这是一行字符串 转为 JavaScript 如下。 { str: '这是一行字符串' } 如果字符串之中包含空格或特殊字符,需要放在引号之中。 str: '内容: 字符串' 转为 JavaScript 如下。 { str: '内容: 字符串' } 单引号和双引号都可以使用,双引号不会对特殊字符转义。 s1: '内容\n字符串' s2: "内容\n字符串" 转为 JavaScript 如下。 { s1: '内容\\n字符串', s2: '内容\n字符串' } 单引号之中如果还有单引号,必须连续使用两个单引号转义。 str: 'labor''s day' 转为 JavaScript 如下。 { str: 'labor\'s day' } 字符串可以写成多行,从第二行开始,必须有一个单空格缩进。换行符会被转为空格。 str: 这是一段 多行 字符串 转为 JavaScript 如下。 { str: '这是一段 多行 字符串' } 多行字符串可以使用|保留换行符,也可以使用>折叠换行。 this: | Foo Bar that: > Foo Bar 转为 JavaScript 代码如下。 { this: 'Foo\nBar\n', that: 'Foo Bar\n' } +表示保留文字块末尾的换行,-表示删除字符串末尾的换行。 s1: | Foo s2: |+ Foo s3: |- Foo 转为 JavaScript 代码如下。 { s1: 'Foo\n', s2: 'Foo\n\n\n', s3: 'Foo' } 字符串之中可以插入 HTML 标记。 message: | <p style="color: red"> 段落 </p> 转为 JavaScript 如下。 { message: '\n<p style="color: red">\n 段落\n</p>\n' } 七、引用 锚点&和别名*,可以用来引用。 defaults: &defaults adapter: postgres host: localhost development: database: myapp_development <<: *defaults test: database: myapp_test <<: *defaults 等同于下面的代码。 defaults: adapter: postgres host: localhost development: database: myapp_development adapter: postgres host: localhost test: database: myapp_test adapter: postgres host: localhost &用来建立锚点(defaults),<<表示合并到当前数据,*用来引用锚点。 下面是另一个例子。 - &showell Steve - Clark - Brian - Oren - *showell 转为 JavaScript 代码如下。 [ 'Steve', 'Clark', 'Brian', 'Oren', 'Steve' ] 八、函数和正则表达式的转换 这是JS-YAML库特有的功能,可以把函数和正则表达式转为字符串。 # example.yml fn: function () { return 1 } reg: /test/ 解析上面的 yml 文件的代码如下。 var yaml = require('js-yaml'); var fs = require('fs'); try { var doc = yaml.load( fs.readFileSync('./example.yml', 'utf8') ); console.log(doc); } catch (e) { console.log(e); } 从 JavaScript 对象还原到 yaml 文件的代码如下。 var yaml = require('js-yaml'); var fs = require('fs'); var obj = { fn: function () { return 1 }, reg: /test/ }; try { fs.writeFileSync( './example.yml', yaml.dump(obj), 'utf8' ); } catch (e) { console.log(e); }

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

用户登录
用户注册