为什么会有虚拟Dom?
一个DOM元素创建后,就拥有了HTML Element、Element两种属性,前者是继承于父类和全局的属性和方法,后者是相同种类的元素所普遍具有的方法和属性,我们都是知道随便一个标签上的属性都是一大串。一个网页的DOM很多,同时又涉及到层层嵌套,仅仅依靠浏览器来一次又一次的渲染html效率是非常低的。
但是我们发现,js依靠轻量的特性和浏览器强大的引擎执行起来是非常快的。用js直接操作真实DOM显然不现实的,因为js操作DOM很麻烦,代码可读性差,不但DOM生成费时间,js运行也费时间。
操作真实DOM不行,那么操作一个用js模拟的“假DOM”,得到结果了再生成真DOM不就快很多了吗?于是虚拟DOM的模式就出现了。
react给虚拟Dom的定义?能做什么?
Virtual DOM 是一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。
通过js的形式记录DOM该有的UI状态,然后又避免了大多数通过js来操作属性、事件处理、手动操作DOM场景(直接用react提供的形式或html语言,如jsx语法、组件、类或者直接写标签)
react的虚拟DOM怎么做的呢?
jsx语法通过babel编译后每个标签都会执行createElement函数 我们也可以通过一下方式直接调用
React.createElement(component, props, ...children)
createElement又做了什么?直接上源码
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
//将config上有但是RESERVED_PROPS上没有的属性,添加到props上
//将config上合法的ref与key保存到内部变量ref和key
if (config != null) {
//判断config是否具有合法的ref与key,有就保存到内部变量ref和key中
if (hasValidRef(config)) {
ref = config.ref;
{
warnIfStringRefCannotBeAutoConverted(config);
}
}
if (hasValidKey(config)) {
key = '' + config.key;
}
//保存self和source
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
//将config上的属性值保存到props的propName属性上
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName) //该属性是否在实例本身而非原型
) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
// 如果只有三个参数,将第三个参数直接覆盖到props.children上
// 如果不止三个参数,将后面的参数组成一个数组,覆盖到props.children上
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// Resolve default props
// 如果有默认的props值,那么将props上为undefined的属性设置初始值
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
//开发环境
if (__DEV__) {
// 需要利用defineKeyPropWarningGetter与defineRefPropWarningGetter标记新组件上的props也就 是这里的props上的ref与key在获取其值得时候是不合法的。
if (key || ref) {
//type如果是个函数说明不是原生的dom标签,可能是一个组件,那么可以取
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
//在开发环境下标记获取新组件的props.key是不合法的,获取不到值
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
//在开发环境下标记获取新组件的props.ref是不合法的,获取不到值
defineRefPropWarningGetter(props, displayName);
}
}
}
//注意生产环境下的ref和key还是被赋值到组件上
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
总结:创建key,ref,props,self等重要参数,同时将多于两个的children处理成数组,并调用ReactElement方法
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// This tag allows us to uniquely identify this as a React Element 是否标识为唯一的reactElement
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type,//分为原生标签,class标签,function标签,特殊的标签,其他
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element. //创建该元素的组件
_owner: owner,
};
//开发环境 会创建三个对象_store,_self,_source
if (__DEV__) {
element._store = {};
// To make comparing ReactElements easier for testing purposes, we make
// the validation flag non-enumerable (where possible, which should
// include every environment we run tests in), so the test framework
// ignores it.
// 验证属性
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
// self and source are DEV only properties.
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
// Two elements created in two different places should be considered
// equal for testing purposes and therefore we hide it from enumeration.
// 应该考虑在两个不同位置创建的两个元素
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
总结:新增了$$typeof、_owner属性,返回一个对象,并记录下来
_owner是记录创建它的组件,$$typeof是干嘛的呢?通过翻译:是否是reactElement唯一标识,感觉没啥用。但是打印出来看看
可以发现$$typeof是个Symbol类型的对象,众所周知:Symbol是一个不可改变的匿名类型,通常在类中作为标识符,也不能被传统的方法遍历出来,是全局唯一的数据类型。
/**
* Verifies the object is a ReactElement. 判断这个对象是否是合法ReactElement
* See https://reactjs.org/docs/react-api.html#isvalidelement
* @param {?object} object
* @return {boolean} True if `object` is a ReactElement.
* @final
*/
function isValidElement(object) {
return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
}
那么$$typeof用处是什么呢?
安全
数据库是无法存储 Symbol 类型数据的,所以用户恶意存入的数据是无法带有合法的 typeof 合法性的验证即可防止恶意代码的插入。有效防止了XSS攻击,低版本不支持 Symbol 的浏览器是没有这个安全特性的
通过Babel编译后及两个函数处理后,dom在js中以对象的形式存储起来,这就是react中的虚拟DOM 有个问题,每次更新都去调用这些方法是不是太浪费了?如何有效的更新UI又保证不重复执行这些方法?
答案就是利用算法来计算,将新旧“DOM树” 进行对比,然后差异性在更新,但是现有算法复杂度太高
Diff算法诞生了,有两个假设:
-
两个不同类型的元素会产生出不同的树;
-
开发者可以通过
key来暗示哪些子元素在不同的渲染下能保持稳定;
Diff算法做什么?
1、首先对比根元素,只要不同直接重新生成新的树,及销毁组件及子组件
2、对比同类元素(组件),保留整个DOM,只更新上面的属性,并进行递归
新的问题
diff算法是同级比较再递归,末尾插入新DOM,删除修改DOM都只是操作一次就行
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
但是再开头就插入新DOM,导致所有DOM位置改变,意味着所有DOM全部都要操作一遍
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
key出现了
作为唯一的key,diff函数只需要对比前后相同key的同级元素,不同的更新就行了,这样算法的复杂度就变为了O(n)(n为元素个数)
我们发现平时我们敲jsx的时候,只有map循环的时候才写了key,不写就警告啥的,其他情况没写也没什么问题;还有就是之前没有警告key,莫名其妙突然就警告key相同,怎么回事?
问题一:在render阶段会检查dom是否有key 没有就会创建一个作为key然后进行初次渲染;但是在map时,由于数据是异步的,所以警告是数据回来后做diff时候报的,并非初次渲染,初始渲染应该是[]元素渲染的时候,就是“白屏”时候
//子为数组时候的部分diff算法 处理无key值
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, expirationTime) {
// This algorithm can't optimize by searching from both ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
// with that model. If it ends up not being worth the tradeoffs, we can
// add it later.
// Even with a two ended optimization, we'd want to optimize for the case
// where there are few changes and brute force the comparison instead of
// going for the Map. It'd like to explore hitting that path first in
// forward-only mode and only go for the Map once we notice that we need
// lots of look ahead. This doesn't handle reversal as well as two ended
// search but that's unusual. Besides, for the two ended optimization to
// work on Iterables, we'd need to copy the whole set.
// In this first iteration, we'll just live with hitting the bad case
// (adding everything to a Map) in for every insert/move.
// If you change this code, also update reconcileChildrenIterator() which
// uses the same algorithm.
{
// First, validate keys.
var knownKeys = null;
for (var i = 0; i < newChildren.length; i++) {
var child = newChildren[i]; //循环所有的子元素
knownKeys = warnOnInvalidKey(child, knownKeys); //执行warnOnInvalidKey函数
}
}
}
/**
* Warns if there is a duplicate or missing key
*/
function warnOnInvalidKey(child, knownKeys) {
{
if (typeof child !== 'object' || child === null) { //判断child是否为空或对象(虚拟DOM为对象)
return knownKeys;
}
//和前面我们猜想一样 首先会利用判断是否是reactElement
switch (child.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
warnForMissingKey(child); //如果ReactDOM.createPortal创建的portal类型会警告他缺少key
var key = child.key;
if (typeof key !== 'string') {
break; //key不是字符串也是合法的
}
if (knownKeys === null) {
knownKeys = new Set(); //如果key是空的 会new一个Set类型 Set内的属性是不可重复的
knownKeys.add(key);//并在该类型上加上key
break;
}
if (!knownKeys.has(key)) {
knownKeys.add(key); //Set里没有该key 也就是判断是否key相同会添加上
break;
}
error('Encountered two children with the same key, `%s`. ' + 'Keys should be unique so that components maintain their identity ' + 'across updates. Non-unique keys may cause children to be ' + 'duplicated and/or omitted — the behavior is unsupported and ' + 'could change in a future version.', key);
//不为null,且Set中有该key 则表示存在相同的key 返回一个错误
break;
}
}
return knownKeys;
}
问题二:在执行更新操作,非初次渲染时才会进行diff对比,此时同级的相同兄弟元素没有key都是null判定为相同便发出警告
几点建议:同级相同元素必须带上key,不做curd操作可以使用index作为key,尽量在key前面带上私有前缀