如何实现一个mini-react的大概思路

准备工作

git地址 mini-react
如果不能使用请联系我。
这里不做太多 工具的介绍,所以打包工具选用相对简单的parcel

npm install -g parcel-bundler

接下来新建index.jsindex.html,在index.html中引入index.js
然后就是babel的配置
.babelrc

{
    "presets": ["env"],
    "plugins": [
        ["transform-react-jsx", {
            "pragma": "React.createElement"
        }]
    ]
}

这个transform-react-jsx就是将jsx转换成js的babel插件,它有一个pragma项,可以定义jsx转换方法的名称,你也可以将它改成h(这是很多类React框架使用的名称)或别的。

准备完成以后我们就可以用命令parcel index.html将它跑起来了。

jsx

在开始之前要先了解一个概念,在react中render或者函数组件中返回的代码,如下:

const title = <h1 className="title">Hello, world!</h1>;

这段代码并不是合法的js代码,它是一种被称为jsx的语法扩展,通过它我们就可以很方便的在js代码中书写html片段。

本质上,jsx是语法糖,上面这段代码会被babel转换成如下代码

const title = React.createElement(
    'h1',
    { className: 'title' },
    'Hello, world!'
);

有兴趣,可以用babel做个测试 babel测试

React.createElement和虚拟DOM

我们知道所有的jsx代码,都会被转换成React.createElement这种形式
从jsx解析结果来看,createElement方法参数应该是:

createElement(tag,attrs,child1,child2,child3,child4)

第一个参数是DOM元素的标签名,比如说:div,span,h1,main
第二个参数是一个对象,里面包含了所有属性,可能包含className,id等等
第三个参数开始,就是它的子节点。
那我们只要自己一个React全局对象,给它挂载这个React.createElement方法就可以进行接下来的处理:

const React = {};
React.createElement = function(tag, attrs, ...children) {
  return {
    tag,
    attrs,
    children
  };
};
export default React;

我们的createElement方法返回的对象记录了这个DOM节点所有信息,换句话说,通过它我们就可以生成真正的DOM,这个记录信息的对象我们称之为虚拟DOM。
接下来来测试下:

image

输出:
image

打开调试工具,我们可以看到输出的对象和我们预想的一致

处理好了jsx,现在我们来开始处理入口

ReactDOM.render方法是我们的入口
先定义ReactDOM对象,然后看它的render方法~
先看看默认解析是什么样

const ReactDOM={};
ReactDOM.render(
    <h1>Hello, world!</h1>,
    document.getElementById('root')
);

//经过转换
ReactDOM.render(
    React.createElement( 'h1', null, 'Hello, world!' ),
    document.getElementById('root')
);

可以看到ReactDom.render的解析,所以render第一个参数是createElement返回的对象,也就是虚拟DOM,,第二个参数也就是要挂载目标的DOM。
也就是说render方法就把虚拟dom对象-js对象变成真实dom对象,然后插入到根标签内。
然后开始定义:

const ReactDom = {};
const render = function(vnode, container) {
  return container.appendChild(_render(vnode));
};
ReactDom.render = render;
思路: 先把虚拟dom对象-js对象变成真实dom对象,然后插入到根标签内。

_render方法,接受虚拟dom对象,返回真实dom对象:
如果传入的是null,字符串或者数字 那么直接转换成真实dom然后返回就可以了~,如果是其他类型要做一些特殊处理,看代码把。

