Code Monkey home page Code Monkey logo

anu's Issues

scheduler的优化

确保里面为空

if(scheduler.count && !scheduler.setTimeoutID){
    scheduler.setTimeoutID = setTimeout(function(){
        console.log(scheduler.count, 'mountComponent')
       scheduler.run()
        scheduler.setTimeoutID = null
    })
}

React16的异步更新调研

React16通过createRoot实现全局异步,通过unstable_AsyncComponent实现局部异步

异步时间差可能大于100ms, unstable_AsyncComponent会让底下的组件更新全部变成异步

anu 最新版本 componentdidcatch 有问题

使用的是anu最新版本,从github直接下的.

import React from 'react';
import ReactDOM from 'react-dom';
// import PropTypes from 'props-type';

const Hello = ({ name }) => <h1>Hello {name}!</h1>;

class ShowMyError extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: false };
  }

  componentDidCatch(error, info) {
    this.setState({ error, info });
  }

  render() {
    if (this.state.error) {
      return (
        <div>
          <h1>
            Error AGAIN: {this.state.error.toString()}
          </h1>
          {this.state.info &&
            this.state.info.componentStack.split("\n").map(i => {
              return (
                <div key={i}>
                  {i}
                </div>
              );
            })}
        </div>
      );
    }
    return this.props.children;
  }
}

class Broken extends React.Component {
  constructor(props) {
    super(props);
    this.state = { throw: false, count: 0 };
  }

  render() {
    if (this.state.throw) {
      throw new Error("YOLO");
    }

    return (
      <div>
        <button
          onClick={e => {
            this.setState({ throw: true });
          }}
        >
          button will render error.
        </button>

        <button onClick={e => {
          this.setState(({ count }) => ({
            count: count + 1
          }));
        }}>button will not throw</button>

        <div>
          {"All good here. Count: "}{this.state.count}
        </div>
      </div>
    );
  }
}

class App extends React.Component {
  render() {
    const styles = {
      fontFamily: "sans-serif",
      textAlign: "center"
    };
    return (
      <div style={styles}>
        <Hello name="ShowMyError" />
        <h2>
          Start clicking to see some {"\u2728"}magic{"\u2728"}
        </h2>
        <ShowMyError>
          <Broken />
        </ShowMyError>
      </div>
    );
  }
}

ReactDOM.render(<App />, document.getElementById("root"));

以上的例子在Anu中的截图是:
2017-12-14 11 07 34
2017-12-14 11 07 40

下面是在react 16.2.0中的截图

2017-12-14 11 09 33

在componentDidMount钩子中执行setState不会引发死循环

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <style>
      .aaa {
            width: 200px;
            height: 200px;
            background: red;
        }
       .bbb {
            width: 200px;
            height: 200px;
            background: lawngreen;
        }
    </style> 
   <!-- <script type='text/javascript' src="./dist/polyfill.js"></script>-->
<!--<script type='text/javascript' src="./dist/React.js"></script>-->
 
    <script src="./test/react.js"></script>
     <script src="./test/react-dom.js"></script>
 <script src="./dist/babel.js"></script>
    <script type='text/babel'>
       class A extends React.Component{
           constructor(props){
               super(props)
               this.state = {
                   aaa: 1
               }
           }
           componentWillMount(){
              console.log('A will mount')
               this.setState({
                   aaa: 111
               })
           }
            componentDidMount(){
              console.log('A did mount')
           }
           render(){
               return <strong ref={(a)=>{console.log('A的内部',a)}}  >A内容{this.state.aaa}<B /></strong>
           }
       }
      class B extends React.Component{
           constructor(props){
               super(props)
               this.state = {
                   aaa: 2
               }
           }
           componentWillMount(){
               console.log('B will mount')
               this.setState({
                   aaa: 222
               })
           }
          componentDidMount(){
                 console.log('B did mount')
               this.setState({
                   aaa: 2222
               })
           }
           componentWillUpdate(){
              console.log('B will change')
           }
           render(){
               return <strong>{this.state.aaa}</strong>
           }
       }
        class C extends React.Component{
           constructor(props){
               super(props)
               this.state = {
                   aaa: 3
               }
           }
           componentWillMount(){
              console.log('C will mount')
               this.setState({
                   aaa: 333
               })
           }
            componentDidMount(){
              console.log('C did mount')
           }
           render(){
               return <span>{this.state.aaa}<D /></span>
           }
       }
        class D extends React.Component{
           constructor(props){
               super(props)
               this.state = {
                   aaa: 4
               }
           }
           componentWillMount(){
              console.log('D will mount')
               this.setState({
                   aaa: 444
               })
           }
            componentDidMount(){
              console.log('D did mount')
           }
            
           render(){
               return <span>{this.state.aaa}</span>
           }
       }
  
       
       class App extends React.Component{
            click(){
                length = 7
                length2 = 5
                this.forceUpdate()
            }
            render(){
               return <div><A /><div ref={()=>{console.log('1.1')}}><div ref={()=>{console.log('1.2')}}>AC之间</div></div><C /><div ref={()=>{console.log('执行aaa')}} >xxxx</div></div>
           }
       }
      
       window.onload = function(){
           ReactDOM.render(<App />, document.getElementById('example'))
       }
       </script>
</head>

<body>

    <div>这个默认会被清掉</div>
    <div id='example'></div>


</body>

</html>

快速扁平化children,并为它添加上deep

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <style>
        .aaa {
            width: 200px;
            height: 200px;
            background: red;
        }

        .bbb {
            width: 200px;
            height: 200px;
            background: lawngreen;
        }
    </style>

    <!--<script type='text/javascript' src="./dist/React.js"></script>-->
    <!-- <script src="./test/react.js"></script>
    <script src="./test/react-dom.js"></script>
    <script src="./test/redux.js"></script>
    <script src="./test/react-redux.js"></script>
    <script src="./test/babel.js"></script>-->
    <script>
        window.onload = function () {
          
         
           var React = {
                createElement: function (type, configs) {
                  
                    var props = {},
                        key = null,
                        ref = null,
                        pChildren = null, //位于props中的children
                        props = {},
                        stack = [],
                        vtype = 1,
                        typeType = typeof type,

                        isEmptyProps = true
                    if (configs) {
                        for (var i in configs) {
                            var val = configs[i]
                            if (i === 'key') {
                                key = val
                            } else if (i === 'ref') {
                                ref = val
                            } else if (i === 'children') {
                                pChildren = val
                            } else {
                                isEmptyProps = false
                                props[i] = val
                            }
                        }
                    }
                
                    if (typeType === 'function') {
                        vtype = type.prototype && type.prototype.render ?
                            2 :
                            4
                    }
                    for (var i = 2, n = arguments.length; i < n; i++) {
                        stack.push(arguments[i])
                    }

                    if (!stack.length && pChildren && pChildren) {
                        stack.push(pChildren)
                    }
                   
                    props.children = flattenChildren(stack)
                    return new VNode(type, props, key, vtype)

                }
            }
            function flattenChildren(){
                 
            }
            function VNode(type, props, key, vtype) {
                this.type = type
                this.props = props
                this.vtype = vtype
                this.deep = 0
                if (key) {
                    this.key = key
                }

            }

            var a = React.createElement('div', {}, "xxx", "yyy", ["aaa", 'bbb', [React.createElement('span', {})],
                    React.createElement('p', {}), [React.createElement('b', {})]
                ],  "zzzz")

            console.log(a)
        //==========a 的结构如下:
           var a = {type: 'div', vtype: 1, props: {
                children: [
                    {type: '#text', deep: 1, text: "xxxyyyaaabbb"},{type: 'span', deep: 2, props: {}, vtype: 1},
                    {type: 'p', deep: 1, props: {}, vtype: 1},{type: 'b', deep: 2, props: {}, vtype: 1},
                    {type: '#text', deep: 0, text: "zzzz"}
                ]
            }}
        }
    </script>
</head>

<body>

    <div>这个默认会被清掉</div>
    <div id='example'></div>


</body>

</html>

受控组件的复杂度控制

原来非受控组件的if else分支太多,会影响效率,也影响检测程序的评估分数
image

function getOptionValue(props) {
    //typeof props.value === 'undefined'
    return isDefined(props.value)
        ? props.value
        : props.children[0].text
}
function isDefined(a) {
    return !(a === null || a === undefined)
}
export function postUpdateSelectedOptions(vnode) {
    var props = vnode.props,
        multiple = !!props.multiple,
        value = isDefined(props.value)
            ? props.value
            : isDefined(props.defaultValue)
                ? props.defaultValue
                : multiple
                    ? []
                    : '',
        options = [];
    collectOptions(vnode, props, options)
    if (multiple) {
        updateOptionsMore(vnode, options, options.length, value)
    } else {
        updateOptionsOne(vnode, options, options.length, value)
    }

}

function collectOptions(vnode, props, ret) {
    var arr = props.children
    for (var i = 0, n = arr.length; i < n; i++) {
        var el = arr[i]
        if (el.type === 'option') {
            ret.push(el)
        } else if (el.type === 'optgroup') {
            collectOptions(el, el.props, ret)
        }
    }
}

