游戏电视苹果数码历史美丽
投稿投诉
美丽时装
彩妆资讯
历史明星
乐活安卓
数码常识
驾车健康
苹果问答
网络发型
电视车载
室内电影
游戏科学
音乐整形

我让虚拟DOM的diff算法过程动起来了

  去年写了一篇文章手写一个虚拟DOM库,彻底让你理解diff算法介绍虚拟DOM的patch过程和diff算法过程,当时使用的是双端diff算法,今年看到了Vue3使用的已经是快速diff算法,所以也想写一篇来记录一下,但是肯定已经有人写过了,所以就在想能不能有点不一样的,上次的文章主要是通过画图来一步步展示diff算法的每一种情况和过程,所以就在想能不能改成动画的形式,于是就有了这篇文章。当然目前的实现还是基于双端diff算法的,后续会补充上快速diff算法。
  传送门:双端Diff算法动画演示。
  界面就是这样的,左侧可以输入要比较的新旧VNode列表,然后点击启动按钮就会以动画的形式来展示从头到尾的过程,右侧是水平的三个列表,分别代表的是新旧的VNode列表,以及当前的真实DOM列表,DOM列表初始和旧的VNode列表一致,算法结束后会和新的VNode列表一致。
  需要说明的是这个动画只包含diff算法的过程,不包含patch过程。
  先来回顾一下双端diff算法的函数:constdiff(el,oldChildren,newChildren){指针letoldStartIdx0letoldEndIdxoldChildren。length1letnewStartIdx0letnewEndIdxnewChildren。length1节点letoldStartVNodeoldChildren〔oldStartIdx〕letoldEndVNodeoldChildren〔oldEndIdx〕letnewStartVNodenewChildren〔newStartIdx〕letnewEndVNodenewChildren〔newEndIdx〕while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){if(oldStartVNodenull){oldStartVNodeoldChildren〔oldStartIdx〕}elseif(oldEndVNodenull){oldEndVNodeoldChildren〔oldEndIdx〕}elseif(newStartVNodenull){newStartVNodeoldChildren〔newStartIdx〕}elseif(newEndVNodenull){newEndVNodeoldChildren〔newEndIdx〕}elseif(isSameNode(oldStartVNode,newStartVNode)){头头patchVNode(oldStartVNode,newStartVNode)更新指针oldStartVNodeoldChildren〔oldStartIdx〕newStartVNodenewChildren〔newStartIdx〕}elseif(isSameNode(oldStartVNode,newEndVNode)){头尾patchVNode(oldStartVNode,newEndVNode)把oldStartVNode节点移动到最后el。insertBefore(oldStartVNode。el,oldEndVNode。el。nextSibling)更新指针oldStartVNodeoldChildren〔oldStartIdx〕newEndVNodenewChildren〔newEndIdx〕}elseif(isSameNode(oldEndVNode,newStartVNode)){尾头patchVNode(oldEndVNode,newStartVNode)把oldEndVNode节点移动到oldStartVNode前el。insertBefore(oldEndVNode。el,oldStartVNode。el)更新指针oldEndVNodeoldChildren〔oldEndIdx〕newStartVNodenewChildren〔newStartIdx〕}elseif(isSameNode(oldEndVNode,newEndVNode)){尾尾patchVNode(oldEndVNode,newEndVNode)更新指针oldEndVNodeoldChildren〔oldEndIdx〕newEndVNodenewChildren〔newEndIdx〕}else{letfindIndexfindSameNode(oldChildren,newStartVNode)newStartVNode在旧列表里不存在,那么是新节点,创建插入if(findIndex1){el。insertBefore(createEl(newStartVNode),oldStartVNode。el)}else{在旧列表里存在,那么进行patch,并且移动到oldStartVNode前letoldVNodeoldChildren〔findIndex〕patchVNode(oldVNode,newStartVNode)el。insertBefore(oldVNode。el,oldStartVNode。el)将该VNode置为空oldChildren〔findIndex〕null}newStartVNodenewChildren〔newStartIdx〕}}旧列表里存在新列表里没有的节点,需要删除if(oldStartIdxoldEndIdx){for(letioldStartIdx;ioldEndIdx;i){removeEvent(oldChildren〔i〕)oldChildren〔i〕el。removeChild(oldChildren〔i〕。el)}}elseif(newStartIdxnewEndIdx){letbeforenewChildren〔newEndIdx1〕?newChildren〔newEndIdx1〕。el:nullfor(letinewStartIdx;inewEndIdx;i){el。insertBefore(createEl(newChildren〔i〕),before)}}}
  该函数具体的实现步骤可以参考之前的文章,本文就不再赘述。
  我们想让这个diff过程动起来,首先要找到动画的对象都有哪些,从函数的参数开始看,首先oldChildren和newChildren两个VNode列表是必不可少的,可以通过两个水平的列表表示,然后是四个指针,这是双端diff算法的关键,我们通过四个箭头来表示,指向当前所比较的节点,然后就开启循环了,循环中新旧VNode列表其实基本上是没啥变化的,我们实际操作的是VNode对应的真实DOM元素,包括patch打补丁、移动、删除、新增等等操作,所以我们再来个水平的列表表示当前的真实DOM列表,最开始肯定是和旧的VNode列表是对应的,通过diff算法一步步会变成和新的VNode列表对应。
  再来回顾一下创建VNode对象的h函数:exportconsth(tag,data{},children){lettextletelletkey文本节点if(typeofchildrenstringtypeofchildrennumber){textchildrenchildrenundefined}elseif(!Array。isArray(children)){childrenundefined}if(datadata。key){keydata。key}return{tag,元素标签children,子元素text,文本节点的文本el,真实domkey,data}}
  我们输入的VNode列表数据会使用h函数来创建成VNode对象,所以可以输入的最简单的结构如下:〔{tag:p,children:文本节点的内容,data:{key:a}}〕
  输入的新旧VNode列表数据会保存在store中,可以通过如下方式获取到:输入的旧VNode列表store。oldVNode输入的新VNode列表store。newVNode
  接下来定义相关的变量:指针列表constoldPointerListref(〔〕)constnewPointerListref(〔〕)真实DOM节点列表constactNodeListref(〔〕)新旧节点列表constoldVNodeListref(〔〕)constnewVNodeListref(〔〕)提示信息constinforef()
  指针的移动动画可以使用css的transition属性来实现,只要修改指针元素的left值即可,真实DOM列表的移动动画可以使用Vue的列表过渡组件TransitionGroup来轻松实现,模板如下:!指针{{item。name}}{{item。value}}imgsrca2020imgdataimg。jpgdatasrcimg02。bs178。combkahe617e762922f97f8。jpgalt!旧节点列表0旧的VNode列表TransitionGroupnamelist{{item?item。children:空}}TransitionGroup!新节点列表0新的VNode列表TransitionGroupnamelist{{item。children}}TransitionGroup!提示信息{{info}}!指针imgsrca2020imgdataimg。jpgdatasrcimg02。bs178。combkahaaea1f3ea833cd01。jpgalt{{item。value}}{{item。name}}!真实DOM列表0真实DOM列表TransitionGroupnamelist{{item。children}}TransitionGroup
  双端diff算法过程中是不会修改新的VNode列表的,但是旧的VNode列表是有可能被修改的,也就是当首尾比较没有找到可以复用的节点,但是通过直接在旧的VNode列表中搜索找到了,那么会移动该VNode对应的真实DOM,移动后该VNode其实就相当于已经被处理过了,但是该VNode的位置又是在当前指针的中间,不能直接被删除,所以只好置为空null,所以可以看到模板中有处理这种情况。
  另外我们还创建了一个info元素用来展示提示的文字信息,作为动画的描述。
  但是这样还是不够的,因为每个旧的VNode是有对应的真实DOM元素的,但是我们输入的只是一个普通的json数据,所以模板还需要新增一个列表,作为旧的VNode列表的关联节点,这个列表只要提供节点引用即可,不需要可见,所以把它的display设为none:根据输入的旧VNode列表创建元素constoldVNodeListcomputed((){returnJSON。parse(store。oldVNode)})引用DOM元素constoldNoderef(null)constoldNodeListref(〔〕)!隐藏{{item。children}}
  然后当我们点击启动按钮,就可以给我们的三个列表变量赋值了,并使用h函数创建新旧VNode对象,然后传递给打补丁的patch函数就可以开始进行比较更新实际的DOM元素了:conststart(){nextTick((){表示当前真实的DOM列表actNodeList。valueJSON。parse(store。oldVNode)表示旧的VNode列表oldVNodeList。valueJSON。parse(store。oldVNode)表示新的VNode列表newVNodeList。valueJSON。parse(store。newVNode)nextTick((){letoldVNodeh(p,{key:1},JSON。parse(store。oldVNode)。map((item,index){创建VNode对象letvnodeh(item。tag,item。data,item。children)关联真实的DOM元素vnode。eloldNodeList。value〔index〕returnvnode}))列表的父节点也需要关联真实的DOM元素oldVNode。eloldNode。valueletnewVNodeh(p,{key:1},JSON。parse(store。newVNode)。map(item{returnh(item。tag,item。data,item。children)}))调用patch函数进行打补丁patch(oldVNode,newVNode)})})}
  可以看到我们输入的新旧VNode列表是作为一个节点的子节点的,这是因为只有当比较的两个节点都存在非文本节点的子节点时才需要使用diff算法来高效的更新他们的子节点,当patch函数运行完后你可以打开控制台查看隐藏的DOM列表,会发现是和新的VNode列表保持一致的,那么你可能要问,为什么不直接用这个列表来作为真实DOM列表呢,还要自己额外创建一个actNodeList列表,其实是可以,但是diff算法过程中是使用insertBefore等方法来移动真实DOM节点的,所以不好加过渡动画,只会看到节点瞬间换位置,不符合我们的动画需求。
  到这里效果如下:
  接下来我们先把指针搞出来,我们创建一个处理函数对象,这个对象上会挂载一些方法,用于在diff算法过程中调用,在函数中更新相应的变量。consthandles{更新指针updatePointers(oldStartIdx,oldEndIdx,newStartIdx,newEndIdx){oldPointerList。value〔{name:oldStartIdx,value:oldStartIdx},{name:oldEndIdx,value:oldEndIdx}〕newPointerList。value〔{name:newStartIdx,value:newStartIdx},{name:newEndIdx,value:newEndIdx}〕},}
  然后我们就可以在diff函数中通过handles。updatePointers()更新指针了:constdiff(el,oldChildren,newChildren){指针。。。handles。updatePointers(oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)。。。}
  这样指针就出来了:
  然后在while循环中会不断改变这四个指针,所以在循环中也需要更新:while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){。。。handles。updatePointers(oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)}
  但是这样显然是不行的,为啥呢,因为循环也就一瞬间就结束了,而我们希望每次都能停留一段时间,很简单,我们写个等待函数:constwaitt{returnnewPromise(resolve{setTimeout((){resolve()},t3000)})}
  然后我们使用asyncawait语法,就可以轻松在循环中实现等待了:constdiffasync(el,oldChildren,newChildren){。。。while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){。。。handles。updatePointers(oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)awaitwait()}}
  接下来我们新增两个变量,来突出表示当前正在比较的两个VNode:当前比较中的节点索引constcurrentCompareOldNodeIndexref(1)constcurrentCompareNewNodeIndexref(1)consthandles{更新当前比较节点updateCompareNodes(a,b){currentCompareOldNodeIndex。valueacurrentCompareNewNodeIndex。valueb}}{{item?item。children:空}}{{item。children}}
  给当前比较中的节点添加一个类名,用来突出显示,接下来还是一样,需要在diff函数中调用该函数,但是,该怎么加呢:while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){if。。。}elseif(isSameNode(oldStartVNode,newStartVNode)){。。。oldStartVNodeoldChildren〔oldStartIdx〕newStartVNodenewChildren〔newStartIdx〕}elseif(isSameNode(oldStartVNode,newEndVNode)){。。。oldStartVNodeoldChildren〔oldStartIdx〕newEndVNodenewChildren〔newEndIdx〕}elseif(isSameNode(oldEndVNode,newStartVNode)){。。。oldEndVNodeoldChildren〔oldEndIdx〕newStartVNodenewChildren〔newStartIdx〕}elseif(isSameNode(oldEndVNode,newEndVNode)){。。。oldEndVNodeoldChildren〔oldEndIdx〕newEndVNodenewChildren〔newEndIdx〕}else{。。。newStartVNodenewChildren〔newStartIdx〕}
  我们想表现出头尾比较的过程,其实就在这些if条件中,也就是要在每个if条件中停留一段时间,那么可以直接这样吗:constisSameNodeasync(){。。。handles。updateCompareNodes()awaitwait()}if(awaitisSameNode(oldStartVNode,newStartVNode))
  很遗憾,我尝试了不行,那么只能改写成其他形式了:while(oldStartIdxoldEndIdxnewStartIdxnewEndIdx){letstopfalseletisSameNodefalseif(oldStartVNodenull){callbacks。updateInfo()oldStartVNodeoldChildren〔oldStartIdx〕stoptrue}。。。if(!stop){callbacks。updateInfo(头头比较)callbacks。updateCompareNodes(oldStartIdx,newStartIdx)isSameNodeisSameNode(oldStartVNode,newStartVNode)if(isSameNode){callbacks。updateInfo(key值相同,可以复用,进行patch打补丁操作。新旧节点位置相同,不需要移动对应的真实DOM节点)}awaitwait()}if(!stopisSameNode){。。。oldStartVNodeoldChildren〔oldStartIdx〕newStartVNodenewChildren〔newStartIdx〕stoptrue}。。。}
  我们使用一个变量来表示是否进入到了某个分支,然后把检查节点是否能复用的结果也保存到一个变量上,这样就可以通过不断检查这两个变量的值来判断是否需要进入到后续的比较分支中,这样比较的逻辑就不在if条件中了,就可以使用await了,同时我们还使用updateInfo增加了提示语:consthandles{更新提示信息updateInfo(tip){info。valuetip}}
  接下来看一下节点的移动操作,当头(oldStartIdx对应的oldStartVNode节点)尾(newEndIdx对应的newEndVNode节点)比较发现可以复用时,在打完补丁后需要将oldStartVNode对应的真实DOM元素移动到oldEndVNode对应的真实DOM元素的位置,也就是插入到oldEndVNode对应的真实DOM的后面一个节点的前面:if(!stopisSameNode){头尾patchVNode(oldStartVNode,newEndVNode)把oldStartVNode节点移动到最后el。insertBefore(oldStartVNode。el,oldEndVNode。el。nextSibling)更新指针oldStartVNodeoldChildren〔oldStartIdx〕newEndVNodenewChildren〔newEndIdx〕stoptrue}
  那么我们可以在insertBefore方法移动完真实的DOM元素后紧接着调用一下我们模拟列表的移动节点的方法:if(!stopisSameNode){。。。el。insertBefore(oldStartVNode。el,oldEndVNode。el。nextSibling)callbacks。moveNode(oldStartIdx,oldEndIdx1)。。。}
  我们要操作的实际上是代表真实DOM节点的actNodeList列表,那么关键是要找到具体是哪个,首先头尾的四个节点指针它们表示的是在新旧VNode列表中的位置,所以我们可以根据oldStartIdx和oldEndIdx获取到oldVNodeList中对应位置的VNode,然后通过key值在actNodeList列表中找到对应的节点,进行移动、删除、插入等操作:consthandles{移动节点moveNode(oldIndex,newIndex){letoldVNodeoldVNodeList。value〔oldIndex〕letnewVNodeoldVNodeList。value〔newIndex〕letfromIndexfindIndex(oldVNode)lettoIndexfindIndex(newVNode)actNodeList。value〔fromIndex〕actNodeList。value。splice(toIndex,0,oldVNode)actNodeList。valueactNodeList。value。filter(item{returnitem!})}}constfindIndex(vnode){return!vnode?1:actNodeList。value。findIndex(item{returnitemitem。data。keyvnode。data。key})}
  其他的插入节点和删除节点也是类似的:
  插入节点:consthandles{插入节点insertNode(newVNode,index,inNewVNode){letnode{data:newVNode。data,children:newVNode。text}lettargetIndex0if(index1){actNodeList。value。push(node)}else{if(inNewVNode){letvNodenewVNodeList。value〔index〕targetIndexfindIndex(vNode)}else{letvNodeoldVNodeList。value〔index〕targetIndexfindIndex(vNode)}actNodeList。value。splice(targetIndex,0,node)}}}
  删除节点:consthandles{删除节点removeChild(index){letvNodeoldVNodeList。value〔index〕lettargetIndexfindIndex(vNode)actNodeList。value。splice(targetIndex,1)}}
  这些方法在diff函数中的执行位置其实就是执行insertBefore、removeChild方法的地方,具体可以本文源码,这里就不在具体介绍了。
  另外还可以凸显一下已经结束比较的元素、即将被添加的元素、即将被删除的元素等等,最终效果:
  时间原因,目前只实现了双端diff算法的效果,后续会增加上快速diff算法的动画过程,有兴趣的可以点个关注哟
  仓库:https:github。comwanglin2VNodevisualization。