import handleAttrs from './handleAttrs';
import {createComponent,setComponentProps} from '../components/utills';
const ReactDom = {};
//传入虚拟dom节点和真实包裹节点,把虚拟dom节点通过_render方法转换成真实dom节点,然后插入到包裹节点中,这个就是react的初次渲染
const render = function (vnode, container) {
    return container.appendChild(_render(vnode));
};
ReactDom.render = render;
export function _render(vnode) {
    console.log('reactDom render', vnode);
    if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {//做个防错
        vnode = '';
    }
    if (typeof vnode === 'number') {
        vnode = String(vnode);
    }
    if (typeof vnode === 'string') {//如果是最里面一层或者就是个字符串的话,转化成Node类型,遵从appendChild要求
        let textNode = document.createTextNode(vnode);
        return textNode;
    }
    if (typeof vnode.tag === 'function') {//是一个组件<app></app>这种  babel会给我们做转换 vnode.tag得类型
        //hooks
        const component = createComponent(vnode.tag, vnode.attrs);//组件类型的话,创建组件
        setComponentProps(component, vnode.attrs);//设置组件属性,并且转换为真实dom,component.base里存着真实dom
        return component.base;
    }
    // vnode= {tag,props,children}
    // {tag:"li",attrs:{xxx:},children:1}   这几种情况都排除之后,那就是html元素了
    const dom = document.createElement(vnode.tag);
    if (vnode.attrs) {//但是有可能传入的是个div标签,而且它有属性。那么需要处理属性,由于这个处理属性的函数需要大量复用,我们单独定义成一个函数:   
        Object.keys(vnode.attrs).forEach(key => {
            const value = vnode.attrs[key];
            handleAttrs(dom, key, value);//如果有属性,例如style标签、onClick事件等,都会通过这个函数,挂在到dom上
        });
    }
    vnode.children && vnode.children.forEach(child => render(child, dom)); // 有子集的话,递归渲染子节点
    return dom;
}

export default ReactDom;

但是可能有子节点(babel转的结构,可以下代码输出看下,完事会把git链接放上来)的嵌套,于是要用到递归(就在上面代码得结尾):

 vnode.children && vnode.children.forEach(child => render(child, dom));

如果要是html元素标签,而且它有属性。那么需要处理属性,由于这个处理属性的函数需要大量复用,我们单独定义成一个函数(上面handleAttrs就是这个setAttribute得导出):

export default function setAttribute(dom, name, value) {
    if (name === 'className') name = 'class';
    if (/on\w+/.test(name)) {
      name = name.toLowerCase();
      dom[name] = value || '';
    } else if (name === 'style') {
      if (!value || typeof value === 'string') {
        dom.style.cssText = value || '';
      } else if (value && typeof value === 'object') {
        for (let name in value) {
          dom.style[name] =
            typeof value[name] === 'number' ? value[name] + 'px' : value[name];
        }
      }
    } else {
      if (name in dom) {
        dom[name] = value || '';
      }
      if (value) {
        dom.setAttribute(name, value);
      } else {
        dom.removeAttribute(name);
      }
    }
  }
  

然后处理上面说的组件类型了,加入新一个新的处理方式:

我们先定义好Component这个类,并且挂载到全局React的对象上

import { enqueueSetState } from './setState';
export class Component {
    constructor(props = {}) {
        this.state = {};
        this.props = props;
    }
    setState(stateChange){
        console.log('state')
      
        enqueueSetState(newState, this);//异步合并state,并更新组件得方法
    }
}

image

组件类型 babel也会通过React.crateElement()来给我们做转换,下面就是实现_render里所用到得createComponent创建和setComponentProps设置属性以及相关得renderComponent根据虚拟DOM生成真实DOM加载等方法。

