react 数组_手写一个 React

项目介绍

原生 JS 实现一个简易的 React , 并且实现基本功能(React 的基本用法): 模块化,组件及组件间通信,setState 等功能

具体实现

先看这张图,了解 React 的基本原理

9a94d6c090433112321d2bbc4617ead0.png

根据这张图,实现其中的逻辑

JSX 与虚拟DOM

全程 JavaScript XML ,是 JavaScript 的语法扩展,看起来像 html。

let element = <h1>hello world</h1>

浏览器不能直接运行,需要工具进行编译

经过工具 https://babeljs.io/repl 转换后

var element = React.createElement("h1", null, "hello world");

可以在浏览器环境下运行,但还需要有有其他代码配合

const React = {}
React.createElement = function() {

}

编译后

var React = {};

React.createElement = function () {};

var element = React.createElement("h1", null, "hello world");

这样就可以在浏览器环境下跑通了

以上只是在沙箱练习环境(直接拷贝)去运行的,如何在真正的工作环境下运行起来呢?

  1. 初始化环境
  2. 当前目录下运行yarn init -y,生成 package.json
  3. 运行yarn add parcel-bundler,使用 parcel 进行打包
  4. 创建 index.html index.js 文件

index.js

const Jreact = {}
Jreact.createElement = function() {
  console.log(arguments)
}
let element = <h1>hello world</h1>

//var element = Jreact.createElement("h1",null,"hello world")
  • 创建 .babelrc(babel的配置文件),因为要使用到 babel
  • 设置 .babelrc
{
  "presets": ["env"], //覆盖范围(最新语法转换成浏览器支持的语法)
  "plugins": [
    ["transform-react-jsx",{
      处理 JSX 的函数名,对jsx 语法进行拆解,作为这个函数的参数,Jreact.createElement("h1", null, "hello world");
      "pragma": "Jreact.createElement" 
    }]
  ]
}
  1. 在 index.html 中引入 index.js
<body>
  <script src="index.js"></script>
</body>
  1. 执行npx parcel index.html,安装 json5 yarn add json5
  2. 打开http://localhost:xxxx

控制台输出Arguments(3) ["h1", null, "hello world", callee: ƒ, Symbol(Symbol.iterator): ƒ]

总结

通过 babel ,里面使用了一些插件,当运行之后,代码自动编译,把 JSX 变成 浏览器可运行的 Javascriptdist 代码,dist 目录下为编译后的代码

测试

把 JSX 写的复杂些,

...
let element = (
  <div className="wrapper">
    <h1>hello {name}</h1>
    <button onClick = {clickBtn}>click</button>
  </div>
)

经过转码后

var element = React.createElement("div", {
  className: "wrapper"
}, React.createElement("h1", null, "hello ", name), React.createElement("button", {
  onClick: clickBtn
}));

React.createElement(标签,{所有的属性},React.createElement(子元素标签,{所有的属性},文本内容),React.createElement(子元素标签,{所有属性}),文本内容)

解析过程中,遇到子元素,再次执行 React.createElement(递归操作)

React.createElement 1. 标签 2. 这个标签上的属性构成的对象 3. 子元素,React.createElement() 执行后,返回的结果

解析中遇到 {} 会当做变量处理,所以变量必须提前声明好,编译后才能运行

const Jreact = {}
Jreact.createElement = function(tag, attrs, ...children) { //es6语法中的数组,所有的子元素放数组里
  return {
    tag,
    attrs,
    children
  }
}

let name = 'zhangsan'
function clickBtn() {
  console.log('click me')
}

let element = (
  <div className="wrapper">
    <h1>hello {name}</h1>
    <button onClick = {clickBtn}></button>
  </div>
)

console.log(element)

输出

35dad165417a0a102d50a96ca3963d99.png

编译过程:

执行 Jreact.createElement 这个函数,JSX 作为参数传递进去, 执行结果得到一个对象,这个对象有 JSX 的层次结构(JSON 对象),这个 element 就叫做虚拟 DOM

有了虚拟 DOM 后,如何放到页面上,变成实体 DOM 呢?

虚拟 DOM 渲染

利用函数去处理,如何写呢

思路:根据 vnode 的结构,需要用到递归,而且子节点可能为对象,可能为字符串,所以需要做两种处理,

function render(vnode, container) { 
  if(typeof vnode === 'string') { //创建文本节点,挂载到容器中
    return container.appendChild(document.createTextNode(vnode))
  }

  if(typeof vnode === 'object') {
    let dom = document.createElement(vnode.tag) 
    setAttribute(dom, vnode.attrs)

    container.appendChild(dom)
  }
}

function setAttribute(dom, attrs) {
  //...
}

设置好标签和属性后,里面还有子元素怎么办呢?

对子元素做遍历,处理每一个子元素:利用 render 函数渲染每一个 vnodeChild, 放入到当前的 dom 中,这样就把所有子节点都放入当前 dom 中了,再把这个 dom 挂载到页面上,

function render(vnode, container) { 
  if(typeof vnode === 'string') { //创建文本节点,挂载到容器中
    return container.appendChild(document.createTextNode(vnode))
  }

  if(typeof vnode === 'object') {
    let dom = document.createElement(vnode.tag) 
    setAttribute(dom, vnode.attrs)
    if(vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(vnodeChild => {
        render(vnodeChild, dom)
      })
    }

    container.appendChild(dom)
  }
}

测试

6e237955a56d4d7411a4ae48f13f93ad.png

b753be2eb4ac4a126f40a210f591bffb.png

但是 dom 节点上并没有属性,如何定义设置属性的函数呢

处理事件绑定

真实 dom 节点上可以直接设置 id class noclick 但是这里是 className, onClick 大写的,该如何设置呢

function setAttribute(dom, attrs) {
  for(let key in attrs) {
    if(/^on/.test(key)) { //对事件绑定的处理,以 on 开头的,dom[onclick] = attrs[onClick]
      dom[key.toLocaleLowerCase()] = attrs[key]
    }
  }
}

把 style 设置成 dom 上的属性

...
let styleObj = {
  color: 'red',
  fontSize: '20px'
}

let vnode = (
  <div className="wrapper">
    <h1 style={ styleObj }>hello {name}</h1>
    <button onClick = {clickBtn}>click me</button>
  </div>
)
...
function setAttribute(dom, attrs) {
  for(let key in attrs) {
    ...
    if(key === 'style') { //对 style 的处理
      dom.style = attrs[key]
    }
  }
}

2c0093f020cf9d78bed75d9a24d5cc49.png

但是并未生效, dom 对象不能直接修改他的 style (直接覆盖,重置是不行的),正确的做法.style.color = 'red'

所以需要修改

function setAttribute(dom, attrs) {
  for(let key in attrs) {
    ...
    if(key === 'style') { //对 style 的处理
      Object.assign(dom.style, attrs[key]) //新增的会赋值到 dom.style 上,同名的属性会覆盖
    }
  }
}

测试

82330b7bf1d6983ea5de089d6541beb6.png

点击click 执行了这个函数,打印出 click me

以上实现了把虚拟 DOM 变成真实 DOM ,挂载到页面上

完整代码

const Jreact = {}
Jreact.createElement = function(tag, attrs, ...children) { //es6语法中的数组,所有的子元素放数组里
  return {
    tag,
    attrs,
    children
  }
}

function render(vnode, container) { 
  if(typeof vnode === 'string') { //创建文本节点,挂载到容器中
    return container.appendChild(document.createTextNode(vnode))
  }

  if(typeof vnode === 'object') {
    let dom = document.createElement(vnode.tag) 
    setAttribute(dom, vnode.attrs)
    if(vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(vnodeChild => {
        render(vnodeChild, dom)
      })
    }

    container.appendChild(dom)
  }
}

function setAttribute(dom, attrs) {
  for(let key in attrs) {
    if(/^on/.test(key)) { //对事件绑定的处理,以 on 开头的,dom[onclick] = attrs[onClick]
      dom[key.toLocaleLowerCase()] = attrs[key]
    } else if(key === 'style') { //对 style 的处理
      Object.assign(dom.style, attrs[key]) //新增的会赋值到 dom.style 上,同名的属性会覆盖
    } else { //其他的直接作为 dom 的属性
      dom[key] = attrs[key]
    }
  }
}


let name = 'zhangsan'
function clickBtn() {
  console.log('click me')
}
let styleObj = {
  color: 'red',
  fontSize: '20px'
}

let vnode = (
  <div className="wrapper">
    <h1 style={ styleObj }>hello {name}</h1>
    <button onClick = {clickBtn}>click me</button>
  </div>
)

console.log(vnode)

render(vnode, document.querySelector('#app')) //虚拟 dom 变成 真实的dom 节点后,挂载到容器上

打造 React 雏形

模拟 React

const Jreact = { //创建元素,组件
  createElement
}

const JreactDOM = { //用于去渲染,做一些其他的事情
  render
}

function createElement(tag, attrs, ...children) { //es6语法中的数组,所有的子元素放数组里
  return {
    tag,
    attrs,
    children
  }
}

