新手友好React源码,了解虚拟dom、render和useState内部简易实现

328 阅读7分钟

因为公司需求,所以最近一直在学习react,也一直在翻看React源码的blog和一些国外大神的源码解析,最近发现了一个油管上面的一个react源码教程,仅仅只有40分钟,就把render和useState的基础原理讲解出来了,所以,我想将这个视频以文章的方式展示出来,分享给和我一起刚入门react的朋友们

1、项目搭建

此次需要parcel,这是一款打包工具,官网有详细介绍(Parcel v2.6.0 | Parcel 中文网 (parceljs.cn))

//初始化
yarn init -y

//安装parcel
yarn add parcel

安装完事之后就可以进入我们的学习时间了

2、虚拟dom

虚拟dom的定义这里就先不进行描写了,网络上一抓一大把,如果大家需要的话,我可以后面将这一块详细分出一篇文章

我们先在项目的根目录下面创建两个文件

image.png

在index.html中引入index.jsx

//index.html
<script src="./index.jsx"></script>

打开index.jsx,开始正式编写react代码块

const React = {
    createElement:(...args)=>{
        console.log(args)
    }
}

let App = <div>hello</div>

因为我们安装了parcel,他会自动处理.jsx文件。 这时候浏览器中所打印的为

image.png

可以看到,args中有三个数值

第一个为我们这个dom元素的类型,我们写的为div,所以此处打印的也为div

第二个参数为一个对象,我们稍后讨论

第三个参数是我们在div中填写的内容。

我们需要的就是将其变成虚拟dom,

const React = {
    createElement:(tag,props,...children)=>{
        let element = {
            tag,
            props:{
                ...props,
                children
            }
        }
        console.log(element);
    }
}

let App = <div>hello</div>

创建一个element对象,将刚才三个参数进行重新组合,tag不变,将props和children放入props 看到这里,也许你也就知道react的父子组件的通信了。也可以通过props.children将子组件展示出来。

这时候的App我们会发现并不是函数组件而是一个变量, 这里我们将App变成一个函数。并在底部调用它。

const React = {
    createElement: (tag, props, ...children) => {
        if(typeof tag === 'function'){
            return tag(props)
        }
        let element = {
            tag,
            props: {
                ...props,
                children
            }
        }
        return element
    }
}

let App = () => <div>hello</div>;
<App/>

在createElement也需要判断tag是否为函数,如果是函数的话,需要返回这个函数并将props最为参数传入tag中,我们可以将tag打印出来,其实就是React.createElement(div,{....})这样一个类型的函数。

此时,其实我们已经完成了虚拟dom,没错,其实虚拟dom简易理解就是一个对象。

接着我们就可以将这个虚拟dom变成真实dom渲染到页面上面了。

3、render函数

其实我们在学习原生js时,在操作各个节点时候就是操作的原始dom,而render函数就是将我们生成的虚拟dom变成真实dom的一个函数。

接下来我们看代码

//上面的不变
const render = (reactElement,container)  =>{

}
render(<App/>,document.getElementById('root'))

let App = () => <div>hello</div>;
<App/>

可以看见我们创建render函数,那么在react框架中,我们也是会在index.html中创建一个root节点,其实就是一个div id为root的节点。

我们要将App挂载到index.html中root节点上面,就自然需要这两个参数,一个为reactElement,一个为container。

我们需要将reactElement变为真实dom并且挂载到container容器上面

//完整代码
const React = {
    createElement: (tag, props, ...children) => {
        if(typeof tag === 'function'){
            return tag(props)
        }
        let element = {
            tag,
            props: {
                ...props,
                children
            }
        }
        return element
    }
}

const render = (reactElement,container)  =>{
//如果是字符串或者数字的话,我们需要创建TextNode,然后将其放入容器中,返回
    if(['string','number'].includes(typeof reactElement)){
        container.appendChild(document.createTextNode(String(reactElement)))
        return
    }
    //创建对应的真实dom
    actualDomElement = document.createElement(reactElement.tag)
    //遍历虚拟dom除了children的属性,将其放入真实dom中
    if(reactElement.props){
        Object.keys(reactElement.props).filter(key => key !== 'children').forEach(key => {
            actualDomElement[key] = reactElement.props[key]
        })
    }
    //依次遍历每个虚拟dom的孩子节点,将其放入这个真实dom的节点下面
    if(reactElement.props.children){
        reactElement.props.children.forEach(child => render(child,actualDomElement))
    }
    //放入容器中
    container.appendChild(actualDomElement)
}

let App = () => <div>hello</div>;
render(<App/>,document.getElementById('root'))


这样我们就可以在网页中查看到我们写的hello文字了

image.png

4、响应式数据

接下来我们就要实现响应式数据的实现

通俗来讲,响应式数据就是将数据更改后凡是用到这个数据的视图可以进行相应的更新

一般我们在hook中使用的数据都是用以下形式给出的

const [data,setData] = useState()

data就是我们所使用的数据,而setData就是我们要修改数据进行调用的函数

所以useState这个函数实际就是返回一个数组,里面有一个是数据,一个是修改数据的函数

那么接下来我们来进行编写这个函数。

