简单版react原理分析(虚拟dom,节点渲染,数据更新,diff算法等)

0.前言

本文将结合简化版本的react代码,对react的核心工作流程和原理进行梳理和讲解,本文重点会顺着react的执行流程机制进行原理性的描述,整个简化版的react代码量不多,可以参照代码结合讲解,效果更佳,代码戳这里

1. React

React为一个对象,拥有rendercreateElementcreateClass方法

1.1. React.render

传入虚拟dom及根节点进行挂载渲染
参数:
React.render(element, container)

  • elementReact.createElementReact.createClass返回的元素对象,即虚拟dom(见第1.2.节解析)
  • container是html内容挂载的节点

方法体:

var componentInstance = instantiateReactComponent(element)
var markup = componentInstance.mountComponent(React.nextReactRootIndex++)
  • instantiateReactComponent(element)
    instantiateReactComponent是普通方法,通过判断element虚拟dom的type属性,返回不同虚拟dom类型对应的组件类实例(见第2.节解析),这些组件类实例除了保存虚拟dom对象之外,还拥有组装节点、更新节点等原型方法
  • 调用componentInstance.mountComponent实例方法返回组装的html节点,并加以挂载

1.2. React.createElement

传入元素类型,属性和子节点
参数:
createElement(type, config, children)

  • type为传入对应的不同元素,可以为html默认元素(如’div’)或者自定义元素(class);文本节点也是其中一种type,但由于其没有children和key等config,故不用使用createElement方法生成虚拟dom而直接使用render方法传入文本渲染
  • config为传入元素的属性,包含key,事件或自定义等属性
  • children为传入元素的子节点,可以为createElement生成单个元素或多个元素数组

方法体:

  • 处理config,将key单独提取出来,其他属性保存为props
  • 处理children,将children保存到props.children属性

返回:
new ReactElement(type, key, props)

  • 参数在上一步已处理好,ReactElement虚拟dom类,保存了type, key, props3个实例属性

1.3. React.createClass

返回自定义元素类(CustomClass),该类继承了超类的原型方法,并将自定义原型方法合并入该类原型链
参数:
createClass(classConfig)

  • 自定义元素的配置项,可以包含初始化state,时间及render等方法

方法体:

  • 定义空的自定义元素类CustomClass
  • 继承超类的原型方法,CustomClass.prototype = new ReactClass()ReactClassCustomClass的超类,见下文解释
  • 将自定义原型方法合并入该类原型链,Object.assign(CustomClass.prototype, classConfig)

返回:
CustomClass

关联
超类ReactClass

  • 所有自定义组件的超类
  • 原型链方法render,空方法,需要子类自定义
  • 原型链方法setState,调用自定义元素组件类实例的receiveComponent方法,更新自定义元素:
    this._reactInternalInstance.receiveComponent(null, newState)
    this._reactInternalInstance自定义元素组件类的实例,自定义元素组件类是react内部的实现,即ReactCompositeComponent,保存了自定义元素类关联的虚拟dom及一系列dom的操作方法;该属性在ReactCompositeComponent实例的mountComponent方法初始化,见2.3.节
    注意区别自定义元素类自定义元素组件类,前者是我们自己写的自定义元素的那个class,后者是react内部的ReactComponent类,包含自定义元素类实例,具有组装节点和更新等方法

2. ReactComponent

ReactComponent是不同元素/虚拟dom对应的元素组件类,元素组件类保存虚拟dom等数据,以及拥有组装节点,更新等一系列操作dom的方法

2.1. ReactDOMTextComponent

纯文本元素组件类
实例属性

  • this._currentElement = text,保存当前文本
  • this._rootNodeID,根节点id,在调用mountComponent方法的时候传入并保存

原型方法

  • mountComponent(rootID),传入根节点id,组装节点,返回html字符串
  • receiveComponent(nextText),传入新文本节点,与旧节点对比,不同则更新dom节点内容

2.2. ReactDOMComponent

html默认元素组件类
实例属性

  • this._currentElement = element,保存当前虚拟dom节点(见1.2.节)
  • this._rootNodeID,根节点id,在调用mountComponent方法的时候传入并保存
  • this._renderedChildren,保存子节点元素类实例,对应createElement传入的第3个参数和虚拟dom的props.children属性(见1.2.节),用于后续的节点更新

