构建自己的react

   转载自唐鼎的博客建造属于你的react(译文),原作者是Rodrigo Pombo的博文Build your own React,本文已获得转载权限,如果需要转载请联系原作者或者唐鼎。
我们将一步一步重建一个属于我们自己的react。我们的react架构将和真实的react架构相同,但是去掉了大部分的优化和一些目前不必要的功能。

1.createElement (生成虚拟dom)

   我们来实现一个我们自己的createElement,首先是把JSX替换成createElement函数
   我们先大致写一个目录,然后只装个bable来将jsx转成createElement

npm init -y
npm install --save-dev @babel/core @babel/cli @babel/preset-react

   根目录下新建个bable.config.json,把 ↓粘进去

{
  "presets": ["@babel/preset-react"]
}

   然后在package.json里写个脚本 对应build命令,运行他来让bable编译代码

“build”: “babel main.jsx --watch --out-dir build”
npm run build

   大致如下
在这里插入图片描述

const App = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
React.render(element, container)

  App被转换后会成这样↓

const App = React.createElement(
	'div',
	{id: 'foo'},
	React.createElement('a',null,'bar'),
	React.createElement('b')
)

  我们来写几个例子

// 使用 createElement("div"),返回:
{
  "type": "div",
  "props": { "children": [] }
}
// 使用 createElement("div", {foo: 'bar'}, a), 返回:
{
  "type": "div",
  "props": { "children": [a],"foo": "bar" }
}

// 使用 createElement("div", null, a, b),返回:
{
  "type": "div",
  "props": { "children": [a, b] }
}

  children数组除了dom之外,还可能包含一些基本数据类型,如数字或字符串,我们用TEXT_ELEMENT来表示

function createElement(type, props, ...children) {
	return {
		type,
		props: {
			...props,
			children: children.map(child => 
			typeof child === 'object'
			? child
			: createTextNode(child))
		}
	}
}

function createTextNode(text) {
	return {
		type:"TEXT_ELEMENT",
		props:{
			nodeValue: text,
			children: []
		}
	}
}

const React = {createElement}

   在实际的react中是不会把基本类型包装成对象,我这么做是为了后边简化代码

2.render函数

   我们来完成自己的ReactDom.render函数
   现在先只考虑将虚拟dom渲染到真实dom上,不考虑更新和删除
   先将element上的type转换成dom,然后塞入到容器中,根据这个规律来递归的调用render完成渲染

function render (element, container) {
	const { type,props } = element;
	const { children } = props;
	const dom = document.createElement(type)
	children.forEach(child => {
		render(child, dom)
	})
	container.appendChild(dom)
}

   还需要单独处理type 为 TEXT_ELEMENT 的文本节点 修改如下

function render (element, container) {
	// ...省略已有代码
	const dom = type === 'TEXT_ELEMENT'
	? document.createTextNode('')
	: document.createElement(type)
	// ...省略已有代码
}

   最后我们还需要把 虚拟dom上的props 同步到dom上,完整代码如下↓

function render (element, container) {
	const { type,props } = element;
	const { children } = props;

	const dom = type === 'TEXT_ELEMENT'
	? document.createTextNode('')
	: document.createElement(type);
	
	// ------处理props
	const isProperty = key => key !== "children"
	Object.keys(props)
	.filter(isProperty)
	.forEach(name => dom[name] = props[name])
	// ------处理props结束
	
	children.forEach(child => {
		render(child, dom)
	})
	container.appendChild(dom)
}
const React = {createElement,render}

到这里之后,我们就完成了一个简单的将jsx转换成真实dom。启动index.html就能看到。

3.Concurrent Mode

   一旦开始把虚拟dom转成真实dom,我们在整个递归过程完成之前不能停止,如果 虚拟dom树过于庞大,这个渲染过程就会占用主线程过长时间,如果在这个时候浏览器需要去处理一些高响应的操作(比如用户输入)将在完成渲染前产生卡顿

   因此我们要把工作拆成一个个小的单元,每个单元完成后查看一下浏览器是否有更重要的任务,如果有我们就打断当前的渲染循环。

let nextUnitOfWork = null; // 下一个工作单元

