前言
因为工作的原因,好久没有静下心来沉淀自己。忙里偷闲,介绍下虚拟dom,无论是react还是vue,都涉及到一个非常重要的概念:虚拟dom。在粗略调试了下后,记录下react中虚拟dom的形式以及我们写的jsx是如何变成虚拟dom的。(个人理解,有错误欢迎指出,目前一些细节暂时没研究)
JSX示例
// 这里写一个简单的示例以供演示
const App = () => {
return (
<>
<span className="text" key="span-key">hello luckydog</span>
</>
);
};
export default App;
jsx如何解析hello luckydog这个节点?我按照顺序逐一整理了下:
jsxWithValidation函数参数
首先触发jsxWithValidation函数(整体也是围绕此函数讲解),先介绍此函数入参:
export function jsxWithValidation(
// 标签类型 例如:span div等,后续有函数验证此type是否合法时详细介绍
type,
// dom节点的属性,包括子节点
props,
// 节点的唯一标识
key,
// true或者false,与节点中子节点个数有关
isStaticChildren,
// 记录代码文件信息等
source,
// 暂时没懂
self,
) {...}
isValidElementType
顾名思义,验证dom元素类型是否合法,也是进入jsxWithValidation函数后遇到的第一个函数。
rc在ReactSymbols.js中定义了表示rc元素类型的变量,如果浏览器支持Symbol,则定义为Symbol类型的变量,否则为十六进制的数字。
export let REACT_ELEMENT_TYPE = 0xeac7;
export let REACT_PORTAL_TYPE = 0xeaca;
export let REACT_FRAGMENT_TYPE = 0xeacb;
export let REACT_STRICT_MODE_TYPE = 0xeacc;
// .....
if (typeof Symbol === 'function' && Symbol.for) {
const symbolFor = Symbol.for;
REACT_ELEMENT_TYPE = symbolFor('react.element');
REACT_PORTAL_TYPE = symbolFor('react.portal');
REACT_FRAGMENT_TYPE = symbolFor('react.fragment');
// ......
}
当type属于string、function或者以上部分类型,或者type.$$typeof属于以上部分类型时,就证明其是合法的,
isValidElementType函数路径:src/react/node_modules/shared/isValidElementType.js(建议亲自看一下,这里不做过多介绍)
不合法的类型?
那么rc为什么要做这个操作呢,试想一下,如果前端期望从接口中获取字符串渲染,如果有用户恶意存入这种数据:
...
return (<>
// 渲染后端返回的数据
{text}
</>)
...
// 数据形如以下类型
const text = {
type: 'script'
props: {src: 'http://...'}
}
如果不做如上校验,那么一个存在风险的第三方script标签就已经入侵了我们的页面。
jsxDev => ReactElement (虚拟dom的生成)
校验元素类型合法后,来到jsxWithValidation函数中第二个函数jsxDev,此函数通过ReactElement创建出虚拟dom,直接看代码以及实际入参:
jsxDev
export function jsxDEV(type, config, maybeKey, source, self) {
if (__DEV__) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
/**
* 官方注释,大致解释下
* <div key="Hi" {...props}></div>
* <div {...props} key="Hi"></div>
* 如果props中也存在属性key
* 在第一种情况中,key会取props中的key值
* 在第二种情况中,key会直接取值Hi,并且生成虚拟dom是通过createElementWithValidation函数
* (暂时没看懂rc中是如何判断何时通过哪个函数渲染createElementWithValidation vs jsxWithValidation)
*/
if (maybeKey !== undefined) {
key = '' + maybeKey;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
if (hasValidRef(config)) {
ref = config.ref;
warnIfStringRefCannotBeAutoConverted(config, self);
}
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
/**
* 开发模式中,不允许通过props获取key或者ref属性
* type如果是函数,表示当前元素是组件
* displayName用于在报错过程中显示是哪一个组件报错了
* type如果不是函数 直接返回元素类型字符串
* defineKeyPropWarningGetter/defineRefPropWarningGetter
* 这两个函数是如何实现读取key或ref就报错的,原理也较简单
* 通过Object.defineProperty重写key或ref的get属性
* 有兴趣可自行查看
*/
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
}
ReactElement
/**
* 先看一下官方对这个函数的介绍
* 使用工厂方法模式创建一个新的react元素
* 不再坚持使用类模式,所以不要使用new方法来调用此函数
* 并且,instanceof检查也不再使用
* 使用$$typeof以及Symbol.for('react.element')字段来检查是否是react元素
*/
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 这个标签允许我们将它唯一地标识为一个 React 元素
// 最终渲染到DOM上时,需要判断$$typeof===REACT_ELEMENT_TYPE
$$typeof: REACT_ELEMENT_TYPE,
// 元素内置属性
type: type,
key: key,
ref: ref,
props: props,
// 负责记录创建此元素的组件。来源于ReactCurrentOwner.current
_owner: owner,
};
if (__DEV__) {
/**
* _store _self _source 这三个属性是开发环境下特有的
* _store:在此存放验证标识
*/
element._store = {};
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};
目前,虚拟dom就已经生成了,看一下虚拟dom长什么样子
整体阅读jsxWithValidation函数
export function jsxWithValidation(
type,
props,
key,
isStaticChildren,
source,
self,
) {
if (__DEV__) {
// 验证元素类型是否正确
const validType = isValidElementType(type);
if (!validType) {
// ...todo
}
// 生成虚拟dom
const element = jsxDEV(type, props, key, source, self);
if (element == null) {
return element;
}
/**
* 验证元素子节点
* 子节点有多个时 isStaticChildren为true,并且子节点为数组形式
* 子节点有一个时 isStaticChildren为false 为对象或者字符串
* 主要验证生成的虚拟dom $$typeof是否正确
* 验证通过后 会将_store中的validated设为true
*/
if (validType) {
const children = props.children;
if (children !== undefined) {
if (isStaticChildren) {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
validateChildKeys(children[i], type);
}
if (Object.freeze) {
Object.freeze(children);
}
} else {
console.error(
'React.jsx: Static children should always be an array. ' +
'You are likely explicitly calling React.jsxs or React.jsxDEV. ' +
'Use the Babel transform instead.',
);
}
} else {
validateChildKeys(children, type);
}
}
}
/**
* 检验是否存在这种写法<%s {...props} key={key} />
* 在props后显示传递key
*/
if (warnAboutSpreadingKeyToJSX) {
if (hasOwnProperty.call(props, 'key')) {
console.error(
'React.jsx: Spreading a key to JSX is a deprecated pattern. ' +
'Explicitly pass a key after spreading props in your JSX call. ' +
'E.g. <%s {...props} key={key} />',
getComponentName(type) || 'ComponentName',
);
}
}
/**
* 虚拟dom的解析是由内向外的
* 在我们给出的例子中 先解析span标签 后解析React.fragment标签
* 在此处也是需要根据type类型验证元素以及fragment的属性
*/
if (type === REACT_FRAGMENT_TYPE) {
validateFragmentProps(element);
} else {
// 给定一个元素,验证它的 props 是否遵循 propTypes 定义
validatePropTypes(element);
}
return element;
}
}
在经历一系列解析、验证等,最终便生成并返回了一个对象用以代表虚拟dom,以上只是我的初略见解,还遗留了很多问题所在,例如虚拟dom中的_owner到底是如何生成的,它与fiber是什么关系,ReactCurrentOwner.current是什么。。。如有见解错误,欢迎大家指正。