6 React

JSX本质

  • React.createElementh函数,返回vnode
  • 第一个参数,可能是组件,也可能是html tag
  • 组件名,首字母必须是大写(React规定)
    // React.createElement写法
    React.createElement('tag', null, [child1,child2])
    React.createElement('tag', props, child1,child2,child3)
    React.createElement(Comp, props, child1,child2,'文本节点')
    // jsx基本用法
    <div className="container">
      <p>tet</p>
      <img src={imgSrc} />
    </div>
    
    // 编译后 https://babeljs.io/repl
    React.createElement(
      "div",
      {
        className: "container"
      },
      React.createElement("p", null, "tet"),
      React.createElement("img", {
        src: imgSrc
      })
    );
    // jsx style
    const styleData = {fontSize:'20px',color:'#f00'}
    const styleElem = <p style={styleData}>设置style</p>
    
    // 编译后
    const styleData = {
      fontSize: "20px",
      color: "#f00"
    };
    const styleElem = React.createElement(
      "p",
      {
        style: styleData
      },
      "\u8BBE\u7F6Estyle"
    );
    // jsx加载组件
    const app = <div>
        <Input submitTitle={onSubmitTitle} />
        <List list={list} />
    </div>
    
    // 编译后
    const app = React.createElement(
      "div",
      null,
      React.createElement(Input, {
        submitTitle: onSubmitTitle
      }),
      React.createElement(List, {
        list: list
      })
    );
    // jsx事件
    const eventList = <p onClick={this.clickHandler}>text</p>
    
    // 编译后
    const eventList = React.createElement(
      "p",
      {
        onClick: (void 0).clickHandler
      },
      "text"
    );
    // jsx列表
    const listElem = <ul>
    {
      this.state.list.map((item,index)=>{
        return <li key={index}>index:{index},title:{item.title}</li>
      })
     }
    </ul>
    
    // 编译后
    
    const listElem = React.createElement(
      "ul",
      null,
      (void 0).state.list.map((item, index) => {
        return React.createElement(
          "li",
          {
            key: index
          },
          "index:",
          index,
          ",title:",
          item.title
        );
      })
    );

React合成事件机制

  • React16事件绑定到document
  • React17事件绑定到root组件上,有利于多个react版本共存,例如微前端
  • event不是原生的,是SyntheticEvent合成事件对象
  • Vue不同,和DOM事件也不同

合成事件图示

为何需要合成事件

  • 更好的兼容性和跨平台,如react native
  • 挂载到documentroot上,减少内存消耗,避免频繁解绑
  • 方便事件的统一管理(如事务机制)
    // 获取 event
    clickHandler3 = (event) => {
        event.preventDefault() // 阻止默认行为
        event.stopPropagation() // 阻止冒泡
        console.log('target', event.target) // 指向当前元素,即当前元素触发
        console.log('current target', event.currentTarget) // 指向当前元素,假象!!!
    
        // 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent 组合事件
        console.log('event', event) // 不是原生的 Event ,原生的 MouseEvent
        console.log('event.__proto__.constructor', event.__proto__.constructor)
    
        // 原生 event 如下。其 __proto__.constructor 是 MouseEvent
        console.log('nativeEvent', event.nativeEvent)
        console.log('nativeEvent target', event.nativeEvent.target)  // 指向当前元素,即当前元素触发
        console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!
    
        // 1. event 是 SyntheticEvent ,模拟出来 DOM 事件所有能力
        // 2. event.nativeEvent 是原生事件对象
        // 3. 所有的事件,都被挂载到 document 上
        // 4. 和 DOM 事件不一样,和 Vue 事件也不一样
    }

setState和batchUpdate机制

  • setState在react事件、生命周期中是异步的(在react上下文中是异步);在setTimeout、自定义DOM事件中是同步的
  • 有时合并(对象形式setState({}) => 通过Object.assign形式合并对象),有时不合并(函数形式setState((prevState,nextState)=>{})

核心要点

1.setState主流程

  • setState是否是异步还是同步,看是否能命中batchUpdate机制,判断isBatchingUpdates
  • 哪些能命中batchUpdate机制
    • 生命周期
    • react中注册的事件和它调用的函数
    • 总之在react的上下文中
  • 哪些不能命中batchUpdate机制
    • setTimeoutsetInterval
    • 自定义DOM事件
    • 总之不在react的上下文中,react管不到的

  1. batchUpdate机制

    // setState batchUpdate原理模拟
    let isBatchingUpdate = true;
    
    let queue = [];
    let state = {number:0};
    function setState(newSate){
      //state={...state,...newSate}
      // setState异步更新
      if(isBatchingUpdate){
        queue.push(newSate);
      }else{
        // setState同步更新
        state={...state,...newSate}
      }   
    }
    
    // react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
    // 模拟react中事件点击
    function handleClick(){
      isBatchingUpdate=true; // 批量更新标志
    
      /**我们自己逻辑开始 */
      setState({number:state.number+1});
      setState({number:state.number+1});
      console.log(state); // 0
      setState({number:state.number+1});
      console.log(state); // 0
      /**我们自己逻辑结束 */
    
      state= queue.reduce((newState,action)=>{
        return {...newState,...action}
      },state); 
    
      isBatchingUpdate=false; // 执行结束设置false
    }
    handleClick();
    console.log(state); // 1
  1. transaction事务机制

    // setState现象演示
    
    import React from 'react'
    
    // 函数组件(后面会讲),默认没有 state
    class StateDemo extends React.Component {
        constructor(props) {
            super(props)
    
            // 第一,state 要在构造函数中定义
            this.state = {
                count: 0
            }
        }
        render() {
            return <div>
                <p>{this.state.count}</p>
                <button onClick={this.increase}>累加</button>
            </div>
        }
        increase = () => {
            // // 第二,不要直接修改 state ,使用不可变值 ----------------------------
            // // this.state.count++ // 错误
            // this.setState({
            //     count: this.state.count + 1 // SCU
            // })
            // 操作数组、对象的的常用形式
    
            // 第三,setState 可能是异步更新(有可能是同步更新) ----------------------------
            
            // this.setState({
            //     count: this.state.count + 1
            // }, () => {
            //     // 联想 Vue $nextTick - DOM
            //     console.log('count by callback', this.state.count) // 回调函数中可以拿到最新的 state
            // })
            // console.log('count', this.state.count) // 异步的,拿不到最新值
    
            // // setTimeout 中 setState 是同步的
            // setTimeout(() => {
            //     this.setState({
            //         count: this.state.count + 1
            //     })
            //     console.log('count in setTimeout', this.state.count)
            // }, 0)
    
            // 自己定义的 DOM 事件,setState 是同步的。再 componentDidMount 中
    
            // 第四,state 异步更新的话,更新前会被合并 ----------------------------
            
            // 传入对象,会被合并(类似 Object.assign )。执行结果只一次 +1
            // this.setState({
            //     count: this.state.count + 1
            // })
            // this.setState({
            //     count: this.state.count + 1
            // })
            // this.setState({
            //     count: this.state.count + 1
            // })
            
            // 传入函数,不会被合并。执行结果是 +3
            this.setState((prevState, props) => {
                return {
                    count: prevState.count + 1
                }
            })
            this.setState((prevState, props) => {
                return {
                    count: prevState.count + 1
                }
            })
            this.setState((prevState, props) => {
                return {
                    count: prevState.count + 1
                }
            })
        }
        // bodyClickHandler = () => {
        //     this.setState({
        //         count: this.state.count + 1
        //     })
        //     console.log('count in body event', this.state.count)
        // }
        // componentDidMount() {
        //     // 自己定义的 DOM 事件,setState 是同步的
        //     document.body.addEventListener('click', this.bodyClickHandler)
        // }
        // componentWillUnmount() {
        //     // 及时销毁自定义 DOM 事件
        //     document.body.removeEventListener('click', this.bodyClickHandler)
        //     // clearTimeout
        // }
    }
    
    export default StateDemo
    
    // -------------------------- 我是分割线 -----------------------------
    
    // 不可变值(函数式编程,纯函数) - 数组
    // const list5Copy = this.state.list5.slice()
    // list5Copy.splice(2, 0, 'a') // 中间插入/删除
    // this.setState({
    //     list1: this.state.list1.concat(100), // 追加
    //     list2: [...this.state.list2, 100], // 追加
    //     list3: this.state.list3.slice(0, 3), // 截取
    //     list4: this.state.list4.filter(item => item > 100), // 筛选
    //     list5: list5Copy // 其他操作
    // })
    // // 注意,不能直接对 this.state.list 进行 push pop splice 等,这样违反不可变值
    
    // 不可变值 - 对象
    // this.setState({
    //     obj1: Object.assign({}, this.state.obj1, {a: 100}),
    //     obj2: {...this.state.obj2, a: 100}
    // })
    // // 注意,不能直接对 this.state.obj 进行属性设置,这样违反不可变值
    // setState笔试题考察 下面这道题输出什么
    class Example extends React.Component {
      constructor() {
      super()
      this.state = {
        val: 0
      }
    }
    // componentDidMount中isBatchingUpdate=true setState批量更新
    componentDidMount() {
      // setState传入对象会合并,后面覆盖前面的Object.assign({})
      this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理 
      console.log(this.state.val)
      // 第 1 次 log
      this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
      console.log(this.state.val)
      // 第 2 次 log
      setTimeout(() => {
        // 到这里this.state.val结果等于1了
        // 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
        this.setState({ val: this.state.val + 1 }) // 同步更新
        console.log(this.state.val)
        // 第 3 次 log
        this.setState({ val: this.state.val + 1 }) // 同步更新
        console.log(this.state.val)
        // 第 4 次 log
        }, 0)
      }
      render() {
        return null
      }
    }
    
    // 答案:0, 0, 2, 3

补充(现代做法):上面「setTimeout/原生事件中 setState 同步、不批处理」的结论只适用于 React 17 及更早版本。React 18 引入了自动批处理(Automatic Batching):无论 setState 写在事件回调、setTimeoutPromise.then 还是原生 DOM 事件里,只要使用 createRoot 挂载,多次更新都会被合并成一次重渲染。因此在 React 18 下,上面那道经典题的 setTimeout 分支会变成批处理,输出由 0,0,2,3 变为 0,0,1,1。如确需在某处立即同步刷新(极少用),可用 flushSync(() => setState(...)) 退出批处理。另外函数组件用 useState,更新函数式写法 setCount(c => c + 1) 才能拿到累积后的最新值。

根据jsx写出vnode和render函数

    <!-- jsx -->
    <div className="container">
      <p onClick={onClick} data-name="p1">
        hello <b>{name}</b>
      </p>
      <img src={imgSrc} />
      <MyComponent title={title}></MyComponent>
    </div>

注意

  • 注意JSX中的常量和变量
  • 注意JSX中的HTML tag和自定义组件
    const vnode = {
      tag: 'div',
      props: {
        className: 'container'
      },
      children: [
        // <p>
        {
          tag: 'p',
          props: {
            dataset: {
              name: 'p1'
            },
            on: {
              click: onClick // 变量
            }
          },
          children: [
            'hello',
            {
              tag: 'b',
              props: {},
              children: [name] // name变量
            }
          ]
        },
        // <img />
        {
          tag: 'img',
          props: {
            src: imgSrc // 变量
          },
          children: [/**无子节点**/]
        },
        // <MyComponent>
        {
          tag: MyComponent, // 变量
          props: {
            title: title, // 变量
          },
          children: [/**无子节点**/]
        }
      ]
    }
    // render函数
    function render() {
      // h(tag, props, children)
      return h('div', {
        props: {
          className: 'container'
        }
      }, [
    
        // p
        h('p', {
          dataset: {
            name: 'p1'
          },
          on: {
            click: onClick
          }
        }, [
          'hello',
          h('b', {}, [name])
        ])
    
        // img
        h('img', {
          props: {
            src: imgSrc
          }
        }, [/**无子节点**/])
    
        // MyComponent
        h(MyComponent, {
          title: title
        }, [/**无子节点**/])
      ]
      )
    }

在react中jsx编译后

    // 使用https://babeljs.io/repl编译后效果
    
    React.createElement(
      "div",
      {
        className: "container"
      },
      React.createElement(
        "p",
        {
          onClick: onClick,
          "data-name": "p1"
        },
        "hello ",
        React.createElement("b", null, name)
      ),
      React.createElement("img", {
        src: imgSrc
      }),
      React.createElement(MyComponent, {
        title: title
      })
    );

虚拟DOM(vdom)真的很快吗

  • virutal DOM,虚拟DOM
  • 用JS对象模拟DOM节点数据
  • vdom并不快,JS直接操作DOM才是最快的
    • vue为例,data变化 => vnode diff => 更新DOM 肯定是比不过直接操作DOM节点快的
  • 但是"数据驱动视图"要有合适的技术方案,不能全部DOM重建
  • dom就是目前最合适的技术方案(并不是因为它快,而是合适)
  • 在大型系统中,全部更新DOM的成本太高,使用vdom把更新范围减少到最小

并不是所有的框架都在用vdomsvelte就不用vdom

react组件渲染过程

  • JSX如何渲染为页面
  • setState之后如何更新页面
  • 面试考察全流程

1.组件渲染过程

  • 分析
    • propsstate 变化
    • render()生成vnode
    • patch(elem, vnode) 渲染到页面上(react并一定用patch
  • 渲染过程
    • setState(newState) => newState存入pending队列,判断是否处于batchUpdate状态,保存组件于dirtyComponents中(可能有子组件)
    • 遍历所有的dirtyComponents调用updateComponent生成newVnode
    • patch(vnode,newVnode)

2.组件更新过程

  • patch更新被分为两个阶段
    • reconciliation阶段 :执行diff算法,纯JS计算
    • commit阶段 :将diff结果渲染到DOM
  • 如果不拆分,可能有性能问题
    • JS是单线程的,且和DOM渲染共用一个线程
    • 当组件足够复杂,组件更新时计算和渲染都压力大
    • 同时再有DOM操作需求(动画、鼠标拖拽等)将卡顿
  • 解决方案Fiber
    • reconciliation阶段拆分为多个子任务
    • DOM需要渲染时更新,空闲时恢复在执行计算
    • 通过window.requestIdleCallback来判断浏览器是否空闲

React setState经典面试题

    // setState笔试题考察 下面这道题输出什么
    class Example extends React.Component {
      constructor() {
      super()
      this.state = {
        val: 0
      }
    }
    componentDidMount() {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
      // 第 1 次 log
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
      // 第 2 次 log
      setTimeout(() => {
        this.setState({ val: this.state.val + 1 }) 
        console.log(this.state.val)
        // 第 3 次 log
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val)
        // 第 4 次 log
        }, 0)
      }
      render() {
        return null
      }
    }
    // 答案
    0
    0
    2
    3
  • 关于setState的两个考点
    • 同步或异步
    • state合并或不合并
      • setState传入函数不会合并覆盖
      • setState传入对象会合并覆盖Object.assigin({})
  • 分析
    • 默认情况
      • state默认异步更新
      • state默认合并后更新(后面的覆盖前面的,多次重复执行不会累加)
    • setState在合成事件和生命周期钩子中,是异步更新的
    • react同步更新,不在react上下文中触发
      • 原生事件setTimeoutsetIntervalpromise.thenAjax回调中,setState是同步的,可以马上获取更新后的值
        • 原生事件如document.getElementById('test').addEventListener('click',()=>{this.setState({count:this.state.count + 1}})
      • 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步
    • 注意:在react18中不一样
      • 上述场景,在react18中可以异步更新(Auto Batch
      • 需将ReactDOM.render替换为ReactDOM.createRoot

如需要实时获取结果,在回调函数中获取 setState({count:this.state.count + 1},()=>console.log(this.state.count)})

    // setState原理模拟
    let isBatchingUpdate = true;
    
    let queue = [];
    let state = {number:0};
    function setState(newSate){
      //state={...state,...newSate}
      // setState异步更新
      if(isBatchingUpdate){
        queue.push(newSate);
      }else{
        // setState同步更新
        state={...state,...newSate}
      }   
    }
    
    // react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
    // 模拟react中事件点击
    function handleClick(){
      isBatchingUpdate=true; // 批量更新标志
    
      /**我们自己逻辑开始 */
      setState({number:state.number+1});
      setState({number:state.number+1});
      console.log(state); // 0
      setState({number:state.number+1});
      console.log(state); // 0
      /**我们自己逻辑结束 */
    
      state= queue.reduce((newState,action)=>{
        return {...newState,...action}
      },state); 
    }
    handleClick();
    console.log(state); // 1
    // setState笔试题考察 下面这道题输出什么
    class Example extends React.Component {
      constructor() {
      super()
      this.state = {
        val: 0
      }
    }
    // componentDidMount中isBatchingUpdate=true setState批量更新
    componentDidMount() {
      // setState传入对象会合并,后面覆盖前面的Object.assign({})
      this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理 
      console.log(this.state.val)
      // 第 1 次 log
      this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
      console.log(this.state.val)
      // 第 2 次 log
      setTimeout(() => {
        // 到这里this.state.val结果等于1了
        // 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
        this.setState({ val: this.state.val + 1 }) // 同步更新
        console.log(this.state.val)
        // 第 3 次 log
        this.setState({ val: this.state.val + 1 }) // 同步更新
        console.log(this.state.val)
        // 第 4 次 log
        }, 0)
      }
      render() {
        return null
      }
    }
    
    // 答案:0, 0, 2, 3

React 18之前,setStateReact的合成事件中是合并更新的,在setTimeout的原生事件中是同步按序更新的。例如

    handleClick = () => {
      this.setState({ age: this.state.age + 1 });
      console.log(this.state.age); // 0
      this.setState({ age: this.state.age + 1 });
      console.log(this.state.age); // 0
      this.setState({ age: this.state.age + 1 });
      console.log(this.state.age); // 0
      setTimeout(() => {
        this.setState({ age: this.state.age + 1 });
        console.log(this.state.age); // 2
        this.setState({ age: this.state.age + 1 });
        console.log(this.state.age); // 3
      });
    };

而在React 18中,不论是在合成事件中,还是在宏任务中,都是会合并更新

    function handleClick() {
      setState({ age: state.age + 1 }, onePriority);
      console.log(state.age);// 0
      setState({ age: state.age + 1 }, onePriority);
      console.log(state.age); // 0
      setTimeout(() => {
        setState({ age: state.age + 1 }, towPriority);
        console.log(state.age); // 1
        setState({ age: state.age + 1 }, towPriority);
        console.log(state.age); // 1
      });
    }
    // 拓展:setState传入函数不会合并
    class Example extends React.Component {
      constructor() {
      super()
      this.state = {
        val: 0
      }
    }
    componentDidMount() {
      this.setState((prevState,props)=>{
        return {val: prevState.val + 1}
      })
      console.log(this.state.val) // 0
      // 第 1 次 log
      this.setState((prevState,props)=>{ // 传入函数,不会合并覆盖前面的
        return {val: prevState.val + 1}
      })
      console.log(this.state.val) // 0
      // 第 2 次 log
      setTimeout(() => {
        // setTimeout中setState同步执行
        // 到这里this.state.val结果等于2了
        this.setState({ val: this.state.val + 1 }) 
        console.log(this.state.val) // 3
        // 第 3 次 log
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val) // 4
        // 第 4 次 log
        }, 0)
      }
      render() {
        return null
      }
    }
    // 答案:0 0 3 4 
    // react hooks中打印
    
    function useStateDemo() {
      const [value, setValue] = useState(100)
    
      function clickHandler() {
        // 1.传入常量,state会合并
        setValue(value + 1)
        setValue(value + 1)
        console.log(1, value) // 100
        // 2.传入函数,state不会合并
        setValue(value=>value + 1)
        setValue(value=>value + 1)
        console.log(2, value) // 100
    
        // 3.setTimeout中,React18也开始合并state(之前版本会同步更新、不合并)
        setTimeout(()=>{
          setValue(value + 1)
          setValue(value + 1)
          console.log(3, value) // 100
          setValue(value + 1)
        })
    
        // 4.同理 setTimeout中,传入函数不合并
        setTimeout(()=>{
          setValue(value => value + 1)
          setValue(value => value + 1)
          console.log(4, value) // 100
        })
      }
      return (
        <button onClick={clickHandler}>点击 {value}</button>
      )
    }

连环问:setState是宏任务还是微任务

  • setState本质是同步的
    • setState是同步的,不过让react做成异步更新的样子而已
      • 如果setState是微任务,就不应该在promise.then微任务之前打印出来(promise then微任务先注册)
    • 因为要考虑性能,多次state修改,只进行一次DOM渲染
    • 日常所说的“异步”是不严谨的,但沟通成本低
  • 总结
    • setState是同步执行,state都是同步更新(只是我们日常把setState当异步来处理)
    • 在微任务promise.then之前,state已经计算完了
    • 同步,不是微任务或宏任务
    import React from 'react'
    
    class Example extends React.Component {
      constructor() {
        super()
        this.state = {
          val: 0
        }
      }
    
      clickHandler = () => {
        // react事件中 setState异步执行
        console.log('--- start ---')
    
        Promise.resolve().then(() => console.log('promise then') /* callback */)
    
        // “异步”
        this.setState(
          { val: this.state.val + 1 },
          () => { console.log('state callback...', this.state) } // callback
        )
    
        console.log('--- end ---')
    
        // 结果: 
        // start 
        // end
        // state callback {val:1} 
        // promise then 
    
        // 疑问?
        // promise then微任务先注册的,按理应该先打印promise then再到state callback
        // 因为:setState本质是同步的,不过让react做成异步更新的样子而已
        // 因为要考虑性能,多次state修改,只进行一次DOM渲染
      }
    
      componentDidMount() {
        setTimeout(() => {
          // setTimeout中setState是同步更新
          console.log('--- start ---')
    
          Promise.resolve().then(() => console.log('promise then'))
    
          this.setState(
            { val: this.state.val + 1 }
          )
          console.log('state...', this.state)
      
          console.log('--- end ---')
        })
    
        // 结果: 
        // start 
        // state {val:1} 
        // end
        // promise then 
      }
    
      render() {
        return <p id="p1" onClick={this.clickHandler}>
          setState demo: {this.state.val}
        </p>
      }
    }

React useEffect闭包陷阱问题

问:按钮点击三次后,定时器输出什么?

    function useEffectDemo() {
      const [value,setValue] = useState(0)
    
      useEffect(()=>{
        setInterval(()=>{
          console.log(value)
        },1000)
      }, [])
    
      const clickHandler = () => {
        setValue(value + 1)
      }
    
      return (
        <div>
          value: {value} <button onClick={clickHandler}>点击</button>
        </div>
      )
    }

答案一直是0useEffect闭包陷阱问题,useEffect依赖是空的,只会执行一次。setInterval中的value就只会获取它之前的变量。而react有个特点,每次value变化都会重新执行useEffectDemo这个函数。点击了三次函数会执行三次,三次过程中每个函数中value都不一样,setInterval获取的永远是第一个函数里面的0

    // 追问:怎么才能打印出3?
    
    function useEffectDemo() {
      const [value,setValue] = useState(0)
    
      useEffect(()=>{
        const timer = setInterval(()=>{
          console.log(value) // 3
        },1000)
        return ()=>{
          clearInterval(timer) // value变化会导致useEffectDemo函数多次执行,多次执行需要清除上一次的定时器,否则多次注册定时器
        }
      }, [value]) // 这里增加依赖项,每次依赖变化都会重新执行
    
      const clickHandler = () => {
        setValue(value + 1)
      }
    
      return (
        <div>
          value: {value} <button onClick={clickHandler}>点击</button>
        </div>
      )
    }

Vue React diff 算法有什么区别

diff 算法

  • Vue React diff 不是对比文字,而是 vdom 树,即 tree diff
  • 传统的 tree diff 算法复杂度是 O(n^3) ,算法不可用。

优化

Vue React 都是用于网页开发,基于 DOM 结构,对 diff 算法都进行了优化(或者简化)

  • 只在同一层级比较,不跨层级(DOM 结构的变化,很少有跨层级移动)
  • tag 不同则直接删掉重建,不去对比内部细节(DOM 结构变化,很少有只改外层,不改内层)
  • 同一个节点下的子节点,通过 key 区分

最终把时间复杂度降低到 O(n) ,生产环境下可用。这一点 Vue React 都是相同的。

React diff 特点 - 仅向右移动

比较子节点时,仅向右移动,不向左移动。

Vue2 diff 特点 - 双端比较

定义四个指针,分别比较

  • oldStartNodenewStartNode 头头
  • oldStartNodenewEndNode 头尾
  • oldEndNodenewStartNode 尾头
  • oldEndNodenewEndNode 尾尾

然后指针继续向中间移动,直到指针汇合

Vue3 diff 特点 - 最长递增子序列

例如数组 [3,5,7,1,2,8] 的最长递增子序列就是 [3,5,7,8 ] 。这是一个专门的算法。

算法步骤

  • 通过“前-前”比较找到开始的不变节点 [A, B]
  • 通过“后-后”比较找到末尾的不变节点 [G]
  • 剩余的有变化的节点 [F, C, D, E, H]
    • 通过 newIndexToOldIndexMap 拿到 oldChildren 中对应的 index [5, 2, 3, 4, -1]-1 表示之前没有,要新增)
    • 计算最长递增子序列 得到 [2, 3, 4] ,对应的就是 [C, D, E] ,即这些节点可以不变
    • 剩余的节点,根据 index 进行新增、删除

该方法旨在尽量减少 DOM 的移动,达到最少的DOM操作

总结

  • React diff 特点 - 仅向右移动
  • Vue2 diff 特点 - updateChildren双端比较
  • Vue3 diff 特点 - updateChildren增加了最长递增子序列,更快
    • Vue3增加了patchFlag、静态提升、函数缓存等

连环问:diff 算法中 key 为何如此重要

无论在 Vue 还是 React 中,key 的作用都非常大。以 React 为例,是否使用 key 对内部 DOM 变化影响非常大。

    <ul>
      <li v-for="(index, num) in nums" :key="index">
        {{num}}
      </li>
    </ul>
    const todoItems = todos.map((todo) =>
      <li key={todo.id}>
        {todo.text}
      </li>
    )

如何统一监听React组件报错

  • ErrorBoundary组件
    • react16版本之后,增加了ErrorBoundary组件
    • 监听所有下级组件报错,可降级展示UI
    • 只监听组件渲染时报错,不监听DOM事件错误、异步错误
      • ErrorBoundary没有办法监听到点击按钮时候的在click的时候报错
      • 只能监听组件从一开始渲染到渲染成功这段时间报错,渲染成功后在怎么操作产生的错误就不管了
      • 可用try catch或者window.onerror(二选一)
    • 只在production环境生效(需要打包之后查看效果),dev会直接抛出错误
  • 总结
    • ErrorBoundary监听组件渲染报错
    • 事件报错使用try catchwindow.onerror
    • 异步报错使用window.onerror
    // ErrorBoundary.js
    
    import React from 'react'
    
    class ErrorBoundary extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          error: null // 存储当前的报错信息
        }
      }
      static getDerivedStateFromError(error) {
        // 更新 state 使下一次渲染能够显示降级后的 UI
        console.info('getDerivedStateFromError...', error)
        return { error } // return的信息会等于this.state的信息
      }
      componentDidCatch(error, errorInfo) {
        // 统计上报错误信息
        console.info('componentDidCatch...', error, errorInfo)
      }
      render() {
        if (this.state.error) {
          // 提示错误
          return <h1>报错了</h1>
        }
    
        // 没有错误,就渲染子组件
        return this.props.children
      }
    }
    // index.js 中使用
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import ErrorBoundary from './ErrorBoundary'
    
    ReactDOM.render(
      <React.StrictMode>
        <ErrorBoundary>
          <App />
        </ErrorBoundary>
      </React.StrictMode>,
      document.getElementById('root')
    );

在实际工作中,你对React做过哪些优化

  • 修改CSS模拟v-show
        // 原始写法
    {!flag && <MyComonent style={{display:'none'}} />}
    {flag && <MyComonent />}
    
    // 模拟v-show
    {<MyComonent style={{display:flag ? 'block' : 'none'}} />}
  • 循环使用key
    • key不要用index
  • 使用Flagment或 <></>空标签包裹减少多个层级组件的嵌套
  • jsx中不要定义函数JSX会被频繁执行的
        // bad 
    // react中的jsx被频繁执行(state更改)应该避免函数被多次新建
    <button onClick={()=>{}}>点击</button>
    // goods
    function useButton() {
      const handleClick = ()=>{}
      return <button onClick={handleClick}>点击</button>
    }
  • 使用shouldComponentUpdate
    • 判断组件是否需要更新
    • 或者使用React.PureComponent比较props第一层属性
    • 函数组件使用React.memo(comp, fn)包裹 function fn(prevProps,nextProps) {// 自己实现对比,像shouldComponentUpdate}
  • Hooks缓存数据和函数
    • useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果
    • useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染
  • 使用异步组件
        import React,{lazy,Suspense} from 'react'
    const OtherComp = lazy(/**webpackChunkName:'OtherComp'**/ ()=>import('./otherComp'))
    
    function MyComp(){
      return (
        <Suspense fallback={<div>loading...</div>}>
          <OtherComp />
        </Suspense>
      )
    }
  • 路由懒加载
        import React,{lazy,Suspense} from 'react'
    import {BrowserRouter as Router,Route, Switch} from 'react-router-dom'
    
    const Home = lazy(/**webpackChunkName:'h=Home'**/()=>import('./Home'))
    const List = lazy(/**webpackChunkName:'List'**/()=>import('./List'))
    
    const App = ()=>(
      <Router>
        <Suspense fallback={<div>loading...</div>}>
          <Switch>
            <Route exact path='/' component={Home} />
            <Route exact path='/list' component={List} />
          </Switch>
        </Suspense>
      </Router>
    )
  • 使用SSRNext.js

连环问:你在使用React时遇到过哪些坑

  • 自定义组件的名称首字母要大写
        // 原生html组件
    <input />
    
    // 自定义组件
    <Input />
  • JS关键字的冲突
        // for改成htmlFor,class改成className
    <label htmlFor="input-name" className="label">
      用户名 <input id="username" />
    </label>
  • JSX数据类型
        // correct
    <Demo flag={true} />
    // error
    <Demo flag="true" />
  • setState不会马上获取最新的结果

    • 如需要实时获取结果,在回调函数中获取 setState({count:this.state.count + 1},()=>console.log(this.state.count)})
    • setState在合成事件和生命周期钩子中,是异步更新的
    • 原生事件setTimeout中,setState是同步的,可以马上获取更新后的值;
    • 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
        // setState原理模拟
    let isBatchingUpdate = true;
    
    let queue = [];
    let state = {number:0};
    function setState(newSate){
      //state={...state,...newSate}
      // setState异步更新
      if(isBatchingUpdate){
        queue.push(newSate);
      }else{
        // setState同步更新
        state={...state,...newSate}
      }   
    }
    
    // react事件是合成事件,在合成事件中isBatchingUpdate需要设置为true
    // 模拟react中事件点击
    function handleClick(){
      isBatchingUpdate=true; // 批量更新标志
    
      /**我们自己逻辑开始 */
      setState({number:state.number+1});
      setState({number:state.number+1});
      console.log(state); // 0
      setState({number:state.number+1});
      console.log(state); // 0
      /**我们自己逻辑结束 */
    
      state= queue.reduce((newState,action)=>{
        return {...newState,...action}
      },state); 
    }
    handleClick();
    console.log(state); // 1
        // setState笔试题考察 下面这道题输出什么
    class Example extends React.Component {
      constructor() {
      super()
      this.state = {
        val: 0
      }
    }
    // componentDidMount中isBatchingUpdate=true setState批量更新
    componentDidMount() {
      this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
      console.log(this.state.val)
      // 第 1 次 log
      this.setState({ val: this.state.val + 1 }) // 添加到queue队列中,等待处理
      console.log(this.state.val)
      // 第 2 次 log
      setTimeout(() => {
        // 在原生事件和setTimeout中(isBatchingUpdate=false),setState同步更新,可以马上获取更新后的值
        this.setState({ val: this.state.val + 1 }) // 同步更新
        console.log(this.state.val)
        // 第 3 次 log
        this.setState({ val: this.state.val + 1 }) // 同步更新
        console.log(this.state.val)
        // 第 4 次 log
        }, 0)
      }
      render() {
        return null
      }
    }
    
    // 答案:0, 0, 2, 3

React真题

1. 函数组件和class组件区别

  • 纯函数,输入props,输出JSX
  • 没有实例、没有生命周期、没有state
  • 不能拓展其他方法

2. 什么是受控组件

  • 表单的值,受到state控制
  • 需要自行监听onChange,更新state
  • 对比非受控组件

3. 何时使用异步组件

  • 加载大组件
  • 路由懒加载

4. 多个组件有公共逻辑如何抽离

  • HOC高阶组件
  • Render Props
  • React Hooks

5. react router如何配置懒加载

React和Vue的区别(常考)

共同

  • 都支持组件化
  • 都是数据驱动视图
  • 都用vdom操作DOM

区别

  • React使用JSX拥抱JSVue使用模板拥抱HTML
  • React函数式编程,Vue是声明式编程
  • React更多的是自力更生,Vue把你想要的都给你

当比较React和Vue时,以下是一些详细的区别:

  1. 构建方式:
  • React:React是一个用于构建用户界面的JavaScript库。它使用JSX语法,将组件的结构和逻辑放在一起,通过组件的嵌套和组合来构建应用程序。
  • Vue:Vue是一个渐进式框架,可以用于构建整个应用程序或仅用于特定页面的一部分。它使用模板语法,将HTML模板和JavaScript代码分离,通过指令和组件来构建应用程序。
  1. 学习曲线:
  • React:React相对来说更加灵活和底层,需要对JavaScript和JSX有一定的了解。它提供了更多的自由度和灵活性,但也需要更多的学习和理解。
  • Vue:Vue则更加简单和易于上手,它使用了模板语法和一些特定的概念,使得学习和使用起来更加直观。Vue的文档和教程也非常友好和详细。
  1. 数据绑定:
  • React:React使用单向数据流,通过props将数据从父组件传递到子组件。如果需要在子组件中修改数据,需要通过回调函数来实现。
  • Vue:Vue支持双向数据绑定,可以通过v-model指令实现数据的双向绑定。这使得在Vue中处理表单和用户输入更加方便。
  1. 组件化开发:
  • React:React的组件化开发非常灵活,组件可以通过props接收数据,通过state管理内部状态。React还提供了生命周期方法,可以在组件的不同阶段执行特定的操作。
  • Vue:Vue的组件化开发也非常强大,组件可以通过props接收数据,通过data属性管理内部状态。Vue还提供了生命周期钩子函数,可以在组件的不同阶段执行特定的操作。
  1. 生态系统:
  • React:React拥有庞大的生态系统,有许多第三方库和工具可供选择。React还有一个强大的社区支持,提供了大量的教程、文档和示例代码。
  • Vue:Vue的生态系统也很活跃,虽然相对React来说规模较小,但也有许多第三方库和工具可供选择。Vue的文档和教程也非常友好和详细。
  1. 性能:
  • React:React通过虚拟DOM(Virtual DOM)和高效的diff算法来提高性能。它只更新需要更新的部分,减少了对实际DOM的操作次数。
  • Vue:Vue也使用虚拟DOM来提高性能,但它采用了更细粒度的观察机制,可以精确追踪数据的变化,从而减少不必要的更新操作。
Last Updated:
Contributors: leeguooooo