Banshee

Banshee 查看完整档案

海外编辑长江职业学院  |  Mechatronics 编辑自由前端人  |  web前端开发cv工程师 编辑 Bashee 编辑
编辑

To die is to die, to live is to suffer
软件:http://www.sharerw.com/

个人动态

Banshee 赞了文章 · 7月17日

React和Vue语法并排比较:状态管理

1_Bhm8I97WYhascHsTO6gDUg.png

这是有关React和Vue语法比较的第三篇文章。在本文中,将比较两种生态系统中最著名的状态管理库(Redux和Vuex)的语法。

其它两篇:

议程

  • 创建Store
  • Action
  • 异步Action
  • Reducer | Mutation
  • Combine-Reducers | Modules
  • Connect-with-Component
  • 中间件 | 插件
  • Selector | Getter
  • DevTools

创建Store

Redux: https://redux.js.org/basics/s...

import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
const store = createStore(todoApp)
render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

Vuex: https://vuex.vuejs.org/guide/

const store = new Vuex.Store({
  state: { ... },
  mutations: { ... }
})
...
new Vue({
  el: '#app',
  store,
});

Action

Redux: https://redux.js.org/basics/a...

const ADD_TODO = 'ADD_TODO'
function addTodo(text) {
  return {
    type: ADD_TODO,
    text,
  }
}

Vuex: https://vuex.vuejs.org/guide/...

const store = new Vuex.Store({
  actions: {
    increment (context) {
      context.commit('increment') // commit a mutation to trigger state update
    }
  }
})

异步Action

Redux(redux-thunk): https://redux.js.org/advanced...

// apply redux-thunk
import thunkMiddleware from 'redux-thunk'
const store = createStore(
  rootReducer,
  applyMiddleware(thunkMiddleware)
)
...
export function fetchPosts() {
  return function (dispatch) {
    dispatch(requestPosts())
    return fetch('xxx')
      .then(response => response.json())
      .then(json => dispatch(receivePosts(json)))
  }
}

Vuex: https://vuex.vuejs.org/guide/...

actions: {
  async fetchPosts ({ commit }) {
    commit('requestPosts');
    const res = await fetch('xxx');
    commit('receivePosts', res);
  },
}

Reducer | Mutation

Redux(reducer): https://redux.js.org/basics/r...

const initialState = {
  todos: [],
}
function todoApp(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false,
          }
        ],
      }
    default:
      return state
  }
}

Vuex(mutation): https://vuex.vuejs.org/guide/...

const store = new Vuex.Store({
  mutations: {
    addTodo (state, payload) {
      state.todos = [
        ...state.todos,
        { text: payload.text, completed: false }
      ]
    }
  }
})

Combine-Reducers | Modules

Redux(combine-reducers): https://redux.js.org/api/comb...

import { combineReducers } from 'redux'
const reducers = combineReducers({
  reducerA,
  reducerB,
})
export default reducers

Vuex(modules): https://vuex.vuejs.org/guide/...

const moduleA = {
  state: { ... },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}
const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

Connect-with-Component

Redux: https://redux.js.org/basics/u...

import { connect } from 'react-redux'
import { addTodo } from '../actions'
import TargetComp from '../components/TargetComp'
// state
const mapStateToProps = (state, ownProps) => {
  return {
    todos: state.todos,
  }
}
// action
const mapDispatchToProps = (dispatch, ownProps) => {
  return {
    addTodo: (text) => {
      dispatch(addTodo(text))
    }
  }
}
const TargetContainer = connect(mapStateToProps, mapDispatchToProps)(TargetComp)
export default TargetContainer

Vuex

state: https://vuex.vuejs.org/guide/...

import { mapState } from 'vuex'
export default {
  computed: {
    ...mapState(['count']),
  }
}

action: https://vuex.vuejs.org/guide/...

import { mapActions } from 'vuex'
export default {
  methods: {
    ...mapActions(['increment']),
  }
}

中间件 | 插件

Redux(middleware): https://redux.js.org/advanced...

import { createStore, combineReducers, applyMiddleware } from 'redux'
const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}
const todoApp = combineReducers(reducers)
const store = createStore(
  todoApp,
  applyMiddleware(logger)
)

Vuex(plugin): https://vuex.vuejs.org/guide/...

const myPluginWithSnapshot = store => {
  let prevState = _.cloneDeep(store.state)
  store.subscribe((mutation, state) => {
    let nextState = _.cloneDeep(state)
    // compare `prevState` and `nextState`...
    // save state for next mutation
    prevState = nextState
  })
}
const store = new Vuex.Store({
  ...,
  plugins: process.env.NODE_ENV !== 'production' ? [myPluginWithSnapshot] : [],
})

Selector | Getter

Redux(reselect): https://redux.js.org/recipes/...

import { createSelector } from 'reselect'
const getTodos = state => state.todos
export const getDoneTodos = createSelector(
  [getTodos],
  todos.filter(t => t.completed),
)
...
import { connect } from 'react-redux'
import TodoList from '../components/TodoList'
import { getDoneTodos } from '../selectors'
const mapStateToProps = state => {
  return {
    doneTodos: getDoneTodos(state)
  }
}
const DoneTodoList = connect(mapStateToProps)(TodoList)
export default DoneTodoList

Vuex: https://vuex.vuejs.org/guide/...

const store = new Vuex.Store({
  state: { ... },
  getters: {
    doneTodos: state => {
      return state.todos.filter(t => t.completed)
    }
  }
})
...
import { mapGetters } from 'vuex'
export default {
  computed: {
    ...mapGetters(['doneTodos'])
  }
}

DevTools

Redux
https://chrome.google.com/web...

Vuex
https://chrome.google.com/web...


来源:https://medium.com/js-dojo,作者:Oahehc (Andrew),翻译:公众号《前端全栈开发者》
subscribe2.png

查看原文

赞 12 收藏 8 评论 0

Banshee 赞了文章 · 7月17日

React.js和Vue.js的语法并列比较

vuevsreact.png

React.js和Vue.js都是很好的框架。而且Next.js和Nuxt.js甚至将它们带入了一个新的高度,这有助于我们以更少的配置和更好的可维护性来创建应用程序。但是,如果你必须经常在框架之间切换,在深入探讨另一个框架之后,你可能会轻易忘记另一个框架中的语法。在本文中,我总结了这些框架的基本语法和方案,然后并排列出。我希望这可以帮助我们尽快掌握语法,不过限于篇幅,这篇文章只比较React.js和Vue.js,下一篇再谈Next.js个Nuxt.js。

Github:https://github.com/oahehc/rea...

Render

React.js

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

Vue.js

new Vue({
  render: (h) => h(App),
}).$mount("#root");

基本组件

React.js

Class component

class MyReactComponent extends React.Component {
  render() {
    return <h1>Hello world</h1>;
  }
}

Function component

function MyReactComponent() {
  return <h1>Hello world</h1>;
}

Vue.js

<template>
  <h1>Hello World</h1>
</template>
<script>
  export default {
    name: "MyVueComponent",
  };
</script>

Prop

React.js

function MyReactComponent(props) {
  const { name, mark } = props;
  return <h1>Hello {name}{mark}</h1>;
}
MyReactComponent.propTypes = {
  name: PropTypes.string.isRequired,
  mark: PropTypes.string,
}
MyReactComponent.defaultProps = {
  mark: '!',
}
...
<MyReactComponent name="world">

Vue.js

<template>
  <h1>Hello {{ name }}</h1>
</template>
<script>
  export default {
    name: "MyVueComponent",
    props: {
      name: {
        type: String,
        required: true,
      },
      mark: {
        type: String,
        default: "!",
      },
    },
  };
</script>
...
<MyVueComponent name="World" />

事件绑定

React.js

Class component

class MyReactComponent extends React.Component {
  save = () => {
    console.log("save");
  };
  render() {
    return <button onClick={this.save}>Save</button>;
  }
}

Function component

function MyReactComponent() {
  const save = () => {
    console.log("save");
  };
  return <button onClick={save}>Save</button>;
}

Vue.js

<template>
  <button @click="save()">Save</button>
</template>
<script>
  export default {
    methods: {
      save() {
        console.log("save");
      },
    },
  };
</script>

自定义事件

React.js

function MyItem({ item, handleDelete }) {
  return <button onClick={() => handleDelete(item)}>{item.name}</button>;
  /*
   * 应用useCallback钩子来防止在每次渲染时生成新的函数。
   *
   * const handleClick = useCallback(() => handleDelete(item), [item, handleDelete]);
   *
   * return <button onClick={handleClick}>{item.name}</button>;
  */
}
...
function App() {
  const handleDelete = () => { ... }
  return <MyItem item={...} handleDelete={handleDelete} />
}

Vue.js

<template>
  <button @click="deleteItem()">{{item.name}}</button>
</template>
<script>
  export default {
    name: "my-item",
    props: {
      item: Object,
    },
    methods: {
      deleteItem() {
        this.$emit("delete", this.item);
      },
    },
  };
</script>
...
<template>
  <MyItem :item="item" @delete="handleDelete" />
</template>
<script>
  export default {
    components: {
      MyItem,
    },
    methods: {
      handleDelete(item) { ... }
    },
  };
</script>

State

React.js

Class component

class MyReactComponent extends React.Component {
  state = {
    name: 'world,
  }
  render() {
    return <h1>Hello { this.state.name }</h1>;
  }
}

Function component

function MyReactComponent() {
  const [name, setName] = useState("world");
  return <h1>Hello {name}</h1>;
}

Vue.js

<template>
  <h1>Hello {{ name }}</h1>
  <!-- 使用组件状态作为prop -->
  <my-vue-component :name="name">
</template>
<script>
  export default {
    data() {
      return { name: "world" };
    },
  };
</script>

Change-State

React.js

Class component

class MyReactComponent extends React.Component {
  state = {
    count: 0,
  };
  increaseCount = () => {
    this.setState({ count: this.state.count + 1 });
    // 在更新之前获取当前状态,以确保我们没有使用陈旧的值
    // this.setState(currentState => ({ count: currentState.count + 1 }));
  };
  render() {
    return (
      <div>
        <span>{this.state.count}</span>
        <button onClick={this.increaseCount}>Add</button>
      </div>
    );
  }
}

Function component

function MyReactComponent() {
  const [count, setCount] = useState(0);
  const increaseCount = () => {
    setCount(count + 1);
    // setCount(currentCount => currentCount + 1);
  };
  return (
    <div>
      <span>{count}</span>
      <button onClick={increaseCount}>Add</button>
    </div>
  );
}

Vue.js

<template>
  <div>
    <span>{{count}}</span>
    <button @click="increaseCount()">Add</button>
  </div>
</template>
<script>
  export default {
    data() {
      return { count: 0 };
    },
    methods: {
      increaseCount() {
        this.count = this.count + 1;
      },
    },
  };
</script>

双向绑定 (仅Vue.js)

React.js

React没有双向绑定,因此我们需要自己处理数据流

function MyReactComponent() {
  const [content, setContent] = useState("");
  return (
    <input
      type="text"
      value={content}
      onChange={(e) => setContent(e.target.value)}
    />
  );
}

Vue.js

<template>
  <input type="text" v-model="content" />
</template>
<script>
  export default {
    data() {
      return { content: "" };
    },
  };
</script>

计算属性

React.js

React.js没有计算属性,但我们可以通过react hook轻松实现

function DisplayName({ firstName, lastName }) {
  const displayName = useMemo(() => {
    return `${firstName} ${lastName}`;
  }, [firstName, lastName]);
  return <div>{displayName}</div>;
}
...
<DisplayName firstName="Hello" lastName="World" />

Vue.js

<template>
  <div>{{displayName}}</div>
</template>
<script>
  export default {
    name: "display-name",
    props: {
      firstName: String,
      lastName: String,
    },
    computed: {
      displayName: function () {
        return `${this.firstName} ${this.lastName}`;
      },
    },
  };
</script>
...
<DisplayName firstName="Hello" lastName="World" />

Watch

React.js

React.js没有 watch 属性,但是我们可以通过react hook轻松实现

function MyReactComponent() {
  const [count, setCount] = useState(0);
  const increaseCount = () => {
    setCount((currentCount) => currentCount + 1);
  };
  useEffect(() => {
    localStorage.setItem("my_count", newCount);
  }, [count]);
  return (
    <div>
      <span>{count}</span>
      <button onClick={increaseCount}>Add</button>
    </div>
  );
}

Vue.js

<template>
  <div>
    <span>{{count}}</span>
    <button @click="increaseCount()">Add</button>
  </div>
</template>
<script>
  export default {
    data() {
      return { count: 0 };
    },
    methods: {
      increaseCount() {
        this.count = this.count + 1;
      },
    },
    watch: {
      count: function (newCount, oldCount) {
        localStorage.setItem("my_count", newCount);
      },
    },
  };
</script>

Children-and-Slot

React.js

function MyReactComponent({ children }) {
  return <div>{children}</div>;
}
...
<MyReactComponent>Hello World</MyReactComponent>

Vue.js

<template>
  <div>
    <slot />
  </div>
</template>
<script>
  export default {
    name: "my-vue-component",
  };
</script>
...
<MyVueComponent>Hello World</MyVueComponent>

渲染HTML

React.js

function MyReactComponent() {
  return <div dangerouslySetInnerHTML={{ __html: "<pre>...</pre>" }} />;
}

Vue.js

<template>
  <div v-html="html"></div>
</template>
<script>
  export default {
    data() {
      return {
        html: "<pre>...</pre>",
      };
    },
  };
</script>

条件渲染

React.js

function MyReactComponent() {
  const [isLoading, setLoading] = useState(true);
  return (
    <div>
      {isLoading && <span>Loading...</span>}
      {isLoading ? <div>is loading</div> : <div>is loaded</div>}
    </div>
  );
}

Vue.js

<template>
  <div>
    <!--v-show: 总是渲染,但根据条件更改CSS-->
    <span v-show="loading">Loading...</span>
    <div>
      <div v-if="loading">is loading</div>
      <div v-else>is loaded</div>
    </div>
  </div>
</template>
<script>
  export default {
    data() {
      return { loading: true };
    },
  };
</script>

列表渲染

React.js

function MyReactComponent({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          {item.name}: {item.desc}
        </li>
      ))}
    </ul>
  );
}

Vue.js

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{item.name}}: {{item.desc}}
    </li>
  </ul>
</template>
<script>
  export default {
    props: {
      items: Array,
    },
  };
</script>

Render-Props

React.js

function Modal({children, isOpen}) {
  const [isModalOpen, toggleModalOpen] = useState(isOpen);
  return (
    <div className={isModalOpen ? 'open' : 'close'}>
      {type children === 'function' ? children(toggleModalOpen) : children}
    </div>)
  ;
}
Modal.propTypes = {
  isOpen: PropTypes.bool,
  children: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
}
Modal.defaultProps = {
  isOpen: false,
}
...
<Modal isOpen>
  {(toggleModalOpen) => {
    <div>
      <div>...</div>
      <button onClick={() => toggleModalOpen(false)}>Cancel</button>
    </div>
  }}
</Modal>

Vue.js(slot)

<template>
  <div v-show="isModalOpen">
    <slot v-bind:toggleModal="toggleModalOpen" />
  </div>
</template>
<script>
  export default {
    name: "modal",
    props: {
      isOpen: {
        type: Boolean,
        default: false,
      },
    },
    data() {
      return {
        isModalOpen: this.isOpen,
      };
    },
    methods: {
      toggleModalOpen(state) {
        this.isModalOpen = state;
      },
    },
  };
</script>
...
<Modal isOpen>
  <template v-slot="slotProps">
    <div>...</div>
    <button @click="slotProps.toggleModal(false)">Close</button>
  </template>
</Modal>

生命周期

React.js

Class component

class MyReactComponent extends React.Component {
  static getDerivedStateFromProps(props, state) {}
  componentDidMount() {}
  shouldComponentUpdate(nextProps, nextState) {}
  getSnapshotBeforeUpdate(prevProps, prevState) {}
  componentDidUpdate(prevProps, prevState) {}
  componentWillUnmount() {}
  render() {
    return <div>Hello World</div>;
  }
}

Function component

function MyReactComponent() {
  // componentDidMount
  useEffect(() => {}, []);
  // componentDidUpdate + componentDidMount
  useEffect(() => {});
  // componentWillUnmount
  useEffect(() => {
    return () => {...}
  }, []);
  // 在渲染之后但在屏幕更新之前同步运行
  useLayoutEffect(() => {}, []);
  return <div>Hello World</div>;
}

Vue.js

<template>
  <div>Hello World</div>
</template>
<script>
  export default {
    beforeCreate() {},
    created() {},
    beforeMount() {},
    mounted() {},
    beforeUpdate() {},
    updated() {},
    beforeDestroy() {},
    destroyed() {},
  };
</script>

错误处理

React.js

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError(error) {
    // 更新状态,这样下一个渲染将显示回退UI。
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {}
  render() {
    if (this.state.hasError) return <h1>Something went wrong.</h1>;
    return this.props.children;
  }
}
...
<ErrorBoundary>
  <App />
</ErrorBoundary>

Vue.js

const vm = new Vue({
  data: {
    error: "",
  },
  errorCaptured: function(err, component, details) {
    error = err.toString();
  }
}

Ref

React.js

Class component

class AutofocusInput extends React.Component {
  constructor(props) {
    super(props);
    this.ref = React.createRef();
  }
  state = {
    content: "",
  };
  componentDidMount() {
    this.ref.current.focus();
  }
  setContent = (e) => {
    this.setState({ content: e.target.value });
  };
  render() {
    return (
      <input
        ref={this.ref}
        type="text"
        value={this.state.content}
        onChange={this.setContent}
      />
    );
  }
}

Function component

function AutofocusInput() {
  const [content, setContent] = useState("");
  const ref = useRef(null);
  useEffect(() => {
    if (ref && ref.current) {
      ref.current.focus();
    }
  }, []);
  return (
    <input
      ref={ref}
      type="text"
      value={content}
      onChange={(e) => setContent(e.target.value)}
    />
  );
}

Vue.js

<template>
  <input ref="input" type="text" v-model="content" />
</template>
<script>
  export default {
    name: "autofocus-input",
    data() {
      return { content: "" };
    },
    mounted() {
      this.$refs.input.focus();
    },
  };
</script>

性能优化

React.js

PureComponent

class MyReactComponent extends React.PureComponent {
  ...
}

shouldComponentUpdate

class MyReactComponent extends React.Component {
  shouldComponentUpdate(nextProps) {...}
  ...
}

React.memo

export default React.memo(
  MyReactComponent,
  (prevProps, nextProps) => {
    ...
  }
);

useMemo

export default function MyReactComponent() {
  return React.useMemo(() => {
    return <div>...</div>;
  }, []);
}

useCallback

function MyItem({ item, handleDelete }) {
  const handleClick = useCallback(() => handleDelete(item), [
    item,
    handleDelete,
  ]);
  return <button onClick={handleClick}>{item.name}</button>;
}

Vue.js

v:once

<span v-once>This will never change: {{msg}}</span>

函数式组件:我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。

<template functional>
  <h1>Hello {{ name }}</h1>
</template>
<script>
  export default {
    name: "MyVueComponent",
    props: {
      name: String,
    },
  };
</script>

keep-alive 组件

<keep-alive>
  <component :is="view"></component>
</keep-alive>

文章首发《前端外文精选》微信公众号

1.png

如果对你有所启发和帮助,可以点个关注、收藏、转发,也可以留言讨论,这是对作者的最大鼓励。

作者简介:Web前端工程师,全栈开发工程师、持续学习者。

继续阅读其他高赞文章


查看原文

赞 39 收藏 19 评论 5

Banshee 发布了文章 · 7月12日

uni-app开发小程序适配苹果端采坑总结

不得不说,任何一个开发者遇到苹果手机都会或多或少需要做适配,在使用uni-app开发小程序时,我就发现不少需要兼容的地方,今天做个小结

1.由于页面大多数内容都是动态获取,当页面高度不给固定值时,让其随内容的填充自适应的时候,iPhoneXS Max手机在页面底部会出现白版,安卓手机不会有此现象. 解决办法:

    //通过获取系统信息拿到屏幕宽高比,
    const SCREEN_WIDTH = 750
    const RATE = wx.getSystemInfoSync().screenHeight / wx.getSystemInfoSync().screenWidth
    data(){
        ScreenTotalH: SCREEN_WIDTH * RATE, //拿到真实手机高度
    }
    //展示内容高度
    <view class="main" :style="{height:ScreenTotalH+'rpx'}">
    //这样后苹果大屏手机内容底部不会白板

2.获取到时间字符串"2020-07-12 09:00:30" ,当我截取后取时和分为单位,如果时为"00"时,页面<text>{{dian}}点</text>,显示为'0'而不是我要的'00',这个问题十分奇葩,虽然不是大的影响,加个三目判断:<text>{{dian=='00'?'00':dian}}点</text>,就ok.
3.有时候小程序需要显示APP分享过来的H5页面,或者小程序本身需要显示H5页面时,需要配置https的域名,不然会无法正常显示。补充:(H5页面一般通过<web-view :data-original="src"></web-view>来展示,SRC里面的页面都是后台配置好的)
目前这是我遇到的苹果端适配问题以及解决办法,还有其他关于小程序苹果端的适配问题,大家网上去搜索一大把,解决办法大佬们都写了,我再此就不重复别人的成果了.

查看原文

赞 1 收藏 1 评论 0

Banshee 发布了文章 · 7月5日

react+antd密码编辑组件

之前的一篇文章里,写过关于忘记密码这部分的业务,今天这个主要是详细写密码编辑这一块功能,需求也很明确,考虑到密码未输入,密码输入错误以及确认密码等情况,代码里面会添加注释.总结一点,写react组件概括就是封装函数,将函数调用到不同的生命周期钩子触发响应事件

`import React from 'react';`
`import message from 'antd/lib/message';`
import { View } from 'core';   //组件继承传值封装
import { Log, Toolkit } from 'util';  //工具封装

export default class EditPassWordView extends View {
    constructor(props) {
        super(props);
        this.state = {
            showSuccessWin: false,//密码修改成功提示框显示开关
            password: '',   //输入密码
            showError: false,   // 密码输入错误显示
            showError1: false,  // 新密码未输入错误显示
            showError2: false,  //新密码和确认密码输入错误显示
            errorMsg: '',   //错误信息
            newpassword: '',    //输入新密码
            surepassword: '',   //输入确认密码
        };
        this.validateCallBack = this.validateCallBack.bind(this);
        this.handleCloseWin = this.handleCloseWin.bind(this);
    }
    /*
     *检查旧密码是否正确
    */
    _checkPassword() {
        if (!this.state.password) {
            return false;
        }
        this.props.action.checkPassword({
            oldPassword: this.state.password,
            uid: this.props.uid, //找到唯一键值
        }).then((respData) => {
            if (respData.code === 0) {
                this.setState({
                    showError: false,
                });
            } else {
                this.setState({
                    showError: true,
                    showError1: false,
                    showError2: false,
                    errorMsg: respData.msg,
                });
            }
        }, (error) => {
            Log.error(error.reason);
        });
        return false;
    }
    /**
     * 校验密码强度回调函数
     * @param {*string} validateFlag 校验结果
     * @param {*string} errTips 错误提示语
     */
    validateCallBack(validateFlag, errTips) {
        // 校验失败
        if (!validateFlag) {
            this.setState({
                showError: false,
                showError2: false,
                showError1: true,
                errorMsg: errTips,
            });
        }
    }
    /*
     *修改密码
    */
    _editPassword() {
        const { password, newpassword, surepassword } = this.state;
        // if (!this.validatePassWord(password)) {
        //     return false;
        // }
        if (!password) {
            this.setState({
                showError: true,
                showError2: false,
                showError1: false,
                errorMsg: '请输入密码',
            });
            return false;
        }
        if (!Toolkit.validatePassWord(newpassword, this.validateCallBack)) {
            return false;
        }
        if (newpassword !== surepassword) {
            this.setState({
                showError2: true,
                showError: false,
                showError1: false,
                errorMsg: '两次密码输入不一致!',
            });
            return false;
        }

        const data = {
            oldPassword: password,
            newPassword: newpassword,
            uid: this.props.uid,
        };
        this.props.action.editPassword(data).then((respData) => {
            if (respData.code === 0) {
                this.setState({
                    showError2: false,
                    showSuccessWin: true,
                });
            } else {
                message.error(respData.msg || respData.reason);
            }
        }, (error) => {
            message.error(error.msg || error.reason);
            Log.error(error.reason);
        });
        return false;
    }
    /**
     * 关闭弹出框事件
     */
    handleCloseWin() {
        this.setState({
            showSuccessWin: false,
            password: '',
            newpassword: '',
            surepassword: '',
        });
    }
    /**
     * 渲染密码修改成功的提示弹出框
     */
    _renderPopWin() {
        const { showSuccessWin } = this.state;
        if (showSuccessWin) {
            return (
                <div className="stopPropa dyModal pop-win-box">
                    <div className="wrap">
                        <div className="dyModalBg" />
                        <div className="modalContent">
                            <div className="header">
                                <h3>提示</h3>
                                <i className="close-icon" onClick={this.handleCloseWin} />
                            </div>
                            <div className="content">
                                <div className="tips">
                                    <p style={{ fontWeight: 'bold' }}>密码修改成功</p>
                                    <p className="sub">请使用新密码重新登录一次</p>
                                </div>
                                <div className="btn-box">
                                    <a className="btn-sure" href={this.props.logoutUrl}>确定</a>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            );
        }
        return '';
    }
    _render() {
        return (
            <div>
                {this._renderPopWin()}
                <div className="change_password">
                    <h1>修改密码</h1>
                    <ul>
                        <li>
                            <span>原密码:</span>
                            <input
                                type="password"
                                name="password"
                                value={this.state.password}
                                onChange={(evt) => {
                                    this.setState({
                                        showError: false,
                                        showError2: false,
                                    });
                                    this.state.password = evt.target.value;
                                }}
                                onKeyUp={
                                    (evt) => {
                                        if (evt.keyCode === 13) {
                                            this._editPassword();
                                        }
                                    }
                                }
                            />
                            <strong
                                className="password"
                                style={{ display: this.state.showError ? 'block' : 'none' }}
                            >{this.state.errorMsg}</strong>
                        </li>
                        <li>
                            <span>新密码:</span>
                            <input
                                type="password"
                                name="newpassword"
                                value={this.state.newpassword}
                                onChange={(evt) => {
                                    this.setState({
                                        showError1: false,
                                        showError2: false,
                                    });
                                    this.state.newpassword = evt.target.value;
                                }}
                                onKeyUp={
                                    (evt) => {
                                        if (evt.keyCode === 13) {
                                            this._editPassword();
                                        }
                                    }
                                }
                            />
                            <strong
                                className="password"
                                style={{ display: this.state.showError1 ? 'block' : 'none' }}
                            >{this.state.errorMsg}</strong>
                        </li>
                        <li>
                            <span>确认密码:</span>
                            <input
                                type="password"
                                name="surepassword"
                                value={this.state.surepassword}
                                onChange={(evt) => {
                                    this.setState({
                                        showError2: false,
                                    });
                                    this.state.surepassword = evt.target.value;
                                }}
                                onKeyUp={
                                    (evt) => {
                                        if (evt.keyCode === 13) {
                                            this._editPassword();
                                        }
                                    }
                                }
                            />
                            <strong
                                className="password"
                                style={{ display: this.state.showError2 ? 'block' : 'none' }}
                            >{this.state.errorMsg}</strong>
                        </li>
                    </ul>
                    <button
                        onClick={this._editPassword.bind(this)}
                    >修改密码</button>
                </div>
            </div>
        );
    }
}
查看原文

赞 0 收藏 0 评论 0

Banshee 发布了文章 · 6月27日

react定义异常组件

企业开发中,组件众多,甚至组件嵌套组件也很多,所以当成百上千个组件加载起来,难免的会有些意外报错情况,根据现有的开发经验,将react项目里面异常组件定义出来,并给与提示信息而非直接抛出红色错误
这个里面有个额外的知识补充,依据于[Error.captureStackTrace],关于这个方面的知识我也是学习的这个大佬的文章,大家也可以看看:https://segmentfault.com/a/11...

export default class Exception extends Error {

    /* Exceptions for Remote START */
    static CONNECTION_TIMEOUT = '远端请求超时。';
    static NO_CONTAINER = '未找到容器。';
    /* Exceptions for Remote END */

    static PARAMETER_INVALIDE = '无效的参数。';
    static TYPE_INCORRECT = '错误的数据类型。';
    static LOG_DRIVER_UNDEFINED = '未定义的日志驱动。';
    /**
     * 构造函数。
     */
    constructor(message) {
        super();
        if ('captureStackTrace' in Error) {
            Error.captureStackTrace(this, Exception);
        }
        this.name = 'WHException';
        this.message = message;
    }
}

使用此文件一般可以放在Action里面

查看原文

赞 0 收藏 0 评论 0

Banshee 关注了用户 · 6月20日

浪子神剑 @haizlin

前端面试每日3+1发起者
前端剑解公众号作者
https://github.com/haizlin/fe...
羽毛球、前端架构师

关注 44

Banshee 赞了文章 · 6月17日

浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务

浏览器原理.jpg

关键词:多进程、单线程、事件循环、消息队列、宏任务、微任务

看到这些词仿佛比较让人摸不着头脑,其实在我们的日常开发中,早就和他们打过交道了。

我来举几个常见的例子:

  • 我执行了一段js,页面就卡了挺久才有响应
  • 我触发了一个按钮的click事件,click事件处理器做出了响应
  • 我用setTimeout(callback, 1000)给代码加了1s的延时,1秒里发生了很多事情,然后功能正常了
  • 我用setInterval(callback, 100)给代码加了100ms的时间轮训,直到期待的那个变量出现再执行后续的代码,并且结合setTimeout删除这个定时器
  • 我用Promise,async/await顺序执行了异步代码
  • 我用EventEmitter、new Vue()做事件广播订阅
  • 我用MutationObserver监听了DOM更新
  • 我手写了一个Event类做事件的广播订阅
  • 我用CustomEvent创建了自定义事件
  • 我·······

其实上面举的这些click, setTimeout, setInterval, Promise,async/await, EventEmitter, MutationObserver, Event类, CustomEvent多进程、单线程、事件循环、消息队列、宏任务、微任务或多或少的都有所联系。

而且也与浏览器的运行原理有一些关系,作为每天在浏览器里辛勤耕耘的前端工程师们,浏览器的运行原理(多进程、单线程、事件循环、消息队列、宏任务、微任务)可以说是必须要掌握的内容了,不仅对面试有用,对手上负责的开发工作也有很大的帮助。

  • 浅谈浏览器架构

    • 浏览器可以是哪种架构?
    • 如何理解Chrome的多进程架构?
    • 前端最核心的渲染进程包含哪些线程?

      • 主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成)
      • 光栅线程(Raster thread)
      • 合成线程(Compositor thread)
      • 工作线程(Worker thread)
  • 浅谈单线程js

    • js引擎图
    • 什么是单线程js?
    • 单线程js属于浏览器的哪个进程?
    • js为什么要设计成单线程的?
  • 事件循环与消息队列

    • 什么是事件循环?
    • 什么是消息队列?
    • 如何实现一个 EventEmitter(支持 on,once,off,emit)?
  • 宏任务和微任务

    • 哪些属于宏任务?
    • 哪些属于微任务?
    • 事件循环,消息队列与宏任务、微任务之间的关系是什么?
    • 微任务添加和执行流程示意图
  • 浏览器页面循环系统原理图

    • 消息队列和事件循环
    • setTimeout
    • XMLHttpRequest
    • 宏任务
  • 参考资料

