最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React 16.8 浅析 [工作原理]

    正文概述 掘金(半路杀出个程咬金)   2020-11-30   575

    前言

    我们今天不分享 React 具体语法,组件, 通信 ,Ref ,Portals ,Context ,Hoc ,Hook 等等知识点,这些东西,大家细致的看一下官方文档都可以熟悉的进行开发任务,而今天我想谈谈 React 的工作原理 。

    Lets Go !

    什么是虚拟 Dom ? 什么是 Jsx ? React 又是怎么工作的 ? ... ....

    我们来根据上边的三个问题,来对 React 进行一次浅度剖析 !

    什么是虚拟 Dom ?

    Virtual DOM (虚拟Dom) 是一种编程概念 ,在这个概念里 ,UI 是以一种虚拟的结构形式保存在内存中,然后通过编译转换,变成真实Dom的一种技术。那它到底是什么呢 ?

    对象 ,yes ,就是一个 Javascript 对象 , 用这个 Javascript 对象来表示Dom信息和结构,当状态变更的时候,重新渲染这个对象结构,这个 Javascript 对象称为 Virtual DOM 。

    为什么不直接操作DOM ? 很简单,因为DOM操作很慢,细小的改变都有可能导致页面重排与重绘,耗性能 ; 所以 , 通过diff 算法对 Js 对象的操作可以批量的,最小化的执行 Dom 操作,从而提高性能 。diff 我们后期分析

    什么是Jsx ?

    你可以理解为是 React 独有的语法糖吧 , 其实 Jsx 是 javascript + xml 的语法扩展 , 但凡是遇到 { } 会被当做 Javascript 来执行,否则则是 xml 来执行 ;

    为什么需要用 Jsx ? 也许是因为 Jsx 模板简洁,语法灵活吧,这一点我并不是太清楚 ;

    原理:babel-loader 会预编译 Jsx 为 React.createElement(...) 函数执行 。

    那React 又是怎么工作的呢 ?

    Jsx 入手吧 , 首先来我们看官网的一段代码 【少许加工了一下,为了看的更明白】

    一个常规的 React 组件 【编译前】

    class HelloMessage extends React.Component {
      render() {
        return (
          <div className="HelloClass" num="1">
            Hello {this.props.name}
            <span> is span</span>
          </div>
        );
      }
    }
    ReactDOM.render(
      <HelloMessage name="Taylor" />,
      document.getElementById('hello-example')
    );
    

    这个常规的 React 组件,经过 babel-loader 【编译后】

    class HelloMessage extends React.Component {
      render() {
        return React.createElement(
          "div", // div 类型
          { className: "HelloClass", num: "1" }, // div 的属性
          "Hello ",  // div 的内容
          this.props.name, // div 的内容 , props 直接传递
          React.createElement( // 嵌套-继续
            "span",
            null,
            " is span"
          )
        );
      }
    }
    
    // 类组件也是转换成 `React.createElement(...)` 函数执行
    
    ReactDOM.render(React.createElement(HelloMessage, { name: "Taylor" }), 
    document.getElementById('hello-example'));
    

    看过代码,简单的分析过后,有没有发现,验证了我们上边说到的 Jsx 原理 ,经过babel-loader编译之后变成了React.createElement(...) 函数执行,如果有嵌套,则 React.createElement(...) 也嵌套

    有没有发现,我们不管是编译前,还是编译后,有一句代码没有变,那就是 class HelloMessage extends React.Component 继承了 React.component 类,其实简单点说,就是继承父组件的 props

    okey ! 到这里 ,我们可以有一个简单的结论,就是:

    1. React 组件需要继承 React.Component 类 class HelloMessage extends React.Component
    2. Jsx 语法经过 babel-loader 编译后会变成 React.createElement(...) 函数执行,多层则嵌套
    3. 通过 ReactDom.render 方法将元素挂载在页面上

    我们来依次分析,并简单实现一下 :

    React.component

    export default function Component (props) {
      this.props = props;
    }
    // 定义一个类组件与函数组件的标识 , 源码是这样标识的,虽然不知道为啥没有写成布尔值
    Component.prototype.isReactComponent = {};
    

    React.createElement(...)

    回到React.createElement(...) , 那么 React.createElement(...) 到底接收几个参数,分别又是干什么的呢 ? 可以看出来:

    • 参数1:类型 (渲染的元素类型,组件类型 等)
    • 参数2:属性 (元素或者组件的属性,包含 className,id,自定义 props 属性等)
    • 参数3:子节点内容 可能有很多个子级

    okey , 综上所述 ,我们来手写一个 mini 的 React.createElement() 。 在写 React.createElement(...) 之前,我们首先要搞懂两个事情,传入什么参数 ,返回什么值 ,传入什么参数,上边我们已经分析了出来,经过babel-loader 编译后需要传入三个参数分别是 类型,属性,子节点 ,那返回什么呢 ? 我们来看看createElement源码:

    // 位置 : react/packages/react/src/ReactElement.js 348 行 
    // 删掉了一些 if(_DEV_)的多余代码
    export function createElement(type, config, children) {
      // 一系列声明
      let propName;
      const props = {};
      let key = null;
      let ref = null;
      let self = null;
      let source = null;
      // 如果传入了 config , 对以上声明属性进行赋值
      if (config != null) {
        if (hasValidRef(config)) {
          ref = config.ref;
        }
        if (hasValidKey(config)) {
          key = '' + config.key;
        }
        self = config.__self === undefined ? null : config.__self;
        source = config.__source === undefined ? null : config.__source;
        for (propName in config) {
          if (
            hasOwnProperty.call(config, propName) &&
            !RESERVED_PROPS.hasOwnProperty(propName)
          ) {
            props[propName] = config[propName];
          }
        }
      }
    
      // children : (arguments.length - 2) 去除前两个参数的剩下所有参数
      // 最后将 children 并入 props 属性中
      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; // children 返回一个数组
      }
    
      // 默认值
      if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
          if (props[propName] === undefined) {
            props[propName] = defaultProps[propName];
          }
        }
      }
      // 最后返回 ReactElement 这个对象 
      return ReactElement(
        type,
        key,
        ref,
        self,
        source,
        ReactCurrentOwner.current,
        props,
      );
    

    一目了然 ,需要返回一个 ReactElement 对象,包含type,key, ref ,self , source , props 等 ,在这里我们主要为了了解 react 的工作原理 , 所以择取重要的来简单实现一下,暂时抛弃 key,ref,self 来, 话不多说,上代码

    /*
      三个参数 , 暂时返回两个属性 type 和 props 
      type:创建类型 , 原生标签 , 文本 , 函数组件 , 类组件  等
      config: 属性
      例举属性如 : 函数组件:{className: "border", name: "函数组件", __source: {…}, __self: undefined }
      children: 子节点 , 多少个就不知道了
    */
    function createElement (type, config, ...children) {
      // 移除 config 暂时不用的 __source: {…}, __self: undefined 属性,方便控制台查看
      if (config) {
        delete config.__source;
        delete config.__self;
      }
      // 这里不考虑 key . ref . slef
      const props = {
        ...config,
        /*
    		createTextNode 统一文本节点的数据结构,方便后续统一处理(child 直接就是文本内容)
    		如果过是一个 object 说明子节点下还有子节点,依然利用 React.createEement 继续编译为
    		{
    			type:"类型",
    			props:{}
    		}
    		的形式, 重点:babel-loader 将 jsx 编译成 React.creteElement 嵌套的形式 
    	*/  
        children: children.map(child =>
          typeof child === "object" ? child : createTextNode(child)
        )
      };
      console.log({type,props});
      return {
        type,
        props
      };
    }
    
    // 纯文本统一一下格式,跟其它元素一样的格式,方便处理,类型自定义为 "TEXT" 
    function createTextNode (text) {
      return {
        type: "TEXT",
        props: {
          children: [],
          nodeValue: text
        }
      };
    }
    

    来看看最后的返回结果,我自己的样例 React 16.8  浅析 [工作原理]

    okey ! 一个 mini 版本的 React.createElement() 方法就这样实现了,好,我们接着往下走,是不是到了 render 方法了 , 走,再去看一看源码 ~~~

    ReactDOM.render(···)

    render 方法在 react-dom 模块里边

    // 位置 : react/packages/react-dom/src/client/ReactDOMLegacy.js 287 行
    export function render(
      element: React$Element<any>, // react 元素
      container: Container, // 要放置的容器
      callback: ?Function, // 回调函数
    ) {
      invariant(
        isValidContainer(container),
        'Target container is not a DOM element.',
      );
      // ··· 这里返回调用了  legacyRenderSubtreeIntoContainer 方法 
      return legacyRenderSubtreeIntoContainer(
        null,
        element,
        container,
        false,
        callback,
      );
    }
    

    legacyRenderSubtreeIntoContainer 顺着继续往下走

    // 位置 : react/packages/react-dom/src/client/ReactDOMLegacy.js 175 行
    function legacyRenderSubtreeIntoContainer(
      parentComponent: ?React$Component<any, any>,
      children: ReactNodeList,
      container: Container,
      forceHydrate: boolean,
      callback: ?Function,
    ) {
      let root: RootType = (container._reactRootContainer: any);
      let fiberRoot;
      // 判断是不是第一次渲染该组件 , 如果是第一次则创建,然后更新 dom , 否则直接进行 diff 更新 dom
      if (!root) {
        root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
          container,
          forceHydrate,
        );
        fiberRoot = root._internalRoot;
        if (typeof callback === 'function') {
          const originalCallback = callback;
          callback = function() {
            const instance = getPublicRootInstance(fiberRoot);
            originalCallback.call(instance);
          };
        }
        // 更新 dom 
        unbatchedUpdates(() => {
          updateContainer(children, fiberRoot, parentComponent, callback);
        });
      } else {
        fiberRoot = root._internalRoot;
        if (typeof callback === 'function') {
          const originalCallback = callback;
          callback = function() {
            const instance = getPublicRootInstance(fiberRoot);
            originalCallback.call(instance);
          };
        }
        // 更新 dom 
        updateContainer(children, fiberRoot, parentComponent, callback);
      }
      return getPublicRootInstance(fiberRoot);
    }
    

    看到这里我们心中依然有了一个简单的概念,那就是

    • 1,render 方法接收三个参数,react 元素,渲染 react 元素的容器 ,回调函数
    • 2,紧跟着调用 legacyRenderSubtreeIntoContainer 方法,创建 dom || 更新dom ,这里边牵扯到两个重点,也是 React 的核心知识点【fiber 数据结构 , difff 算法】 我们后续来了解

    今天在这里呢,我们以简洁的方式来实现一下 render 方法 :

    创建一个 render 函数,做两件事儿(抛开回调函数来说)

    /*
      vnode  jsx 经过 React.createElemnt 编译后的虚拟 dom  
      container 容器  
      步骤 1 :  vnode -> node  , 虚拟 dom 转换为真实 dom 
      步骤 2 : container.appendChild(node);  , 将真实 dom   挂载给容器 container 
    */
    function render (vnode, container) {
      const node = createNode(vnode, container);
      node && container.appendChild(node);
    }
    

    实现一个 createNode 将虚拟 dom 转换为真实 dom

    /*
      虚拟 dom 渲染成真实 dom  
      vnode 虚拟 dom 
      parentNode 父节点 , 也就是将虚拟dom挂载的对应父容器   
    */
    function createNode (vnode, parentNode) {
      // 转换组合的最后真实 dom 节点
      let node = null; 
      /*
        type:"类型"
        props:参数和子节点 children 
      */
      const { type, props } = vnode;
    
      // todo 根据节点类型,生成 dom 节点 
      if (type === "TEXT") {
        // 文本节点 , type 上边自定义为 "TEXT" , nodeValue 是文本节点的内容值
        node = document.createTextNode("");
      } else if (typeof type === "string") {
        // 原生标签节点,type 类型为字符串 :如 div , p , span 等 , 创建对应的标签元素
        node = document.createElement(type);
      } else if (typeof type === "function") {
        // 类组件与函数组件 ,type 类型为 function 
        // isReactComponent 自定义区分标识 , Component 原型上定义 
        node = type.prototype.isReactComponent
          ? updateClassComponent(vnode, parentNode) // 类组件转换dom方法  
          : updateFunctionComponent(vnode, parentNode);  // 函数组件转换 dom 方法 
      }
    
      /*
      	子节点转换 dom , 其实就是遍历子节点 , 递归调用 render , 根据上边的逻辑继续转换
     	node : 当前节点,也就是当前子节点的父节点
        props.children : 子节点
        像类组件,函数组件一样,我们把它放置在外边
      */
      reconcileChildren(node, props.children);
    
      /*
    	更新 dom
    	node : 真实 node 节点
    	props :  props 属性和子节点属性以及子节点内容值
    	将所有属性和子节点属性以及内容值解析渲染给node 
      */   
      updateNode(node, props);
    
      return node;
    }
    

    好了,一个简洁的 render 方法逻辑已经呈现在我们的眼前了,剩下的就是要一步步实现对应的转换逻辑了,我们一个一个来:

    类组件

    先模拟写一个类组件,继承了上边我们自定义的 React.component 类 ;

    class ClassComponent extends Component {
      render () {
        return (
          <div className="border">
            ClassComponent - {this.props.name}
          </div>
        );
      }
    }
    

    解析类组件其实就是需要先实例化,然后再执行 render 返回对应的虚拟 dom 再通过上边 createNode 逻辑进行转换

    function updateClassComponent (vnode, parentNode) {
      const { type, props } = vnode;
      const instance = new type(props);
      const vvnode = instance.render();
      const node = createNode(vvnode, parentNode);
      return node;
    }
    

    函数组件

    function FunctionComponent (props) {
      return <div className="border">FunctionComponent-{props.name}</div>;
    }
    

    函数组件就比类组件更简单一些,直接执行函数,返回对应的虚拟 dom 再通过 createNode 逻辑进行转换

    function updateFunctionComponent (vnode, parentNode) {
      const { type, props } = vnode;
      // console.log(vnode) // 打印出来看看 , 创建的时候就决定了类型,原生标签,文本,函数,类,等   
      const vvnode = type(props); // 执行函数,返回子节点  
      const node = createNode(vvnode, parentNode);
      return node;
    }
    

    子节点 遍历子节点,递归执行 render 走到 createNode 转换逻辑 ;

    function reconcileChildren (node, children) {
      for (let i = 0; i < children.length; i++) {
        const child = children[i];
        render(child, node);
      }
    }
    

    更新dom

    更新 dom 节点 , nextVal => props 属性和子节点属性以及值 最后挂载给真实 dom node 节点 ,返回

    function updateNode (node, nextVal) {
      Object.keys(nextVal)
        .filter(k => k !== "children")
        .forEach(k => {
          // console.log(node[k] + "----" + nextVal[k]);
          node[k] = nextVal[k];
        });
    }
    

    回炉 ~

    再回到我们的官方实例, 是不是一个简单的工作原理已经了然于心了

    class HelloMessage extends React.Component {
      render() {
        return (
          <div>
            Hello {this.props.name}
          </div>
        );
      }
    }
    ReactDOM.render(
      <HelloMessage name="Taylor" />,
      document.getElementById('hello-example')
    );
    

    mini-react-demo 入口

    后续: 我们再来研究 fiber 数据结构 和 diff 算法


    起源地下载网 » React 16.8 浅析 [工作原理]

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元