手写react简易版 fiber

基本概念

手写react简易版fiber 源码地址

react 描述页面是使用jsx来描述的,经过babel编译后会生成 React.createElement 的函数。
在这里插入图片描述
上面的 jsx会转换成对应的 createElement 代码。
在这里插入图片描述
然后生成createElement函数被调用返回 vdom。

{
    "children": [
        {
            "type": "li",
            "props": {
                "children": [
                    {
                        "type": "TEXT",
                        "props": {
                            "nodeValue": "1",
                            "children": []
                        }
                    }
                ]
            }
        },
        {
            "type": "li",
            "props": {
                "children": [
                    {
                        "type": "TEXT",
                        "props": {
                            "nodeValue": "2",
                            "children": []
                        }
                    }
                ]
            }
        },
        {
            "type": "li",
            "props": {
                "children": [
                    {
                        "type": "TEXT",
                        "props": {
                            "nodeValue": "3",
                            "children": []
                        }
                    }
                ]
            }
        }
    ]
}

这个就是 vdom 生成的流程,也就是React 16 之前的架构,之后最大的区别就是 16 之后 引入了 fiber,又基于 fiber 实现了 hooks, 天天都听着说 fiber,那fiber到底是什么呢?它和vdom又是什么关系呢?

jsx 编译

react的jsx是依赖于babel生成的createElement函数的
打开项目 执行 npm install --save-dev @babel/core @babel/cli @babel/preset-react
分别是下载了babel的核心库,babel命令行工具cli,babel编译jsx的预设
新建.babelrc 文件

{
    "presets": [
      [
        "@babel/preset-react",
        {
          "pragma": "my.createElement"
        }
      ]
    ]
  }

执行 npx babel index.js -d build 打包代码。
上面就是配置好了 jsx代码编译,pragma是自定义名称,这里就使用自己的createElement

实现my.createElement

我们只要实现createElement函数就可以拿到vdom了

const my = {
    createElement: (type, poprs, ...childer) => {
        return {
            type,
            props: {
                ...poprs ? poprs : {},
                children: childer.map(chiler=> {
                    if (typeof chiler=== 'object') {
                        return chiler;
                    } else {
                    	//文本也有自己对应的vdom
                        return {
                            type: "TEXT",
                            props: {
                                nodeValue: chiler,
                                children: [],
                            },
                        }
                    }
                })
            }
        };
    }
};

渲染出来就是这样

{
    "children": [
        {
            "type": "li",
            "props": {
                "children": [
                    {
                        "type": "TEXT",
                        "props": {
                            "nodeValue": "1",
                            "children": []
                        }
                    }
                ]
            }
        },
        {
            "type": "li",
            "props": {
                "children": [
                    {
                        "type": "TEXT",
                        "props": {
                            "nodeValue": "2",
                            "children": []
                        }
                    }
                ]
            }
        },
        {
            "type": "li",
            "props": {
                "children": [
                    {
                        "type": "TEXT",
                        "props": {
                            "nodeValue": "3",
                            "children": []
                        }
                    }
                ]
            }
        }
    ]
}

先把 vdom 转 fiber,也就是 reconcile 的过程,因为 fiber 是链表,就可以打断,用 schedule 来空闲时调度,最后全部转完之后,再一次性 render,这个过程叫做 commit

schedule实现(调度器,有空闲时继续调度 reconcile)

利用 setTimeout 唤醒 reconcile 协调器

  function reconcile(time){
        const timeSlice = (res) => {
            const start = performance.now();
            do{
            	//它做的事情就是循环处理完所有的 reconcile:
                nextFiberReconcileWork = performNextWork(
                    nextFiberReconcileWork
                );
                //超过 5 毫秒暂停函数
            }while (nextFiberReconcileWork && (performance.now() - start) <= time);
            if(!nextFiberReconcileWork)res()
            if(nextFiberReconcileWork){
            	//休眠这一轮宏任务,唤醒后继续递归执行 reconcile
                setTimeout(()=>{
                    timeSlice(res);
                });
            };
        }
        return new Promise(timeSlice)
    };
    reconcile(5)

给reconcile 5 毫秒执行时间,超过5毫秒使用 setTimeout 休眠任务,利用nextFiberReconcileWork 全局变量保存当前 fiber 下一轮reconcile 继续使用 nextFiberReconcileWork 协调 fiber

如果全部都转完了,那就 commit

	reconcile(5).then(()=>{
		if (!nextFiberReconcileWork) {
		    commitRoot();
		}
	})

