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

資訊專欄INFORMATION COLUMN

終極蛇皮上帝視角之微信小程序之告別 setData

wuyumin / 1782人閱讀

摘要:而小程序官方的是在中調(diào)用方法來改變數(shù)據(jù),從而改變界面。為了寫測試讓咱們來重構(gòu)一把,利用學(xué)習(xí)過的函數(shù)式編程中的高階函數(shù)把依賴注入。也就是說當(dāng)中的某個(gè)數(shù)據(jù)更新的時(shí)候,我們并不知道它會(huì)影響哪個(gè)中的屬性,特別的還有依賴于的情況。

眾所周知 Vue 是借助 ES5 的 Object.defineProperty 方法設(shè)置 getter、setter 達(dá)到數(shù)據(jù)驅(qū)動(dòng)界面,當(dāng)然其中還有模板編譯等等其他過程。

而小程序官方的 api 是在 Page 中調(diào)用 this.setData 方法來改變數(shù)據(jù),從而改變界面。

那么假如我們將兩者結(jié)合一下,將 this.setData 封裝起來,豈不是可以像開發(fā) Vue 應(yīng)用一樣地使用 this.foo = "hello" 來開發(fā)小程序了?

更進(jìn)一步地,可以實(shí)現(xiàn) h5 和小程序 js 部分代碼的同構(gòu)

更進(jìn)一步地,增加模板編譯和解析就可以連 wxml/html 部分也同構(gòu)

更進(jìn)一步地,兼容 RN/Weex/快應(yīng)用

更進(jìn)一步地,世界大同,天下為公,前端工程師全部失業(yè)...23333

0.源碼地址

github 地址

1.綁定簡單屬性

第一步我們先定一個(gè)小目標(biāo):掙他一個(gè)億!??!

對(duì)于簡單非嵌套屬性(非對(duì)象,數(shù)組),直接對(duì)其賦值就能改變界面。


msg: {{ msg }}
// index.js
TuaPage({
    data () {
        return {
            msg: "hello world",
        }
    },
    methods: {
        tapMsg () {
            this.msg = this.reverseStr(this.msg)
        },
        reverseStr (str) {
            return str.split("").reverse().join("")
        },
    },
})

這一步很簡單啦,直接對(duì)于 data 中的每個(gè)屬性都綁定下 getter、setter,在 setter 中調(diào)用下 this.setData 就好啦。

/**
 * 將 source 上的屬性代理到 target 上
 * @param {Object} source 被代理對(duì)象
 * @param {Object} target 被代理目標(biāo)
 */
const proxyData = (source, target) => {
    Object.keys(source).forEach((key) => {
        Object.defineProperty(
            target,
            key,
            Object.getOwnPropertyDescriptor(source, key)
        )
    })
}

/**
 * 遍歷觀察 vm.data 中的所有屬性,并將其直接掛到 vm 上
 * @param {Page|Component} vm Page 或 Component 實(shí)例
 */
const bindData = (vm) => {
    const defineReactive = (obj, key, val) => {
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get () { return val },
            set (newVal) {
                if (newVal === val) return

                val = newVal
                vm.setData($data)
            },
        })
    }

    /**
     * 觀察對(duì)象
     * @param {any} obj 待觀察對(duì)象
     * @return {any} 已被觀察的對(duì)象
     */
    const observe = (obj) => {
        const observedObj = Object.create(null)

        Object.keys(obj).forEach((key) => {
            // 過濾 __wxWebviewId__ 等內(nèi)部屬性
            if (/^__.*__$/.test(key)) return

            defineReactive(
                observedObj,
                key,
                obj[key]
            )
        })

        return observedObj
    }

    const $data = observe(vm.data)

    vm.$data = $data
    proxyData($data, vm)
}

/**
 * 適配 Vue 風(fēng)格代碼,使其支持在小程序中運(yùn)行(告別不方便的 setData)
 * @param {Object} args Page 參數(shù)
 */
