学习react原理(一)

525 阅读4分钟

react原理

虚拟dom:

用js对象表示dom信息和结构,当状态变更的时候,重新渲染这个js的对象结构,这个js对象称为虚拟dom。

为什么需要虚拟dom?

dom操作很慢,小操作都可能导致重绘,非常消耗性能,相比于dom,js对象操作起来更加快,而且更简单。通过diff算法对比新旧虚拟dom之间的差异,可以批量,最小化的操作don,提高性能。

怎么将虚拟dom转换为真实dom:

react中通过jsx描述视图,jsx其实是一种语法糖。通过babel-loader将jsx语法糖转译,变成React.createElement(...)形式,将虚拟dom转换为真实dom。如果状态变化,虚拟dom也将变化,通过diff算法对比新老虚拟dom,实现真实dom变化。

jsx:

为什么需要jsx?

  • 1.开发效率:使用jsx编写模版简单快速
  • 2.执行效率:jsx编译为js代码后进行了优化,执行更快
  • 3.类型安全:在编译过程中就能发现错误    

原理:babel-loader会将jsx预编译为React.createElement()

可以看下react官网一个例子: 使用jsx:

class HelloMessage extends React.Component {
  render() {
    return (
      <div>
        Hello {this.props.name}
      </div>
    );
  }
}

ReactDOM.render(
  <HelloMessage name="Taylor" />,
  document.getElementById('hello-example')
);

不使用jsx:

class HelloMessage extends React.Component {
  render() {
    return React.createElement(
      "div",
      null,
      "Hello ",
      this.props.name
    );
  }
}

ReactDOM.render(React.createElement(HelloMessage, { name: "Taylor" }), document.getElementById('hello-example'));

下面看下react中几个api

  • React.createElement:创建虚拟dom
  • React.Component:实现自定义组件
  • ReactDom.render():渲染真实dom

看下createElement源码:

/**
 * Create and return a new ReactElement of the given type.
 * See https://reactjs.org/docs/react-api.html#createelement
 */
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;

  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;

      if (__DEV__) {
        warnIfStringRefCannotBeAutoConverted(config);
      }
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 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];
      }
    }
  }

  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  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
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  if (__DEV__) {
    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,
  );
}

通过上面源码结合不使用jsx的代码,可以看出createElement是将传入的节点转换为虚拟dom。 第一个参数是type,表示节点类型,这里节点类型有很多,如:文本节点,html标签节点,class组件,函数组件,fragment。 第二个参数是config,表示节点的属性和value。 第三个参赛是children,表示子节点。

看下ReactDom.render()源码:

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {
  invariant(
    isValidContainer(container),
    'Target container is not a DOM element.',
  );
  if (__DEV__) {
    const isModernRoot =
      isContainerMarkedAsRoot(container) &&
      container._reactRootContainer === undefined;
    if (isModernRoot) {
      console.error(
        'You are calling ReactDOM.render() on a container that was previously ' +
          'passed to ReactDOM.createRoot(). This is not supported. ' +
          'Did you mean to call root.render(element)?',
      );
    }
  }
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

源码结合render使用,可以看出第一个参数是要渲染的dom,第二个参数是要将dom渲染到的容器,最后返回。

再看下Compoent源码:

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};

其中通过Component.prototype.isReactComponent来区分是否是class组件还是函数组件。

下面实现下上面三个简易api

/src/index.js

import React from "./react/index";
import ReactDOM from "./react/react-dom";
import Component from "./react/Component";
import "./index.css";

const jsx = (
  <div className="border">
    <p>aaa</p>
  </div>
);

ReactDOM.render(jsx, document.getElementById("root"));

因为是通过babel编译jsx为createElement,通过React导出。所以在index.js中写createElement

// react/index.js

// 创建react element,并返回
function createElement(type, config, ...children) {
  // children是数组,此时需要判断child是否是对象,不是对象代表是文本节点,为了统一数据结构,
  // 创建文本节点函数如下,包括type和props
  const props = {
    ...config,
    children: children.map(child =>
      typeof child === "object" ? child : createTextNode(child)
    )
  };
  return {
    type,
    props
  };
}
// 创建文本节点
function createTextNode(text) {
  return {
    type: 'TEXT',
    props: {
      children: [],
      nodeValue: text
    }
  };
}
// 导出createElement
export default {
  createElement
};

class组件是继承Component,所以Component是构造函数,将props挂载出去,同时标志是否是函数组件还是class组件

// react/Component.js
export default function Component(props) {
  this.props = props;
}