schedule 的代码就是这样的

	let nextFiberReconcileWork = null;
	let wipRoot = null;
	  function reconcile(time){
        const timeSlice = (res) => {
            const start = performance.now();
            do{
            	//它做的事情就是循环处理完所有的 reconcile:
                nextFiberReconcileWork = performNextWork(
                    nextFiberReconcileWork
                );
                //超过 5 毫秒暂停函数
            }while (nextFiberReconcileWork && (performance.now() - start) <= time);
            if(!nextFiberReconcileWork)res()
            if(nextFiberReconcileWork){
            	//休眠这一轮宏任务,唤醒后继续递归执行 reconcile
                setTimeout(()=>{
                    timeSlice(res);
                });
            };
        }
        return new Promise(timeSlice)
    };
    reconcile(5).then(()=>{
        if(!nextFiberReconcileWork){
            commitRoot(wipRoot);
        };
    })

每次执行的 performNextWork 函数就是 reconcile:

	function performNextWork(fiber) {
		//拼接fiber树
	   reconcile(fiber);
	    if (fiber.child) {
	        return fiber.child;
	    }
	    let nextFiber = fiber;
	    while (nextFiber) {
	        if (nextFiber.sibling) {
	            return nextFiber.sibling;
	        }
	        nextFiber = nextFiber.return;
	    }
	}

reconcile 当前 fiber 节点,然后再按照顺序继续处理 child、sibling,处理完之后回到 return 的 fiber 节点。这样不断的调度 reconcile。这就是 schedule 做的事情:schedule 就是通过给定的时间调度每个 fiber 节点的 reconcile(vdom 转 fiber),全部 reconcile 完了就执行 commit。

reconcile (调度器)

render 函数实现

function render(element, container) {
    wipRoot = {
        dom: container,
        props: {
            children: [element],
        }
    }
    nextFiberReconcileWork = wipRoot
}

创建根 fiber 节点,赋值给 wipRoot,也就是 working in progress 的 fiber root 的意思。并且下一个处理的 fiber 节点指向它,那么下次 schedule 就会调度这个 fiber 节点,开始 reconcile。

reconcile 的实现是这样的:

function reconcile(fiber) {
    if (!fiber.dom) {
        fiber.dom = createDom(fiber)
    }
    reconcileChildren(fiber, fiber.props.children)
}

fiber.props.children 就是 vdom 的子节点,这里的 reconcileChildren 就是把之前的 vdom 转成 child、sibling、return 这样串联起来的 fiber 链表:
循环处理每一个 vdom 的 elements,如果 index 是 0,那就是 child 串联,否则是 sibling 串联。创建出的节点都要用 return 指向父节点:

function reconcileChildren(fiber, child) {
    let i = 0;
    let preSibing = null
    while (i < child.length) {
        const item = child[i]
        const newFiber = {
            return: fiber,
            sibling: null,
            child: null,
            type: item.type,
            props: item.props,
            effectTag: "PLACEMENT"
        };
        if (i === 0) {
            fiber.child = newFiber
        } else {
            preSibing.sibling = newFiber
        };
        preSibing = newFiber
        i++;
    }
}

因为我们只实现渲染,暂时不做 diff 和删除修改,所以这里的 effectTag 都是 placement,也就是新增元素。
通过 schdule 空闲调度这样处理每一个 vdom 转 fiber,就能生成整个 fiber 链表。

所以,这就是 reconcile 做的事情: reconcile 负责 vdom 转 fiber,并且还会准备好要用的 dom 节点、确定好是增、删、还是改,通过 schdule 的调度,最终把整个 vdom 树转成了 fiber 链表。当 fiber 转完了,那么 schdule 调度就进入到了这里

	reconcile(5).then(()=>{
	    if(!nextFiberReconcileWork){
	        commitRoot(wipRoot);
	    };
	})

开始执行 commit:commitcommit 就是对 dom 的增删改,而且比之前 vdom 架构时的渲染还要快,因为 dom 都提前创建了、也知道是增是删还是改了,那剩下的不就很简单了么?我们从根 fiber 开始 commit,并且把 wipRoot 设置为空,因为不再需要调度它了

	function commitRoot() {
	    commitWork(wipRoot.child);
	    wipRoot = null
	}

每个 fiber 节点的渲染就是按照 child、sibling 的顺序以此插入到 dom 中:

	function commitWork(fiber) {
	   let f = fiber;
	    while(f){
	        let domParentFiber = f.return;
	        if(f.dom){
	            domParentFiber.dom.appendChild(f.dom);
	        };
	        if(f.child){
	            f = f.child;
	        }else{
	            const wrapFun = () => {
	                let s = f;
	                while(s){
	                    if(s.sibling){
	                        f = s.sibling;
	                        return;
	                    };
	                    s = s.return;
	                };
	                f = null;
	            }
	            wrapFun()
	        }
	    }
	}

这里每个 fiber 节点都要往上找它的父节点,因为我们只是新增,那么只需要 appendChild 就行

