从0实现一个简易React

目录

本文将分以下四个部分进行:
1、实现 React.createElement && ReactDOM.render 方法
2、实现组件Component 及生命周期函数
3、实现 setState 的异步更新
4、实现DOM Diff 算法

1、实现 React.createElement && ReactDOM.render 方法

  • 关于JSX

JSX本身是一种语法糖,会被 Babel 的 transform-react-jsx 插件转换为 JS 代码

// 转换前
const profile = (
  

{[user.firstName, user.lastName].join(' ')}

); // 转换后 const profile = React.createElement("div", null, React.createElement("img", { src: "avatar.png", className: "profile" }), React.createElement("h3", null, [user.firstName, user.lastName].join(" ")) );

这个转换后的React.createElement是可以通过babel配置的:

{
  "plugins": [
    ["@babel/plugin-transform-react-jsx", {
      "pragma": " React.createElement", 
    }]
  ]
}

默认的 pragma 就是 React.createElement,所以一般不需要额外配置。这也是为什么在写React组件时,明明没有用到React,但还是要将其引入到文件中的原因。(你写的代码是编译前的,但是实际运行的代码是编译后的,而编译后的文件需要 React,所以必须在编译前就引入,否则就是报 React 找不到的错误)

  • 关于虚拟DOM(称之为 vdom)

虚拟DOM本质上就是一个JS对象,只不过是一个真实DOM的映射,能反应出真实DOM的 tag , attributes , children等信息。结合diff 算法,用虚拟DOM描述真实DOM有利于在DOM更新时提升更新效率。

// 虚拟 DOM
{
    tag,
    attrs,
    children
}
  • 实现 React.createElement

createElement 的主要作用就是创建虚拟DOM,这里简单实现为返回上述的虚拟DOM

React.createElement = function(tag,attrs,...children){ // 第三个参数开始是子元素,用扩展运算符接收得到子元素数组
     return {
         tag,
         attrs,
         children
     }
}

注意这里参数顺序是跟Babel转换后的代码一致的,也就是说参数的值由 Babel 负责传递

  • 实现 ReactDOM.render

ReactDOM.render方法接受一个JSX参数(转化后就是vdom)和一个挂载元素

ReactDOM.render = function(vdom,container){
      container.innerHTML = ''  // 先清空
      container.appendChild(render(vdom) )  // render 负责将 vdom 编译成 dom 片段
}

要实现 render,这里要说下 vdom 中 children 元素的类型,不是所有 children 元素都是 vdom 类型:对于文本, children 元素是一个字符串;对于非文本, children 元素才是vdom,且对于自定义组件,vdom的tag是function类型,html标签的tag是string类型

function render(vdom,container){
   if(typeof vdom === 'string'){       // 文本节点单独对待
        return document.createTextNode(vdom)      
   }
   const dom = document.createElement(vdom.tag)
   for(let key in vdom.attrs){
         if(vdom.attrs.hasOwnProperty(key)){
                 setAttributes(dom,key,vdom.attrs[key])          // 设置属性,如事件、样式、自定义属性等
         }
   }
   vdom.children.forEach((item,key)=>render(item,dom))        // 遍历子节点,递归调用 render 完成渲染
   return dom
}

接下来就是属性的设置了,主要是区分事件、样式和其它属性的设置

// 转换前
const b=

a

// 转换后 var b = React.createElement("p", { onClick: test, style: { color: 'red' }, className: 'class', custom: 123 }, "a");

对着上述转换结果,可写出 setAttributes 函数

function setAttributes(dom,key,value){
    // 设置事件,React中事件以 onXXX 开头,这里用正则判断
   if(/on[A-Z]/.test(key)){
      dom.addEventListener(key.slice(2).toLowerCase(),value,false)
   }else if(key==='className'){
      dom.classList.add(value)
   }else if(key==='style'){
      for(let i in style){
         dom.style.cssText+=`${i}:${style[i]};`
      }
   }else{
         dom.setAttribute(key,value)
   }
}

2、实现类组件Component,函数式组件及各生命周期函数

函数式组件只是类组件的一个特例,将函数式组件作为类组件的render函数基本就可以将函数式组件扩展为类式组件,所以本质上二者是一样的。以下专注于处理类式组件。

  • 实现 React.Component
    Component 作为组件基类,最基本的需要提供构造函数以及组件更新的 setState 函数
class Component{
      constructor(props){
            this.props = props
            this.state = {}
      }
      setState(state){
            Object.assign(this.state,state)
            renderComponent(this)
      }
      forceUpdate(){   // 强制更新
            renderComponent(this)
      }
}