function wookLoop(deadline) {
	let shouldYield = false;
	while (nextUnitOfWork && !shouldYield) {
		nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
		shouldYield = deadline.timeRemaining() < 1;
	}
	requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

// 处理工作单元
function performUnitOfWork (nextUnitOfWork) {
// TODO
}

   我们用requestIdleCallback这个浏览器的api来完成循环,可以把这个api理解成 setTimeout类似的功能(把任务放在当前微任务最后)但是不同的是requestIdleCallback会在浏览器主线程空闲的时候触发,而不是像setTimeout制定一个执行时间
   requestIdleCallback提供了deadline参数,我们可以用它来确认在浏览器接管线程前我们我们有多少时间。
   React不适用这个api,自己完成了一套调度器

  1. 浏览器兼容性不好
  2. 他的FPS只有20,人眼在FPS低于60的时候会感觉到卡顿,可以这么理解:FPS 20 就是一秒刷20张图 (20帧),1帧用时50毫秒(1000 / 20),也就是间隔50毫秒才刷新一次(1秒只触发20次),远远低于流畅度的要求

requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it’s not really useful for UI work。—— from Releasing Suspense

   为了实现上面的循环,我们需要完成performUnitOfWork函数,他除了执行一个小单元的工作外,还需要返回下一个需要被执行的单元

4.Fibers

   为了实现一个单元一个单元的工作,我们需要引入fiber的数据结构,一个虚拟dom对应一个fiber结构,一个fiber结构对应一个单元的工作。
   来看一个例子,有这么一个需要渲染的元素树

React.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

  上面结构映射成 fiber 树后大体为下图结构:
请添加图片描述

  在render 函数中我们需要穿件rootFiber(根fiber),将其设置为nextUnitofWork,剩下的工作将在performUnitOfWork函数中完成,我们将在每一个fiber中做3件事:

  1. 给fiber生成对应的dom
  2. 给fiber的子节点们创建fiber
  3. 返回下一个工作单元
      fiber有一个重要的功能就是能够轻易的找到下一个工作单元,这就是为什么fiber有指向第一个子节点和第一个兄弟节点和自己父节点的原因,当我们完成当前fiber的工作后,fiber的child属性可以直接指向下一个需要进行工作的子节点。
      如果fiber没有子节点了我们就使用sibling属性去找他兄弟节点作为下一个工作单元。
      当fiber既没有child也没有sibling,我们让fiber去找他叔叔(父亲的兄弟节点),如果fiber的父节点没有兄弟节点,我们继续往上找父节点的兄弟节点,直到根节点,当我们到达根节点的时候,这就意味着着一次render 我们完成了所有工作
      现在我们按照这个思路来在代码中实现一下。
      首先我们移除到render函数中的代码,将其移到createDom中,后续需要适用
function createDom (fiber) {
	const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type)const isProperty = key => key !== "children"
	  Object.keys(fiber.props)
	    .filter(isProperty)
	    .forEach(name => {
	      dom[name] = fiber.props[name]
	    })return dom
}

function render (element, container) {
	// TODO 生成rootFiber
}

   在render函数中我们设置nextUnitOfWork为fiber root 节点

function render (element, container) {
	nextUnitOfWork = {
		dom: container,
		props: {
			children:[element] // 我们都用数组后边就不用刻意去区分数据类型了
		}
	}
}

let nextUnitOfWork = null;

   这样当浏览器空闲的时候 requestIdleCallback就会触发,执行workLoop开始在root节点上工作
   接下来我们来完成 performUnitOfWork

function performUnitOfWork (fiber) {
// 生成dom节点
	if (!fiber.dom) {
		fiber.dom = createDom(fiber)
	}
	if (fiber.parent) {
	    fiber.parent.dom.appendChild(fiber.dom)
	}
// TODO 处理fiber的子节点们生成fiber然后建立fiber链
// TODO 返回下一个工作单元
}

   然后我们循环给所有子节点创建fiber节点,我们把这些子节点根据是否为第一个子节点添加到 =fiber rootchild或者childsibling上面。

function performUnitOfWork (fiber) {
// ---省略创建dom
// 处理fiber的子节点们生成fiber然后建立fiber链

	 const elements = fiber.props.children
	let index = 0;
	let prevSibling = null;
	
	while(index < elements.length) {
		const element = elements[index]
		const newFiber = {
			dom: null,
			props: element.props,
			parent: fiber,
			type: element.type
		}
		if (index === 0) {
			fiber.child = element
		}else {
			prevSibling = newFiber
		}
		prevSibling = newFiber
		index++
	}
// TODO 返回下一个工作单元
}

   最后我们来处理返回下一个工作单元, 先尝试返回child,然后是sibling,再然后是parent的sibling,继续往上直到fiber root

