简单实现Vue响应式原理@郝晨光

前言

Vue的数据双向绑定,响应式原理,其实就是通过Object.defineProperty()结合发布者订阅者模式来实现的。
我们可以先试着拆分一下Vue的核心模块。

  1. Vue构造函数,集中以下模块实现MVVM。
  2. Observer 通过Object.definePropty进行数据劫持
  3. Dep 发布订阅者,添加观察者者以及在数据发生改变的时候通知观察者
  4. Watcher 观察者,对数据进行观察以及保存数据修改需要触发的回调
  5. Compiler 模板编译器,对HTML模板进行编译,提取其中的变量并转化为数据。
简单实现Vue响应式原理@郝晨光_第1张图片
Vue数据劫持结合发布者订阅者模式工作流程

对于整个Vue响应式的简单实现来说,在文中并不能做过多的介绍,只能依靠读者自己去试,按照注释来进行理解。推荐的学习方式就是通过本文的实现代码一步一步的自己实现一下,然后再自己实现的基础上自己编写注释。



正文

先看一下我们最终实现的效果吧

HTML

哈哈

{{msg}}

{{a.b}}

JavaScript

new Vue({
    data() {
        return {
            msg: '呃呃呃呃呃',
            name: '郝晨光',
            a: {
                b: 'bbbbb'
            }
        }
    },
    methods: {
        setName() {
            this.msg = '哈哈';
        }
    },
    created() {
        this.msg = '郝晨光哈哈';
        console.log('实例初始化完成')
    },
    mounted() {
        console.log('DOM挂载完成')
    }
}).$mount('#app'); // 此处通过el属性绑定也是没有任何问题的
简单实现Vue响应式原理@郝晨光_第2张图片
Vue简单实现

Vue构造函数

// Vue构造函数
function Vue(options) {
    // 如果当前Vue不是通过new 关键字调用,就进行报错
    if(!(this instanceof arguments.callee)) {
        error('Vue是一个构造函数,必须通过new关键字调用!');
    }
    // 如果是的话,就接着执行_init方法
    this._init(options);
}
// 实例化Vue的方法
Vue.prototype._init = function(options) {
    // 先将options保存在Vue的this.$options上
    this.$options = options;
    // 再拿到对应的data中的值,没有默认为空对象
    this.$data = initData(this.$options) || {};
    // 拿到对应的方法,没有默认为空对象
    this.$methods = this.$options.methods || {};
    // 进行数据劫持
    new Observer(this.$data);
    // 对数据和方法进行代理
    proxyData(this, this.$data);
    proxyData(this, this.$methods);
    // 生命周期created函数
    this.$options.created.apply(this);
    // 如果有el属性的话,自动调用$mount方法,挂载到DOM节点中
    if(this.$options.el) {
        this.$mount(this.$options.el);
    }
};

// $mount方法,将Vue实例挂载到DOM节点上
Vue.prototype.$mount = function(el) {
    // 拿到对应的DOM节点
    let $el = typeof el === 'string'
            ?
            document.querySelector(el)
            : el.nodeType === 1
                ?
                el
                :
                error('el必须是一个选择器或者是一个DOM节点!');
    // 将DOM保存在$el属性上
    this.$el = $el;
    // 通过Compiler编译器进行编译
    new Compiler(this.$el, this);
    // 调用mounted生命周期钩子函数
    this.$options.mounted.apply(this);
    // 返回当前的Vue实例,保证外部能够拿到正确的Vue实例
    return this;
};

// 初始化Vue实例的data
function initData(options) {
    // 拿到data的数据类型
    const type = typeof options.data;
    // 如果是function的话,调用函数拿到对象,否则直接返回对象
    return type === 'function' ? options.data() : options.data;
}

// 对data内的数据进行代理
function proxyData(target, proxy) {
    // 拿到对象上的所有key值组成的数组,并进行遍历
    Object.keys(proxy).forEach(key => {
        // 通过Object.defineProperty方法对数据进行代理
        Object.defineProperty(target, key, {
            get() {
                return proxy[key];
            },
            set(newValue) {
                proxy[key] = newValue;
            }
        })
    });
}

// 错误信息
function error(info) {
    throw new Error(info);
}

Observer数据劫持

// 数据劫持
function Observer(data) {
    // Observer必须是一个构造函数,如果不是通过new关键字调用的话,
    // 在内部使用new关键字。
    if(!(this instanceof arguments.callee)) {
        return new arguments.callee(data);
    }
    // 如果data不是一个对象的话,提示错误,
    // 因为只有对象才能调用Object.defineProperty
    if(!data || typeof data !== 'object') {
        error('代理的data必须是一个对象')
    }
    // 调用observe方法
    this.observe(data);
}

Observer.prototype.observe = function(data) {
    if(!data || typeof data !== 'object') {
        return;
    }
    // 获取对象上的键值数组并对它进行遍历
    Object.keys(data).forEach(key => {
        // 调用数据劫持方法
        this.defineReactive(data, key, data[key]);
        // 判断如果当前的值还是对象的话,递归劫持
        if(typeof data[key] === 'object') {
            this.observe(data[key]); // 递归劫持所有的值
        }
    })
};

Observer.prototype.defineReactive = function(data, key, value) {
    // 保存this
    const _this = this;
    // 添加观察者
    let dep = new Dep();
    // 数据劫持
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举的
        configurable: true, // 可删除的
        // 代理get
        get() {
            // 当前Dep.target是指的Watcher(订阅者)实例,
            // 向dep实例中添加Watcher实例
            Dep.target && dep.addSub(Dep.target);
            return value;
        },
        // 代理set
        set(newValue) {
            // 如果新的值和旧的值不相等的情况下
            if(newValue !== value) {
                // 重新调用observe劫持数据
                _this.observe(newValue);
                // 设置新的值
                value = newValue;
                // dep实例通知订阅者进行修改
                dep.notify();
            }
        }
    })
};

Dep发布者

// Dep发布者将要执行的函数统一存储在一个数组中管理,
// 当达到某个执行条件时,循环这个数组并执行每一个成员。
function Dep() {
    this.subs = [];
}

// 在发布者Dep实例上添加订阅者
Dep.prototype.addSub = function(watcher) {
    this.subs.push(watcher);
};

// 通知订阅者进行修改
Dep.prototype.notify = function() {
    // 遍历所有的订阅者,调用订阅者上的update方法进行修改。
    this.subs.forEach(watcher => watcher.update());
};

Watcher订阅者

// 订阅者
function Watcher(vm, variable, callback) {
    // 保存vm实例
    this.vm = vm;
    // 保存需要修改的属性
    this.variable = variable;
    // 保存属性修改时需要触发的回调
    this.callback = callback;
    // 保存属性的初始值,并将当前订阅者添加到发布者上
    this.value = this.get();
}

Watcher.prototype.get = function() {
    // 将当前的 watcher 添加到Dep发布者的静态属性上
    Dep.target = this;
    // 获取到当前的属性值
    let value = CompilerUtil.getValue(this.vm, this.variable);
    // 在Dep发布者的静态属性上清除当前 watcher
    Dep.target = null;
    // 返回拿到的值
    return value;
};

Watcher.prototype.update = function() {
    // 发生修改的时候,重新获取值
    let newValue = CompilerUtil.getValue(this.vm, this.variable);
    // 先获取旧的值
    let oldValue = this.value;
    // 如果两个值不等的话,调用修改DOM的回调函数
    if(newValue !== oldValue) {
        this.callback(newValue);
    }
};

Compiler模板编译器

// Compiler模板编译器
function Compiler(el, vm) {
    // 先拿到需要编译的DOM节点
    this.el = el.nodeType === 1 ? el : document.querySelector(el);
    // 拿到当前的vm实例
    this.vm = vm;
    // 如果当前的el存在,就开始编译
    if(this.el) {
        // 将真实的DOM转换为文档碎片
        let fragment = this.vNodeFragment(this.el);
        // 调用compile方法进行编译
        this.compile(fragment);
        // 编译完成之后再添加到真实DOM中
        this.el.appendChild(fragment);
    }
}

// DOM文档片段
Compiler.prototype.vNodeFragment = function(el) {
    // 创建文档片段
    let fragment = document.createDocumentFragment();
    let firstChild;
    // 遍历当前所有的DOM子节点
    while (firstChild = el.firstChild) {
        // 将真实DOM节点添加到文档片段中
        fragment.appendChild(firstChild);
    }
    // 返回虚拟文档片段
    return fragment;
};

// 进行编译
Compiler.prototype.compile = function(fragment) {
    // 拿到文档片段的所有子节点
    // 必须通过childNodes拿,因为childNodes不会忽略文本节点。
    let children = fragment.childNodes;
    // 转换为真实数组并进行遍历
    Array.prototype.slice.call(children).forEach(node => {
        // 如果当前是元素节点的话,继续递归遍历,并编译元素节点
        if(node.nodeType === 1) {
            this.compile(node); // 对当前节点内的子节点进行递归遍历
            this.compileElement(node); // 编译元素节点
        }else {
            // 否则是文本节点,就开始编译文本
            this.compileText(node);
        }
    })
};