对于类式组件,JSX被Babel编译后产生的vnode中的tag是函数类型。所以要渲染类式组件,需要调整上述render函数的实现:

   // 新增 tag 为 function 的情况(组件)
  if(typeof vdom.tag === 'function'){
         const comp = createComponent(vdom.tag,vdom.attrs)
         setComponentProps(comp,vdom.attrs)
         renderComponent(comp)
         return comp.base  // 将渲染的真实dom存在base属性上
   }

createComponent 负责创建组件的实例,也就是创建一个JS对象,要注意区分函数组件和类组件,这步操作实际上已经屏蔽了 类式组件和函数式组件的区别。

function createComponent(fun,props){
     let comp;
     if(fun.prototype.render){
           comp = new fun(props) 
     }else{
           comp = new Component(props)
           comp.constructor = fun
           comp.render = function(){
                   return fun(props)
           }
     }
     return comp
}

setComponentProps 负责更新属性,并调用相关生命周期函数

function setComponentProps(comp,props){
      if(comp.base){
            // dom已存在,做更新操作
           if(comp.componentWillReceiveProps){
                comp.componentWillReceiveProps(props)
           }
      }else{
            // dom 还未生成,做初始操作
             if(comp.componentWillMount){
                comp.componentWillMount(props)
             }
      }
      comp.props = props
}

renderComponent 负责将comp转化为dom片段

function renderComponent(comp){
      const vdom = comp.render()
      if(comp.base){
           // dom 已存在,做更新
           if(comp.componentWillUpdate){
                comp.componentWillUpdate(comp.props)
           }
      }
      const base = render(vdom)
      if(comp.base){
           // dom 已存在,做更新
           if(comp.componentDidUpdate){
                comp.componentDidUpdate()
           }
      }else{
           // dom 不存在,做创建操作
           if(comp.componentDidMount){
                comp.componentDidMount()
           }
      }
      if(comp.base.parentNode){    // 第一次挂载后,通过setState更新组件时,要替换老节点
           comp.base.parentNode.replaceChild(base,comp.base)
      }
      comp.base =base      //  覆盖老的 dom
}

3、实现 setState 的异步更新

上述 setState 的实现是同步的,每次调用 setState 都会触发 renderComponent,当setState 调用频繁时会是一个性能瓶颈,以下将实现 setState 的异步更新,基本原理是利用 微任务队列控制 renderComponent 的执行时机

setState(state){
      enQueueState(state,this)
}
// 辅助函数-队列保存历史state
const queue=[]
function enQueueState(state,comp){
       if(queue.length===0){
             return new Promise().then(flush)
       }
       queue.push({
            state,
            comp
       })
}
// 辅助函数-清空队列,合并state并渲染组件
function flush(){
     let item;
     while(item=queue.shift()){
           const {state,comp} = item
           Object.assign(comp.state,state)
           if(queue.length===0){
                 renderComponent(comp)
           }
     }
}

4、实现DOM Diff 算法

diff 算法有两种实现,一种是用新旧vdom对比,得出变化部分再做更新操作;另一种是用真实旧dom和新的vdom对比,边对比边更新,这里采用后者来实现,主要说下思路:
diff 函数接受旧dom和新vdom为参数,返回更新后的真实dom

function diff(dom,vdom){
    const ret = diffNode( dom, vnode );
    if ( container && ret.parentNode !== container ) {
        container.appendChild( ret );
    }
    return ret;
}

diffNode 分五种情况:
1、diff 文本
2、diff 组件
3、diff 不同类型原始标签
4、同类型原始标签时,diff 属性
5、diff children 里递归调用 diffNode

diff 的调用时机在 renderComponent 函数里:

function renderComponent(comp){
      const vdom = comp.render()
      if(comp.base){
           // dom 已存在,做更新
           if(comp.componentWillUpdate){
                comp.componentWillUpdate(comp.props)
           }
      }
      const base = diff(comp.base,vdom)
      if(comp.base){
           // dom 已存在,做更新
           if(comp.componentDidUpdate){
                comp.componentDidUpdate()
           }
      }else{
           // dom 不存在,做创建操作
           if(comp.componentDidMount){
                comp.componentDidMount()
           }
      }
      if(comp.base.parentNode){    // 第一次挂载后,通过setState更新组件时,要替换老节点
           comp.base.parentNode.replaceChild(base,comp.base)
      }
      comp.base =base      //  覆盖老的 dom
}

参考

  • 从零开始实现一个React

你可能感兴趣的:(从0实现一个简易React)