function updateOptionsOne(vnode, options, n, propValue) {
    // Do not set `select.value` as exact behavior isn't consistent across all
    // browsers for all cases.
    var selectedValue = '' + propValue;
    for (let i = 0; i < n; i++) {
        let option = options[i]
        let value = getOptionValue(option.props)
        if (value === selectedValue) {
            setDomSelected(option, true)
            return
        }
    }
    if (n) {
        setDomSelected(options[0], true)
    }

}

function updateOptionsMore(vnode, options, n, propValue) {

    var selectedValue = {}
    try {
        for (let i = 0; i < propValue.length; i++) {
            selectedValue['&' + propValue[i]] = true
        }
    } catch (e) {
        /* istanbul ignore next */
        console.warn('<select multiple="true"> 的value应该对应一个字符串数组')
    }
    for (let i = 0; i < n; i++) {
        let option = options[i]
        let value = getOptionValue(option.props)
        let selected = selectedValue.hasOwnProperty('&' + value)
        setDomSelected(option, selected)

    }
}

function setDomSelected(option, selected) {
    if (option._hostNode) {
        option._hostNode.selected = selected
    }
}

//react的单向流动是由生命周期钩子的setState选择性调用(不是所有钩子都能用setState),受控组件,事务机制

function stopUserInput(e) {
    var target = e.target
    var name = e.type === 'textarea'
        ? 'innerHTML'
        : 'value'
    target[name] = target._lastValue
}

function stopUserClick(e) {
    e.preventDefault()
}

export function processFormElement(vnode, dom, props) {
    var domType = dom.type
    if (/text|password|number|date|time|color|month/.test(domType)) {
        if ('value' in props && !hasOtherControllProperty(props, textMap)) {
            console.warn(`你为${domType}元素指定了value属性,但是没有提供另外的${Object.keys(textMap)}
           等用于控制value变化的属性,那么它是一个非受控组件,用户无法通过输入改变元素的value值`)
            dom.oninput = stopUserInput
        }
    } else if (/checkbox|radio/.test(domType)) {
        if ('checked' in props && !hasOtherControllProperty(props, checkedMap)) {
            console.warn(`你为${domType}元素指定了value属性,但是没有提供另外的${Object.keys(checkedMap)}
           等用于控制value变化的属性,那么它是一个非受控组件,用户无法通过输入改变元素的value值`)
            dom.onclick = stopUserClick
        }
    } else if (/select/.test(domType)) {
        if (!('value' in props || 'defaultValue' in props)) {
            console.warn(`select元素必须指定value或defaultValue属性`)
        }
        postUpdateSelectedOptions(vnode)
    }
}

var textMap = {
    onChange: 1,
    onInput: 1,
    readOnly: 1,
    disabled: 1
}
var checkedMap = {
    onChange: 1,
    onClick: 1,
    readOnly: 1,
    disabled: 1
}

function hasOtherControllProperty(props, map) {
    for (var i in props) {
        if (map[i]) {
           return true
        }
    }
    return false
}

上面的processFormElement 的复杂度为8。

如果使用映射,可以大大减少复杂度

export function processFormElement(vnode, dom, props) {
    var domType = dom.type
    var duplexType = duplexMap[domType]
    if (duplexType) {
        var data = duplexData[duplexType]
        var duplexProp = data[0]
        var keys = data[1]
        var eventName = data[2]
        if (duplexProp in props && !hasOtherControllProperty(props, keys)) {
            console.warn(`你为${vnode.type}[type=${domType}]元素指定了${duplexProp}属性,但是没有提供另外的${Object.keys(keys)}
           等用于控制${duplexProp}变化的属性,那么它是一个非受控组件,用户无法通过输入改变元素的${duplexProp}值`)
            dom[eventName] = data[3]
        }
        if (duplexType === 3) {
            postUpdateSelectedOptions(vnode)
        }

    }

}

function hasOtherControllProperty(props, keys) {
    for (var key in props) {
        if (keys[key]) {
            return true
        }
    }
}
var duplexMap = {
    color: 1,
    date: 1,
    datetime: 1,
    'datetime-local': 1,
    email: 1,
    month: 1,
    number: 1,
    password: 1,
    range: 1,
    search: 1,
    tel: 1,
    text: 1,
    time: 1,
    url: 1,
    week: 1,
    textarea: 1,
    checkbox: 2,
    radio: 2,
    'select-one': 3,
    'select-multiple': 3
}


function preventUserInput(e) {
    var target = e.target
    var name = e.type === 'textarea' ?
        'innerHTML' :
        'value'
    target[name] = target._lastValue
}

function preventUserClick(e) {
    e.preventDefault()
}

function preventUserChange(e) {
    var target = e.target
    var value = target._lastValue
    var options = target.options
    if (target.multiple) {
        updateOptionsMore(options, options.length, value)
    } else {
        updateOptionsOne(options, options.length, value)
    }
}

var duplexData = {
    1: ['value', {
        onChange: 1,
        onInput: 1,
        readOnly: 1,
        disabled: 1
    }, 'oninput', preventUserInput],
    2: ['checked', {
        onChange: 1,
        onClick: 1,
        readOnly: 1,
        disabled: 1
    }, 'onclick', preventUserClick],
    3: ['value', {
        onChange: 1,
        disabled: 1
    }, 'onchange', preventUserChange]
}


export function postUpdateSelectedOptions(vnode) {
    var props = vnode.props,
        multiple = !!props.multiple,
        value = isDefined(props.value) ?
        props.value :
        isDefined(props.defaultValue) ?
        props.defaultValue :
        multiple ? [] :
        '',
        options = [];
    collectOptions(vnode, props, options)
    if (multiple) {
        updateOptionsMore(options, options.length, value)
    } else {
        updateOptionsOne(options, options.length, value)
    }

}

function isDefined(a) {
    return !(a === null || a === void 666)
}

/**
 * 收集虚拟DOM select下面的options元素,如果是真实DOM直接用select.options
 * 
 * @param {VNode} vnode 
 * @param {any} props 
 * @param {Array} ret 
 */
function collectOptions(vnode, props, ret) {
    var arr = props.children
    for (var i = 0, n = arr.length; i < n; i++) {
        var el = arr[i]
        if (el.type === 'option') {
            ret.push(el)
        } else if (el.type === 'optgroup') {
            collectOptions(el, el.props, ret)
        }
    }
}


function updateOptionsOne(options, n, propValue) {
    var selectedValue = '' + propValue;
    for (let i = 0; i < n; i++) {
        let option = options[i]
        let value = getOptionValue(option, option.props)
        if (value === selectedValue) {
            getOptionSelected(option, true)
            return
        }
    }
    if (n) {
        getOptionSelected(options[0], true)
    }
}

function updateOptionsMore(options, n, propValue) {
    var selectedValue = {}
    try {
        for (let i = 0; i < propValue.length; i++) {
            selectedValue['&' + propValue[i]] = true
        }
    } catch (e) {
        /* istanbul ignore next */
        console.warn('<select multiple="true"> 的value应该对应一个字符串数组')
    }
    for (let i = 0; i < n; i++) {
        let option = options[i]
        let value = getOptionValue(option, option.props)
        let selected = selectedValue.hasOwnProperty('&' + value)
        getOptionSelected(option, selected)
    }
}

function getOptionValue(option, props) {
    if (!props) {
        return getDOMOptionValue(option)
    }
    return props.value === undefined ? props.children[0].text : props.value
}

function getDOMOptionValue(node) {
    if (node.hasAttribute && node.hasAttribute('value')) {
        return node.getAttribute('value')
    }
    var attr = node.getAttributeNode('value')
    if (attr && attr.specified) {
        return attr.value
    }
    return node.innerHTML.trim()
}


function getOptionSelected(option, selected) {
    var dom = option._hostNode || option
    dom.selected = selected
}

image

销毁方法的改进

disposeVnode是用于销毁虚拟DOM及其下面的所有孩子, 并将真实DOM的内部清空

React16的componentDidCatch

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
  <title>树组件</title>

  <script type='text/javascript' src="./dist/React.js"></script>
  <script type='text/javascript' src="./lib/ReactTestUtils.js"></script>
  <script type='text/javascript' src="./lib/babel.js"></script>
    
</head>

<body>

    <div>测试</div>
    <div id='example'></div>
    <script type='text/babel'>
      var container = document.getElementById("example")
      var div = container
     // var PropTypes = React.PropTypes
      if(!window.ReactDOM){
        window.ReactDOM = window.React
       
      }
      var PropTypes = React.PropTypes
      var expect = function(a) {
          return {
              toBe: function(b) {
                  console.log(a, "\nvs\n",b, a === b)
              }
          }
      }
      var app;
      var count = 0;

      class App extends React.Component {
          render() {
              if (this.props.stage === 1) {
                  return <div><UnunmountableComponent /></div>;
              } else {
                  return null;
              }
          }
      }

      class UnunmountableComponent extends React.Component {
          componentWillUnmount() {
              app.setState({});
              count++;
              throw Error("always fails");
          }

          render() {
              return <div>Hello {this.props.name}</div>;
          }
      }

      var container = document.createElement("div");

      var setRef = ref => {
          if (ref) {
              app = ref;
          }
      };

   //   expect(function() {
     try{
          ReactDOM.render(<App ref={setRef} stage={1} />, container);
          ReactDOM.render(<App ref={setRef} stage={2} />, container);
    //  }).toThrow();
     }catch(e){
       console.log(e)
     }
      expect(count).toBe(1);
    </script>
    <pre>
     
    </pre>
