Friday, April 15, 2022

SegmentFault 最新的文章

SegmentFault 最新的文章


petite-vue源码剖析-逐行解读@vue-reactivity之Map和Set的reactive

Posted: 14 Apr 2022 06:59 PM PDT

本篇我们会继续探索reactive函数中对Map/WeakMap/Set/WeakSet对象的代理实现。

Map/WeakMap/Set/WeakSet的操作

由于WeakMap和WeakSet分别是Map和Set的不影响GC执行垃圾回收的版本,这里我们只研究Map和Set即可。

Set的属性和方法

  • size: number 为访问器属性(accessor property),返回Set对象中的值的个数
  • add(value: any): Set 向Set对象队尾添加一个元素
  • clear(): void 移除Set对象内所有元素
  • delete(value: any): boolean 移除Set中与入参值相同的元素,移除成功则返回true
  • has(value: any): boolean 判断Set中是否存在与入参值相同的元素
  • values(): Iterator 返回一个新的迭代器对象,包含Set对象中按插入顺序排列的所有元素
  • keys(): Iteratorvalues(): Iterator一样的功效
  • @@iteratorvalues(): Iterator一样的功效,for of中调用
  • entries(): Iterator 返回一个新的迭代器对象,包含Set对象中按插入顺序排列的所有元素,但为与Map使用一致每次迭代返回的内容为[value, value]
  • forEach(callbackFn: { (value: any, set: Set) => any } [, thisArg]) 按插入顺序遍历Set对象的每一个元素

Map的属性和方法

  • size: number 为访问器属性(accessor property),返回Set对象中的值的个数
  • set(key: any, value: any): Map 向Map对象添加或更新一个指定键的值
  • clear(): void 移除Map对象内所有键值对
  • delete(key: any): boolean 移除Map对象中指定的键值对,移除成功则返回true
  • has(key: any): boolean 判断Map中是否存在键与入参值相同的键值对
  • values(): Iterator 返回一个新的迭代器对象,包含Map对象中按插入顺序排列的所有值
  • keys(): Iterator 返回一个新的迭代器对象,包含Map对象中按插入顺序排列的所有键
  • @@iteratorentries(): Iterator一样的功效,for of中调用
  • entries(): Iterator 返回一个新的迭代器对象,包含Map对象中按插入顺序排列的所有键值对
  • forEach(callbackFn: { (value: any, key: any, map: Map) => any } [, thisArg]) 按插入顺序遍历Map对象的每一个键值对
  • get(key: any): any 返回Map对象中指定键对应的值,若没有则返回undefined

逐行看代码我是认真的

// reactive.ts  export const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {   get: /*#__PURE__*/ createInstrumentationGetter(false, false) }

由于Map/Set不像Object或Array那样可直接通过属性访问的方式获取其中的元素,而是通过add,has,delete操作,因此需要像处理Array的slice等方法那样代理Map/Set的这些方法。

// collectionHandlers.ts  type MapTypes = Map<any, any> | WeakMap<any, any> type SetTypes = Set<any, any> | WeakSet<any, any>  // 代理Map/Set原生的方法 // 没有代理返回迭代器的方法?? const mutableInstrumentations = {   get(this: MapTypes, key: unknown) {     return get(this, key)   }   get size() {     // 原生的size属性就是一个访问器属性     return size(this as unknown as IterableCollections)   },   has,   add,   set,   delete: deleteEntry, // delete 是关键字不能作为变量或函数名称   clear,   forEach: createForEach(false, false) }  function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {   const instrumentations = mutableInstrumentations    return (     target: CollectionTypes,     key: string | symbol,     receiver: CollectionTypes   ) => {     if (key === ReactiveFlags.IS_REACTIVE) {       return !isReadonly     }     else if (key === ReactiveFlags.IS_READONLY) {       return isReadonly     }     else if (key === ReactiveFlags.RAW) {       return target     }      // 代理Map/WeakMap/Set/WeakSet的内置方法     return Reflect.get(       hasOwn(instrumentations, key) && key in target         ? instrumentations         : target,       key,       receiver     )   } }

