Прощай, мини-программы WeChat с точки зрения Ultimate Snake Skin God’s Perspective setData

внешний интерфейс Апплет WeChat функциональное программирование Vue.js

Как мы все знаем, Vue использует ES5.Object.definePropertyМетод устанавливает геттер, сеттер для достижения управляемого данными интерфейса, конечно, есть и другие процессы, такие как компиляция шаблона.

Официальный API апплета находится вPageвызыватьthis.setDataспособ изменить данные и, таким образом, изменить интерфейс.

Итак, если мы объединим их, мы получимthis.setDataИнкапсулированный, можно ли его использовать как разработку приложения Vue?this.foo = 'hello'Разработать небольшую программу?

  • Далее можно реализовать изоморфизм части кода h5 и js апплета
  • Кроме того, добавление компиляции и синтаксического анализа шаблона позволяет даже части wxml/html быть изоморфной.
  • Кроме того, совместимость с RN/Weex/Quick App
  • Дальше мир гармоничен, мир публичен, а все фронтенд инженеры безработные... 23333

否认三连.jpg

0. Адрес источника

1. Привязать простые свойства

Первый шаг — поставить маленькую цель:Заработай ему 100 миллионов! ! !

Для простых невложенных свойств (не объектов, массивов) их прямое назначение может изменить интерфейс.

<!-- index.wxml -->
<view>msg: {{ msg }}</view>
<button bindtap="tapMsg">change msg</button>
// index.js
TuaPage({
    data () {
        return {
            msg: 'hello world',
        }
    },
    methods: {
        tapMsg () {
            this.msg = this.reverseStr(this.msg)
        },
        reverseStr (str) {
            return str.split('').reverse().join('')
        },
    },
})

Этот шаг очень прост, напрямую привяжите геттер и сеттер к каждому атрибуту в данных и вызовите его в сеттере.this.setDataПросто хорошо.

/**
 * 将 source 上的属性代理到 target 上
 * @param {Object} source 被代理对象
 * @param {Object} target 被代理目标
 */
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 实例
 */
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)
            },
        })
    }

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

        Object.keys(obj).forEach((key) => {
            // 过滤 __wxWebviewId__ 等内部属性
            if (/^__.*__$/.test(key)) return

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

        return observedObj
    }

    const $data = observe(vm.data)

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

/**
 * 适配 Vue 风格代码,使其支持在小程序中运行(告别不方便的 setData)
 * @param {Object} args Page 参数
 */
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. Привязка вложенных объектов

Так что, если данные являются вложенным объектом?

На самом деле это тоже очень просто, рассмотрим это рекурсивно.

<!-- index.wxml -->
<view>a.b: {{ a.b }}</view>
<button bindtap="tapAB">change a.b</button>
// 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Если суждение является объектом, оно будет наблюдаться рекурсивно.

// ...

/**
 * 递归观察对象
 * @param {any} obj 待观察对象
 * @return {any} 已被观察的对象
 */
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. Перехват методов массива

Как мы все знаем, Vue перехватывает некоторые методы массива. Давайте также осознаем это по рисунку тыквы ~

/**
 * 劫持数组的方法
 * @param {Array} arr 原始数组
 * @return {Array} observedArray 被劫持方法后的数组
 */
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
}

На самом деле, Vue также сделал оптимизацию, если текущая среда__proto__атрибут, затем добавьте вышеуказанные методы непосредственно в цепочку прототипов массива вместо изменения метода каждого массива данных.

4. Реализуйте вычисляемую функцию

computedФункция довольно часто используется каждый день, благодаря существующемуdataМетаданные, которые получают некоторые удобные новые данные.

добиться, потому чтоcomputedВсе данные определены как функции, поэтому на самом деле установите их непосредственно вgetterПросто сделай это.

/**
 * 将 computed 中定义的新属性挂到 vm 上
 * @param {Page|Component} vm Page 或 Component 实例
 * @param {Object} computed 计算属性对象
 */
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 中数据变化时可以一起被 setData
    proxyData($computed, vm.$data)

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

5. Реализуйте функцию часов

Далее еще одна жареная курица.watchфункция, то есть слушаниеdataилиcomputedДанные в , вызывают функцию обратного вызова, когда они изменяются, и передаютnewValа такжеoldVal.

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)

            // 实现 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)

                // 实现 watch computed 属性
                const watchFn = watch[key]
                if (typeof watchFn === 'function' && newVal !== oldVal) {
                    watchFn.call(vm, newVal, oldVal)
                }

                // 重置 oldVal
                oldVal = newVal

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

    // ...
}

Выглядит хорошо, но это не так.

Теперь у нас есть проблема:Как прослушивать вложенные данные, такие как «a.b»?

Причина этой проблемы в том, что мы не записывали путь при рекурсивном обходе данных.

