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

理解JSX和虚拟DOM #1

Open
Vibing opened this issue May 12, 2018 · 0 comments
Open

理解JSX和虚拟DOM #1

Vibing opened this issue May 12, 2018 · 0 comments

Comments

@Vibing
Copy link
Owner

Vibing commented May 12, 2018

前言

react的火热程度已经达到了94.5k个start,本系列文章主要用简单的代码来实现一个react,来了解JSX、虚拟DOM、diff算法以及state和setState的设计。

提到react,当然少不了vue,vue的api设计十分简单 上手也非常容易,但黑魔法很多,使用起来有点虚, 而react没有过多的api,它的深度体现在设计思想上,使用react开发则让人比较踏实、能拿捏的住,这也是我喜欢react的原因之一。

JSX

react怎么少的了JSXJSX是什么,让我来看个例子
现在有下面这段代码:

const el = <h3 className="title">Hello Javascript</h3>

这样的js代码如果不经过处理会报错,jsx是语法糖,它让这段代码合法化,通过babel转化后是这样的:

const el = React.createElement(
    'h3',
    { className: 'title' },
    'Hello Javascript'
)

这种例子官网首页也有demo

准备开始

开始编码之前,先介绍两个东西:parcelbabel-plugin-transform-jsx,等会我们用parcel搭建一个开发工程,babel-plugin-transform-jsxbabel的一个插件,它可以将jsx语法转成React.createElement(...)

下面我们开始

简单的搭建

parcel这里就不介绍了,一句话概况就是为你生成一个零配置的开发环境。

  1. yarn global add parcel-bundlernpm install -g parcel-bundler
  2. 新建项目文件夹,这里取名为simple-react
  3. simple-react中执行 yarn init -ynpm init -y 生成package.json
  4. 创建一个index.html
  5. 创建src文件夹 再在src下创建index.js 然后再index.html中引入index.js

如果你先麻烦,可以直接下载源码修改。

以上步骤完可能不完整,最好参考parcel里的内容。以上工作完成后,我们需要安装babel-plugin-transform-jsx

npm insatll babel-plugin-transform-jsx --save-dev
或者
yarn add babel-plugin-transform-jsx --dev

然后添加.babelrc文件,并在该文件中加入下面这段代码:

{
  "presets": ["env"],
  "plugins": [["transform-jsx", { "function": "React.createElement" }]]
}

上面代码的意思是 使用transform-jsx插件,并配置为使用React.createElement方法来解析JSX,当然你也可以不用React.createElement和自定义方法,比如preact使用的h方法。

React.createElement()

现在我们在index.js里开始编码。
首先写入代码:

const el = <h3 className="title">Hello Javascript</h3>;
console.log(el);

我们在什么都不写的情况下,打印看看el是什么。
react-1

打印报错:React没有定义。 这是因为在.babelrc文件中,我们使用的这段代码起了作用:

["transform-jsx", { "function": "React.createElement" }]

上面说过,它会通过React.createElement方法来转译JSX,那么我们就给出这个方法:
我们把刚才那段代码改变一下:

const React = {
  createElement: function(...args) {
    return args[0];
  }
};

const el = <h3 className="title">Hello Javascript</h3>;

console.log(el);

上面代码添加了一个React对象,并在其中添加一个createElement方法,现在再执行一下看看打印出什么:
react-7

由打印结果可以看出,jsx在使用React.createElement方法转译时,createElement方法应该是这样的:

createElement({ elementName, attributes, children });
  • elementName: dom对象的标签名 比如div、span等等
  • attributes: 当前dom对象的属性集合 比如class、id等等
  • children: 所有子节点

现在我们改写一下createElement方法,让key的名称简单一点:

const React = {
  createElement: function({ elementName, attributes, children }) {
    return {
      tag: elementName,
      attrs: attributes,
      children
    };
  }
};

现在可以看到打印结果是:
react-3

我们再打印个复杂点的DOM结构:

const el = (
  <div style="color:red;">
    Hello <span className="title">JavaScript</span>
  </div>
);

console.log(el);

react-4

和我们想要的结构一样。
其实上面打印出来的就是虚拟DOM,现在我们要做的就是如何把虚拟DOM转成真正的DOM对象并显示在浏览器上。

ReactDOM.render()

要想将虚拟dom转成真实dom并渲染到页面上,就需要调用ReactDOM.render,比如:

ReactDOM.render(<h1>Hello World</h1>, document.getElementById('root'));

这段代码转换后的样子:

ReactDOM.render(
  React.createElement('h1', null, 'Hello World'),
  document.getElementById('root')
);

这时,react会将<h1>Hello World</h1>挂载到id为root的dom下,从而在页面上显示出来。

现在我们实现render方法:

function render(vnode, container) {
  const dom = createDom(vnode); //将vnode转成真实DOM
  container.appendChild(dom);
}

上面代码中先调用createDom将虚拟dom转成真实DOM然后挂载到container下。

我们来实现createDom方法:

function createDom(vnode) {
  if (vnode === undefined || vnode === null || typeof vnode === 'boolean') {
    vnode = '';
  }

  if (typeof vnode === 'string' || typeof vnode === 'number') {
    return document.createTextNode(String(vnode));
  }

  const dom = document.createElement(vnode.tag);

    //设置属性
  if (vnode.attrs) {
    for (let key in vnode.attrs) {
      const value = vnode.attrs[key];
      setAttribute(dom, key, value);
    }
  }
    //递归render子节点
  vnode.children.forEach(child => render(child, dom));
  return dom;
}

由于属性的种类比较多,我们抽出一个setAttribute方法来设置属性:

function setAttribute(dom, key, value) {
  //className
  if (key === 'className') {
    dom.setAttribute('class', value);

    //事件
  } else if (/on\w+/.test(key)) {
    key = key.toLowerCase();
    dom[key] = value || '';
    //style
  } else if (key === 'style') {
    if (typeof value === 'string') {
      dom.style.cssText = value || '';
    } else if (typeof value === 'object') {
      // {width:'',height:20}
      for (let name in value) {
      //如果是数字可以忽略px
        dom.style[name] =
          typeof value[name] === 'number' ? value[name] + 'px' : value[name];
      }
    }

    //其他
  } else {
    dom.setAttribute(key, value);
  }
}

现在render方法已经完整的实现了,我们将创建ReactDOM对象,将render方法挂上去:

const ReactDOM = {
  render: function(vnode, container) {
    container.innerHTML = '';
    render(vnode, container);
  }
};

这里在调用render之前加了一句container.innerHTML = '',就不解释了,相信大家都明白。

那么万事具备,我们来测试一下,直接上一个比较复杂的dom结构并加上属性:

const element = (
  <div
    className="Hello"
    onClick={() => alert(1)}
    style={{ color: 'red', fontSize: 30 }}
  >
    Hello <span style={{ color: 'blue' }}>javascript!</span>
  </div>
);

ReactDOM.render(element, document.getElementById('root'));

打开页面,是我们想要的结果:

react-5

再看看控制台的dom:

react-6

很完美,这是我们想要的东西

@Vibing Vibing changed the title 亲手写一个React(一):JSX和虚拟DOM 理解JSX和虚拟DOM Jun 9, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant