高性能迷你React框架anu在低版本IE的实践

理想是丰满的,现实是骨感的,react早期的版本虽然号称支持IE8,但是页面总会不自觉切换到奇异模式下,导致报错。因此必须让react连IE6,7都支持,这才是最安全。但React本身并不支持IE6,7,因此anu使有用武之地了。

https://github.com/RubyLouvre...

但光是anu不行,兼容IE是一个系统性的工程,涉及到打包压缩,各种polyfill垫片。

首先说一下anu如何支持低版本浏览器。anu本身没有用到太高级的API,像Object.defineProperty,Object.seal,Object.freeze,Proxy,WeakMap等无法 模拟的新API,anu一个也没有用,而const,let,箭头函数,es6模块,通过babel编译就可以搞定了。

而框架用到的一些es5,es6方法,我已经提供了一个叫polyfill的文件为大家准备好,大家也可以使用bable.polyfill实现兼容。

  1. Array.prototype.forEach

  2. Function.prototype.bind

  3. JSON

  4. window.console

  5. Object.keys

  6. Object.is

  7. Object.assign

  8. Array.isArray

https://github.com/RubyLouvre...

剩下就是事件系统的兼容。React为了实现一个全能的事件系统,3万行的react-dom,有一半是在搞事件的。事件系统之所以这么难写,是因为React要实现整个标准事件流,从捕获阶段到target阶段再到冒泡阶段。如果能获取事件源对象到document这一路经过的所有元素,就能实现事件流了。但是在IE下,只有冒泡阶段,并且许多重要的表单事件不支持冒泡到document。为了事件冒泡,自jQuery时代起,前端高手们已经摸索出一套方案了。使用另一个相似的事件来伪装不冒泡事件,冒泡到document后,然后变成原来的事件触发对应的事件。

比如说IE下,使用focusin冒充focus,focusout冒充blur。chrome下,则通过addEventListener的第三个参加为true,强制让focus,blur被document捕获到。

//Ie6-9
if(msie < 9){
  eventHooks.onFocus = function(dom) {
    addEvent(dom,"focusin",function(e) {
      addEvent.fire(dom,"focus");
    });
  };
  eventHooks.onBlur = function(dom) {
    addEvent(dom,"blurout","blur");
    });
  };
}else{
eventHooks.onFocus = function(dom) {
  addEvent(
    dom,"focus","focus");
    },true
  );
};
eventHooks.onBlur = function(dom) {
  addEvent(
    dom,"blur","blur");
    },true
  );
};
}

低版本的oninput, onchange事件是一个麻烦,它们最多冒泡到form元素上。并且IE也没有oninput,只有一个相似的onpropertychange事件。IE9,IE10的oninput其实也有许多BUG,但大家要求放低些,我们也不用理会IE9,IE10的oninput事件。IE6-8的oninput事件,我们是直接在元素上绑定onpropertychange事件,然后触发一个datasetchanged 事件冒泡到document上,并且这个datasetchanged事件对象带有一个__type__属性,用来说明它原先冒充的事件。

function fixIEInput(dom,name) {
  addEvent(dom,"propertychange",function(e) {
    if (e.propertyName === "value") {
      addEvent.fire(dom,"input");
    }
  });
}

addEvent.fire = function dispatchIEEvent(dom,type,obj) {
    try {
      var hackEvent = document.createEventObject();
      if (obj) {
        Object.assign(hackEvent,obj);
      }
      hackEvent.__type__ = type;
      //IE6-8触发事件必须保证在DOM树中,否则报"SCRIPT16389: 未指明的错误"
      dom.fireEvent("ondatasetchanged",hackEvent);
    } catch (e) {}
  };


function dispatchEvent(e) {//document上绑定的事件派发器
  var __type__ = e.__type__ || e.type;
  e = new SyntheticEvent(e);
  var target = e.target;
  var paths = [];//获取整个冒泡的路径
  do {
    var events = target.__events;
    if (events) {
      paths.push({ dom: target,props: events });
    }
  } while ((target = target.parentNode) && target.nodeType === 1);
  // ...略
}

