热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

重新完成一个简易版React(三)

写在开首从新完成一个简易版React(二)地点:https:segmentfault.coma11…在上一节,我们的react已具有了衬着功用。在这一节我们将偏重完成它的更新,说到

写在开首

从新完成一个简易版React(二)地点:https://segmentfault.com/a/11…
在上一节,我们的react已具有了衬着功用。
在这一节我们将偏重完成它的更新,说到更新,人人能够都邑想到React的diff算法,它能够说是React机能高效的保证,同时也是最神奇,最难明白的部份(个人以为),想当初我也是看了许多文章,敲了N次代码,调试了几十遍,才总算明白了它的也许。在这也算是把我的明白论述出来。

进入正题

一样,我们会完成三种ReactComponent的update要领。不过在这之前,我们先想一想,该怎样触发React的更新呢?没错,就是setState要领。

// 一切自定义组件的父类
class Component {
constructor(props) {
this.props = props
}
setState(newState) {
this._reactInternalInstance.updateComponent(null, newState)
}
}
//代码地点:src/react/Component.js

这里的reactInternalInstance就是我们在衬着ReactCompositeComponent时保存下的本身的实例,经由过程它挪用了ReactCompositeComponent的update要领,接下来,我们就先完成这个update要领。

ReactCompositeComponent

这里的update要领同mount有点相似,都是挪用生命周期和render要领,先上代码:

class ReactCompositeComponent extends ReactComponent {
constructor(element) {
super(element)
// 寄存对应的组件实例
this._instance = null
this._renderedCompOnent= null
}
mountComponent(rootId) {
//内容略
}
// 更新
updateComponent(nextVDom, newState) {
// 假如有新的vDom,就运用新的
this._vDom = nextVDom || this._vDom
const inst = this._instance
// 猎取新的state,props
const nextState = { ...inst.state, ...newState }
const nextProps = this._vDom.props
// 推断shouldComponentUpdate
if (inst.shouldComponentUpdate && (inst.shouldComponentUpdate(nextProps, nextState) === false)) return
inst.componentWillUpdate && inst.componentWillUpdate(nextProps, nextState)
// 变动state,props
inst.state = nextState
inst.props = nextProps
const prevCompOnent= this._renderedComponent
// 猎取render新旧的vDom
const prevRenderVDom = prevComponent._vDom
const nextRenderVDom = inst.render()
// 推断是须要更新照样从新衬着
if (shouldUpdateReactComponent(prevRenderVDom, nextRenderVDom)) {
// 更新
prevComponent.updateComponent(nextRenderVDom)
inst.componentDidUpdate && inst.componentDidUpdate()
} else {
// 从新衬着
this._renderedCompOnent= instantiateReactComponent(nextRenderVDom)
// 从新天生对应的元素内容
const nextMarkUp = this._renderedComponent.mountComponent(this._rootNodeId)
// 替代悉数节点
$(`[data-reactid="${this._rootNodeId}"]`).replaceWith(nextMarkUp)
}
}
}
//代码地点:src/react/component/ReactCompositeComponent.js

有两点要申明:

  1. 熟习React的都晓得,许多时刻组件的更新,vDom并没有变化,我们能够经由过程shouldComponentUpdate这个生命周期来优化这点,当shouldComponentUpdate为false时,直接return,不实行下面的代码。
  2. 当挪用render猎取到新的vDom时,将会比较新旧的vDom范例是不是雷同,这也属于diff算法优化的一部份,假如范例雷同,则实行更新,反之,就从新衬着。

// 推断是更新照样衬着
function shouldUpdateReactComponent(prevVDom, nextVDom) {
if (prevVDom != null && nextVDom != null) {
const prevType = typeof prevVDom
const nextType = typeof nextVDom
if (prevType === 'string' || prevType === 'number') {
return nextType === 'string' || nextType === 'number'
} else {
return nextType === 'object' && prevVDom.type === nextVDom.type && prevVDom.key === nextVDom.key
}
}
}
//代码地点:src/react/component/util.js

