精通React原理之实现自己的React袖珍框架

441 阅读3分钟

前言

之前看过react源码并在csdn上发过相关的文章,最近我打算再看一下react17的源码,因为任何书籍和文章每次读的感悟是不同的,源码亦是如此。并在掘金和github同时做每次学习的笔记。(计划历时半个月左右更新完毕)

github: github.com/China-forre…

  • jsx & react元素的只读性
/* 
编译 》 执行
显示编译成createElement的形式,执行的时候调用函数,成为虚拟DOM
render方法负责把虚拟DOM变成真实的DOM插入到容器里。
*/

/* 
react元素是不可变的,我们每次都是创建新的react元素进行渲染 
react只会更新必要的部分

React17之前, React规定React元素是不可变的、但是可以扩展添加属性,但是之后就不可以了Object.seal()

let element = {type: 'h1'}
Object.freeze(element)
而Object.freeze()是通过 Object.defineProperty()设置 writable: false实现的;
//自己手写实现一个Object.freeze()
Object.defineProperty(Object, 'freeze', {
  value: function(obj) {
    let i;
    for(i in obj) {
      if(obj.hasOwnproperty(i)) {
        Object.defineProperty(obj, i, {
          writable: false    //不可修改
        })
      }
    }
    // 不可扩展
    Object.seal(obj);
  }
})
*/

  • 实现原生组件的渲染 设置环境变量(我们写自己的react,所以我们要禁用一下react17的jsx转换器,这样我们才能看到createElement,原因在下面): "start": "set DISABLE_NEW_JSX_TRANSTORM=true&&react-scripts start" 设置开发模式进行打包(压缩之前)"start": "set NODE_ENV=development&&react-scripts start" 我们可以观察到React17中启动了新的jsx转换器, 这样我们不引入import React from 'react'也可以编译jsx

由新的编译器自动引入: import { jsx as _jsx } from 'react/jsx-runtime.js' 新的jsx转换器(为了减少耦合): 一个单独的包require('jsx-transform')('h1'); 再有我们的新版本(17)也不用设置环境变量了,因为内部指定了:

image.png

实现创建虚拟DOM的方法React.createElement,实现渲染的方法render。 JSON.stringify()的常见用法: www.cnblogs.com/echolun/p/9… replacer是替换器

JSON.stringify(obj, replacer, 2);
function replacer(key, value) {
    if(key === 'age'){
        return  value + 1;
    }else {
        return value;
    }
}
{
  "type": "h1",
  "key": null,
  "ref": null,
  "props": {
    "id": "title",
    "className": "title",
    "style": {
      "color": "red"
    },
    "children": [
      {
        "type": "span",
        "key": null,
        "ref": null,
        "props": {
          "children": "成功了"
        },
        "_owner": null,
        "_store": {}
      },
      "hello"
    ]
  },
  "_owner": null,
  "_store": {}
}

createElement的实现

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

const React = { createElement };
export default React;

实现React.render()方法进行挂载

/*
把vdom虚拟DOM变成真实DOM
把虚拟DOM上的属性更新到dom上;
把虚拟DOM的儿子们也变成真实DOM挂载到自己的dom上  dom.appendChlid
 把自己挂载到容器上
*/
function render(vdom, container) {
  const dom = createDOM(vdom);
  container.appendChild(dom);
}
/* 把虚拟DOM变成真实DOM */
function createDOM(vdom) {
  if (typeof vdom === 'string' || typeof vdom === 'number') {
    return document.createTextNode(vdom);
  }
  // 否则就是一个虚拟DOM对象,也就是React元素;
  let { type, props } = vdom;
  let dom;
  if(typeof type === 'function') {
    console.log('vdom', vdom);
    console.log('type', type);
    return mountFunctionComponent(vdom);
  }else {
     dom = document.createElement(type);
  }
  // 使用虚拟DOM的属性更新刚创建出来的真实DOM的属性
  updateProps(dom, props);
  // 单独在这里处理children
  // 如果只有一个儿子,并且这个儿子是一个虚拟DOM元素
  if (typeof props.children === 'string' || typeof props.children === 'number') {
    dom.textContent = props.children;
  } else if (typeof props.children === 'object' && props.children.type) {
    // 把儿子变成真实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属性放到虚拟DOM,为以后的更新做准备
  return dom;
}

// 把一个类型为自定义函数组件的虚拟DOM转换成为真实DOM并返回;
function mountFunctionComponent(vdom) {//类型为自定义函数组件的虚拟DOM
  let {type: FunctionComponent, props} = vdom;
  let renderVdom = FunctionComponent(props);
  return createDOM(renderVdom);
}

function reconcileChildren(childrenVdom, parentDOM) {
  for (let i = 0; i < childrenVdom.length; i++) {
    let childVdom = childrenVdom[i];
    render(childVdom, parentDOM)
  }
}

function updateProps(dom, newProps) {
  for (let key in newProps) {
    if (key === 'children') continue;
    if (key === 'style') {
      let styleObj = newProps.style;
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else {
      dom[key] = newProps[key];
    }
  }
}
const ReactDOM = { render };
export default ReactDOM;
  • 实现函数组件的渲染
let { type, porps} = vdom;
let dom;
if(typeof vdom  === 'function') {
    return mountFunctionComponent(vdom)
}else {
    dom = document.createElement(type);
}
// 把一个类型为自定义函数组件的虚拟DOM转换成为真实DOM并返回;
function mountFunctionComponent(vdom) {//类型为自定义函数组件的虚拟DOM
  let {type: FunctionComponent, props} = vdom;
  let renderVdom = FunctionComponent(props);
  return createDOM(renderVdom);
}
  • 实现类组件的渲染和更新 github.com/China-forre…
  • 状态的批量更新 我们都知道React类组件中的setState。并且都知道setState在合成事件和钩子函数中是异步的,在原生事件中是同步的。那其原因是什么呢,如何实现的呢?
在react中事件的更新可能是异步的、批量的;调用setState之后状态并没有立即更新,而是缓存起来
了,等事件函数处理完成后再进行批量更新,一次更新并重新渲染,避免了重复刷新组件, 这也是性能优
化的一个小点。
  handlerClick = () => {
    this.setState((lastState) => ({ number: lastState.number + 1 }), () => {
      console.log("callback1", this.state.number);
    });
    console.log("this.state.number", this.state.number);
    this.setState((lastState) => ({ number: lastState.number + 1 }, , () => {
      console.log("callback1", this.state.number);
    }));
    console.log('this.state.number', this.state.number);
    Promise.resolve().then(() => {
      console.log('this.state,number', this.state.number);
      this.setState((lastState) => ({ number: lastState.number + 1 } , () => {
      console.log("callback1", this.state.number);
    }));
      console.log('this.state,number', this.state.number);
      this.setState((lastState) => ({ number: lastState.number + 1 } , () => {
      console.log("callback1", this.state.number);
    }));
      console.log('this.state,number', this.state.number);
    });
  }
// this.state.number: 0 0 2 3 4 (输出结果之后进行的批量更新) 
//批量收集完毕之后进行更新,因此callback: 2  2  3  4 (回调函数式批量更新之后执行输出的结果)
  • 合成事件和批量更新

  • 生命周期 不管是属性变化和状态变化组件都是需要更新的。

现在的函数式组件可以不用从顶部引入import React from 'react

React17以前babel通过React.createElement("div")进行转译;

React17以后: 
因为: 新的runtime transformer
require('react/jsx-runtime')("div")  这样就是自己引入模块了,就不需要外部变量了(import React from 'react'