不做具体代码分析,只是作为源码思想的解读
虚拟DOM的实现
1 | import React from 'react'; |
打印出来的这个element对象就是虚拟DOM
属性解析:
$$typeof:
Symbol(react.element) 表示这是一个react元素;
标识元素类型的,有:div,Class,Function….
如果说$$typeof是Symbol的话可以仿XSS攻击,因为Symbol是唯一值,如果后台直接返虚拟DOM直接显示在页面上,带Symbol就可以防止后台返回的数据有恶意的虚拟DOM类型,可以避免这个问题,Symbol只有前端有,后台没有。
key: 就是唯一标识
props: 属性,这里没有所以为空
ref:
type: 类型就是这里的这个App Class 如果是函数组件就是函数App
_owner: 它的所有者是谁,谁创建了它 (带下划线的都是内部属性,不是核心属性)
_source: 是源代码哪个文件里的第几行
这就是React元素,它就是一个普通的对象,描述了真实DOM的样子
将上面line3 ~ line13 的代码放到babel中编译,如下:
1 | class App extends React.Component { |
新建./react/ReactElement.js
1 | import ReactCurrentOwner from './ReactCurrentOwner'; |
将上述babel编辑的代码转换成表示虚拟DOM的那个对象
1 | Component.prototype.isReactComponent = {}; |
在React内部是凭借这个变量来判断是不是一个React组件的
因为组件定义有两种方式,一是类组件,一是函数组件,都被babel编译成函数
虚拟DOM的渲染
1 | import ReactDOM from 'react-dom'; |
新建./react-dom/index.js
1 | import { createDOM } from '../react/vdom'; |
新建./react/vdom.js
1 | import { TEXT, ELEMENT, CLASS_COMPONENT, FUNCTION_COMPONENT } from './constants'; |
对$$typeof的判断,来创建真实DOM元素
合成事件
react本身模拟了一套事件机制
index.js
1 | import React from 'react'; |
分析:一个button按钮,id为sayHello,上面有一个onClick事件,包含say内容和一个span标签,span标签内容为hello,颜色为红色
新建./react/event.js
1 | import { updateQueue } from './component'; |
unstable_batchedUpdates 批量强制更新
1 | // 情况一 |
原理
1 | import { updateQueue } from './component'; |
补充一下updateQueue
1 | export let updateQueue = { |
DOM-DIFF
- DOM 节点跨层级的移动操作特别少,可以忽略不计
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
- 对于同一层级的一组子节点,它们可以通过唯一key进行区分
- DIFF算法在执行时有三个维度,分别是Tree DIFF、Component DIFF和Element DIFF,执行时按顺序依次执行,它们的差异仅仅因为DIFF粒度不同、执行先后顺序不同
Tree DIFF
- Tree DIFF是对树的每一层进行遍历,如果某组件不存在了,则会直接销毁
- 当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建
Component DIFF
- 如果是同一类型的组件,按照原策略继续比较
- 类型不同则直接替换
原理分析
老节点 A B C D
新节点 A C B E F
(1) 针对老节点做一个map映射,属性是老节点的key,值是老节点(即React元素节点)(A:A元素,B:B元素,C:C元素,D:D元素)
(2) lastIndex 指向老节点中可复用,上一个不需要移动的元素的索引
(3) _mountIndex 挂载索引,表示这个节点在兄弟节点中的位置 (0 1 2 3)
1、开始遍历新节点的数组
1.1、在map里找有没有可复用的老节点,通过key去查找,如果新老两个元素key一样,type类型也是一样的,那就可以复用
【比较A节点】
通过key去查找老节点,key、type都一样,找到A
可复用的A节点 _mountIndex < lastIndex 则需要移动,否则不用移动可复用,现在对于A节点来说,_mountIndex 为0,lastIndex 为0,所以DOM节点A不用移动,复用老的DOM节点,用新属性更新这个DOM节点
如图,此时lastIndex还是指向0,_mountIndex 指向新的A节点的下标 0
【比较C节点】
继续,通过key去查找老节点,key、type都一样,找到C
可复用的老节点的_mountIndex:2, lastIndex:0 不移动,可复用,复用老的DOM节点,用新属性更新这个DOM节点,下标改为1
并且lastIndex = Math.max(lastIndex,老节点的挂载点_mountIndex 即2),这里lastIndex为2
每次比较都会取最大值给lastIndex,并且更新挂载点的索引
【比较B节点】
继续,通过key去查找老节点,key、type都一样,找到B
老的B节点的_mountIndex: 1,现在lastIndex: 2, _mountIndex < lastIndex ,移动节点,一定到新节点B的同下标位置 2
【比较节点E】
继续,通过key去查找老节点,没有找到,新节点E插入,挂载点是 3
【比较节点F】
继续,通过key去查找老节点,没有找到,新节点E插入,挂载点是 4
新数组遍历完了,将老节点中没用到的节点删除
【小结】
1、把老节点放在map里,属性为key,值为虚拟DOM
2、遍历新子元素数组,先去map里找有没有能复用的,如果找到了就更新DOM属性,并且判断_mountIndex是否小于lastIndex,如果小于则需要移动,从老节点原来的位置删除,并插入新数组的当前位置,然后把老节点的挂载点_mountIndex等于此节点在新数组中的索引,另外更新lastIndex为lastIndex和老挂载点的较大值
3、如果找不到可以复用的老节点,则直接在当前索引位置插入新节点
4、新数组遍历完成后,如果还有没有用到的老节点,删除。
深度优先遍历,如果节点下还有子节点,会先去遍历子节点
这里的类型比较是type,$$type大类型,可能都是element,而type则是比较的是div、span这种
【示例】
如果 A 下面有节点 B C,改成 A C B,会先去更新B C 下面的子节点,再整体移动 B 放到 C 的后边
部分代码实现
更新子节点
1 | let diffQueue = [];// 这是一个补丁包,记录了哪些节点需要删除,哪些节点需要添加 |
开始打包,这里操作的是真实的DOM元素
- 第一步,把该删除的删除 MOVE REMOVE 移动的和删除的
- 第二步,插入移动的
这里指的是移动的B 和 删除的 D,其中要把移动的B放在一个Map里面缓存起来后面复用
先更新父节点的属性,再更新子节点的属性
先移动子节点,再移动父节点
相关文章:
http://www.zhufengpeixun.cn/2020/html/96.1.react16.html
生命周期
context
1 | function createContext(defaultValue){ |
1 | export function onlyOne(obj){ |