addEvent.fire这个方法在不同浏览器的实现是不一样的,这里显示的IE6-8的版本,IE9及标准浏览器是使用document.createEvent,initEvent,dispatchEvent等API来创建事件对象与触发事件。在IE6-8中,则需要用document.createEventObject创建事件对象,fireEvent来触发事件。

ondatasetchanged事件是IE一个非常偏门的事件,因为IE的 fireEvent只能触发它官网上列举的几十个事件,不能触发自定义事件。而ondatasetchanged事件在IE9,chrome,firefox等浏览器中是当成一个自定义事件来对待,但那时它是使用elem.dispatchEvent来触发了。ondatasetchanged是一个能冒泡的事件,只是充作信使,将我们要修改的属性带到document上。

此是其一,onchange事件也要通过ondatasetchanged也冒充,因为IE下它也不能冒泡到document。onchange事件在IE还是有许多BUG(或叫差异点)。checkbox,radio的onchange事件必须在失去焦点时才触发,因此我们在内部用onclick来触发,而select元素在单选时候下,用户选中了某个option,select.value会变成option的value值,但在IE6-8下它竟然不会发生改变。最绝的是select元素也不让你修改value值,后来我奠出修改HTMLSelectElement原型链的大招搞定它。

try {
    Object.defineProperty(HTMLSelectElement.prototype,"value",{
      set: function(v) {
        this._fixIEValue = v;
      },get: function() {
        return this._fixIEValue;
      }
    });
  } catch (e) {}

function fixIEChange(dom,name) {
  //IE6-8,radio,checkbox的点击事件必须在失去焦点时才触发
  var eventType = dom.type === "radio" || dom.type === "checkbox"
    ? "click"
    : "change";
  addEvent(dom,eventType,function(e) {
    if (dom.type === "select-one") {
      var idx = dom.selectedIndex,option,attr;
      if (idx > -1) {
        //IE 下select.value不会改变
        option = dom.options[idx];
        attr = option.attributes.value;
        dom.value = attr && attr.specified ? option.value : option.text;
      }
    }
    addEvent.fire(dom,"change");
  });
}

此外,滚动事件的兼容性也非常多,但在React官网中,统一大家用onWheel接口来调用,在内部实现则需要我们根据浏览器分别用onmousewheel,onwheel,DOMMouseScroll来模拟了。

当然还有很多很多细节,这里就不一一列举了。为了防止像React那样代码膨胀,针对旧版本的事件兼容,我都移到ieEvent.js文件中。然后基于它,打包了一个专门针对旧版本IE的ReactIE

https://github.com/RubyLouvre...

大家也可以通过npm安装,1.0.2就拥有这个文件

npm install anujs

下面通过一个示例介绍如何使用ReactIE.

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">

    <script src="./dist/polyfill.js"></script>
    <script src="./dist/ReactIE.js"></script>
    <script src="./dist/index9.js"></script>

</head>

<body>

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


</body>

</html>

首先建立一个页面,里面有三个JS,其实前两个文件也能单独打包的。

index.js的源码是这样的,业务线开发时是直接上JSX与es6,为了兼容IE6-8,请不要在业务代码上用Object.defineProperty与Proxy

class Select extends React.Component{
     constructor() {
        super()
        this.state = {
            value: 'bbb'
        }
        this.onChange = this.onChange.bind(this)
    }
    onChange(e){
       console.log(e.target.value)
       this.setState({
           value: e.target.value
       })
    }
    render() {
        return <div><select  value={this.state.value} onChange={this.onChange}>
            <option value='aaa'>aaa</option>
            <option value='bbb'>bbb</option>
            <option value='ccc'>ccc</option>
        </select><p>{this.state.value}</p></div>
    }
}
class Input extends React.Component{
     constructor() {
        super()
        this.state = {
            value: 'input'
        }
        this.onInput = this.onInput.bind(this)
    }
    onInput(e){
       this.setState({
           value: e.target.value
       })
    }
    render() {
        return <div><input value={this.state.value} onInput={this.onInput} />{this.state.value}</div>
    }
}
class Radio extends React.Component{
     constructor(props) {
        super(props)
        this.state = {
            value: this.props.value
        }
        this.onChange = this.onChange.bind(this)
    }
    onChange(e){
        console.log(e.target.value)
       this.setState({
           value: e.target.value
       })
    }
    render() {
        return <span><input type='radio' name={this.props.name} value={this.props.value}  onChange={this.onChange} />{this.state.value+''}</span>
    }
}
class Playground extends React.Component{
     constructor(props) {
        super(props)
        this.state = {
            value: '请上下滚动鼠标滚轮'
        }
        this.onWheel = this.onWheel.bind(this)
    }
    onWheel(e){
       this.setState({
           value: e.wheelDelta
       })
    }
    render() {
        return <div style={{width:300,height:300,backgroundColor:'red',display:'inline-block'}} onWheel={this.onWheel} >{this.state.value}</div>
    }
}
class MouseMove extends React.Component{
     constructor(props) {
        super(props)
        this.state = {
            value: '请在绿色区域移动'
        }
        this.onMouseMove = this.onMouseMove.bind(this)
    }
    onMouseMove(e){
       var v = e.pageX+' '+e.pageY;
       this.setState({
           value: v
       })
    }
    render() {
        return <div style={{width:300,backgroundColor:'#a9ea00',display:'inline-block'}} onMouseMove={this.onMouseMove} >{this.state.value}</div>
    }
}
class FocusEl extends React.Component{
     constructor(props) {
        super(props)
        this.state = {
            value: '点我'
        }
        this.onFocus = this.onFocus.bind(this)
    }
    onFocus(e){
       console.log(e.target.title)
    }
    render() {
        return <input  title={this.props.title} onKeyUp={(e)=>{console.log(e.which)}} style={{width:100,height:50,backgroundColor:'green',display:'inline-block'}} onFocus={this.onFocus} />
    }
}
window.onload = function(){
    window.s = ReactDOM.render( <div><Select /><Input /><Radio name='sex' value="男" /><Radio name='sex' value='女'/>
    <p><Playground /> <MouseMove /><FocusEl title="aaa" /><FocusEl title="bbb" /></p>
    
    </div>,document.getElementById('example'))
}

然后我们建一个webpack.config.js,用的是webpack1

const webpack = require("webpack");
const path = require("path");
const fs = require("fs");
var es3ifyPlugin = require('es3ify-webpack-plugin');

module.exports = {
  context: __dirname,entry: {
    index9: "./src/index9.js"
  },output: {
    path: __dirname + "/dist/",filename: "[name].js"
  },plugins: [new es3ifyPlugin()],module: {
    loaders: [
      {
        test: /\.jsx?$/,loader: "babel-loader",exclude: path.resolve(__dirname,"node_modules")
      }
    ]
  },resolve: {
    //如果不使用anu,就可以把这里注释掉
    alias: {
      react: "anujs/dist/ReactIE.js","react-dom": "anujs/dist/ReactIE.js"
    }
  }
};

es3ify-webpack-plugin是专门将es5代码转换为es3代码,因为es5是允许用关键字,保留字作为对象的方法与属性,而es3不能。万一碰上module.default,我们就坑大了。es3ify是一个利器。

babel是通过.babelrc来配置,里面用到一个

{
     "presets": [
         ["es2015",{ "modules": false }],"react"
     ],"plugins": [
         [
             "transform-es2015-classes",{
                 "loose": true
             }
         ]
     ]
 }

babel-plugin-transform-es2015-classes记使用loose模式。

babel-preset-es2015后面这样设置是禁用生成 "use strict",也建议直接换成babel-preset-avalon,这是个preset生成的代码兼容性更好。

如果大家用 uglify-js进行代码上线,这也要注意一下,这里有许多坑,它默认会把es3ify干的活全部白做了。详见 https://github.com/zuojj/fedl... 这篇文章

最后大家可以通过加Q 79641290 联系我。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom