成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

從頭實(shí)現(xiàn)一個(gè)簡易版React(三)

yvonne / 406人閱讀

摘要:寫在開頭從頭實(shí)現(xiàn)一個(gè)簡易版二地址在上一節(jié),我們的已經(jīng)具備了渲染功能。參考資料,感謝幾位前輩的分享陳屹深入技術(shù)棧

寫在開頭

從頭實(shí)現(xiàn)一個(gè)簡易版React(二)地址:https://segmentfault.com/a/11...
在上一節(jié),我們的react已經(jīng)具備了渲染功能。
在這一節(jié)我們將著重實(shí)現(xiàn)它的更新,說到更新,大家可能都會(huì)想到React的diff算法,它可以說是React性能高效的保證,同時(shí)也是最神秘,最難理解的部分(個(gè)人覺得),想當(dāng)初我也是看了好多文章,敲了N次代碼,調(diào)試了幾十遍,才總算理解了它的大概。在這也算是把我的理解闡述出來。

進(jìn)入正題

同樣,我們會(huì)實(shí)現(xiàn)三種ReactComponent的update方法。不過在這之前,我們先想想,該如何觸發(fā)React的更新呢?沒錯(cuò),就是setState方法。

// 所有自定義組件的父類
class Component {
  constructor(props) {
    this.props = props
  }

  setState(newState) {
    this._reactInternalInstance.updateComponent(null, newState)
  }
}
//代碼地址:src/react/Component.js

這里的reactInternalInstance就是我們在渲染ReactCompositeComponent時(shí)保存下的自身的實(shí)例,通過它調(diào)用了ReactCompositeComponent的update方法,接下來,我們就先實(shí)現(xiàn)這個(gè)update方法。

ReactCompositeComponent

這里的update方法同mount有點(diǎn)類似,都是調(diào)用生命周期和render方法,先上代碼:

class ReactCompositeComponent extends ReactComponent {
  constructor(element) {
    super(element)
    // 存放對(duì)應(yīng)的組件實(shí)例
    this._instance = null
    this._renderedComponent = null
  }
  
 mountComponent(rootId) {
  //內(nèi)容略
  }

  // 更新
  updateComponent(nextVDom, newState) {
    // 如果有新的vDom,就使用新的
    this._vDom = nextVDom || this._vDom
    const inst = this._instance
    // 獲取新的state,props
    const nextState = { ...inst.state, ...newState }
    const nextProps = this._vDom.props

    // 判斷shouldComponentUpdate
    if (inst.shouldComponentUpdate && (inst.shouldComponentUpdate(nextProps, nextState) === false)) return

    inst.componentWillUpdate && inst.componentWillUpdate(nextProps, nextState)

    // 更改state,props
    inst.state = nextState
    inst.props = nextProps

    const prevComponent = this._renderedComponent

    // 獲取render新舊的vDom
    const prevRenderVDom = prevComponent._vDom
    const nextRenderVDom = inst.render()

    // 判斷是需要更新還是重新渲染
    if (shouldUpdateReactComponent(prevRenderVDom, nextRenderVDom)) {
      // 更新
      prevComponent.updateComponent(nextRenderVDom)
      inst.componentDidUpdate && inst.componentDidUpdate()
    } else {
      // 重新渲染
      this._renderedComponent = instantiateReactComponent(nextRenderVDom)
      // 重新生成對(duì)應(yīng)的元素內(nèi)容
      const nextMarkUp = this._renderedComponent.mountComponent(this._rootNodeId)
      // 替換整個(gè)節(jié)點(diǎn)
      $(`[data-reactid="${this._rootNodeId}"]`).replaceWith(nextMarkUp)
    }
  }
}
//代碼地址:src/react/component/ReactCompositeComponent.js

有兩點(diǎn)要說明:

熟悉React的都知道,很多時(shí)候組件的更新,vDom并沒有變化,我們可以通過shouldComponentUpdate這個(gè)生命周期來優(yōu)化這點(diǎn),當(dāng)shouldComponentUpdate為false時(shí),直接return,不執(zhí)行下面的代碼。

當(dāng)調(diào)用render獲取到新的vDom時(shí),將會(huì)比較新舊的vDom類型是否相同,這也屬于diff算法優(yōu)化的一部分,如果類型相同,則執(zhí)行更新,反之,就重新渲染。

// 判斷是更新還是渲染
function shouldUpdateReactComponent(prevVDom, nextVDom) {
  if (prevVDom != null && nextVDom != null) {
    const prevType = typeof prevVDom
    const nextType = typeof nextVDom

    if (prevType === "string" || prevType === "number") {
      return nextType === "string" || nextType === "number"
    } else {
      return nextType === "object" && prevVDom.type === nextVDom.type && prevVDom.key === nextVDom.key
    }
  }
}
//代碼地址:src/react/component/util.js

注意,這里我們使用到了key,當(dāng)type相同時(shí)使用key可以快速準(zhǔn)確得出兩個(gè)vDom是否相同,這是為什么React要求我們在循環(huán)渲染時(shí)必須添加key這個(gè)props。

ReactTextComponent

ReactTextComponent的update方法非常簡單,判斷新舊文本是否相同,不同則更新內(nèi)容,直接貼代碼:

class ReactTextComponent extends ReactComponent {
  mountComponent(rootId) {
  //省略
  }

  // 更新
  updateComponent(nextVDom) {
    const nextText = "" + nextVDom

    if (nextText !== this._vDom) {
      this._vDom = nextText
    }
    // 替換整個(gè)節(jié)點(diǎn)
    $(`[data-reactid="${this._rootNodeId}"]`).html(this._vDom)
  }
// 代碼地址:src/react/component/ReactTextComponent.js
}
ReactDomComponent

ReactDomComponent的update最復(fù)雜,可以說diff的核心都在這里,本文的重心也就放在這。
整個(gè)update分為兩塊,props的更新和children的更新。

class ReactDomComponent extends ReactComponent {
  mountComponent(rootId) {
  //省略
  }

  // 更新
  updateComponent(nextVDom) {
    const lastProps = this._vDom.props
    const nextProps = nextVDom.props

    this._vDom = nextVDom

    // 更新屬性
    this._updateDOMProperties(lastProps, nextProps)
    // 再更新子節(jié)點(diǎn)
    this._updateDOMChildren(nextVDom.props.children)
  }
// 代碼地址:src/react/component/ReactDomComponent.js
}

props的更新非常簡單,無非就是遍歷新舊props,刪除不在新props里的老props,添加不在老props里的新props,更新新舊都有的props,事件特殊處理。

  _updateDOMProperties(lastProps, nextProps) {
    let propKey = ""

    // 遍歷,刪除已不在新屬性集合里的老屬性
    for (propKey in lastProps) {
      // 屬性在原型上或者新屬性里有,直接跳過
      if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) {
        continue
      }

      // 對(duì)于事件等特殊屬性,需要多帶帶處理
      if (/^on[A-Za-z]/.test(propKey)) {
        const eventType = propKey.replace("on", "")
        // 針對(duì)當(dāng)前的節(jié)點(diǎn)取消事件代理
        $(document).undelegate(`[data-reactid="${this._rootNodeId}"]`, eventType, lastProps[propKey])
        continue
      }
      
    }

    // 對(duì)于新的屬性,需要寫到dom節(jié)點(diǎn)上
    for (propKey in nextProps) {
      // 更新事件屬性
      if (/^on[A-Za-z]/.test(propKey)) {
        var eventType = propKey.replace("on", "")

        // 以前如果已經(jīng)有,需要先去掉
        lastProps[propKey] && $(document).undelegate(`[data-reactid="${this._rootNodeId}"]`, eventType, lastProps[propKey])

        // 針對(duì)當(dāng)前的節(jié)點(diǎn)添加事件代理
        $(document).delegate(`[data-reactid="${this._rootNodeId}"]`, `${eventType}.${this._rootNodeId}`, nextProps[propKey])
        continue
      }

      if (propKey === "children") continue

      // 更新普通屬性
      $(`[data-reactid="${this._rootNodeId}"]`).prop(propKey, nextProps[propKey])
    }
  }
// 代碼地址:src/react/component/ReactDomComponent.js

children的更新則相對(duì)復(fù)雜了很多,陳屹老師的《深入React技術(shù)棧》中提到,diff算法分為3塊,分別是

tree diff

component diff

element diff

上文中的shouldUpdateReactComponent就屬于component diff,接下來,讓我們依據(jù)這三種diff實(shí)現(xiàn)updateChildren。

// 全局的更新深度標(biāo)識(shí),用來判定觸發(fā)patch的時(shí)機(jī)
let updateDepth = 0
// 全局的更新隊(duì)列
let diffQueue = []

 _updateDOMChildren(nextChildVDoms) {
    updateDepth++

    // diff用來遞歸查找差異,組裝差異對(duì)象,并添加到diffQueue中
    this._diff(diffQueue, nextChildVDoms)
    updateDepth--

    if (updateDepth === 0) {
      // 具體的dom渲染
      this._patch(diffQueue)
      diffQueue = []
    }

這里通過updateDepth對(duì)vDom樹進(jìn)行層級(jí)控制,只會(huì)對(duì)相同層級(jí)的DOM節(jié)點(diǎn)進(jìn)行比較,只有當(dāng)一棵DOM樹全部遍歷完,才會(huì)調(diào)用patch處理差異。也就是所謂的tree diff。
確保了同層次后,我們要實(shí)現(xiàn)_diff方法。
已經(jīng)渲染過的子ReactComponents在這里是數(shù)組,我們要遍歷出里面的vDom進(jìn)行比較,這里就牽扯到上文中的key,在有key時(shí),我們優(yōu)先用key來獲取vDom,所以,我們首先遍歷數(shù)組,將其轉(zhuǎn)為map(這里先用object代替,以后會(huì)更改成es6的map),如果有key值的,就用key值作標(biāo)識(shí),無key的,就用index。
下面是array到map的代碼:

// 將children數(shù)組轉(zhuǎn)化為map
export function arrayToMap(array) {
  array = array || []
  const childMap = {}

  array.forEach((item, index) => {
    const name = item && item._vDom && item._vDom.key ? item._vDom.key : index.toString(36)
    childMap[name] = item
  })
  return childMap
}

部分diff方法:

// 將之前子節(jié)點(diǎn)的component數(shù)組轉(zhuǎn)化為map
const prevChildComponents = arrayToMap(this._renderedChildComponents)
// 生成新的子節(jié)點(diǎn)的component對(duì)象集合
const nextChildComponents = generateComponentsMap(prevChildComponents, nextChildVDoms)

將ReactComponent數(shù)組轉(zhuǎn)化為map后,用老的ReactComponents集合和新vDoms數(shù)組生成新的ReactComponents集合,這里會(huì)使用shouldUpdateReactComponent進(jìn)行component diff,如果相同,則直接更新即可,反之,就重新生成ReactComponent

/**
 * 用來生成子節(jié)點(diǎn)的component
 * 如果是更新,就會(huì)繼續(xù)使用以前的component,調(diào)用對(duì)應(yīng)的updateComponent
 * 如果是新的節(jié)點(diǎn),就會(huì)重新生成一個(gè)新的componentInstance
 */
function generateComponentsMap(prevChildComponents, nextChildVDoms = []) {
  const nextChildComponents = {}

  nextChildVDoms.forEach((item, index) => {
    const name = item.key ? item.key : index.toString(36)
    const prevChildComponent = prevChildComponents && prevChildComponents[name]

    const prevVdom = prevChildComponent && prevChildComponent._vDom
    const nextVdom = item

    // 判斷是更新還是重新渲染
    if (shouldUpdateReactComponent(prevVdom, nextVdom)) {
      // 更新的話直接遞歸調(diào)用子節(jié)點(diǎn)的updateComponent
      prevChildComponent.updateComponent(nextVdom)
      nextChildComponents[name] = prevChildComponent
    } else {
      // 重新渲染的話重新生成component
      const nextChildComponent = instantiateReactComponent(nextVdom)
      nextChildComponents[name] = nextChildComponent
    }
  })
  return nextChildComponents
}

經(jīng)歷了以上兩步,我們已經(jīng)獲得了新舊同層級(jí)的ReactComponents集合。需要做的,只是遍歷這兩個(gè)集合,進(jìn)行比較,同屬性的更新一樣,進(jìn)行移動(dòng),新增,和刪除,當(dāng)然,在這個(gè)過程中,我會(huì)包含我們的第三種優(yōu)化,element diff。它的策略是這樣的:首先對(duì)新集合的節(jié)點(diǎn)進(jìn)行循環(huán)遍歷,通過唯一標(biāo)識(shí)可以判斷新老集合中是否存在相同的節(jié)點(diǎn),如果存在相同節(jié)點(diǎn),則進(jìn)行移動(dòng)操作,但在移動(dòng)前需要將當(dāng)前節(jié)點(diǎn)在老集合中的位置與 lastIndex 進(jìn)行比較,if (prevChildComponent._mountIndex < lastIndex),則進(jìn)行節(jié)點(diǎn)移動(dòng)操作,否則不執(zhí)行該操作。這是一種順序優(yōu)化手段,lastIndex 一直在更新,表示訪問過的節(jié)點(diǎn)在老集合中最右的位置(即最大的位置),如果新集合中當(dāng)前訪問的節(jié)點(diǎn)比 lastIndex 大,說明當(dāng)前訪問節(jié)點(diǎn)在老集合中就比上一個(gè)節(jié)點(diǎn)位置靠后,則該節(jié)點(diǎn)不會(huì)影響其他節(jié)點(diǎn)的位置,因此不用添加到差異隊(duì)列中,即不執(zhí)行移動(dòng)操作,只有當(dāng)訪問的節(jié)點(diǎn)比 lastIndex 小時(shí),才需要進(jìn)行移動(dòng)操作。
上完整的diff方法代碼:

// 差異更新的幾種類型
const UPDATE_TYPES = {
  MOVE_EXISTING: 1,
  REMOVE_NODE: 2,
  INSERT_MARKUP: 3
}

   // 追蹤差異
  _diff(diffQueue, nextChildVDoms) {
    // 將之前子節(jié)點(diǎn)的component數(shù)組轉(zhuǎn)化為map
    const prevChildComponents = arrayToMap(this._renderedChildComponents)
    // 生成新的子節(jié)點(diǎn)的component對(duì)象集合
    const nextChildComponents = generateComponentsMap(prevChildComponents, nextChildVDoms)

    // 重新復(fù)制_renderChildComponents
    this._renderedChildComponents = []
    for (let name in nextChildComponents) {
      nextChildComponents.hasOwnProperty(name) && this._renderedChildComponents.push(nextChildComponents[name])
    }

    let lastIndex = 0 // 代表訪問的最后一次老的集合位置
    let nextIndex = 0 // 代表到達(dá)的新的節(jié)點(diǎn)的index

    // 通過對(duì)比兩個(gè)集合的差異,將差異節(jié)點(diǎn)添加到隊(duì)列中
    for (let name in nextChildComponents) {
      if (!nextChildComponents.hasOwnProperty(name)) continue

      const prevChildComponent = prevChildComponents && prevChildComponents[name]
      const nextChildComponent = nextChildComponents[name]

      // 相同的話,說明是使用的同一個(gè)component,需要移動(dòng)
      if (prevChildComponent === nextChildComponent) {
        // 添加差異對(duì)象,類型:MOVE_EXISTING
        prevChildComponent._mountIndex < lastIndex && diffQueue.push({
          parentId: this._rootNodeId,
          parentNode: $(`[data-reactid="${this._rootNodeId}"]`),
          type: UPDATE_TYPES.MOVE_EXISTING,
          fromIndex: prevChildComponent._mountIndex,
          toIndex: nextIndex
        })

        lastIndex = Math.max(prevChildComponent._mountIndex, lastIndex)
      } else {
        // 如果不相同,說明是新增的節(jié)點(diǎn)
        // 如果老的component在,需要把老的component刪除
        if (prevChildComponent) {
          diffQueue.push({
            parentId: this._rootNodeId,
            parentNode: $(`[data-reactid="${this._rootNodeId}"]`),
            type: UPDATE_TYPES.REMOVE_NODE,
            fromIndex: prevChildComponent._mountIndex,
            toIndex: null
          })

          // 去掉事件監(jiān)聽
          if (prevChildComponent._rootNodeId) {
            $(document).undelegate(`.${prevChildComponent._rootNodeId}`)
          }

          lastIndex = Math.max(prevChildComponent._mountIndex, lastIndex)
        }

        // 新增加的節(jié)點(diǎn)
        diffQueue.push({
          parentId: this._rootNodeId,
          parentNode: $(`[data-reactid="${this._rootNodeId}"]`),
          type: UPDATE_TYPES.INSERT_MARKUP,
          fromIndex: null,
          toIndex: nextIndex,
          markup: nextChildComponent.mountComponent(`${this._rootNodeId}.${name}`)
        })
      }

      // 更新_mountIndex
      nextChildComponent._mountIndex = nextIndex
      nextIndex++
    }

    // 對(duì)于老的節(jié)點(diǎn)里有,新的節(jié)點(diǎn)里沒有的,全部刪除
    for (let name in prevChildComponents) {
      const prevChildComponent = prevChildComponents[name]

      if (prevChildComponents.hasOwnProperty(name) && !(nextChildComponents && nextChildComponents.hasOwnProperty(name))) {
        diffQueue.push({
          parentId: this._rootNodeId,
          parentNode: $(`[data-reactid="${this._rootNodeId}"]`),
          type: UPDATE_TYPES.REMOVE_NODE,
          fromIndex: prevChildComponent._mountIndex,
          toIndex: null
        })

        // 如果渲染過,去掉事件監(jiān)聽
        if (prevChildComponent._rootNodeId) {
          $(document).undelegate(`.${prevChildComponent._rootNodeId}`)
        }
      }
    }
  }
//  代碼地址:src/react/component/ReactDomCompoent.js

調(diào)用diff方法后,會(huì)回到tree diff那一步,當(dāng)一整棵樹遍歷完后,就需要通過Patch將更新的內(nèi)容渲染出來了,patch方法相對(duì)比較簡單,由于我們把更新的內(nèi)容都放入了diffQueue中,只要遍歷這個(gè)數(shù)組,根據(jù)不同的類型進(jìn)行相應(yīng)的操作就行。

  // 渲染
  _patch(updates) {
    // 處理移動(dòng)和刪除的
    updates.forEach(({ type, fromIndex, toIndex, parentNode, parentId, markup }) => {
      const updatedChild = $(parentNode.children().get(fromIndex))

      switch (type) {
        case UPDATE_TYPES.INSERT_MARKUP:
          insertChildAt(parentNode, $(markup), toIndex) // 插入
          break
        case UPDATE_TYPES.MOVE_EXISTING:
          deleteChild(updatedChild) // 刪除
          insertChildAt(parentNode, updatedChild, toIndex)
          break
        case UPDATE_TYPES.REMOVE_NODE:
          deleteChild(updatedChild)
          break
        default:
          break
      }
    })
  }
// 代碼地址:src/react/component/ReactDomComponent.js
總結(jié)

以上,整個(gè)簡易版React就完成了,可以試著寫些簡單的例子跑跑看了,是不是非常有成就感呢?

總結(jié)下更新:
ReactCompositeComponent:負(fù)責(zé)調(diào)用生命周期,通過component diff將更新都交給了子ReactComponet
ReactTextComponent:直接更新內(nèi)容
ReactDomComponent:先更新props,在更新children,更新children分為三步,tree diff保證同層級(jí)比較,使用shouldUpdateReactComponent進(jìn)行component diff,最后在element diff通過lastIndex順序優(yōu)化

至此,整個(gè)從頭實(shí)現(xiàn)簡易版React就結(jié)束了,感謝大家的觀看。

參考資料,感謝幾位前輩的分享:
https://www.cnblogs.com/sven3...
https://github.com/purplebamb...
陳屹 《深入React技術(shù)?!?/p>

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/93134.html

相關(guān)文章

  • 從頭實(shí)現(xiàn)一個(gè)簡易React(一)

    摘要:既然看不懂,那就看看社區(qū)前輩們寫的一些源碼分析文章以及實(shí)現(xiàn)思路吧,又這么過了幾天,總算是摸清點(diǎn)思路,于是在參考了前輩們的基礎(chǔ)上,實(shí)現(xiàn)了一個(gè)簡易版的??偨Y(jié)以上就是實(shí)現(xiàn)一個(gè)的總體思路,下節(jié)我們重點(diǎn)放在不同的上。 寫在開頭 工作中使用react也很長一段時(shí)間了,雖然對(duì)它的用法,原理有了一定的了解,但是總感覺停留在表面。本著知其然知其所以然的態(tài)度,我試著去看了react源碼,幾天下來,發(fā)現(xiàn)并不...

    meislzhua 評(píng)論0 收藏0
  • 從頭實(shí)現(xiàn)一個(gè)簡易React(二)

    摘要:寫在開頭從頭實(shí)現(xiàn)一個(gè)簡易版一地址上一節(jié),我們詳細(xì)介紹了實(shí)現(xiàn)一個(gè)簡易的思路以及整體的結(jié)構(gòu),但是對(duì)于渲染和更新的原理,卻還沒有提及,因此,本節(jié)我們將重點(diǎn)放在的渲染上。 寫在開頭 從頭實(shí)現(xiàn)一個(gè)簡易版React(一)地址:https://segmentfault.com/a/11...上一節(jié),我們詳細(xì)介紹了實(shí)現(xiàn)一個(gè)簡易R(shí)eact的思路以及整體的結(jié)構(gòu),但是對(duì)于渲染和更新的原理,卻還沒有提及,因此...

    vvpvvp 評(píng)論0 收藏0
  • 基于react native的登錄界面demo 超簡易教程 redux

    摘要:登錄視圖登陸失敗用戶名或密碼不能為空彈出提示框成功是點(diǎn)擊登錄按鈕后調(diào)用的函數(shù),這里的功能比較簡單。通過把發(fā)出去密碼登錄聲明組件需要整個(gè)中的哪一部分?jǐn)?shù)據(jù)作為自己的將和組件聯(lián)系在一起編寫是負(fù)責(zé)生成的,所以在大項(xiàng)目中還會(huì)用到合并。 本豬說 本豬豬剛學(xué)react,也剛看RN,就叫寫這個(gè),苦不堪言,搭環(huán)境就搭了好久??淳W(wǎng)上教程也是改了好多小地方才寫完了。本著雷鋒精神手把手教你寫(假的)。 sho...

    scq000 評(píng)論0 收藏0
  • 關(guān)于Vue2一些值得推薦的文章 -- 五、六月份

    摘要:五六月份推薦集合查看最新的請點(diǎn)擊集前端最近很火的框架資源定時(shí)更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風(fēng)荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點(diǎn)擊::集web前端最近很火的vue2框架資源;定時(shí)更新,歡迎 Star 一下。 蘇...

    sutaking 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<