import { Component } from './component';
export function createComponent(component, props) {
    let inst;
    //如果是类定义组件component是function,直接返回实例。
    if (component.prototype && component.prototype.render) {
        inst = new component(props);//实例化
    } else {//如果是函数组件
        inst = new Component(props);
        inst.constructor = component; //把函数组件储存下,方便统一render调用
        inst.render = function () {
            this.constructor(props);//调用函数
        }
    }
    return inst;//返回组件实例
}
export function setComponentProps(component, props) {//设置属性,并执行部分生命周期
    if (!component.base) {//首次加载
        if (component.componentWillMount) component.componentWillMount();//执行将要装载生命周期
    } else if (component.base && component.componentWillReceiveProps) {//props变化
        component.componentWillReceiveProps(props);
    }

    component.props = props;//props变化了,重新赋值
    renderComponent(component);//生成真实dom  
}
export function renderComponent(component) {
    console.log('renderComponent');
    let base;
    //调用render方法,返回jsx,通过createElement返回虚拟dom对象 这里会用到state 此时的state已经通过上面的setState时队列合并 更新了
    const renderer = component.render();
    if (component.base && component.shouldComponentUpdate) {//优化用得生命周期,根据返回值确定是否继续要渲染
        let result = true;
        result = component.shouldComponentUpdate({}, component.newState);//props得处理有点问题,后续要改进下
        if (!result) {
            return;
        }
    }

    if (component.base && component.componentWillUpdate) {//组件即将更新得时候触发生命周期函数,首次加载不触发
        component.componentWillUpdate();
    }

    //根据diff算法,得到真实dom对象
    base = diffNode(component.base, renderer);

    if (component.base) {
        if (component.componentDidUpdate) component.componentDidUpdate();//更新完毕生命周期,首次不加载
    } else {
        component.base = base;//base是真实DOM,将本次得真实DOM挂载到组件上,方便判断是否首次挂载。
        base_component = component;//互相引用,方便后续队列处理
        component.componentDidMount && component.componentDidMount();//首次挂载完后,真实dom生成后得生命周期
        return;
    }

    //不是首次加载,挂载真实的dom对象到对应的 组件上 方便后期对比
    component.base = base;

    //不是首次加载,挂载对应到组件到真实dom上 方便后期对比~
    base._component = component;
}

然后就是根据虚拟DOM得到真实DOM得 diff算法

总而言之,我们的diff算法有两个原则:

  • 对比当前真实的DOM和虚拟DOM,在对比过程中直接更新真实DOM
  • 只对比同一层级的变化
import handleAttrs from '../reactDom/handleAttrs';
import {setComponentProps,createComponent} from '../components/utills';
/**
 * @param {HTMLElement} dom 真实DOM
 * @param {vnode} vnode 虚拟DOM
 * @param {HTMLElement} container 容器
 * @returns {HTMLElement} 更新后的DOM
 */
