美文网首页
学习并实现react (1)

学习并实现react (1)

作者: coolheadedY | 来源:发表于2018-03-09 18:27 被阅读47次

搭建学习环境

npm install -g parcel-bundler

package.json

{
  "name": "myReact",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "parcel index.html",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "babel-preset-env": "^1.6.1",
    "babel-preset-es2015": "^6.24.1",
    "parcel-bundler": "^1.6.2"
  },
  "devDependencies": {
    "babel-plugin-transform-react-jsx": "^6.24.1"
  }
}

支持 JSX

在 js 文件中我们是不能写 jsx 语法的,必须使用一种 babel 插件 transform-react-jsx 才能使用。
新建.babelrc

{
  "presets": ["env"],
  "plugins": [
    ["transform-react-jsx", {
      "pragma": "createElement"
    }]
  ]
}
这样我们在写React 组件时 babel 会帮我们自动编译成

实现一个 createElement

.babelrc 文件中使用了transform-react-jsx 插件,告诉babel 解析 jsx 需要 createElement方法,也就是 babel 编译后的React.createElement

createElement 有三个参数

function createElement(type, props, ...args) {
    props = props || {}
    let children = []
    for (let i = 0; i < args.length; i++) {
        if (Array.isArray(args[i])) {
          children = [ ...children, ...args[i] ]
        } else {
          children = [ ...children, args[i] ]
        }
    }

  return { type, props, children }
}

然后我们来试验下createElement 结果

import { createElement } from './src/react'

const React = {}
React.createElement = createElement
React.Component = class Component {}

class App extends React.Component {
  render() {
    return (
      <div>
        <span>App</span>
        <span>component</span>
      </div>
    )
  }
}

const app = new App().render()
console.log(app)

new App().render() 这种格式跟 react 组件区别有些大,再实现一个renderVDOM(<App />) 的格式

function reactVDOM(vnode) {
    if (typeof vnode === 'string') return vnode // 文本节点
    if (typeof vnode.type === 'string') { // type 为标签名 - dom节点
        let ret = {
            type: ret.type,
            props: ret.props,
            children: []
        }
        for (let i = 0; i < vnode.children.length; i++) {
            ret.children.push(reactVDOM(vnode.children[i])) // 递归children
        }
        return ret 
    } 
    if (typeof vnode.type === 'function') { // type 为 class 组件
        let func = vnode.type
        let inst = new func(vnode.props) // 把 props 传入
        let innerVNODE = inst.render()
        return reactVDOM(innerVNODE) // 递归渲染后的组件
    }
}
import { createElement } from './src/react'
import { renderVDOM } from './src/renderVDOM'

const React = {}
React.createElement = createElement
React.Component = class Component {}

class App extends React.Component {
  render() {
    return (
      <div>
        <span>App</span>
        <span>component</span>
      </div>
    )
  }
}

const app = renderVDOM(<App />)
console.log(app) // 与 new App().render() 一样

父子组件

class ChildrenChild extends React.Component {
  render() {
    return (
      <div>
        children-child
      </div>
    )
  }
}

class Children extends React.Component {
  render() {
    return (
      <div>
        children
        <ChildrenChild />
      </div>
    )
  }
}

class App extends React.Component {
  render() {
    return (
      <div>
        <span>App</span>
        <span>component</span>
        <Children />
      </div>
    )
  }
}

const app = renderVDOM(<App />)

结果中组件的文本内容、dom、组件实例都在children 数组里,React.render 时只需要识别这些children 就可以做到真实渲染

实现 render

改写 renderVDMO 加入真实 dom 操作


function render(vnode, parent) {
  let dom
  if (typeof vnode === 'string') { // 文本节点直接渲染
    dom = document.createTextNode(vnode)
    parent.appendChild(dom)
  }
  
  if (typeof vnode.type === 'string') { // dom 节点
    dom = document.createElement(vnode.type)
    setAttrs(dom, vnode.props) // props 已经被createElement 解析成对象
    parent.appendChild(dom)

    for(let i = 0; i < vnode.children.length; i++) {
      render(vnode.children[i], dom) // 递归 render children
    }
  }

  if (typeof vnode.type === 'function') { // class 组件
    let func = vnode.type
    let inst = new func(vnode.props) // props 已经被createElement 解析成对象
    let innerVNode = inst.render()
    render(innerVNode, parent)
  }
}

function setAttrs(dom, props) {
  Object.keys(props).forEach(k => {
    const v = props[k]
    
    if (k === 'className') {
      dom.setAttribute('class', v)
      return
    }

    if (k === 'style') {
      if (typeof v === 'string') dom.style.cssText = v
      if (typeof v === 'object') {
        for (let i in v) {
          dom.style[i] = v[i]
        }
      }
      return
    }

    if (k[0] === 'o' && k[1] === 'n') { // onClick of onClickCapture
      const capture = k.indexOf('Capture') !== -1
      dom.addEventListener(k.replace('Capture', '').substring(2).toLowerCase(), v, capture)
      return
    }

    dom.setAttribute(k, v)
  })
}

把上面例子换一下

// app.js
class ChildrenChild extends React.Component {
  render() {
    return (
      <div>
        children-child
      </div>
    )
  }
}

class Children extends React.Component {
  render() {
    return (
      <div>
        children
        <ChildrenChild />
      </div>
    )
  }
}

class App extends React.Component {
  render() {
    return (
      <div>
        <span>App</span>
        <span>component</span>
        <Children />
      </div>
    )
  }
}

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

// index.html
<body>
  <div id="app"></div>
  <script src="./app.js"></script>
</body>
结果

实现props 和state