浅谈Chrome架构

浏览器可以是哪种架构?

浏览器本质上也是一个软件,它运行于操作系统之上,一般来说会在特定的一个端口开启一个进程去运行这个软件,开启进程之后,计算机为这个进程分配CPU资源、运行时内存,磁盘空间以及网络资源等等,通常会为其指定一个PID来代表它。

先来看看我的机器上运行的微信和Chrome的进程详情

软件CPU(%)线程PID内存端口
微信0.146587555MB124301
Chrome7.948481603MB1487

如果自己设计一个浏览器,浏览器可以是那种架构呢?

  • 单进程架构(线程间通信)
  • 多进程架构(进程间IPC通信)

如果浏览器单进程架构的话,需要在一个进程内做到网络、调度、UI、存储、GPU、设备、渲染、插件等等任务,通常来说可以为每个任务开启一个线程,形成单进程多线程的浏览器架构。

但是由于这些功能的日益复杂,例如将网络,存储,UI放在一个线程中的话,执行效率和性能越来越地下,不能再向下拆分出类似“线程”的子空间

因此,为了逐渐强化浏览器的功能,于是产生了多进程架构的浏览器,可以将网络、调度、UI、存储、GPU、设备、渲染、插件等等任务分配给多个单独的进程,在每一个单独的进程内,又可以拆分出多个子线程,极大程度地强化了浏览器。

如何理解Chrome的多进程架构?

Chrome作为浏览器届里的一哥,他也是多进程IPC架构的。
image

Chrome多进程架构主要包括以下4个进程:

  • Browser进程(负责地址栏、书签栏、前进后退、网络请求、文件访问等)
  • Renderer进程(负责一个Tab内所有和网页渲染有关的所有事情,是最核心的进程
  • GPU进程(负责GPU相关的任务)
  • Plugin进程(负责Chrome插件相关的任务)

Chrome 多进程架构的优缺点
优点

  • 每一个Tab就是要给单独的进程
  • 由于每个Tab都有自己独立的Renderer进程,因此某一个Tab出问题不会影响其它Tab

缺点

  • Tab间内存不共享,不同进程内存包含相同内容

Chrome多进程架构实锤图
image

前端最核心的渲染(Renderer)进程包含哪些线程?

image

渲染进程主要包括4个线程:

  • 主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成)
  • 光栅线程(Raster thread)
  • 合成线程(Compositor thread)
  • 工作线程(Worker thread)

渲染进程的主线程知识点:

  • 下载资源:主线程可以通过Browser进程的network线程下载图片,css,js等渲染DOM需要的资源文件
  • 执行JS:主线程在遇到<script>标签时,会下载并且执行js,执行js时,为了避免改变DOM的结构,解析HTML停滞,js执行完成后继续解析HTML。正是因为JS执行会阻塞UI渲染,而JS又是浏览器的一哥,因此浏览器常常被看做是单线程的。
  • 计算样式:主线程会基于CSS选择器或者浏览器默认样式去进行样式计算,最终生成Computed Style
  • 进行布局:主线程计算好样式以后,可以确定元素的位置信息以及盒模型信息,对元素进行布局
  • 进行绘制:主线程根据先后顺序以及层级关系对元素进行渲染,通常会生成多个图层
  • 最终合成:主线程将渲染后的多个frame(帧)合成,类似flash的帧动画和PS的图层

渲染进程的主线程细节可以查阅Chrome官方的博客:Inside look at modern web browser (part 3)Rendering Performance

渲染进程的合成线程知识点:

  • 浏览器滚动时,合成线程会创建一个新的合成帧发送给GPU
  • 合成线程工作与主线程无关,不用等待样式计算或者JS的执行,因此合成线程相关的动画比涉及到主线程重新计算样式和js的动画更加流畅

下面来看下主线程、合成线程和光栅线程一起作用的过程
1.主线程主要遍历布局树生成层树
image
2.栅格线程栅格化磁贴到GPU
image
3.合成线程将磁贴合成帧并通过IPC传递给Browser进程,显示在屏幕上
image

图片引自Chrome官方博客:Inside look at modern web browser (part 3)

浅谈单线程js

js引擎图

应用程序(实现)方言和最后版本ECMAScript版本
Google Chrome,V8引擎JavaScriptECMA-262,版本6
Mozilla Firefox,Gecko排版引擎,SpiderMonkey和RhinoJavaScript 1.8.5ECMA-262,版本6
Safari,Nitro引擎JavaScriptECMA-262,版本6
Microsoft Edge,Chakra引擎JavaScriptEMCA-262,版本6
Opera,Carakan引擎(改用V8之前)一些JavaScript 1.5特性及一些JScript扩展[12]ECMA-262,版本5.1
KHTML排版引擎,KDE项目的KonquerorJavaScript 1.5ECMA-262,版本3
Adobe AcrobatJavaScript 1.5ECMA-262,版本3
OpenLaszloJavaScript 1.4ECMA-262,版本3
Max/MSPJavaScript 1.5ECMA-262,版本3
ANT Galio 3JavaScript 1.5附带RMAI扩展ECMA-262,版本3

什么是单线程js?

如果仔细阅读过第一部分“谈谈浏览器架构”的话,这个答案其实已经非常显而易见了。
在”前端最核心的渲染进程包含哪些线程?“这里我们提到了主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成,注意其中的执行js,这里其实已经明确告诉了我们Chrome中JavaScript运行的位置。

那么Chrome中JavaScript运行的位置在哪里呢?

渲染进程(Renderer Process)中的主线程(Main Thread)

单线程js属于浏览器的哪个进程?

单线程的js -> 主线程(Main Thread)-> 渲染进程(Renderer Process)

js为什么要设计成单线程的?

其实更为严谨的表述是:“浏览器中的js执行和UI渲染是在一个线程中顺序发生的。”

这是因为在渲染进程的主线程在解析HTML生成DOM树的过程中,如果此时执行JS,主线程会主动暂停解析HTML,先去执行JS,等JS解析完成后,再继续解析HTML。

那么为什么要“主线程会主动暂停解析HTML,先去执行JS,再继续解析HTML呢”?

这是主线程在解析HTML生成DOM树的过程中会执行style,layout,render以及composite的操作,而JS可以操作DOM,CSSOM,会影响到主线程在解析HTML的最终渲染结果,最终页面的渲染结果将变得不可预见。

如果主线程一边解析HTML进行渲染,JS同时在操作DOM或者CSSOM,结果会分为以下情况:

  • 以主线程解析HTML的渲染结果为准
  • 以JS同时在操作DOM或者CSSOM的渲染结果为准