</body>

</html>

image

updateChildren的改进

function updateChildren(lastVnode, nextVnode, parentNode, context, mountQueue) {
    let lastChildren = lastVnode.vchildren,
        nextChildren = flattenChildren(nextVnode),//nextVnode.props.children;
        childNodes = parentNode.childNodes,
        hashcode = {},
        mountAll = mountQueue.mountAll;
    if (nextChildren.length == 0) {
        lastChildren
            .forEach(function (el) {
                var node = el._hostNode;
                if (node) {
                    removeDOMElement(node);
                }
                disposeVnode(el);
            });
        return;
    }


    lastChildren.forEach(function (el) {
        let key = el.type + (el.key || "");
        if (el._disposed) {
            return;
        }
        let list = hashcode[key];
        if (list) {
            list.push(el);
        } else {
            hashcode[key] = [el];
        }
    });
    nextChildren.forEach(function (el) {
        let key = el.type + (el.key || "");
        let list = hashcode[key];
        if (list) {
            let old = list.shift();
            if (old) {
                el.old = old;
                if (!list.length) {
                    delete hashcode[key];
                }
            }
        }
    });
    for (let i in hashcode) {
        let list = hashcode[i];
        if (Array.isArray(list)) {
            list
                .forEach(function (el) {
                    let node = el._hostNode;
                    if (node) {
                        removeDOMElement(node);
                    }
                    disposeVnode(el);
                });
        }
    }

    var beforeDOM;

    for (let i = nextChildren.length - 1; i >= 0; i--) {
        let el = nextChildren[i],
            old = el.old,
            dom,
            queue = mountAll
                ? mountQueue
                : [];
        if (old) {
            delete el.old;
            if (el === old && old._hostNode && !contextHasChange) {
                //cloneElement
                dom = old._hostNode;
                let curDOM = childNodes[i];
                if (dom !== curDOM && curDOM) {
                    parentNode.replaceChild(dom, curDOM);
                }
            } else {
                dom = updateVnode(old, el, context, queue);
                if (!dom) {
                    dom = createDOMElement({ vtype: "#comment", text: "placeholder" });
                    replaceChildDeday([old, el, context, queue], dom, parentNode);
                }
            }
        } else {
            dom = mountVnode(el, context, null, queue);
        }
        if (dom !== beforeDOM) {
            insertDOM(parentNode, dom, beforeDOM);
        }
        beforeDOM = dom;
        if (!mountAll && queue.length) {
            clearRefsAndMounts(queue);
        }
    }
}

createElement应该与官方一致

function flattenChildren(stack) {
    var lastText,
        child,
        hasText = false,
        children = [];

    while (stack.length) {
        //比较巧妙地判定是否为子数组
        if ((child = stack.pop()) && child.pop) {
            if (child.toJS) {
                //兼容Immutable.js
                child = child.toJS();
            }
            for (let i = 0; i < child.length; i++) {
                stack[stack.length] = child[i];
            }
        } else {
            // eslint-disable-next-line
            var childType = typeNumber(child);

            if (childType < 3 // 0, 1, 2
            ) {
                continue;
            }

            if (childType < 6) {
                //!== 'object' 不是对象就是字符串或数字
                if (hasText) {
                    lastText = children[0] = child + lastText;
                    continue;
                }
                child = child + ""
                lastText = child
                hasText = true
            } else {
                hasText = lastText = null;
            }

            children.unshift(child);
        }
    }
    if (!children.length) {
        children = EMPTY_CHILDREN;
    }
    return children;
}

IESetTimeout

import { options, typeNumber } from "./util";
import { modern, document } from "./browser";

if (0 === [1, 2].splice(0).length) {
console.warn("请引入polyfill进行修复"); // eslint-disable-line
}

export var scheduler = {
list: [],
add: function (el) {
this.count = this.list.push(el);
},
addAndRun: function (fn) {
this.add(fn);
defer(function () {
scheduler.run();
}, 0);
},
run: function () {
if (this.count === 0) return;
this.count = 0;
this.list.splice(0).forEach(function (instance) {
if (typeNumber(instance) === 5) {
instance(); //处理ref方法
return;
}
if (instance._pendingCallbacks.length) {
//处理componentWillMount产生的回调
instance._pendingCallbacks.splice(0).forEach(function (fn) {
fn.call(instance);
});
}
if (instance.componentDidMount) {
instance._updating = true;
instance.componentDidMount();
instance.componentDidMount = instance._updating = false;
instance._hasDidMount = true;
//处理componentDidMount里调用 setState产生重绘
if (instance._pendingStates.length && !instance._disableSetState) {
options.refreshComponent(instance);
}
}
});
}
};
var defer = modern ? setTimeout : IESetTimeout
function IESetTimeout(cb) {
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "data:text/javascript,";
script.onreadystatechange = function () {
canceller();
cb()
};
var canceller = function () {
script.onreadystatechange = null;
document.body.removeChild(script);
};
document.body.appendChild(script);
}

9.1 事件系统

for (var i = paths.length; i--;) { //从上到下
          var path = paths[i]
          var fn = path.props[captured]
          if (typeof fn === 'function') {
              event.currentTarget = path.dom
              fn.call(path.dom, event)
              if (event._stopPropagation) {
                  break
              }
          }
      }

      for (var i = 0, n = paths.length; i < n; i++) { //从下到上
          var path = paths[i]
          var fn = path.props[bubble]
          if (typeof fn === 'function') {
              event.currentTarget = path.dom
              fn.call(path.dom, event)
              if (event._stopPropagation) {
                  break
              }

          }
      }

event应该改成e

9.3章

toVnode里面添加

  var instance = new Type(props, context)
                    //必须在这里添加vnode,因为willComponent里可能进行setState操作
 instance.vnode = vnode 

Component.call(instance, props, context) //重点!!

React.js里面添加

    transaction.isInTransation = true
    var root = toVnode(vnode, {})
    transaction.isInTransation = false

