It is well known that Vue uses ES5’s Object.defineProperty method to set getters and setters to the data-driven interface, as well as template compilation and other processes.

The applets’ official API is to call the this.setData method in the Page to change the data, thus changing the interface.

So if we combine the two and wrap this.setData, wouldn’t we be able to use this.foo = ‘hello’ as we would for a Vue application?

  • Further, h5 and small program JS part of the code isomorphism can be realized
  • Furthermore, adding template compilation and parsing makes even the WXML/HTML part isomorphic
  • Further, compatible with RN/Weex/ fast applications
  • Further, the world is one, the world is for the common, front-end engineers are all unemployed… 23333

0. Source code address

  • Making the address

1. Bind simple properties

The first step we set a small goal: earn him 100 million!!

For simple non-nested properties (non-objects, arrays), assigning to them directly changes the interface.

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

This step is easy. Just bind the getter and setter for each property in data and call this.setData in the setter.

@param {Object} source proxied Object * @param {Object} target proxied target */
const proxyData = (source, target) = > {
    Object.keys(source).forEach((key) = > {
        Object.defineProperty(
            target,
            key,
            Object.getOwnPropertyDescriptor(source, key)
        )
    })
}

/ * * * through observation of the vm. The data of all attributes, and place it directly on a vm * @ param {Page | Component} * / vm Page or Component instance
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)
            },
        })
    }

    /** * Observed object * @param {any} obj Object to be observed * @return {any} Observed object */
    const observe = (obj) = > {
        const observedObj = Object.create(null)

        Object.keys(obj).forEach((key) = > {
            // Filter internal attributes such as __wxWebviewId__
            if ($/ / ^ __. * __.test(key)) return

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

        return observedObj
    }

    const $data = observe(vm.data)

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

/** * Adapt Vue style code to support running in applets (say no to inconvenient setData) * @param {Object} args Page parameter */
export const TuaPage = (args = {}) = > {
    const {
        data: rawData = {}, methods = {}, ... rest } = argsconst data = typeof rawData === 'function'? rawData() : rawData Page({ ... rest, ... methods, data, onLoad (... options) { bindData(this)

            rest.onLoad && rest.onLoad.apply(this, options)
        },
    })
}
Copy the code

2. Bind nested objects

So what if the data is nested?

It’s actually pretty simple. Let’s just recurse.

<! -- index.wxml -->
<view>a.b: {{ a.b }}</view>
<button bindtap="tapAB">change a.b</button>
Copy the code
// 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(' ')}}})Copy the code

ObserveDeep: Observe recursively if observeDeep is an object.

// ...

/** * Recursive observation object * @param {any} obj Object to be observed * @return {any} Observed object */
const observeDeep = (obj) = > {
    if (typeof obj === 'object') {
        const observedObj = Object.create(null)

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

            defineReactive(
                observedObj,
                key,
                // -> notice the recursion here
                observeDeep(obj[key]),
            )
        })

        return observedObj
    }

    // Simple attributes are returned directly
    return obj
}

// ...
Copy the code

3. Hijack the array method

As you all know, Vue hijacks some array methods. Let’s also follow the gourd gourd gourd gourd implement ~

@param {Array} arr original Array * @return {Array} observedArray hijacked method after the Array */
const observeArray = (arr) = > {
    constobservedArray = 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
}
Copy the code

If the current environment has a __proto__ attribute, then the above methods are added directly to the array’s prototype chain, rather than changing the methods for each array.

4. Implement computed

Computed data is commonly used in everyday life, and it can derive some convenient new data from existing data metadata.

To do this, because data in computed is defined as a function, you can simply set it to a getter.

/ new properties defined in * * * will be computed on a vm * @ param {Page | Component} vm Page or Component instance * @ param {Object} computed attribute Object * /
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)

    // attach to $data so that data changes in data can be setData together
    proxyData($computed, vm.$data)

    / / initialization
    vm.setData($computed)
}
Copy the code

5. Realize the watch function

Next comes the handy watch function, which listens for data in data or computed data, calls callbacks when it changes, and passes in newVal and oldVal.

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

            // Save oldVal here
            const oldVal = val
            val = newVal
            vm.setData($data)

            // Implement the Watch data property
            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) = > {
        // Save oldVal here
        let oldVal = computed[key].call(vm)

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

                // Implement the watch computed property
                const watchFn = watch[key]
                if (typeof watchFn === 'function'&& newVal ! == oldVal) { watchFn.call(vm, newVal, oldVal) }/ / reset oldVal
                oldVal = newVal

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

    // ...
}
Copy the code

It looks good, but it’s not.

We now have a problem: how to listen for nested data like ‘a.b’?

The reason for this problem is that we do not record the path when we recursively traverse the data.

6. Record the path