class Color extends React.Component {
  render() {
    return (
      <div style={{ color: this.props.color }}>color is: {this.props.color}</div>
    )
  }
}

const colorArr = ['red', 'blue', 'black', 'green', 'gray']
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      color: 'black'
    }
  }
  handleClick() {
    console.log("handleClick")
    this.setState({
      color: colorArr[Math.random() * 5 | 0]
    })
  }
  render() {
    return (
      <div onClick={this.handleClick.bind(this)}>
        <Color color={this.state.color} />
      </div>
    )
  }
}
render(<App />, document.getElementById('app'))

目的: 我们要通过点击 App 组件中的元素来改变 Color 文字的颜色
步骤: 把新的state 传入 this.setState 来更新组件 - 调用render 方法

setState

export default class Component {
  constructor(props) {
    this.props = props
  }

  setState(state) {
    setTimeout(() => {
      this.state = state
      // ...render()
    })
  }
}

回忆 render 函数有两个参数,vnodeparentvnode 我们可以使用 this.render() 获取当前组件,但我们要知道需要更新dom 内容的 parent 就需要在首次render 时记录。

改写 render

给render 增加参数,comp(当前更新组件), olddom(当前组件曾经的dom)
拿首次渲染举例: parent - document.getElementById('app'), comp - <App />, olddom - 当App组件更新时就是App 首次渲染的dom

export function render(vnode, parent, comp, olddom) {
  let dom
  if (typeof vnode === 'string') { // 文本节点直接渲染
    dom = document.createTextNode(vnode)
    comp && (comp.__rendered = dom)

    if (olddom) parent.replaceChild(dom, olddom)
    else parent.appendChild(dom)
  }

  if (typeof vnode.type === 'string') { // dom 节点
    dom = document.createElement(vnode.type)

    comp && (comp.__rendered = dom)
    setAttrs(dom, vnode.props) // props 已经被createElement 解析成对象

    if (olddom) parent.replaceChild(dom, olddom)
    else parent.appendChild(dom)

    for(let i = 0; i < vnode.children.length; i++) {
      render(vnode.children[i], dom, null, null) // 递归 render children
    }
  }

  if (typeof vnode.type === 'function') { // class 组件
    let func = vnode.type
    let inst = new func(vnode.props) // props 已经被createElement 解析成对象

    comp && (comp.__rendered = inst) // 这里记录的是 Component 实例

    let innerVNode = inst.render()
    render(innerVNode, parent, inst, olddom)
  }
}

在这里我们每次render 的时候都会判断这次render 是否是class 组件触发的render,如果是组件触发的render 我们就会在这个组件comp 上增加 __rendered 记录当前渲染的 dom 或 当前渲染的组件 (组件追溯到顶层也是dom) ,这时候我们需要一个方法来获得olddom

function getDOM (comp) {
  let rendered = comp.__rendered
  while (rendered instanceof Component) {
    rendered = rendered.__rendered
  }
  return rendered
}
实现 setState
import { getDOM } from './util'
import { render } from './render'

export default class Component {
  constructor(props) {
    this.props = props
  }

  setState(state) {
    setTimeout(() => {
      this.state = state
      const vnode = this.render()
      let olddom = getDOM(this) // 获取渲染此实例的 olddom
      render(vnode, olddom.parentNode, this, olddom)
    })
  }
}
实现效果
import { render, createElement, Component } from './src/code1/react'

const React = {}
React.createElement = createElement
React.Component = Component


class Color extends React.Component {
  render() {
    return (
      <div style={{ color: this.props.color }}>color is: {this.props.color}</div>
    )
  }
}

const colorArr = ['red', 'blue', 'black', 'green', 'gray']
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      color: 'black'
    }
  }
  handleClick() {
    console.log("handleClick")
    this.setState({
      color: colorArr[Math.random() * 5 | 0]
    })
  }
  render() {
    return (
      <div onClick={this.handleClick.bind(this)}>
        <Color color={this.state.color} />
      </div>
    )
  }
}

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

相关文章

  • 学习并实现react (1)

    搭建学习环境 package.json 支持 JSX 在 js 文件中我们是不能写 jsx 语法的,必须使用一种 ...

  • 学习并实现react (3)

    复用组件 React 组件书写规则 组件可以直接渲染组件组件渲染多个children 时需要用 dom 元素进行包...

  • 学习并实现react (4)

    实现生命周期 生命周期介绍 React 生命周期图 React 子组件在父组件下的生命周期流程 实现 compon...

  • 学习并实现react (2)

    组件列表渲染场景 DOM复用 react 的重点在于首次渲染和更新渲染,现在考虑更新渲染如何复用DOM 让渲染更有...

  • 第二十五周打卡总结

    本周计划回顾 上周的计划是:1.学习react技术栈(react、routes、redux),并输出相关总结。(1...

  • 如何创建React组件并发布到npm?

    实现步骤: 创建React组件项目; 创建测试项目并引用组件; 发布React组件到npm上; 一、创建React...

  • 2019-12-19 React Blob 文件下载乱码处理

    最近开始着手React 学习。前后端分离,实现EXCEL导出; 1. 后端easypoi 导出excel文件实现方...

  • 学习react no.1

    学习react no.1 react 定义 react 特点 react

  • Redux学习讲解

    Redux学习讲解 大神地址:从0到1实现一个Redux 借鉴地址:官网地址 react全家桶包括react、we...

  • 【RN】- 撸个登录页

    简介 React Native源码分析这篇文章算是学习RN跨平台开发的开始,文章简单对RN源码进行了分析,并实现了...

网友评论

      本文标题:学习并实现react (1)

      本文链接:https://www.haomeiwen.com/subject/dszyfftx.html