function render() { //... }
...
JreactDOM.render((
  <div className="wrapper">
    <h1 style={styleObj}>hello {name}</h1>
    <button onClick={clickBtn}>click me</button>
  </div>
), document.querySelector('#app'))

一样的效果,写法上更像 React

实现计数器功能

做一个计时器,点开始时,开始计时,点停止时,停止计时。

...
let num = 0
let timer = null
let styleObj = {
  color: 'red',
  fontSize: '20px'
}

onStart() //一开始时执行

function onStart() {
  console.log('click me')
  timer = setInterval(() => { //启动时,每秒钟计时一次,做一次渲染
    JreactDOM.render((
      <div className="wrapper">
        <h1 style = { styleObj }>Number: { num }</h1>
        <button onClick = { onStart }>start</button>
        <button onClick = { onPause }>pause</button>
      </div>
    ), document.querySelector('#app'))
  }, 1000)
}

function onPause() {
  clearInterval(timer) //点击停止时,清除定时器
}

问题1:变量 num 没有显示出来

bug 排查

用编译工具编译 JSX 代码,解析 num 时,执行 render 函数,执行 render 时,做了判断,如果 typeof 为 string, 传递的num 不是字符串而是数字,所以需要修改判断逻辑

function render(vnode, container) {
  if (typeof vnode === 'string' || typeof vnode === 'number') {  //如果是 string 或者 nubmer 都去创建文本节点
    return container.appendChild(document.createTextNode(vnode))
  }
  ...
}

问题2:计时时,页面上会渲染出很多dom

const Jreact = { //创建元素,组件
  createElement
}

const JreactDOM = { //用于去渲染,做一些其他的事情
  render
}

function createElement(tag, attrs, ...children) { //es6语法中的数组,所有的子元素放数组里
  return {
    tag,
    attrs,
    children
  }
}

function render(vnode, container) {
  if (typeof vnode === 'string' || typeof vnode === 'number') { //如果是 string 或者 nubmer 都去创建文本节点
    return container.appendChild(document.createTextNode(vnode))
  }

  if (typeof vnode === 'object') {
    let dom = document.createElement(vnode.tag)
    setAttribute(dom, vnode.attrs)
    if (vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(vnodeChild => {
        render(vnodeChild, dom)
      })
    }

    container.appendChild(dom)
  }
}

function setAttribute(dom, attrs) {
  for (let key in attrs) {
    if (/^on/.test(key)) { //对事件绑定的处理,以 on 开头的,dom[onclick] = attrs[onClick]
      dom[key.toLocaleLowerCase()] = attrs[key]
    } else if (key === 'style') { //对 style 的处理
      Object.assign(dom.style, attrs[key]) //新增的会赋值到 dom.style 上,同名的属性会覆盖
    } else { //其他的直接作为 dom 的属性
      dom[key] = attrs[key]
    }
  }
}

let num = 0
let timer = null
let styleObj = {
  color: 'red',
  fontSize: '20px'
}

onStart() //一开始时执行

function onStart() {
  console.log('click me')
  timer = setInterval(() => { //启动时,每秒钟计时一次,做一次渲染
    JreactDOM.render((
      <div className="wrapper">
        <h1 style = { styleObj }>Number: { num }</h1>
        <button onClick = { onStart }>start</button>
        <button onClick = { onPause }>pause</button>
      </div>
    ), document.querySelector('#app'))
  }, 1000)
}

function onPause() {
  clearInterval(timer) //点击停止时,清除定时器
}

02c8c0df6420810016883ef1c1259d2e.png

把之前的给清除掉

function render(vnode, container) { //每次调用 render 时,先把之前的清空
  container.innerHTML = ''
  _render(vnode, container)
}
function _render(vnode, container) {
  //...
}

完整代码

const Jreact = { //创建元素,组件
  createElement
}

const JreactDOM = { //用于去渲染,做一些其他的事情
  render
}

function createElement(tag, attrs, ...children) { //es6语法中的数组,所有的子元素放数组里
  return {
    tag,
    attrs,
    children
  }
}

function render(vnode, container) { //每次调用 render 时,先把之前的清空
  container.innerHTML = ''
  _render(vnode, container)
}

function _render(vnode, container) {
  if (typeof vnode === 'string' || typeof vnode === 'number') { //如果是 string 或者 nubmer 都去创建文本节点
    return container.appendChild(document.createTextNode(vnode))
  }

  if (typeof vnode === 'object') {
    let dom = document.createElement(vnode.tag)
    setAttribute(dom, vnode.attrs)
    if (vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(vnodeChild => {
        _render(vnodeChild, dom) //记得这里是 _render , 这里的逻辑是不清空的
      })
    }

    container.appendChild(dom)
  }
}

function setAttribute(dom, attrs) {
  for (let key in attrs) {
    if (/^on/.test(key)) { //对事件绑定的处理,以 on 开头的,dom[onclick] = attrs[onClick]
      dom[key.toLocaleLowerCase()] = attrs[key]
    } else if (key === 'style') { //对 style 的处理
      Object.assign(dom.style, attrs[key]) //新增的会赋值到 dom.style 上,同名的属性会覆盖
    } else { //其他的直接作为 dom 的属性
      dom[key] = attrs[key]
    }
  }
}

let num = 0
let timer = null
let styleObj = {
  color: 'red',
  fontSize: '20px'
}

onStart() //一开始时执行

function onStart() {
  console.log('click me')
  timer = setInterval(() => { //启动时,每秒钟计时一次,做一次渲染
    num++
    JreactDOM.render((
      <div className="wrapper">
        <h1 style = { styleObj }>Number: { num }</h1>
        <button onClick = { onStart }>start</button>
        <button onClick = { onPause }>pause</button>
      </div>
    ), document.querySelector('#app'))
  }, 1000)
}

function onPause() {
  clearInterval(timer) //点击停止时,清除定时器
}

2e10496c38f6f9947da8c27df504173c.png

完美实现。

模块化

拆分 index.js 文件

目录

fc3e4ecd4a75090efc6845300d8a2ef4.png

jreact.js JSX 变成虚拟 DOM

function createElement(tag, attrs, ...children) { 
  return {
    tag,
    attrs,
    children
  }
}

export default {
  createElement
}

jreact-dom.js 虚拟 DOM 渲染

function render(vnode, container) { //每次调用 render 时,先把之前的清空
  container.innerHTML = ''
  _render(vnode, container)
}

function _render(vnode, container) {
  if (typeof vnode === 'string' || typeof vnode === 'number') { //如果是 string 或者 nubmer 都去创建文本节点
    return container.appendChild(document.createTextNode(vnode))
  }

  if (typeof vnode === 'object') {
    let dom = document.createElement(vnode.tag)
    setAttribute(dom, vnode.attrs)
    if (vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(vnodeChild => {
        _render(vnodeChild, dom) //记得这里是 _render , 这里的逻辑是不清空的
      })
    }

    container.appendChild(dom)
  }
}

function setAttribute(dom, attrs) {
  for (let key in attrs) {
    if (/^on/.test(key)) { //对事件绑定的处理,以 on 开头的,dom[onclick] = attrs[onClick]
      dom[key.toLocaleLowerCase()] = attrs[key]
    } else if (key === 'style') { //对 style 的处理
      Object.assign(dom.style, attrs[key]) //新增的会赋值到 dom.style 上,同名的属性会覆盖
    } else { //其他的直接作为 dom 的属性
      dom[key] = attrs[key]
    }
  }
}

export default {
  render
}

index.js 业务代码

import Jreact from './lib/jreact' 
import JreactDOM from './lib/jreact-dom'

let num = 0
let timer = null
let styleObj = {
  color: 'red',
  fontSize: '20px'
}

onStart() //一开始时执行

function onStart() {
  console.log('click me')
  timer = setInterval(() => { //启动时,每秒钟计时一次,做一次渲染
    num++
    JreactDOM.render((
      <div className="wrapper">
        <h1 style = { styleObj }>Number: { num }</h1>
        <button onClick = { onStart }>start</button>
        <button onClick = { onPause }>pause</button>
      </div>
    ), document.querySelector('#app'))
  }, 1000)
}

function onPause() {
  clearInterval(timer) //点击停止时,清除定时器
}

进入到 chapter-2 目录下,运行npx parcel index.html,正常运行。

510829f41afa86b9ad212572c39cd9d8.png

以上完成了代码的拆分,实现了模块化。

实现 React 组件

实现以下这种书写方式

import Jreact from './lib/jreact' 
import JreactDOM from './lib/jreact-dom'

class App extends Jreact.Component { 
  render() {
    return (
      <h1>hello</h1>
    )
  }
}

JreactDOM.render(<App/>, document.querySelector('#app'))

用 babel 转换

  • <App/>得到React.createElement(App, null);
  • <app>hello</app>得到React.createElement("app", null, "hello");

<App/>首字母大写,转义后,参数 App 是一个变量,并不是字符串(标签),所以自定义的组件必须首字符大写,JSX 语法规定的,这样才会当做变量去处理,这个 App 可以做一些事情了。

JSX 经过处理后得到的虚拟 DOM 的第一个参数是一个变量(render 是处理标签,字符串的),用这个变量去创建一个对象,创造自己的组件,这里稍微有些复杂,先实现其他部分。

所有的组件都继承了 Jreact.Component, 所以先定义 Component

jreact.js

...
class Component { 
  constructor(props) {
    this.props = props //构造组件时,需要一些属性
    this.state = {} //组件内部有些状态/变量

    renderComponent() //创建组件后,需要去渲染这个组件(变成真实的DOM放到页面上)
  }
}

function renderComponent() {
  console.log('renderComponent')
}

export default {
  createElement,
  Component
}

变成虚拟 DOM 后,如何去渲染呢,遇到组件时,_render 如何处理呢?

先构造一个复杂的组件

index.js

import Jreact from './lib/jreact'
import JreactDOM from './lib/jreact-dom'

//得到了 Component 中的 props,render方法
// new App 时,就会去渲染组件
class App extends Jreact.Component {
  render() {
    return (
      <div className="wrapper">
        <h1 className="title">hello <span>张三</span></h1>
        <Job></Job>
      </div>
    )
  }
}

class Job extends Jreact.Component {
  render() {
    return (
      <div className="job">我的工作是前端工程师</div>
    )
  }
}

JreactDOM.render(<App></App>, document.querySelector('#app'))

_render 处理 vnode,把 vnode 打印出来

b5bb37cd6b001137ced2d039225b3e16.png

发现 App 的 tag 是一个函数,所以渲染虚拟 DOM 时,就需要创建这个函数,最终返回一个真实的 DOM 节点,并挂载到页面上

jreact-dom.js

...
function _render(vnode, container) {
  //...
  if (typeof vnode === 'object') {
    if(typeof vnode.tag === 'function') { //当 vnode.tag 是个函数时,就去创造一个组件
      let dom = createComponent(vnode.tag, vnode.attrs) //第一个参数是构造函数名,第二个参数是组件的属性
      return container.appendChild(dom) //返回的是一个真实的 DOM 节点,挂载到容器上
    }
    //...
  }
}
...

如何创造这个组件呢

执行JreactDOM.render(<App></App>, document.querySelector('#app')),调用 render 函数

把 _render 中的渲染 vnode 的逻辑抽离出来,因为在渲染组件的过程中,还会再次渲染组件中的 JSX,抽离出来后,方便进行再次处理,否则就会执行两次'挂载到页面上'这部分逻辑了

...
function _render(vnode, container) {
  let dom = createDomfromVnode(vnode)
  container.appendChild(dom)
}

function createDomfromVnode(vnode) {
  if (typeof vnode === 'string' || typeof vnode === 'number') { //如果是 string 或者 nubmer 都去创建文本节点
    return document.createTextNode(vnode)
  }

  if (typeof vnode === 'object') {
    if(typeof vnode.tag === 'function') { //当 vnode.tag 是个函数时,就去创建组件
      let dom = createComponent(vnode.tag, vnode.attrs) //第一个参数是构造函数名,第二个参数是组件的属性
      return dom
    }

    let dom = document.createElement(vnode.tag)
    setAttribute(dom, vnode.attrs)
    if (vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(vnodeChild => {
        _render(vnodeChild, dom) //记得这里是 _render , 这里的逻辑是不清空的
      })
    }
    return dom
  }
}

//创建组件
function createComponent(constructor, attrs) {
  let component = new constructor(attrs) //创造一个组件对象
  let vnode = component.render() //调用他的 render 方法,得到组件对应的虚拟节点(jsx)
  let dom = createDomfromVnode(vnode) //渲染成真实的 DOM
  component.$root = dom //方便后续拿到组件对应的真实的 DOM
  return dom
}
...

页面显示

3a093c72216a97449fad1e458839828f.png

查看创建的组件

window.c = []
function createComponent(constructor, attrs) {
  let component = new constructor(attrs) //创造一个组件对象
  c.push(component)
  ...
}

控制台输入c打印出

5f57ce335a0346dd3c81b4fc67a27e53.png

state props 组件间通信,均正常显示

import Jreact from './lib/jreact'
import JreactDOM from './lib/jreact-dom'

class App extends Jreact.Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '张三',
      job: '后端工程师'
    }
  }
  render() {
    return (
      <div className="wrapper">
        <h1 className="title">hello <span>{ this.state.name }</span></h1>
        <Job job={ this.state.job }></Job>
      </div>
    )
  }
}

class Job extends Jreact.Component {
  render() {
    return (
      <div className="job">我的工作是{ this.props.job }</div>
    )
  }
}

JreactDOM.render(<App></App>, document.querySelector('#app'))

显示

63b2aabef83f60750e089d50d1b681bc.png

组件的其他写法:function

class App extends Jreact.Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '张三',
      job: '后端工程师',
      hobby: '看电影'
    }
  }
  render() {
    return (
      <div className="wrapper">
        //...
        <Hobby hobby={ this.state.hobby }></Hobby>
      </div>
    )
  }
}

function Hobby(props) {
  return (
    <p>我的兴趣是{ props.hobby }</p>
  )
}

以上代码会报错,component.render is not a function

new 构造函数时(tag对应的函数),返回的是函数 return 出来的东西,所以执行 createComponent 时,componet 就是虚拟dom,上面没有 render方法,而这里把虚拟 dom 当成组件处理了

所以需要对两种组件的写法 Class , function,分别做处理

对 createComponent 做处理,判断 constructor 是什么类型,但是 Class function 都是函数(Class 是语法糖),如何判断呢

思路:通过 Class 构造出来的组件是继承了 Component, function 构造出来的是没有继承的,通过这一点来做判断

xxx.prototype instanceof Component 是 true 还是 false 判断是不是 Class