考虑到最终页面的渲染效果的一致性,所以js在浏览器中的实现,被设计成为了JS执行阻塞UI渲染型。

事件循环

什么是事件循环?

事件循环英文名叫做Event Loop,是一个在前端届老生常谈的话题。
我也简单说一下我对事件循环的认识:

事件循环可以拆为“事件”+“循环”。
先来聊聊“事件”:

如果你有一定的前端开发经验,对于下面的“事件”一定不陌生:

  • click、mouseover等等交互事件
  • 事件冒泡、事件捕获、事件委托等等
  • addEventListener、removeEventListener()
  • CustomEvent(自定义事件实现自定义交互)
  • EventEmitter、EventBus(on,emit,once,off,这种东西经常出面试题)
  • 第三方库的事件系统

有事件,就有事件处理器:在事件处理器中,我们会应对这个事件做一些特殊操作。

那么浏览器怎么知道有事件发生了呢?怎么知道用户对某个button做了一次click呢?

如果我们的主线程只是静态的,没有循环的话,可以用js伪代码将其表述为:

function mainThread() {
     console.log("Hello World!");
     console.log("Hello JavaScript!");
}
mainThread();

执行完一次mainThread()之后,这段代码就无效了,mainThread并不是一种激活状态,对于I/O事件是没有办法捕获到的。

因此对事件加入了“循环”,将渲染进程的主线程变为激活状态,可以用js伪代码表述如下:

// click event
function clickTrigger() {
    return "我点击按钮了"
}
// 可以是while循环
function mainThread(){
    while(true){
        if(clickTrigger()) { console.log(“通知click事件监听器”) }
        clickTrigger = null;
     }
}
mainThread();

也可以是for循环

for(;;){
    if(clickTrigger()) { console.log(“通知click事件监听器”) }
    clickTrigger = null;
}

在事件监听器中做出响应:

button.addEventListener('click', ()=>{
    console.log("多亏了事件循环,我(浏览器)才能知道用户做了什么操作");
})

什么是消息队列?

消息队列可以拆为“消息”+“队列”。
消息可以理解为用户I/O;队列就是先进先出的数据结构。
而消息队列,则是用于连接用户I/O与事件循环的桥梁。

队列数据结构图

image

入队出队图

image

在js中,如何发现出队列FIFO的特性?

下面这个结构大家都熟悉,瞬间体现出队列FIFO的特性。

// 定义一个队列
let queue = [1,2,3];
// 入队
queue.push(4); // queue[1,2,3,4]
// 出队
queue.shift(); // 1 queue [2,3,4]

假设用户做出了"click button1","click button3","click button 2"的操作。
事件队列定义为:

const taskQueue = ["click button1","click button3","click button 2"];
while(taskQueue.length>0){
    taskQueue.shift(); // 任务依次出队
}

任务依次出队:
"click button1"
"click button3"
"click button 2"

此时由于mainThread有事件循环,它会被浏览器渲染进程的主线程事件循环系统捕获,并在对应的事件处理器做出响应。

button1.addEventListener('click', ()=>{
    console.log("click button1");
})
button2.addEventListener('click', ()=>{
    console.log("click button 2");
})
button3.addEventListener('click', ()=>{
   console.log("click button3")
})

依次打印:"click button1","click button3","click button 2"。

因此,可以将消息队列理解为连接用户I/O操作和浏览器事件循环系统的任务队列

如何实现一个 EventEmitter(支持 on,once,off,emit)?

/**
 * 说明:简单实现一个事件订阅机制,具有监听on和触发emit方法
 * 示例:
 * on(event, func){ ... }
 * emit(event, ...args){ ... }
 * once(event, func){ ... }
 * off(event, func){ ... }
 * const event = new EventEmitter();
 * event.on('someEvent', (...args) => {
 *     console.log('some_event triggered', ...args);
 * });
 * event.emit('someEvent', 'abc', '123');
 * event.once('someEvent', (...args) => {
 *     console.log('some_event triggered', ...args);
 * });
 * event.off('someEvent', callbackPointer); // callbackPointer为回调指针,不能是匿名函数
 */
class EventEmitter {
  constructor() {
    this.listeners = [];
  }
  on(event, func) {
    const callback = (listener) => listener.name === event;
    const idx = this.listeners.findIndex(callback);
    if (idx === -1) {
      this.listeners.push({
        name: event,
        callbacks: [func],
      });
    } else {
      this.listeners[idx].callbacks.push(func);
    }
  }
  emit(event, ...args) {
    if (this.listeners.length === 0) return;
    const callback = (listener) => listener.name === event;
    const idx = this.listeners.findIndex(callback);
    if (idx === -1) return;
    const listener = this.listeners[idx];

    if (listener.isOnce) {
      listener.callbacks[0](...args);
      this.listeners.splice(idx, 1);
    } else {
      listener.callbacks.forEach((cb) => {
        cb(...args);
      });
    }
  }
  once(event, func) {
    const callback = (listener) => listener.name === event;
    let idx = this.listeners.findIndex(callback);
    if (idx !== -1) return;
    this.listeners.push({
      name: event,
      callbacks: [func],
      isOnce: true,
    });
  }
  off(event, func) {
    if (this.listeners.length === 0) return;
    const callback = (listener) => listener.name === event;
    let idx = this.listeners.findIndex(callback);
    if (idx === -1) return;
    let callbacks = this.listeners[idx].callbacks;
    for (let i = 0; i < callbacks.length; i++) {
      if (callbacks[i] === func) {
        callbacks.splice(i, 1);
        break;
      }
    }
  }
}

// let event = new EventEmitter();
// let onceCallback = (...args) => {
//   console.log("once_event triggered", ...args);
// };
// let onceCallback1 = (...args) => {
//   console.log("once_event 1 triggered", ...args);
// };
// // once仅监听一次
// event.once("onceEvent", onceCallback);
// event.once("onceEvent", onceCallback1);
// event.emit("onceEvent", "abc", "123");
// event.emit("onceEvent", "abc", "456");

// let onCallback = (...args) => {
//   console.log("on_event triggered", ...args);
// };
// let onCallback1 = (...args) => {
//   console.log("on_event 1 triggered", ...args);
// };
// event.on("onEvent", onCallback);
// event.on("onEvent", onCallback1);
// event.emit("onEvent", "abc", "123");
// // off销毁指定回调
// event.off("onEvent", onCallback);
// event.emit("onEvent", "abc", "123");

宏任务和微任务

  • 哪些属于宏任务?
  • 哪些属于微任务?
  • 事件循环,消息队列与宏任务、微任务之间的关系是什么?
  • 微任务添加和执行流程示意图

哪些属于宏任务?

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI渲染

哪些属于微任务?

  • Promise
  • MutationObserver
  • process.nextTick
  • queueMicrotask

事件循环,消息队列与宏任务、微任务之间的关系是什么?

  • 宏任务入队消息队列,可以将消息队列理解为宏任务队列
  • 每个宏任务内有一个微任务队列,执行过程中微任务入队当前宏任务的微任务队列
  • 宏任务微任务队列为空时才会执行下一个宏任务
  • 事件循环捕获队列出队的宏任务和微任务并执行

事件循环会不断地处理消息队列出队的任务,而宏任务指的就是入队到消息队列中的任务,每个宏任务都有一个微任务队列,宏任务在执行过程中,如果此时产生微任务,那么会将产生的微任务入队到当前的微任务队列中,在当前宏任务的主要任务完成后,会依次出队并执行微任务队列中的任务,直到当前微任务队列为空才会进行下一个宏任务。

微任务添加和执行流程示意图

假设在执行解析HTML这个宏任务的过程中,产生了Promise和MutationObserver这两个微任务。

// parse HTML···
Promise.resolve();
removeChild();

微任务队列会如何表现呢?

image

image

图片引自:极客时间的《浏览器工作原理与实践》

过程可以拆为以下几步:

  1. 主线程执行JS Promise.resolve(); removeChild();
  2. parseHTML宏任务暂停
  3. Promise和MutationObserver微任务入队到parseHTML宏任务的微任务队列
  4. 微任务1 Promise.resolve()执行
  5. 微任务2 removeChild();执行
  6. 微任务队列为空,parseHTML宏任务继续执行
  7. parseHTML宏任务完成,执行下一个宏任务

浏览器页面循环系统原理图

以下所有图均来自极客时间《《浏览器工作原理与实践》- 浏览器中的页面循环系统》,可以帮助理解消息队列,事件循环,宏任务和微任务。

  • 消息队列和事件循环
  • setTimeout
  • XMLHttpRequest
  • 宏任务

消息队列和事件循环

线程的一次执行
image
在线程中引入事件循环
image
渲染进程线程之间发送任务
image

image
线程模型:队列 + 循环
image
跨进程发送消息
image
单个任务执行时间过久
image

setTimeout

长任务导致定时器被延后执行
image
循环嵌套调用 setTimeout
image

XMLHttpRequest

消息循环系统调用栈记录
image
XMLHttpRequest 工作流程图
image
HTTPS 混合内容警告
image
使用 XMLHttpRequest 混合资源失效
image

宏任务

宏任务延时无法保证
image

参考资料

如果文中有不对的地方,欢迎指正和交流~

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

努力成为优秀前端工程师!
查看原文

赞 29 收藏 22 评论 0

Banshee 收藏了文章 · 6月17日

浅谈浏览器架构、单线程js、事件循环、消息队列、宏任务和微任务

浏览器原理.jpg

关键词:多进程、单线程、事件循环、消息队列、宏任务、微任务

看到这些词仿佛比较让人摸不着头脑,其实在我们的日常开发中,早就和他们打过交道了。