Component.js的updateComponentProxy

    if (!instance.vnode.dom) {
        var parentNode = instance.container
        instance.state = this.state //将merged state赋给它
        toDOM(instance.vnode, instance.context, parentNode)
    } else {

React.Children.map的key生成策略

它会对原来的虚拟DOM进行克隆,添加与原来不一样的key

   map(children, callback, context) {
        var ret = [];
        _flattenChildren(children, "").forEach(function(old, index) {
            let el = callback.call(context, old, index);
            if (el === null) {
                return;
            }
            if (el.vtype) {
                //如果返回的el等于old,还需要使用原来的key, _prefix
                let curKey = el && el.key !== null ? el.key : null;
                let oldKey = old && old.key !== null ? old.key : null;
                let oldFix = old && old._prefix,
                    key;
                if (oldKey && curKey) {
                    key = oldFix + "$" + oldKey;
                    if (oldKey !== curKey) {
                        key = curKey + "/" + key;
                    } 
                } else {
                    key = curKey || oldKey;
                    if (key) {
                        if (oldFix) {
                            key = oldFix + "$" + key;
                        }
                    } else {
                        key = oldFix || "." + index;
                    }
                }
                key = key.replace(/\d+\$/, "$");
                ret.push(cloneElement(el, { key }));
            } else if (el.type) {
                ret.push(Object.assign({}, el));
            } else {
                ret.push(el);
            }
        });
        return ret;
    },

支持React16的组件返回数组或简单类型的行为

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <script type='text/javascript' src="./test/react.development.js"></script>
  <script type='text/javascript' src="./test/react-dom.development.js"></script>
  <script type='text/javascript' src="./lib/babel.js"></script>

</head>

<body>
  <div>开发者工具</div>
  <pre>
  </pre>
  <div id='example'></div>
  <script type='text/babel'>
   
     
      var container = document.getElementById("example")
      var div = container
     // var PropTypes = React.PropTypes
      if(!window.ReactDOM){
        window.ReactDOM = window.React
       
      }
      var PropTypes = React.PropTypes
      var expect = function(a) {
          return {
              toBe: function(b) {
                  console.log(a, "\nvs\n",b, a === b)
              }
          }
      }
      class Child extends React.Component{
        constructor(props){
          super(props)
          this.state = {
            aaa: 111
          }
        }
        componentWillMount(){
          console.log("child will mount")
        }
        componentWillUpdate(){
          console.log("child will update")
        }
        componentDidUpdate(){
          console.log("child did update")
        }
        componentDidMount(){
          console.log("child did mount")
        }
        componentWillUnmount(){
          console.log("child will unmount")
        }
        render(){
            return <strong>Child</strong>
        }
      }
      class Radio extends React.Component{
        constructor(props){
          super(props)
          this.state = {
            aaa: 111
          }
        }
        componentWillMount(){
          console.log("radio will mount")
        }
        componentWillUpdate(){
          console.log("radio will update")
        }
        componentDidUpdate(){
          console.log("radio did update")
        }
        componentDidMount(){
          console.log("radio did mount")
        }
        render(){
            return <input type="radio" />
        }
      }
      class App extends React.Component{
        constructor(props){
          super(props)
          this.state = {
            aaa: 111
          }
        }
        componentWillMount(){
          console.log("app will mount")
        }
        componentWillUpdate(){
          console.log("app will update")
        }
        componentDidUpdate(){
          console.log("app did update")
        }
        componentDidMount(){
          console.log("app did mount, setState")
          this.setState({
            aaa: 333
          })
        }
        render(){
            return this.state.aaa === 333? [<Radio/>,<Radio/>,<Radio/>] :[<Child />,<Child/>]
        }
      }
      var s = ReactDOM.render(<App /> ,div)
    </script>

</body>

</html>

image

React16的children操作

Children内部改用operateChildren实现转换

import { operateChildren } from "./createElement";
import { cloneElement } from "./cloneElement";
import { extend } from "./util";

export const Children = {
    only(children) {
        //only方法接受的参数只能是一个对象,不能是多个对象(数组)。
        if (children && children.vtype) {
            return children;
        }
        throw new Error("expect only one child");
    },
    count(children) {
        if (children == null) {
            return 0;
        }
        var index = 0;
        operateChildren(children, false, function() {
            index++;
        });
        return index;
    },
    map(children, callback, context) {
        if (children == null) {
            return children;
        }
        var index = 0;
        return operateChildren(children, true, function(ret, old, keeper) {
            if (old == null || old === false || old === true) {
                old = null;
            } else if (!old._prefix) {
                old._prefix = "." + keeper.unidimensionalIndex;
                keeper.unidimensionalIndex++;
            }
            let outerIndex = index;
            let el = callback.call(context, old, index++);
            if (el == null) {
                return;
            }
            if (el.vtype) {
                //如果返回的el等于old,还需要使用原来的key, _prefix
                var key = computeKey(old, el, outerIndex);
                ret.push(cloneElement(el, { key }));
            } else if (el.type) {
                ret.push(extend({}, el));
            } else {
                ret.push(el);
            }
        });
    },
    forEach(children, callback, context) {
        if (children != null) {
            var index = 0;
            operateChildren(children, false, function(array, el) {
                if (el == null || el === false || el === true) {
                    el = null;
                }
                callback.call(context, el, index++);
            });
        }
    },
    toArray: function(children) {
        if (children == null) {
            return [];
        }
        return Children.map(children, function(el) {
            return el;
        });
    }
};
var rthimNumer = /\d+\$/;
function computeKey(old, el, index) {
    let curKey = el && el.key != null ? escapeKey(el.key) : null;
    let oldKey = old && old.key != null ? escapeKey(old.key) : null;
    let oldFix = old && old._prefix,
        key;
    if (oldKey && curKey) {
        key = oldFix + "$" + oldKey;
        if (oldKey !== curKey) {
            key = curKey + "/" + key;
        }
    } else {
        key = curKey || oldKey;
        if (key) {
            if (oldFix) {
                key = oldFix + "$" + key;
            }
        } else {
            key = oldFix || "." + index;
        }
    }
    return key.replace(rthimNumer, "$");
}
function escapeKey(key) {
    return String(key).replace(/[=:]/g, escaperFn);
}
var escaperLookup = {
    "=": "=0",
    ":": "=2"
};
function escaperFn(match) {
    return escaperLookup[match];
}

大菜是operateChildren,用于替代旧的_flattenChildren,更加语义化与更具通用性

export function operateChildren(children, isMap, callback) {
    let ret = [],
        keeper = {
            unidimensionalIndex: 0
        },
        child,
        iteractorFn,
        temp = Array.isArray(children) ? children.slice(0) : [children];
    while (temp.length) {
        if ((child = temp.shift()) && (child.shift || (iteractorFn = getIteractor(child)))) {
            //比较巧妙地判定是否为子数组
            if (iteractorFn) {
                //兼容Immutable.js, Map, Set
                child = callIteractor(iteractorFn, child);
                iteractorFn = false;
                temp.unshift.apply(temp, child);
                continue;
            }
            if (isMap) {
                if (!child._prefix) {
                    child._prefix = "." + keeper.unidimensionalIndex;
                    keeper.unidimensionalIndex++; //维护第一层元素的索引值
                }
                for (let i = 0; i < child.length; i++) {
                    if (child[i]) {
                        child[i]._prefix = child._prefix + ":" + i;
                    }
                }
            }
            temp.unshift.apply(temp, child);
        } else {
            if (typeNumber(child) === 8  && !child.type) {
                throw Error("invalid type");
            }
            callback(ret, child, keeper);
        }
    }
    return ret;
}

报错如下

createUncontrollable.js:33 Uncaught TypeError: Cannot read property 'propTypes' of undefined
at uncontrollable (createUncontrollable.js:33)
at Object. (Dropdown.js:417)
at webpack_require (bootstrap 31fd461a5a2ccf02c19c:585)
at fn (bootstrap 31fd461a5a2ccf02c19c:109)
at Object. (index.js:66)
at webpack_require (bootstrap 31fd461a5a2ccf02c19c:585)
at fn (bootstrap 31fd461a5a2ccf02c19c:109)
at Object. (bootstrap 31fd461a5a2ccf02c19c:631)
at webpack_require (bootstrap 31fd461a5a2ccf02c19c:585)
at bootstrap 31fd461a5a2ccf02c19c:631
React.js:1025 createClass已经废弃,请改用es6方式定义类
createClass @ React.js:1025
React.js:395 请限制使用Children.only,不要窥探虚拟DOM的内部实现,会导致升级问题
Children.(anonymous function) @ React.js:395
React.js:395 请限制使用Children.forEach,不要窥探虚拟DOM的内部实现,会导致升级问题
Children.(anonymous function) @ React.js:395
React.js:395 请限制使用Children.map,不要窥探虚拟DOM的内部实现,会导致升级问题
Children.(anonymous function) @ React.js:395
React.js:153 Uncaught Error: @root#render:You may have returned undefined, an array or some other invalid object
at checkNull (React.js:153)
at Stateless.renderComponent [as render] (React.js:2056)
at Object.mountStateless (React.js:2074)
at mountVnode (React.js:1912)
at Object.mountComponent (React.js:2034)
at mountVnode (React.js:1912)
at genVnodes (React.js:1888)
at renderByAnu (React.js:1854)
at render (React.js:1759)
at Object. (index.js:12)

createClass

 var ReactComponent = React.Component;

  var _assign = Object.assign;

  var _invariant = function _invariant(condition, format, a, b, c, d, e, f) {
    if (!condition) {
      var error;
      if (format === undefined) {
        error = new Error(
          "Minified exception occurred; use the non-minified dev environment " +
            "for the full error message and additional helpful warnings."
        );
      } else {
        var args = [a, b, c, d, e, f];
        var argIndex = 0;
        error = new Error(
          format.replace(/%s/g, function() {
            return args[argIndex++];
          })
        );
        error.name = "Invariant Violation";
      }

      error.framesToPop = 1; // we don't care about invariant's own frame
      throw error;
    }
  };

  var MIXINS_KEY = "mixins";

  /**
 * Policies that describe methods in `ReactClassInterface`.
 */

  var injectedMixins = [];

  var ReactClassInterface = {
    mixins: "DEFINE_MANY",
    statics: "DEFINE_MANY",
    propTypes: "DEFINE_MANY",
    contextTypes: "DEFINE_MANY",
    childContextTypes: "DEFINE_MANY",
    getDefaultProps: "DEFINE_MANY_MERGED",
    getInitialState: "DEFINE_MANY_MERGED",
    getChildContext: "DEFINE_MANY_MERGED",
    render: "DEFINE_ONCE",
    componentWillMount: "DEFINE_MANY",
    componentDidMount: "DEFINE_MANY",
    componentWillReceiveProps: "DEFINE_MANY",
    shouldComponentUpdate: "DEFINE_ONCE",
    componentWillUpdate: "DEFINE_MANY",
    componentDidUpdate: "DEFINE_MANY",
    componentWillUnmount: "DEFINE_MANY",
    updateComponent: "OVERRIDE_BASE"
  };

  /**
 * Mapping from class specification keys to special processing functions.
 *
 * Although these are declared like instance properties in the specification
 * when defining classes using `React.createClass`, they are actually static
 * and are accessible on the constructor instead of the prototype. Despite
 * being static, they must be defined outside of the "statics" key under
 * which all other static methods are defined.
 */
  var RESERVED_SPEC_KEYS = {
    displayName: function displayName(Constructor, _displayName) {
      return (Constructor.displayName = _displayName);
    },
    mixins: function mixins(Constructor, _mixins) {
      if (_mixins) {
        for (var i = 0; i < _mixins.length; i++) {
          mixSpecIntoComponent(Constructor, _mixins[i]);
        }
      }
    },
    childContextTypes: function childContextTypes(
      Constructor,
      _childContextTypes
    ) {
      return (Constructor.childContextTypes = _assign(
        {},
        Constructor.childContextTypes,
        _childContextTypes
      ));
    },
    contextTypes: function contextTypes(Constructor, _contextTypes) {
      return (Constructor.contextTypes = _assign(
        {},
        Constructor.contextTypes,
        _contextTypes
      ));
    },
    /**
   * Special case getDefaultProps which should move into statics but requires
   * automatic merging.
   */
    getDefaultProps: function getDefaultProps(Constructor, _getDefaultProps) {
      if (Constructor.getDefaultProps) {
        Constructor.getDefaultProps = createMergedResultFunction(
          Constructor.getDefaultProps,
          _getDefaultProps
        );
      } else {
        Constructor.getDefaultProps = _getDefaultProps;
      }
    },

    propTypes: function propTypes(Constructor, _propTypes) {
      Constructor.propTypes = _assign({}, Constructor.propTypes, _propTypes);
    },
    statics: function statics(Constructor, _statics) {
      mixStaticSpecIntoComponent(Constructor, _statics);
    },
    autobind: function autobind() {}
  };

  function validateMethodOverride(isAlreadyDefined, name) {
    var specPolicy = ReactClassInterface.hasOwnProperty(name)
      ? ReactClassInterface[name]
      : null;

    // Disallow overriding of base class methods unless explicitly allowed.
    if (ReactClassMixin.hasOwnProperty(name)) {
      _invariant(
        specPolicy === "OVERRIDE_BASE",
        "ReactClassInterface: You are attempting to override " +
          "`%s` from your class specification. Ensure that your method names " +
          "do not overlap with React methods.",
        name
      );
    }

    // Disallow defining methods more than once unless explicitly allowed.
    if (isAlreadyDefined) {
      _invariant(
        specPolicy === "DEFINE_MANY" || specPolicy === "DEFINE_MANY_MERGED",
        "ReactClassInterface: You are attempting to define " +
          "`%s` on your component more than once. This conflict may be due " +
          "to a mixin.",
        name
      );
    }
  }

  /**
 * Mixin helper which handles policy validation and reserved
 * specification keys when building React classes.
 */
  function mixSpecIntoComponent(Constructor, spec) {
    if (!spec) {
      return;
    }

    _invariant(
      typeof spec !== "function",
      "ReactClass: You're attempting to " +
        "use a component class or function as a mixin. Instead, just use a " +
        "regular object."
    );

    var proto = Constructor.prototype;
    var autoBindPairs = proto.__reactAutoBindPairs;

    // By handling mixins before any other properties, we ensure the same
    // chaining order is applied to methods with DEFINE_MANY policy, whether
    // mixins are listed before or after these methods in the spec.
    if (spec.hasOwnProperty(MIXINS_KEY)) {
      RESERVED_SPEC_KEYS.mixins(Constructor, spec.mixins);
    }

    for (var name in spec) {
      if (!spec.hasOwnProperty(name)) {
        continue;
      }

      if (name === MIXINS_KEY) {
        // We have already handled mixins in a special case above.
        continue;
      }

      var property = spec[name];
      var isAlreadyDefined = proto.hasOwnProperty(name);
      validateMethodOverride(isAlreadyDefined, name);
      if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) {
        RESERVED_SPEC_KEYS[name](Constructor, property);
      } else {
        // Setup methods on prototype:
        // The following member methods should not be automatically bound:
        // 1. Expected ReactClass methods (in the "interface").
        // 2. Overridden methods (that were mixed in).
        var isReactClassMethod = ReactClassInterface.hasOwnProperty(name);
        var isFunction = typeof property === "function";
        var shouldAutoBind =
          isFunction &&
          !isReactClassMethod &&
          !isAlreadyDefined &&
          spec.autobind !== false;

        if (shouldAutoBind) {
          autoBindPairs.push(name, property);
          proto[name] = property;
        } else {
          if (isAlreadyDefined) {
            var specPolicy = ReactClassInterface[name];

            // These cases should already be caught by validateMethodOverride.
            _invariant(
              isReactClassMethod &&
                (specPolicy === "DEFINE_MANY_MERGED" ||
                  specPolicy === "DEFINE_MANY"),
              "ReactClass: Unexpected spec policy %s for key %s " +
                "when mixing in component specs.",
              specPolicy,
              name
            );

            // For methods which are defined more than once, call the existing
            // methods before calling the new property, merging if appropriate.
            if (specPolicy === "DEFINE_MANY_MERGED") {
              console.log(name, "createMergedResultFunction");

              proto[name] = createMergedResultFunction(proto[name], property);
            } else if (specPolicy === "DEFINE_MANY") {
              proto[name] = createChainedFunction(proto[name], property);
            }
          } else {
            proto[name] = property;
            {
              // Add verbose displayName to the function, which helps when looking
              // at profiling tools.
              if (typeof property === "function" && spec.displayName) {
                proto[name].displayName = spec.displayName + "_" + name;
              }
            }
          }
        }
      }
    }
  }

  function mixStaticSpecIntoComponent(Constructor, statics) {
    if (!statics) {
      return;
    }
    for (var name in statics) {
      var property = statics[name];
      if (!statics.hasOwnProperty(name)) {
        continue;
      }

      var isReserved = name in RESERVED_SPEC_KEYS;
      _invariant(
        !isReserved,
        "ReactClass: You are attempting to define a reserved " +
          'property, `%s`, that shouldn\'t be on the "statics" key. Define it ' +
          "as an instance property instead; it will still be accessible on the " +
          "constructor.",
        name
      );

      var isInherited = name in Constructor;
      _invariant(
        !isInherited,
        "ReactClass: You are attempting to define " +
          "`%s` on your component more than once. This conflict may be " +
          "due to a mixin.",
        name
      );
      Constructor[name] = property;
    }
  }

  function mergeIntoWithNoDuplicateKeys(one, two) {
    _invariant(
      one &&
        two &&
        (typeof one === "undefined" ? "undefined" : _typeof(one)) ===
          "object" &&
        (typeof two === "undefined" ? "undefined" : _typeof(two)) === "object",
      "mergeIntoWithNoDuplicateKeys(): Cannot merge non-objects."
    );

    for (var key in two) {
      if (two.hasOwnProperty(key)) {
        one[key] = two[key];
      }
    }
    return one;
  }

  function createMergedResultFunction(one, two) {
    return function mergedResult() {
      var a = one.apply(this, arguments);
      var b = two.apply(this, arguments);
      if (a == null) {
        return b;
      } else if (b == null) {
        return a;
      }
      var c = {};
      mergeIntoWithNoDuplicateKeys(c, a);
      mergeIntoWithNoDuplicateKeys(c, b);
      return c;
    };
  }

  function createChainedFunction(one, two) {
    return function chainedFunction() {
      one.apply(this, arguments);
      two.apply(this, arguments);
    };
  }

  function bindAutoBindMethod(component, method) {
    var boundMethod = method.bind(component);
    return boundMethod;
  }

  function bindAutoBindMethods(component) {
    var pairs = component.__reactAutoBindPairs;
    for (var i = 0; i < pairs.length; i += 2) {
      var autoBindKey = pairs[i];
      var method = pairs[i + 1];
      component[autoBindKey] = bindAutoBindMethod(component, method);
    }
  }

  var ReactClassMixin = {
    /**
   * TODO: This will be deprecated because state should always keep a consistent
   * type signature and the only use case for this, is to avoid that.
   */
    replaceState: function replaceState(newState, callback) {
      // this.updater.enqueueReplaceState(this, newState, callback);
    },

    /**
   * Checks whether or not this composite component is mounted.
   * @return {boolean} True if mounted, false otherwise.
   * @protected
   * @final
   */
    isMounted: function isMounted() {
      return !!this._rendered && this._rendered._hostNode;
    }
  };

  var ReactClassComponent = function ReactClassComponent() {};
  _assign(
    ReactClassComponent.prototype,
    ReactComponent.prototype,
    ReactClassMixin
  );

  /**
 * Creates a composite component class given a class specification.
 * See https://facebook.github.io/react/docs/top-level-api.html#react.createclass
 *
 * @param {object} spec Class specification (which must define `render`).
 * @return {function} Component constructor function.
 * @public
 */
  function newCtor(className, ReactComponent, bindAutoBindMethods) {
    var curry = Function(
      "ReactComponent",
      "bindAutoBindMethods",
      `return function ${className}(props, context) {
    ReactComponent.call(this, props, context);
    var initialState = this.getInitialState ? this.getInitialState() : {};

    this.state = initialState;
    // This constructor gets overridden by mocks. The argument is used
    // by mocks to assert on what gets mounted.

    // Wire up auto-binding
    if (this.__reactAutoBindPairs.length) {
      bindAutoBindMethods(this);
    }
  };`
    );
    return curry(ReactComponent, bindAutoBindMethods);
  }

  function createClass(spec) {
    // To keep our warnings more understandable, we'll use a little hack here to
    // ensure that Constructor.name !== 'Constructor'. This makes sure we don't
    // unnecessarily identify a class without displayName as 'Constructor'.
    var Constructor = newCtor(
      spec.displayName || "Component",
      ReactComponent,
      bindAutoBindMethods
    );

    Constructor.prototype = new ReactClassComponent();
    Constructor.prototype.constructor = Constructor;
    Constructor.prototype.__reactAutoBindPairs = [];

    injectedMixins.forEach(mixSpecIntoComponent.bind(null, Constructor));

    // mixSpecIntoComponent(Constructor, IsMountedPreMixin);
    mixSpecIntoComponent(Constructor, spec);
    // mixSpecIntoComponent(Constructor, IsMountedPostMixin);

    // Initialize the defaultProps property after all mixins have been merged.
    if (Constructor.getDefaultProps) {
      Constructor.defaultProps = Constructor.getDefaultProps();
    }

    _invariant(
      Constructor.prototype.render,
      "createClass(...): Class specification must implement a `render` method."
    );

    // Reduce time spent doing lookups by setting these on the prototype.
    for (var methodName in ReactClassInterface) {
      if (!Constructor.prototype[methodName]) {
        Constructor.prototype[methodName] = null;
      }
    }

    return Constructor;
  }
  React.createClass = createClass;