jreact-dom.js

import Jreact from './jreact'
...
function createComponent(constructor, attrs) {
  let component
  if(constructor.prototype instanceof Jreact.Component) {
    component = new constructor(attrs) 
  } else {
    component = new Jreact.Component(attrs) //使组件具有 state, props
    component.constructor = constructor
    component.render = function() { //增加 render 方法
      return this.constructor(attrs)
    }
  }
  let vnode = component.render() 
  //c.push(component)

  let dom = createDomfromVnode(vnode) 
  component.$root = dom 
  return dom
}

显示

1d81a1b879d50bca63f8f8520b74a1f9.png

继续完善,子组件绑定事件,去修改父组件数据

在子组件里绑定事件,点击修改按钮,触发事件,执行回调(通过父组件传递过来的事件属性),在父组件中定义回调函数

class App extends Jreact.Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '张三',
      job: '后端工程师',
      hobby: '看电影'
    }
  }
  render() {
    return (
      <div className="wrapper">
        <h1 className="title">hello <span>{ this.state.name }</span></h1>
        <p>hobby: { this.state.hobby }</p>
        <Job job={ this.state.job } onModifyJob = { this.onModifyJob.bind(this) }></Job>
        <Hobby hobby={ this.state.hobby }></Hobby>
      </div>
    )
  }
  onModifyJob(newJob) {
    this.setState({job: newJob})
  }
}

class Job extends Jreact.Component {
  render() {
    return (
      <div className="job">
        我的工作是{ this.props.job }
        <button onClick = { this.modifyJob.bind(this) }>修改工作</button>
      </div>
    )
  }
  modifyJob() {
    this.props.onModifyJob('React工程师')
  }
}

数据改变时,组件重新渲染(走之前的流程,逻辑写到 createComponent 里,而不是 jreact.js 中),做一个虚拟 dom 的替换,把以前的 dom 节点替换掉。

先拆分 createComponent,因为里面包含了'创建''渲染'两部分逻辑,抽离出'渲染'逻辑(renderComponent)

