React:深入探索(一)

1. 什么是React

React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库,由Facebook和一个大型开发者社区共同维护。使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称作“组件”。

2. React的特点

  • 声明式设计 - React采用声明范式,可以轻松描述应用。

  • 高效 - React通过对DOM的模拟,最大限度地减少与DOM的交互。

  • 灵活 - React可以与已知的库或框架很好地配合。

  • JSX - JSX是JavaScript语法的扩展。

  • 单向响应式的数据流 - React实现了单向相应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。

  • 组件 - 通过React构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。

3. React的核心

真正去了解React还是要去深入学习它的核心东西,核心内容主要包括以下几点:

  • JSX:即Javascript XML,一个看起来很像 XML 的 JavaScript 语法扩展。用自己的话来说就是可以在js里面直接写DOM元素。

  • 虚拟DOM:一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式(将真实的DOM结构映射成一个JSON数据结构 )被保存于内存中。并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调

  • Diff算法:上面说到的虚拟DOM与“真实的” DOM同步的问题,也就是协调,协调需要采用Diff算法实现。普通diff的复杂度对于大量dom对比会出现严重的性能问题,React团队对diff算法进行了优化,主要有三个方面:tree diff、component diff、element diff。

  • 组件化:Web Component通过自定义元素的方式实现组件化,React的组件元素被描述成纯粹的JSON对象,由三部分组成——属性(props),状态(state)以及生命周期方法。组件化的目的就是为了能够进行复用,减少代码的冗余。

4. JSX

const element = <h1>Hello, world!</h1>;

这个有趣的标签语法既不是字符串也不是 HTML。它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式(JSX 可以生成 React “元素”)。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。

  • 在 JSX 中嵌入表达式

    在 JSX 语法中,你可以在大括号内放置任何有效的 JavaScript 表达式

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
ReactDOM.render(
  element,
  document.getElementById('root')
);
  • JSX 也是一个表达式

    你可以在 if 语句和 for 循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX

function getGreeting(user) {
  if (user) {
    return <h1>Hello, {formatName(user)}!</h1>;  
  }
  return <h1>Hello, Stranger.</h1>;
}
  • JSX 特定属性

可以通过使用引号,来将属性值指定为字符串字面量:

const element = <div tabIndex="0"></div>;

也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:

const element = <img src={user.avatarUrl}></img>;

注意:在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。

  • JSX 防止注入攻击
const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;

React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。

  • JSX 表示对象

    Babel 会把 JSX 转译成一个名为 React.createElement() 函数调用。实际上,JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖,以下两种示例代码完全等效:

const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
);

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

// 实际上React.createElement创建了一个这样的对象:
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
    children: 'Hello, world!'
  }
};

这些对象被称为 “React 元素”。它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。将 React 元素渲染为 DOM,通过ReactDOM.render()实现

// “根” DOM 节点,因为该节点内的所有内容都将由 React DOM 管理。
<div id="root"></div>

// 想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render():
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
  • 解析React.createElement()
    每次新增一个组件页面,都需要引入React,而实际在代码中并未使用到,删除React又会报错,其实在渲染元素的时候调用了React.createElement(),只要我们使用了JSX,就要引入React。React.createElement()到底干了些什么事情?
<h1 id="title" className="title_class" style={{color: 'red'}}>
   hello
   <span>world</span>
</h1>

// 当我们将上面的代码经过babel转译后,调用React.createElement(component, props, ...children),如下:

React.createElement("h1", {
  id: "title",
  className: "title_class",
  style: {
    color: 'red'
  }
}, "hello", React.createElement("span", null, "world"));

// React.createElement创建了一个这样的对象:
const element = {
  type: 'h1',
  props: {
    id: 'title'
    className: 'title_class',
    style: {
        color: 'red'
    }
    children: ['Hello, world!',React.createElement("span", null, "world")]
  }
};