export function diffNode(dom, vnode) {//当前得dom 真实DOM,当前得vnode 虚拟DOM
    let out = dom;
    if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {
        vnode = '';
    }
    if (typeof vnode === 'number') {
        vnode = String(vnode);
    }

    if (typeof vnode === 'string') {//diff  text node  文本节点
        if (dom && dom.nodeType === 3) {//当前dom就是文本节点
            if (dom.textContent !== vnode) {//如果内容和虚拟dom不一样,更改
                dom.textContent = vnode;
            }
        } else {//如果当前dom不是文本节点,创建一个新的文本节点,并更新;
            out = document.createTextNode(vnode);
            if (dom && dom.parentNode) {
                //https://www.w3school.com.cn/jsref/met_node_replacechild.asp
                dom.parentNode.replaceChild(out, dom);
            }
        }
        return out;//更新完,返回
    }

    if (typeof vnode.tag === 'function') {//因为会递归调用,如果某一次调用传入得是组件类型,也就是说调用过程中有一层得节点是组件,就对比组件更新
        return diffComponent(dom, vnode);
    }
    console.log(dom, vnode)
    if (!dom || !isSameNodeType(dom, vnode)) {//如果真实DOM要是不存在,或者当前元素标签层级有变化得话
        out = document.createElement(vnode.tag);

        if (dom) {
            [...dom.childNodes].map(item => out.appendChild(item)); // 将原来的子节点移到新节点下
            console.log(out, 'out', dom.childNodes)
            if (dom.parentNode) {
                dom.parentNode.replaceChild(out, dom); // 移除掉原来的DOM对象
            }
        }
    }

    if (
        (vnode.children && vnode.children.length > 0) ||
        (out.childNodes && out.childNodes.length > 0)
    ) {//如果 有子集的话,对比子集更新, 两个都要判断一种虚拟dom有子集,一种是虚拟DOM没子集,但是真实dom有子集
        diffChildren(out, vnode.children);
    }

    diffAttributes(out, vnode);//更新属性
    return out;

}
export function diffAttributes(dom, vnode) {
    const old = {}; // 当前DOM的属性
    const attrs = vnode.attrs; // 虚拟DOM的属性

    for (let i = 0; i < dom.attributes.length; i++) {
        const attr = dom.attributes[i];
        old[attr.name] = attr.value;
    }

    // 如果原来的属性不在新的属性当中,则将其移除掉(属性值设为undefined)
    for (let name in old) {
        if (!(name in attrs)) {
            handleAttrs(dom, name, undefined);
        }
    }

    // 更新新的属性值
    for (let name in attrs) {
        if (old[name] !== attrs[name]) {
            handleAttrs(dom, name, attrs[name]);
        }
    }
}
function diffChildren(dom, vchildren) {//父级真实dom,和虚拟DOM子集
    const domChildren = dom.childNodes;//真实DOM子集  和vchildren对应 同层次对比
    //没有key得真实DOM集合
    const children = [];
    //有key得集合
    const keyed = {};
    if (domChildren.length > 0) {//真实DOM有子集
        for (let i = 0; i < domChildren.length; i++) {
            const child = domChildren[i];
            const key = child.key;
            if (key) {//根据有没有key,把子集分一下
                keyed[key] = child;
            } else {
                children.push(child);
            }
        }
    }
    if (vchildren && vchildren.length > 0) {//虚拟dom子集有
        let min = 0;
        let childrenLen = children.length;//无key得长度
        for (let i = 0; i < vchildren.length; i++) {//循环虚拟dom
            const vchild = vchildren[i];
            const key = vchild.key;
            let child;
            if (key) {//有key
                if (keyed[key]) {//从真实dom里找一下,看有没有
                    child = keyed[key];//储存下
                    keyed[key] = undefined;//清空
                }
            } else if (min < childrenLen) {//否则没有key,从没key里找,并且开始childrenLen不是0
                for (let j = min; j < childrenLen; j++) {
                    let c = children[j];

                    if (c && isSameNodeType(c, vchild)) {//用真实DOM和虚拟DOM比对一下看是不是同一个节点类型和值相等,包括组件得比对
                        child = c;//是同一个找到了,存一下
                        children[j] = undefined;//清空下
                        if (j === childrenLen - 1) childrenLen--;//做下标记,这个元素找过了,下次略过这个元素min也一样
                        if (j === min) min++;
                        break;
                    }
                }
            }

            child = diffNode(child, vchild);//当前这项真实DOM和虚拟DOM做一个比对,更新如果里面还有子集又会调用diffChildren,返回真实Dom
            const f = domChildren[i];//获取原来真实dom集合中得当前项
            console.log(child, f)
            if (child && child !== f) {//如果比对完得child和当前这个f不一样
                if (!f) {//如果不存在,reacrDom第一次render时,直接添加到父级里
                    dom.appendChild(child);
                } else {//child 已经从真实dom找过一轮,并且和虚拟DOM对比生成过的了。
                    // dom.insertBefore(child, f);
                    // dom.removeChild(f);
                    dom.replaceChild(child, f);//替换掉
                }
            }

        }
        if (dom) {
            if (childrenLen > vchildren.length) {//删除多余节点
                for (let i = vchildren.length; i < childrenLen; i++) {
                    dom.removeChild(children[i]);
                }
            }
        }
    }
}

function isSameNodeType(dom, vnode) {//这个方法很多地方用到
    if (typeof vnode === 'string' || typeof vnode === 'number') {
        return dom.nodeType === 3 && dom === String(vnode); //查看是否是文本节点
    }
    if (typeof vnode.tag === 'string') {
        return dom.nodeName.toLowerCase() === vnode.tag.toLowerCase(); //查看当前层级是否标签换了
    }
    return dom && dom._component && dom._component.constructor === vnode.tag;//在renderComponent做过互相引用,
    //通过createComponent方法里实例化处理根据constructor判断是否是vbode.tag得实例,如果不是当前层级 组件更换了,
    //diffChildren里找对应组件时会用到这里
}
function diffComponent(dom, vnode) {
    let c = dom && dom._component;
    let oldDom = dom;
    // 如果组件类型没有变化,则重新set props
    if (c && c.constructor === vnode.tag) {
        setComponentProps(c, vnode.attrs);
        dom = c.base;
        // 如果组件类型变化,则移除掉原来组件,并渲染新的组件
    } else {
        if (c) {
            unmountComponent(c);
            oldDom = null;
        }

        c = createComponent(vnode.tag, vnode.attrs);

        setComponentProps(c, vnode.attrs);
        dom = c.base;

        if (oldDom && dom !== oldDom) {
            oldDom._component = null;
            removeNode(oldDom);
        }
    }
    return dom;
}