//创建组件
function createComponent(constructor, attrs) {
  let component
  if(constructor.prototype instanceof Jreact.Component) {
    component = new constructor(attrs) 
  } else {
    component = new Jreact.Component(attrs) //使组件具有 state, props
    component.constructor = constructor
    component.render = function() { //增加 render 方法
      return this.constructor(attrs)
    }
  }
  return component
}

//渲染组件
function renderComponent(component) {
  let vnode = component.render()
  let dom = createDomfromVnode(vnode)

  //修改后的 dom 做替换
}

修改后的 dom 替换的逻辑

//渲染组件
function renderComponent(component) {
  ...

  if(component.$root && component.$root.parentNode) {
    component.$root.parentNode.replaceChild(dom, component.$root)
  }
  component.$root = dom
}

完整代码

jreact-dom.js

import Jreact from './jreact'

function render(vnode, container) { //每次调用 render 时,先把之前的清空
  container.innerHTML = ''
  console.log(vnode)
  _render(vnode, container)
}

function _render(vnode, container) {
  let dom = createDomfromVnode(vnode)
  container.appendChild(dom)
}


//window.c = []
function createDomfromVnode(vnode) {
  if (typeof vnode === 'string' || typeof vnode === 'number') { //如果是 string 或者 nubmer 都去创建文本节点
    return document.createTextNode(vnode)
  }

  if (typeof vnode === 'object') {
    if(typeof vnode.tag === 'function') { //当 vnode.tag 是个函数时,就去创建组件
      let component = createComponent(vnode.tag, vnode.attrs) //第一个参数是构造函数名,第二个参数是组件的属性
      renderComponent(component)
      return component.$root
    }

    let dom = document.createElement(vnode.tag)
    setAttribute(dom, vnode.attrs)
    if (vnode.children && Array.isArray(vnode.children)) {
      vnode.children.forEach(vnodeChild => {
        _render(vnodeChild, dom) //记得这里是 _render , 这里的逻辑是不清空的
      })
    }
    return dom
  }
}

//创建组件
function createComponent(constructor, attrs) {
  let component
  if(constructor.prototype instanceof Jreact.Component) {
    component = new constructor(attrs) 
  } else {
    component = new Jreact.Component(attrs) //使组件具有 state, props
    component.constructor = constructor
    component.render = function() { //增加 render 方法
      return this.constructor(attrs)
    }
  }
  return component
}

//渲染组件
function renderComponent(component) {
  let vnode = component.render()
  let dom = createDomfromVnode(vnode)

  if(component.$root && component.$root.parentNode) {
    component.$root.parentNode.replaceChild(dom, component.$root)
  }
  component.$root = dom
}

function setAttribute(dom, attrs) {
  for (let key in attrs) {
    if (/^on/.test(key)) { //对事件绑定的处理,以 on 开头的,dom[onclick] = attrs[onClick]
      dom[key.toLocaleLowerCase()] = attrs[key]
    } else if (key === 'style') { //对 style 的处理
      Object.assign(dom.style, attrs[key]) //新增的会赋值到 dom.style 上,同名的属性会覆盖
    } else { //其他的直接作为 dom 的属性
      dom[key] = attrs[key]
    }
  }
}

export default {
  render,
  renderComponent
}

jreact.js

import jreactDom from "./jreact-dom";

function createElement(tag, attrs, ...children) { 
  return {
    tag,
    attrs,
    children
  }
}

class Component { 
  constructor(props) {
    this.props = props //构造组件时,需要一些属性
    this.state = {} //组件内部有些状态/变量
  }

  setState(state) {
    this.state = Object.assign(this.state, state) //额外的新增或修改,不是覆盖,所以用 Object.assign
    jreactDom.renderComponent(this)
  }
}

export default {
  createElement,
  Component
}

显示

9e4ccff8406d9ff1989549cfa909d2a8.png

以上实现了 React 的基本功能(用法),但是 dom 的更新是全局更新的(以组件的方式去更新的)App里的数据发生改变,会去渲染所有的组件,DOM 的频繁操作开销是很大的,可以精细化操作,比如修改了 name 数据,对应的 DOM 只去修改对应的那一部分即可(h1 中的 span 即可)

虚拟 DOM 的 diff,修改对应的状态时,重新调用 renderComponent 重新执行 JSX,得到新的虚拟 DOM ,再去执行自己的 render 方法,渲染到页面上,渲染的过程中,新的和之前的虚拟 DOM 做个对比,发现只有微小的差异,只更新这部分,开销就变小了,性能就优化了

diff 算法

实现的效果

9e33a9aae3c6098f94270900c44066cd.png

index.js

import Jreact from './lib/jreact.js'
import JreactDOM from './lib/jreact-dom.js'

class App extends Jreact.Component {
  constructor(props) {
    super(props)
    this.state = {
      name: '小讲堂',
      courses: ['数学', '语文', '英语'],
      styleObj: {
        color: 'red',
        fontWeight: 'bold'
      }
    }
  }

  render() {
    return (
      <div className="container">
        <h1>欢迎到<span className="name" style={ this.state.styleObj }>{ this.state.name }</span>来学习</h1>
        <p>aaa</p>
        <p>bbb</p>
        <div className="action">
          <button onClick = { this.modifyName.bind(this) }>修改名字</button>
          <button onClick = { this.setStyle.bind(this) }>样式</button>
        </div>
      </div>
    )
  }