调度系统

mainQueue就是传送带,currentQueue或其他queue就是托运箱,job就是要加工的材料,exec是决定如何加工。

export function drainQueue() {
    
    var queue = mainQueue.shift()
    //如果父元素拥有多个子组件,如果第一个组件在mounted/updated钩子里再次更新父元素,
    //那么mainQueue可能没有子数组了,需要置换queue为currentQueue,执行里面的mounted/updated钩子
    if(!queue &&  currentQueue.length){
        queue = currentQueue;
    }
    options.beforePatch();
    //先执行所有元素虚拟DOMrefs方法(从上到下)
    Refs.clearElementRefs();
    let needSort = [],
        unique = {},
        job, updater;

    while( job = queue.shift() ){
        updater = job.host;
        //queue可能中途加入新元素,  因此不能直接使用queue.forEach(fn)
        if (updater._disposed) {
            continue;
        }
        if (!unique[updater._mountOrder]) {
            unique[updater._mountOrder] = 1;
            needSort.push(job);
        }
        // currentQueue = queue;
           
        // var command = job.exec === updater.onUpdate ? "update" : job.exec === updater.onEnd ? "end" : "receive";
        // console.log(updater.name, command,updater._hookName );
        job.exec.call(updater,queue);
        // if(!queue.length){
        /*  while((el =  mainQueue.shift())){
                    if(el.length){
                        queue = el;
                        break;
                    }
                }
           */
        // }
    }

    if (mainQueue.length > 1) {
       mainQueue.push(currentQueue);
    }
    //再执行所有setState/forceUpdate回调,根据从下到上的顺序执行
    needSort.sort(mountSorter).forEach(function(job) {
        var updater = job.host;
        clearArray(updater._pendingCallbacks).forEach(function(fn) {
            fn.call(updater._instance);
        });
    });
    options.afterPatch();
}