// 编译元素节点
Compiler.prototype.compileElement = function (node) {
    // 获取到元素所有的属性
    let attrs = node.attributes;
    // 转换为真实数组并进行遍历
    Array.prototype.slice.call(attrs).forEach(attr => {
        // 获取到当前的属性名
        let attrName = attr.name;
        // 判断当前的属性是否是指令
        if(attrName.includes('v-')) {
            // 如果是指令的话,拿到当前的属性值
            let value = attr.value;
            // 拿到当前的指令名
            let [,type] = attrName.split('-');
            // 对当前指令执行编译
            CompilerUtil[type](node, this.vm, value);
            // 判断当前属性是否是事件
        }else if(attrName.includes('@')) {
            // 拿到事件名称
            let event = attrName.slice(1);
            // 拿到事件需要触发的方法名称
            let method = attr.value;
            // 对当前元素添加DOM事件
            CompilerUtil.addEvent(node, event, method, this.vm);
        }
    })
};

// 编译文本节点
Compiler.prototype.compileText = function (node) {
    let content = node.textContent; // 获取文本节点的内容
    let reg = /\{\{(.+?)\}\}/g; // 匹配模板编译器的内容
    // 如果能匹配到模板编译器
    if(reg.test(content)) {
        // 编译文本节点
        CompilerUtil.text(node, this.vm, content);
    }
};

模板编译工具

// 模板编译工具对象
const CompilerUtil =  {
    // 文本编译的回调函数
    textUpdater(node, value) {
        node.textContent = value;
    },
    // input编译的回调函数
    modelUpdater(node, value) {
        node.value = value;
    },
    // 获取vm实例中对应的值
    getValue(vm, variable) {
        // 获取对象的属性
        variable = variable.split('.');
        // 通过reduce方法递归遍历vm.$data,拿到最终在vm实例中的属性值
        return variable.reduce((prev, next) => prev[next], vm.$data);
    },
    // 获取文本中变量对应的内容
    getTextValue(vm, variable) {
        // 通过正则匹配,拿到属性名
        let reg = /\{\{([^}]+)\}\}/g;
        return variable.replace(reg, ($0, $1) => {
            // 通过属性名,调用getValue方法,获取属性值
            return this.getValue(vm, $1);
        })
    },
    // 设置Value
    setValue(vm, variable, newValue) {
        // 获取对象的属性名
        variable = variable.split('.');
        // 通过reduce方法遍历
        return variable.reduce((prev, next, index) => {
            // 如果当前是匹配的属性名的话
            if(index === variable.length - 1) {
                // 给当前的属性设置值
                return prev[next] = newValue;
            }
            // 如果不是就返回继续计算
            return prev[next];
        }, vm.$data);
    },
    // 双向数据绑定 v-model的简单实现
    model(node, vm, variable) {
        // 获取到双向数据绑定的修改方法
        let updateFn = this.modelUpdater;
        // 获取到对应的值
        let value = this.getValue(vm, variable);
        // 添加订阅者, 给订阅者添加回调
        new Watcher(vm, variable, newValue => {
            // 当数据发生修改的时候,就触发当前回调,修改元素节点的值
            updateFn && updateFn(node, newValue);
        });
        // 将v-model属性从DOM节点上删除
        node.removeAttribute('v-model');
        // 给当前元素节点添加input事件
        node.addEventListener('input', e => {
            // 拿到对应的值
            let value = e.target.value;
            // 设置值
            this.setValue(vm, variable, value);
        });
        // 初次渲染的时候,也要设置一次值
        updateFn && updateFn(node, value);
    },
    // 添加事件
    addEvent(node, event, method, vm) {
        // 给元素删除事件符
        node.removeAttribute('@'+event);
        // 给元素添加事件
        node.addEventListener(event, (...args) => {
            // 调用vm上的方法,并传入参数
            vm[method].apply(vm, args);
        })
    },
    // 编译文本节点的变量
    text(node, vm, variable) {
        // 文本节点的修改函数
        let updateFn = this.textUpdater;
        // 获取到文本节点变量的值
        let value = this.getTextValue(vm, variable);
        // 定义正则
        let reg = /\{\{(.+?)\}\}/g;
        // 通过正则匹配变量,给变量添加观察者
        variable.replace(reg, ($0, $1) => {
            // 当解析模板遇到变量的时候,应该使用观察者监听这个变量
            new Watcher(vm, $1, newValue => {
                // 观察者的回调函数,当数据发生改变就触发该回调
                updateFn && updateFn(node, newValue);
            })
        });
        // 第一次设置值
        updateFn && updateFn(node, value);
    }
};



结束

参考文章链接:
一起学习、手写MVVM框架
前端 实现一个简易版的vue,了解vue的运行机制
JS实现一个简易版的vue

如果本文对您有帮助,可以看看本人的其他文章:
前端常见面试题(十六)@郝晨光
前端常见面试题(十五)@郝晨光
前端常见面试题(十四)@郝晨光

结言
感谢您的查阅,本文由郝晨光整理并总结,代码冗余或者有错误的地方望不吝赐教;菜鸟一枚,请多关照

你可能感兴趣的:(简单实现Vue响应式原理@郝晨光)