原型方法

  • 1.mountComponent(rootID)
    1.1. 根据保存的虚拟domthis._currentElementtypeprops组装本节点的html
    1.2. 根据this._currentElementprops.children,生成每个children的实例并执行mountComponent方法组装节点(该方法必要时会递归子节点mountComponent方法,见本文末尾流程图紫色线

  • 2.receiveComponent(nextElement)
    传入新节点的虚拟domnextElement
    2.1. this._updateDOMProperties(lastProps, nextProps),处理当前节点的属性变更
    2.2. this._updateDOMChildren(nextElement.props.children),处理当前节点的子节点变更

  • 3._updateDOMProperties(lastProps, nextProps)
    新老属性比对,更新当前节点的属性

  • 4._updateDOMChildren(nextChildrenElements)
    处理子节点的变更
    4.1. this._diff(diffQueue, nextChildrenElements),diff算法,递归找出差别,组装差异对象,添加到全局更新队列diffQueue
    4.2. this._patch(diffQueue),patch算法,在合适的时机调用(一轮更新的递归调用完毕,具体代码使用全局计数器updateDepth来标记,具体可见仓库代码),根据diffQueue执行具体的dom操作

  • 5._diff(diffQueue, nextChildrenElements)

    diff算法,递归找出差别,组装差异对象,添加到全局更新队列diffQueue

    5.1. var prevChildren = flattenChildren(self._renderedChildren)
    使用 flattenChildren方法将_renderedChildren子节点元素组件类实例数组转化为一个映射,映射的键为子节点的key,若不存在则直接只用子节点的index作为键

    5.2. nextChildren = generateComponentChildren(prevChildren, nextChildrenElements)
    传入旧子节点映射及新节点子节点虚拟dom集合数组,返回新子节点元素组件类实例映射
    5.2.1.遍历nextChildrenElements数组,取子节点nextElement虚拟dom的key或者index作为键值key
    5.2.2.用上一步的key取出prevChildren子节点实例对用对应的虚拟domprevElement,使用_shouldUpdateReactComponent(prevElement, nextElement)判断子节点是需要更新还是直接替换(该方法见3.1.节)
    5.2.3.需要更新的话则使用prevChild.receiveComponent(nextElement)更新(注意该方法可能会进行递归更新,见本文末尾流程图的绿色线),nextChildren的值仍然为prevChild ;不需要更新则使用instantiateReactComponent(nextElement)生成一个新子节点元素类的实例,并作为nextChildren的值
    5.2.4.返回nextChildren映射对象

    5.3.diff算法核心
    5.3.1. 维护两个计数变量
    lastIndex:代表访问的旧子节点集合最大index,该变量会随着遍历新子节点对应的旧子节点的最大index而变更
    nextIndex:代表到达的新子节点的index,该变量会随着遍历新子节点而递增

    5.3.2. 遍历nextChildren映射对象,通过key值获取对应的新子节点nextChild,从prevChildren取出旧子节点prevChild

    5.3.3. 对比prevChildnextChild
    若相等,再看看prevChild._mountIndex < lastIndex是否成立,若成立,说明最新的nextChild对应的旧子节点在原来已经遍历过的旧子节点之前,但由于nextChildren是按顺序遍历的,所以新的nextChild应该是要在原来遍历过的旧子节点之后的,,也就是需要移动,所以prevChild._mountIndex < lastIndex成立的话,就把当前需要移动的位置和移动到哪个位置(index)记录下来,操作类型记为UPDATE_TYPES.MOVE_EXISTING,将该记录的对象push进diffQueue;同时更新lastIndex = Math.max(prevChild._mountIndex, lastIndex)
    若不相等,则说明nextChild是新节点,说明需要新插入,使用nextChild.mountComponent方法组装html,并标注插入的位置(index),操作类型记为UPDATE_TYPES.INSERT_MARKUP,将该记录的对象push进diffQueue;同时再根据同名key获取prevChild,如果存在的话说明旧节点已经被新节点取代,需要删除,标注起始位置(index),操作类型记为UPDATE_TYPES.REMOVE_NODE,将该记录的对象push进diffQueue,同时更新lastIndex = Math.max(prevChild._mountIndex, lastIndex)
    每个循环完毕执行nextIndex++

    5.3.4. 5.3.2-5.3.3只是处理了新子节点的新增和移动,还需要处理旧节点的删除。遍历prevChildren映射对象,通过key值获取对应的旧子节点并判断旧子节点是否还在新子节点对象nextChildren之中,若不存在说明需要删除,标注起始位置(index),操作类型记为UPDATE_TYPES.REMOVE_NODE,将该记录的对象push进diffQueue

    diff核心算法(5.3.节)伪代码(若想细致了解diff算法可以戳这里

lastIndex=0
nextIndex=0
do
if  prevChild === nextChild
     prevChild._mountIndex < lastIndex && 
     diffQueue.push(MOVE_EXISTING)
     lastIndex = Math.max(prevChild._
     mountIndex, lastIndex)
else
     if prevChild
         diffQueue.push(REMOVE_
         NODE);
         lastIndex = Math.max(prevChild._
         mountIndex, lastIndex)
     end
     diffQueue.push(INSERT_MARKUP)
end
nextChild._mountIndex = nextIndex
nextIndex++
while nextChild
//
do
    if !nextChildren[prevChildren.name]
        diffQueue.push(REMOVE_NODE)
    end
while preChild
  • 6._patch(diffQueue)
    根据diff算法已经计算出的差异化队列diffQueue,根据每个不同的类型:UPDATE_TYPES.MOVE_EXISTINGUPDATE_TYPES.INSERT_MARKUPUPDATE_TYPES.REMOVE_NODE,执行具体的dom操作
    6.1. 遍历diffQueue,把需要移动(UPDATE_TYPES.REMOVE_NODE)和删除(UPDATE_TYPES.MOVE_EXISTING)的类型节点先找出来,统一放到deleteChildren.push(updatedChild),然后批量删除(执行dom操作):
    $.each(deleteChildren, function(index, child) { $(child).remove(); });
    6.2再次遍历diffQueue,处理新增和修改的节点,使用insertChildAt(parentNode, childNode, index)方法统一处理,传入的参数分别为父节点、子节点和需要移动的位置

普通方法

  • flattenChildren:见5.1.节
  • generateComponentChildren:见5.2.节
  • insertChildAt:见6.2节

2.3. ReactCompositeComponent

自定义元素组件类

实例属性

  • this._currentElement = element,保存当前虚拟dom节点(见1.2.节)
  • this._rootNodeID,根节点id,在调用mountComponent方法的时候传入并保存
  • this._instance自定义元素类的实例,自定义元素类为传入createElement的第一个参数,继承ReactClass的子类,是我们可以书写的class;在调用mountComponent方法的时候保存
  • this._reactInternalInstance自定义元素组件类的实例,自定义元素组件类是react内部的实现,即ReactCompositeComponent,保存了自定义元素类关联的虚拟dom及一系列dom的操作方法(注意区分自定义元素类自定义元素组件类 (实际上应该为this._instance._reactInternalInstance,因为不是该类的实例属性,故删除,可见1.3.ReactClass
  • this._renderedComponent渲染的组件类的实例,即通过this._instance.render()返回元素对应的元素组件类的实例,是我们在class的渲染函数内自定义的渲染,可以是new ReactDOMTextComponentnew ReactDOMComponent()new ReactCompositeComponent任意一种;该实例是自定义元素组件类真实渲染的组件实例

原型方法

  • 1.mountComponent(rootID)

    1.1.实例化自定义元素类
    var ReactClass = this._currentElement.type;
    var inst = new ReactClass(publicProps);
    this._instance = inst;
    取出虚拟dom里的type,对应用户自定义的元素class,并进行实例化

    1.2.调用自定义元素类实例inst.componentWillMount()生命周期(若有)

    1.3.组装节点
    调用this._instance.render()返回class的渲染虚拟dom,再调用instantiateReactComponent返回渲染的实例,最后使用renderedComponentInstance.mountComponent(this._rootNodeID)返回组装的节点html

    1.4.调用自定义元素类实例inst.componentDidMount()生命周期(若有)

  • 2.receiveComponent(nextElement, newState)
    自定义元素组件类的更新方法,接收新节点对应的虚拟dom以及新state

    2.1.更新this._currentElement = nextElement属性,合并statenextState = Object.assign(inst.state, newState)并更新this._instance.state = nextState,获取新propsnextProps = this._currentElement.props

    2.2.调用自定义元素类实例inst.componentWillUpdate(nextProps, nextState)生命周期(若有)

    2.3.重新调用this._instance.render()返回class新的渲染虚拟domnextRenderedElement,调用_shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)判断节点是需要更新还是直接替换(该方法见3.1.节)
    若需要更新,则调用prevComponentInstance.receiveComponent(nextRenderedElement)更新(注意该方法可能会进行递归更新,见本文末尾流程图的绿色线),同时调用自定义元素类实例inst.componentDidUpdate()生命周期(若有)
    若直接替换,则生成nextRenderedElement对应的元素组件实例同时调用mountComponent方法重新组装,并替换整个节点:
    $('[data-reactid="' + this._rootNodeID + '"]').replaceWith(nextMarkup)

3. 其他方法

3.1 _shouldUpdateReactComponent

比较节点是否可以进行更新,用于新旧节点对比,以决定接下来是只更新还是替换

  • 参数
    _shouldUpdateReactComponent(prevElement, nextElement)
    分别传入新旧节点的虚拟dom
  • 方法体
    3.1.1. 比较是否均为文本节点,是的话返回true
    3.1.2. 比较是否为虚拟dom节点,若是的话比较虚拟dom的type和key是否相同,是的话返回true,否的话返回false

4. 再谈diff算法

diff算法本质就是在比较两颗dom树,传统 diff 算法的复杂度为 O(n^3),react对该算法做了几个预设和优化,将复杂度降低为O(n)

  • tree diff
    Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。对树进行分层比较,两棵树只会对同一层次的节点进行比较。该策略对应2.2.节_updateDOMChildren方法
    在这里插入图片描述

  • component diff
    如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。该策略对应3.1.节_shouldUpdateReactComponent方法

  • element diff
    当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。该策略对应5.3.节diff核心算法
    此算法依赖于每个节点的唯一key,即key是每个节点的唯一标识,推荐固定的key值,不指定key则使用index作为key,在这种情况下,key会随着节点的位置改变,在某些情况下会造成大的渲染消耗或者不可复用(例如在子节点头部插入一个全新类型的子节点)。

关于diff算法的详细解释可参考这篇文章

5. 流程图

流程图是根据源代码绘制的,所以配合源代码和本文文字解读观看效果更佳,仓库链接
在这里插入图片描述

参考文章:
React 源码分析
React 源码剖析系列 - 不可思议的 react diff
reactjs源码分析-上篇(首次渲染实现原理)
reactjs源码分析-下篇(更新机制实现原理

属于自己的文字,理解,观点,欢迎交流

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值