__events赋值问题

dom.__events 是什么时候赋值的, 很奇怪

经过了这个方法 action = strategyCache[which] = getPropAction(dom, name, isSVG);

dom的events属性突然就有值了, 但这里面并没有看到给dom新增__events属性啊

10.3

顺整diff机制

先将孩子插入到parentNode中,然后 diffChildren, 然后 diffProps与ref, 再处理受控组件,然后生命周期钩子

调整diffProps,将属性根据元素类型 进行, svg一律使用getXXXNS进行操作, 普通元素根据固有属性进行处理,固有属性又划分出字符串属性。

9.2

diffProps 方法不能修改props属性,因为它可能已经冻结了

 var builtIdProperties = /^(?:className|id|title|htmlFor)$/
  export function diffProps(dom, instance, props, nextProps) {

      if (props === nextProps) {
          return
      }

      for (let name in nextProps) {
          if (name === 'children') {
              continue
          }
          var val = nextProps[name]
          if (name === 'ref') {
              if (props[name] !== val) {
                  instance && patchRef(instance, val, dom)
              }
              continue
          }
          if (name === 'style') {
              patchStyle(dom, props[style], val)
              continue
          }
          if (name === 'dangerouslySetInnerHTML') {
              var oldhtml = props[name] && props[name]._html
              instance && (instance._hasSetInnerHTML = true)
              if (val && val._html !== oldhtml) {
                  dom.innerHTML = val._html
              }
          }
          if (isEvent(name)) {
              if (!props[name]) { //添加全局监听事件
                  var eventName = getBrowserName(name)
                  addGlobalEventListener(eventName)
              }
              if (inMobile && eventName === 'click') {
                  elem.addEventListener('click', clickHack)

              }
              var events = (dom.__events || (dom.__events = {}))
                  //   events[name] = props[name] = val
              events[name] = val
              continue
          }

          if (val !== props[name]) {
              //移除属性
              if (val === false || val === void 666 || val === null) {
                  dom.removeAttribute(name)
                      // delete props[name]
              } else { //添加新属性
                  if (builtIdProperties.test(name)) {
                      dom[name] = val + ''
                  } else {
                      dom.setAttribute(name, val + '')
                  }

                  //  props[name] = val // 不能改旧的props
              }
          }
      }
      for (let name in props) {
          if (!(name in nextProps)) {
              if (isEvent(name)) { //移除事件
                  var events = dom.__events || {}
                  delete events[name]
              } else { //移除属性
                  if (builtIdProperties.test(name)) {
                      dom[name] = ''
                  } else {
                      dom.removeAttribute(name)
                  }
              }
              // delete props[name]
          }
      }
  }

新的diff机制

##虚拟DOM的_owner属性的设置问题

它必须在组件render时,通过CurrenOwner.cur中获取

有了_owner,才能正确将ref的名字与值放到对应的组件实例的refs对象上。

##组件的初始阶段的构建问题

组件的componentDidMount回调,必须等到它下面的所有子组组件都执行了componentDidMount回调才执行自己的。并且DidMount回调,必须等到其refs对象构建好才执行。

为了达到这个目标,在ReactDOM.render阶段,有一个mountQueue数组,它有一个mountAll=true的属性,它会一路传递下去。收集内部的组件实例。最后执行clearRefsAndMounts。

在更新过程,如果两个虚拟DOM 的类型不一致,也会产生一个mountQueue数组,再进行比较,最后执行clearRefsAndMounts。

##两处阻止组件更新的时机

componentWillMount与componentWillRecieveProps会阻止组件进行更新,通过_dirty 这个标识实现

if (!this._dirty && (this._dirty = true)) {
      defer(() => {
        this._dirty = false
        options.refreshComponent(this);
      }, 16)
    }

##父组件在更新过程中,子组件在componentWillRecieveProps中执行父组件的方法间接或直接执行父组件的setState方法,这时父组件会在子组件update之后,立即再次同步更新自身。

通过_updating与CurrentOwner.updating 实现。

##组件的componentWillComponent与componentDidComponent钩子都有setState,并且setState都有回调,亦即所谓的pendingCallback,它们会呆在componentDidUpdate执行(有待优化)。

##虚拟DOM 不再清空_hostNode属性。

##去掉_hostParent属性

##所有虚拟DOM 都有_hostNode属性,组件不再通过instanceMap来寻找自己的DOM

function sameVnode(a, b) {
  return a.type === b.type && a.key === b.key
}
function patchVnode(lastVnode, nextVnode, args) {
  return updateVnode(lastVnode, nextVnode, lastVnode._hostNode, args[0], args[1])
}
function updateChildren(lastVnode, nextVnode, parentNode, parentContext, mountQueue) {
  let newCh = nextVnode.props.children
  let oldCh = lastVnode.props.children
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldCh.length - 1;
  let oldStartVnode = oldCh[0];
  let oldEndVnode = oldCh[oldEndIdx];
  let newEndIdx = newCh.length - 1;
  let newStartVnode = newCh[0];
  let newEndVnode = newCh[newEndIdx];
  let oldKeyMap, idxInOld, dom, ref, elmToMove;

  var args = [parentContext, mountQueue]
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {

    if (sameVnode(oldStartVnode, newStartVnode)) {
      dom = patchVnode(oldStartVnode, newStartVnode, args);
      ref = childNodes[newStartIdx]
      if (dom !== ref) {
        parentNode.replaceChild(dom, ref)
        oldStartVnode._hostNode = dom
      }
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      dom = patchVnode(oldEndVnode, newEndVnode, args);
      ref = childNodes[newEndIdx]
      if (dom !== ref) {
        parentNode.replaceChild(dom, ref)
        oldEndVnode._hostNode = dom
        // insertDOM(parentNode, dom, ref)
      }
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      //如果新的最后一个等于旧的第一个
      dom = patchVnode(oldStartVnode, newEndVnode, args);
      parentNode.insertBefore(dom, oldStartVnode._hostNode.nextSibling)
      //  api.insertBefore(parentNode, oldStartVnode._hostNode as Node,
      // api.nextSibling(oldEndVnode._hostNode as Node));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      dom = patchVnode(oldEndVnode, newStartVnode, args);
      parentNode.insertBefore(oldEndVnode._hostNode, oldStartVnode._hostNode);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (oldKeyMap === undefined) {
        oldKeyMap = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
      idxInOld = oldKeyMap[newStartVnode.key];
      if (isUndef(idxInOld)) { // New element
        dom = mountVnode(newStartVnode, parentContext, null, mountQueue)
        parentNode.insertBefore(dom, oldStartVnode._hostNode);
        newStartVnode = newCh[++newStartIdx];
      } else {
        elmToMove = oldCh[idxInOld];
        if (elmToMove.type !== newStartVnode.type) {
          dom = mountVnode(newStartVnode, parentContext, null, mountQueue)
          parentNode.insertBefore(parentNode, dom, oldStartVnode._hostNode);
        } else {
          patchVnode(elmToMove, newStartVnode, mountQueue);
          oldCh[idxInOld] = undefined;
          parentNode.insertBefore(parentNode, (elmToMove._hostNode), oldStartVnode._hostNode);
        }
        newStartVnode = newCh[++newStartIdx];
      }
    }
  }
  if (oldStartIdx > oldEndIdx) {
    let before = newCh[newEndIdx + 1] == null
      ? null
      : newCh[newEndIdx + 1]._hostNode;
    addVnodes(parentNode, before, newCh, newStartIdx, newEndIdx, mountQueue);
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentNode, oldCh, oldStartIdx, oldEndIdx);
  }
}

