react-dom(山寨版)
Posted 大版黑鸡
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了react-dom(山寨版)相关的知识,希望对你有一定的参考价值。
react-dom
使用react也蛮久了,所以本人打算更进一步,了解一下react更深层的东西。
首先,我们知道在cra
(create-react-app
)中是给我们引入了react-dom
这个包的,并且是通过它的render
函数渲染了视图,所以我就想去了解一下这个render
函数到底做了什么,所以在此手写一个简洁版的react-dom
首先我们知道,我们写的jsx都会被编译成虚拟dom,所以我们先修改一下cra
项目的入口文件,这里不使用项目本身的react-dom
,而使用我们自己写的my-react-dom
// /src/index.js
import ReactDom from './my-react-dom'
let jsx = (
<div>
<h1>我是文本内容</h1>
</div>
);
ReactDom.render(
jsx,
document.getElemntById('root')
)
render函数
当然上面这样写肯定是会报错的,所以我们需要在src目录下新建一个my-react-dom.js
并导出一个拥有render
函数的方法,比着葫芦画瓢,我们看到原版的render
函数有两个参数,第一个是jsx也就是我们的虚拟dom,第二个参数是存放dom节点的容器,所以我们可以这么写
// /src/my-react-dom.js
function render(vnode, container){
console.log(vnode)
}
export default {
render
}
虚拟dom
然后在在页面中打开控制台,就发现没有报错了,并且页面上也打印出了我们的jsx编译成的虚拟dom节点,打印的虚拟dom结构大致如下:
{
key:null,
ref:null,
type:'div'
props:{
children:{
key:null,
ref:null,
type:'h1',
props:{
children:'我是文本内容'
}
}
}
}
这里省去了一些不太重要的属性,有兴趣的可以自己试一下。通过打印出来的这个对象我们也就了解虚拟dom的本质了,虚拟dom实际就是js对象,通过js对象来模拟真实的dom节点生成树状的对象也就是我们说的虚拟dom树,更新视图时也就是通过diff算法比较新树和旧树的差异对视图做出更新。
根据打印出来数据我们更了解了虚拟dom,然后我们来完善一下自己写的render
函数。
创建标签节点
按照我们的想法,render
函数需要将虚拟dom渲染成真实的dom节点,然后插入到容器中。所以我们需要一个可以将虚拟dom转化为真实dom的方法,这里我们取名叫createNode
,根据上面的虚拟dom对象我们知道,type
属性就是节点的标签名,我们可以根据这个来创建dom节点,而文本节点则没有type属性,因为考虑到后续还要处理函数组件和类组件,为了方便后续管理,我们将每种不同节点的创建抽离成一个单独的函数在createNode
中分情况调用。
// /src/my-react-dom.js
function render(vnode, container){
console.log(vnode);
// 通过createNode将虚拟dom转化为真实dom
const node = createNode(vnode);
//将真实dom渲染到我们的容器中
container.appendChild(node)
}
//创建节点
function createNode(vnode){
let node;
const {type} = vnode;
if(typeof type === 'string'){
// type是string代表是原生标签节点
node = updateHostComponent(vnode);
}
return node
}
//创建原生标签节点
function updateHostComponent(vnode){
const {type, props} = vnode;
const node = document.createElement(type);
return node
}
export default {
render
}
然后在开发者工具中查看,标签节点发现<div id="root"></div>
下已经多了一个<div></div>
这时候发现其实我们已经将虚拟dom转化成了真实的dom,但是问题就是它只渲染了一个空的<div></div>
标签,而它的子标签并没有渲染,这里我们要注意,上面打印的虚拟dom中children
属性的数据类型是object
但是这是因为我们的虚拟dom只有一个子节点,但是有多个子节点的情况下children
的数据类型其实是Array
,所以我们需要将children
属性进行一下处理,转换成Array
然后统一遍历渲染,我们再添加一个reconcileChildren
方法用来渲染子节点
// /src/my-react-dom.js
...
function updateHostComponent(vnode){
const {type, props} = vnode;
const node = document.createElement(type)
//对子节点进行渲染
reconcileChildren(node, props.children)
return node;
}
function reconcileChildren(parentNode, children){
//不是数组的话转化成数组
const newChildren = Array.isArray(children) ? children, [children];
//对遍历渲染子节点
for(let i = 0; i < newChildren.length; i++){
let child = newChildren[i];
render(child, parentNode)
}
}
...
创建文本节点
在添加完毕上面代码之后,看似非常合理,但是打开页面就会发现,报错了,报错的原因是appendChild
的参数必须是一个节点,而在这里出现了undefined
,所以报错了,那么我们就要检查一下代码,到底这个undefined
是在哪里出现的。
通过检查我们就会发现问题在createNode
这一步,在这个方法中我们是判断type
属性的数据类型是不是string
所以渲染原生标签是没问题的,但是标签里面的文本是没有type
属性的,所以这一步在渲染文本的时候并没有给node
赋值,所以这个时候就返回了一个undefined
,而在这时将undefined
插入到标签内部时就发生了报错,OK,既然知道了问题所在,我们就解决一下这个问题。
解决方案是,如果这个vnode
是字符串或者数字就代表它是文本节点,就创建一个文本节点插入即可
// /src/my-react-dom.js
...
function isStringOrNumber(vnode){
return typeof vnode === 'string' || typeof vnode === 'number';
}
function createNode(vnode){
let node;
const {type} = vnode;
if(typeof type === 'string'){
//代表是原生标签
node = updateHostComponent(vnode)
}else if( isStringOrNumber(vnode) ){
//代表是文本节点
node = updateTextComponent(vnode)
}
return node
}
//创建文本节点
function updateTextComponent(vnode){
const node = document.createTextNode(vnode);
return node
}
...
这时再打开页面,其实我们的页面就可以正常的渲染了,不过在此我们再进一步完善一下函数组件和类组件的渲染
处理函数组件
我们知道,函数组件就是函数将html代码片段返回,所以我们只要执行函数就能获取到组件包含的虚拟dom,按照这个思路我们在createNode
方法中添加一下对函数组件的处理,处理之前先在index.js
中添加一个函数组件
// /src/index.js
import ReactDom from './my-react-dom'
function FC(props){
return (
<div>
函数组件
</div>
)
}
let jsx = (
<div>
<h1>我是文本内容</h1>
<FC/>
</div>
);
ReactDom.render(
jsx,
document.getElemntById('root')
)
然后扩展createNode
方法
// /src/my-react-dom.js
...
function createNode(vnode){
let node;
const {type} = vnode;
if(typeof type === 'string'){
//代表是原生标签
node = updateHostComponent(vnode)
}else if( isStringOrNumber(vnode) ){
//代表是文本节点
node = updateTextComponent(vnode)
}else if(typeof type === 'function'){
//代表是函数组件
node = updateFunctionComponent(vnode)
}
return node
}
//处理函数组件
function updateFunctionComponent(vnode){
const {type, props} = vnode;
const child = type(props);
const node = createNode(child);
return node;
}
...
处理类组件
经过上面的处理之后函数组件也就可以正常的渲染了,接下来我们再处理一下类组件,我们知道js中的类其实还是函数,并且要继承一下React.Component
这个类,那么为什么必须要继承这个类呢,原因就是函数组件和类组件的typeof
都是function
所以需要继承这个类做一个标识好加以区分,首先我们先创建一个Component.js
,来为类组件做一个标识
// /src/Component.js
export default function Component(props){
this.props = props
}
Component.prototype.isReactComponent = {};
然后我们创建的类组件都继承这个Component
我们就可以通过原型上的isReactComponent
属性来对类组件和函数组件加以区分了。
// /src/index.js
import ReactDom from './my-react-dom'
import Component from './Component'
class CC extends Component{
render(){
return (
<div>
类组件
</div>
)
}
}
function FC(props){
return (
<div>
函数组件
</div>
)
}
let jsx = (
<div>
<h1>我是文本内容</h1>
<FC/>
<CC/>
</div>
);
ReactDom.render(
jsx,
document.getElemntById('root')
)
然后扩展createNode
方法
// /src/my-react-dom.js
...
//创建节点
function createNode(vnode){
console.log(vnode)
let node;
const {type} = vnode;
if(typeof type === 'string'){
// type是string代表是原生标签节点
node = updateHostComponent(vnode);
}else if ( isStringOrNumber(vnode) ){
// 文本节点
node = updateTextComponent(vnode);
}else if(typeof type === 'function'){
// 根据isReactComponent判断是不是类组件
node = type.prototype.isReactComponent ?
updateClassComponent(vnode) :
updateFunctionComponent(vnode)
}
return node
}
function updateClassComponent(vnode){
const {type, props} = vnode;
const instance = new type(props);
const child = instance.render();
const node = createNode();
return node;
}
...
到了这里这个渲染看起来好像就挺完善的了,但是我们不要忽略了空标签,在react中我们除了使用一般的html标签之外有时还是用到空标签,而现在我们使用空标签是会报错的,所以我们不能忘了,还要处理一下空标签
处理其他
// /src/my-react-dom.js
...
//创建节点
function createNode(vnode){
console.log(vnode)
let node;
const {type} = vnode;
if(typeof type === 'string'){
// type是string代表是原生标签节点
node = updateHostComponent(vnode);
}else if ( isStringOrNumber(vnode) ){
// 文本节点
node = updateTextComponent(vnode);
}else if(typeof type === 'function'){
// 根据isReactComponent判断是不是类组件
node = type.prototype.isReactComponent ?
updateClassComponent(vnode) :
updateFunctionComponent(vnode)
}else{
//处理其他情况
node = updateFragmentComponent(vnode);
}
return node
}
function updateFragmentComponent(vnode){
const node = document.createDocumentFragment();
reconcileChildren(node, vnode.props.children);
return node;
}
...
这样的话基本就处理完全了,因为我们只是渲染的标签,并没有去对属性做处理,接下来我们再来处理一下标签属性的问题。
处理节点属性
我们处理属性的时候需要先想一下在哪里处理,首先,属性是标签才有的,所以意味着我们只需要在创建原生标签这个函数中处理就可以了
// /src/my-react-dom.js
...
//处理属性
function updateNode(node, nextVal){
Object.keys(nextVal)
.filter(k => k !== 'children')
.forEach(k => {
node[k] = nextVal[k]
})
}
//创建原生标签节点
function updateHostComponent(vnode){
const {type, props} = vnode;
const node = document.createElement(type);
updateNode(node, props)
reconcileChildren(node, props.children)
return node
}
...
至此,一个简易版的react-dom
就书写完毕了,接下来附上全部代码
// /src/my-react-dom
function render(vnode, container){
// console.log(vnode);
// 通过createNode将虚拟dom转化为真实dom
const node = createNode(vnode);
//将真实dom渲染到我们的容器中
container.appendChild(node)
}
//判断是否是文本
function isStringOrNumber(sth){
return typeof sth === 'string' || typeof sth === 'number';
}
//创建节点
function createNode(vnode){
console.log(vnode)
let node;
const {type} = vnode;
if(typeof type === 'string'){
// type是string代表是原生标签节点
node = updateHostComponent(vnode);
}else if ( isStringOrNumber(vnode) ){
// 文本节点
node = updateTextComponent(vnode);
}else if(typeof type === 'function'){
node = type.prototype.isReactComponent ?
updateClassComponent(vnode) :
updateFunctionComponent(vnode)
}else{
node = updateFragmentComponent(vnode)
}
return node
}
function updateNode(node, nextVal){
Object.keys(nextVal)
.filter(k => k !== 'children')
.forEach(k => {
node[k] = nextVal[k]
})
}
//创建原生标签节点
function updateHostComponent(vnode){
const {type, props} = vnode;
const node = document.createElement(type);
updateNode(node, props)
reconcileChildren(node, props.children)
return node
}
//创建文本节点
function updateTextComponent(vnode){
const node = document.createTextNode(vnode);
return node;
}
//处理函数组件
function updateFunctionComponent(vnode){
const {type, props} = vnode;
const child = type(props); //获取函数组件返回的虚拟dom对象
const node = createNode(child); // 将函数组件包含的虚拟dom转化成真实dom
return node;
}
//处理类组件
function updateClassComponent(vnode){
const {type, props} = vnode;
const instance = new type(props);
const child = instance.render();
const node = createNode(child);
return node;
}
function updateFragmentComponent(vnode){
const node = document.createDocumentFragment();
reconcileChildren(node, vnode.props.children);
return node;
}
function reconcileChildren(parentNode, children){
const newChildren = Array.isArray(children) ? children : [children];
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
render(child, parentNode)
}
}
export default {
render
}
以上是关于react-dom(山寨版)的主要内容,如果未能解决你的问题,请参考以下文章