export const TuaPage = (args = {}) => {
    const {
        data: rawData = {},
        methods = {},
        ...rest
    } = args

    const data = typeof rawData === "function"
        ? rawData()
        : rawData

    Page({
        ...rest,
        ...methods,
        data,
        onLoad (...options) {
            bindData(this)

            rest.onLoad && rest.onLoad.apply(this, options)
        },
    })
}
2.綁定嵌套對(duì)象
那么如果數(shù)據(jù)是嵌套的對(duì)象咋辦咧?

其實(shí)也很簡單,咱們遞歸觀察一下就好。


a.b: {{ a.b }}
// index.js
TuaPage({
    data () {
        return {
            a: { b: "this is b" },
        }
    },
    methods: {
        tapAB () {
            this.a.b = this.reverseStr(this.a.b)
        },
        reverseStr (str) {
            return str.split("").reverse().join("")
        },
    },
})

observe -> observeDeep:在 observeDeep 中判斷是對(duì)象就遞歸觀察下去。

// ...

/**
 * 遞歸觀察對(duì)象
 * @param {any} obj 待觀察對(duì)象
 * @return {any} 已被觀察的對(duì)象
 */
const observeDeep = (obj) => {
    if (typeof obj === "object") {
        const observedObj = Object.create(null)

        Object.keys(obj).forEach((key) => {
            if (/^__.*__$/.test(key)) return

            defineReactive(
                observedObj,
                key,
                // -> 注意在這里遞歸
                observeDeep(obj[key]),
            )
        })

        return observedObj
    }

    // 簡單屬性直接返回
    return obj
}

// ...
3.劫持?jǐn)?shù)組方法

大家都知道,Vue 劫持了一些數(shù)組方法。咱們也來依葫蘆畫瓢地實(shí)現(xiàn)一下~

/**
 * 劫持?jǐn)?shù)組的方法
 * @param {Array} arr 原始數(shù)組
 * @return {Array} observedArray 被劫持方法后的數(shù)組
 */
const observeArray = (arr) => {
    const observedArray = arr.map(observeDeep)

    ;[
        "pop",
        "push",
        "sort",
        "shift",
        "splice",
        "unshift",
        "reverse",
    ].forEach((method) => {
        const original = observedArray[method]

        observedArray[method] = function (...args) {
            const result = original.apply(this, args)
            vm.setData($data)

            return result
        }
    })

    return observedArray
}
其實(shí),Vue 還做了個(gè)優(yōu)化,如果當(dāng)前環(huán)境有 __proto__ 屬性,那么就把以上方法直接加到數(shù)組的原型鏈上,而不是對(duì)每個(gè)數(shù)組數(shù)據(jù)的方法進(jìn)行修改。
4.實(shí)現(xiàn) computed 功能

computed 功能日常還蠻常用的,通過已有的 data 元數(shù)據(jù),派生出一些方便的新數(shù)據(jù)。

要實(shí)現(xiàn)的話,因?yàn)?computed 中的數(shù)據(jù)都定義成函數(shù),所以其實(shí)直接將其設(shè)置為 getter 就行啦。

/**
 * 將 computed 中定義的新屬性掛到 vm 上
 * @param {Page|Component} vm Page 或 Component 實(shí)例
 * @param {Object} computed 計(jì)算屬性對(duì)象
 */
const bindComputed = (vm, computed) => {
    const $computed = Object.create(null)

    Object.keys(computed).forEach((key) => {
        Object.defineProperty($computed, key, {
            enumerable: true,
            configurable: true,
            get: computed[key].bind(vm),
            set () {},
        })
    })

    proxyData($computed, vm)

    // 掛到 $data 上,這樣在 data 中數(shù)據(jù)變化時(shí)可以一起被 setData
    proxyData($computed, vm.$data)

    // 初始化
    vm.setData($computed)
}
5.實(shí)現(xiàn) watch 功能

接下來又是一個(gè)炒雞好用的 watch 功能,即監(jiān)聽 datacomputed 中的數(shù)據(jù),在其變化的時(shí)候調(diào)用回調(diào)函數(shù),并傳入 newValoldVal。

const defineReactive = (obj, key, val) => {
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get () { return val },
        set (newVal) {
            if (newVal === val) return

            // 這里保存 oldVal
            const oldVal = val
            val = newVal
            vm.setData($data)

            // 實(shí)現(xiàn) watch data 屬性
            const watchFn = watch[key]
            if (typeof watchFn === "function") {
                watchFn.call(vm, newVal, oldVal)
            }
        },
    })
}