注重,这里我们运用到了key,当type雷同时运用key能够疾速正确得出两个vDom是不是雷同,这是为何React请求我们在轮回衬着时必需增添key这个props。

ReactTextComponent

ReactTextComponent的update要领异常简朴,推断新旧文本是不是雷同,差别则更新内容,直接贴代码:

class ReactTextComponent extends ReactComponent {
mountComponent(rootId) {
//省略
}
// 更新
updateComponent(nextVDom) {
const nextText = '' + nextVDom
if (nextText !== this._vDom) {
this._vDom = nextText
}
// 替代悉数节点
$(`[data-reactid="${this._rootNodeId}"]`).html(this._vDom)
}
// 代码地点:src/react/component/ReactTextComponent.js
}

ReactDomComponent

ReactDomComponent的update最庞杂,能够说diff的中心都在这里,本文的重心也就放在这。
悉数update分为两块,props的更新和children的更新。

class ReactDomComponent extends ReactComponent {
mountComponent(rootId) {
//省略
}
// 更新
updateComponent(nextVDom) {
const lastProps = this._vDom.props
const nextProps = nextVDom.props
this._vDom = nextVDom
// 更新属性
this._updateDOMProperties(lastProps, nextProps)
// 再更新子节点
this._updateDOMChildren(nextVDom.props.children)
}
// 代码地点:src/react/component/ReactDomComponent.js
}

props的更新异常简朴,不过就是遍历新旧props,删除不在新props里的老props,增添不在老props里的新props,更新新旧都有的props,事宜特别处置惩罚。

_updateDOMProperties(lastProps, nextProps) {
let propKey = ''
// 遍历,删除已不在新属性鸠合里的老属性
for (propKey in lastProps) {
// 属性在原型上或许新属性里有,直接跳过
if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
continue
}
// 关于事宜等特别属性,须要零丁处置惩罚
if (/^on[A-Za-z]/.test(propKey)) {
const eventType = propKey.replace('on', '')
// 针对当前的节点作废事宜代办
$(document).undelegate(`[data-reactid="${this._rootNodeId}"]`, eventType, lastProps[propKey])
continue
}
}
// 关于新的属性,须要写到dom节点上
for (propKey in nextProps) {
// 更新事宜属性
if (/^on[A-Za-z]/.test(propKey)) {
var eventType = propKey.replace('on', '')
// 之前假如已有,须要先去掉
lastProps[propKey] && $(document).undelegate(`[data-reactid="${this._rootNodeId}"]`, eventType, lastProps[propKey])
// 针对当前的节点增添事宜代办
$(document).delegate(`[data-reactid="${this._rootNodeId}"]`, `${eventType}.${this._rootNodeId}`, nextProps[propKey])
continue
}
if (propKey === 'children') continue
// 更新一般属性
$(`[data-reactid="${this._rootNodeId}"]`).prop(propKey, nextProps[propKey])
}
}
// 代码地点:src/react/component/ReactDomComponent.js

children的更新则相对庞杂了许多,陈屹先生的《深切React手艺栈》中提到,diff算法分为3块,分别是

  1. tree diff
  2. component diff
  3. element diff

上文中的shouldUpdateReactComponent就属于component diff,接下来,让我们依据这三种diff完成updateChildren。

