从零开始实现 React(一): JSX 和虚拟 DOM - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
iamjiulong
V2EX    前端开发

从零开始实现 React(一): JSX 和虚拟 DOM

  •  
  •   iamjiulong 2018-03-20 10:01:59 +08:00 1619 次点击
    这是一个创建于 2849 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    React 是前端最受欢迎的框架之一,解读其源码的文章非常多,但是我想从另一个角度去解读 React:从零开始实现一个 React,从 API 层面实现 React 的大部分功能,在这个过程中去探索为什么有虚拟 DOM、diff、为什么 setState 这样设计等问题。

    提起 React,总是免不了和 Vue 做一番对比

    <>Vue 的 API 设计非常简洁,但是其实现方式却让人感觉是“魔法”,开发者虽然能马上上手,但是为什么能实现功能却很难说清楚。

    相比之下 React 的设计哲学非常简单,虽然经常有需要自己处理各种细节问题,但是却让人感觉它非常“真实”,能清楚地感觉到自己仍然是在写 js。

    关于 jsx

    在开始之前,我们有必要搞清楚一些概念。

    我们来看一下这样一段代码:

    const title = <h1 className="title">Hello, world!</h1>; 

    这段代码并不是合法的 js 代码,它是一种被称为 jsx 的语法扩展,通过它我们就可以很方便的在 js 代码中书写 html 片段。

    本质上,jsx 是语法糖,上面这段代码会被 babel 转换成如下代码

    const title = React.createElement( 'h1', { className: 'title' }, 'Hello, world!' ); 

    你可以在 babel 官网提供的在线转译测试 jsx 转换后的代码,这里有一个稍微复杂一点的例子

    准备工作

    为了集中精力编写逻辑,在代码打包工具上选择了最近火热的零配置打包工具 parcel,需要先安装 parcel:

    npm install -g parcel-bundler 

    接下来新建index.jsindex.html,在index.html中引入index.js

    当然,有一个更简单的方法,你可以直接下载这个仓库的代码:

    https://github.com/hujiulong/simple-react/tree/chapter-1

    注意一下 babel 的配置 .babelrc

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

    这个transform-react-jsx就是将 jsx 转换成 js 的 babel 插件,它有一个pragma项,可以定义 jsx 转换方法的名称,你也可以将它改成h(这是很多类 React 框架使用的名称)或别的。

    准备工作完成后,我们可以用命令parcel index.html将它跑起来了,当然,现在它还什么都没有。

    React.createElement 和虚拟 DOM

    前文提到,jsx 片段会被转译成用React.createElement方法包裹的代码。所以第一步,我们来实现这个React.createElement方法

    从 jsx 转译结果来看,createElement 方法的参数是这样:

    createElement( tag, attrs, child1, child2, child3 ); 

    第一个参数是 DOM 节点的标签名,它的值可能是divh1span等等 第二个参数是一个对象,里面包含了所有的属性,可能包含了classNameid等等 从第三个参数开始,就是它的子节点

    我们对 createElement 的实现非常简单,只需要返回一个对象来保存它的信息就行了。

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

    函数的参数...children使用了 ES6 的rest 参数,它的作用是将后面 child1,child2 等参数合并成一个数组 children。

    现在我们来试试调用它

    // 将上文定义的 createElement 方法放到对象 React 中 const React = { createElement } const element = ( <div> hello<span>world!</span> </div> ); console.log( element ); 

    打开调试工具,我们可以看到输出的对象和我们预想的一致

    1

    我们的 createElement 方法返回的对象记录了这个 DOM 节点所有的信息,换言之,通过它我们就可以生成真正的 DOM,这个记录信息的对象我们称之为虚拟 DOM

    ReactDOM.render

    接下来是 ReactDOM.render 方法,我们再来看这段代码

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

    经过转换,这段代码变成了这样

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

    所以render的第一个参数实际上接受的是 createElement 返回的对象,也就是虚拟 DOM 而第二个参数则是挂载的目标 DOM

    总而言之,render 方法的作用就是将虚拟 DOM 渲染成真实的 DOM,下面是它的实现:

    function render( vnode, container ) { // 当 vnode 为字符串时,渲染结果是一段文本 if ( typeof vnode === 'string' ) { const textNode = document.createTextNode( vnode ); return container.appendChild( textNode ); } const dom = document.createElement( vnode.tag ); if ( vnode.attrs ) { Object.keys( vnode.attrs ).forEach( key => { if ( key === 'className' ) key = 'class'; // 当属性名为 className 时,改回 class dom.setAttribute( key, vnode.attrs[ key ] ) } ); } vnode.children.forEach( child => render( child, dom ) ); // 递归渲染子节点 return container.appendChild( dom ); // 将渲染结果挂载到真正的 DOM 上 } 

    这里注意 React 为了避免类名class和 js 关键字class冲突,将类名改成了 className,在渲染成真实 DOM 时,需要将其改回。

    这里其实还有个小问题:当多次调用render函数时,不会清除原来的内容。所以我们将其附加到 ReactDOM 对象上时,先清除一下挂载目标 DOM 的内容:

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

    渲染和更新

    到这里我们已经实现了 React 最为基础的功能,可以用它来做一些事了。

    我们先在 index.html 中添加一个根节点

    <div id="root"></div> 

    我们先来试试官方文档中的Hello,World

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

    可以看到结果: 2

    试试渲染一段动态的代码,这个例子也来自官方文档

    function tick() { const element = ( <div> <h1>Hello, world!</h1> <h2>It is {new Date().toLocaleTimeString()}.</h2> </div> ); ReactDOM.render( element, document.getElementById( 'root' ) ); } setInterval( tick, 1000 ); 

    可以看到结果: 2

    后话

    这篇文章中,我们实现了 React 非常基础的功能,也了解了 jsx 和虚拟 DOM,下一篇文章我们将实现非常重要的组件功能。

    最后留下一个小问题 在定义 React 组件或者书写 React 相关代码,不管代码中有没有用到 React 这个对象,我们都必须将其 import 进来,这是为什么?

    例如:

    import React from 'react'; // 下面的代码没有用到 React 对象,为什么也要将其 import 进来 import ReactDOM from 'react-dom'; ReactDOM.render( <App />, document.getElementById( 'editor' ) ); 

    不知道答案的同学再仔细看看这篇文章哦

    从零开始实现 React 系列

    React 是前端最受欢迎的框架之一,解读其源码的文章非常多,但是我想从另一个角度去解读 React:从零开始实现一个 React,从 API 层面实现 React 的大部分功能,在这个过程中去探索为什么有虚拟 DOM、diff、为什么 setState 这样设计等问题。

    整个系列大概会有六篇左右,我每周会更新一到两篇,我会第一时间在 github 上更新,有问题需要探讨也请在 github 上回复我~

    博客地址: https://github.com/hujiulong/blog 关注点 star,订阅点 watch

    目前尚无回复
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2973 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 27ms UTC 14:10 PVG 22:10 LAX 06:10 JFK 09:10
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86