Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1.JSX 和 Virtual DOM #24

Closed
MuYunyun opened this issue Jul 6, 2018 · 15 comments
Closed

1.JSX 和 Virtual DOM #24

MuYunyun opened this issue Jul 6, 2018 · 15 comments

Comments

@MuYunyun
Copy link
Owner

MuYunyun commented Jul 6, 2018

该系列文章在实现 cpreact 的同时理顺 React 框架的核心内容

项目地址

环境准备

首先安装以下 babel 模块,其具体作用会在后文 JSX 和 虚拟 DOM 中提及

扩展延伸:babel 执行机制

"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "v8.0.0-beta.0",

同时 .babelrc 配置如下:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "> 0.25%, not dead",
        "useBuiltIns": "entry"
      }
    ],
    [
      "@babel/preset-react",
      {
        "pragma": "cpreact.createElement" // 该参数传向 transform-react-jsx 插件,是前置的一个核心,后文有解释为什么使用 cpreact.createElement
      }
    ]
  ]
}

配置好 babel 后,接着提供两套打包工具的配置方案,读者可以自行选择。

方案 1:使用 webpack

webpack 拥有一个活跃的社区,提供了更为丰富的打包能力。

首先安装以下模块:

"webpack": "^4.17.2",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.8"

在根目录的 webpack.config.js 配置如下:

const webpack = require('webpack')
const path = require('path')
const rootPath = path.resolve(__dirname)

module.exports = {
  entry: path.resolve(rootPath, 'test', 'index.js'),
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist'
  },
  output: {
    filename: 'cpreact.js',
    path: path.resolve(rootPath, 'dist'),
    libraryTarget: 'umd'
  },
  module: {
    rules: [{
      test: /\.js$/,
      loader: "babel-loader",
    }]
  },
}

然后在 package.json 里加上如下配置:

"scripts": {
  "start": "webpack-dev-server --open",
},

具体可以参照 0.4.3 版本

方案 2:使用 parcel

parcel 是一款上手极快的打包工具,使用其可以快速地进入项目开发的状态。在 package.json 加上如下配置,具体可以参照 0.1 版本

"scripts": {
  "start": "parcel ./index.html --open -p 8080 --no-cache"
},

JSX 和 虚拟 DOM

const element = (
  <div className="title">
    hello<span className="content">world!</span>
  </div>
)

JSX 是一种语法糖,经过 babel 转换结果如下,可以发现实际上转化成 React.createElement() 的形式:

扩展:babel执行机制

var element = React.createElement(
  "div",
  { className: "title" },
  "hello",
  React.createElement(
    "span",
    { className: "content" },
    "world!"
  )
);

打印 element, 结果如下:

{
  attributes: {className: "title"}
  children: ["hello", t] // t 和外层对象相同
  key: undefined
  nodeName: "div"
}

因此,我们得出结论:JSX 语法糖经过 Babel 编译后转换成一种对象,该对象即所谓的虚拟 DOM,使用虚拟 DOM 能让页面进行更为高效的渲染。

我们按照这种思路进行函数的构造:

const React = {
  createElement
}

function createElement(tag, attr, ...child) {
  return {
    attributes: attr,
    children: child,
    key: undefined,
    nodeName: tag,
  }
}

// 测试
const element = (
  <div className="title">
    hello<span className="content">world!</span>
  </div>
)

console.log(element) // 打印结果符合预期
// {
//   attributes: {className: "title"}
//   children: ["hello", t] // t 和外层对象相同
//   key: undefined
//   nodeName: "div"
// }

虚拟 DOM 转化为真实 DOM

上个小节介绍了 JSX 转化为虚拟 DOM 的过程,这个小节接着来实现将虚拟 DOM 转化为真实 DOM (页面上渲染的是真实 DOM)。

我们知道在 React 中,将虚拟 DOM 转化为真实 DOM 是使用 ReactDOM.render 实现的,使用如下:

import ReactDOM from 'react-dom'

ReactDOM.render(
  element, // 上文的 element,即虚拟 dom
  document.getElementById('root')
)

接着来实现 ReactDOM.render 的逻辑:

const ReactDOM = {
  render
}

/**
 * 将虚拟 DOM 转化为真实 DOM
 * @param {*} vdom      虚拟 DOM
 * @param {*} container 需要插入的位置
 */
function render(vdom, container) {
  if (typeof(vdom) === 'string') {
    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))
  container.appendChild(dom)
}

/**
 * 给节点设置属性
 * @param {*} dom   操作元素
 * @param {*} attr  操作元素属性
 * @param {*} value 操作元素值
 */
function setAttribute(dom, attr, value) {
  if (attr === 'className') {
    attr = 'class'
  }
  if (attr.match(/on\w+/)) {   // 处理事件的属性:
    const eventName = attr.toLowerCase().splice(1)
    dom.addEventListener(eventName, value)
  } else if (attr === 'style') { // 处理样式的属性:
    let styleStr = ''
    let standardCss
    for (let klass in value) {
      standardCss = humpToStandard(klass) // 处理驼峰样式为标准样式
      value[klass] = _.isNumber(+value[klass]) ? value[klass] + 'px' : value[klass] // style={{ className: '20' || '20px' }}>
      styleStr += `${standardCss}: ${value[klass]};`
    }
    dom.setAttribute(attr, styleStr)
  } else {                       // 其它属性
    dom.setAttribute(attr, value)
  }
}