考虑是不是要实现getType方法

var __type = Object.prototype.toString

export function getType(obj){
    var type = __type.call(obj)
    return typeCache[type] || (typeCache[type] = (type.slice(8,-1).toLowerCase()))
}
var typeCache = {}
'Function,Date,Undefined,String,Number,Object,Null,'.replace(/\w+/,function(){
   typeCache['[object '+ name] = name.toLowerCase()
})

真实DOM的命名空间的获取

// https://developer.mozilla.org/en-US/docs/Web/MathML/Element/math
var rmathTags = /^m/;

var namespaceMap = oneObject("svg", NAMESPACE.svg);
namespaceMap.semantics = NAMESPACE.math;
// http://demo.yanue.net/HTML5element/
"meter,menu,map,meta,mark".replace(/\w+/g, function(tag) {
    namespaceMap[tag] = null;
});
export function getNs(type) {
    if (namespaceMap[type] !== void 666) {
        return namespaceMap[type];
    } else {
        return (namespaceMap[type] = rmathTags.test(type) ? NAMESPACE.math : null);
    }
}

1.1.2的updateChildren

1.1.2的节点排序算法,通过一次循环,同时构建三个辅助对象,actions, removeHits, fuzzyHits
第二次循环,实现所有节点排序,第三次循环,实现多余节点的移除。

function diffChildren(lastVnode, nextVnode, parentNode, context, mountQueue) {
    let lastChildren = lastVnode.vchildren,
        nextChildren = flattenChildren(nextVnode),
        nextLength = nextChildren.length,
        lastLength = lastChildren.length;
    //如果旧数组长度为零
    if (nextLength && !lastLength) {
        nextChildren.forEach(function(vnode){
            let curNode = mountVnode(null, vnode, lastVnode, context, mountQueue);
            parentNode.appendChild(curNode);
        });
        return;
    }
    let maxLength = Math.max(nextLength, lastLength),
        insertPoint = parentNode.firstChild,
        removeHits = {},
        fuzzyHits = {},
        actions = [],
        i = 0,
        hit,
        dom,
        oldDom,
        nextChild,
        lastChild;
    //第一次循环,构建移动指令(actions)与移除名单(removeHits)与命中名单(fuzzyHits)
    if(nextLength){
        actions.length = nextLength;
        while (i < maxLength) {
            nextChild = nextChildren[i];
            lastChild = lastChildren[i];
            if (nextChild && lastChild && isSameNode(lastChild, nextChild)) {
            //  如果能直接找到,命名90%的情况
                actions[i] = {
                    last: lastChild,
                    next: nextChild,
                    directive: "update"
                };
                removeHits[i] = true;
            } else {
                if (nextChild) {
                    hit = nextChild.type + (nextChild.key || "");
                    if (fuzzyHits[hit] && fuzzyHits[hit].length) {
                        var oldChild = fuzzyHits[hit].shift();
                        // 将旧的节点移动新节点的位置,向后移动
                        actions[i] = {
                            last: oldChild,
                            next: nextChild,
                            directive: "moveAfter"
                        };
                        removeHits[oldChild._i] = true;
                    }
                }
                if (lastChild) {
                //如果不相同,储存它们的key
                    lastChild._i = i;
                    hit = lastChild.type + (lastChild.key || "");
                    let hits = fuzzyHits[hit];
                    if (hits) {
                        hits.push(lastChild);
                    } else {
                        fuzzyHits[hit] = [lastChild];
                    }
                }
            }
            i++;
        }
    }
    for (let j = 0, n = actions.length; j < n; j++) {
        let action = actions[j];
        if (!action) {
            let curChild = nextChildren[j];
            hit = curChild.type + (curChild.key || "");
            if (fuzzyHits[hit] && fuzzyHits[hit].length) {
                oldChild = fuzzyHits[hit].shift();
                oldDom = oldChild._hostNode;
                parentNode.insertBefore(oldDom, insertPoint);
                dom = updateVnode(oldChild, curChild, lastVnode, context, mountQueue);
                removeHits[oldChild._i] = true;
            } else {
                //如果找不到对应的旧节点,创建一个新节点放在这里
                dom = mountVnode(null, curChild, lastVnode, context, mountQueue);
                parentNode.insertBefore(dom, insertPoint);
            }
        } else {
            oldDom = action.last._hostNode;
            if(action.action === "moveAfter"){
                parentNode.insertBefore(oldDom, insertPoint); 
            }
            dom = updateVnode(
                action.last,
                action.next,
                lastVnode,
                context,
                mountQueue
            );
        }
        insertPoint = dom.nextSibling;
    }
    //移除
    lastChildren.forEach(function(el, i) {
        if (!removeHits[i]) {
            var node = el._hostNode;
            if (node) {
                removeDOMElement(node);
            }
            disposeVnode(el);
        }
    });
}

function isSameNode(a, b) {
    if (a.type === b.type && a.key === b.key) {
        return true;
    }
}

元素虚拟DOM也有updater

组件虚拟DOM会产生 CompositeUpdater
元素虚拟DOM会产生 DOMUpdater

它们至少用于控制ref的执行

got "ReferenceError: window is not defined" when importing ReactDOMServer in node.

var ReactDOMServer = require('anujs/dist/ReactDOMServer');

ReferenceError: window is not defined
    at .../node_modules/anujs/dist/ReactDOMServer.js:197:1
    at .../node_modules/anujs/dist/ReactDOMServer.js:2:82
    at Object.<anonymous> (.../node_modules/anujs/dist/ReactDOMServer.js:5:2)
    at Module._compile (module.js:635:30)
    at Object.Module._extensions..js (module.js:646:10)
    at Module.load (module.js:554:32)
    at tryModuleLoad (module.js:497:12)
    at Function.Module._load (module.js:489:3)
    at Module.require (module.js:579:17)
    at require (internal/module.js:11:18)

line 197 in ReactDOMServer.js:

var pendingRefs = [];
window.pendingRefs = pendingRefs;

anu版本1.1.4 context无效的bug

import React from 'anujs';
import ReactDOM from 'anujs';
import PropTypes from 'anujs';

class Parent extends React.Component {
  render() {
    return (<div className="parent"><Son /></div>)
  }
}
class Son extends React.Component {
  render() {
    return (<div className="son">{this.context.value}</div>)
  }
}
class App extends React.Component {
  getChildContext() {
    return {
      value: 666
    };
  }
  render() {
    return (<div className='app'><Parent /></div>)
  }
};
ReactDOM.render(
  <App/>,
  document.getElementById('root')
)

在上述代码中,无法获取到{this.context.value}

代码出处:https://rubylouvre.github.io/anu/context.html

完整实现react的非受控组件需要监控用户对defaultValue/defaultChecked的操作

受控组件与非受控组件的实现 官方一直晦涩

受控组件是指表单元素指定了value/check,同时也指定了onChange/onInput/disabled/readOnly等阻止或让用户改变vaue/check的属性或事件。如果用户没有指定这些方法呢,框架会加上一些onClick, onInput, onChange事件,让你无法通过手动方式改变value/check

这个我老早就实现了。

非受控组件是指表单元素指定了defaultValue/defaultChecked,以后你每次

ReactDOM.render(<textarea defaultValue="monkey" />, container);

可以发现textarea.value都发生变化,你也可以通过键盘输入改这个值,框架不会阻止你改动它的。

但是如果你手动改了它,以后再

ReactDOM.render(<textarea defaultValue="xxx" />, container);

发现textarea的value不再是xxx了,它是你改后的值,换言之,它更看重你的修改,而不是代码实现。