function performUnitOfWork (fiber) {
// --- 省略生成dom节点
// --- 省略处理fiber的子节点们生成fiber然后建立fiber链
//返回下一个工作单元
	if (fiber.child) {
		return fiber.child
	}
	let  nextFiber = fiber;
	while(nextFiber) {
		if (nextFiber.sibling) {
			return nextFiber.sibling
		}
		nexrFiber = nextFiber.parent
	}
}

   目前为止我们的performUnitOfWork就完成,完成的函数如下:

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
  const elements = fiber.props.children
  let index = 0
  let prevSibling = nullwhile (index < elements.length) {
    const element = elements[index]const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

5. Render和Commit 阶段

  我们发现了一个新的问题。
  我们在每一个工作单元中都给其父节点塞入了当前fiber的dom,但这有一个问题,在浏览器比较繁忙的时候会打断我们的工作,这样会程序按出一个不完整UI的渲染,所以我们删除performUnitOfWork中这行添加dom的操作;

function performUnitOfWork(fiber) {
  // ... 省略
  /* 删除 */
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
  /* 删除 */
  // ... 省略
}

  取而代之,我们需要添加名为wipRoot(work in progress root)的fiber来记录fiber节点循环更新后的节点,一旦我们完成所有单元的工作(不存在next unit of work)的时候,我们一次性将fiber树更新到document上。

function commitRoot () {
// TODO add nodes to dom
}

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

let wipRoot = null
let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }// 一次性全部提交
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }requestIdleCallback(workLoop)
}

   我们把这个提交 fiber 树的过程 放在commitRoot 中实现,递归的把节点添加到 document上

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

function commitWork(fiber) {
	if (!fiber) return;
	const domParent = fiber.parent.dom;
	domParent.appendChild(fiber.dom);
	commitWork(fiber.child);
	commitWork(fiber.sibling)
}

6. Reconciliation(调和)

  目前我们只考虑了往document上添加元素,更新和删除却没有做,我们需要比较render函数这次收到的fiber结构和我们上次更新的fiber树有哪些不同。
  因此我们需要在更新完毕后保存一份更新过的fiber树,我们叫它currentRoot。在每一个fiber节点当中我们也添加alternate属性,该属性指向上次更新的fiber节点

function commitRoot() {
  commitWork(wipRoot.child)
  // 添加 currentRoot
  currentRoot = wipRoot
  wipRoot = null
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    // 添加 alternate
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}

let currentRoot = null

   我们现在把performUnitOfWork函数创建新fiber节点部分的代码抽取出来放在reconcileChildren 函数。我们将在reconcileChildren函数中根据老的fiber节点来调和新的react元素

function performUnitOfWork(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }const elements = fiber.props.children
  reconcileChildren(fiber, elements)if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

