看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/...)

项目地址

组件即函数

在上一篇 JSX 和 Virtual DOM 中,解释了 JSX 渲染到界面的过程并实现了相应代码,代码调用如下所示:

import React from 'react'
import ReactDOM from 'react-dom' const element = (
<div className="title">
hello<span className="content">world!</span>
</div>
) ReactDOM.render(
element,
document.getElementById('root')
)

本小节,我们接着探究组件渲染到界面的过程。在此我们引入组件的概念,组件本质上就是一个函数,如下就是一段标准组件代码:

import React from 'react'

// 写法 1:
class A {
render() {
return <div>I'm componentA</div>
}
} // 写法 2:无状态组件
const A = () => <div>I'm componentA</div> ReactDOM.render(<A />, document.body)

<A name="componentA" /> 是 JSX 的写法,和上一篇同理,babel 将其转化为 React.createElement() 的形式,转化结果如下所示:

React.createElement(A, null)

可以看到当 JSX 中是自定义组件的时候,createElement 后接的第一个参数变为了函数,在 repl 打印 <A name="componentA" />,结果如下:

{
attributes: undefined,
children: [],
key: undefined,
nodeName: ƒ A()
}

注意这时返回的 Virtual DOM 中的 nodeName 也变为了函数。根据这些线索,我们对之前的 render 函数进行改造。

function render(vdom, container) {
if (_.isFunction(vdom.nodeName)) { // 如果 JSX 中是自定义组件
let component, returnVdom
if (vdom.nodeName.prototype.render) {
component = new vdom.nodeName()
returnVdom = component.render()
} else {
returnVdom = vdom.nodeName() // 针对无状态组件:const A = () => <div>I'm componentsA</div>
}
render(returnVdom, container)
return
}
}

至此,我们完成了对组件的处理逻辑。

props 和 state 的实现

在上个小节组件 A 中,是没有引入任何属性和状态的,我们希望组件间能进行属性的传递(props)以及组件内能进行状态的记录(state)。

import React, { Component } from 'react'

class A extends Component {
render() {
return <div>I'm {this.props.name}</div>
}
} ReactDOM.render(<A name="componentA" />, document.body)

在上面这段代码中,看到 A 函数继承自 Component。我们来构造这个父类 Component,并在其添加 state、props、setState 等属性方法,从而让子类继承到它们。

function Component(props) {
this.props = props
this.state = this.state || {}
}

首先,我们将组件外的 props 传进组件内,修改 render 函数中以下代码:

function render(vdom, container) {
if (_.isFunction(vdom.nodeName)) {
let component, returnVdom
if (vdom.nodeName.prototype.render) {
component = new vdom.nodeName(vdom.attributes) // 将组件外的 props 传进组件内
returnVdom = component.render()
} else {
returnVdom = vdom.nodeName(vdom.attributes) // 处理无状态组件:const A = (props) => <div>I'm {props.name}</div>
}
...
}
...
}

实现完组件间 props 的传递后,再来聊聊 state,在 react 中是通过 setState 来完成组件状态的改变的,后续章节会对这个 api(异步)深入探究,这里简单实现如下:

function Component(props) {
this.props = props
this.state = this.state || {}
} Component.prototype.setState = function() {
this.state = Object.assign({}, this.state, updateObj) // 这里简单实现,后续篇章会深入探究
const returnVdom = this.render() // 重新渲染
document.getElementById('root').innerHTML = null
render(returnVdom, document.getElementById('root'))
}

此时虽然已经实现了 setState 的功能,但是 document.getElementById('root') 节点写死在 setState 中显然不是我们希望的,我们将 dom 节点相关转移到 _render 函数中:

Component.prototype.setState = function(updateObj) {
this.state = Object.assign({}, this.state, updateObj)
_render(this) // 重新渲染
}

自然地,重构与之相关的 render 函数:

function render(vdom, container) {
let component
if (_.isFunction(vdom.nodeName)) {
if (vdom.nodeName.prototype.render) {
component = new vdom.nodeName(vdom.attributes)
} else {
component = vdom.nodeName(vdom.attributes) // 处理无状态组件:const A = (props) => <div>I'm {props.name}</div>
}
}
component ? _render(component, container) : _render(vdom, container)
}

在 render 函数中分离出 _render 函数的目的是为了让 setState 函数中也能调用 _render 逻辑。完整 _render 函数如下:

function _render(component, container) {
const vdom = component.render ? component.render() : component
if (_.isString(vdom) || _.isNumber(vdom)) {
container.innerText = container.innerText + vdom
return
}
const dom = document.createElement(vdom.nodeName)
for (let attr in vdom.attributes) {
setAttribute(dom, attr, vdom.attributes[attr])
}
vdom.children.forEach(vdomChild => render(vdomChild, dom))
if (component.container) { // 注意:调用 setState 方法时是进入这段逻辑,从而实现我们将 dom 的逻辑与 setState 函数分离的目标;知识点: new 出来的同一个实例
component.container.innerHTML = null
component.container.appendChild(dom)
return
}
component.container = container
container.appendChild(dom)
}

让我们用下面这个用例跑下写好的 react 吧!