TypeScript小课堂as断言——this as unknown as IterableCollections
在TypeScript中可通过类型声明定义变量的类型(其中包含复合类型),而类型推导则可以根据赋值语句中右侧字面量推导出变量的实际类型,或通过当前变量使用的场景推导出当前实际类型(尤其是定义为复合类型)。但有时无法通过当前使用场景执行精确的类型推导,这时开发者可以通过as断言告知TypeScript编译器该变量当前使用范围的数据类型(要相信自己一定比编译器更了解自己的代码:D)。
那么as unknown即表示将类型修改为unknown,那么类型为unknown是表示什么呢?unknown是TypeScript3.0引入的top type(任何其他类型都是它的subtype),意在提供一种更安全的方式替代any类型(any类型是top type也是bottom type,使用它意味和绕过类型检查),具有如下特点:

  1. 任何其它类型都可以赋值给unknown类型的变量
  2. unknown类型的变量只能赋值给anyunknown类型的变量
  3. 如果不对unknown类型的变量执行类型收缩,则无法执行其它任何操作
// 1. 任何其它类型都可以赋值给`unknown`类型的变量  let uncertain: unknown = 'Hello' uncertain = 12 uncertain = { hello: () => 'Hello' }  // 2.`unknown`类型的变量只能赋值给`any`或`unknown`类型的变量  let uncertain: unknown = 'Hello' let noSure: any = uncertain let notConfirm: unknown = uncertain  // 3. 如果不对`unknown`类型的变量执行类型收缩,则无法执行其它任何操作 let uncertain = { hello: () => 'Hello' } uncertain.hello() // 编译报错  // 通过断言as收缩类型 (uncertain as {hello: () => string}).hello()  let uncertain: unknown = 'Hello' // 通过typeof或instanceof收缩类型 if (typeof uncertain === 'string') {   uncertain.toLowerCase() }

那么as unknown后的as IterableCollections意图就十分明显了,就是对变量进行类型收缩。this as unknown as IterableCollections其实就是as IterableCollections啦。

然后我们逐一看看代理方法的实现吧

Mapget方法

get方法只有Map对象拥有,因此其中主要思路是从Map对象中获取值,跟踪键值变化后将值转换为响应式对象返回即可。
但由于要处理readonly(reactive(new Map()))这一场景,添加了很多一时让人看不懂的代码而已。

const getProto = <T extends CollectionTypes>(v: T): any => Reflect.getProrotypeOf(v)  // 代理Map/WeakMap的get方法 function get(   target: MapTypes, // 指向this,由于Map对象已经被代理,因此this为代理代理   key: unknown,   isReadonly = false,   isShallow = false ) {   /**    * 1. 针对readonly(reactive(new Map()))的情况,    *    target获取的是代理对象,而rawTarget的是Map对象    * 2. 针对reactive(new Map())的情况,    *    target和rawTarget都是指向Map对象    */    target = (target as any)[ReactiveFlags.RAW]   const rawTarget = toRaw(target)   /**    * 若key为代理对象,那么被代理对象和代理对象的键都会被跟踪,即    * const key = { value: 'foo' }    * const pKey = reactive(key),     * const kvs = reactive(new Map())    * kvs.set(pKey, 1)    *     * effect(() => {    *   console.log('pKey', kvs.get(pKey))    * })    * effect(() => {    *   console.log('key', kvs.get(key))    * })    *     * kvs.set(pKey, 2)    * // 回显 pkey 2 和 key 2    * kvs.set(key, 3)    * // 回显 key 2    */     const rawKey = toRaw(key)   if (key !== rawKey) {     !isReadonly && track(rawTraget, TrackOpTypes.GET, key)   }   !isReadonly && track(rawTraget, TrackOpTypes.GET, rawKey)    // 获取Map原型链上的has方法用于判断获取成员是否存在于Map對象上   const { has } = getProto(rawTarget)   const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive   /**    * Map对象中存在则从Map对象或代理对象上获取值并转换为响应式对象返回。    * 针对readonly(reactive(new Map()))为什么是从响应对象上获取值,而不是直接从Map对象上获取值呢?    * 这是为了保持返回的值的结构,从响应式对象中获取值是响应式对象,在经过readonly的处理则返回的值就是readonly(reactive({value: 'foo'}))。    */    if (has.call(rawTarget, key)) {     return wrap(target.get(key))   }   else if (has.call(rawTarget, rawKey)) {     return wrap(target.get(rawKey))   }   else if (target !== rawTarget) {     /**      * 针对readonly(reactive(new Map())),即使没有匹配的键值对,也要跟踪对响应式对象某键的依赖信息      * const state = reactive(new Map())      * const readonlyState = readonly(state)      *       * effect(() => {      *  console.log(readonlyState.get('foo'))      * })      * // 回显 undefined      * state.set('foo', 1)      * // 回显 1      */     target.get(key)   }    // 啥都没有找到就默认返回undefined,所以啥都不用写 }