const bindComputed = (vm, computed, watch) => {
    const $computed = Object.create(null)

    Object.keys(computed).forEach((key) => {
        // 這里保存 oldVal
        let oldVal = computed[key].call(vm)

        Object.defineProperty($computed, key, {
            enumerable: true,
            configurable: true,
            get () {
                const newVal = computed[key].call(vm)

                // 實(shí)現(xiàn) watch computed 屬性
                const watchFn = watch[key]
                if (typeof watchFn === "function" && newVal !== oldVal) {
                    watchFn.call(vm, newVal, oldVal)
                }

                // 重置 oldVal
                oldVal = newVal

                return newVal
            },
            set () {},
        })
    })

    // ...
}

看似不錯(cuò),實(shí)則不然。

咱們現(xiàn)在碰到了一個(gè)問題:如何監(jiān)聽類似 "a.b" 這樣的嵌套數(shù)據(jù)?

這個(gè)問題的原因在于我們在遞歸遍歷數(shù)據(jù)的時(shí)候沒有記錄下路徑。

6.記錄路徑

解決這個(gè)問題并不難,其實(shí)我們只要在遞歸觀察的每一步中傳遞 key 即可,注意對(duì)于數(shù)組中的嵌套元素傳遞的是 [${index}]。

并且一旦我們知道了數(shù)據(jù)的路徑,還可以進(jìn)一步提高 setData 的性能。

因?yàn)槲覀兛梢跃?xì)地調(diào)用 vm.setData({ [prefix]: newVal }) 修改其中的部分?jǐn)?shù)據(jù),而不是將整個(gè) $datasetData。

const defineReactive = (obj, key, val, path) => {
    Object.defineProperty(obj, key, {
        // ...
        set (newVal) {
            // ...

            vm.setData({
                // 因?yàn)椴恢酪蕾囁愿抡麄€(gè) computed
                ...vm.$computed,
                // 直接修改目標(biāo)數(shù)據(jù)
                [path]: newVal,
            })

            // 通過路徑來找 watch 目標(biāo)
            const watchFn = watch[path]
            if (typeof watchFn === "function") {
                watchFn.call(vm, newVal, oldVal)
            }
        },
    })
}

const observeArray = (arr, path) => {
    const observedArray = arr.map(
        // 注意這里的路徑拼接
        (item, idx) => observeDeep(item, `${path}[${idx}]`)
    )

    ;[
        "pop",
        "push",
        "sort",
        "shift",
        "splice",
        "unshift",
        "reverse",
    ].forEach((method) => {
        const original = observedArray[method]

        observedArray[method] = function (...args) {
            const result = original.apply(this, args)

            vm.setData({
                // 因?yàn)椴恢酪蕾囁愿抡麄€(gè) computed
                ...vm.$computed,
                // 直接修改目標(biāo)數(shù)據(jù)
                [path]: observedArray,
            })

            return result
        }
    })

    return observedArray
}

const observeDeep = (obj, prefix = "") => {
    if (Array.isArray(obj)) {
        return observeArray(obj, prefix)
    }

    if (typeof obj === "object") {
        const observedObj = Object.create(null)

        Object.keys(obj).forEach((key) => {
            if (/^__.*__$/.test(key)) return

            const path = prefix === ""
                ? key
                : `${prefix}.${key}`

            defineReactive(
                observedObj,
                key,
                observeDeep(obj[key], path),
                path,
            )
        })

        return observedObj
    }

    return obj
}

/**
 * 將 computed 中定義的新屬性掛到 vm 上
 * @param {Page|Component} vm Page 或 Component 實(shí)例
 * @param {Object} computed 計(jì)算屬性對(duì)象
 * @param {Object} watch 偵聽器對(duì)象
 */
const bindComputed = (vm, computed, watch) => {
    // ...

    proxyData($computed, vm)

    // 掛在 vm 上,在 data 變化時(shí)重新 setData
    vm.$computed = $computed

    // 初始化
    vm.setData($computed)
}
7.異步 setData