// React.createElement("span", null, "world")  => {type: 'span',props: {},children: 'world'}
//方法接受三个参数,第一个参数是组件类型,第二个参数是要传递给组件的属性,第三个参数是children。方法最终会返回一个具有以下属性的对象
export function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};   // 等同于config

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  // config对象是dom的属性总和
  if (config != null) {
    if (hasValidRef(config)) {  // 判断config自身是否有ref的属性
      ref = config.ref;
    }
    if (hasValidKey(config)) {  // 判断config自身是否有key的属性,参考for循环添加的key属性
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (hasOwnProperty.call(config, propName) &&!RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }

  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  const childrenLength = arguments.length - 2;  // 获取到子元素
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

5. 虚拟DOM

virtual dom是通过JSX编译生成的一个JavaScript对象,正如上面所讲的调用React.createElement(component, props, ...children)返回的结果,这个结果其实就是虚拟DOM。

// 虚拟dom
const vdom = {
  type: 'h1',
  props: {
    id: 'title'
    className: 'title_class',
    style: {
        color: 'red'
    }
    children: ['Hello, world!',React.createElement("span", null, "world")]
  }
};
img
  • 传统web应用和React web应用

    传统web应用:

    img

    React web应用:

img
  • 虚拟DOM的原理(下节课跟diff算法一起再详细介绍)

    1. 虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象

    2. 状态变更时,记录新树和旧树的差异

    3. 最后把差异更新到真正的dom中

      img
  • 虚拟DOM的优点

    最终表现在DOM上的修改只是变更的部分,可以保证非常高效的渲染。

  • 虚拟DOM的缺点

    首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。

引入虚拟DOM也是React提升性能的一种手段,React非常快速是因为它从不直接操作真实的DOM。虚拟DOM是在DOM的基础上建立了一个抽象层,对数据和状态所做的任何改动,都会被自动且高效的同步到虚拟DOM,最后再批量同步到DOM中。我们开发项目不用接触这个虚拟DOM,但是理解其运行机制不仅有助于更好的理解React组件的生命周期,而且对于进一步优化 React程序也有很大帮助。

6. 渲染DOM

我们拿到了虚拟DOM,最终要渲染到页面中去,生成真实的DOM,具体是怎么实现的呢?

  • 创建元素createElement
export function createElement(
  type: string,  // 标签名
  props: Object,  // 属性
  rootContainerElement: Element | Document,  // 根节点
  parentNamespace: string,
): Element {
  let isCustomComponentTag;
  // 获取ownerDocument
  const ownerDocument: Document = getOwnerDocumentFromRootContainer(
    rootContainerElement,
  );
  let domElement: Element;
  let namespaceURI = parentNamespace;
  if (namespaceURI === HTML_NAMESPACE) {
    namespaceURI = getIntrinsicNamespace(type);
  }
  // 命名空间是否等于W3C规范 http://www.w3.org/1999/xhtml
  if (namespaceURI === HTML_NAMESPACE) {
    if (type === 'script') {  // 创建script标签
      const div = ownerDocument.createElement('div');
      div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
      const firstChild = ((div.firstChild: any): HTMLScriptElement);
      domElement = div.removeChild(firstChild);
    } else if (typeof props.is === 'string') {
      domElement = ownerDocument.createElement(type, {is: props.is});
    } else {
      domElement = ownerDocument.createElement(type);
      // 对select标签做处理
      if (type === 'select') {
        const node = ((domElement: any): HTMLSelectElement);
        if (props.multiple) {
          node.multiple = true;
        } else if (props.size) {
          node.size = props.size;
        }
      }
    }
  } else {
    domElement = ownerDocument.createElementNS(namespaceURI, type);
  }

  return domElement;
}
  • 配置属性、样式以及渲染内容
function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean,
): void {
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) {
      continue;
    }
    const nextProp = nextProps[propKey];
    if (propKey === STYLE) {
      // 配置style里面的样式
      setValueForStyles(domElement, nextProp);  // 下文有贴出源码setValueForStyles
    } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
      // 富文本渲染 dangerouslySetInnerHTML属性
      const nextHtml = nextProp ? nextProp[HTML] : undefined;
      if (nextHtml != null) {
        setInnerHTML(domElement, nextHtml);
      }
    } else if (propKey === CHILDREN) {
      if (typeof nextProp === 'string') {
        // 单纯文本渲染
        const canSetTextContent = tag !== 'textarea' || nextProp !== '';
        if (canSetTextContent) {
          setTextContent(domElement, nextProp);  // 下文有贴出源码setTextContent
        }
      } else if (typeof nextProp === 'number') {
        // 遇到数字转成字符串再渲染
        setTextContent(domElement, '' + nextProp);
      }
    }else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        if (propKey === 'onScroll') {
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
    } else if (nextProp != null) {
      // setAttribute 设置属性以及对应的内容
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}
export function setValueForStyles(node, styles) {
  const style = node.style;
  for (let styleName in styles) {
    if (!styles.hasOwnProperty(styleName)) {
      continue;
    }
    const isCustomProperty = styleName.indexOf('--') === 0;
    const styleValue = dangerousStyleValue(
      styleName,
      styles[styleName],
      isCustomProperty,
    );
    if (styleName === 'float') {
      styleName = 'cssFloat';
    }
    if (isCustomProperty) {
      style.setProperty(styleName, styleValue);
    } else {
      style[styleName] = styleValue;
    }
  }
}
const setTextContent = function(node: Element, text: string): void {
  if (text) {
    const firstChild = node.firstChild;

    if (firstChild && firstChild === node.lastChild && firstChild.nodeType === TEXT_NODE) {
      firstChild.nodeValue = text;
      return;
    }
  }
  node.textContent = text;
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容