function reconcileChildren (wipFiber, elements) {
  let index = 0
  let prevSibling = nullwhile (index < elements.length) {
    const element = elements[index]const newFiber = {
      type: element.type,
      props: element.props,
      parent: wipFiber,
      dom: null,
    }if (index === 0) {
      wipFiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

   我们同时循环老的fiber 树的子节点和我们需要调和的新的react节点,此刻只关心oldFIber 和 react element。 react element 是我们想要更新到document上面的元素,oldFiber是我们上次更新完毕的老的fiber 节点。我们需要比较他们,如果前后有任何的改变都需要更新到domcument上面
   我们需要用type来对他们进行比较
   1. 如果老的fiber和react element 都拥有相同的type(dom节点相同),我们只需要更新他的属性
   2. 如果type不同说明这里需要替换成新的dom节点。我们需要创建
   3. 如果type 不同且同级存在old fiber 说明老接地那需要被删除,我们需要移除老的节点
   react源码中还使用了keys来进行调度调和的优化,通过比较key属性可以得到react elements 中被替换的明确位置

 function reconcileChildren (wipFiber, elements){
	let index = 0;
	let prevSibling = null;
	let oldFiber = 
		wipFiber.alternate 
		&& wipFiber.alternate.child;
		while(index < elements.length || oldFiber) {
			const element = elements[index];
			let newFiber = null;
			const sameType = 
				element && oldFiber 
				&& element.type === oldFiber.type;
				if (sameType) {
					// TODO 更新节点
				}
				if (!sameType && element) {
					// TODO 创建节点
				}
				if (!sameType && oldFiber) {
					// TODO 删除节点
				}
				// ...省略已有代码
		}

 }

   我们来完成比较的部分
   当oldFiber 和 react element 拥有相同的type的时候,我们创建一个新的fiber节点来复用老fiber 的 dom节点,然后从react element 上面取到新的props
  我们还给fiber节点新增一个effectTag属性,commit 阶段用这个标识来给节点更新props、新增还是删除。
  更新给 effectTagUPDATE
  当react element 需要创建新的dom节点时候,给effectTagPLACEMENT
   第三种情况是当我们需要删除节点的时候我们不需要新的fiber节点,所有我们给旧的fiber添加 effectTag.但是这样操作,当我们把fiber树上的节点更新到document上时不会用到old fiber 的数据结构,这会导致删除的操作没做,所以我们还需要新添加一个数组,用来留存需要进行删除的旧fiber节点。
  这部分同步到 render 函数和 commitRoot 函数。


function reconcileChildren (wipFiber, elements){
// ... 省略已有代码
	if (sameType) {
		newFiber = {
		 type: oldFiber.type,
		 props: element.props,
		 dom: oldFiber.dom,
		 parent: wipFiber,
		 alternate: oldFiber,
		 errectTag: "UPDATE"
		}
	}

	if (!sameType && element) {
		newFiber = {
			type: element.type,
			props: element.props,
			parent: wipFiber,
			dom: null,
			alternate: null,
			errectTag: "PLACEMENT"
		}
	}
	if (!sameType && oldFiber) {
		oldFIber.effectTag = "DELETION"
		deletions.push(oldFiber)
	}
	// ...省略已有代码
}

function render(element, container) {
	wipRoot = {
		dom: container,
		props:{children: [element]},
		alternate: currentRoot
	}
	// 新增记录删除的数组
	deletions = []
	nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
// 新增记录删除的数组
let deletions = null;

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

   现在让我们来用刚刚添加的effectTag来更改commitWork函数的代码
  当effectTagPLACEMENT 时我们和之前的操作一样,给父fiber节点添加子节点,让为DELETION时我们做相反的操作。
  当effectTagUPDATE 时我们需要在dom节点上更新改变的props属性

function commitWork (fiber) {
	if (!fiber) return;
	
	const domParent = fiber.parent.dom;
	if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
		domParent.appendChild(fiber.dom)
	}else if (fiber.effectTag === 'UPDATE' && fiber.dom !== null) {
		updataDom(fiber.dom, fiber.alternate.props, fiber.props)
	}else if (fiber.effectTag === 'DELETION') {
		domParent.removeCHild(fiber.dom)
	}

	commitWork(fiber.child)
	commitWork(fiber.sibling)
}
function updataDom (dom, prevProps, nextProps) {
	//TODO 更新属性
}

现在我们来完成 updateDom 函数,我们比较新老节点上的props,移除、更新、新增属性。我们还需要对事件监听类的属性做特殊的处理(react中对事件类型统一 on开头),剔除掉on前缀,删除更改的事件,添加新的事件。

const isEvent = key => key.startsWith("on")
const isProperty = key =>
  key !== "children" && !isEvent(key)
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
  // Remove old or changed event listeners
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key =>
        !(key in nextProps) ||
        isNew(prevProps, nextProps)(key)
    )
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener(
        eventType,
        prevProps[name]
      )
    })// Set new or changed properties
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })

  // Add event listeners
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })
}

到此为止我们的Reconciliation就完成了

7. 函数组件

  接下来我们需要增加对函数式组件(function components)的支持。首先我们需要更改例子为简单的函数式组件,它返回一个 h1 元素。

function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
React.render(element, container)

  同样的,我们把它从jsx转化为js:

function App(props) {
  return React.createElement(
    "h1",
    null,
    "Hi ",
    props.name
  )
}
const element = React.createElement(App, {
  name: "foo",
})

   函数组件有两点不同
   1. 函数组件的fiber节点没有保存dom节点
   2. 函数组件的子节点是通过运行函数得到的,而不是props的children中得到的。
   我们通过检查fiber的type是否为function来确定它是否为函数组件从而进行不同的更新,在updateHostComponent 函数中我们仍然进行之前的逻辑进行非函数组件的更新

function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  // 函数式组件进行专门的函数更新
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

   随后在updateFunctionComponent函数中我们运行函数式组件的函数,得到子节点,比如上面的例子,fiber节点的type保存的是App函数,我们运行函数将会得到h1节点。
   一旦当我们得到子节点之后, reconciliation 函数将一样的工作,我们不需要更改任何的部分

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

但是commitWork函数还是需要进行对应的更改的,因为我们现在拥有了没有保存node节点的函数式组件。我们来更改两个地方。

   首先为了找到dom节点的父节点,我们需要一直往上查找fiber树,直到我们找到拥有dom节点的 fiber 节点(类组件)。

   删除节点的时候我们也需要一直往上查找直到找到拥有node节点的fiber节点。

