React原理剖析

此文项目代码:https://github.com/bei-yang/I-want-to-be-an-architect
码字不易,辛苦点个star,感谢!

引言


此篇文章主要涉及以下内容:

  1. react核心api
  2. 探究setState
  3. 探究diff算法

学习资源


  1. react

react核心API


const React = {
     Children: {
      map,
      forEach,
      count,
      toArray,
      only,
    },
     createRef,
     Component,
     PureComponent,
     createContext,
     forwardRef,
     lazy,
     memo,
     useCallback,
     useContext,
     useEffect,
     useImperativeHandle,
     useDebugValue,
     useLayoutEffect,
     useMemo,
     useReducer,
     useRef,
     useState,
     Fragment: REACT_FRAGMENT_TYPE,
     StrictMode: REACT_STRICT_MODE_TYPE,
     Suspense: REACT_SUSPENSE_TYPE,
     createElement: __DEV__ ? createElementWithValidation : createElement,
     cloneElement: __DEV__ ? cloneElementWithValidation : cloneElement,
     createFactory: __DEV__ ? createFactoryWithValidation : createFactory,
     isValidElement: isValidElement,
     version: ReactVersion,
     unstable_ConcurrentMode: REACT_CONCURRENT_MODE_TYPE,
     unstable_Profiler: REACT_PROFILER_TYPE,
     __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: ReactSharedInternals,
    };
    // Note: some APIs are added with feature flags.
    // Make sure that stable builds for open source
    // don't modify the React object to avoid deopts.
    // Also let's not expose their names in stable builds.
    if (enableStableConcurrentModeAPIs) {
     React.ConcurrentMode = REACT_CONCURRENT_MODE_TYPE;
     React.Profiler = REACT_PROFILER_TYPE;
     React.unstable_ConcurrentMode = undefined;
     React.unstable_Profiler = undefined;
    }
    export default React;

核心精简后:

const React = {
  createElement,
  Component
}

react-dom主要是render逻辑
最核心的api:

  • React.createElement:创建虚拟dom
  • React.Component:实现自定义组件
  • ReactDOM.render:渲染真实DOM

JSX


JSX是对js的扩展,能带来更好的执行速度。
在线尝试
JSX预处理前:


JSX预处理后:

使用自定义组件的情况

function Comp(props) {
 return <h2>hi {props.name}</h2>;
}
const jsx = (
 <div id="demo">
  <span>hi</span>
  <Comp name="kaikeba" />
 </div>
);
console.log(jsx);
ReactDOM.render(jsx, document.querySelector("#root"));

build后

function Comp(props) {
 return React.createElement(
  "h2",
  null,
  "hi ",
  props.name
)
}
ReactDOM.render(React.createElement(
 "div",
{ id: "demo" },
 React.createElement("span", null, "hi"),
 React.createElement(Comp, { name: "kaikeba" })
), mountNode)

构建vdom用js对象形式来描述dom树结构一一对应


输出vdom观察其结构

实现三大接口:React.createElement,React.Component,ReactDom.render


CreateElement

  • 创建./src/kreact.js,它需要包含createElement方法
function createElement() {
  console.log(arguments)
}
export default {createElement}
  • 修改index.js实际引入kcreate,测试
import React from "./kreact"
  • 更新kcreate.js:为createElement添加参数并返回结果对象
function createElement(type, props,...children){
  // 父元素需要子元素返回结果,这里可以通过JSX编译后的代码得出结论
  props.children = children;
  return {type,props}
}
export default {createElement}

render

  • kreact-dom需要提供一个render函数,能够将vdom渲染出来,这里先打印vdom
function render(vnode, container){
  container.innerHTML = `<pre>${JSON.stringify(vnode,null,2)}</pre>`
}
export default {render}

页面效果


创建kvdom.js:将createElement返回的结果对象转换为vdom

  • 传递给createElement的组件有三种组件类型,使用type属性标识,并且抽离vdom相关代码到kvdom.js
  1. dom组件
  2. 函数式组件
  3. class组件
// kvdom.js
export function createVNode(vtype, type, props) {
  let vnode = {
    vtype: vtype, // 用于区分函数组件、类组件、原生组件
    type: type,
    props: props
 }
  return vnode
}
  • 添加Component类,kreact.js
export class Component {
  // 这个组件来区分是不是class组件
  static isClassComponent = true
  constructor(props){
    this.props = props
    this.state = {}
 }
}

浅层封装,setState现在只是一个占位符

  • 添加类组件,index.js
import React, {Component} from "./kreact";
class Comp2 extends Component {
 render() {
  return (
   <div>
    <h2>hi {this.props.name}</h2>
   </div>
 )
}
}
  • 判断组件类型,kreact.js
import { createVNode } from "./kvdom";
function createElement(type, props, ...children) {
 //...
 //判断组件类型
 let vtype;
 if (typeof type === "string") {
  // 原生标签
  vtype = 1;
}  else if (typeof type === "function") {
  if (type.isReactComponent) {
   // 类组件
   vtype = 2;
 } else {
   // 函数组件
   vtype = 3;
 }
}
 return createVNode(vtype, type, props);
}
  • 转换vdom为真实dom

export function initVNode(vnode) {
     let { vtype } = vnode;
     if (!vtype) {
      // 没有vtype,是一个文本节点
      return document.createTextNode(vnode);
    }
     if (vtype === 1) {
      // 1是原生元素
      return createElement(vnode);
    } else if (vtype === 2) {
      // 2是类组件
      return createClassComp(vnode);
    } else if (vtype === 3) {
      // 3是函数组件
      return createFuncComp(vnode);
    }
    }
    // 创建原生元素
    function createElement(vnode) {
     const { type, props } = vnode;
     const node = document.createElement(type);
     // 过滤key,children等特殊props
     const { key, children, ...rest } = props;
     Object.keys(rest).forEach(k => {
      // 需要特殊处理的属性名:class和for
      if (k === "className") {
       node.setAttribute("class", rest[k]);
     } else if (k === "htmlFor") {
       node.setAttribute("for", rest[k]);
     } else {
       node.setAttribute(k, rest[k]);
     }
    });
     
     // 递归初始化子元素
     children.forEach(c => {
      // 子元素也是一个vnode,所以调用initVNode
      node.appendChild(initVNode(c));
    });
     return node;
    }
    // 创建函数组件
    function createFuncComp(vnode) {
     const { type, props } = vnode;
     // type是函数,它本身即是渲染函数,返回vdom
     const newNode = type(props);
     return initVNode(newNode);
    }
    // 创建类组件
    function createClassComp(vnode) {
     const { type } = vnode;
     // 创建类组件实例
     const component = new type(vnode.props);
     // 调用其render获得vdom
     const newNode = component.render();
     return initVNode(newNode);
    }
  • 执行渲染,kreact-dom.js
import { initVNode } from "./kvdom";
function render(vnode, container) {
 const node = initVNode(vnode);
 container.appendChild(node);
 // container.innerHTML = `<pre>${JSON.stringify(vnode,null,2)}</pre>`
}
  • 渲染vdom数组,index.js
class Comp2 extends Component {
 render() {
  const users=[{id:1,name:'tom'},{id:2,name:'jerry'}]
  return (
   <div>
    <h2>hi {this.props.name}</h2>
    <ul>
    {users.map(user=>(<li key={user.id}>{user.name}</li>))}
    </ul>
   </div>
 )
}
}
// 测试报错,因为kvdom中没有考虑到该情况
  • 处理vdom数组,kvdom.js
children.forEach(c => {
  if (Array.isArray(c)) {// c是vdom数组的情况
   c.forEach(n => {
    node.appendChild(initVNode(n));
  });
 } else {
   node.appendChild(initVNode(c));
 }
});

总结

  1. webpack+babel编译时,替换JSX为React.createElement(type,props,...children)
  2. 所有React.createElement()执行结束后得到一个JS对象,它能够完整描述dom结构,称之为vdom
  3. ReactDOM.render(vdom,container)可以将vdom转换为dom并追加到container中,通过递归遍历vdom树,根据vtype不同,执行不同逻辑:vtype为1生成原生元素,为2则需要将类组件实例化并执行其render将返回vdom初始化,为3则将函数执行并将返回vdom初始化。

PureComponent

继承Component,主要是设置了shouldComponentUpdate生命周期

import shallowEqual from './shallowEqual'
import Component from './Component'
export default function PureComponent(props, context) {
Component.call(this, props, context)
}
PureComponent.prototype = Object.create(Component.prototype)
PureComponent.prototype.constructor = PureComponent
PureComponent.prototype.isPureReactComponent = true
PureComponent.prototype.shouldComponentUpdate = shallowCompare
function shallowCompare(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) ||
      !shallowEqual(this.state, nextState)
}

setState


class组件的特点,就是拥有特殊状态并且可以通过setState更新状态,从而重新渲染视图,是学习react中最重要的api。
setState并没有直接操作去渲染,而是执行了一个异步的update队列,我们使用一个类来专门管理,./kkreact/Component.js