目前的代碼還有個(gè)問題:每次對(duì)于 data 某個(gè)數(shù)據(jù)的修改都會(huì)觸發(fā) setData,那么假如反復(fù)地修改同一個(gè)數(shù)據(jù),就會(huì)頻繁地觸發(fā) setData。并且每一次修改數(shù)據(jù)都會(huì)觸發(fā) watch 的監(jiān)聽...

而這恰恰是使用小程序 setData api 的大忌:

總結(jié)一下就是這三種常見的 setData 操作錯(cuò)誤:

頻繁的去 setData

每次 setData 都傳遞大量新數(shù)據(jù)

后臺(tái)態(tài)頁面進(jìn)行 setData

計(jì)將安出?

答案就是緩存一下,異步執(zhí)行 setData~

let newState = null

/**
 * 異步 setData 提高性能
 */
const asyncSetData = ({
    vm,
    newData,
    watchFn,
    prefix,
    oldVal,
}) => {
    newState = {
        ...newState,
        ...newData,
    }

    // TODO: Promise -> MutationObserve -> setTimeout
    Promise.resolve().then(() => {
        if (!newState) return

        vm.setData({
            // 因?yàn)椴恢酪蕾囁愿抡麄€(gè) computed
            ...vm.$computed,
            ...newState,
        })

        if (typeof watchFn === "function") {
            watchFn.call(vm, newState[prefix], oldVal)
        }

        newState = null
    })
}

在 Vue 中因?yàn)榧嫒菪詥栴},優(yōu)先選擇使用 Promise.then,其次是 MutationObserve,最后才是 setTimeout。

因?yàn)?Promise.thenMutationObserve 屬于 microtask,而 setTimeout 屬于 task

為啥要用 microtask ?

根據(jù) HTML Standard,在每個(gè) task 運(yùn)行完以后,UI 都會(huì)重渲染,那么在 microtask 中就完成數(shù)據(jù)更新,當(dāng)前 task 結(jié)束就可以得到最新的 UI 了。反之如果新建一個(gè) task 來做數(shù)據(jù)更新,那么渲染就會(huì)進(jìn)行兩次。(當(dāng)然,瀏覽器實(shí)現(xiàn)有不少不一致的地方)

有興趣的話推薦看下這篇文章:Tasks, microtasks, queues and schedules

8.代碼重構(gòu)

之前的代碼為了方便地獲取 vm 和 watch,在 bindData 函數(shù)中又定義了三個(gè)函數(shù),整個(gè)代碼耦合度太高了,函數(shù)依賴很不明確。

// 代碼耦合度太高
const bindData = (vm, watch) => {
    const defineReactive = () => {}
    const observeArray = () => {}
    const observeDeep = () => {}
    // ...
}

這樣在下一步編寫單元測試的時(shí)候很麻煩。

為了寫測試讓咱們來重構(gòu)一把,利用學(xué)習(xí)過的函數(shù)式編程中的高階函數(shù)把依賴注入。

// 高階函數(shù),傳遞 vm 和 watch 然后得到 asyncSetData
const getAsyncSetData = (vm, watch) => ({ ... }) => { ... }

// 從 bindData 中移出來
// 原來放在里面就是為了獲取 vm,然后調(diào)用 vm.setData
// 以及通過 watch 獲取監(jiān)聽函數(shù)
const defineReactive = ({
    // ...
    asyncSetData, // 不傳 vm 改成傳遞 asyncSetData
}) => { ... }

// 同理
const observeArray = ({
    // ...
    asyncSetData, // 同理
}) => { ... }

// 同樣外移,因?yàn)橐蕾囈炎⑷肓?asyncSetData
const getObserveDeep = (asyncSetData) => { ... }

// 函數(shù)外移后代碼邏輯更加清晰精簡
const bindData = (vm, observeDeep) => {
    const $data = observeDeep(vm.data)
    vm.$data = $data
    proxyData($data, vm)
}

高階函數(shù)是不是很膩害!代碼瞬間就在沒事的時(shí)候,在想的時(shí)候,到一個(gè)地方,不相同的地方,到這個(gè)地方,來了吧!可以瞧一瞧,不一樣的地方,不相同的地方,改變了很多很多

