先来一个最简单的react代码:
_const element = <h1 title="foo">Hello</h1>_
_const container = document.getElementById("root")_
_ReactDOM.render(element, container)_
第一行是jsx,jsx并不是有效的JavaScript代码,一般由构建工具如babel进行一个转义,大概的流程是将对应的代码属性(比如tag name)拿出来传入createElement中,这里的createElement就是由react提供的。
React.createElement通过传入的参数输出一个对象:
const element = React.createElement(
"h1",
{ title: "foo" },
"Hello"
)
元素具备2个基本的属性:type和props(暂时只算这2个)
const element = {
type:'div',
props:{
class:'nb',
children:'hello'
}
}
type表示元素标签(tagName)如div.h1之类。
props表示元素的属性如children.class.style之类。
上面的children属性是一个string,但是一般情况下是一个元素数组。
然后就是React.render(下划线代码第三行),它将我们创建的元素放到DOM里面。
所以我们只要用我们自己的方法代替createElement和render的功能,我们就实现了一个最基本的react。
接下来我们用babel转义传入的参数:
const element = {
type:'div',
props:{
class:'nb',
children:'hello',
}
}
基于上面的条件创建一个dom元素:
const node = document.createElement(element.type)
node['class'] = element.props.class
然后再给children也创建一个文本元素:
const text = document.createTextNode("")
text['nodeValue'] = element.props.children
下一步,我们将text放到node里面,再将node放到dom中:
node.appendChild(text)
container.appendChild(node)
到这里,我们就已经脱离react的方法自己完成了同样的功能(简易)。
接下来进一步解析,createElement拿到的参数是这样的:
const element = React.createElement(
'div',
{id:'nb'},
React.createElement('h1')
)
我们需要的参数是这样的(其实和react的createElement是一样的):
function createElement(type, props, ...children) {
return {
type,
prosp:{
...props,
children
}
}
}
这里面的children是一个元素数组,我们要对字符作特殊处理:
function createElement(type, props, ...children) {
return {
type,
prosp:{
...props,
children:children.map(child=>{
typeof child === 'object'
?child:createTextElement(child)
})
}
}
}
function createTextElement(child){
return {
type:'TEXT_ELEMENT',
props:{
nodeValue:text,
children:[]
}
}
}
现在我们基本的方法创建成功,来试着代替掉React吧!
const myReact = {
createElement
}
const element = myReact.createElement('div', {id:'nb'}, myReact.createElement('h1'))
到这里,我们目前还是用这js实现,如果用JSX的话,我们就得告诉babel来做转义。
/** @jsx myReact.createElement */
我们在代码的最上面加这一行,那么babel就会将我们myReact.createElement参数里面的jsx转化成type.props形式啦。
额,上面createElement差不多了,来看看render函数。
ReactDOM.render(element, container)
先写个自己的框架:
/** @jsx myReact.createElement */
function render(element, container){
//....
}
const myReact = {
createElement,
render
}
const element = (
<div id='nb'><h1>nnn</h1></div>
)
const container = document.getElementById('root')
myReact.render(element, container)
render函数作用是将元素挂到dom上面:
function render(element, container){
const dom = document.createElement(element.type)
container.appendChild(dom)
}
这里考虑element是不是字符元素:
function render(element, container){
const dom = element.type === 'TEXT_ELEMENT'
?document.createTextNode("") : document.createElement(element.type)
container.appendChild(dom)
}
再考虑element里面的children:
function render(element, container){
const dom = element.type === 'TEXT_ELEMENT' ?document.createTextNode("") : document.createElement(element.type)
element.props.children.forEach(child=> render(child)
)
container.appendChild(dom)
}
然后我们将props里面的属性除了children全部都挂到对应的element上:
function render(element, container){
const dom = element.type === 'TEXT_ELEMENT'
?document.createTextNode("") : document.createElement(element.type)
Object.keys(element.props)
.filter(key=>key!=='children')
.forEach(name=>{
dom[name] = element.props[name]
})
element.props.children.forEach(child=>
render(child)
)
container.appendChild(dom)
}
差不多,简单的一个render就完成啦!
但是现在有一个问题,一旦dom树太大,造成dom渲染占主线程时间过长,浏览器就会卡顿,不能做到无感知渲染dom,所以我们将细分每个节点的渲染为一个单位,只在浏览器空闲的时候执行单位任务。
let nextUnitOfWork = null;
function workLoop(deadline) {
let shouldYield = false
while(nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining()<1
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
function performUnitOfWork(){...}
这样的话,只有浏览器有空闲时间的情况下才会执行更新dom节点了,这里面的performUnitOfWork函数需要返回下一个渲染unit。
接下来就是unit怎么个执行流程(顺序)了。我们可以引用fiber tree模式,每个unit是一个fiber(也就是每个节点是一个fiber)。
这里我们重新定义render函数(不进行递归渲染rest节点):
function render(element, container){ ... }
function createDom (fiber) {
const dom = fiber.type === 'TEXT_ELEMENT'
?document.createTextNode('')
:document.createElement(fiber.type)
Object.keys(fiber.type).filter(key=> key !=== children )
.forEach(name => {dom[name] = fiber.props[name]})
return dom
}
在render函数里面我们开启一个root fiber:
let nextUnitOfWork = null
function render(element, container){
nextUnitOfWork = {
dom: container,
props: {
children:[element]
}
}
}
然后我们在浏览器有空闲时间的时候调用workLoop函数时,nextUnitOfWork就有值了:
requestIdleCallback(workLoop)
来看下performUnitOfWork函数:
function performUnitOfWork ( fiber ) {
if(!fiber.dom) { //如果没有dom就用前面的函数create一个
fiber.dom = createDom(fiber)
}
if(fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)//这里挂载dom节点(root节点除外)
}
//这里要给fiber(nextUnitOfWork)赋值还有给新的fiber赋parent属性
//然后返回
const elements = fiber.props.children //子元素集合
let index = 0
let prevSibling = null //放置children中除第一个之外的兄弟元素
while( index < elements.length ) {
const element = elements[index]
const newFiber = {
type:element.type,
props:element.props,
parent:fiber//上面是parent.dom添加所以这里直接写fiber
dom:null//这里也可以用create创建但是上面已经写了
}
if(index === 0){
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber //?这一步很迷惑1
index++
}
if(fiber.child) {
return fiber.child
}
//?这里迷惑2
let nextFiber = fiber
while (nextFiber) {
if(nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
至此,performUnitOfWork完成!
我们每一个unit渲染一个节点,当浏览器没有多余时间给我们的时候渲染就会被中断,这会出现不完整的UI。我们先把appendChild这里删除,再新增一个wipRoot:
let wipRoot = null
let nextUnitOfWork = null
function render(element, container) {
wipRoot = {
dom:element,
props:{
children:[container]
}
nextUnitOfWork = wipRoot
}
在我们完成所有节点的添加之后全部一起渲染:
function workLoop (deadline) {
let shouldYield = false
while(nextUnitOfWork && !shouldYield){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1
}
if(!nextUnitOfWork && wipRoot){
commitRoot();
}
requestIdleCallback(workLoop)
}
function commitRoot() { ... }//在这里将所有节点添加到dom中