我来举几个常见的例子:

  • 我执行了一段js,页面就卡了挺久才有响应
  • 我触发了一个按钮的click事件,click事件处理器做出了响应
  • 我用setTimeout(callback, 1000)给代码加了1s的延时,1秒里发生了很多事情,然后功能正常了
  • 我用setInterval(callback, 100)给代码加了100ms的时间轮训,直到期待的那个变量出现再执行后续的代码,并且结合setTimeout删除这个定时器
  • 我用Promise,async/await顺序执行了异步代码
  • 我用EventEmitter、new Vue()做事件广播订阅
  • 我用MutationObserver监听了DOM更新
  • 我手写了一个Event类做事件的广播订阅
  • 我用CustomEvent创建了自定义事件
  • 我·······

其实上面举的这些click, setTimeout, setInterval, Promise,async/await, EventEmitter, MutationObserver, Event类, CustomEvent多进程、单线程、事件循环、消息队列、宏任务、微任务或多或少的都有所联系。

而且也与浏览器的运行原理有一些关系,作为每天在浏览器里辛勤耕耘的前端工程师们,浏览器的运行原理(多进程、单线程、事件循环、消息队列、宏任务、微任务)可以说是必须要掌握的内容了,不仅对面试有用,对手上负责的开发工作也有很大的帮助。

  • 浅谈浏览器架构

    • 浏览器可以是哪种架构?
    • 如何理解Chrome的多进程架构?
    • 前端最核心的渲染进程包含哪些线程?

      • 主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成)
      • 光栅线程(Raster thread)
      • 合成线程(Compositor thread)
      • 工作线程(Worker thread)
  • 浅谈单线程js

    • js引擎图
    • 什么是单线程js?
    • 单线程js属于浏览器的哪个进程?
    • js为什么要设计成单线程的?
  • 事件循环与消息队列

    • 什么是事件循环?
    • 什么是消息队列?
    • 如何实现一个 EventEmitter(支持 on,once,off,emit)?
  • 宏任务和微任务

    • 哪些属于宏任务?
    • 哪些属于微任务?
    • 事件循环,消息队列与宏任务、微任务之间的关系是什么?
    • 微任务添加和执行流程示意图
  • 浏览器页面循环系统原理图

    • 消息队列和事件循环
    • setTimeout
    • XMLHttpRequest
    • 宏任务
  • 参考资料

浅谈Chrome架构

浏览器可以是哪种架构?

浏览器本质上也是一个软件,它运行于操作系统之上,一般来说会在特定的一个端口开启一个进程去运行这个软件,开启进程之后,计算机为这个进程分配CPU资源、运行时内存,磁盘空间以及网络资源等等,通常会为其指定一个PID来代表它。

先来看看我的机器上运行的微信和Chrome的进程详情

软件CPU(%)线程PID内存端口
微信0.146587555MB124301
Chrome7.948481603MB1487

如果自己设计一个浏览器,浏览器可以是那种架构呢?

  • 单进程架构(线程间通信)
  • 多进程架构(进程间IPC通信)

如果浏览器单进程架构的话,需要在一个进程内做到网络、调度、UI、存储、GPU、设备、渲染、插件等等任务,通常来说可以为每个任务开启一个线程,形成单进程多线程的浏览器架构。

但是由于这些功能的日益复杂,例如将网络,存储,UI放在一个线程中的话,执行效率和性能越来越地下,不能再向下拆分出类似“线程”的子空间

因此,为了逐渐强化浏览器的功能,于是产生了多进程架构的浏览器,可以将网络、调度、UI、存储、GPU、设备、渲染、插件等等任务分配给多个单独的进程,在每一个单独的进程内,又可以拆分出多个子线程,极大程度地强化了浏览器。

如何理解Chrome的多进程架构?

Chrome作为浏览器届里的一哥,他也是多进程IPC架构的。
image

Chrome多进程架构主要包括以下4个进程:

  • Browser进程(负责地址栏、书签栏、前进后退、网络请求、文件访问等)
  • Renderer进程(负责一个Tab内所有和网页渲染有关的所有事情,是最核心的进程
  • GPU进程(负责GPU相关的任务)
  • Plugin进程(负责Chrome插件相关的任务)

Chrome 多进程架构的优缺点
优点

  • 每一个Tab就是要给单独的进程
  • 由于每个Tab都有自己独立的Renderer进程,因此某一个Tab出问题不会影响其它Tab

缺点

  • Tab间内存不共享,不同进程内存包含相同内容

Chrome多进程架构实锤图
image

前端最核心的渲染(Renderer)进程包含哪些线程?

image

渲染进程主要包括4个线程:

  • 主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成)
  • 光栅线程(Raster thread)
  • 合成线程(Compositor thread)
  • 工作线程(Worker thread)

渲染进程的主线程知识点:

  • 下载资源:主线程可以通过Browser进程的network线程下载图片,css,js等渲染DOM需要的资源文件
  • 执行JS:主线程在遇到<script>标签时,会下载并且执行js,执行js时,为了避免改变DOM的结构,解析HTML停滞,js执行完成后继续解析HTML。正是因为JS执行会阻塞UI渲染,而JS又是浏览器的一哥,因此浏览器常常被看做是单线程的。
  • 计算样式:主线程会基于CSS选择器或者浏览器默认样式去进行样式计算,最终生成Computed Style
  • 进行布局:主线程计算好样式以后,可以确定元素的位置信息以及盒模型信息,对元素进行布局
  • 进行绘制:主线程根据先后顺序以及层级关系对元素进行渲染,通常会生成多个图层
  • 最终合成:主线程将渲染后的多个frame(帧)合成,类似flash的帧动画和PS的图层

渲染进程的主线程细节可以查阅Chrome官方的博客:Inside look at modern web browser (part 3)Rendering Performance

渲染进程的合成线程知识点:

  • 浏览器滚动时,合成线程会创建一个新的合成帧发送给GPU
  • 合成线程工作与主线程无关,不用等待样式计算或者JS的执行,因此合成线程相关的动画比涉及到主线程重新计算样式和js的动画更加流畅

下面来看下主线程、合成线程和光栅线程一起作用的过程
1.主线程主要遍历布局树生成层树
image
2.栅格线程栅格化磁贴到GPU
image
3.合成线程将磁贴合成帧并通过IPC传递给Browser进程,显示在屏幕上
image

图片引自Chrome官方博客:Inside look at modern web browser (part 3)

浅谈单线程js

js引擎图

应用程序(实现)方言和最后版本ECMAScript版本
Google Chrome,V8引擎JavaScriptECMA-262,版本6
Mozilla Firefox,Gecko排版引擎,SpiderMonkey和RhinoJavaScript 1.8.5ECMA-262,版本6
Safari,Nitro引擎JavaScriptECMA-262,版本6
Microsoft Edge,Chakra引擎JavaScriptEMCA-262,版本6
Opera,Carakan引擎(改用V8之前)一些JavaScript 1.5特性及一些JScript扩展[12]ECMA-262,版本5.1
KHTML排版引擎,KDE项目的KonquerorJavaScript 1.5ECMA-262,版本3
Adobe AcrobatJavaScript 1.5ECMA-262,版本3
OpenLaszloJavaScript 1.4ECMA-262,版本3
Max/MSPJavaScript 1.5ECMA-262,版本3
ANT Galio 3JavaScript 1.5附带RMAI扩展ECMA-262,版本3

什么是单线程js?