我们可以得出这样的结论
受控组件是更看重于 JSX中的编程事实
非受控组件是看重于 用户的操作效果

如果你想让它还是以代码方式更新视图,你需要

ReactDOM.render(<textarea defaultValue={null} />, container);

null与undefined都行

这时估计内部的标记会被清除掉。

我们可以通过下面的测试来检测你是否完美实现React的非受控组件行为

             var emptyFunction = function(){};
            var renderTextarea = function(component, container) {
                if (!container) {
                    container = document.createElement("div");
                }
                const node = ReactDOM.render(component, container);

                // Fixing jsdom's quirky behavior -- in reality, the parser should strip
                // off the leading newline but we need to do it by hand here.
                node.defaultValue = node.innerHTML.replace(/^\n/, "");
                return node;
            };
            const container = document.createElement("div");
            const node = renderTextarea(<textarea defaultValue="giraffe" />, container);
    
           expect(node.value).toBe("giraffe");// 1 
           expect(node.defaultValue).toEqual("giraffe"); // 2
           expect(node.innerHTML).toEqual("giraffe"); // 3

            // Changing `defaultValue` should do nothing.
            renderTextarea(<textarea defaultValue="gorilla" />, container);
            expect(node.value).toEqual("giraffe"); // 4
            expect(node.defaultValue).toEqual("gorilla"); // 5
            expect(node.innerHTML).toEqual("gorilla"); // 6
            node.value = "cat"; 
    
            renderTextarea(<textarea defaultValue="monkey" />, container);
            expect(node.value).toEqual("cat"); // 7
            expect(node.defaultValue).toEqual("monkey"); // 8

下面的anu的实现

大致是在diffProps中的isSpecialAttr添加两个值

var isSpecialAttr = {
    style: 1,
    defaultValue: 1,
    defaultChecked: 1,
    children: 1,
    innerHTML: 1,
    dangerouslySetInnerHTML: 1
};

让它们进入特殊的钩子中,由于它们的行为相同,让它共同使用一个钩子

var rform = /textarea|input|select/i;
function uncontrolled(dom, name, val) {
    if (rform.test(dom.nodeName)) {
        controlledHook.stopObserve(dom);
        dom[name] = val;
        controlledHook.observe(dom, name);
    } else {
        dom.setAttribute(name, val);
    }
}

export var actionStrategy = {
    innerHTML: noop,
    defaultValue: uncontrolled,
    defaultChecked: uncontrolled,
   //...
}

controlledHook对象在compat文件被加载的情况下会被重写,因此要export出来
这为了监听用户的操作,也就是知道用户是否对dom.defaultValue/defaultChecked进行操作,那么在高级浏览器下,我们可以通过setter,getter知晓。

controlledHook.observe = function(dom, name) {
    try {
        var controllProp = name === "defaultValue" ? "value" : "checked";
        if (!dom._hack) {
            dom._hack = true;
            Object.defineProperty(dom, name, {
                set: function(value) {
                    if (name === "defaultValue") {
                        dom.innerHTML = value;
                    }
                    if (dom._observing) {
                        //变动value/defaultXXX/innerHTML三方
                        if (dom._userSet) {
                            return;
                        }
                        dom.__default = dom[controllProp] = value;
                    } else {
                        dom._userSet = true;
                        if (value == null) {
                            dom._userSet = false;
                        } //value/check不能变,只变defaultXXX
                        dom.__default = value;
                    }
                },
                get: function() {
                    return dom.__default;
                }
            });
        }
    } catch (e) {}
    dom._observing = true;
};
controlledHook.stopObserve = function(dom) {
    dom._observing = false;
};

在旧式IE下我们可以通过onpropertychange得知用户对这个元素的任何元素的修改。

它是放在compat文件中,那么在高级版本不用打包它,有效减少体积

var observeControlledProp = function(e) {
        var dom = e.srcElement,
            prop = e.propertyName;
        if (prop === "defaultValue" || prop === "defaultChecked") {
            var controllProp = prop === "defaultValue" ? "value": "checked";
            var value = dom[prop];
            if (dom._observing) {
                if (dom._userSet) {
                    dom[controllProp] = dom.__default;
                    return;
                }
                dom.__default = dom[controllProp] = value; //同步value/checked
            } else {
                dom._userSet = true;
                if (value == null) {
                    dom._userSet = false;
                }
            }
        }
    };
    controlledHook.observe = function(dom) {
        dom.attachEvent("onpropertychange", observeControlledProp);
        dom._observing = true;
    };
    controlledHook.stopObserve = function(dom) {
        dom._observing = false;
    };

UI测试的异步处理优化

在开发迷你react过程中,我们需要判定官方 react与我们写的迷你react的接口一致性,于是引入了UI测试。

UI测试中最著名的项目是 Selenium, 在nodejs对应的实现是webdriver.io

现在我们重写了一个karma的一个launcher, karma-webdriverio-launcher

然后基于它包装了一个karma-event-driver-ext

这个东西的试验场就是本项目的 aaa分支

大家clone下来npm install,还需要安装两个东西才能运行

一个是selenium-server-standalone-3.3.1.jar
http://selenium-release.storage.googleapis.com/3.3/selenium-server-standalone-3.3.1.jar

另一个是浏览器的driver,这里我们先用chrome,大家选择对应的操作系统版本

http://chromedriver.storage.googleapis.com/index.html?path=2.29/

下回来后放到项目的根目录:

image

然后控制台中执行这两条命令

java -jar selenium-server-standalone-3.3.1.jar

node node_modules/karma-event-driver-ext

它们各自占用一个终端

image

image

最后在第二个终端中看 到测试结果

image

image

UI测试写在test/modules下的node.spec与event.spec里(另两个是普通的单元测试)

image

现在的问题是,karma-event-driver-ext实现得不太友好,测试代码要求用户写太多Promise

import React from 'src/React'
import eventHook, {beforeHook, afterHook, runCommand} from 'karma-event-driver-ext/cjs/event-driver-hooks.js';

describe('ReactDOM.render返回根组件的实例', function () {
    this.timeout(200000);
    before(async() => {
        await beforeHook();
    });
    after(async() => {
        await afterHook(false);
    });
    it('ReactDOM.render返回根组件的实例', async() => {
        class A extends React.Component {
            constructor() {
                super()
                this.state = {
                    aaa: 111
                }
            }
            click(e) {
                console.log('这个已经触发')
                this.setState({
                    aaa: this.state.aaa + 1
                })
                console.log('点击完')
            }
            componentDidMount() {
                resolve()
            }
            componentDidUpdate() {
                console.log('updated')
                console.log(this.state.aaa)
                resolve()

            }
            render() {
                return (
                    <div
                        id="aaa"
                        onClick={this
                        .click
                        .bind(this)}>
                        {this.state.aaa}
                    </div>
                )
            }
        }
        var resolve
        var p = new Promise(function (r) {
            resolve = r
        })
        var rootInstance = ReactDOM.render(
            <A/>, document.body)
        //等待组件mount
        await p
        p = new Promise(function (r) {
            resolve = r
        })
        //确保Promise是在await之前
        await runCommand((browser) => {
            browser.click('#aaa'); 
        });
        //等待组件update
        await p
        expect(rootInstance.state.aaa).toBe(112)
        p = new Promise(function (r) {
            resolve = r
        })
        await runCommand((browser) => {
            browser.click('#aaa'); 
        });
        //等待组件update
        await p
        expect(rootInstance.state.aaa).toBe(113)

    })

})

希望的形式是这样

import React from 'src/React'
import eventHook, {beforeHook, afterHook, runCommand} from 'karma-event-driver-ext/cjs/event-driver-hooks.js';

describe('ReactDOM.render返回根组件的实例', function () {
    this.timeout(200000);
    before(async() => {
        await beforeHook();
    });
    after(async() => {
        await afterHook(false);
    });
    it('ReactDOM.render返回根组件的实例', async() => {
        class A extends React.Component {
            constructor() {
                super()
                this.state = {
                    aaa: 111
                }
            }
            click(e) {
                console.log('这个已经触发')
                this.setState({
                    aaa: this.state.aaa + 1
                })
                console.log('点击完')
            }
            componentDidMount() {
                resolve()
            }
            componentDidUpdate() {
                console.log('updated')
                console.log(this.state.aaa)
                resolve()

            }
            render() {
                return (
                    <div
                        id="aaa"
                        onClick={this
                        .click
                        .bind(this)}>
                        {this.state.aaa}
                    </div>
                )
            }
        }
        var resolve, rootInstance
        await runCommand((brower, r) => {
               resolve = r
               rootInstance = ReactDOM.render(
             <A/>, document.body)
      })
 
        //确保Promise是在await之前
        await runCommand((browser, r) => {
           resolve = r
            browser.click('#aaa'); 
        });
    
        expect(rootInstance.state.aaa).toBe(112)
       
        await runCommand((browser, r) => {
             resolve = r
            browser.click('#aaa'); 
        });
        expect(rootInstance.state.aaa).toBe(113)

    })
})

看起来清爽多了

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.