// 全局的更新深度标识,用来剖断触发patch的机遇
let updateDepth = 0
// 全局的更新行列
let diffQueue = []
_updateDOMChildren(nextChildVDoms) {
updateDepth++
// diff用来递归查找差别,组装差别对象,并增添到diffQueue中
this._diff(diffQueue, nextChildVDoms)
updateDepth--
if (updateDepth === 0) {
// 详细的dom衬着
this._patch(diffQueue)
diffQueue = []
}

这里经由过程updateDepth对vDom树举行层级掌握,只会对雷同层级的DOM节点举行比较,只有当一棵DOM树悉数遍历完,才会挪用patch处置惩罚差别。也就是所谓的tree diff。
确保了同条理后,我们要完成_diff要领。
已衬着过的子ReactComponents在这里是数组,我们要遍历出内里的vDom举行比较,这里就牵涉到上文中的key,在有key时,我们优先用key来猎取vDom,所以,我们起首遍历数组,将其转为map(这里先用object替代,以后会变动成es6的map),假如有key值的,就用key值作标识,无key的,就用index。
下面是array到map的代码:


// 将children数组转化为map
export function arrayToMap(array) {
array = array || []
const childMap = {}
array.forEach((item, index) => {
const name = item && item._vDom && item._vDom.key ? item._vDom.key : index.toString(36)
childMap[name] = item
})
return childMap
}

部份diff要领:

// 将之前子节点的component数组转化为map
const prevChildCompOnents= arrayToMap(this._renderedChildComponents)
// 天生新的子节点的component对象鸠合
const nextChildCompOnents= generateComponentsMap(prevChildComponents, nextChildVDoms)

将ReactComponent数组转化为map后,用老的ReactComponents鸠合和新vDoms数组天生新的ReactComponents鸠合,这里会运用shouldUpdateReactComponent举行component diff,假如雷同,则直接更新即可,反之,就从新天生ReactComponent

/**
* 用来天生子节点的component
* 假如是更新,就会继承运用之前的component,挪用对应的updateComponent
* 假如是新的节点,就会从新天生一个新的componentInstance
*/
function generateComponentsMap(prevChildComponents, nextChildVDoms = []) {
const nextChildCompOnents= {}
nextChildVDoms.forEach((item, index) => {
const name = item.key ? item.key : index.toString(36)
const prevChildCompOnent= prevChildComponents && prevChildComponents[name]
const prevVdom = prevChildComponent && prevChildComponent._vDom
const nextVdom = item
// 推断是更新照样从新衬着
if (shouldUpdateReactComponent(prevVdom, nextVdom)) {
// 更新的话直接递归挪用子节点的updateComponent
prevChildComponent.updateComponent(nextVdom)
nextChildComponents[name] = prevChildComponent
} else {
// 从新衬着的话从新天生component
const nextChildCompOnent= instantiateReactComponent(nextVdom)
nextChildComponents[name] = nextChildComponent
}
})
return nextChildComponents
}

阅历了以上两步,我们已获得了新旧同层级的ReactComponents鸠合。须要做的,只是遍历这两个鸠合,举行比较,同属性的更新一样,举行挪动,新增,和删除,固然,在这个过程当中,我会包括我们的第三种优化,element diff。它的战略是如许的:起首对新鸠合的节点举行轮回遍历,经由过程唯一标识能够推断新老鸠合中是不是存在雷同的节点,假如存在雷同节点,则举行挪动操纵,但在挪动前须要将当前节点在老鸠合中的位置与 lastIndex 举行比较,if (prevChildComponent._mountIndex 上完全的diff要领代码:

// 差别更新的几种范例
const UPDATE_TYPES = {
MOVE_EXISTING: 1,
REMOVE_NODE: 2,
INSERT_MARKUP: 3
}
// 追踪差别
_diff(diffQueue, nextChildVDoms) {
// 将之前子节点的component数组转化为map
const prevChildCompOnents= arrayToMap(this._renderedChildComponents)
// 天生新的子节点的component对象鸠合
const nextChildCompOnents= generateComponentsMap(prevChildComponents, nextChildVDoms)
// 从新复制_renderChildComponents
this._renderedChildCompOnents= []
for (let name in nextChildComponents) {
nextChildComponents.hasOwnProperty(name) && this._renderedChildComponents.push(nextChildComponents[name])
}
let lastIndex = 0 // 代表接见的末了一次老的鸠合位置
let nextIndex = 0 // 代表抵达的新的节点的index
// 经由过程对照两个鸠合的差别,将差别节点增添到行列中
for (let name in nextChildComponents) {
if (!nextChildComponents.hasOwnProperty(name)) continue
const prevChildCompOnent= prevChildComponents && prevChildComponents[name]
const nextChildCompOnent= nextChildComponents[name]
// 雷同的话,申明是运用的同一个component,须要挪动
if (prevChildCompOnent=== nextChildComponent) {
// 增添差别对象,范例:MOVE_EXISTING
prevChildComponent._mountIndex parentId: this._rootNodeId,
parentNode: $(`[data-reactid="${this._rootNodeId}"]`),
type: UPDATE_TYPES.MOVE_EXISTING,
fromIndex: prevChildComponent._mountIndex,
toIndex: nextIndex
})
lastIndex = Math.max(prevChildComponent._mountIndex, lastIndex)
} else {
// 假如不雷同,申明是新增的节点
// 假如老的component在,须要把老的component删除
if (prevChildComponent) {
diffQueue.push({
parentId: this._rootNodeId,
parentNode: $(`[data-reactid="${this._rootNodeId}"]`),
type: UPDATE_TYPES.REMOVE_NODE,
fromIndex: prevChildComponent._mountIndex,
toIndex: null
})
// 去掉事宜监听
if (prevChildComponent._rootNodeId) {
$(document).undelegate(`.${prevChildComponent._rootNodeId}`)
}
lastIndex = Math.max(prevChildComponent._mountIndex, lastIndex)
}
// 新增添的节点
diffQueue.push({
parentId: this._rootNodeId,
parentNode: $(`[data-reactid="${this._rootNodeId}"]`),
type: UPDATE_TYPES.INSERT_MARKUP,
fromIndex: null,
toIndex: nextIndex,
markup: nextChildComponent.mountComponent(`${this._rootNodeId}.${name}`)
})
}
// 更新_mountIndex
nextChildComponent._mountIndex = nextIndex
nextIndex++
}
// 关于老的节点里有,新的节点里没有的,悉数删除
for (let name in prevChildComponents) {
const prevChildCompOnent= prevChildComponents[name]
if (prevChildComponents.hasOwnProperty(name) && !(nextChildComponents && nextChildComponents.hasOwnProperty(name))) {
diffQueue.push({
parentId: this._rootNodeId,
parentNode: $(`[data-reactid="${this._rootNodeId}"]`),
type: UPDATE_TYPES.REMOVE_NODE,
fromIndex: prevChildComponent._mountIndex,
toIndex: null
})
// 假如衬着过,去掉事宜监听
if (prevChildComponent._rootNodeId) {
$(document).undelegate(`.${prevChildComponent._rootNodeId}`)
}
}
}
}
// 代码地点:src/react/component/ReactDomCompoent.js