const React = {
    createElement: (tag, props, ...children) => {
        if(typeof tag === 'function'){
            return tag(props)
        }
        let element = {
            tag,
            props: {
                ...props,
                children
            }
        }
        return element
    }
}

const render = (reactElement,container)  =>{
    if(['string','number'].includes(typeof reactElement)){
        container.appendChild(document.createTextNode(String(reactElement)))
        return
    }
    actualDomElement = document.createElement(reactElement.tag)
    if(reactElement.props){
        Object.keys(reactElement.props).filter(key => key !== 'children').forEach(key => {
            actualDomElement[key] = reactElement.props[key]
        })
    }
    if(reactElement.props.children){
        reactElement.props.children.forEach(child => render(child,actualDomElement))
    }
    container.appendChild(actualDomElement)
}
//定义useState函数,返回一个数组state,和setState
const useState = (initialValue) => {
    let state = initialValue
    const setState = (newValue)=>{
    console.log(newValue)//当外部每次调用setName时,都会打印传入的值
    state = newValue
    }
    return [state,setState]
}
let App = () =>{
    const [name , setName] = React.useState('world')
    
    return (
    <div>hello
        <input type="text" value={name} onchange={(e)=>{setName(e.target.value); console.log(name)} } />
    </div>);
}
render(<App/>,document.getElementById('root'))

在input内部我们也定义了onchage函数,当其中内容发生变化时候,就会调用里面的回调函数 。在回调函数中我们先用setName修改name的值,然后将name打印出来,setName函数也会打印修改后的值,我们看一下name和我们传入的newValue是否时同一个值。

image.png

第一行打印的是newValue,第二行打印的是我们调用setName后面的name的值,我们可以很清晰的看到,其实name的值并没有修改,这是为什么呢?

其实这就涉及到js的一个知识:闭包

相信大家都听说过这个概念,这一部分我就不详细说了,以后有机会可能会出一个单独的文章。

简单来讲,我们修改的其实是闭包里面的数据,并没有修改我们所使用的name数据。

所以我们必须要将数据存储到useState外面。

//前面代码相同
const states = []
let stateIndex = 0
const useState = (value) => {
    let FROZENINDEX = stateIndex
    states[FROZENINDEX] = states[FROZENINDEX] || value
    let setState = (newVlaue) => {
        console.log(newVlaue);
        states[FROZENINDEX] = newVlaue
    }
    stateIndex++
    return [states[FROZENINDEX], setState]
}

//后面代码相同

这里每次调用useState时,就会将变量存储在states这个数组里面,当每次变化变量时候,会直接修改states里面的值,所以这回就会拿到name的值是直接从states里面拿到的。要想每次修改name后拿到修改之后的name的值,我们还需要re-render函数,重新渲染组件。

const reRender = () => {
    stateIndex=0
    document.querySelector("#root").firstChild.remove();
    render(<App />, document.getElementById('root'))
}

const states = []
let stateIndex = 0
const useState = (value) => {
    let FROZENINDEX = stateIndex
    states[FROZENINDEX] = states[FROZENINDEX] || value
    let setState = (newVlaue) => {
        console.log(newVlaue);
        states[FROZENINDEX] = newVlaue
        reRender()

    }
    stateIndex++
    return [states[FROZENINDEX], setState]
}

reRender函数实际上就是将原来我们挂载到root节点上面的dom删除,然后将重新渲染并且挂载到root上面,我们在这里时同步执行的,实际上react渲染的时候是进行双缓冲机制进行的,所以它的渲染速度是非常快的。

5、完整代码

let React = {
    createElement: (tag, props, ...children) => {
        if (typeof tag === 'function') {
            return tag(props)
        }
        return {
            tag,
            props: {
                ...props,
                children
            }
        }
    }
}
const render = (reactElement, container) => {
    console.log(reactElement);

    if (['string', 'number'].includes(typeof reactElement)) {
        container.appendChild(document.createTextNode(String(reactElement)))
        return
    }
    let actualElement = document.createElement(reactElement.tag)
    if (reactElement.props) {
        Object.keys(reactElement.props).filter(p => p !== 'children').forEach(p => actualElement[p] = reactElement.props[p])
    }
    if (reactElement.props.children) {
        reactElement.props.children.forEach(element => {
            render(element, actualElement)
        });
    }
    container.appendChild(actualElement)
}


const reRender = () => {
    stateIndex=0
    document.querySelector("#root").firstChild.remove();
    render(<App />, document.getElementById('root'))
}

const states = []
let stateIndex = 0
const useState = (value) => {
    let FROZENINDEX = stateIndex
    states[FROZENINDEX] = states[FROZENINDEX] || value
    let setState = (newVlaue) => {
        console.log(newVlaue);
        states[FROZENINDEX] = newVlaue
        reRender()

    }
    stateIndex++
    return [states[FROZENINDEX], setState]
}

const App = () => {
    const [name, setName] = useState('jack')
    return (
        <div>
            {name}
            <input type="text" value={name} name="name" onchange={(e) => { setName(e.target.value); console.log(name); }} />
            <p>123</p>
            <h1>321</h1>
        </div>
    )
}
render(<App />, document.getElementById('root'))

6、结语

这是第一次分享技术的文章,希望大家可以指出文中错误的点,小弟也会虚心学习大佬们的建议和意见,并积极改正的。