Component.prototype.isReactComponent = {};

上面src/index.js目前只写了html节点和文本节点,现在实现render将节点显示出来。

// react/react-dom.js

// 将虚拟dom转换为真实dom,并渲染出来
function render(vnode, container){
  // vnode => node
	const node = createNode(vnode);
  container.appendChild(node)
}

// 将vnode转换为真是node
function createNode(vnode){
	const { type, props } = vnode;
  // 开始定义真实节点为null;
  let node = null;
  // 通过type判断是哪种节点
  if(type === 'TEXT'){
  	// 文本节点
    node = document.createTextNode('');
  }else if(typeof type === 'string'){
  	// html节点
    node = document.createElement(type)
  }
  // 最后返回真实node
  return node;
}

上面将父节点dom渲染出来,下面将节点的属性和子节点渲染出来。 渲染子节点:

function createNode(vnode){
  const { type, props } = vnode;
	...
  // 渲染子节点
  // 此时props.children是虚拟节点,node是要渲染到的容器container
  reconcileChildren(props.children, node)
  return node;
}

// 渲染子节点
function reconcileChildren(children, node){
	// children是数组
	for(let i =0;i<children.length;i++){
  	// 考虑jsx是数组渲染dom
    let child = children[i];
    if(Array.isArray(child)){
    	for(let j =0;j<child.length;j++){
      	render(child[j], node)
      }
    }else{
    	render(child, node)
    }
  }
}

渲染节点属性:

function createNode(vnode){
  const { type, props } = vnode;
	...
  // 渲染子节点
  // 此时props.children是虚拟节点,node是要渲染到的容器container
  reconcileChildren(props.children, node)
  // 渲染属性
  // 将props内容渲染到node上面
  updateNode(node, props);
  return node;
}

// 渲染子节点
function reconcileChildren(children, node){
	// children是数组
	for(let i =0;i<children.length;i++){
  	// 考虑jsx是数组渲染dom
    let child = children[i];
    if(Array.isArray(child)){
    	for(let j =0;j<child.length;j++){
      	render(child[j], node)
      }
    }else{
    	render(child, node)
    }
  }
}
// 渲染属性
function updateNode(node, nextVal){
  Object.keys(nextVal).filter(k => k!== 'children').forEach(i => {
    node[i] = nextVal[i]
  })
}

现在将文本节点和html节点渲染出来,看下结果: image.png

下面写下渲染class组件,函数组件,fragment。

更新class组件:

function createNode(vnode){
  const { type, props} = vnode;
  let node = null;
  if(type === 'TEXT'){
    node = document.createTextNode('')
  }else if(typeof type === 'string'){
    node = document.createElement(type)
  }else if(typeof type ==='function'){
    // 通过isReactComponent区分是函数组件还是class组件
    node = type.prototype.isReactComponent ? 
    updateClassCom(vnode):
    updateFuncCom(vnode)
  }else{
    node = document.createDocumentFragment()
  }	
  reconcileChildren(props.children, node)
  updateNode(node,props)
  return node;
}
// 更新class组件
function updateClassCom(vnode){
	const { type, props}  = vnode;
  // 此时type是class,所以需要new
  const cmp = new type(props);
  // 此时cmp原型上有render()
  // 调用render,获取虚拟dom
  const vvnode = cmp.render();
  // 将虚拟dom转为真实dom,并渲染
  const node = createNode(vnode)
  return node;
}

// 更新函数组件
function updateFuncCom(vnode){
	const { type, props} = vnode;
  const cmp = type(props)
  const vvnode = cmp.render();
  const node = createNode(vvnode)
  return node;
}

看下demo如下:

import React from "./react/index";
import ReactDOM from "./react/react-dom";
import Component from "./react/Component";
import "./index.css";

class ClassComponent extends Component {
  static defaultProps = {
    color: "pink"
  };
  render() {
    return (
      <div className="border">
        class组件-{this.props.name}
        <p className={this.props.color}>omg</p>
      </div>
    );
  }
}

function FunctionComponent(props) {
  return <div className="border">函数组件-{props.name}</div>;
}

const jsx = (
  <div className="border">
    <p>aaa</p>
    <ClassComponent name="class" color="red" />
    <FunctionComponent name="function" />
    <>
      <h1>aaa</h1>
      <h1>bbb</h1>
    </>
  </div>
);

ReactDOM.render(jsx, document.getElementById("root"));

image.png

此时,文本节点,html标签,函数组件,class组件和fragment都渲染出来了。

以上学习了createElement,Compoent,render()三个简易api。