如果仔细阅读过第一部分“谈谈浏览器架构”的话,这个答案其实已经非常显而易见了。
在”前端最核心的渲染进程包含哪些线程?“这里我们提到了主线程(Main thread)(下载资源、执行js、计算样式、进行布局、绘制合成,注意其中的执行js,这里其实已经明确告诉了我们Chrome中JavaScript运行的位置。

那么Chrome中JavaScript运行的位置在哪里呢?

渲染进程(Renderer Process)中的主线程(Main Thread)

单线程js属于浏览器的哪个进程?

单线程的js -> 主线程(Main Thread)-> 渲染进程(Renderer Process)

js为什么要设计成单线程的?

其实更为严谨的表述是:“浏览器中的js执行和UI渲染是在一个线程中顺序发生的。”

这是因为在渲染进程的主线程在解析HTML生成DOM树的过程中,如果此时执行JS,主线程会主动暂停解析HTML,先去执行JS,等JS解析完成后,再继续解析HTML。

那么为什么要“主线程会主动暂停解析HTML,先去执行JS,再继续解析HTML呢”?

这是主线程在解析HTML生成DOM树的过程中会执行style,layout,render以及composite的操作,而JS可以操作DOM,CSSOM,会影响到主线程在解析HTML的最终渲染结果,最终页面的渲染结果将变得不可预见。

如果主线程一边解析HTML进行渲染,JS同时在操作DOM或者CSSOM,结果会分为以下情况:

  • 以主线程解析HTML的渲染结果为准
  • 以JS同时在操作DOM或者CSSOM的渲染结果为准

考虑到最终页面的渲染效果的一致性,所以js在浏览器中的实现,被设计成为了JS执行阻塞UI渲染型。

事件循环

什么是事件循环?

事件循环英文名叫做Event Loop,是一个在前端届老生常谈的话题。
我也简单说一下我对事件循环的认识:

事件循环可以拆为“事件”+“循环”。
先来聊聊“事件”:

如果你有一定的前端开发经验,对于下面的“事件”一定不陌生:

  • click、mouseover等等交互事件
  • 事件冒泡、事件捕获、事件委托等等
  • addEventListener、removeEventListener()
  • CustomEvent(自定义事件实现自定义交互)
  • EventEmitter、EventBus(on,emit,once,off,这种东西经常出面试题)
  • 第三方库的事件系统

有事件,就有事件处理器:在事件处理器中,我们会应对这个事件做一些特殊操作。

那么浏览器怎么知道有事件发生了呢?怎么知道用户对某个button做了一次click呢?

如果我们的主线程只是静态的,没有循环的话,可以用js伪代码将其表述为:

function mainThread() {
     console.log("Hello World!");
     console.log("Hello JavaScript!");
}
mainThread();

执行完一次mainThread()之后,这段代码就无效了,mainThread并不是一种激活状态,对于I/O事件是没有办法捕获到的。

因此对事件加入了“循环”,将渲染进程的主线程变为激活状态,可以用js伪代码表述如下:

// click event
function clickTrigger() {
    return "我点击按钮了"
}
// 可以是while循环
function mainThread(){
    while(true){
        if(clickTrigger()) { console.log(“通知click事件监听器”) }
        clickTrigger = null;
     }
}
mainThread();

也可以是for循环

for(;;){
    if(clickTrigger()) { console.log(“通知click事件监听器”) }
    clickTrigger = null;
}

在事件监听器中做出响应:

button.addEventListener('click', ()=>{
    console.log("多亏了事件循环,我(浏览器)才能知道用户做了什么操作");
})

什么是消息队列?

消息队列可以拆为“消息”+“队列”。
消息可以理解为用户I/O;队列就是先进先出的数据结构。
而消息队列,则是用于连接用户I/O与事件循环的桥梁。

队列数据结构图

image

入队出队图

image

在js中,如何发现出队列FIFO的特性?

下面这个结构大家都熟悉,瞬间体现出队列FIFO的特性。

// 定义一个队列
let queue = [1,2,3];
// 入队
queue.push(4); // queue[1,2,3,4]
// 出队
queue.shift(); // 1 queue [2,3,4]

假设用户做出了"click button1","click button3","click button 2"的操作。
事件队列定义为:

const taskQueue = ["click button1","click button3","click button 2"];
while(taskQueue.length>0){
    taskQueue.shift(); // 任务依次出队
}

任务依次出队:
"click button1"
"click button3"
"click button 2"

此时由于mainThread有事件循环,它会被浏览器渲染进程的主线程事件循环系统捕获,并在对应的事件处理器做出响应。

button1.addEventListener('click', ()=>{
    console.log("click button1");
})
button2.addEventListener('click', ()=>{
    console.log("click button 2");
})
button3.addEventListener('click', ()=>{
   console.log("click button3")
})

依次打印:"click button1","click button3","click button 2"。

因此,可以将消息队列理解为连接用户I/O操作和浏览器事件循环系统的任务队列

如何实现一个 EventEmitter(支持 on,once,off,emit)?

/**
 * 说明:简单实现一个事件订阅机制,具有监听on和触发emit方法
 * 示例:
 * on(event, func){ ... }
 * emit(event, ...args){ ... }
 * once(event, func){ ... }
 * off(event, func){ ... }
 * const event = new EventEmitter();
 * event.on('someEvent', (...args) => {
 *     console.log('some_event triggered', ...args);
 * });
 * event.emit('someEvent', 'abc', '123');
 * event.once('someEvent', (...args) => {
 *     console.log('some_event triggered', ...args);
 * });
 * event.off('someEvent', callbackPointer); // callbackPointer为回调指针,不能是匿名函数
 */
class EventEmitter {
  constructor() {
    this.listeners = [];
  }
  on(event, func) {
    const callback = (listener) => listener.name === event;
    const idx = this.listeners.findIndex(callback);
    if (idx === -1) {
      this.listeners.push({
        name: event,
        callbacks: [func],
      });
    } else {
      this.listeners[idx].callbacks.push(func);
    }
  }
  emit(event, ...args) {
    if (this.listeners.length === 0) return;
    const callback = (listener) => listener.name === event;
    const idx = this.listeners.findIndex(callback);
    if (idx === -1) return;
    const listener = this.listeners[idx];

    if (listener.isOnce) {
      listener.callbacks[0](...args);
      this.listeners.splice(idx, 1);
    } else {
      listener.callbacks.forEach((cb) => {
        cb(...args);
      });
    }
  }
  once(event, func) {
    const callback = (listener) => listener.name === event;
    let idx = this.listeners.findIndex(callback);
    if (idx !== -1) return;
    this.listeners.push({
      name: event,
      callbacks: [func],
      isOnce: true,
    });
  }
  off(event, func) {
    if (this.listeners.length === 0) return;
    const callback = (listener) => listener.name === event;
    let idx = this.listeners.findIndex(callback);
    if (idx === -1) return;
    let callbacks = this.listeners[idx].callbacks;
    for (let i = 0; i < callbacks.length; i++) {
      if (callbacks[i] === func) {
        callbacks.splice(i, 1);
        break;
      }
    }
  }
}

// let event = new EventEmitter();
// let onceCallback = (...args) => {
//   console.log("once_event triggered", ...args);
// };
// let onceCallback1 = (...args) => {
//   console.log("once_event 1 triggered", ...args);
// };
// // once仅监听一次
// event.once("onceEvent", onceCallback);
// event.once("onceEvent", onceCallback1);
// event.emit("onceEvent", "abc", "123");
// event.emit("onceEvent", "abc", "456");

// let onCallback = (...args) => {
//   console.log("on_event triggered", ...args);
// };
// let onCallback1 = (...args) => {
//   console.log("on_event 1 triggered", ...args);
// };
// event.on("onEvent", onCallback);
// event.on("onEvent", onCallback1);
// event.emit("onEvent", "abc", "123");
// // off销毁指定回调
// event.off("onEvent", onCallback);
// event.emit("onEvent", "abc", "123");

宏任务和微任务

  • 哪些属于宏任务?
  • 哪些属于微任务?
  • 事件循环,消息队列与宏任务、微任务之间的关系是什么?
  • 微任务添加和执行流程示意图

哪些属于宏任务?

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI渲染

哪些属于微任务?

  • Promise
  • MutationObserver
  • process.nextTick
  • queueMicrotask

事件循环,消息队列与宏任务、微任务之间的关系是什么?

  • 宏任务入队消息队列,可以将消息队列理解为宏任务队列
  • 每个宏任务内有一个微任务队列,执行过程中微任务入队当前宏任务的微任务队列
  • 宏任务微任务队列为空时才会执行下一个宏任务
  • 事件循环捕获队列出队的宏任务和微任务并执行

事件循环会不断地处理消息队列出队的任务,而宏任务指的就是入队到消息队列中的任务,每个宏任务都有一个微任务队列,宏任务在执行过程中,如果此时产生微任务,那么会将产生的微任务入队到当前的微任务队列中,在当前宏任务的主要任务完成后,会依次出队并执行微任务队列中的任务,直到当前微任务队列为空才会进行下一个宏任务。

微任务添加和执行流程示意图

假设在执行解析HTML这个宏任务的过程中,产生了Promise和MutationObserver这两个微任务。

// parse HTML···
Promise.resolve();
removeChild();

微任务队列会如何表现呢?

image

image

图片引自:极客时间的《浏览器工作原理与实践》

过程可以拆为以下几步:

  1. 主线程执行JS Promise.resolve(); removeChild();
  2. parseHTML宏任务暂停
  3. Promise和MutationObserver微任务入队到parseHTML宏任务的微任务队列
  4. 微任务1 Promise.resolve()执行
  5. 微任务2 removeChild();执行
  6. 微任务队列为空,parseHTML宏任务继续执行
  7. parseHTML宏任务完成,执行下一个宏任务

浏览器页面循环系统原理图

以下所有图均来自极客时间《《浏览器工作原理与实践》- 浏览器中的页面循环系统》,可以帮助理解消息队列,事件循环,宏任务和微任务。

  • 消息队列和事件循环
  • setTimeout
  • XMLHttpRequest
  • 宏任务

消息队列和事件循环

线程的一次执行
image
在线程中引入事件循环
image
渲染进程线程之间发送任务
image

image
线程模型:队列 + 循环
image
跨进程发送消息
image
单个任务执行时间过久
image

setTimeout

长任务导致定时器被延后执行
image
循环嵌套调用 setTimeout
image

XMLHttpRequest

消息循环系统调用栈记录
image
XMLHttpRequest 工作流程图
image
HTTPS 混合内容警告
image
使用 XMLHttpRequest 混合资源失效
image

宏任务

宏任务延时无法保证
image

参考资料

如果文中有不对的地方,欢迎指正和交流~

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

努力成为优秀前端工程师!
查看原文

Banshee 发布了文章 · 6月16日

uni-app组件下拉刷新

标题说的很清楚,是组件下拉刷新,组件里面是无法直接触发onPullDownRefresh和onReachBottom,得通过父子传值的方式

父组件:

引入子组件并通过ref来传值给子组件的触发函数


import likeCategory from './Components/likeCategory.vue'
<like-category ref="likeCategory"  ></like-category>
//**父组件是主页面,支持onPullDownRefresh和onReachBottom**

    onReachBottom() {
        this.$refs.likeCategory.getCategoryList()
     },
    onPullDownRefresh() {
        console.log("触发下拉刷新")
        this.$refs.likeCategory.getCategoryList()
    },

子组件:

子组件触发方法,通过携带指定参数完成分页请求

//**注意触发方法时期,我一般用在created生命周期钩子中**
    created(){
        this.getCategoryList()
    },
    getCategoryList() {
        console.log(this.$props)
        let params = {
              shopId: this.$props.shopId1 ? this.$props.shopId1:uni.getStorageSync('ShopInfo').id,
                    access_token: uni.getStorageSync('access_token'),
                    pageSize:this.pageSize,
                    pageNumber:this.pageNumber,
                }
                console.log(params.pageNumber)
                if (this.total > 0 && this.total <= this.goodsInfoList.length) {
                    uni.showToast({
                        title: '没有更多!',
                        icon: 'none'
                    });
                    return;
                }
                uni.showLoading({
                    title: '加载中'
                });
                if (params.shopId) {
                    this.$http.get(this.$api.gussYouLove, {
                        data: params
                    }).then(res => {
                        if (res.data.code == 200) {
                            if(res.data.data.list.length > 0){
                                if(!!this.goodsInfoList&&this.goodsInfoList.length>0){
                                    res.data.data.list.forEach(info =>{
                                        this.goodsInfoList.push(info)
                                    })                                                 
                                }else{
                                    this.goodsInfoList = res.data.data.list
                                }                    
                                this.total = res.data.data.total;
                                this.pageNumber++
                                this.isShow = false 
                                console.log(this.pageNumber)
                            }else{
                                this.goodsInfoList = []
                                 this.isShow = true
                            }      
                            
                        } else {
                            uni.showToast({
                                title: res.data.msg,
                                duration: 2000,
                                icon: 'none'
                            });
                        }
                        uni.stopPullDownRefresh();
                        uni.hideLoading();
                        
                    })
                }

    },
查看原文

赞 0 收藏 0 评论 4

Banshee 赞了文章 · 6月16日

解决 React 中的 input 输入框在中文输入法下的 bug

以下会涉及到的技术点:react mobx compositionstart compositionupdate compositionend

问题描述

在使用 input 时,通常会对输入的内容做校验,校验的方式无非两种:

  1. 允许用户输入,并且做错误提示;
  2. 不允许用户输入正则或者函数匹配到的字符。

现有如下需求:“仅允许输入英文、数字和汉字,不允许输入其他特殊字符和符号”。显然这种场景需要使用第二种校验方式。

然后我自以为很机智的写了下面的代码(引入了组件库 cloud-react),在输入值变化的时候(onChange 事件),处理绑定到 input 上的 value,将除了英文、数字、和汉字之外的字符都替换成空字符串。

export default class CompositionDemo extends Component {
  constructor() {
     this.state = {
       value: ''
     };
  }
  
  onChange(evt) {
     this.setState({
       value: evt.target.value.replace(/[^a-zA-Z0-9\u4E00-\u9FA5]/g, '')
     });
  };
  
  render() {
    return <Input
        onChange={this.onChange.bind(this)}
          value={this.state.value}
       />
  }
}

平平常常,普普通通,一切看起来都是正常的操作,结果,当我输入拼音的时候,神奇的事情发生了:连拼的时候除了最后一个字,前面的都变成了字符。

what??? 小问号,你是否有很多朋友?

于是,我踏上了一条不归路,呸呸呸,是打开了新世界的大门,就是这个门对于我来说可能有点沉,推了两天才看到新世界。

纠其原因:拼音输入是一个过程,确切的说,在这个过程中,你输入的每一个字母都触发了 onChange 事件,而你输入过程中的这个产物在校验中被吃掉了,留下了一坨空字符串,所以就发生了上面那个神奇的现象。

解决方案

这里需要用到两个属性:

compositionstart

compositionend

简单点来说,就是当你开始使用输入法进行新的输入的时候,会触发 compositionstart ,中间过程其实也有一个函数 compositionupdate,顾名思义,输入更新时会触发它;当结束输入法输入的时候,会触发 compositionend。

下面进入正题:

首先,我们先看一下 Input 组件的一个很正常的实现:

import React, { Component } from 'react';
import PropTypes from 'prop-types';

export default class InputDemo1 extends Component {

    constructor(props) {
        super(props);
        this.state = {
            value: '',
        };
    }
    static getDerivedStateFromProps({ value }, { value: preValue }) {
        if (value !== preValue) {
            return { value };
        }
        return null;
    }

    onChange = evt => {
        this.props.onChange(evt);
    };

    render() {
        return <input
            value={this.state.value}
            type="text"
            onChange={this.onChange}
        />
    }
}

Input 组件有两种应用场景:

  1. 不受控的输入框:业务方不给组件传入 value,无法控制输入框的值;
  2. 受控的输入框:业务方可以通过给组件传入 value,从而可以在外部控制输入框的值。

不受控的输入框在我使用过程中并没有什么 bug,此处不做赘述,此处只谈受控的输入框,也就是我们需求(仅允许输入英文、数字和汉字,不允许输入其他特殊字符和符号)中需要使用的场景。

前面提到的 compositionstart 和 compositionend 该出场了:利用这两个属性的特点,在输入拼音的“过程中”不让 input 触发 onChange 事件,自然就不会触发校验,好了,既然有了思路,开始码代码。

我们定义一个变量 isOnComposition 来判断是否在“过程中”

isOnComposition = false;

handleComposition = evt => {
  if (evt.type === 'compositionend') {
    this.isOnComposition = false;
    return;
  }

  this.isOnComposition = true;
};

 onChange = evt => {
   if (!this.isOnComposition) {
     this.props.onChange(evt);
   }
 };

render() {
  const commonProps = {
    onChange: this.onChange,
    onCompositionStart: this.handleComposition,
    onCompositionUpdate: this.handleComposition,
    onCompositionEnd: this.handleComposition,
  };
  return <input
    value={this.state.value}
    type="text"
      {...commonProps}
  />
}

你以为就这么轻松解决了么?

呵,你想多了!

我仍然使用开篇那个 demo 来测试这个代码,发现事情又神奇了一点呢,这次拼音压根就输不进去了哇~

我查看了下在输入拼音时函数的调用:
是的,宁没有看错,只触发了onCompositionstart 和 onCompositionupdate这两个函数,我起初以为是逻辑被我写扣圈了,想了想原因(其实我想了好久,人略笨,见笑):

罪魁祸首就是绑定在 input 上的那个 value,输入拼音的过程中,state.value 一直没变,input 中自然不会有任何输入值,没有输入值,也就完成不了输入过程,触发不了 compositionend,一直处于“过程中”。

所以这次不是程序逻辑扣圈,是中断了。

于是我又想如何把中断的程序接起来(对的,垮掉了我们就捡起来,哈),完成这个链条。

我想了好多办法,也在网上看了好多办法,可惜都解决不了我的困境。

各种心酸不堪回首,幸好最后找到了一个办法:其实想想原来代码中用 state.value 去控制 input 值的变化,还是没有把 input 中何时输入值的控制权放在自己手里,“过程中”这个概念也就失去了意义。只要 state.value 还和 input 绑在一起,就是我自己玩我自己的,人家玩人家的。于是,就有了下面让控制权回到我手中的代码。

import React, { Component, createRef } from 'react';
import PropTypes from 'prop-types';

export default class InputDemo extends Component {

    inputRef = createRef();

    isOnComposition = false;

    componentDidMount() {
        this.setInputValue();
    }

    componentDidUpdate() {
        this.setInputValue();
    }

    setInputValue = () => {
        this.inputRef.current.value = this.props.value || ''
    };

    handleComposition = evt => {
        if (evt.type === 'compositionend') {
            this.isOnComposition = false;
            return;
        }

        this.isOnComposition = true;
    };

    onChange = evt => {
        if (!this.isOnComposition) {
            this.props.onChange(evt);
        }
    };

    render() {
        const commonProps = {
            onChange: this.onChange,
            onCompositionStart: this.handleComposition,
            onCompositionUpdate: this.handleComposition,
            onCompositionEnd: this.handleComposition,
        };
        return <input
            ref={this.inputRef}
            type="text"
            {...commonProps}
        />
    }
}

测了一下,大致上是没问题了。

还要看一下谷歌浏览器和火狐浏览器,果然还有坑:

  1. 火狐浏览器中的执行顺序:compositionstart compositionend onChange
  2. 谷歌浏览器中的执行顺序:compositionstart onChange compositionend

最后再做一下兼容处理,修改一下 handleComposition 函数

handleComposition = evt => {
   if (evt.type === 'compositionend') {
     this.isOnComposition = false;

     // 谷歌浏览器:compositionstart onChange compositionend
     // 火狐浏览器:compositionstart compositionend onChange
     if (navigator.userAgent.indexOf('Chrome') > -1) {
       this.onChange(evt);
     }

     return;
   }

   this.isOnComposition = true;
 };

因为不管中间执行了那些函数,最后都是需要执行 onChange 事件的,因此加了判断,对谷歌浏览器做了特殊处理(其它浏览器暂时没做考虑和处理)。

完整代码 https://github.com/liyuan-meng/my-react-app/tree/master/src/inputAndComposition

后记

到此,正文结束了,我还要说两个需要注意的地方,其实也是踩了的坑:

  1. 如果 Input 组件的实现使用了 React.PureComponent ,在以上需求中会出现的问题:输入特殊字符时,外部通过正则将其 replace 掉了,传入 Input 组件内部的 value 实际上没有任何变化,也不会触发组件 render。这是因为 PureComponent 对 shouldComponentUpdate 函数做了优化,如果发现 props 和 state 上的属性都没有变化,不会重新渲染组件,因此我暂时的处理是:使用 React.Component ,组件实现中对 shouldComponentUpdate 封装。
  2. 在外部使用 mobx 的时候,如果使用 observable 监听 value,会出现和上面蕾丝的情况—输入特殊字符时,通过正则将其 replace 掉了,mobx 也发现 value 没有任何变化,就不会触发 render,我暂时的处理是使用 state,虽然我觉得这不是最好的办法,但我目前还想不到其它的处理方式。
查看原文

赞 7 收藏 3 评论 0

认证与成就

  • 获得 13 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-02
个人主页被 1.5k 人浏览