至此,我们成功将虚拟 DOM 复原为真实 DOM,展示如下:

另外配合热更新,在热更新的时候清空之前的 dom 元素,改动如下:

const ReactDOM = {
  render(vdom, container) {
    container.innerHTML = null
    render(vdom, container)
  }
}

总结

JSX 经过 babel 编译为 React.createElement() 的形式,其返回结果就是 Virtual DOM,最后通过 ReactDOM.render() 将 Virtual DOM 转化为真实的 DOM 展现在界面上。流程图如下:

思考题

如下是一个 react/preact 的常用组件的写法,那么为什么要 import 一个 React 或者 h 呢?

import React, { Component } from 'react' // react
// import { h, Component } from 'preact' // preact

class A extends Component {
  render() {
    return <div>I'm componentA</div>
  }
}

render(<A />, document.body) // 组件的挂载

项目说明

该系列文章会尽可能的分析项目细节,具体的还是以项目实际代码为准。

鸣谢

Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react

@limengke123
Copy link

讲的挺好的,感谢!
所以,为什么要 import 一个 React 或者 h 呢?

@MuYunyun
Copy link
Owner Author

MuYunyun commented Jul 8, 2018

@limengke123 拿 React 来说,JSX 经过 babel 编译为 React.createElement() 的形式,所以要引人 React,从而调用其 createElement 方法生成 Virtual DOM。

@MuYunyun MuYunyun changed the title 从 0 到 1 实现 React 系列 —— JSX 和 Virtual DOM 从 0 到 1 实现 React 系列 —— 1.JSX 和 Virtual DOM Aug 5, 2018
@jack-hate-titanic
Copy link

你写的文章很好,通俗易懂,谢谢作者的分享

@MuYunyun
Copy link
Owner Author

@Y-wson 可以动手实现一遍,收获会很大。

@MuYunyun MuYunyun reopened this Aug 20, 2018
@jack-hate-titanic
Copy link

@MuYunyun
文章中的代码有错误,和code里面的不一样

@MuYunyun
Copy link
Owner Author

MuYunyun commented Aug 21, 2018

@Y-wson 可以在 release 找到与文章对应的相应代码。

@jack-hate-titanic
Copy link

@MuYunyun 你的代码可以复制么?当然会加上自己的见解

@MuYunyun
Copy link
Owner Author

MuYunyun commented Aug 27, 2018

@Y-wson 如果是学习用的话,在文章后面注明引用,我蛮欢迎的。另外如果对代码部分有什么好的见解可以交流

@jack-hate-titanic
Copy link

好的,我也是才学习源码,觉得你的文章通俗易懂,多向你学习

@misaka42
Copy link

  if (typeof(vdom) === 'string') {
    container.innerText = vdom
    return
  }

这里要改成用 TextNode,不然一遇到 String 这个 DOM 就被清空了。

整体还有不少细节的小问题,建议作者可以构造一些单元测试,这些问题都能测出来。

@MuYunyun
Copy link
Owner Author

MuYunyun commented Aug 28, 2018

@PPQ1991 目前也计划着跑 测试用例,至于你反馈的这个点我感觉应该不是 bug,你可以在 测试文件 中跑下设想的场景,看看是否与预期不符。使用方法:

npm install -g parcel-bundler

npm run start

@misaka42
Copy link

@MuYunyun 你的测试文件 VDOM 结构太简单了,没有覆盖多个 children 的场景。

<div>
  child - 1
  <span>child - 2</span>
  child - only show this
</div>

如果不用 TextNode,遇到 String 你就直接 innerText = 'xxx' 了,之前的 children 都被清空了。

@MuYunyun
Copy link
Owner Author

MuYunyun commented Aug 30, 2018

@PPQ1991 我明白你的意思了,在后面的章节中是有解决这个问题的,比如第二章中采用如下的方式来优化:

  if (_.isString(vdom) || _.isNumber(vdom)) {
    container.innerText = container.innerText + vdom
    return
  }

以及再后面章节更为优化的代码,这是一个循序渐进的过程。

  if (_.isString(vdom) || _.isNumber(vdom)) {
    const textNode = document.createTextNode(vdom)
    return textNode
  }

因为这是一个从 0 到 1 实现 react 的过程,很多细节不能构思得一步到位。后面的章节中在增加新特性的时候也会补充前面遗留的坑。感谢持续跟进与交流。

Repository owner deleted a comment from hujiulong Sep 9, 2018
@MuYunyun MuYunyun changed the title 从 0 到 1 实现 React 系列 —— 1.JSX 和 Virtual DOM 1.JSX 和 Virtual DOM Sep 18, 2018
@jack-hate-titanic
Copy link

你这里面有个小错误,setAttribute里面匹配事件的正则不应该加引号

@MuYunyun
Copy link
Owner Author

@Y-wson 已更正

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants