const hostConfig = { getPublicInstance(...args) { console.log('getPublicInstance', ...args); }, getChildHostContext(...args) { console.log('getChildHostContext', ...args); }, getRootHostContext(...args) { console.log('getRootHostContext', ...args); }, appendChildToContainer(...args) { console.log('appendChildToContainer', ...args); }, prepareForCommit(...args) { console.log('prepareForCommit', ...args) }, resetAfterCommit(...args) { console.log('resetAfterCommit', ...args) }, createInstance(...args) { console.log('createInstance', ...args) }, appendInitialChild(...args) { console.log('appendInitialChild', ...args) }, finalizeInitialChildren(...args) { console.log('prepareUpdate', ...args) }, shouldSetTextContent(...args) { console.log('shouldSetTextContent', ...args) }, shouldDeprioritizeSubtree(...args) { console.log('shouldDeprioritizeSubtree', ...args); }, createTextInstance(...args) { console.log('createTextInstance', ...args); }, scheduleDeferredCallback(...args) { console.log('scheduleDeferredCallback', ...args); }, cancelDeferredCallback(...args) { console.log('cancelDeferredCallback', ...args); }, shouldYield(...args) { console.log('shouldYield', ...args); }, scheduleTimeout(...args) { console.log('scheduleTimeout', ...args); }, cancelTimeout(...args) { console.log('cancelTimeout', ...args); }, noTimeout(...args) { console.log('noTimeout', ...args); }, now(...arg){ console.log('now',...args); }, isPrimaryRenderer(...args) { console.log('isPrimaryRenderer', ...args); }, supportsMutation(...args) { console.log('supportsMutation', ...args); }, supportsPersistence(...args) { console.log('supportsPersistence', ...args); }, supportsHydration(...args) { console.log('supportsHydration', ...args); }, }
把now 修改成Date.now 记录成当前时间
getRootHostContext 返回一个对象,里面返回一个对象,传递给子组件!。
getChildHostContext 同样返回一个对象,向下传递
shouldSetTextContent 返回boolean,返回真则开始createInstance,返回false则createTextInstance,
finalizeInitialChildren 返回boolean,主要判断是否autofocus
appendInitialChild 挂载DOM到父级dom上
prepareForCommit 初次渲染时,已经构建好所有dom节点,马上就要挂在到div#root上了,react-dom中暂时禁止事件触发,我们就直接写一个空函数。 更新时即将准备更新DOM
appendChildToContainer 就是将组件挂载到当前容器节点上
resetAfterCommit 此时已经将挂载完成,react-dom中解除事件的禁止,我们还是直接写空函数。 更新DOM后。
prepareUpdate
var updatePayload = prepareUpdate(instance, type, oldProps, newProps, rootContainerInstance, currentHostContext); workInProgress.updateQueue = updatePayload; if (updatePayload) { // 标记为待更新 markUpdate(workInProgress); }
commitUpdate 准备更新
今天我们学习的是怎么去写一个自己的renderer,也就是react的渲染器。开始之前,先来了解一下react的三个核心。
- react 暴露了几个方法,主要就是定义component,并不包含怎么处理更新的逻辑。
- renderer 负责处理视图的更新
- reconciler 16版本之后的react从stack reconciler重写为fiber reconciler,主要作用就是去遍历节点,找出需要更新的节点,然后交由renderer去更新视图
先create-react-app建一个项目,然后安装react-reconciler,修改index.js文件,改为用我们自己写的renderer来渲染。
先建立一个文件,就叫renderer吧,怎么写呢,看下react-reconciler的readme.md如下:
import Reconciler from "react-reconciler" const hostConfig = { // ... 这里写入需要使用的函数 }; const MyReconcilerInstance = Reconciler(hostConfig); const MyCustomRenderer = { render(ele, container, callback){ const container = MyReconcilerInstance.createContainer(container, false); } }; export default MyCustomRenderer
然后hostConfig怎么写呢,官方已经给出了完整的列表,如下:
// 这些是渲染需要的 const hostConfig = { getPublicInstance(...args) { console.log('getPublicInstance', ...args); }, getChildHostContext(...args) { console.log('getChildHostContext', ...args); }, getRootHostContext(...args) { console.log('getRootHostContext', ...args); }, appendChildToContainer(...args) { console.log('appendChildToContainer', ...args); }, prepareForCommit(...args) { console.log('prepareForCommit', ...args) }, resetAfterCommit(...args) { console.log('resetAfterCommit', ...args) }, createInstance(...args) { console.log('createInstance', ...args) }, appendInitialChild(...args) { console.log('appendInitialChild', ...args) }, finalizeInitialChildren(...args) { console.log('prepareUpdate', ...args) }, shouldSetTextContent(...args) { console.log('shouldSetTextContent', ...args) }, shouldDeprioritizeSubtree(...args) { console.log('shouldDeprioritizeSubtree', ...args); }, createTextInstance(...args) { console.log('createTextInstance', ...args); }, scheduleDeferredCallback(...args) { console.log('scheduleDeferredCallback', ...args); }, cancelDeferredCallback(...args) { console.log('cancelDeferredCallback', ...args); }, shouldYield(...args) { console.log('shouldYield', ...args); }, scheduleTimeout(...args) { console.log('scheduleTimeout', ...args); }, cancelTimeout(...args) { console.log('cancelTimeout', ...args); }, noTimeout(...args) { console.log('noTimeout', ...args); }, now(...arg){ console.log('now',...args); }, isPrimaryRenderer(...args) { console.log('isPrimaryRenderer', ...args); }, supportsMutation:true, }
然后我们修改App.js文件,简单的写一个计数器,大致如下:
class App extends Component { state = { count: 1 } increment = () => { this.setState((state) => { return { count: state.count + 1 } }) } decrement = () => { this.setState((state) => { return { count: state.count - 1 } }) } render() { const { count } = this.state; return ( <div> <button onClick={this.decrement}> - </button> <span>{count}</span> <button onClick={this.increment}> + </button> </div> ) } }
打开浏览器看一下发现并没有渲染出任何东西,打开console,这些函数的调用顺序如下图,好的,那我们开始写这些函数:
- now 这个函数是用来返回当前时间的,所以我们就写成Date.now 就可以了。
- getRootHostContext 这个函数可以向下一级节点传递信息,所以我们就简单的返回一个空对象。
// rootContainerInstance 根节点 我们这里就是div#root getRootHostContext(rootContainerInstance){ return {} }
- getChildHostContext 这个函数用来从上一级获取刚才那个函数传递过来的上下文,同时向下传递,所以我们就接着让他返回一个空对象
/** * parentHostContext 从上一级节点传递过来的上下文 * type 当前节点的nodeType * rootContainerInstance 根节点 */ getChildHostContext(parentHostContext, type, rootContainerInstance){ return {} }
- shouldSetTextContent 这个函数是用来判断是否需要去设置文字内容,如果是在react native中就始终返回false,因为文字也需要去单独生成一个节点去渲染。这里我们写简单点,不去考虑dangerouslySetInnerHTML的情况,就直接判断children是不是字符串或者数字就可以了
/* * type 当前节点的nodeType * props 要赋予当前节点的属性 */ shouldSetTextContent(type, props) { return typeof props.children === 'string' || typeof props.children === 'number' },
- createInstance 这个函数就是要生成dom了。
/** * type 当前节点的nodeType * newProps 传递给当前节点的属性 * rootContainerInstance 根节点 * currentHostContext 从上级节点传递下来的上下文 * workInProgress 当前这个dom节点对应的fiber节点 */ createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress) { const element = document.createElement(type); for (const key in newProps) { const val = newProps[key]; if (key === 'className') { element.className = val } else if (key === 'style') { element.style = val } else if (key.startsWith('on')) { const eventType = key.slice(2).toLowerCase(); element.addEventListener(eventType, val); } else if (key === 'children') { if (typeof val === 'string' || typeof val === 'number') { const textNode = document.createTextNode(val); element.appendChild(textNode) } } else { element.setAttribute(key, val) } } return element },
- finalizeInitialChildren 这个函数用来决定当前节点在commit阶段是否无法完成某些功能,需要在确定dom节点已经挂载上之后,才能去完成这个功能,其实主要就是要判断autofocus,所以我们就简单的判断一下是否有这个属性就可以了
/** * domElement 当前已经生成的dom节点 * type nodeType * props 属性 * rootContainerInstance 根节点 * hostContext 上下级传递过来的上下文 */ finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) { return !!props.autoFocus },
- appendInitialChild 这个就是用来将刚刚生成的节点挂载到上一层节点的函数。
/** * parentInstance 上一级节点 * child 子节点 */ appendInitialChild(parentInstance, child) { console.log('appendInitialChild', [parentInstance, child]); parentInstance.appendChild(child); }
- prepareForCommit 这个函数调用的时候,我们的dom节点已经生成了,即将挂载到根节点上,在这里需要做一些准备工作,比如说禁止事件的触发,统计需要autofocus的节点。我们就不需要了,直接写一个空函数就可以了。
// rootContainerInstance 根节点 prepareFomCommit(rootContainerInstance){ }
- appendChildToContainer 这个就是将生成的节点插入根节点的函数了。
// container 我们的根节点 // child 已经生成的节点 appendChildToContainer(container, child){ container.appendChild(child) }
- resetAfterCommit 这个函数会在已经将真实的节点挂载后触发,所以我们还是写一个空函数。
resetAfterCommit(){ }
好了,现在我们的初次渲染已经大功告成了。接下来是更新。
- prepareUpdate 这个函数用来统计怎么更新,返回一个数组代表需要更新,如果不需要更新就返回null。然后返回的数组会返回给当前dom节点对应的fiber节点,赋予fiber节点的updateQueue,同时将当前fiber节点标记成待更新状态。
/** * domElement 当前遍历的dom节点 * type nodeType * oldProps 旧的属性 * newProps 新属性 * rootContainerInstance 根节点 * hostContext 从上一级节点传递下来的上下文 */ prepareUpdate(domElement, type, oldProps, newProps, rootContainerInstance, hostContext) { console.log('prepareUpdate', [...arguments]); let updatePayload = null; for (const key in oldProps) { const lastProp = oldProps[key]; const nextProp = newProps[key]; if (key === 'children') { if (nextProp != lastProp && (typeof nextProp === 'number' || typeof nextProp === 'string')) { updatePayload = updatePayload || []; updatePayload.push(key, nextProp); } } else { // 其余暂不考虑 } }; return updatePayload }
- commitUpdate 这个函数就是已经遍历完成,准备更新了。
/** * domElement 对应的dom节点 * updatePayload 我们刚才决定返回的更新 * type nodeType * oldProps 旧的属性 * newProps 新属性 * internalInstanceHandle 当前dom节点对应的fiber节点 */ commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) { for (var i = 0; i < updatePayload.length; i += 2) { var propKey = updatePayload[i]; var propValue = updatePayload[i + 1]; if (propKey === 'style') { } else if (propKey === 'children') { domElement.textContent = propValue; // 其余情况暂不考虑 } else { } } },
woola!更新也完成了。