function commitWork(fiber) {
  if (!fiber) {
    return
  }
  let domParentFiber = fiber.parent
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.dom

}

// 更改为找到拥有dom节点的fiber为止
function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

8. Hooks

  最后一步,我们现在给函数式组件增加 state。我们来改变之前的例子,写一个经典的计数器组件。每当我们点击一下,计数将增加1。

function Counter() {
  const [state, setState] = React.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  )
}
const element = <Counter />

   函数式组件在uploadFunctionComponent函数中完成,我们需要在调用函数组件之前初始化一些全局变量,这样可以在useState中使用他们。
   首先我们需要设置一个本次调度的fiber树,同时需要一个保存hooks的数组来支持fiber在一个组件中调用多次useState。还有我们需要保持对当前hook的index的跟踪。
   当函数组件调用useState的时候,我们先在alternate属性上面检查是否拥有老的hook。如果老的hook存在。我们直接复制hook上面的state来给新的hook,如果没有我们初始化一个state。然后我们在fiber上面添加这个新的hook,增加hook的index的追踪,然后返回state。
   useState 还需要返回一个更新state的函数,所以我们来完成一个setState的函数,该函数接受一个action的入参,在上面的计数器的例子中action就是个函数来每次给计数加一。
   我们把action保存到hook新增的一个queue属性中。接着我们做和render函数中类似的事情,新建一个fiber节点,把他设置为nextUnitOfWork(下一个工作单元)。这样在后续的更新中会进行调度更新。
   但是目前为止我们仍然未执行action函数,我们在渲染组件的时候来执行action,我们从queue中得到所有的action,然后一个接一个的执行他们得到新的hook和state。所以我们返回的是已经更新过的state。

let wipFiber = null;
let hookIndex = null;

function updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

function useState(initial) {
	const oldHook = 
	wipFiber.alternate
	&& wipFiber.alternate.hooks 
	&& wipFiber.alternate.hooks[hookIndex];
	
	const hook = {
		state: oldHook ? oldHook.state : inital;
		queue: []
	}
	const actions = oldHook ? oldHook.queue : [];
	actions.forEach(action => hook.state = action(hook.state));
	
	const setState = action => {
		hook.queue.push(action)
		wipRoot = {
			dom: currentRoot.dom,
			props: currentRoot.props;
			alternate: currentRoot,
		}
		nextUnitOfWork = wipRoot;
		deletions = []
	}
	wipFiber.hooks.push(hook);
	hookIndex++
	return [hook.state, setState]
}

整体的代码如下↓


function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  }
}function createDom(fiber) {
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type)updateDom(dom, {}, fiber.props)return dom
}const isEvent = key => key.startsWith("on")
const isProperty = key =>
  key !== "children" && !isEvent(key)
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
const isGone = (prev, next) => key => !(key in next)
function updateDom(dom, prevProps, nextProps) {
  //Remove old or changed event listeners
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key =>
        !(key in nextProps) ||
        isNew(prevProps, nextProps)(key)
    )
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener(
        eventType,
        prevProps[name]
      )
    })// Remove old properties
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })// Set new or changed properties
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })// Add event listeners
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })
}function commitRoot() {
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}function commitWork(fiber) {
  if (!fiber) {
    return
  }let domParentFiber = fiber.parent
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.dom
​
  if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {
    domParent.appendChild(fiber.dom)
  } else if (
    fiber.effectTag === "UPDATE" &&
    fiber.dom != null
  ) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent)
  }commitWork(fiber.child)
  commitWork(fiber.sibling)
}function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  deletions = []
  nextUnitOfWork = wipRoot
}let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = nullfunction workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }requestIdleCallback(workLoop)
}requestIdleCallback(workLoop)function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}let wipFiber = null
let hookIndex = nullfunction updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [],
  }const actions = oldHook ? oldHook.queue : []
  actions.forEach(action => {
    hook.state = action(hook.state)
  })const setState = action => {
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot
    deletions = []
  }
​
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = nullwhile (
    index < elements.length ||
    oldFiber != null
  ) {
    const element = elements[index]
    let newFiber = nullconst sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type
​
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",
      }
    }
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      }
    }
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }if (oldFiber) {
      oldFiber = oldFiber.sibling
    }if (index === 0) {
      wipFiber.child = newFiber
    } else if (element) {
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}const React = {
  createElement,
  render,
  useState,
}function Counter() {
  const [state, setState] = React.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  )
}
const element = <Counter />
const container = document.getElementById("root")
React.render(element, container)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值