带你从0~1手写React源码----原生组件的渲染

699 阅读3分钟

前言

React支持jsx语法,我们可以直接将HTML代码写到JS中间,然后渲染到页面上,我们写的HTML如果有更新的话,React还有虚拟DOM的对比,只更新变化的部分,而不重新渲染整个页面,大大提高渲染效率。那么JSX是如何一步步渲染到浏览器页面中呢?下面我们一步步来解析。

通过createElement转换成vdom

我们写JSX代码可以通过babel转换工具转换成React.createElement的形式,如下面的代码

let ary = [
     { name: "xiaomi", age: 12 },
     { name: "xiaoming", age: 120 },
     { name: "xiaohong", age: 121 },
     { name: "xiaohe", age: 122 },
   ];
   function App() {
     return (
       <ul>
         {ary.map(function (item, index) {
           return (
             <li key={index}>
               姓名是:{item.name},年龄是:{item.age}
             </li>
           );
         })}
       </ul>
     );
   }
   ReactDOM.render(
     <React.StrictMode>
       <App />
       <h1>你好!</h1>
       <p>OK</p>
     </React.StrictMode>,
     document.querySelector("#root")
   );

babel转换如下:

image.png 从转换后的代码我们可以看出React.createElement支持多个参数:

  • type:判断元素节点类型(string/function)
  • config:该元素节点的属性,例如ref
  • children:从第三个参数开始就全部是children也就是子元素了,子元素可以有多个,类型可以是简单的文本,也可以还是React.createElement,如果有多个,children是个数组。 React.createElement的返回值是个对象{type:xxx,props:xxx}

自己实现一个createElement函数

const createElement = function createElement(type, config, children) {
  let props
  if (config) {
    delete config.__source, delete config.__self
  }
  props = { ...config }
  if (arguments.length > 3) {
    children = Array.prototype.slice.call(arguments, 2)
  }
  props.children = children
  return {
    type,
    props,
  }
}

通过 render 转换成真实 dom

  1. 把虚拟DOM变成真实DOM
  2. 把虚拟DOM上的属性更新或者同步到真实DOM上
  3. 把虚拟DOM的儿子们也变成真实DOM挂载到自己的dom上
  4. 把自己挂载到容器上
/**
 * 1.把虚拟dom转换成真实DOM
 * 2.把虚拟dom上的属性更新或者同步到dom上
 * 3.把此虚拟dom的儿子也都变成真实dom挂载到自己的dom上
 * 4.把自己挂载到容器上
 * @param {*} vdom 要渲染的虚拟DOM
 * @param {*} container 把虚拟dom转换成真实DOM 并且插入到容器中
 */

function render(vdom, container) {
  const dom = createDOM(vdom)
  container.appendChild(dom)
}

/**
 * 把虚拟dom变成真实dom
 * @param {*} vdom 虚拟dom
 */
function createDOM(vdom) {
  //如果是字符串或者数字 直接返回一个真实的文本节点
  if (typeof vdom === 'string' || typeof vdom === 'number') {
    return document.createTextNode(vdom)
  }
  let { type, props } = vdom
  //创建dom元素
  let dom = document.createElement(type)
  //使用虚拟dom属性更新刚刚创建出来的真实DOM属性
  updateProps(dom, props)
  //在这里处理children
  if (typeof props.children == 'string' || typeof props.children === 'number') {
    dom.textContent = props.children
    //如果只有一个儿子并且是个虚拟dom元素
  } else if (typeof props.children === 'object' && props.children.type) {
    //把此虚拟dom的儿子也都变成真实dom挂载到自己的dom上
    render(props.children, dom)
  } else if (Array.isArray(props.children)) {
    reconcileChildren(props.children, dom)
  } else {
    document.textContent = props.children ? props.children.toString() : ''
  }
  //把真实dom放在虚拟dom属性上,为以后更新做准备
  vdom.dom = dom
  return dom
}

/**
 * 使用虚拟dom属性更新刚刚创建出来的真实DOM属性
 * @param {*} dom 真实DOM
 * @param {*} props 新属性对象
 */
function updateProps(dom, props) {
  for (let key in props) {
    if (key === 'children') continue //单独处理
    if (key === 'style') {
      let styleObj = props[key]
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr]
      }
    } else {
      dom[key] = porps[key]
    }
  }
}

/**
 * @param {*}  childrenVdom 儿子们的虚拟dom
 * @param {*}  parentDOM 父亲的真实dom
 */
function reconcileChildren(childrenVdom, parentDOM) {
  for (let i = 0; i < childrenVdom.length; i++) {
    let childVdom = childrenVdom[i]
    render(childVdom, parentDOM)
  }
}