It’s not too hard to solve this problem, just pass the key at each step of the recursive observation. Note that [${index}] is passed for nested elements in the array.

And once we know the path of the data, we can further improve the performance of setData.

Because we can call vm.setData({[prefix]: newVal}) to modify part of the data, rather than setData the entire $data.

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

            vm.setData({
                // Update the entire computed because you don't know about dependencies. vm.$computed,// Modify the target data directly
                [path]: newVal,
            })

            // Find the watch target by path
            const watchFn = watch[path]
            if (typeof watchFn === 'function') {
                watchFn.call(vm, newVal, oldVal)
            }
        },
    })
}

const observeArray = (arr, path) = > {
    const observedArray = arr.map(
        // Note the path stitching here
        (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({
                // Update the entire computed because you don't know about dependencies. vm.$computed,// Modify the target data directly
                [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
}

/ new properties defined in * * * will be computed on a vm * @ param {Page | Component} vm Page or Component instance * @ param * {Object} computed to calculate attribute Object @param {Object} watch Listener Object */
const bindComputed = (vm, computed, watch) = > {
    // ...

    proxyData($computed, vm)

    // Hang on the VM and reset data when data changes
    vm.$computed = $computed

    / / initialization
    vm.setData($computed)
}
Copy the code

7. Asynchronous setData

Another problem with the current code is that setData is triggered every time you modify data, so if you modify the same data repeatedly, setData will be triggered frequently. And every time the data is modified, watch’s monitoring will be triggered…

And that’s using appletssetDataAPI’s big no-no:

To summarize these three common setData errors:

  1. Go setData frequently
  2. Each time setData passes a lot of new data
  3. Background state page for setData

Will the meter be installed?

The answer is to cache it and asynchronously execute setData~

let newState = null

/** * Async setData improves performance */
constasyncSetData = ({ vm, newData, watchFn, prefix, oldVal, }) => { newState = { ... newState, ... newData, }// TODO: Promise -> MutationObserve -> setTimeout
    Promise.resolve().then((a)= > {
        if(! newState)return

        vm.setData({
            // Update the entire computed because you don't know about dependencies
            ...vm.$computed,
            ...newState,
        })

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

        newState = null})}Copy the code

In Vue, due to compatibility problems, promise. then is preferred, followed by MutationObserve and setTimeout.

Promise. Then and MutationObserve belong to microTask, while setTimeout belongs to task.

Why want to usemicrotask

According to THE HTML Standard, the UI is re-rendered after each task is finished, so the data is updated in MicroTask and the latest UI is available when the current task is finished. On the other hand, if you create a new task to update the data, the rendering will be done twice. (Of course, there are quite a few inconsistencies in browser implementations.)

Queues and Schedules should be read if you are interested

8. Code refactoring

In order to conveniently obtain VM and watch, the previous code defined three more functions in the bindData function. The whole code was too coupled and the function dependence was not clear.

// Code coupling is too high
const bindData = (vm, watch) = > {
    const defineReactive = (a)= > {}
    const observeArray = (a)= > {}
    const observeDeep = (a)= > {}
    // ...
}
Copy the code

This is cumbersome when writing unit tests next.

For testing purposes, let’s refactor dependency injection using the higher-order functions we’ve learned from functional programming.

// High order function, pass vm and watch and get asyncSetData
const getAsyncSetData = (vm, watch) = >({... = > {...}) }// Remove from bindData
// Get the vm and call vm.setData
// And get the listener via watch
const defineReactive = ({
    // ...
    asyncSetData, // Pass asyncSetData instead of passing vm= > {...}) }/ / in the same way
const observeArray = ({
    // ...
    asyncSetData, / / in the same way= > {...}) }// The dependency is injected with asyncSetData
const getObserveDeep = (asyncSetData) = >{... }// The code logic is clearer and simpler after the function is moved out
const bindData = (vm, observeDeep) = > {
    const $data = observeDeep(vm.data)
    vm.$data = $data
    proxyData($data, vm)
}
Copy the code

High order function is not very tired of harm! Code instantly when nothing, in the time to think, to a place, not the same place, to this place, come! You can have a look, different places, different places, a lot of changes

Then you must secretly ask yourself, where are you going to learn such boring skills?

  • slide
  • JavaScript Functional Programming (PART 1)
  • JavaScript Functional Programming (2)
  • JavaScript Functional Programming (part 3)
  • JavaScript Functional programming (4) In the pipeline…

9. Rely on collection

There is another problem with this code that currently doesn’t solve: we don’t know what the dependencies of functions defined in computed are. So we had to calculate everything again when the data was updated.

That is, when something in data is updated, we don’t know which attributes in computed, especially computed dependence on computed.

Will the meter be installed?

Let’s listen to the next time.

To be continued…