MapSetsize访问器属性

function size(target: IterableCollections, isReadonly = false) {   // 针对readonly(reactive(new Map())) 或 readonly(reactive(new Set()))只需获取响应式对象即可,因此reactive对象也会对size的访问进行相同的操作。   target = (target as any)[RectiveFlags.RAW]   // 跟踪ITERATE_KEY即所有修改size的操作均会触发访问size属性的副作用函数   !iReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)   /**    * 由于size为访问器属性因此若第三个参数传递receiver(响应式对象),而响应式对象并没有size访问器属性需要访问的属性和方法,则会报异常``。因此需要最终将Map或Set对象作为size访问器属性的this变量。    */   return Reflect.get(target, 'size', target) }

MapSethas方法

function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {   const target = (this as any)[ReactiveFlags.RAW]   const rawTarget = toRaw(target)   const rawKey = toRaw(key)   // 和get方法代理一样,若key为代理对象则代理对象或被代理对象作为键的键值对发生变化都会触发访问has的副作用函数   if (key !== rawKey) {     !isReadonly && track(rawTarget, TrackOpTypes.HAS, key)   }   !isReadonly && track(rawTarget, TrackOpTypes.HAS, rawKey)    return key === rawKey     ? target.has(key)     : target.has(key) || target.has(rawKey) }

Setadd方法

function add(this: SetTypes, value: unknown) {   value = toRaw(value)   const target = toRaw(this)   const proto = getProto(target)   const hadKey = proto.has.call(target, value)   // 当Set对象中没有该元素时则触发依赖ITERATE_KEY的副作用函数,因此ADD操作会影响Set对象的长度   if (!hadKey) {     target.add(value)     trigger(target, TriggerOpTypes.ADD, value, value)   }    return this }

Mapset方法

function set(this: MapTypes, key: unknown, value: unknown) {   value = toRaw(value)   const target = toRaw(this)   const { has, get } = getProto(target)    // 分别检查代理和非代理版本的key是否存在于Map对象中   let hadKey = has.call(target, key)   if (!hadKey) {     key = toRaw(key)     hadKey = has.call(target.key)   }    const oldValue = get.call(target, key)   target.set(key, value)   if (!hadKey) {     // 当Map对象中没有该元素时则触发依赖ITERATE_KEY的副作用函数,因此ADD操作会影响Map对象的长度     trigger(target, TriggerOpTypes.ADD, key, value)   }   else if (hasChanged(value, oldValue)) {     // 如果新旧值不同则触发修改,依赖该键值对的副作用函数将被触发     trigger(target, TriggerOpTypes.SET, key, value, oldValue)   } }

注意:gethas方法中会同时跟踪代理和非代理版本的键对应的元素变化,而set方法则只会触发查找到的代理或非代理版本的键对应的元素变化。

deleteEntry方法

function deleteEntry(this: CollectionTypes, key: unknown) {   const target = toRaw(this)   const { has, get } = getProto(target)   let hadKey = has.call(target, key)   // 分别检查代理和非代理版本的key是否存在于Map/Set对象中   let hadKey = has.call(target, key)   if (!hadKey) {     key = toRaw(key)     hadKey = has.call(target.key)   }    // 如果当前操作的是Map对象则获取旧值   const oldValue = get ? get.call(target, key) : undefined   const result = target.delete(key)   if (hadKey) {     trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)   }   return result }

注意:gethas方法中会同时跟踪代理和非代理版本的键对应的元素变化,而deleteEntry方法则只会触发查找到的代理或非代理版本的键对应的元素变化。

MapSetclear方法

function clear(this: IterableCollections) {   const target = toRaw(this)   const hadItems = target.size !== 0   const oldTarget = undefined   const result = target.clear()   if (hadItems) {     trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)   }   return result }

MapSetforEach方法

function createForEach(isReadonly: boolean, isShallow: boolean) {   return function forEach(     this: IterableCollections,     callback: Function,     thisArg?: unknown   ) {     const observed = this as any     const target = observed[ReactiveFlags.RAW]     const rawTarget = toRaw(target)     const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive     !isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)     return target.forEach((value: unknown, key: unknown) => {       // 将key和value都转换为代理对象       return callback.call(thisArg, wrap(value), wrap(key), observed)     })   } }

由于forEach会遍历所有元素(Map对象则是所有键值对),因此跟踪ITERATE_KEY即Map/Set对象元素个数发生变化则触发forEach函数的执行。

迭代器对象相关方法

至此我们还没对entries,values,keys@@iterator这些返回迭代器的对象方法进行代理,而源码中则在最后为mutableInstrumentations添加这些方法的代理。

const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator/*就是@@iterator*/] iteratorMethods.forEach(method => {   mutableInstrumentations[method as string] = createIterableMethod(     method,     false,     false   ) })
function createIterableMethod(   method: string | symbol,   isReadonly: boolean,   isShallow: boolean ) {   return function(     this: IterableCollections,     ...args: unknown[]   ): Iterable & Iterator {     /**      * 1. 针对readonly(reactive(new Map()))的情况,      *    target获取的是代理对象,而rawTarget的是Map或Set对象      * 2. 针对reactive(new Map())的情况,      *    target和rawTarget都是指向Map或Set对象      */      const target = (this as any)[ReactiveFlags.RAW]     const rawTarget = toRaw(target)      const targetIsMap = isMap(rawTarget)     const isPair = method === 'entries' || (method === Symbol.iterator && targetIsMap)     /**      * 当调用的是Map对象的keys方法,副作用函数并没有访问值对象,即副作用函数只依赖Map对象的键而没有依赖值。      * 而键只能增加或删除,值可增加、删除和修改,那么此时当且仅当键增删即size属性发生变化时才会触发副作用函数的执行。      * 若依赖值,那么修改其中一个值也会触发副作用函数执行。      */     const isKeyOnly = method === 'keys' && targetIsMap     const innerIterator = target[method](...args)     const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive     !isReadonly &&       track(         rawTarget,         TrackOpTypes.ITERATE,         isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY       )      return {       // 迭代器协议       next() {         const { value, done } = innerIterator.next()         return done           ? { value, done }           : {             value: isPair ? [wrap(value[0], wrap(value[1]))] : wrap(value),             done           }       },       // 可迭代协议       [Symbol.iterator]() {         return this       }     }   } }

可迭代协议(iterable protocol)

可迭代协议(iterable protocol),用于创建迭代器(iterator)。
如下内置类型都实现了可迭代协议:

  • 字符串
  • 数组
  • Set
  • Map
  • arguements对象
  • NodeList等DOM集合类型

下面的语言特性将会接收可迭代协议返回的迭代器

  • for...of循环
  • 数据解构(const [a, b] = [1, 2])
  • 扩展操作符(const a = [1,2], b = [...a])
  • Array.from()
  • 创建Set
  • 创建Map
  • Promise.all()接受可迭代对象
  • Promise.race()接受可迭代对象
  • yield*操作符

让对象支持可迭代协议其实很简单,只需实现返回迭代器的[Symbol.iterator]方法即可。JavaScript Plain Old Object默认并没有支持可迭代协议,那么我们可以自行实现以下:

const iterablizeKeys = (obj: {}) => {   if (!obj[Symbol.iterator]) {     obj[Symbol.iterator] = () => {       const keys = Object.keys(obj) as const       let i = 0        // 返回一个迭代器       return {         next() {           return { value: keys[i++], done: i > keys.length }         }       }     }   }    return obj }   const iterableObj = iterablizeKeys({a: 1, b: 2}) for (let item of iterableObj) {   console.log(item) } // 回显 a  // 回显 b Array.from(iterableObj) // 返回 ['a', 'b']

迭代器协议(iterator protocol)

迭代器协议(iterator protocol),提供不接受任何参数并返回IteratorResult对象的next方法,而IteratorResult对象包含指向当前元素的value属性和表示迭代是否已结束的done属性,当done属性值为true时表示迭代已结束。
迭代器协议的实现正如上面可迭代协议的示例中那样,不过我们还可以将可迭代协议和迭代对象在同一个对象上实现。

const iterablizeKeys = (obj: {}) => {   if (!obj[Symbol.iterator]) {     let iteratorState = {       keys: []       i: 0     }     // 迭代器协议     obj.next = () => ({ value: iteratorState.keys[iteratorState.i++], done: iteratorState.i > iteratorState.key.length })      // 可迭代协议     obj[Symbol.iterator] = () => {       iteratorState.keys = Object.keys(obj) as const       iteratorState.i = 0        // 返回一个迭代器       return this     }   }    return obj }   const iterableObj = iterablizeKeys({a: 1, b: 2}) for (let item of iterableObj) {   console.log(item) } // 回显 a  // 回显 b Array.from(iterableObj) // 返回 ['a', 'b']

总结

本篇我们通过逐行阅读源码了解到reactive如何处理Map和Set对象了,下一篇我们将开始以effect为入口进一步了解副作用函数是如何通过tracktrigger记录依赖和触发的。
尊重原创,转载请注明来自:https://www.cnblogs.com/fsjoh...肥仔John

40行代码实现React核心Diff算法

Posted: 14 Apr 2022 06:23 PM PDT

大家好,我卡颂。

凡是依赖虚拟DOM的框架,都需要比较前后节点变化Diff算法。

网上有大量讲解Diff算法逻辑的文章。然而,即使作者语言再精练,再图文并茂,相信大部分同学看完用不了多久就忘了。

今天,我们换一种一劳永逸的学习方法 —— 实现React的核心Diff算法。

不难,只有40行代码。不信?往下看。

欢迎加入人类高质量前端框架群,带飞

Diff算法的设计思路

试想,Diff算法需要考虑多少种情况呢?大体分三种,分别是:

  1. 节点属性变化,比如:
// 更新前 <ul>   <li key="0" className="before">0</li>   <li key="1">1</li> </ul>  // 更新后 <ul>   <li key="0" className="after">0</li>   <li key="1">1</li> </ul>
  1. 节点增删,比如:
// 更新前 <ul>   <li key="0">0</li>   <li key="1">1</li>   <li key="2">2</li> </ul>  // 更新后 情况1 —— 新增节点 <ul>   <li key="0">0</li>   <li key="1">1</li>   <li key="2">2</li>   <li key="3">3</li> </ul>  // 更新后 情况2 —— 删除节点 <ul>   <li key="0">0</li>   <li key="1">1</li> </ul>
  1. 节点移动,比如:
// 更新前 <ul>   <li key="0">0</li>   <li key="1">1</li> </ul>  // 更新后 <ul>   <li key="1">1</li>   <li key="0">0</li> </ul>

该如何设计Diff算法呢?考虑到只有以上三种情况,一种常见的设计思路是:

  1. 首先判断当前节点属于哪种情况
  2. 如果是增删,执行增删逻辑
  3. 如果是属性变化,执行属性变化逻辑
  4. 如果是移动,执行移动逻辑

按这个方案,其实有个隐含的前提—— 不同操作的优先级是相同的。但在日常开发中,节点移动发生较少,所以Diff算法会优先判断其他情况。

基于这个理念,主流框架(React、Vue)的Diff算法都会经历多轮遍历,先处理常见情况,后处理不常见情况

所以,这就要求处理不常见情况的算法需要能给各种边界case兜底。

换句话说,完全可以仅使用处理不常见情况的算法完成Diff操作。主流框架之所以没这么做是为了性能考虑。

本文会砍掉处理常见情况的算法,保留处理不常见情况的算法

这样,只需要40行代码就能实现Diff的核心逻辑。

Demo介绍

首先,我们定义虚拟DOM节点的数据结构:

type Flag = 'Placement' | 'Deletion';  interface Node {   key: string;   flag?: Flag;   index?: number; }

keynode的唯一标识,用于将节点在变化前、变化后关联上。

flag代表node经过Diff后,需要对相应的真实DOM执行的操作,其中:

  • Placement对于新生成的node,代表对应DOM需要插入到页面中。对于已有的node,代表对应DOM需要在页面中移动
  • Deletion代表node对应DOM需要从页面中删除

index代表该node在同级node中的索引位置

注:本Demo仅实现为node标记flag,没有实现根据flag执行DOM操作

我们希望实现的diff方法,接收更新前更新后NodeList,为他们标记flag

type NodeList = Node[];  function diff(before: NodeList, after: NodeList): NodeList {   // ...代码 }

比如对于:

// 更新前 const before = [   {key: 'a'} ] // 更新后 const after = [   {key: 'd'} ]  // diff(before, after) 输出 [   {key: "d", flag: "Placement"},   {key: "a", flag: "Deletion"} ]

{key: "d", flag: "Placement"}代表d对应DOM需要插入页面。

{key: "a", flag: "Deletion"}代表a对应DOM需要被删除。

执行后的结果就是:页面中的a变为d。

再比如:

// 更新前 const before = [   {key: 'a'},   {key: 'b'},   {key: 'c'}, ] // 更新后 const after = [   {key: 'c'},   {key: 'b'},   {key: 'a'} ]  // diff(before, after) 输出 [   {key: "b", flag: "Placement"},   {key: "a", flag: "Placement"} ]

由于b之前已经存在,{key: "b", flag: "Placement"}代表b对应DOM需要向后移动(对应parentNode.appendChild方法)。abc经过该操作后变为acb

由于a之前已经存在,{key: "a", flag: "Placement"}代表a对应DOM需要向后移动。acb经过该操作后变为cba

执行后的结果就是:页面中的abc变为cba。

Diff算法实现

核心逻辑包括三步:

  1. 遍历前的准备工作
  2. 遍历after
  3. 遍历后的收尾工作
function diff(before: NodeList, after: NodeList): NodeList {   const result: NodeList = [];    // ...遍历前的准备工作    for (let i = 0; i < after.length; i++) {     // ...核心遍历逻辑   }    // ...遍历后的收尾工作    return result; }

遍历前的准备工作

我们将before中每个node保存在以node.keykeynodevalueMap中。

这样,以O(1)复杂度就能通过key找到before中对应node

// 保存结果 const result: NodeList = [];    // 将before保存在map中 const beforeMap = new Map<string, Node>(); before.forEach((node, i) => {   node.index = i;   beforeMap.set(node.key, node); })

遍历after

当遍历after时,如果一个node同时存在于beforeafterkey相同),我们称这个node可复用。

比如,对于如下例子,b是可复用的:

// 更新前 const before = [   {key: 'a'},   {key: 'b'} ] // 更新后 const after = [   {key: 'b'} ]

对于可复用的node,本次更新一定属于以下两种情况之一:

  • 不移动
  • 移动

如何判断可复用的node是否移动呢?

我们用lastPlacedIndex变量保存遍历到的最后一个可复用node在before中的index

// 遍历到的最后一个可复用node在before中的index let lastPlacedIndex = 0;  

当遍历after时,每轮遍历到的node,一定是当前遍历到的所有node中最靠右的那个。

如果这个node可复用的node,那么nodeBeforelastPlacedIndex存在两种关系:

注:nodeBefore代表该可复用的nodebefore中的对应node
  • nodeBefore.index < lastPlacedIndex

代表更新前该nodelastPlacedIndex对应node左边。

而更新后该node不在lastPlacedIndex对应node左边(因为他是当前遍历到的所有node中最靠右的那个)。

这就代表该node向右移动了,需要标记Placement

  • nodeBefore.index >= lastPlacedIndex

node在原地,不需要移动。

// 遍历到的最后一个可复用node在before中的index let lastPlacedIndex = 0;    for (let i = 0; i < after.length; i++) { const afterNode = after[i]; afterNode.index = i; const beforeNode = beforeMap.get(afterNode.key);  if (beforeNode) {   // 存在可复用node   // 从map中剔除该 可复用node   beforeMap.delete(beforeNode.key);    const oldIndex = beforeNode.index as number;    // 核心判断逻辑   if (oldIndex < lastPlacedIndex) {     // 移动     afterNode.flag = 'Placement';     result.push(afterNode);     continue;   } else {     // 不移动     lastPlacedIndex = oldIndex;   }  } else {   // 不存在可复用node,这是一个新节点   afterNode.flag = 'Placement';   result.push(afterNode); }

遍历后的收尾工作

经过遍历,如果beforeMap中还剩下node,代表这些node没法复用,需要被标记删除。

比如如下情况,遍历完after后,beforeMap中还剩下{key: 'a'}

// 更新前 const before = [   {key: 'a'},   {key: 'b'} ] // 更新后 const after = [   {key: 'b'} ]

这意味着a需要被标记删除。

所以,最后还需要加入标记删除的逻辑:

beforeMap.forEach(node => {   node.flag = 'Deletion';   result.push(node); });

完整代码见在线Demo地址

总结

整个Diff算法的难点在于lastPlacedIndex相关逻辑。

跟着Demo多调试几遍,相信你能明白其中原理。

开发者必读:2022年移动应用趋势洞察白皮书

Posted: 13 Apr 2022 06:53 PM PDT

华为开发者联盟与艾瑞咨询联合发布《2022年移动应用趋势洞察白皮书》,本白皮书主要分析移动应用行业发展现状和趋势,并对影音娱乐、通讯社交、电商生活、运动健康、出行导航等细分行业场景进行分析,把握移动应用细分行业发展特色和趋势,为广大开发者的开发和运营决策提供参考。

华为开发者联盟一直致力于全方位联接全球开发者,以领先技术和开放能力赋能开发者创新,以丰富多元的运营推广资源加速开发者商业成功,携手全球开发者共建HMS生态沃土。以下内容摘自白皮书。

查看更多内容请前往华为开发者论坛

No comments:

Post a Comment