React Hooks曾经推出一段时间,大家应该比拟相熟,或者多多少少在我的项目中用过。写这篇文章简略剖析一下Hooks的原理,并带大家实现一个简易版的Hooks。

这篇写的比拟细,相干的知识点都会解释,给大家刷新一下记忆。

Hooks

Hooks是React 16.8推出的新性能。以这种更简略的形式进行逻辑复用。之前函数组件被认为是无状态的。然而通过Hooks,函数组件也能够有状态,以及类组件的生命周期办法。

useState用法示例:

import React, { useState } from 'react';function Example() {  // count是组件的状态  const [count, setCount] = useState(0);  return (    <div>      <p>You clicked {count} times</p>      <button onClick={() => setCount(count + 1)}>        Click me      </button>    </div>  );}

闭包

开始之前,咱们来简略回顾一下闭包的概念,因为Hooks的实现是高度依赖闭包的。

闭包(Closure),Kyle Simpson在《你不晓得的Javascript》中总结闭包是:

Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.

闭包就是,函数能够拜访到它所在的词法作用域,即便是在定义以外的地位调用。

闭包的一个重要利用就是,实现外部变量/公有数据。

var counter = 0;// 给计数器加1function add() {  counter += 1;}// 调用 add() 3次add(); // 1add(); // 2counter = 1000;add(); // 1003

这里因为counter不是外部变量,所以谁都能批改它的值。咱们不想让人随便批改counter怎么办?这时候就能够用闭包:

function getAdd() {  var counter = 0;  return function add() {counter += 1;}}var add = getAdd();add(); // 1add(); // 2add(); // 3counter = 1000 // error! 以后地位无法访问counter

咱们还能够把函数的定义挪到调用的地位,用一个立刻执行函数表达式IIFE(Immediately Invoked Function Expression):

var add = (function getAdd() {  var counter = 0;  return function add() {counter += 1;}})();add(); // 1add(); // 2add(); // 3

这种通过IIFE创立闭包的形式也叫做模块模式(Module Pattern),它创立了一个关闭的作用域,只有通过返回的对象/办法来操纵作用域中的值。这个模式由来已久了,之前很多Javascript的库,比方jQuery,就是用它来导出本人的实例的。

开始入手实现

理清闭包的概念后能够着手写了。从简略的动手,先来实现setState。

function useState(initialValue) {  var _val = initialValue; // _val是useState的变量  function state() {    // state是一个外部函数,是闭包    return _val;  }  function setState(newVal) {    _val = newVal;  }  return [state, setState];}var [foo, setFoo] = useState(0);console.log(foo()); // 0setFoo(1);console.log(foo()) // 1

依据useState的定义来实现。比较简单不须要多解释。

将useState利用到组件中

当初咱们将这个简易版的useState利用到一个Counter组件中:

function Counter() {  const [count, setCount] = useState(0);  return {    click: () => setCount(count() + 1),    render: () => console.log('render:', { count: count() })  }}const C = Counter();C.render(); // render: { count: 0 }C.click();C.render(); // render: { count: 1 }

这里简略起见,就不render实在DOM了,因为咱们只关怀组件的状态,所以每次render的时候打印count的值。

这里点击click之后,counter的值加一,useState的基本功能实现了。但当初state是一个函数而不是一个变量,这和React的API不统一,接下来咱们就来改过这一点。

过期闭包

function useState(initialValue) {  var _val = initialValue  // 去掉了state()函数  function setState(newVal) {    _val = newVal  }  return [_val, setState] //间接返回_val}var [foo, setFoo] = useState(0)console.log(foo) // 0setFoo(1) // 更新_valconsole.log(foo) // 0 - BUG!

如果咱们间接把state从函数改成变量,问题就呈现了,state不更新了。无论点击几次,Counter的值始终不变。这个是过期闭包问题(Stale Closure Problem)。因为在useState返回的时候,state就指向了初始值,所以前面即便counter的值扭转了,打印进去的依然就旧值。咱们想要的是,返回一个变量的同时,还能让这个变量和实在状态同步。那如何来实现呢?参考 前端进阶面试题具体解答

模块模式

解决办法就是将闭包放在另一个闭包中。

const MyReact = (function() {  let _val //将_val晋升到外层闭包  return {    render(Component) {      const Comp = Component()      Comp.render()      return Comp    },    useState(initialValue) {      _val = _val || initialValue //每次刷新      function setState(newVal) {        _val = newVal      }      return [_val, setState]    }  }})()

咱们使用之前提到的模块模式,创立一个MyReact模块(第一层闭包),返回的对象中蕴含useState办法(第二层闭包)。useState返回值中的state,指向的是useState闭包中的_val,而每次调用useState,_val都会从新绑定到下层的_val上,保障返回的state的值是最新的。解决了过期闭包的问题。

MyReact还提供了另外一个办法render,办法中调用组件的render办法来“渲染”组件,也是为了不渲染DOM的状况下进行测试。

function Counter() {  const [count, setCount] = MyReact.useState(0)  return {    click: () => setCount(count + 1),    render: () => console.log('render:', { count })  }}let AppApp = MyReact.render(Counter) // render: { count: 0 }App.click()App = MyReact.render(Counter) // render: { count: 1 }

这里每次调用MyReact.render(Counter),都会生成新的Counter实例,调用实例的render办法。render办法中调用了MyReact.useState()。MyReact.useState()在屡次执行之间,外层闭包中的_val值放弃不变,所以count会绑定到以后的_val上,这样就能够打印出正确的count值了。

实现useEffect

实现了useState之后,接下来实现useEffect。

const MyReact = (function() {  let _val, _deps // 将状态和依赖数组保留到外层的闭包中  return {    render(Component) {      const Comp = Component()      Comp.render()      return Comp    },    useEffect(callback, depArray) {      const hasNoDeps = !depArray      const hasChangedDeps = _deps ? !depArray.every((el, i) => el === _deps[i]) : true      if (hasNoDeps || hasChangedDeps) {        callback()        _deps = depArray      }    },    useState(initialValue) {      _val = _val || initialValue      function setState(newVal) {        _val = newVal      }      return [_val, setState]    }  }})()// usagefunction Counter() {  const [count, setCount] = MyReact.useState(0)  MyReact.useEffect(() => {    console.log('effect', count)  }, [count])  return {    click: () => setCount(count + 1),    noop: () => setCount(count),    render: () => console.log('render', { count })  }}let AppApp = MyReact.render(Counter)// effect 0// render {count: 0}App.click()App = MyReact.render(Counter)// effect 1// render {count: 1}App.noop()App = MyReact.render(Counter)// // 没有执行effect// render {count: 1}App.click()App = MyReact.render(Counter)// effect 2// render {count: 2}

在MyReact.useEffect中,咱们将依赖数组保留到_deps,每次调用,都和前一次的依赖数组进行比对。发生变化才触发回调。

留神这里在比拟依赖时用的是Object.is, React在比拟state变动时也是用它。留神Object.is在比拟时不会做类型转换(和==不同)。另外NaN === NaN返回false,然而Object.is(NaN, NaN)会返回true。

(简略起见,咱们实现的useEffect,回调函数是同步执行的,所以打印进去的log是effect先执行,而后才是render。理论React中useEffect的回调函数应该是异步执行的)

反对多个Hooks

到此为止咱们曾经简略实现了useState和useEffect。但还有一个问题,就是useState和useEffect每个组件中只能用一次。

那么怎么能力反对应用屡次hooks呢,咱们能够将hooks保留到一个数组中。

const MyReact = (function() {  let hooks = [],    currentHook = 0 // 存储hooks的数组,和数组指针  return {    render(Component) {      const Comp = Component() // 执行effect      Comp.render()      currentHook = 0 // 每次render后,hooks的指针清零      return Comp    },    useEffect(callback, depArray) {      const hasNoDeps = !depArray      const deps = hooks[currentHook]      const hasChangedDeps = deps ? !depArray.some((el, i) => !Object.is(el, deps[i])) : true      if (hasNoDeps || hasChangedDeps) {        callback()        hooks[currentHook] = depArray      }      currentHook++ // 每调用一次指针加一    },    useState(initialValue) {      hooks[currentHook] = hooks[currentHook] || initialValue      const setStateHookIndex = currentHook // 留神⚠️这句不是没用。是防止过期闭包问题。      const setState = newState => (hooks[setStateHookIndex] = newState)      return [hooks[currentHook++], setState]    }  }})()

留神这里用了一个新的变量setStateHookIndex来保留currentHook的值。这是为了防止useState闭包包住旧的currentHook的值。

将改变利用到组件中:

function Counter() {  const [count, setCount] = MyReact.useState(0)  const [text, setText] = MyReact.useState('foo') // 第二次用了useState  MyReact.useEffect(() => {    console.log('effect', count, text)  }, [count, text])  return {    click: () => setCount(count + 1),    type: txt => setText(txt),    noop: () => setCount(count),    render: () => console.log('render', { count, text })  }}let AppApp = MyReact.render(Counter)// effect 0 foo// render {count: 0, text: 'foo'}App.click()App = MyReact.render(Counter)// effect 1 foo// render {count: 1, text: 'foo'}App.type('bar')App = MyReact.render(Counter)// effect 1 bar// render {count: 1, text: 'bar'}App.noop()App = MyReact.render(Counter)// // 不运行effect// render {count: 1, text: 'bar'}App.click()App = MyReact.render(Counter)// effect 2 bar// render {count: 2, text: 'bar'}

实现多个hooks反对的基本思路,就是用一个数组寄存hooks。每次应用hooks时,将hooks指针加1。每次render当前,将指针清零。

Custom Hooks

接下来,能够借助曾经实现的hooks持续实现custom hooks:

function Component() {  const [text, setText] = useSplitURL('www.google.com')  return {    type: txt => setText(txt),    render: () => console.log({ text })  }}function useSplitURL(str) {  const [text, setText] = MyReact.useState(str)  const masked = text.split('.')  return [masked, setText]}let AppApp = MyReact.render(Component)// { text: [ 'www', 'google', 'com' ] }App.type('www.reactjs.org')App = MyReact.render(Component)// { text: [ 'www', 'reactjs', 'org' ] }}

从新了解Hooks规定

理解Hooks的实现能够帮忙咱们了解Hooks的应用规定。还记得应用Hooks的准则吗?hooks只能用到组件最外层的代码中,不能包裹在if或者循环里,起因是在React外部,通过数组来存储hooks。所以必须保障每次render,hooks的程序不变,数量不变,能力做deps的比对。