  modifyName() {
    let newName = window.prompt('输入标题','小讲堂')
    this.setState({name: newName})
  }

  setStyle() {
    this.setState({
      styleObj: {
        color: 'blue'
      }
    })
  }
}

window.JreactDOM = JreactDOM

JreactDOM.render(<App/>, document.querySelector('#app'))

思路

第一次挂载到页面上时

27c968d8cc5521a880e3a8a2b58fe138.png

修改后,让虚拟dom 和真实的DOM 对应起来,只修改要修改的东西

b67720de9bffc0b843f7b328ea8c21d9.png

让标签,属性,子元素去做对比,第一个标签都是div,没有变动,属性都是class-box,也没有变动,所以 <div class="box">是可以保留的

  • 那么如何比较子元素呢?
  • 假设第一个标签变了,由div变成了p,该如何去处理呢?
  • 假设不是div,就是普通的文本做了修改,由zhangsan变为lisi,又该如何处理?

处理以上这些场景,需要虚拟 DOM 和页面上的 DOM 做一一映射

再捋一下思路

  1. JreactDOM.render JSX 时,调用 render 方法(之前是清空容器,根据 vnode 创建真实的 DOM,并挂载),调用 diff(),传递三个参数
  2. 要对比的页面上的 DOM(第一次挂载时调用 render() 时,要对比的 DOM 并不存在,所以先传递一个 null,后续重新渲染页面时,调用 diff, 之前的 DOM 就有了)
  3. 虚拟 DOM
  4. 要挂载的容器
  5. 定义 diff 函数:拿页面上真实的 DOM 和一开始渲染好的和他对应的虚拟 DOM 对比,操作...,把修改后的部分和真实的部分做替换,替换之后,页面上上的 DOM 为最新修改后的,再返回出去,供其他地方使用
function diffNode(dom, vnode) {

}

先看看 vnode 和 dom 有什么差别,先了解下虚拟 DOM 有哪几种类型:

{
  tga: "div",
  attrs: {className: "box"},
  children: [
    "hello",
    {
      tag: "span",
      attrs: null,
      children: [
        "zhangsan"
      ],
      {
        tag: Box //变量(函数),可以是组件
        attrs: null,
        children: []
      }
    }
  ]
}

有对象并且值为字符串的{xx:'xxx'} , 为字符串的'hello',有对象并且值为变量的{xx:Xxx} ,3种类型

如何对这三种类型做比较呢?

虚拟 DOM 为字符串时

f1eaeed4e5e2bdec5b71f0496c8639b9.png
function diffNode(dom, vnode) {
  let patchedDom = dom

  //如果是文本类型的虚拟DOM ,要么替换内容,要么替换元素
  if (typeof vnode === 'string' || typeof vnode === 'number') {
    if (patchedDom && patchedDom.nodeType === 3) { //真实DOM存在,并且是个字符串
      if (patchedDom.textContent !== vnode) { // hello 不等于 'hello' 时
        patchedDom.textContent = vnode //虚拟dom赋值给真实dom即可(修改)
      }
    } else { //若不是字符串,而是元素
      patchedDom = document.createTextNode(vnode) //直接创建一个文本节点,替换掉该元素
    }
    return patchedDom
  }
}

虚拟 DOM 为组件时

fd0ae60754b9b7fb8c082441f0a5761b.png
//如果是组件,就diff组件
  if (typeof vnode === 'object' && typeof vnode.tag === 'function') {
    patchedDom = diffComponent(dom, vnode) //交给他去处理
    return patchedDom
  }

虚拟 DOM 新增时(真实 DOM 不存在)

17ff9b039dce753ce028dbf05feb86c7.png
//否则就是普通的 vnode
  //看 dom 是不是存在,如果不存在就根据 vnode 创建
  if (!dom) {
    patchedDom = document.createElement(vnode.tag) //根据虚拟 dom 的标签去创造
  }

虚拟 DOM 和真实 DOM 都为 tag,但是不相同时

没有直接修改标签名的 api, 所以

04bed907db0a13ab95bb4328a4d1f0cd.png
//如果存在但标签变了,就修正标签(创建新标签的 dom ,但旧标签 dom 的孩子放到新标签 dom 里,旧标签替换成新标签)
  if (dom && dom.nodeName.toLowerCase() !== vnode.tag.toLowerCase()) { 
    patchedDom = document.createElement(vnode.tag)
    dom.childNodes.forEach((child) => patchedDom.appendChild(child))
    replaceDom(patchedDom, dom)
  }

以上是对标签的比较。

对属性的比较

对子元素的比较(重点)

一开始渲染好了,现在子元素发生了变化:

  • 新增
  • 修改
  • 删除
  • 移位

虚拟 DOM 变化时,真实 DOM 也要跟着发生改变,最大程度上利用已有的 DOM ,例如 b没变,就不要再去渲染b,a节点知识内容变化了,所以只需要改变文本内容,e节点只需要换个位置,多了个 g,加上去,把 f 删掉

具体如何做呢

逐层对比,逐个对比

e4ddaddb2acfcee1f6774eb1c0adf4c0.png

23b3c4371c38efabce4d234a51a3589a.png