dom 已经在 reconcile 节点就创建好了,当时我们没细讲,现在来看下 dom 创建逻辑:

	function createDom(fiber) {
	    let dom = ''
	    if(fiber.type == "TEXT"){
	        dom = document.createTextNode(fiber.props.nodeValue);
	    }else if(typeof fiber.type === 'function'){
	        const isFunC = f => {
	            let cFiber = f;
	            if (typeof cFiber.type === 'function') {
	              return isFunC(cFiber.type(cFiber.props));
	            } else {
	              return cFiber;
	            }
	          };
	          const cFiber = isFunC(fiber)
	          fiber.props = cFiber.props;
	          dom = createDom(cFiber);
	    }else{
	        dom = document.createElement(fiber.type);
	    }
	    return dom;
	}

就是根据类型创建元素,然后返回dom节点,如果是 fiber.type === ‘function’ 说明是函数组件,只需要拿着 Fiber.type 调用函数,拿到 vdom 继续剩下的流程

组件其实只是换了个方式储存 vdom 函数组件和class组件同理,如果是class组件直接调用render函数就行了。

这样,我们就实现了简易版 React,当然,目前只实现了渲染,页面效果:
在这里插入图片描述

完整代码:

const data = {
    item1: 'bb',
    item2: 'cc'
};
const my = {
    createElement: (type, poprs, ...childer) => {
        const t = childer.reduce((arr, item)=>{
            if(Array.isArray(item)){
                arr.push(...item)
            }else{
                arr.push(item)
            }
            return arr
        }, [])
        return {
            type,
            props: {
                ...poprs ? poprs : {},
                children: t.map(item => {
                    if (typeof item === 'object') {
                        return item;
                    } else {
                        return {
                            type: "TEXT",
                            props: {
                                nodeValue: item,
                                children: [],
                            },
                        }
                    }
                })
            }
        };
    }
};
const App = () => {
    return (
        <ui>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ui>
    )
};
console.log(App())
function render(element, container) {
    let nextFiberReconcileWork = null;
    let wipRoot = null;
    function commitWork(fiber) {
        let f = fiber;
        while(f){
            let domParentFiber = f.return;
            if(f.dom){
                domParentFiber.dom.appendChild(f.dom);
            };
            if(f.child){
                f = f.child;
            }else{
                const wrapFun = () => {
                    let s = f;
                    while(s){
                        if(s.sibling){
                            f = s.sibling;
                            return;
                        };
                        s = s.return;
                    };
                    f = null;
                }
                wrapFun()
            }
        }
    }
    function commitRoot() {
        commitWork(wipRoot.child);
    }
    function reconcileChildren(fiber, child) {
        let i = 0;
        let preSibing = null
        while (i < child.length) {
            const item = child[i]
            const newFiber = {
                return: fiber,
                sibling: null,
                child: null,
                type: item.type,
                props: item.props,
                effectTag: "PLACEMENT"
            };
            if (i === 0) {
                fiber.child = newFiber
            } else {
                preSibing.sibling = newFiber
            };
            preSibing = newFiber
            i++;
        }
    }
    const setAttribute = (dom, key, value) => {
        if (key === 'children') {
            return;
        }
        if (key === 'nodeValue') {
            dom.textContent = value;
        } else {
            dom.setAttribute(key, value);
        }
    };
    function createDom(fiber) {
        let dom = ''
        if(fiber.type == "TEXT"){
            dom = document.createTextNode(fiber.props.nodeValue);
        }else if(typeof fiber.type === 'function'){
            const isFunC = f => {
                let cFiber = f;
                if (typeof cFiber.type === 'function') {
                  return isFunC(cFiber.type(cFiber.props));
                } else {
                  return cFiber;
                }
              };
              const cFiber = isFunC(fiber)
              fiber.props = cFiber.props;
              dom = createDom(cFiber);
        }else{
            dom = document.createElement(fiber.type);
        }
        return dom;
    }
    function reconcile(fiber) {
        let _fiber = fiber;
        if (!_fiber.dom) {
            _fiber.dom = createDom(_fiber)
        };
        reconcileChildren(_fiber, _fiber.props.children)
    }
    function performNextWork(fiber) {
        reconcile(fiber);
        if (fiber.child) {
            return fiber.child;
        }
        let nextFiber = fiber;
        while (nextFiber) {
            if (nextFiber.sibling) {
                return nextFiber.sibling;
            }
            nextFiber = nextFiber.return;
        }
    }
    wipRoot = {
        dom: container,
        props: {
            children: [element],
        },
    }
    nextFiberReconcileWork = wipRoot;
    function timeSlice(time){
        const p = (res) => {
            const start = performance.now();
            do{
                nextFiberReconcileWork = performNextWork(
                    nextFiberReconcileWork
                );
            }while (nextFiberReconcileWork && (performance.now() - start) <= time);
            if(!nextFiberReconcileWork)res()
            if(nextFiberReconcileWork){
                setTimeout(()=>{
                    p(res);
                });
            };
        }
        return new Promise(p)
    };
    timeSlice(5).then(()=>{
        if(!nextFiberReconcileWork){
            commitRoot(wipRoot);
        };
    })
}
render(<App />, document.querySelector("#root"))
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值