function removeNode(dom) {
    if (dom && dom.parentNode) {
        dom.parentNode.removeChild(dom);
    }
}

这上面里的是整套得diff算法,包含了对各种类型得处理以及子集得处理,可以跟着代码仓库例子跑一跑,自己手写一下,diffChildren会稍微复杂一点。
shouldComponentUpdate中得props第一个参数有点问题。有兴趣得同学可以看看看着在代码里做一下优化
剩下就是优化setState得合成
优化使用requestAnimationFrame实现。
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行


import { renderComponent } from './utills';

/**
 * 队列   先进先出 后进后出 ~
 * @param {Array:Object} setStateQueue  抽象队列 每个元素都是一个key-value对象 key:对应的stateChange value:对应的组件
 * @param {Array:Component} renderQueue  抽象需要更新的组件队列 每个元素都是Component
 */
const setStateQueue = [];
const renderQueue = [];
function defer(fn) {
    //requestIdleCallback的兼容性不好,对于用户交互频繁多次合并更新来说,requestAnimation更有及时性高优先级,requestIdleCallback则适合处理可以延迟渲染的任务~
    //   if (window.requestIdleCallback) {
    //     console.log('requestIdleCallback');
    //     return requestIdleCallback(fn);
    //   }
    //高优先级任务 异步的 先挂起  
    //告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
    return requestAnimationFrame(fn);
}
export function enqueueSetState(stateChange, component) {
    //第一次进来肯定会先调用defer函数
    if (setStateQueue.length === 0) {
        //清空队列的办法是异步执行,下面都是同步执行的一些计算
        defer(flush);
    }
    // setStateQueue:[{state:{a:1},component:app},{state:{a:2},component:test},{state:{a:3},component:app}]

    //向队列中添加对象 key:stateChange value:component
    setStateQueue.push({
        stateChange,
        component
    });
    //如果渲染队列中没有这个组件 那么添加进去
    if (!renderQueue.some(item => item === component)) {
        renderQueue.push(component);
    }
}

function flush() {//下次重绘之前调用,合并state
    let item, component;
    //依次取出对象,执行 
    while ((item = setStateQueue.shift())) {
        const { stateChange, component } = item;

        // 如果没有prevState,则将当前的state作为初始的prevState
        if (!component.prevState) {
            component.prevState = Object.assign({}, component.state);
        }
        let newState;
        // 如果stateChange是一个方法,也就是setState的第二种形式
        if (typeof stateChange === 'function') {
            newState = Object.assign(
                component.state,
                stateChange(component.prevState, component.props)
            );
        } else {
            // 如果stateChange是一个对象,则直接合并到setState中
            newState = Object.assign(component.state, stateChange);
        }
        component.newState = newState;
        component.prevState = component.state;
    }
    //先做一个处理合并state的队列,然后把state挂载到component下面 这样下面的队列,遍历时候,能也拿到state属性
    //依次取出组件,执行更新逻辑,渲染
    while ((component = renderQueue.shift())) {
        renderComponent(component);
    }
}

有兴趣的同学,可以拉下来代码自己改改看看呦。
window.requestAnimationFrame

文章参考
从零自己编写一个React框架 【中高级前端杀手锏级别技能】
hujiulong的博客

未完待续

阅读 173

推荐阅读

世界核平

0 人关注
17 篇文章
专栏主页