React17系列(3)-jsx如何转成虚拟dom

前言

因为工作的原因,好久没有静下心来沉淀自己。忙里偷闲,介绍下虚拟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,
) {...}

jsxWithValidation参数.jpg

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

jsxdev入参.png

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

ReactElement入参.png

/**
 * 先看一下官方对这个函数的介绍
 * 使用工厂方法模式创建一个新的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长什么样子

虚拟dom.png

整体阅读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是什么。。。如有见解错误,欢迎大家指正。

猜你喜欢

转载自juejin.im/post/7126478423286874143