那么接下來你一定會(huì)偷偷地問自己,這么膩害的技術(shù)要去哪里學(xué)呢?

slide

JavaScript 函數(shù)式編程(一)

JavaScript 函數(shù)式編程(二)

JavaScript 函數(shù)式編程(三)

JavaScript 函數(shù)式編程(四)正在醞釀...

9.依賴收集

其實(shí)以上代碼還有一個(gè)目前解決不了的問題:我們不知道 computed 里定義的函數(shù)的依賴是什么。所以在 data 數(shù)據(jù)更新的時(shí)候我們只好全部再算一遍。

也就是說當(dāng) data 中的某個(gè)數(shù)據(jù)更新的時(shí)候,我們并不知道它會(huì)影響哪個(gè) computed 中的屬性,特別的還有 computed 依賴于 computed 的情況。

計(jì)將安出?

且聽下回分解~溜了溜了,嘿嘿嘿...

以上 to be continued...

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

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

相關(guān)文章

  • 終極蛇皮上帝視角信小程序告別“刀耕火種”

    開門見山地說,小程序在日常開發(fā)中使用原生框架來開發(fā)還是挺不方便的,比如: 不支持 npm 包 不支持各種 CSS 預(yù)編譯器 不支持配置 Babel 來轉(zhuǎn)換一些 JavaScript 新特性 這樣一來和日常開發(fā)前端頁面的體驗(yàn)相比來說,簡直就像在刀耕火種。 那么為了解決這些問題,我們能不能將前端開發(fā)中常用的 webpack 移植到小程序開發(fā)中呢? 當(dāng)然可以! showImg(https://seg...

    TerryCai 評(píng)論0 收藏0
  • 信小程序如何使用自定義組件封裝原生 image 組件

    摘要:假如圖片鏈接有問題比如,依然展示占位圖。使用單文件組件將配置模板腳本樣式寫在一個(gè)文件中,方便維護(hù)。 零、問題的由來 一般在前端展示圖片時(shí)都會(huì)碰到這兩個(gè)常見的需求: 圖片未加載完成時(shí)先展示占位圖,等到圖片加載完畢后再展示實(shí)際的圖片。 假如圖片鏈接有問題(比如 404),依然展示占位圖。甚至你還可以增加點(diǎn)擊圖片再次加載的功能。(例如知乎) 然鵝,小程序原生組件 image 并沒有提供這...

    cnTomato 評(píng)論0 收藏0
  • 「前端早讀君009」快速小程序開發(fā)信小程序內(nèi)嵌 H5

    摘要:前言微信小程序中可以直接運(yùn)行頁面,這一新組件的產(chǎn)生,可能直接導(dǎo)致小程序數(shù)量迎來一波高峰。微信小程序配置系列問題配置域名業(yè)務(wù)域名中配置的就是小程序以及和中引用的域名。 今日勵(lì)志語 要接受自己行動(dòng)所帶來的責(zé)任而非自己成就所帶來的榮耀。 前言 微信小程序中可以直接運(yùn)行 web 頁面,這一新組件 web-view 的產(chǎn)生,可能直接導(dǎo)致小程序數(shù)量迎來一波高峰。本篇博文將從業(yè)務(wù)選型,微信小程序后臺(tái)...

    wh469012917 評(píng)論0 收藏0
  • 微信開發(fā)微信jssdk錄音功能開發(fā)

    項(xiàng)目需求簡單描述 用戶長按錄音,松手后直接結(jié)束錄音,結(jié)束錄音后,用戶可以選擇重新錄音、播放剛才的錄音,上傳錄音(這里的上傳錄音指上傳到自己服務(wù)器,上傳步驟是,前端調(diào)用wx.uploadVoice,后臺(tái)再到微信服務(wù)器下載音頻文件,上傳到自己的服務(wù)器)。注意,音頻文件自上傳時(shí)間算起在微信服務(wù)器的有效期為3天。由于后臺(tái)從微信服務(wù)器下載的音頻文件是amr格式的,需要后臺(tái)先把a(bǔ)mr文件轉(zhuǎn)換成MP3,前端用a...

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

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

0條評(píng)論

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