vue2.x响应式原理解析(mini-vue)

73 阅读2分钟

看过Vue源码后不难发现,Vue洋洋洒洒上万行代码,但是核心的代码并不会太多。其中很大一部分是各平台的兼容性代码以及异常的捕获与处理。虽然阅读这些代码对我们编写更严谨的代码有很大的帮助,但是对我们了解Vue的核心原理造成了巨大得阻碍,所以是否可以把这些代码删除,只保留核心代码,实现一个能够正常工作的Vue呢。

export function Vue(options = {}) {
    this._init(options)
}

Vue.prototype._init = function(options) {
    this.$options = options
    this.$el = options.el
    this.$data = options.data
    this.$methods = options.methods

    proxy(this, this.$data)

    observer(this.$data)
    new Compiler(this);
}

function proxy(target, data) {
    Object.keys(data).forEach(key => {
        Object.defineProperty(target, key, {
            enumerable: true,
            configurable: true,
            get() {
                return data[key]
            },
            set(newVal) {
                if(isSameValue(newVal, data[key])) return;
                data[key] = newVal
            }
        })
    })
}

function observer(data) {
    new Observer(data)
}

class Observer {
    constructor (data) {
        this.walk(data)
    }

    walk(data) {
        if(data && typeof data === 'object') {
            Object.keys(data).forEach(key => this.defineReactive(data, key, data[key]))
        }
    }

    // 收集data中每个数据
    defineReactive(obj, key, value) {
        let that = this
        // 递归
        this.walk(value)
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                Dep.target && dep.add(Dep.target);
                return value
            },
            set(newVal) {
                if(isSameValue(newVal, value)) return;
                value = newVal
                // 此部操作,防止新值是对象或者数组,如果是对象或者数组,需要对新值newVal所有元素添加响应式,反之则不需要
                // 注意:此时的this执行发生变化,that指当前实例
                that.walk(newVal)
                dep.notify();
            }
        })
    }
}

// 视图更新
// 数据更新,需要更新视图,需要去观察
class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm; // 实例
        this.key = key;
        this.cb = cb;

        Dep.target = this;
        this.__old = vm[key];
        Dep.target = null;
    }

    update() {
        let newVal = this.vm[this.key];
        if(!isSameValue(newVal, this.__old)) this.cb(newVal);
    }
}

class Dep {
    constructor() {
        this.watchers = new Set();
    }

    add(watcher) {
        if(watcher && watcher.update) this.watchers.add(watcher)
    }

    notify() {
        this.watchers.forEach(watcher => watcher.update())
    }
}

class Compiler {
    constructor(vm) {
        this.el = vm.$el;
        this.vm = vm;
        this.methods = vm.$methods;

        this.compile(vm.$el);
    }
    // 这里是递归编译 #app 下面的所有的节点内容;
    compile(el) {
        let childNodes = el.childNodes;
        // 类数组
        Array.from(childNodes).forEach(node => {
            // 判断如果是文本节点
            if(node.nodeType === 3) {
                this.compileText(node)
            }
            // 如果是元素节点
            else if(node.nodeType === 1) {
                this.compileElement(node)
            }
            // 如果还有子节点,就递归下去。
            if(node.childNodes && node.childNodes.length) this.compile(node);
            // ...
        })
    }

    compileText(node) {
        // 匹配出来 {{massage}}
        let reg = /\{\{(.+?)\}\}/;
        let value = node.textContent;
        if(reg.test(value)) {
            let key = RegExp.$1.trim()
            // 开始时赋值。
            node.textContent = value.replace(reg, this.vm[key]);
            // 添加观察者
            new Watcher(this.vm, key, val => {
                // 数据改变时的更新
                node.textContent = val;
            })
        }
    }

    compileElement(node) {
        // 简化,只做匹配 v-on 和 v-model 的匹配
        if(node.attributes.length) {
            Array.from(node.attributes).forEach(attr => {
                let attrName = attr.name;
                if(attrName.startsWith('v-')) {
                    // v- 指令匹配成功,可能是 v-on:click 或者 v-model
                    attrName = attrName.indexOf(':') >-1 ? attrName.substr(5): attrName.substr(2)
                    let key = attr.value;
                    // 
                    this.update(node, key, attrName, this.vm[key])
                }
            })
        }
    }

    update(node, key, attrName, value) {
        if(attrName === 'model') {
            node.value = value;
            new Watcher(this.vm, key, val => node.value = val);
            node.addEventListener('input', () => {
                this.vm[key] = node.value;
            })
        } else if (attrName === 'click') {
            node.addEventListener(attrName, this.methods[key].bind(this.vm))
        }
    }
}

function isSameValue(a, b) {
    // 考虑NaN情况
    return a === b || ((Number.isNaN(a)) && (Number.isNaN(b)))
}