<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          從Preact中了解React組件和hooks基本原理

          共 24476字,需瀏覽 49分鐘

           ·

          2021-06-05 06:27

          關(guān)注并將「趣談前端」設(shè)為星標(biāo)

          每早08:30按時推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

          作者:荒山

          https://juejin.im/post/5cfa29e151882539c33e4f5e

          React 的代碼庫現(xiàn)在已經(jīng)比較龐大了,加上 v16 的 Fiber 重構(gòu),初學(xué)者很容易陷入細(xì)節(jié)的汪洋大海,搞懂了會讓人覺得自己很牛逼,搞不懂很容易讓人失去信心, 懷疑自己是否應(yīng)該繼續(xù)搞前端。那么嘗試在本文這里找回一點(diǎn)自信吧(高手繞路).

          Preact 是 React 的縮略版, 體積非常小, 但五臟俱全. 如果你想了解 React 的基本原理, 可以去學(xué)習(xí)學(xué)習(xí) Preact 的源碼, 這也正是本文的目的。

          關(guān)于 React 原理的優(yōu)秀的文章已經(jīng)非常多, 本文就是老酒裝新瓶, 算是自己的一點(diǎn)總結(jié),也為后面的文章作一下鋪墊吧.

          文章篇幅較長,閱讀時間約 20min,主要被代碼占據(jù),另外也畫了流程圖配合理解代碼。

          注意:代碼有所簡化,忽略掉 svg、replaceNode、context 等特性 本文代碼基于 Preact v10 版本

          • Virtual-DOM

          • 從 createElement 開始

          • Component 的實(shí)現(xiàn)

          • diff 算法

            • diffChildren
            • diff
            • diffElementNodes
            • diffProps
          • Hooks 的實(shí)現(xiàn)

            • useState
            • useEffect
          • 技術(shù)地圖

          • 擴(kuò)展

          Virtual-DOM

          3cc5ed823ba41c8690ee68375c264c1a.webp

          Virtual-DOM 其實(shí)就是一顆對象樹,沒有什么特別的,這個對象樹最終要映射到圖形對象. Virtual-DOM 比較核心的是它的diff算法.

          你可以想象這里有一個DOM映射器,見名知義,這個’DOM 映射器‘的工作就是將 Virtual-DOM 對象樹映射瀏覽器頁面的 DOM,只不過為了提高 DOM 的'操作性能'. 它不是每一次都全量渲染整個 Virtual-DOM 樹,而是支持接收兩顆 Virtual-DOM 對象樹(一個更新前,一個更新后), 通過 diff 算法計(jì)算出兩顆 Virtual-DOM 樹差異的地方,然后只應(yīng)用這些差異的地方到實(shí)際的 DOM 樹, 從而減少 DOM 變更的成本.

          Virtual-DOM 是比較有爭議性,推薦閱讀《網(wǎng)上都說操作真實(shí) DOM 慢,但測試結(jié)果卻比 React 更快,為什么?》 。切記永遠(yuǎn)都不要離開場景去評判一個技術(shù)的好壞。當(dāng)初網(wǎng)上把 React 吹得多么牛逼, 一些小白就會覺得 Virtual-DOM 很吊,JQuery 弱爆了。

          我覺得兩個可比性不大,從性能上看, 框架再怎么牛逼它也是需要操作原生 DOM 的,而且它未必有你使用 JQuery 手動操作 DOM 來得'精細(xì)'. 框架不合理使用也可能出現(xiàn)修改一個小狀態(tài),導(dǎo)致渲染雪崩(大范圍重新渲染)的情況; 同理 JQuery 雖然可以精細(xì)化操作 DOM, 但是不合理的 DOM 更新策略可能也會成為應(yīng)用的性能瓶頸. 所以關(guān)鍵還得看你怎么用.

          那為什么需要 Virtual-DOM?

          我個人的理解就是為了解放生產(chǎn)力?,F(xiàn)如今硬件的性能越來越好,web 應(yīng)用也越來越復(fù)雜,生產(chǎn)力也是要跟上的. 盡管手動操作 DOM 可能可以達(dá)到更高的性能和靈活性,但是這樣對大部分開發(fā)者來說太低效了,我們是可以接受犧牲一點(diǎn)性能換取更高的開發(fā)效率的.

          所以說 Virtual-DOM 更大的意義在于開發(fā)方式的改變: 聲明式、 數(shù)據(jù)驅(qū)動, 讓開發(fā)者不需要關(guān)心 DOM 的操作細(xì)節(jié)(屬性操作、事件綁定、DOM 節(jié)點(diǎn)變更),也就是說應(yīng)用的開發(fā)方式變成了view=f(state), 這對生產(chǎn)力的解放是有很大推動作用的.

          當(dāng)然 Virtual-DOM 不是唯一,也不是第一個的這樣解決方案. 比如 AngularJS, Vue1.x 這些基于模板的實(shí)現(xiàn)方式, 也可以說實(shí)現(xiàn)這種開發(fā)方式轉(zhuǎn)變的. 那相對于他們 Virtual-DOM 的買點(diǎn)可能就是更高的性能了, 另外 Virtual-DOM 在渲染層上面的抽象更加徹底, 不再耦合于 DOM 本身,比如可以渲染為 ReactNative,PDF,終端 UI 等等。

          從 createElement 開始

          很多小白將 JSX 等價為 Virtual-DOM,其實(shí)這兩者并沒有直接的關(guān)系, 我們知道 JSX 不過是一個語法糖.

          例如<a href="/"><span>Home</span></a>最終會轉(zhuǎn)換為h('a', { href:'/' }, h('span', null, 'Home'))這種形式, h是 JSX Element 工廠方法.

          h 在 React 下約定是React.createElement, 而大部分 Virtual-DOM 框架則使用h. hcreateElement 的別名, Vue 生態(tài)系統(tǒng)也是使用這個慣例, 具體為什么沒作考究(比較簡短?)。

          可以使用@jsx注解或 babel 配置項(xiàng)來配置 JSX 工廠:

              /**
          * @jsx h
          */

          render(<div>hello jsx</div>, el);

          本文不是 React 或 Preact 的入門文章,所以點(diǎn)到為止,更多內(nèi)容可以查看官方教程.

          現(xiàn)在來看看createElement, createElement 不過就是構(gòu)造一個對象(VNode):

              // ??type 節(jié)點(diǎn)的類型,有DOM元素(string)和自定義組件,以及Fragment, 為null時表示文本節(jié)點(diǎn)exportfunctioncreateElement(type, props, children) {
          props.children = children;
          // ??應(yīng)用defaultPropsif (type != null && type.defaultProps != null)
          for (let i in type.defaultProps)
          if (props[i] === undefined) props[i] = type.defaultProps[i];
          let ref = props.ref;
          let key = props.key;
          // ...// ??構(gòu)建VNode對象return createVNode(type, props, key, ref);
          }

          exportfunctioncreateVNode(type, props, key, ref) {
          return { type, props, key, ref, /* ... 忽略部分內(nèi)置字段 */constructor: undefined };
          }

          通過 JSX 和組件, 可以構(gòu)造復(fù)雜的對象樹:

              render(
          <divclassName="container"><SideBar /><Body /></div>,
          root,
          );

          Component 的實(shí)現(xiàn)

          對于一個視圖框架來說,組件就是它的靈魂, 就像函數(shù)之于函數(shù)式語言,類之于面向?qū)ο笳Z言, 沒有組件則無法組成復(fù)雜的應(yīng)用.

          組件化的思維推薦將一個應(yīng)用分而治之, 拆分和組合不同級別的組件,這樣可以簡化應(yīng)用的開發(fā)和維護(hù),讓程序更好理解. 從技術(shù)上看組件是一個自定義的元素類型,可以聲明組件的輸入(props)、有自己的生命周期和狀態(tài)以及方法、最終輸出 Virtual-DOM 對象樹, 作為應(yīng)用 Virtual-DOM 樹的一個分支存在.

          Preact 的自定義組件是基于 Component 類實(shí)現(xiàn)的. 對組件來說最基本的就是狀態(tài)的維護(hù), 這個通過 setState 來實(shí)現(xiàn):

              functionComponent(props, context) {}

          // ??setState實(shí)現(xiàn)
          Component.prototype.setState = function(update, callback) {
          // 克隆下一次渲染的State, _nextState會在一些生命周期方式中用到(例如shouldComponentUpdate)let s = (this._nextState !== this.state && this._nextState) ||
          (this._nextState = assign({}, this.state));

          // state更新if (typeof update !== 'function' || (update = update(s, this.props)))
          assign(s, update);

          if (this._vnode) { // 已掛載// 推入渲染回調(diào)隊(duì)列, 在渲染完成后批量調(diào)用if (callback) this._renderCallbacks.push(callback);
          // 放入異步調(diào)度隊(duì)列
          enqueueRender(this);
          }
          };

          enqueueRender 將組件放進(jìn)一個異步的批執(zhí)行隊(duì)列中,這樣可以歸并頻繁的 setState 調(diào)用,實(shí)現(xiàn)也非常簡單:

              let q = [];
          // 異步調(diào)度器,用于異步執(zhí)行一個回調(diào)const defer = typeofPromise == 'function'
          ? Promise.prototype.then.bind(Promise.resolve()) // micro task
          : setTimeout; // 回調(diào)到setTimeoutfunctionenqueueRender(c) {
          // 不需要重復(fù)推入已經(jīng)在隊(duì)列的Componentif (!c._dirty && (c._dirty = true) && q.push(c) === 1)
          defer(process); // 當(dāng)隊(duì)列從空變?yōu)榉强諘r,開始調(diào)度
          }

          // 批量清空隊(duì)列, 調(diào)用Component的forceUpdatefunctionprocess() {
          let p;
          // 排序隊(duì)列,從低層的組件優(yōu)先更新?
          q.sort((a, b) => b._depth - a._depth);
          while ((p = q.pop()))
          if (p._dirty) p.forceUpdate(false); // false表示不要強(qiáng)制更新,即不要忽略shouldComponentUpdate
          }

          Ok, 上面的代碼可以看出 setState 本質(zhì)上是調(diào)用 forceUpdate 進(jìn)行組件重新渲染的,來往下挖一挖 forceUpdate 的實(shí)現(xiàn).

          這里暫且忽略 diff, 將 diff 視作一個黑盒,他就是一個 DOM 映射器, 像上面說的 diff 接收兩棵 VNode 樹, 以及一個 DOM 掛載點(diǎn), 在比對的過程中它可以會創(chuàng)建、移除或更新組件和 DOM 元素,觸發(fā)對應(yīng)的生命周期方法.

              Component.prototype.forceUpdate = function(callback) { // callback放置渲染完成后的回調(diào)let vnode = this._vnode, dom = this._vnode._dom, parentDom = this._parentDom;

          if (parentDom) { // 已掛載過const force = callback !== false;
          let mounts = [];
          // 調(diào)用diff對當(dāng)前組件進(jìn)行重新渲染和Virtual-DOM比對// ??暫且忽略這些參數(shù), 將diff視作一個黑盒,他就是一個DOM映射器,
          dom = diff(parentDom, vnode, vnode, mounts, this._ancestorComponent, force, dom);
          if (dom != null && dom.parentNode !== parentDom)
          parentDom.appendChild(dom);
          commitRoot(mounts, vnode);
          }
          if (callback) callback();
          };

          在看看 render 方法, 實(shí)現(xiàn)跟 forceUpdate 差不多, 都是調(diào)用 diff 算法來執(zhí)行 DOM 更新,只不過由外部指定一個 DOM 容器:

              // 簡化版exportfunctionrender(vnode, parentDom) {
          vnode = createElement(Fragment, null, [vnode]);
          parentDom.childNodes.forEach(i => i.remove())
          let mounts = [];
          diffChildren(parentDom, null oldVNode, mounts, vnode, EMPTY_OBJ);
          commitRoot(mounts, vnode);
          }

          梳理一下上面的流程:37ee79273b32d2cdf690fa8010c67905.webp到目前為止沒有看到組件的其他功能,如初始化、生命周期函數(shù)。這些特性在 diff 函數(shù)中定義,也就是說在組件掛載或更新的過程中被調(diào)用。下一節(jié)就會介紹 diff

          diff 算法

          千呼萬喚始出來,通過上文可以看出,createElementComponent 邏輯都很薄, 主要的邏輯還是集中在 diff 函數(shù)中. React 將這個過程稱為 Reconciliation, 在 Preact 中稱為 Differantiate.

          為了簡化程序 Preact 的實(shí)現(xiàn)將 diff 和 DOM 雜糅在一起, 但邏輯還是很清晰,看下目錄結(jié)構(gòu)就知道了:

              src/diff
          ├── children.js # 比對children數(shù)組
          ├── index.js # 比對兩個節(jié)點(diǎn)
          └── props.js # 比對兩個DOM節(jié)點(diǎn)的props
          ```js

          ![](https://gw.alicdn.com/tfs/TB1pr4MlHr1gK0jSZR0XXbP8XXa-1156-960.png)

          在深入 diff 程序之前,先看一下基本的對象結(jié)構(gòu), 方便后面理解程序流程. 先來看下 VNode 的外形:
          `
          ``js
          type ComponentFactory<P> = preact.ComponentClass<P> | FunctionalComponent<P>;

          interface VNode<P = {}> {
          // 節(jié)點(diǎn)類型, 內(nèi)置DOM元素為string類型,而自定義組件則是Component類型,Preact中函數(shù)組件只是特殊的Component類型
          type: string | ComponentFactory<P> | null;
          props: P & { children: ComponentChildren } | string | number | null;
          key: Key
          ref: Ref<any> | null;

          /**
          * 內(nèi)部緩存信息
          */
          // VNode子節(jié)點(diǎn)
          _children: Array<VNode> | null;
          // 關(guān)聯(lián)的DOM節(jié)點(diǎn), 對于Fragment來說第一個子節(jié)點(diǎn)
          _dom: PreactElement | Text | null;
          // Fragment, 或者組件返回Fragment的最后一個DOM子節(jié)點(diǎn),
          _lastDomChild: PreactElement | Text | null;
          // Component實(shí)例
          _component: Component | null;
          }

          diffChildren

          先從最簡單的開始, 上面已經(jīng)猜出 diffChildren 用于比對兩個 VNode 列表.c7c3db1ac9a77503202902de1613e157.webp如上圖, 首先這里需要維護(hù)一個表示當(dāng)前插入位置的變量 oldDOM, 它一開始指向 DOM childrenNode 的第一個元素, 后面每次插入更新或插入 newDOM,都會指向 newDOM 的下一個兄弟元素.

          在遍歷 newChildren 列表過程中, 會嘗試找出相同 key 的舊 VNode,和它進(jìn)行 diff. 如果新 VNode 和舊 VNode 位置不一樣,這就需要移動它們;對于新增的 DOM,如果插入位置(oldDOM)已經(jīng)到了結(jié)尾,則直接追加到父節(jié)點(diǎn), 否則插入到 oldDOM 之前。

          最后卸載舊 VNode 列表中未使用的 VNode.

          來詳細(xì)看看源碼:

              exportfunctiondiffChildren(
          parentDom, // children的父DOM元素
          newParentVNode, // children的新父VNode
          oldParentVNode, // children的舊父VNode,diffChildren主要比對這兩個Vnode的children
          mounts, // 保存在這次比對過程中被掛載的組件實(shí)例,在比對后,會觸發(fā)這些組件的componentDidMount生命周期函數(shù)
          ancestorComponent, // children的直接父'組件', 即渲染(render)VNode的組件實(shí)例
          oldDom, // 當(dāng)前掛載的DOM,對于diffChildren來說,oldDom一開始指向第一個子節(jié)點(diǎn)
          ) {
          let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, (newParentVNode._children = []), coerceToVNode, true,);
          let oldChildren = (oldParentVNode && oldParentVNode._children) || EMPTY_ARR;
          // ...// ??遍歷新childrenfor (i = 0; i < newChildren.length; i++) {
          childVNode = newChildren[i] = coerceToVNode(newChildren[i]); // 規(guī)范化VNodeif (childVNode == null) continue// ??查找oldChildren中是否有對應(yīng)的元素,如果找到則通過設(shè)置為undefined,從oldChildren中移除// 如果沒有找到則保持為null
          oldVNode = oldChildren[i];
          for (j = 0; j < oldChildrenLength; j++) {
          oldVNode = oldChildren[j];
          if (oldVNode && childVNode.key == oldVNode.key && childVNode.type === oldVNode.type) {
          oldChildren[j] = undefined;
          break;
          }
          oldVNode = null; // 沒有找到任何舊node,表示是一個新的
          }
          // ?? 遞歸比對VNode
          newDom = diff(parentDom, childVNode, oldVNode, mounts, ancestorComponent, null, oldDom);
          // vnode沒有被diff卸載掉if (newDom != null) {
          if (childVNode._lastDomChild != null) {
          // ??當(dāng)前VNode是Fragment類型// 只有Fragment或組件返回Fragment的Vnode會有非null的_lastDomChild, 從Fragment的結(jié)尾的DOM樹開始比對:// <A> <A>// <> <> ?? Fragment類型,diff會遞歸比對它的children,所以最后我們只需要將newDom指向比對后的最后一個子節(jié)點(diǎn)即可// <a>a</a> <- diff -> <b>b</b>// <b>b</b> <a>a</a> ----+// </> </> \// <div>x</div> ??oldDom會指向這里// </A> </A>
          newDom = childVNode._lastDomChild;
          } elseif (oldVNode == null || newDom != oldDom || newDom.parentNode == null) {
          // ?? newDom和當(dāng)前oldDom不匹配,嘗試新增或修改位置
          outer: if (oldDom == null || oldDom.parentNode !== parentDom) {
          // ??oldDom指向了結(jié)尾, 即后面沒有更多元素了,直接插入即可; 首次渲染一般會調(diào)用到這里
          parentDom.appendChild(newDom);
          } else {
          // 這里是一個優(yōu)化措施,去掉也不會影響正常程序. 為了便于理解可以忽略這段代碼// 嘗試向后查找oldChildLength/2個元素,如果找到則不需要調(diào)用insertBefore. 這段代碼可以減少insertBefore的調(diào)用頻率for (sibDom = oldDom, j = 0; (sibDom = sibDom.nextSibling) && j < oldChildrenLength; j += 2) {
          if (sibDom == newDom)
          break outer;
          }

          // ??insertBefore() 將newDom移動到oldDom之前
          parentDom.insertBefore(newDom, oldDom);
          }
          }
          // ??其他情況,newDom === oldDOM不需要處理// ?? oldDom指向下一個DOM節(jié)點(diǎn)
          oldDom = newDom.nextSibling;
          }
          }

          // ?? 卸載掉沒有被置為undefined的元素for (i = oldChildrenLength; i--; )
          if (oldChildren[i] != null) unmount(oldChildren[i], ancestorComponent);
          }

          配圖理解一下 diffChilrend 的調(diào)用過程:b3053ce5d5009951230758585eb6c848.webp

          總結(jié)一下流程圖f77f20669f6c43618a8b62278be80bdc.webp

          diff

          diff 用于比對兩個 VNode 節(jié)點(diǎn). diff 函數(shù)比較冗長, 但是這里面并沒有特別復(fù)雜邏輯,主要是一些自定義組件生命周期的處理。所以先上流程圖,代碼不感興趣可以跳過.d76bf6177427201b990fc83c99dbc3e0.webp

          源代碼解析:

              exportfunctiondiff(
          parentDom, // 父DOM節(jié)點(diǎn)
          newVNode, // 新VNode
          oldVNode, // 舊VNode
          mounts, // 存放已掛載的組件, 將在diff結(jié)束后批量處理
          ancestorComponent, // 直接父組件
          force, // 是否強(qiáng)制更新, 為true將忽略掉shouldComponentUpdate
          oldDom, // 當(dāng)前掛載的DOM節(jié)點(diǎn)
          ) {
          //...try {
          outer: if (oldVNode.type === Fragment || newType === Fragment) {
          // ?? Fragment類型,使用diffChildren進(jìn)行比對
          diffChildren(parentDom, newVNode, oldVNode, mounts, ancestorComponent, oldDom);

          // ??記錄Fragment的起始DOM和結(jié)束DOMlet i = newVNode._children.length;
          if (i && (tmp = newVNode._children[0]) != null) {
          newVNode._dom = tmp._dom;
          while (i--) {
          tmp = newVNode._children[i];
          if (newVNode._lastDomChild = tmp && (tmp._lastDomChild || tmp._dom))
          break;
          }
          }
          } elseif (typeof newType === 'function') {
          // ??自定義組件類型if (oldVNode._component) {
          // ?? ?已經(jīng)存在組件實(shí)例
          c = newVNode._component = oldVNode._component;
          newVNode._dom = oldVNode._dom;
          } else {
          // ??初始化組件實(shí)例if (newType.prototype && newType.prototype.render) {
          // ??類組件
          newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
          } else {
          // ??函數(shù)組件
          newVNode._component = c = new Component(newVNode.props, cctx);
          c.constructor = newType;
          c.render = doRender;
          }
          c._ancestorComponent = ancestorComponent;
          c.props = newVNode.props;
          if (!c.state) c.state = {};
          isNew = c._dirty = true;
          c._renderCallbacks = [];
          }

          c._vnode = newVNode;
          if (c._nextState == null) c._nextState = c.state;

          // ??getDerivedStateFromProps 生命周期方法if (newType.getDerivedStateFromProps != null)
          assign(c._nextState == c.state
          ? (c._nextState = assign({}, c._nextState)) // 惰性拷貝
          : c._nextState,
          newType.getDerivedStateFromProps(newVNode.props, c._nextState),
          );

          if (isNew) {
          // ?? 調(diào)用掛載前的一些生命周期方法// ?? componentWillMountif (newType.getDerivedStateFromProps == null && c.componentWillMount != null) c.componentWillMount();

          // ?? componentDidMount// 將組件推入mounts數(shù)組,在整個組件樹diff完成后批量調(diào)用, 他們在commitRoot方法中被調(diào)用// 按照先進(jìn)后出(棧)的順序調(diào)用, 即子組件的componentDidMount會先調(diào)用if (c.componentDidMount != null) mounts.push(c);
          } else {
          // ?? 調(diào)用重新渲染相關(guān)的一些生命周期方法// ?? componentWillReceivePropsif (newType.getDerivedStateFromProps == null && force == null && c.componentWillReceiveProps != null)
          c.componentWillReceiveProps(newVNode.props, cctx);

          // ?? shouldComponentUpdateif (!force && c.shouldComponentUpdate != null && c.shouldComponentUpdate(newVNode.props, c._nextState, cctx) === false) {
          // shouldComponentUpdate返回false,取消渲染更新
          c.props = newVNode.props;
          c.state = c._nextState;
          c._dirty = false;
          newVNode._lastDomChild = oldVNode._lastDomChild;
          break outer;
          }

          // ?? componentWillUpdateif (c.componentWillUpdate != null) c.componentWillUpdate(newVNode.props, c._nextState, cctx);
          }

          // ??至此props和state已經(jīng)確定下來,緩存和更新props和state準(zhǔn)備渲染
          oldProps = c.props;
          oldState = c.state;
          c.props = newVNode.props;
          c.state = c._nextState;
          let prev = c._prevVNode || null;
          c._dirty = false;

          // ??渲染let vnode = (c._prevVNode = coerceToVNode(c.render(c.props, c.state)));

          // ??getSnapshotBeforeUpdateif (!isNew && c.getSnapshotBeforeUpdate != null) snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);

          // ??組件層級,會影響更新的優(yōu)先級
          c._depth = ancestorComponent ? (ancestorComponent._depth || 0) + 1 : 0;
          // ??遞歸diff渲染結(jié)果
          c.base = newVNode._dom = diff(parentDom, vnode, prev, mounts, c, null, oldDom);

          if (vnode != null) {
          newVNode._lastDomChild = vnode._lastDomChild;
          }
          c._parentDom = parentDom;
          // ??應(yīng)用refif ((tmp = newVNode.ref)) applyRef(tmp, c, ancestorComponent);
          // ??調(diào)用renderCallbacks,即setState的回調(diào)while ((tmp = c._renderCallbacks.pop())) tmp.call(c);

          // ??componentDidUpdateif (!isNew && oldProps != null && c.componentDidUpdate != null) c.componentDidUpdate(oldProps, oldState, snapshot);
          } else {
          // ??比對兩個DOM元素
          newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, mounts, ancestorComponent);

          if ((tmp = newVNode.ref) && oldVNode.ref !== tmp) applyRef(tmp, newVNode._dom, ancestorComponent);
          }
          } catch (e) {
          // ??捕獲渲染錯誤,傳遞給上級組件的didCatch生命周期方法
          catchErrorInComponent(e, ancestorComponent);
          }

          return newVNode._dom;
          }

          diffElementNodes

          比對兩個 DOM 元素, 流程非常簡單:1500a2b03ece4ff7764d0c0f1cf0bc69.webp

              functiondiffElementNodes(dom, newVNode, oldVNode, mounts, ancestorComponent) {
          // ...// ??創(chuàng)建DOM節(jié)點(diǎn)if (dom == null) {
          if (newVNode.type === null) {
          // ??文本節(jié)點(diǎn), 沒有屬性和子級,直接返回returndocument.createTextNode(newProps);
          }
          dom = document.createElement(newVNode.type);
          }

          if (newVNode.type === null) {
          // ??文本節(jié)點(diǎn)更新if (oldProps !== newProps) dom.data = newProps;
          } else {
          if (newVNode !== oldVNode) {
          // newVNode !== oldVNode 這說明是一個靜態(tài)節(jié)點(diǎn)let oldProps = oldVNode.props || EMPTY_OBJ;
          let newProps = newVNode.props;

          // ?? dangerouslySetInnerHTML處理let oldHtml = oldProps.dangerouslySetInnerHTML;
          let newHtml = newProps.dangerouslySetInnerHTML;
          if (newHtml || oldHtml)
          if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html)
          dom.innerHTML = (newHtml && newHtml.__html) || '';

          // ??遞歸比對子元素
          diffChildren(dom, newVNode, oldVNode, context, mounts, ancestorComponent, EMPTY_OBJ);
          // ??遞歸比對DOM屬性
          diffProps(dom, newProps, oldProps, isSvg);
          }
          }

          return dom;
          }

          diffProps

          diffProps 用于更新 DOM 元素的屬性

              exportfunctiondiffProps(dom, newProps, oldProps, isSvg) {
          let i;
          const keys = Object.keys(newProps).sort();
          // ??比較并設(shè)置屬性for (i = 0; i < keys.length; i++) {
          const k = keys[i];
          if (k !== 'children' && k !== 'key' &&
          (!oldProps || (k === 'value' || k === 'checked' ? dom : oldProps)[k] !== newProps[k]))
          setProperty(dom, k, newProps[k], oldProps[k], isSvg);
          }

          // ??清空屬性for (i in oldProps)
          if (i !== 'children' && i !== 'key' && !(i in newProps))
          setProperty(dom, i, null, oldProps[i], isSvg);
          }
          ```js

          `
          diffProps` 實(shí)現(xiàn)比較簡單,就是遍歷一下屬性有沒有變動,有變動則通過`setProperty` 設(shè)置屬性。對于失效的 `props` 也會通過 `setProperty` 置空。這里面稍微有點(diǎn)復(fù)雜的是 `setProperty`. 這里涉及到事件的處理, 命名的轉(zhuǎn)換等等:
          `
          ``js
          functionsetProperty(dom, name, value, oldValue, isSvg) {
          if (name === 'style') {
          // ??樣式設(shè)置const set = assign(assign({}, oldValue), value);
          for (let i in set) {
          // 樣式屬性沒有變動if ((value || EMPTY_OBJ)[i] === (oldValue || EMPTY_OBJ)[i]) continue;
          dom.style.setProperty(
          i[0] === '-' && i[1] === '-' ? i : i.replace(CAMEL_REG, '-$&'),
          value && i in value
          ? typeof set[i] === 'number' && IS_NON_DIMENSIONAL.test(i) === false
          ? set[i] + 'px'
          : set[i]
          : '', // 清空
          );
          }
          } elseif (name[0] === 'o' && name[1] === 'n') {
          // ??事件綁定let useCapture = name !== (name = name.replace(/Capture$/, ''));
          let nameLower = name.toLowerCase();
          name = (nameLower in dom ? nameLower : name).slice(2);
          if (value) {
          // ??首次添加事件, 注意這里是eventProxy為事件處理器// preact統(tǒng)一將所有事件處理器收集在dom._listeners對象中,統(tǒng)一進(jìn)行分發(fā)// function eventProxy(e) {// return this._listeners[e.type](options.event ? options.event(e) : e);// }if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
          } else {
          // 移除事件
          dom.removeEventListener(name, eventProxy, useCapture);
          }
          // 保存事件隊(duì)列
          (dom._listeners || (dom._listeners = {}))[name] = value;
          } elseif (name !== 'list' && name !== 'tagName' && name in dom) {
          // ??DOM對象屬性
          dom[name] = value == null ? '' : value;
          } elseif (
          typeof value !== 'function' &&
          name !== 'dangerouslySetInnerHTML'
          ) {
          // ??DOM元素屬性if (value == null || value === false) {
          dom.removeAttribute(name);
          } else {
          dom.setAttribute(name, value);
          }
          }
          }

          OK 至此 Diff 算法介紹完畢,其實(shí)這里面的邏輯并不是特別復(fù)雜, 當(dāng)然 Preact 只是一個極度精簡的框架,React 復(fù)雜度要高得多,尤其 React Fiber 重構(gòu)之后。你也可以把 Preact 當(dāng)做 React 的歷史回顧,有興趣再深入了解 React 的最新架構(gòu)。

          Hooks 的實(shí)現(xiàn)

          React16.8 正式引入的 hooks,這玩意帶來了全新的 React 組件開發(fā)方式,讓代碼變得更加簡潔。React hooks: not magic, just arrays這篇文章已經(jīng)揭示了 hooks 的基本實(shí)現(xiàn)原理, 它不過是基于數(shù)組實(shí)現(xiàn)的。preact 也實(shí)現(xiàn)了 hooks 機(jī)制,實(shí)現(xiàn)代碼也就百來行,讓我們來體會體會.

          hooks 功能本身是沒有集成在 Preact 代碼庫內(nèi)部的,而是通過preact/hooks導(dǎo)入

              import { h } from'preact';
          import { useEffect } from'preact/hooks';
          functionFoo() {
          useEffect(() => {
          console.log('mounted');
          }, []);
          return<div>hello hooks</div>;
          }

          那 Preact 是如何擴(kuò)展 diff 算法來實(shí)現(xiàn) hooks 的呢?實(shí)際上 Preact 提供了options對象來對 Preact diff 進(jìn)行擴(kuò)展,options 類似于 Preact 生命周期鉤子,在 diff 過程中被調(diào)用(為了行文簡潔,上面的代碼我忽略掉了)。例如:

              exportfunctiondiff(/*...*/) {
          // ...// ??開始diffif ((tmp = options.diff)) tmp(newVNode);

          try {
          outer: if (oldVNode.type === Fragment || newType === Fragment) {
          // Fragment diff
          } elseif (typeof newType === 'function') {
          // 自定義組件diff// ??開始渲染if ((tmp = options.render)) tmp(newVNode);
          try {
          // ..
          c.render(c.props, c.state, c.context),
          } catch (e) {
          // ??捕獲異常if ((tmp = options.catchRender) && tmp(e, c)) return;
          throw e;
          }
          } else {
          // DOM element diff
          }
          // ??diff結(jié)束if ((tmp = options.diffed)) tmp(newVNode);
          } catch (e) {
          catchErrorInComponent(e, ancestorComponent);
          }
          return newVNode._dom;
          }
          // ...

          useState

          先從最常用的 useState 開始:

              exportfunctionuseState(initialState) {
          // ??OK只是數(shù)組,沒有Magic,每個hooks調(diào)用都會遞增currenIndex, 從當(dāng)前組件中取出狀態(tài)const hookState = getHookState(currentIndex++);

          // ?? 初始化if (!hookState._component) {
          hookState._component = currentComponent; // 當(dāng)前組件實(shí)例
          hookState._value = [
          // ??state, 初始化statetypeof initialState === 'function' ? initialState() : initialState,
          // ??dispatch
          value => {
          const nextValue = typeof value === 'function' ? value(hookState._value[0]) : value;
          if (hookState._value[0] !== nextValue) {
          // ?? 保存狀態(tài)并調(diào)用setState強(qiáng)制更新
          hookState._value[0] = nextValue;
          hookState._component.setState({});
          }
          },
          ];
          }

          return hookState._value; // [state, dispatch]
          }
          ```js

          從代碼可以看到,關(guān)鍵在于`
          getHookState`的實(shí)現(xiàn)
          `
          ``js
          import { options } from'preact';

          let currentIndex; // 保存當(dāng)前hook的索引let currentComponent;

          // ??render 鉤子, 在組件開始渲染之前調(diào)用// 因?yàn)镻react是同步遞歸向下渲染的,而且Javascript是單線程的,所以可以安全地引用當(dāng)前正在渲染的組件實(shí)例
          options.render = vnode => {
          currentComponent = vnode._component; // 保存當(dāng)前正在渲染的組件
          currentIndex = 0; // 開始渲染時index重置為0// 暫時忽略,下面講到useEffect就能理解// 清空上次渲染未處理的Effect(useEffect),只有在快速重新渲染時才會出現(xiàn)這種情況,一般在異步隊(duì)列中被處理if (currentComponent.__hooks) {
          currentComponent.__hooks._pendingEffects = handleEffects(
          currentComponent.__hooks._pendingEffects,
          );
          }
          };

          // ??no magic!, 只是一個數(shù)組, 狀態(tài)保存在組件實(shí)例的_list數(shù)組中functiongetHookState(index) {
          // 獲取或初始化列表const hooks = currentComponent.__hooks ||
          (currentComponent.__hooks = {
          _list: [], // 放置狀態(tài)
          _pendingEffects: [], // 放置待處理的effect,由useEffect保存
          _pendingLayoutEffects: [], // 放置待處理的layoutEffect,有useLayoutEffect保存
          });

          // 新建狀態(tài)if (index >= hooks._list.length) {
          hooks._list.push({});
          }

          return hooks._list[index];
          }

          大概的流程如下:29b50ad84d224e060577596fd7ff47f7.webp

          useEffect

          再看看 useEffect 和 useLayoutEffect. useEffect 和 useLayouteEffect 差不多, 只是觸發(fā) effect 的時機(jī)不一樣,useEffect 在完成渲染后繪制觸發(fā),而 useLayoutEffect 在 diff 完成后觸發(fā):

              exportfunctionuseEffect(callback, args) {
          const state = getHookState(currentIndex++);
          if (argsChanged(state._args, args)) {
          // ??狀態(tài)變化
          state._value = callback;
          state._args = args;
          currentComponent.__hooks._pendingEffects.push(state); // ??推進(jìn)_pendingEffects隊(duì)列
          afterPaint(currentComponent);
          }
          }

          exportfunctionuseLayoutEffect(callback, args) {
          const state = getHookState(currentIndex++);
          if (argsChanged(state._args, args)) {
          // ??狀態(tài)變化
          state._value = callback;
          state._args = args;
          currentComponent.__hooks._pendingLayoutEffects.push(state); // ??推進(jìn)_pendingLayoutEffects隊(duì)列
          }
          }

          看看如何觸發(fā) effect. useEffect 和上面看到的enqueueRender差不多,放進(jìn)一個異步隊(duì)列中,由requestAnimationFrame進(jìn)行調(diào)度,批量處理:

              // 這是一個類似于上面提到的異步隊(duì)列
          afterPaint = component => {
          if (!component._afterPaintQueued && // 避免組件重復(fù)推入
          (component._afterPaintQueued = true) &&
          afterPaintEffects.push(component) === 1// 開始調(diào)度
          )
          requestAnimationFrame(scheduleFlushAfterPaint); // 由requestAnimationFrame調(diào)度
          };

          functionscheduleFlushAfterPaint() {
          setTimeout(flushAfterPaintEffects);
          }

          functionflushAfterPaintEffects() {
          afterPaintEffects.some(component => {
          component._afterPaintQueued = false;
          if (component._parentDom)
          // 清空_pendingEffects隊(duì)列
          component.__hooks._pendingEffects = handleEffects(component.__hooks._pendingEffects);
          });
          afterPaintEffects = [];
          }

          functionhandleEffects(effects) {
          // 先清除后調(diào)用effect
          effects.forEach(invokeCleanup); // 請調(diào)用清理
          effects.forEach(invokeEffect); // 再調(diào)用effectreturn [];
          }

          functioninvokeCleanup(hook) {
          if (hook._cleanup) hook._cleanup();
          }

          functioninvokeEffect(hook) {
          const result = hook._value();
          if (typeof result === 'function') hook._cleanup = result;
          }

          再看看如何觸發(fā) LayoutEffect, 很簡單,在 diff 完成后觸發(fā), 這個過程是同步的.

              options.diffed = vnode => {
          const c = vnode._component;
          if (!c) return;
          const hooks = c.__hooks;
          if (hooks) {
          hooks._pendingLayoutEffects = handleEffects(hooks._pendingLayoutEffects);
          }
          };

          ??,hooks 基本原理基本了解完畢, 最后還是用一張圖來總結(jié)一下吧。00b4151fd02fb590e8e748593462ad72.webp

          技術(shù)地圖

          文章篇幅很長,主要是太多代碼了, 我自己也不喜歡看這種文章,所以沒期望讀者會看到這里. 后面文章再想辦法改善改善. 謝謝你閱讀到這里。

          本期的主角本身是一個小而美的視圖框架,沒有其他技術(shù)棧. 這里就安利一下 Preact 作者developit的另外一些小而美的庫吧.

          • Workerize 優(yōu)雅地在 webWorker 中執(zhí)行和調(diào)用程序
          • microbundle 零配置的庫打包工具
          • greenlet 和 workerize 差不多,這個將單個異步函數(shù)放到 webworker 中執(zhí)行,而 workerize 是將一個模塊
          • mitt 200byte 的 EventEmitter
          • dlv 安全地訪問深嵌套的對象屬性,類似于 lodash 的 get 方法
          • snarkdown 1kb 的 markdown parser
          • unistore 簡潔類 Redux 狀態(tài)容器,支持 React 和 Preact
          • stockroom 在 webWorker 支持狀態(tài)管理器

          擴(kuò)展

          • Preact:Into the void 0(譯)
          • React Virtual DOM vs Incremental DOM vs Ember’s Glimmer: Fight


          ?? 看完三件事

          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

          • 點(diǎn)個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容

          • 關(guān)注公眾號【趣談前端】,不定期分享?前端工程化?/ 可視化 / 低代碼?等技術(shù)文章。


          ad720692bd4f6455aa7d98c3feba03ea.webp

          10款2021年國外頂尖的lowcode開發(fā)平臺

          2個小時, 從學(xué)到做, 我用Dooring制作了3個電商H5

          H5編輯器H5-dooring最新更新指南



          ac4ba8569e7cd759e1dc24b9cb04d6a3.webpef0e92fc8301da98ac310a9cd3d0665c.webp

          點(diǎn)個在看你最好看



          瀏覽 42
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  女人考逼久久久 | 黄色级片网站视频 | 久久人热视频综合网 | 天天色天天操天天射 | JJ视频在线观看 |