export let updateQueue = {
  updaters: [],
  isPending: false,
  add(updater) {
    _.addItem(this.updaters, updater);
  },
  batchUpdate() {
    if (this.isPending) {
      return;
    }
    this.isPending = true;
    /*
    each updater.update may add new updater to updateQueue
    clear them with a loop
    event bubbles from bottom-level to top-level
    reverse the updater order can merge some props and state and reduce the
    refresh times
    see Updater.update method below to know why
    */
    let { updaters } = this;
    let updater;
    while ((updater = updaters.pop())) {
      updater.updateComponent();
    }
    this.isPending = false;
  }
};
function Updater(instance) {
  this.instance = instance;
  this.pendingStates = [];
  this.pendingCallbacks = [];
  this.isPending = false;
  this.nextProps = this.nextContext = null;
  this.clearCallbacks = this.clearCallbacks.bind(this);
}
Updater.prototype = {
  emitUpdate(nextProps, nextContext) {
    this.nextProps = nextProps;
    this.nextContext = nextContext;
    // receive nextProps!! should update immediately
    nextProps || !updateQueue.isPending
      ? this.updateComponent()
      : updateQueue.add(this);
  },
  updateComponent() {
    let { instance, pendingStates, nextProps, nextContext } = this;
    if (nextProps || pendingStates.length > 0) {
      nextProps = nextProps || instance.props;
      nextContext = nextContext || instance.context;
      this.nextProps = this.nextContext = null;
      // merge the nextProps and nextState and update by one time
      shouldUpdate(
        instance,
        nextProps,
        this.getState(),
        nextContext,
        this.clearCallbacks
      );
    }
  },
  addState(nextState) {
    if (nextState) {
      _.addItem(this.pendingStates, nextState);
      if (!this.isPending) {
        this.emitUpdate();
      }
    }
  },
  replaceState(nextState) {
    let { pendingStates } = this;
    pendingStates.pop();
    // push special params to point out should replace state
    _.addItem(pendingStates, [nextState]);
  },
  getState() {
    let { instance, pendingStates } = this;
    let { state, props } = instance;
    if (pendingStates.length) {
      state = _.extend({}, state);
      pendingStates.forEach(nextState => {
        let isReplace = _.isArr(nextState);
        if (isReplace) {
          nextState = nextState[0];
        }
        if (_.isFn(nextState)) {
          nextState = nextState.call(instance, state, props);
        }
        // replace state
        if (isReplace) {
          state = _.extend({}, nextState);
        } else {
          _.extend(state, nextState);
        }
      });
      pendingStates.length = 0;
    }
    return state;
  },
  clearCallbacks() {
    let { pendingCallbacks, instance } = this;
    if (pendingCallbacks.length > 0) {
      this.pendingCallbacks = [];
      pendingCallbacks.forEach(callback => callback.call(instance));
    }
  },
  addCallback(callback) {
    if (_.isFn(callback)) {
      _.addItem(this.pendingCallbacks, callback);
    }
  }
};

虚拟dom&&diff算法


常见问题:react virtual dom 是什么?说一下diff算法?
what?用JavaScript对象表示DOM信息和结构,当状态变更的时候,重新渲染这个JavaScript的对象结构,这个JavaScript对象称为virtual dom;
传统dom渲染流程


diff算法

diff策略

  1. 同级比较,Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。



  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
    例如:div->p,CompA->CompB
  3. 对于同一层级的一组子节点,通过唯一的key进行区分。

基于以上三个前提策略,React分别对tree diff、component diff以及element diff进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。

  • tree diff
  • component diff
  • element diff

element diff


差异类型:

  1. 替换原来的节点,例如把div换成了p,Comp1换成Comp2;
  2. 移动、删除、新增子节点,例如ul中的多个子节点li中出现了顺序互换;
  3. 修改了节点的属性,例如节点类名发生了变化;
  4. 对于文本节点,文本内容可能会改变。

重排(reorder)操作:
INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和REMOVE_NODE(删除)

  • INSERT_MARKUP,新的component类型不在老集合里,即是全新的节点,需要对新节点执行插入操作。

  • MOVE_EXISTING,在老集合有新component类型,且element是可更新的类型。

  • REMOVE_NODE,老component类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者老component不在新集合里的,也需要执行删除操作。



ReactDom.render

function renderTreeIntoContainer(vnode, container, callback, parentContext) {
  if (!vnode.vtype) {
    throw new Error(`cannot render ${vnode} to container`);
  }
  if (!isValidContainer(container)) {
    throw new Error(`container ${container} is not a DOM element`);
  }
  let id = container[COMPONENT_ID] || (container[COMPONENT_ID] = _.getUid());
  let argsCache = pendingRendering[id];
  // component lify cycle method maybe call root rendering
  // should bundle them and render by only one time
  if (argsCache) {
    if (argsCache === true) {
      pendingRendering[id] = argsCache = { vnode, callback, parentContext };
    } else {
      argsCache.vnode = vnode;
      argsCache.parentContext = parentContext;
      argsCache.callback = argsCache.callback
        ? _.pipe(
            argsCache.callback,
            callback
          )
        : callback;
    }
    return;
  }
  pendingRendering[id] = true;
  let oldVnode = null;
  let rootNode = null;
  if ((oldVnode = vnodeStore[id])) {
    rootNode = compareTwoVnodes(
      oldVnode,
      vnode,
      container.firstChild,
      parentContext
    );
  } else {
    rootNode = initVnode(vnode, parentContext, container.namespaceURI);
    var childNode = null;
    while ((childNode = container.lastChild)) {
      container.removeChild(childNode);
    }
    container.appendChild(rootNode);
  }
  vnodeStore[id] = vnode;
  let isPending = updateQueue.isPending;
  updateQueue.isPending = true;
  clearPending();
  argsCache = pendingRendering[id];
  delete pendingRendering[id];
  let result = null;
  if (typeof argsCache === "object") {
    result = renderTreeIntoContainer(
      argsCache.vnode,
      container,
      argsCache.callback,
      argsCache.parentContext
    );
  } else if (vnode.vtype === VELEMENT) {
    result = rootNode;
  } else if (vnode.vtype === VCOMPONENT) {
    result = rootNode.cache[vnode.uid];
  }
  if (!isPending) {
    updateQueue.isPending = false;
    updateQueue.batchUpdate();
  }
  if (callback) {
    callback.call(result);
  }
  return result;
}

redux

  1. 为什么需要redux,他是什么
  2. 解决了什么问题
  3. 如何使用
  4. 单向数据流
export function createStore(reducer, enhancer) {
  if (enhancer) {
    return enhancer(createStore)(reducer);
  }
  let currentState = {};
  let currentListeners = [];
  function getState() {
    return currentState;
  }
  function subscribe(listener) {
    currentListeners.push(listener);
  }
  function dispatch(action) {
    currentState = reducer(currentState, action);
    currentListeners.forEach(v => v());
    return action;
  }
  dispatch({ type: "@kaikeba/sheng" });
  return { getState, subscribe, dispatch };
}
export function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args);
    let dispatch = store.dispatch;
    const midApi = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    };
    const middlewareChain = middlewares.map(middleware => middleware(midApi));
    dispatch = compose(...middlewareChain)(store.dispatch);
    return {
      ...store,
      dispatch
    };
  };
}
export function compose(...funcs) {
  if (funcs.length == 0) {
    return arg => arg;
  }
  if (funcs.length == 1) {
    return funcs[0];
  }
  return funcs.reduce((ret, item) => (...args) => ret(item(...args)));
}
function bindActionCreator(creator, dispatch) {
  return (...args) => dispatch(creator(...args));
}
export function bindActionCreators(creators, dispatch) {
  return Object.keys(creators).reduce((ret, item) => {
    ret[item] = bindActionCreator(creators[item], dispatch);
    return ret;
  }, {});
}

react-redux

import React from "react";
import PropTypes from "prop-types";
import { bindActionCreators } from "./woniu-redux";
export const connect = (
  mapStateToProps = state => state,
  mapDispatchToProps = {}
) => WrapComponent => {
  return class ConnectComponent extends React.Component {
    static contextTypes = {
      store: PropTypes.object
    };
    constructor(props, context) {
      super(props, context);
      this.state = {
        props: {}
      };
    }
    componentDidMount() {
      const { store } = this.context;
      store.subscribe(() => this.update());
      this.update();
    }
    update() {
      const { store } = this.context;
      const stateProps = mapStateToProps(store.getState());
      const dispatchProps = bindActionCreators(
        mapDispatchToProps,
        store.dispatch
      );
      this.setState({
        props: {
          ...this.state.props,
          ...stateProps,
          ...dispatchProps
        }
      });
    }
    render() {
      return <WrapComponent {...this.state.props}></WrapComponent>;
    }
  };
};
export class Provider extends React.Component {
  static childContextTypes = {
    store: PropTypes.object
  };
  getChildContext() {
    return { store: this.store };
  }
  constructor(props, context) {
    super(props, context);
    this.store = props.store;
  }
  render() {
    return this.props.children;
  }
}

你的赞是我前进的动力

求赞,求评论,求分享...

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