乐趣区

关于javascript:一步步实现ReactHooks核心原理

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;

// 给计数器加 1
function add() {counter += 1;}

// 调用 add() 3 次
add(); // 1
add(); // 2
counter = 1000;
add(); // 1003

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

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

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

var add = (function getAdd() {
  var counter = 0;
  return function add() {counter += 1;}
})();
add(); // 1
add(); // 2
add(); // 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()); // 0
setFoo(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) // 0
setFoo(1) // 更新_val
console.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 App
App = 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]
    }
  }
})()

// usage
function 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 App
App = 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 App
App = 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 App
App = 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 的比对。

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

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;

// 给计数器加 1
function add() {counter += 1;}

// 调用 add() 3 次
add(); // 1
add(); // 2
counter = 1000;
add(); // 1003

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

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

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

var add = (function getAdd() {
  var counter = 0;
  return function add() {counter += 1;}
})();
add(); // 1
add(); // 2
add(); // 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()); // 0
setFoo(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) // 0
setFoo(1) // 更新_val
console.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 App
App = 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]
    }
  }
})()

// usage
function 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 App
App = 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 App
App = 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 App
App = 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 的比对。

退出移动版