益母草颗粒能催经吗益母草颗粒什么时候喝相信女性朋友对益母草颗粒应该都不会陌生,是治疗妇科疾病的良药,可以有效地缓解痛经、闭经等症状。那么益母草颗粒具有催经效果吗,我们一起来看看。益母草颗粒能催经吗益母草颗粒的……女性免疫力低下怎么调理?这些方法搭配汤臣倍健女士多维各种繁琐小事,长期下去,容易出现免疫力低下的问题,这会反过来对工作和生活都造成巨大的影响,造成恶性循环。因此,一旦发现自己免疫功能不正常,一定要及早重视。那么,女性免疫力低下怎……氮泵怎么喝效果最好氮泵和肌酸能一起吃吗相信很多健身的朋友对氮泵这个名字并不陌生,很多健身为了突破瓶颈期就会服用一些氮泵来帮助自己。那么氮泵怎么喝效果最好呢,这些常识你get到了吗。氮泵怎么喝效果最好1、运动前……对电动汽车说不!印度汽车巨头将联手丰田押宝氢能混动车特斯拉的出现,对全球汽车能源格局的影响不可谓不大,纯电动汽车的市场份额正以肉眼可见的速度在增长,再加上全球近几年的疫情影响以及俄乌冲突,进一步加剧了新能源汽车的发展。然而……吃党参有什么好处?如何吃?不妨来看看无限能党参破壁片党参是我国传统名贵中药材,最早见于《本草从新》,并指出党参的特征是狮子盘头。党参也是一种重要的常用中药,那么吃党参有什么好处呢?如何吃呢?下面来看看吧!在《中华人民共和国……早报卡塔尔航空成为巴黎圣日耳曼主赞助商AC米兰与彪马续约伊利成为葡萄牙国家队中国区赞助商;拜仁慕尼黑与科乐美续约;意甲将采用同分球队用附加赛争冠的新规禹唐DAILY为你带来每日国内外体育产业要闻国内产业资讯速递……象棋中局疑难问题解答弃子多兵未挽劣势如图,轮到红方行棋。此时黑卒渡河,红如相五进七吃卒,演示下去,红多子优。所以红只能直走车四平五,则卒进,马七退六,黑反先。临场红不顾失先,走出吃卒弃马的棋。问:红方弃马有……氨糖可以长期服用吗氨糖怎么吃最好氨糖控制着关节软骨滑膜的代谢平衡,还控制着人体骨关节的健康。着年龄的增长,人体内的氨糖合成会减缓,进而影响关节内软骨细胞的新陈代谢,所以老年朋友服用氨糖会比较多。氨糖可以长期服……氨糖可以治疗滑膜炎吗滑膜炎吃哪种氨糖最好氨糖全名是氨基葡萄糖,是人体内天然存在的一种单糖,主要分布在软骨和结缔组织中,缺乏氨糖会导致各种骨关节疾病的发生,必要时需要服用。氨糖可以治疗滑膜炎吗氨糖可辅助治疗滑膜炎……最能对抗美国田径队的这支队伍已聚集最强力量八位世界冠军到场谁是田径世锦赛上,美国队的最强挑战者?不是很多人想象的牙买加队,而是肯尼亚队。他们在世锦赛奖牌榜上总排名第二。只说最近几届,17年和19年世锦赛,肯尼亚队奖牌榜排名第二。……牛磺酸能兴奋中枢神经吗牛磺酸有抗疲劳作用吗牛磺酸,是从牛黄中分离的一种物质。牛磺酸以多种形式存在,它可能存在于饮料中,也可能存在于药品中,那么牛磺酸到底是什么东西,它可以兴奋中枢神经吗。牛磺酸能兴奋中枢神经吗不能……御夫子膏有什么作用御夫子膏的用法很多人在生活中喜欢使用膏药产品,涂抹在身上既方便又快捷。有一款药膏御夫子相信大家也听说过,那么御夫子到底是什么,它有什么作用,让我们一起来了解一下。御夫子膏有什么作用它的……
再严重的皮肤病,也害怕这四种中成药皮炎湿疹荨麻疹皆可用再严重的皮肤病,也害怕这4个中成药,接诊中很多患者胳膊上、腿上,或后背上有一块皮肤老是痒,抹了很多种药,刚涂上还可以,一会儿就不管用了,该痒还是痒,抹什么药都是这样,还定期发作……曝网红痞幼曾与张继科交往!两人的私密照被曝光,现男友怒提分手文娱来君近日,关于曾经的兵乒球世界冠军张继科私生活混乱一事,不断在网上发酵,随着事件越演越烈,有知情人透露张继科本人已被带走调查,可关于他的光荣事迹依旧没有停止。这……平江县志愿红扮靓油菜花海平江县油菜花海。岳阳日报全媒体讯(记者陈丽虹通讯员朱炳文喻西子)正值雷锋月,近日,平江县第六届油菜花节在平江县木金乡举行,来自各行各业140名志愿者积极响应号召,在花海中……利弊思维让你的选择更清晰有一天早上,我送女儿坐校车。像往常一样,第三趟校车慢悠悠地驶过来,停到路边。小朋友们很自觉地排成一队,有条不紊地上车。我女儿排在最后,因为她不想跟别人挤,总是最后才上车。眼看着……馆员荐书少儿新书推荐这个四月,让我们一起来阅读吧让我们跟着文字,去那遥远的时空听一听古人感悟春花秋月的烂漫看一看大家对于未来的瑰丽想象没有约束没有羁绊,只有信马由缰0……五粮液曾从钦找定位找方向找基点,打造中国式现代化的中国酒业样近日,由中国酒业协会主办,以长周期、新作为、共美好为主题的第十二届中国白酒T9峰会在安徽举行,五粮液、茅台、泸州老窖、洋河、汾酒、古井贡酒等9家白酒行业龙头企业齐聚,共话新形势……清研智谈智慧政务赋能,让政务服务和城市治理更加精准高效智慧政务是指通过运用大数据、区块链、云计算、人工智能等新一代信息技术,高效整合各政府部门的信息、资源,从而有效提升政务服务效率、政府监管水平的一种手段,是数字政府建设的重要途径……中国人来重庆旅游必吃的几道菜,外国人却都受不了,而你吃过哪个春暖花开天气转暖,一些有钱有闲的人也开始自己的旅行计划了,在小编印象里一直有东方赛博朋克之都的重庆一直是许多人旅行常去的,既然来了总要尝尝当地的特色美食,提起重庆美食一般人想到……自制老味糕点蜜三刀,家里有面粉就能做,甜蜜酥香,名不虚传自制老味糕点蜜三刀,家里有面粉就能做,甜蜜酥香,名不虚传。这是江苏徐州地区的一道特色美食小吃,传说是北宋那时候就有了,到了清朝,乾隆皇帝吃了后觉得非常好吃,就钦定为宫廷贡品,然……望海楼3月12日植树节,一个读到泪目、读到郁郁葱葱的故事分享给你。是的,这里的望海楼,没有海。这里的望海楼,是用来望林海、观火情的小楼房,散落在河北塞罕……浅谈四川棋队掌门人蒋全胜平凡又极富传奇色彩的象棋人生只那么弹指一挥间,1960年出生的蒋全胜已经62岁了,在2020年,刚满60岁的蒋全胜辞去了四川棋院院长的职务,由危建华接任。从2012年担任四川棋院掌门人以来,蒋全胜的各项工……三月初屏幕显示效果最好的十大手机!苹果iPhone14Pro相信大家在挑选手机时肯定很好奇自己的手机屏幕显示效果怎么样,排名如何?现在,hrart找到了DxOMark最新数据供大家参考。DXOMARK,是一个评估智能手机的商业评测……
友情链接:易事利快生活快传网聚热点七猫云快好知快百科中准网快好找文好找中准网快软网