6. Запишите путь

Решить эту задачу несложно, по сути, нам нужно только пройти в каждом шаге рекурсивного наблюденияkeyВот и все, обратите внимание, что для вложенных элементов массива передается то, что[${index}].

И как только мы узнаем путь данных, его можно еще улучшить.setDataпредставление.

Потому что мы можем позвонить мелкоvm.setData({ [prefix]: newVal })изменить некоторые данные, а не все$dataВсеsetData.

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

            vm.setData({
                // 因为不知道依赖所以更新整个 computed
                ...vm.$computed,
                // 直接修改目标数据
                [path]: newVal,
            })

            // 通过路径来找 watch 目标
            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({
                // 因为不知道依赖所以更新整个 computed
                ...vm.$computed,
                // 直接修改目标数据
                [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 实例
 * @param {Object} computed 计算属性对象
 * @param {Object} watch 侦听器对象
 */
const bindComputed = (vm, computed, watch) => {
    // ...

    proxyData($computed, vm)

    // 挂在 vm 上,在 data 变化时重新 setData
    vm.$computed = $computed

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

7. Асинхронный набор данных

Также есть проблема с текущим кодом: каждый раз дляdataИзменение данных вызоветsetData, то если вы неоднократно изменяете одни и те же данные, они будут срабатывать частоsetData. И каждый раз, когда данные изменяются, он срабатываетwatchмонитор...

И это именно использование апплетаsetDataБольшое табу API:

Подводя итог этим трем распространенным ошибкам операции setData:

  1. Часто заходите в setData
  2. Передайте много новых данных каждый раз, когда setData
  3. Страница фонового состояния для setData

План выйдет?

改代码.png

Ответ заключается в том, чтобы кэшировать его и выполнять асинхронно.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({
            // 因为不知道依赖所以更新整个 computed
            ...vm.$computed,
            ...newState,
        })

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

        newState = null
    })
}

В Vue из-за проблем с совместимостью предпочтительно использоватьPromise.then,С последующимMutationObserve, и наконецsetTimeout.

потому чтоPromise.thenа такжеMutationObserveпринадлежатьmicrotask,а такжеsetTimeoutпринадлежатьtask.

зачем использоватьmicrotask?

согласно сHTML Standard, в каждомtaskПосле бега,UIбудет перерисован, то вmicrotaskОбновление данных завершено вtaskконец, чтобы получить последниеUI. Вионт, если новыйtaskЧтобы выполнить обновление данных, рендеринг будет выполнен дважды. (Конечно, в реализации браузера довольно много несоответствий)

Если вам интересно, пожалуйста, прочитайте эту статью:Tasks, microtasks, queues and schedules

8. Рефакторинг кода

В предыдущем коде, чтобы легко получить vm и смотреть,bindDataВ функции определены три функции, связанность всего кода слишком высока, а функциональные зависимости очень неясны.

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

Это неудобно при написании модульных тестов на следующем этапе.

Для того, чтобы написать тест, давайте реконструируем, благодаря использованию обученияфункциональное программированиесерединаФункции высшего порядкавнедрить зависимости.

// 高阶函数,传递 vm 和 watch 然后得到 asyncSetData
const getAsyncSetData = (vm, watch) => ({ ... }) => { ... }

// 从 bindData 中移出来
// 原来放在里面就是为了获取 vm,然后调用 vm.setData
// 以及通过 watch 获取监听函数
const defineReactive = ({
    // ...
    asyncSetData, // 不传 vm 改成传递 asyncSetData
}) => { ... }

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

// 同样外移,因为依赖已注入了 asyncSetData
const getObserveDeep = (asyncSetData) => { ... }

// 函数外移后代码逻辑更加清晰精简
const bindData = (vm, observeDeep) => {
    const $data = observeDeep(vm.data)
    vm.$data = $data
    proxyData($data, vm)
}

Разве функции высшего порядка не скучны! код в одно мгновениеКогда вы свободны, когда вы думаете, идите в место, в другое место, идите в это место, приходите! Вы можете посмотреть, разные места, разные места, многое изменилось, многое

Тогда вы должны втайне спросить себя, где научиться такой скучной технологии?

9. Сбор зависимостей

На самом деле в приведенном выше коде все еще есть проблема, которую нельзя решить в настоящее время: мы не знаемcomputedКаковы зависимости функций, определенных в . так вdataКогда данные обновятся, нам придется снова делать все расчеты.

То есть когдаdataКогда часть данных обновляется, мы не знаем, на какую из них это повлияет.computedсвойства в , в частностиcomputedзависит отcomputedСлучай.

План выйдет?

Давай послушаем разложение в следующий раз~ Я ускользнул, хе-хе-хе...

塘主.gif

выше продолжение следует...