挪用diff要领后,会回到tree diff那一步,当一整棵树遍历完后,就须要经由过程Patch将更新的内容衬着出来了,patch要领相对照较简朴,因为我们把更新的内容都放入了diffQueue中,只需遍历这个数组,依据差别的范例举行响应的操纵就行。

// 衬着
_patch(updates) {
// 处置惩罚挪动和删除的
updates.forEach(({ type, fromIndex, toIndex, parentNode, parentId, markup }) => {
const updatedChild = $(parentNode.children().get(fromIndex))
switch (type) {
case UPDATE_TYPES.INSERT_MARKUP:
insertChildAt(parentNode, $(markup), toIndex) // 插进去
break
case UPDATE_TYPES.MOVE_EXISTING:
deleteChild(updatedChild) // 删除
insertChildAt(parentNode, updatedChild, toIndex)
break
case UPDATE_TYPES.REMOVE_NODE:
deleteChild(updatedChild)
break
default:
break
}
})
}
// 代码地点:src/react/component/ReactDomComponent.js

总结

以上,悉数简易版React就完成了,能够试着写些简朴的例子跑跑看了,是不是是异常有成就感呢?

总结下更新:
ReactCompositeComponent:担任挪用生命周期,经由过程component diff将更新都交给了子ReactComponet
ReactTextComponent:直接更新内容
ReactDomComponent:先更新props,在更新children,更新children分为三步,tree diff保证同层级比较,运用shouldUpdateReactComponent举行component diff,末了在element diff经由过程lastIndex递次优化

至此,悉数从新完成简易版React就完毕了,谢谢人人的寓目。

参考资料,谢谢几位先辈的分享:
https://www.cnblogs.com/sven3…
https://github.com/purplebamb…
陈屹 《深切React手艺栈》


