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'){
// typestring代表是原生标签节点
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'){
// typestring代表是原生标签节点
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'){
// typestring代表是原生标签节点
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'){
// typestring代表是原生标签节点
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(山寨版)的主要内容,如果未能解决你的问题,请参考以下文章

AC的故事大结局山寨版(下)

AC的故事大结局山寨版(下)(最大流)

判断当前窗口是否是全屏的山寨版和官方版

山寨版“滴滴”,竟是招嫖软件!

山寨版“滴滴”,竟是招嫖软件!

山寨版“滴滴”,竟是招嫖软件!