class A extends Component {
constructor(props) {
super(props)
this.state = {
count: 1
}
} click() {
this.setState({
count: ++this.state.count
})
} render() {
return (
<div>
<button onClick={this.click.bind(this)}>Click Me!</button>
<div>{this.props.name}:{this.state.count}</div>
</div>
)
}
} ReactDOM.render(
<A name="count" />,
document.getElementById('root')
)

效果图如下:

至此,我们实现了 props 和 state 部分的逻辑。

小结

组件即函数;当 JSX 中是自定义组件时,经过 babel 转化后的 React.createElement(fn, ..) 后中的第一个参数变为了函数,除此之外其它逻辑与 JSX 中为 html 元素的时候相同;

此外我们将 state/props/setState 等 api 封装进了父类 React.Component 中,从而在子类中能调用这些属性和方法。

在下篇,我们会继续实现生命周期机制,如有疏漏,欢迎斧正。

项目地址

从 0 到 1 实现 React 系列 —— 2.组件和 state|props的更多相关文章

  1. 从 0 到 1 实现 React 系列 —— 1.JSX 和 Virtual DOM

    看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...

  2. 从 0 到 1 实现 React 系列 —— 5.PureComponent 实现 && HOC 探幽

    本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/...) ...

  3. 从 0 到 1 实现 React 系列 —— 4.setState优化和ref的实现

    看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...

  4. 从 0 到 1 实现 React 系列 —— 3.生命周期和 diff 算法

    看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...

  5. 学习React系列(十)——Render Props

    解决问题:将行为封装,供多个组件使用(在多个组件之间分享某段代码) 组件中的props属性中包含一个"render"属性(该属性为一个返回值为元素的方法),然后在该组件的rende ...

  6. React高阶组件 和 Render Props

    高阶组件 本质 本质是函数,将组件作为接收参数,返回一个新的组件.HOC本身不是React API,是一种基于React组合的特而形成的设计模式. 解决的问题(作用) 一句话概括:功能的复用,减少代码 ...

  7. 说说React组件的State

    说说React组件的State React的核心思想是组件化的思想,应用由组件搭建而成, 而组件中最重要的概念是State(状态). 正确定义State React把组件看成一个状态机.通过与用户的交 ...

  8. React组件的State

    React组件的State 1.正确定义State React把组件看成一个状态机.通过与用户的交互,实现不同状态,然后渲染UI,让用户界面和数据保持一致.组件的任何UI改变,都可以从State的变化 ...

  9. React中父子组件数据传递

    Vue.js中父子组件数据传递:Props Down ,  Events Up Angular中父子组件数据传递:Props Down,  Events  Up React中父子组件数据传递:Prop ...

随机推荐

  1. C++反射机制:可变参数模板实现C++反射

    1. 概要   本文描述一个通过C++可变参数模板实现C++反射机制的方法.该方法非常实用,在Nebula高性能网络框架中大量应用,实现了非常强大的动态加载动态创建功能.Nebula框架在Github ...

  2. Team Services的打包管理

    Team Services的打包管理 概述 Package Management (打包管理)是一种扩展,可以更容易地发现.安装和发布包. 它与Team Services中心如构建功能深度集成,这样打 ...

  3. Scala并发编程【进阶】

    package com.dingxin.entrance import java.text.SimpleDateFormat import java.util.Date import scala.ac ...

  4. Linux中安装硬盘后对硬盘的分区以及挂载

    我将使用VM来进行模拟 先使用df看下我的电脑硬盘信息: df -h 可以看到只有一个sda1分区装载/boot,还有一个扩展分区 查看dev下的硬盘: 只有一个硬盘(两个分区) 注意: 如果你是ID ...

  5. SQL Server基础之表级触发器

    触发器分为两种,一种与数据表绑定,响应数据表指定动作(insert.delete或update),此处称为表级:一种与数据库本身绑定,响应数据定义语句(主要是CREATE.ALTER 和 DROP 开 ...

  6. shell编程 学好内功(一)

    shell 背景 什么是shell编程 高大上的解释,往往让人摸不住头脑.一句话概括就是:shell编程就是对一堆Linux命令的逻辑化处理. 为什么要会shell编程 举个简单的例子,我们做java ...

  7. nmap参数原理抓包分析

    nmap参数原理抓包分析 实验环境: Nmap7.70 实验步骤: 1.主机发现 2.端口扫描 3.服务版本探测 一.主机发现 主机发现,如果主机活跃,扫描1000个常用的tcp端口 1.Nmap i ...

  8. PE 添加系统管理员账号(域控可加)转

    使用U盘制作一个PE系统,这里推荐老毛桃或者大白菜:开机进入Bios,选择U盘启动:进入U盘启动画面后,选择一个PE系统:进入PE系统后,我们去本机系统盘,将 C:/Windows/System32/ ...

  9. Day 1 For Knowledge Management

    Hi, There: This is my first day to use CNblogs as my personal knowledge management on internet. I wa ...

  10. Linux的基础命令

    -shutdown –h now(root用户才有效)     立即关机 -shutdown –r now(root用户才有效)     立即重新启动计算机 -reboot               ...