推荐阅读
  • 重要知识点有:函数参数默许值、盈余参数、扩大运算符、new.target属性、块级函数、箭头函数以及尾挪用优化《深切明白ES6》笔记目次函数的默许参数在ES5中,我们给函数传参数, ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 本文详细介绍了一种利用 ESP8266 01S 模块构建 Web 服务器的成功实践方案。通过具体的代码示例和详细的步骤说明,帮助读者快速掌握该模块的使用方法。在疫情期间,作者重新审视并研究了这一未被充分利用的模块,最终成功实现了 Web 服务器的功能。本文不仅提供了完整的代码实现,还涵盖了调试过程中遇到的常见问题及其解决方法,为初学者提供了宝贵的参考。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 题目《BZOJ2654: Tree》的时间限制为30秒,内存限制为512MB。该问题通过结合二分查找和Kruskal算法,提供了一种高效的优化解决方案。具体而言,利用二分查找缩小解的范围,再通过Kruskal算法构建最小生成树,从而在复杂度上实现了显著的优化。此方法不仅提高了算法的效率,还确保了在大规模数据集上的稳定性能。 ... [详细]
  • 在 Vue 应用开发中,页面状态管理和跨页面数据传递是常见需求。本文将详细介绍 Vue Router 提供的两种有效方式,帮助开发者高效地实现页面间的数据交互与状态同步,同时分享一些最佳实践和注意事项。 ... [详细]
  • 在最近的学习过程中,我对Vue.js中的Prop属性有了更深入的理解,并认为这一知识点至关重要,因此在此记录一些心得体会。Prop属性用于在组件之间传递数据。由于每个组件实例的作用域都是独立的,无法直接引用父组件的数据。通过使用Prop,可以将数据从父组件安全地传递到子组件,确保数据的隔离性和可维护性。 ... [详细]
  • Python 数据可视化实战指南
    本文详细介绍如何使用 Python 进行数据可视化,涵盖从环境搭建到具体实例的全过程。 ... [详细]
  • 结城浩(1963年7月出生),日本资深程序员和技术作家,居住在东京武藏野市。他开发了著名的YukiWiki软件,并在杂志上发表了大量程序入门文章和技术翻译作品。结城浩著有30多本关于编程和数学的书籍,其中许多被翻译成英文和韩文。 ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • 在多线程并发环境中,普通变量的操作往往是线程不安全的。本文通过一个简单的例子,展示了如何使用 AtomicInteger 类及其核心的 CAS 无锁算法来保证线程安全。 ... [详细]
  • 第二十五天接口、多态
    1.java是面向对象的语言。设计模式:接口接口类是从java里衍生出来的,不是python原生支持的主要用于继承里多继承抽象类是python原生支持的主要用于继承里的单继承但是接 ... [详细]
  • 本文将详细介绍如何在Mac上安装Jupyter Notebook,并提供一些常见的问题解决方法。通过这些步骤,您将能够顺利地在Mac上运行Jupyter Notebook。 ... [详细]
  • H凹变换优化技术——lhMorphConcave详解与应用摘要:本文详细介绍了lhMorphConcave技术,该技术通过优化H凹变换来提高图像处理的精度。具体而言,该函数在5×5的正方形区域内对输入图像进行二值化处理,以实现更精确的形态学分析。参数设置方面,sr参数用于控制变换的具体细节,从而确保在不同应用场景中都能获得理想的效果。此外,文章还探讨了该技术在实际项目中的应用案例,展示了其在图像分割、特征提取等领域的强大潜力。 ... [详细]
  • 在分析和解决 Keepalived VIP 漂移故障的过程中,我们发现主备节点配置如下:主节点 IP 为 172.16.30.31,备份节点 IP 为 172.16.30.32,虚拟 IP 为 172.16.30.10。故障表现为监控系统显示 Keepalived 主节点状态异常,导致 VIP 漂移到备份节点。通过详细检查配置文件和日志,我们发现主节点上的 Keepalived 进程未能正常运行,最终通过优化配置和重启服务解决了该问题。此外,我们还增加了健康检查机制,以提高系统的稳定性和可靠性。 ... [详细]
author-avatar
xiaol
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有