比较第一个,aa 变成a ,里面的内容变化了(或者标签或者属性); 比较第二个,没变;第三个,把 c变成e, 第四个,没变,第五个,把e变为c,第六个,把 f 变为 g

做对比时,再次执行 diffNode()

优点:简单快速 缺点:遇到极端情况时,比如把最后一个移位到第一个,逐个对比时,基本全部挪动一下位置,做了100%的更新(但实际上只是两个互换了一下位置)

优化:增加 key 值

f2402ffd8b71fdc29e88251cc2b7f277.png
  • 第一个,对于没有 key 值的,不知道是旧的还是新的,直接去做比较,然后修改真实 DOM 中的文本内容就行了
  • 第二个,虚拟 DOM 中 b 的 key 值是 b1, 通过b1 :b 找到真实 DOM 中的 b(而且位置也对),什么都不做
  • 第三个,e 没有 key,直接去对比,把真实dom中的 c 替换成 e
  • 第四个,d 通过 key 找到了 d,没有变化
  • 第五个,c 通过 key 发现真实 DOM 中没有,把 e(虚拟 c 对应的位置) 变成 c

带 key 值的移动位置时

8a6b1bdf173d29a71d0ffcaa27f9cc4f.png

虚拟dom 中的 d ,通过 d1:d , 在真实 dom 中找到了 d,并改变他的位置

以上算法还是有一些弊端,可以再优化

有 key 的都可以复用

165d5c887f20c360808aba45fd6796ad.png
  • 序号1 没有 key 值,直接替换掉,
  • 序号2 有key 值,通过 key 值找到后 放入到 2 号位置,并没有新增,只是做了一个移位(复用以前的),
  • 序号3 没有 key 值,从没有 key 的这一列去找,找到 e,放入到 第三位,
  • 序号4 有key ,从有key 的一列去找,找到了放入到 第4的位置,
  • 序号 5 有key,从有 key 的一列去找,找到了放入到第 5 的位置,
  • 序号 6 没有 key 值,从没有 key 的这一列去找,找到了f ,修改为 g

若虚拟dom 中没有,实体 dom 中有,则把对应的给删掉

代码实现 因为过于复杂,稍微简单的方式实现

function diffChildren(patchedDom, vChildren) {
  let domChildren = patchedDom.childNodes //子节点
  let domsHasKey = {} //原来 dom 里带 key 的拿出来 {b1:b,c1:c,d1:d}
  for (let dom of domChildren) {
    if (dom.key) {
      domsHasKey[dom.key] = dom
    }
  }

  //用最长的做判断(dom, vdom) 循环一次即可,
  let vChild
  let patchChildDom
  let length = Math.max(domChildren.length, vChildren.length)

  for (let i = 0; i < length; i++) {
    vChild = vChildren[i]

    //有 key 的处理逻辑
    if (vChild.key && domsHasKey[vChild.key]) {
      patchChildDom = diffNode(domsHasKey[vChild.key], vChild)
    } else { //不带 key 的处理逻辑
      patchChildDom = diffNode(domChildren[i], vChild)
    }

    if (patchChildDom.parentNode !== patchedDom) {
      patchedDom.appendChild(patchChildDom)
    }
    //设置子元素在父元素中的位置
    setOrderInContainer(patchedDom, patchChildDom, i)
  }
}

function setOrderInContainer(container, dom, order) {
  if(container.childNodes[order] !== dom) { //如果本身位置是对应的,就不走下面的逻辑了
    container.childNodes[order].insertAdjacentElement('beforebegin', dom) //找到原来的位置,放置于他的前面即可
  }
}

属性如何 diff 呢

function diffAttributes(dom, vnode) {
  const old = {}
  const attrs = vnode.attrs

  //找到真实 dom 的属性
  for (var i = 0; i < dom.attributes.length; i++) {
    const attrs = dom.attributes[i]
    old[attrs.name] = attr.value
  }
  //自己的属性不在新的属性里,把他删除掉
  for (var key in old) {
    if(!(key in attrs)) {
      setAttribute(dom, key, undefined)
    }
  }
  //重新遍历,设置为新的属性
  for (var key in attrs) {
    console.log(key)
    if(old[key] !== attrs[key]) {
      setAttribute(dom, key, attrs[key])
    }
  }
}

如果不是标签,而是组件呢,如何做呢

function diffComponent(dom, vnode) {
  let component = dom?dom_component:null //从 dom 上拿到这个组件

  //如果 component 是存在的,组件的 constructor 是这个函数(tag: App),
  if(component && component.constructor === vnode.tag) {
    setComponentProps(component,vnode.attrs) //设置组件的属性
  }else {
    component = createComponent(vnode.tag, vnode.attrs)
    setComponentProps(component, vnode.attrs)
  }

  return component.$root
}

function setComponentProps(component, props) {
  component.props = props //设置组件属性
  renderComponent(component) //渲染组件
}

实现了 DOM 利用的最大化,提高了性能

以上就是 diff 算法的完整实现

测试

'小讲堂'修改为'大课堂'

a4acb3fd56602a1e49fdb798e483b05f.png

只有span 处闪动,说明只有这里进行了替换

修改样式

1cef5b28d14fa603b6fac9961b20ac6b.png

只有 style 处闪动,说明只有这里进行了替换。

完整代码

效果预览

部署到 Github 上时遇到的问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值