整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          深入詳解大佬用33行代碼實現了React

          深入詳解大佬用33行代碼實現了React


          者: 李松峰

          轉發連接:https://mp.weixin.qq.com/s/dIG7QxHyP-EKWH0Fq98dGQ

          自:coderwhy


          前面說過,整個前端已經是組件化的天下,而CSS的設計就不是為組件化而生的,所以在目前組件化的框架中都在需要一種合適的CSS解決方案。

          一. React中的css方案

          1.1. react中的css

          事實上,css一直是React的痛點,也是被很多開發者吐槽、詬病的一個點。

          在組件化中選擇合適的CSS解決方案應該符合以下條件:

          • 可以編寫局部css:css具備自己的具備作用域,不會隨意污染其他組件內的原生;
          • 可以編寫動態的css:可以獲取當前組件的一些狀態,根據狀態的變化生成不同的css樣式;
          • 支持所有的css特性:偽類、動畫、媒體查詢等;
          • 編寫起來簡潔方便、最好符合一貫的css風格特點;
          • 等等...

          在這一點上,Vue做的要遠遠好于React:

          • Vue通過在.vue文件中編寫 <style><style> 標簽來編寫自己的樣式;
          • 通過是否添加 scoped 屬性來決定編寫的樣式是全局有效還是局部有效;
          • 通過 lang 屬性來設置你喜歡的 less、sass等預處理器;
          • 通過內聯樣式風格的方式來根據最新狀態設置和改變css;
          • 等等...

          Vue在CSS上雖然不能稱之為完美,但是已經足夠簡潔、自然、方便了,至少統一的樣式風格不會出現多個開發人員、多個項目采用不一樣的樣式風格。

          相比而言,React官方并沒有給出在React中統一的樣式風格:

          • 由此,從普通的css,到css modules,再到css in js,有幾十種不同的解決方案,上百個不同的庫;
          • 大家一直在尋找最好的或者說最適合自己的CSS方案,但是到目前為止也沒有統一的方案;

          在這篇文章中,我會介紹挑選四種解決方案來介紹:

          • 方案一:內聯樣式的寫法;
          • 方案二:普通的css寫法;
          • 方案三:css modules;
          • 方案四:css in js(styled-components);

          1.2. 普通的解決方案

          1.2.1. 內聯樣式

          內聯樣式是官方推薦的一種css樣式的寫法:

          • style 接受一個采用小駝峰命名屬性的 JavaScript 對象,,而不是 CSS 字符串;
          • 并且可以引用state中的狀態來設置相關的樣式;
          export default class App extends PureComponent {
            constructor(props) {
              super(props);
          
              this.state = {
                titleColor: "red"
              }
            }
          
            render() {
              return (
                <div>
                  <h2 style={{color: this.state.titleColor, fontSize: "20px"}}>我是App標題</h2>
                  <p style={{color: "green", textDecoration: "underline"}}>我是一段文字描述</p>
                </div>
              )
            }
          }

          內聯樣式的優點:

          • 1.內聯樣式, 樣式之間不會有沖突
          • 2.可以動態獲取當前state中的狀態

          內聯樣式的缺點:

          • 1.寫法上都需要使用駝峰標識
          • 2.某些樣式沒有提示
          • 3.大量的樣式, 代碼混亂
          • 4.某些樣式無法編寫(比如偽類/偽元素)

          所以官方依然是希望內聯合適和普通的css來結合編寫;

          1.2.2. 普通的css

          普通的css我們通常會編寫到一個單獨的文件。

          App.js中編寫React邏輯代碼:

          import React, { PureComponent } from 'react';
          
          import Home from './Home';
          
          import './App.css';
          
          export default class App extends PureComponent {
            render() {
              return (
                <div className="app">
                  <h2 className="title">我是App的標題</h2>
                  <p className="desc">我是App中的一段文字描述</p>
                  <Home/>
                </div>
              )
            }
          }

          App.css中編寫React樣式代碼:

          .title {
            color: red;
            font-size: 20px;
          }
          
          .desc {
            color: green;
            text-decoration: underline;
          }

          這樣的編寫方式和普通的網頁開發中編寫方式是一致的:

          • 如果我們按照普通的網頁標準去編寫,那么也不會有太大的問題;
          • 但是組件化開發中我們總是希望組件是一個獨立的模塊,即便是樣式也只是在自己內部生效,不會相互影響;
          • 但是普通的css都屬于全局的css,樣式之間會相互影響;

          比如編寫Home.js的邏輯代碼:

          import React, { PureComponent } from 'react';
          
          import './Home.css';
          
          export default class Home extends PureComponent {
            render() {
              return (
                <div className="home">
                  <h2 className="title">我是Home標題</h2>
                  <span className="desc">我是Home中的span段落</span>
                </div>
              )
            }
          }

          又編寫了Home.css的樣式代碼:

          .title {
            color: orange;
          }
          
          .desc {
            color: purple;
          }

          最終樣式之間會相互層疊,只有一個樣式會生效;

          1.2.3. css modules

          css modules并不是React特有的解決方案,而是所有使用了類似于webpack配置的環境下都可以使用的。

          但是,如果在其他項目中使用,那么我們需要自己來進行配置,比如配置webpack.config.js中的modules: true等。

          但是React的腳手架已經內置了css modules的配置:

          • .css/.less/.scss 等樣式文件都修改成 .module.css/.module.less/.module.scss 等;
          • 之后就可以引用并且進行使用了;

          使用的方式如下:

          css modules用法

          這種css使用方式最終生成的class名稱會全局唯一:

          生成的代碼結構

          css modules確實解決了局部作用域的問題,也是很多人喜歡在React中使用的一種方案。

          但是這種方案也有自己的缺陷:

          • 引用的類名,不能使用連接符(.home-title),在JavaScript中是不識別的;
          • 所有的className都必須使用{style.className} 的形式來編寫;
          • 不方便動態來修改某些樣式,依然需要使用內聯樣式的方式;

          如果你覺得上面的缺陷還算OK,那么你在開發中完全可以選擇使用css modules來編寫,并且也是在React中很受歡迎的一種方式。

          二. CSS in JS

          2.1. 認識CSS in JS

          實際上,官方文檔也有提到過CSS in JS這種方案:

          • “CSS-in-JS” 是指一種模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定義;
          • 注意此功能并不是 React 的一部分,而是由第三方庫提供。 React 對樣式如何定義并沒有明確態度;

          在傳統的前端開發中,我們通常會將結構(HTML)、樣式(CSS)、邏輯(JavaScript)進行分離。

          • 但是在前面的學習中,我們就提到過,React的思想中認為邏輯本身和UI是無法分離的,所以才會有了JSX的語法。
          • 樣式呢?樣式也是屬于UI的一部分;
          • 事實上CSS-in-JS的模式就是一種將樣式(CSS)也寫入到JavaScript中的方式,并且可以方便的使用JavaScript的狀態;
          • 所以React有被人稱之為 All in JS;

          當然,這種開發的方式也受到了很多的批評:

          • Stop using CSS in JavaScript for web development
          • https://hackernoon.com/stop-using-css-in-javascript-for-web-development-fa32fb873dcc

          批評聲音雖然有,但是在我們看來很多優秀的CSS-in-JS的庫依然非常強大、方便:

          • CSS-in-JS通過JavaScript來為CSS賦予一些能力,包括類似于CSS預處理器一樣的樣式嵌套、函數定義、邏輯復用、動態修改狀態等等;
          • 依然CSS預處理器也具備某些能力,但是獲取動態狀態依然是一個不好處理的點;
          • 所以,目前可以說CSS-in-JS是React編寫CSS最為受歡迎的一種解決方案;

          目前比較流行的CSS-in-JS的庫有哪些呢?

          • styled-components
          • emotion
          • glamorous

          目前可以說styled-components依然是社區最流行的CSS-in-JS庫,所以我們以styled-components的講解為主;

          安裝styled-components:

          yarn add styled-components

          2.2. styled-components

          2.2.1. 標簽模板字符串

          ES6中增加了模板字符串的語法,這個對于很多人來說都會使用。

          但是模板字符串還有另外一種用法:標簽模板字符串(Tagged Template Literals)。

          我們一起來看一個普通的JavaScript的函數:

          function foo(...args) {
            console.log(args);
          }
          
          foo("Hello World");

          正常情況下,我們都是通過 函數名() 方式來進行調用的,其實函數還有另外一種調用方式:

          foo`Hello World`; // [["Hello World"]]

          如果我們在調用的時候插入其他的變量:

          • 模板字符串被拆分了;
          • 第一個元素是數組,是被模塊字符串拆分的字符串組合;
          • 后面的元素是一個個模塊字符串傳入的內容;
          foo`Hello ${name}`; // [["Hello ", ""], "kobe"];

          在styled component中,就是通過這種方式來解析模塊字符串,最終生成我們想要的樣式的

          2.2.2. styled基本使用

          styled-components的本質是通過函數的調用,最終創建出一個組件:

          • 這個組件會被自動添加上一個不重復的class;
          • styled-components會給該class添加相關的樣式;

          比如我們正常開發出來的Home組件是這樣的格式:

          <div>
            <h2>我是Home標題</h2>
            <ul>
              <li>我是列表1</li>
              <li>我是列表2</li>
              <li>我是列表3</li>
            </ul>
          </div>

          我們希望給外層的div添加一個特殊的class,并且添加相關的樣式:

          styled-components基本使用

          另外,它支持類似于CSS預處理器一樣的樣式嵌套:

          • 支持直接子代選擇器或后代選擇器,并且直接編寫樣式;
          • 可以通過&符號獲取當前元素;
          • 直接偽類選擇器、偽元素等;
          const HomeWrapper = styled.div`
            color: purple;
          
            h2 {
              font-size: 50px;
            }
          
            ul > li {
              color: orange;
          
              &.active {
                color: red;
              }
          
              &:hover {
                background: #aaa;
              }
          
              &::after {
                content: "abc"
              }
            }
          `

          最終效果如下

          2.2.3. props、attrs屬性

          props可以穿透

          定義一個styled組件:

          const HYInput = styled.input`
            border-color: red;
          
            &:focus {
              outline-color: orange;
            }
          `

          使用styled的組件:

          <HYInput type="password"/>

          props可以被傳遞給styled組件

          <HomeWrapper color="blue">
          </HomeWrapper>

          使用時可以獲取到傳入的color:

          • 獲取props需要通過${}傳入一個插值函數,props會作為該函數的參數;
          • 這種方式可以有效的解決動態樣式的問題;
          const HomeWrapper = styled.div`
            color: ${props => props.color};
          }

          添加attrs屬性

          const HYInput = styled.input.attrs({
            placeholder: "請填寫密碼",
            paddingLeft: props => props.left || "5px"
          })`
            border-color: red;
            padding-left: ${props => props.paddingLeft};
          
            &:focus {
              outline-color: orange;
            }
          `

          2.2.4. styled高級特性

          支持樣式的繼承

          編寫styled組件

          const HYButton = styled.button`
            padding: 8px 30px;
            border-radius: 5px;
          `
          
          const HYWarnButton = styled(HYButton)`
            background-color: red;
            color: #fff;
          `
          
          const HYPrimaryButton = styled(HYButton)`
            background-color: green;
            color: #fff;
          `

          按鈕的使用

          <HYButton>我是普通按鈕</HYButton>
          <HYWarnButton>我是警告按鈕</HYWarnButton>
          <HYPrimaryButton>我是主要按鈕</HYPrimaryButton>

          styled設置主題

          在全局定制自己的主題,通過Provider進行共享:

          import { ThemeProvider } from 'styled-components';
          
          <ThemeProvider theme={{color: "red", fontSize: "30px"}}>
            <Home />
            <Profile />
          </ThemeProvider>

          在styled組件中可以獲取到主題的內容:

          const ProfileWrapper = styled.div`
            color: ${props => props.theme.color};
            font-size: ${props => props.theme.fontSize};
          `

          2.3. classnames

          vue中添加class

          在vue中給一個元素添加動態的class是一件非常簡單的事情:

          你可以通過傳入一個對象:

          <div
            class="static"
            v-bind:class="{ active: isActive, 'text-danger': hasError }"
          ></div>

          你也可以傳入一個數組:

          <div v-bind:class="[activeClass, errorClass]"></div>

          甚至是對象和數組混合使用:

          <div v-bind:class="[{ active: isActive }, errorClass]"></div>

          react中添加class

          React在JSX給了我們開發者足夠多的靈活性,你可以像編寫JavaScript代碼一樣,通過一些邏輯來決定是否添加某些class:

          import React, { PureComponent } from 'react'
          
          export default class App extends PureComponent {
            constructor(props) {
              super(props);
          
              this.state = {
                isActive: true
              }
            }
          
            render() {
              const {isActive} = this.state; 
          
              return (
                <div>
                  <h2 className={"title " + (isActive ? "active": "")}>我是標題</h2>
                  <h2 className={["title", (isActive ? "active": "")].join(" ")}>我是標題</h2>
                </div>
              )
            }
          }
          

          這個時候我們可以借助于一個第三方的庫:classnames

          • 很明顯,這是一個用于動態添加classnames的一個庫。

          我們來使用一下最常見的使用案例:

          classNames('foo', 'bar'); // => 'foo bar'
          classNames('foo', { bar: true }); // => 'foo bar'
          classNames({ 'foo-bar': true }); // => 'foo-bar'
          classNames({ 'foo-bar': false }); // => ''
          classNames({ foo: true }, { bar: true }); // => 'foo bar'
          classNames({ foo: true, bar: true }); // => 'foo bar'
          
          // lots of arguments of various types
          classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
          
          // other falsy values are just ignored
          classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'

          我是@半糖學前端 ,專注前端技術領域分享,關注我和我一起學習,共同進步!

          題中我們提出一個問題:react 代碼如何跑在小程序上?目前看來大致兩種思路:


          1. 把 react 代碼編譯成小程序代碼,這樣我們可以開發用 react,然后跑起來還是小程序原生代碼,結果很完美,但是把 react 代碼編譯成各個端的小程序代碼是一個力氣活,而且如果想用 vue 來開發的話,那么還需要做一遍 vue 代碼的編譯,這是 taro 1/2 的思路。


          2. 我們可以換個問題思考,react 代碼是如何跑在瀏覽器里的?

          • 站在瀏覽器的角度來思考:無論開發用的是什么框架,React 也好,Vue 也罷,最終代碼經過運行之后都是調用了瀏覽器的那幾個 BOM/DOM 的 API ,如:createElement、appendChild、removeChild 等。
          • Taro 3 主要通過在小程序端模擬實現 DOM、BOM API 來讓前端框架直接運行在小程序環境中。


          下面我們具體看看各自的實現。


          Taro 1/2

          Taro 1/2 的架構主要分為:編譯時 和 運行時。


          其中編譯時主要是將 Taro 代碼通過 Babel 轉換成 小程序的代碼,如:JS、WXML、WXSS、JSON。


          運行時主要是進行一些:生命周期、事件、data 等部分的處理和對接。


          Taro 編譯時

          Taro 的編譯,使用 babel-parser 將 Taro 代碼解析成抽象語法樹,然后通過 babel-types 對抽象語法樹進行一系列修改、轉換操作,最后再通過 babel-generate 生成對應的目標代碼。

          整個編譯時最復雜的部分在于 JSX 編譯。


          我們都知道 JSX 是一個 JavaScript 的語法擴展,它的寫法千變萬化,十分靈活。這里我們是采用 窮舉 的方式對 JSX 可能的寫法進行了一一適配,這一部分工作量很大,實際上 Taro 有大量的 Commit 都是為了更完善的支持 JSX 的各種寫法。


          Taro 運行時

          接下來,我們可以對比一下編譯后的代碼,可以發現,編譯后的代碼中,React 的核心 render 方法 沒有了。同時代碼里增加了 BaseComponent 和 createComponent ,它們是 Taro 運行時的核心。

          // 編譯前
          import Taro, { Component } from '@tarojs/taro'
          import { View, Text } from '@tarojs/components'
          import './index.scss'
          
          
          export default class Index extends Component {
          
          
            config={
              navigationBarTitleText: '首頁'
            }
          
          
            componentDidMount () { }
          
          
            render () {
              return (
                <View className=‘index' onClick={this.onClick}>
                  <Text>Hello world!</Text>
                </View>
              )
            }
          }
          
          
          // 編譯后
          import {BaseComponent, createComponent} from '@tarojs/taro-weapp'
          
          
          class Index extends BaseComponent {
          
          
          // ...
          
          
            _createDate(){
              //process state and props
            }
          }
          
          
          export default createComponent(Index)


          BaseComponent 主要是對 React 的一些核心方法:setState、forceUpdate 等進行了替換和重寫,結合前面編譯后 render 方法被替換,大家不難猜出:Taro 當前架構只是在開發時遵循了 React 的語法,在代碼編譯之后實際運行時,和 React 并沒有關系。


          而 createComponent 主要作用是調用 Component() 構建頁面;對接事件、生命周期等;進行 Diff Data 并調用 setData 方法更新數據。


          這樣的實現過程有三?缺點:

          • JSX ?持程度不完美。Taro 對 JSX 的?持是通過編譯時的適配去實現的,但 JSX ??常之靈活,因此還不能做到 100% ?持所有的 JSX 語法。JSX 是一個 JavaScript 的語法擴展,它的寫法千變萬化,十分靈活。之前Taro團隊是采用窮舉的方式對 JSX 可能的寫法進行了一一適配,這一部分工作量很大。
          • 不?持 source-map。Taro 對源代碼進?了?系列的轉換操作之后,就不?持 source-map 了,?戶 調試、使?這個項?就會不?便。
          • 維護和迭代?分困難。Taro 編譯時代碼?常的復雜且離散,維護迭代都?常的困難。


          Taro 3

          Taro 3 則可以大致理解為解釋型架構(相對于 Taro 1/2 而言),主要通過在小程序端模擬實現 DOM、BOM API 來讓前端框架直接運行在小程序環境中,從而達到小程序和 H5 統一的目的。


          而對于生命周期、組件庫、API、路由等差異,依然可以通過定義統一標準,各端負責各自實現的方式來進行抹平。


          而正因為 Taro 3 的原理,在 Taro 3 中同時支持 React、Vue 等框架,甚至還支持了 jQuery,還能支持讓開發者自定義地去拓展其他框架的支持,比如 Angular,Taro 3 整體架構如下:



          模擬實現 DOM、BOM API

          Taro 3 創建了 taro-runtime 的包,然后在這個包中實現了 一套 高效、精簡版的 DOM/BOM API(下面的 UML 圖只是反映了幾個主要的類的結構和關系):



          • TaroEventTarget類,實現addEventListener和removeEventListener。
          • TaroNode類繼承TaroEventTarget類,主要實現insertBefore、appendChild等操作 Dom 節點的方法。下面在頁面渲染我們會具體看這幾個方法的實現。
          • TaroElement類繼承TaroNode類,主要是節點屬性相關的方法和dispatchEvent方法,dispatchEvent方法在下面講事件觸發的時候也會涉及到。
          • TaroRootElement類繼承TaroElement類,其中最主要是enqueueUpdate和performUpdate,把虛擬 DOM setData 成小程序 data 的操作就是這兩個函數。


          然后,我們通過 Webpack 的 ProvidePlugin 插件,注入到小程序的邏輯層。


          Webpack ProvidePlugin 是一個 webpack 自帶的插件,用于在每個模塊中自動加載模塊,而無需使用 import/require 調用。該插件可以將全局變量注入到每個模塊中,避免在每個模塊中重復引用相同的依賴。

          // trao-mini-runner/src/webpack/build.conf.ts
          plugin.providerPlugin=getProviderPlugin({
            window: ['@tarojs/runtime', 'window'],
            document: ['@tarojs/runtime', 'document'],
            navigator: ['@tarojs/runtime', 'navigator'],
            requestAnimationFrame: ['@tarojs/runtime', 'requestAnimationFrame'],
            cancelAnimationFrame: ['@tarojs/runtime', 'cancelAnimationFrame'],
            Element: ['@tarojs/runtime', 'TaroElement'],
            SVGElement: ['@tarojs/runtime', 'SVGElement'],
            MutationObserver: ['@tarojs/runtime', 'MutationObserver'],
            history: ['@tarojs/runtime', 'history'],
            location: ['@tarojs/runtime', 'location'],
            URLSearchParams: ['@tarojs/runtime', 'URLSearchParams'],
            URL: ['@tarojs/runtime', 'URL'],
          })
          
          
          // trao-mini-runner/src/webpack/chain.ts
          export const getProviderPlugin=args=> {
            return partial(getPlugin, webpack.ProvidePlugin)([args])
          }


          這樣,在小程序的運行時,就有了 一套高效、精簡版的 DOM/BOM API。


          taro-react:小程序版的 react-dom

          在 DOM/BOM 注入之后,理論上來說,react 就可以直接運行了。


          但是因為 React-DOM 包含大量瀏覽器兼容類的代碼,導致包太大。Taro 自己實現了 react 的自定義渲染器,代碼在taro-react包里。


          在 React 16+ ,React 的架構如下:


          最上層是 React 的核心部分 react-core ,中間是 react-reconciler,其的職責是維護 VirtualDOM 樹,內部實現了 Diff/Fiber 算法,決定什么時候更新、以及要更新什么。


          而 Renderer 負責具體平臺的渲染工作,它會提供宿主組件、處理事件等等。例如 React-DOM 就是一個渲染器,負責 DOM 節點的渲染和 DOM 事件處理。


          Taro實現了taro-react 包,用來連接 react-reconciler 和 taro-runtime 的 BOM/DOM API。是基于 react-reconciler 的小程序專用 React 渲染器,連接 @tarojs/runtime的DOM 實例,相當于小程序版的react-dom,暴露的 API 也和react-dom 保持一致。


          這里涉及到一個問題:如何自定義 React 渲染器?


          第一步: 實現宿主配置( 實現react-reconciler的hostConfig配置)

          這是react-reconciler要求宿主提供的一些適配器方法和配置項。這些配置項定義了如何創建節點實例、構建節點樹、提交和更新等操作。即在 hostConfig 的方法中調用對應的 Taro BOM/DOM 的 API。


          1. 創建形操作

          createInstance(type,newProps,rootContainerInstance,_currentHostContext,workInProgress)。


          react-reconciler 使用該方法可以創建對應目標平臺的UI Element實例。比如 document.createElement 根據不同類型來創建 div、img、h2等DOM節點,并使用 newProps參數給創建的節點賦予屬性。而在 Taro 中:

          import { document } from '@tarojs/runtime'
          // 在 ReactDOM 中會調用 document.createElement 來生成 dom,
          // 而在小程序環境中 Taro 中模擬了 document,
          // 直接返回 `document.createElement(type)` 即可
          createInstance (type, props: Props, _rootContainerInstance: any, _hostContext: any, internalInstanceHandle: Fiber) {
            const element=document.createElement(type)
          
          
            precacheFiberNode(internalInstanceHandle, element)
            updateFiberProps(element, props)
          
          
            return element
          },


          createTextInstance

          如果目標平臺允許創建純文本節點。那么這個方法就是用來創建目標平臺的文本節點。

          import { document } from '@tarojs/runtime'
          // Taro: 模擬的 document 支持創建 text 節點, 返回 `document.createTextNode(text)` 即可.
          createTextInstance (text: string, _rootContainerInstance: any, _hostContext: any, internalInstanceHandle: Fiber) {
            const textNode=document.createTextNode(text)
          
          
            precacheFiberNode(internalInstanceHandle, textNode)
          
          
            return textNode
          },


          2. UI樹操作

          appendInitialChild(parent, child)

          初始化UI樹創建。

          // Taro: 直接 parentInstance.appendChild(child) 即可
          appendInitialChild (parent, child) {
            parent.appendChild(child)
          },


          appendChild(parent, child)

          此方法映射為 domElement.appendChild 。

          appendChild (parent, child) {
            parent.appendChild(child)
          },


          3. 更新prop操作

          finalizeInitialChildren

          finalizeInitialChildren 在組件掛載到頁面中前調用,更新時不會調用。


          這個方法我們下面事件注冊時還會提到。

          finalizeInitialChildren (dom, type: string, props: any) {
            updateProps(dom, {}, props) 
            // 提前執行更新屬性操作,Taro 在 Page 初始化后會立即從 dom 讀取必要信息
          
          
            // ....
          },


          prepareUpdate(domElement, oldProps, newProps)

          這里是比較oldProps,newProps的不同,用來判斷是否要更新節點。

          prepareUpdate (instance, _, oldProps, newProps) {
            return getUpdatePayload(instance, oldProps, newProps)
          },
          
          
          // ./props.ts
          export function getUpdatePayload (dom: TaroElement, oldProps: Props, newProps: Props){
            let i: string
            let updatePayload: any[] | null=null
          
          
            for (i in oldProps) {
              if (!(i in newProps)) {
                (updatePayload=updatePayload || []).push(i, null)
              }
            }
            const isFormElement=dom instanceof FormElement
            for (i in newProps) {
              if (oldProps[i] !==newProps[i] || (isFormElement && i==='value')) {
                (updatePayload=updatePayload || []).push(i, newProps[i])
              }
            }
          
          
            return updatePayload
          }


          commitUpdate(domElement, updatePayload, type, oldProps, newProps)

          此函數用于更新domElement屬性,下文要講的事件注冊就是在這個方法里。

          // Taro: 根據 updatePayload,將屬性更新到 instance 中,
          // 此時 updatePayload 是一個類似 `[prop1, value1, prop2, value2, ...]` 的數組
          
          
          commitUpdate (dom, updatePayload, _, oldProps, newProps) {
            updatePropsByPayload(dom, oldProps, updatePayload)
            updateFiberProps(dom, newProps)
          },
          
          
          export function updatePropsByPayload (dom: TaroElement, oldProps: Props, updatePayload: any[]){
            for(let i=0; i < updatePayload.length; i +=2){ // key, value 成對出現
              const key=updatePayload[i]; 
              const newProp=updatePayload[i+1]; 
              const oldProp=oldProps[key]
              setProperty(dom, key, newProp, oldProp)
            }
          }
          function setProperty (dom: TaroElement, name: string, value: unknown, oldValue?: unknown) {
            name=name==='className' ? 'class' : name
          
          
            if (
              name==='key' ||
              name==='children' ||
              name==='ref'
          ) {
              // skip
            } else if (name==='style') {
              const style=dom.style
              if (isString(value)) {
                style.cssText=value
              } else {
                if (isString(oldValue)) {
                  style.cssText=''
                  oldValue=null
                }
          
          
                if (isObject<StyleValue>(oldValue)) {
                  for (const i in oldValue) {
                    if (!(value && i in (value as StyleValue))) {
                      setStyle(style, i, '')
                    }
                  }
                }
          
          
                if (isObject<StyleValue>(value)) {
                  for (const i in value) {
                    if (!oldValue || value[i] !==(oldValue as StyleValue)[i]) {
                      setStyle(style, i, value[i])
                    }
                  }
                }
              }
            } else if (isEventName(name)) {
              setEvent(dom, name, value, oldValue)
            } else if (name==='dangerouslySetInnerHTML') {
              const newHtml=(value as DangerouslySetInnerHTML)?.__html ?? ''
              const oldHtml=(oldValue as DangerouslySetInnerHTML)?.__html ?? ''
              if (newHtml || oldHtml) {
                if (oldHtml !==newHtml) {
                  dom.innerHTML=newHtml
                }
              }
            } else if (!isFunction(value)) {
              if (value==null) {
                dom.removeAttribute(name)
              } else {
                dom.setAttribute(name, value as string)
              }
            }
          }


          上面是hostConfig里必要的回調函數的實現,源碼里還有很多回調函數的實現,詳見trao-react源碼。


          第二步:實現渲染函數,類似于ReactDOM.render() 方法。可以看成是創建 Taro DOM Tree 容器的方法。

          源碼實現詳見trao-react/src/render.ts。

          export function render (element: ReactNode, domContainer: TaroElement, cb: Callback) {
            const root=new Root(TaroReconciler, domContainer)
            return root.render(element, cb)
          }
          export function createRoot (domContainer: TaroElement, options: CreateRootOptions={}) {
            // options should be an object
            const root=new Root(TaroReconciler, domContainer, options)
            // ......
            return root
          }
          class Root {
            public constructor (renderer: Renderer, domContainer: TaroElement, options?: CreateRootOptions) {
              this.renderer=renderer
              this.initInternalRoot(renderer, domContainer, options)
            }
            private initInternalRoot (renderer: Renderer, domContainer: TaroElement, options?: CreateRootOptions) {
              // .....
              this.internalRoot=renderer.createContainer(
                containerInfo,
                tag,
                null, // hydrationCallbacks
                isStrictMode,
                concurrentUpdatesByDefaultOverride,
                identifierPrefix,
                onRecoverableError,
                transitionCallbacks
              )
            }
            public render (children: ReactNode, cb: Callback) {
              const { renderer, internalRoot }=this
              renderer.updateContainer(children, internalRoot, null, cb)
              return renderer.getPublicRootInstance(internalRoot)
            }
          }


          而 Root 類最后調用TaroReconciler的createContainr``updateContainer和 getPublicRootInstance 方法,實際上就是react-reconciler包里面對應的方法。


          渲染函數是在什么時候被調用的呢?

          在編譯時,會引入插件taro-plugin-react, 插件內會調用 modifyMiniWebpackChain=> setAlias。

          // taro-plugin-react/src/webpack.mini.ts
          function setAlias (ctx: IPluginContext, framework: Frameworks, chain) {
            if (framework==='react') {
              alias.set('react-dom$', '@tarojs/react')
            }
          }


          這樣ReactDOM.createRoot和ReactDOM.render實際上調用的就是trao-react的createRoot和render方法。


          經過上面的步驟,React 代碼實際上就可以在小程序的運行時正常運行了,并且會生成 Taro DOM Tree。那么偌大的 Taro DOM Tree 怎樣更新到頁面呢?


          從虛擬 Dom 到小程序頁面渲染

          因為?程序并沒有提供動態創建節點的能?,需要考慮如何使?相對靜態的 wxml 來渲染相對動態的 Taro DOM 樹。Taro使?了模板拼接的?式,根據運?時提供的 DOM 樹數據結構,各 templates 遞歸地 相互引?,最終可以渲染出對應的動態 DOM 樹。


          模版化處理

          首先,將小程序的所有組件挨個進行模版化處理,從而得到小程序組件對應的模版。如下圖就是小程序的 view 組件模版經過模版化處理后的樣子。?先需要在 template ??寫?個 view,把它所有的屬性全部列出來(把所有的屬性都列出來是因為?程序??不能去動態地添加屬性)。


          模板化處理的核心代碼在 packages/shared/src/template.ts 文件中。會在編譯工程中生成 base.wxml文件,這是我們打包產物之一。

          // base.wxml
          <wxs module="xs" src="./utils.wxs" />
          <template name="taro_tmpl">
            <block wx:for="{{root.cn}}" wx:key="sid">
              // tmpl_' + 0 + '_' + 2
              <template is="{{xs.a(0, item.nn, '')}}" data="{{i:item,c:1,l:''}}" />
            </block>
          </template>
          ....
          <template name="tmpl_0_2">
            <view style="{{i.st}}" class="{{i.cl}}"  id="{{i.uid||i.sid}}" data-sid="{{i.sid}}">
              <block wx:for="{{i.cn}}" wx:key="sid">
                <template is="{{xs.a(c, item.nn, l)}}" data="{{i:item,c:c+1,l:xs.f(l,item.nn)}}" />
              </block>
            </view>
          </template>


          打包產生的頁面代碼是這樣的:

          // pages/index/index.wxml
          <import src="../../base.wxml"/>
          <template is="taro_tmpl" data="{{root:root}}" />


          接下來是遍歷渲染所有?節點,基于組件的 template,動態 “遞歸” 渲染整棵樹。


          具體流程為先去遍歷 Taro DOM Tree 根節點的子元素,再根據每個子元素的類型選擇對應的模板來渲染子元素,然后在每個模板中我們又會去遍歷當前元素的子元素,以此把整個節點樹遞歸遍歷出來。


          hydrate Data

          而動態遞歸時需要獲取到我們的 data,也就是 root。


          首先,在 createPageConfig 中會對 config.data 進行初始化,賦值 {root:{cn:[]}}。

          export function createPageConfig (component: any, pageName?: string, data?: Record<string, unknown>, pageConfig?: PageConfig) {
            // .......
            if (!isUndefined(data)) {
              config.data=data
            }
            // .......
          }


          React在commit階段會調用HostConfig里的appendInitialChild方法完成頁面掛載,在Taro中則繼續調用:appendInitialChild —> appendChild —> insertBefore —> enqueueUpdate。

          // taro-react/src/reconciler.ts
          appendInitialChild (parent, child) {
            parent.appendChild(child)
          },
          appendChild (parent, child) {
            parent.appendChild(child)
          },
          // taro-runtime/src/dom/node.ts
          public appendChild (newChild: TaroNode) {
            return this.insertBefore(newChild)
          }
          public insertBefore<T extends TaroNode> (newChild: T, refChild?: TaroNode | null, isReplace?: boolean): T {
            // 忽略了大部分代碼
            this.enqueueUpdate({
              path: newChild._path,
              value: this.hydrate(newChild)
            })
          
          
            return newChild
          }


          這里看到最終調用enqueueUpdate方法,傳入一個對象,值為 path 和 value,而 value 值是hydrate方法的結果。


          hydrate方法我們可以翻譯成“注水”,函數 hydrate 用于將虛擬 DOM(TaroElement 或 TaroText)轉換為小程序組件渲染所需的數據格式(MiniData)。


          回想一下小程序員生的 data 里都是我們頁面需要的 state,而 taro 的hydrate方法返回的 miniData 是把 state 外面在包裹上我們頁面的 node 結構值。舉例來看,我們一個 helloword 代碼所hydrate的 miniData 如下(可以在小程序IDE中的 ”AppData“ 標簽欄中查看到完整的data數據結構):

          {
            "root": {
              "cn": [
                {
                  "cl": "index",
                  "cn": [
                    {
                      "cn": [
                        {
                          "nn": "8",
                          "v": "Hello world!"
                        }
                      ],
                      "nn": "4",
                      "sid": "_AH"
                    },
                    {
                      "cn": [
                        {
                          "nn": "8",
                          "v": "HHHHHH"
                        }
                      ],
                      "nn": "2",
                      "sid": "_AJ"
                    },
                    {
                      "cl": "blue",
                      "cn": [
                        {
                          "nn": "8",
                          "v": "Page bar: "
                        },
                        {
                          "cl": "red",
                          "cn": [
                            {
                              "nn": "8",
                              "v": "red"
                            }
                          ],
                          "nn": "4",
                          "sid": "_AM"
                        }
                      ],
                      "nn": "4",
                      "sid": "_AN"
                    }
                  ],
                  "nn": "2",
                  "sid": "_AO"
                }
              ],
              "uid": "pages/index/index?$taroTimestamp=1691064929701"
            },
            "__webviewId__": 1
          }


          這里的字段含義解釋一下 :(我想這里縮寫是可能盡可能讓每一次setData的內容更小。)

          Container='container',
          Childnodes='cn',
          Text='v',
          NodeType='nt',
          NodeName='nn',
          
          
          // Attrtibutes
          Style='st',
          Class='cl',
          Src='src


          我們獲取到以上的 data 數據,去執行enqueueUpdate函數,enqueueUpdate函數內部執行performUpdate函數,performUpdate函數最終執行 ctx.setData,ctx 是小程序的實例,也就是執行我們熟悉的 setData 方法把上面hydrate的 miniData賦值給 root,這樣就渲染了小程序的頁面數據。

          // taro-runtime/src/dom/root.ts
          public enqueueUpdate (payload: UpdatePayload): void {
            this.updatePayloads.push(payload)
          
          
            if (!this.pendingUpdate && this.ctx) {
              this.performUpdate()
            }
          }
          
          
          public performUpdate (initRender=false, prerender?: Func) {
            // .....
            while (this.updatePayloads.length > 0) {
              const { path, value }=this.updatePayloads.shift()!
              if (path.endsWith(Shortcuts.Childnodes)) {
                resetPaths.add(path)
              }
              data[path]=value
            }
            // .......
            if (initRender) {
              // 初次渲染,使用頁面級別的 setData
              normalUpdate=data
            }
            // ........
            ctx.setData(normalUpdate, cb)
          }


          整體流程可以概括為:當在React中調用 this.setState 時,React內部會執行reconciler,進而觸發 enqueueUpdate 方法,如下圖:



          事件處理

          事件注冊

          在HostConfig接口中,有一個方法 finalizeInitialChildren,在這個方法里會調用updateProps。這是掛載頁面階段時間的注冊時機。updateProps 會調用 updatePropsByPayload 方法。

          finalizeInitialChildren (dom, type: string, props: any) {
            updateProps(dom, {}, props)
          
          
            //....
          },


          在HostConfig接口中,有一個方法 commitUpdate,用于在react的commit階段更新屬性:

          commitUpdate (dom, updatePayload, _, oldProps, newProps) {
            updatePropsByPayload(dom, oldProps, updatePayload)
            updateFiberProps(dom, newProps)
          },


          進一步的調用方法:updatePropsByPayload=> setProperty=> setEvent。

          // taro-react/src/props.ts
          function setEvent (dom: TaroElement, name: string, value: unknown, oldValue?: unknown) {
            const isCapture=name.endsWith('Capture')
            let eventName=name.toLowerCase().slice(2)
            if (isCapture) {
              eventName=eventName.slice(0, -7)
            }
          
          
            const compName=capitalize(toCamelCase(dom.tagName.toLowerCase()))
          
          
            if (eventName==='click' && compName in internalComponents) {
              eventName='tap'
            }
            // 通過addEventListener將事件注冊到dom中
            if (isFunction(value)) {
              if (oldValue) {
                dom.removeEventListener(eventName, oldValue as any, false)
                dom.addEventListener(eventName, value, { isCapture, sideEffect: false })
              } else {
                dom.addEventListener(eventName, value, isCapture)
              }
            } else {
              dom.removeEventListener(eventName, oldValue as any)
            }
          }


          進一步的看看dom.addEventListener做了什么?addEventListener是類TaroEventTarget的方法:

          export class TaroEventTarget {
            public __handlers: Record<string, EventHandler[]>={}
          
          
            public addEventListener (type: string, handler: EventHandler, options?: boolean | AddEventListenerOptions) {
              type=type.toLowerCase()
          
          
              // 省略很多代碼
          
          
              const handlers=this.__handlers[type]
              if (isArray(handlers)) {
                handlers.push(handler)
              } else {
                this.__handlers[type]=[handler]
              }
            }
          }


          可以看到事件會注冊到dom對象上,最終會放入到 dom 內部變量 __handlers 中保存。


          事件觸發

          // base.wxml
          <template name="tmpl_0_7">
            <view
              hover-class="{{xs.b(i.p1,'none')}}"
              hover-stop-propagation="{{xs.b(i.p4,!1)}}"
              hover-start-time="{{xs.b(i.p2,50)}}"
              hover-stay-time="{{xs.b(i.p3,400)}}"
              bindtouchstart="eh"
              bindtouchmove="eh"
              bindtouchend="eh"
              bindtouchcancel="eh"
              bindlongpress="eh"
              animation="{{i.p0}}"
              bindanimationstart="eh"
              bindanimationiteration="eh"
              bindanimationend="eh"
              bindtransitionend="eh"
              style="{{i.st}}"
              class="{{i.cl}}"
              bindtap="eh"
              id="{{i.uid||i.sid}}"
              data-sid="{{i.sid}}"
            >
              <block wx:for="{{i.cn}}" wx:key="sid">
                <template is="{{xs.a(c, item.nn, l)}}" data="{{i:item,c:c+1,l:xs.f(l,item.nn)}}" />
              </block>
            </view>
          </template>


          上面是base.wxml其中的一個模板,可以看到,所有組件中的事件都會由 eh 代理。在createPageConfig時,會將 config.eh 賦值為 eventHandler。

          // taro-runtime/src/dsl/common.ts
          function createPageConfig(){
              const config={...} // config會作為小程序 Page() 的入參
              config.eh=eventHandler
              config.data={root:{cn:[]}}
              return config
          }


          eventHandler 最終會觸發 dom.dispatchEvent(e)。

          // taro-runtime/src/dom/element.ts
          class TaroElement extends TaroNode {
              dispatchEvent(event){
                  const listeners=this.__handlers[event.type]  // 取出回調函數數組
                  for (let i=listeners.length; i--;) {
                      result=listener.call(this, event)  // event是TaroEvent實例
                  }
              }
          }


          至此,react 代碼終于是可以完美運行在小程序環境中。


          還要提到一點的是,Taro3 在 h5 端的實現也很有意思,Taro在 H5 端實現一套基于小程序規范的組件庫和 API 庫,在這里就不展開說了。


          總結

          Taro 3從之前的重編譯時,到現在的重運行時,解決了架構問題,可以用 react、vue 甚至 jQuery 來寫小程序,但也帶來了一些性能問題。


          為了解決性能問題,Taro 3 也提供了預渲染和虛擬列表等功能和組件。


          但從長遠來看,計算機硬件的性能越來越冗余,如果在犧牲一點可以容忍的性能的情況下換來整個框架更大的靈活性和更好的適配性,并且能夠極大的提升開發體驗,也是值得的。

          作者:孟祥輝

          來源:微信公眾號:哈啰技術

          出處:https://mp.weixin.qq.com/s/134VAXPJczElvdYzNFcHhA


          主站蜘蛛池模板: 亚洲视频一区调教| 免费一区二区三区四区五区| 国产高清在线精品一区| 日本免费一区二区三区四区五六区| 看电影来5566一区.二区| 中文字幕一区在线播放| 精品乱人伦一区二区三区| 日韩电影一区二区| 亚洲一区二区三区在线观看网站| 亚洲AV无码国产精品永久一区| 日韩久久精品一区二区三区| 国产成人精品一区二区三区| 日韩精品在线一区二区| 亚洲狠狠狠一区二区三区| 精品国产一区二区三区av片| 精品人妻少妇一区二区| 蜜桃臀无码内射一区二区三区| 亚洲av日韩综合一区二区三区| 丝袜美腿高跟呻吟高潮一区| 国产精品无码一区二区三级 | AA区一区二区三无码精片| 成人精品一区二区三区校园激情| 无码人妻一区二区三区免费视频 | 国产免费av一区二区三区| 精彩视频一区二区| 亚洲AV色香蕉一区二区| 视频一区视频二区制服丝袜| 国产自产V一区二区三区C| 日本精品无码一区二区三区久久久| 日本一区二区在线不卡| 亚洲综合一区二区| 亚洲一区二区三区在线视频| 国内自拍视频一区二区三区 | 亚洲一区二区中文| 国产精品亚洲一区二区三区久久 | 亚洲国产精品一区第二页| 日韩精品一区二区三区国语自制| 久久亚洲国产精品一区二区| 一区二区手机视频| 国产精品一区视频| 性色AV一区二区三区|