美文网首页
从0实现一个简易React

从0实现一个简易React

作者: 风之化身呀 | 来源:发表于2019-10-03 21:36 被阅读0次

    目录

    本文将分以下四个部分进行:
    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 = (
      <div>
        <img src="avatar.png" className="profile" />
        <h3>{[user.firstName, user.lastName].join(' ')}</h3>
      </div>
    );
    // 转换后
    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 = ''  // 先清空
          render(vdom,container)  // 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 container && container.appendChild(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 完成渲染
       container && container.appendChild(dom)
    }
    

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

    // 转换前
    const b=<p onClick={test} style={{color:'red'}} className={'class'} custom={123}>a</p>
    // 转换后
    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
    }
    

    参考

    相关文章

      网友评论

          本文标题:从0实现一个简易React

          本文链接:https://www.haomeiwen.com/subject/aooblctx.html