# 简单实现vue响应式原理

本篇只是简单的实现一个vue响应式demo,并未对源码做解读。

# 想要的效果

<div id="app">
    <input type="text" v-model="text"> {{text}}
</div>
var vm = new Vue({
    el: '#app',
    data: {
        text: 'hello world',
    }
});
效果

右侧的文本内容会随着输入框的内容及时改变。

# 实现思路

我们可以先通过这篇文章(Javascript常用的设计模式详解 (opens new window))了解一下发布-订阅模式。

发布---订阅模式又叫观察者模式,它定义了对象间的一种一对多的关系,让多个观察者对象同时监听某一个主题对象,当一个对象发生改变时,所有依赖于它的对象都将得到通知。

我们将对 data 对象的每一个属性,都使用发布-订阅模式。

每个属性都将有一个 Dep 对象,Dep 对象上有一个 subs 数组,用于存储所有的观察者 Watcher。

每一个 Watcher 对象有一个 update 方法,用于更新视图。

我们将通过Object.defineProperty() (opens new window)方法, 重写getter和setter,来分变实现观察者的收集和发布功能。

# 实现

  1. Vue构造函数
function Vue(option) {
    this.data = option.data;    // {text:"..."}

    // 遍历每一个属性,建立发布-订阅模式
    observe(this.data, this);    // {text:"..."},Vue实例

    // 遍历每一个dom节点,初始化每一个观察者
    var id = option.el;     // "app"
    var dom = nodeToFragment(document.getElementById(id), this);    // "#app",Vue实例

    // 编译完成后,dom返回到app
    document.getElementById(id).appendChild(dom)    // 更新dom节点
}
  1. Dep构造函数
function Dep() {
    this.subs = []; // 用于存储每一个观察者
}

Dep.prototype = {
    // 添加观察者
    addSub: function (sub) {
        this.subs.push(sub);
    },
    // 发布,通知每一个观察者
    notify: function () {
        this.subs.forEach(function (sub) {
            sub.update();   // 即调用每个Watcher的update方法来达到更新视图的目的
        })
    }
}
  1. observe方法
function observe(obj, vm) { // data对象 {text:"..."}, Vue实例
    // 遍历每一个属性,调用defineReactive方法
    Object.keys(obj).forEach(function (key) {
        defineReactive(vm, key, obj[key]);  // Vue实例,属性名 text, 属性值 "hello world"
    });
}
  1. defineReactive方法
function defineReactive(obj, key, val) {    // Vue实例, 属性名 text, 属性值 "hello world"
    var dep = new Dep();// 一个属性对应的观察者数组,对应当前属性文档中所有需要响应的节点

    // 重写getter和setter
    Object.defineProperty(obj, key, {
        get: function () {
            // console.log(Dep.target) // Watcher
            if (Dep.target) dep.addSub(Dep.target); // Dep.target属性用于当实例化Watcher并初始化dom节点时,收集该观察者
            return val
        },
        set: function (newVal) {
            if (newVal === val) return
            val = newVal;
            // 属性值每次更新时发布通知
            dep.notify();
        }
    })
}
  1. Watcher构造函数
function Watcher(vm, node, name) {  // Vue实例,文本节点,text
    Dep.target = this;  // Watcher
    this.name = name;   // 属性名
    this.node = node;   // 节点
    this.vm = vm;       // Vue实例
    this.update();      // 初始化文本节点的nodeValue
    Dep.target = null;  // 初始化之后销毁
}

Watcher.prototype = {
    // 更新当前节点视图
    update: function () {
        this.get();
        this.node.nodeValue = this.value;
    },
    // 获取vue实例data对象相应的属性值
    get: function () {
        this.value = this.vm[this.name]; // this.vm[this.name]会触发第四步定义的getter,当Dep.target存在时就会收集观察者
    }
}
  1. nodeToFragment方法

Document.createDocumentFragment() (opens new window)

Node.appendChild (opens new window)

function nodeToFragment(node, vm) { // vue实例绑定的dom节点 "#app", Vue实例
    var flag = document.createDocumentFragment(); // 创建一个新的空白的文档片段作为容器
    var child;

    while (child = node.firstChild) {   // while遍历每一个子节点
        complie(child, vm); // 编译子节点
        flag.appendChild(child);    // appendChild 从原父节点移除该子节点后再添加到容器下
    }

    return flag
}
  1. complie方法
function complie(node, vm) {    // 节点, Vue实例
    // 节点类型为元素
    if (node.nodeType === 1) {
        var attr = node.attributes; // 获取元素节点所有属性
        for (var i = 0; i < attr.length; i++) {
            if (attr[i].nodeName === 'v-model') { // 匹配到'v-model'
                var name = attr[i].nodeValue;   // 获取'v-model'绑定的属性名text
                node.addEventListener('input', function (e) { // 绑定input事件
                    // 给相应的data属性赋值,进而触发该属性的setter,进行发布
                    vm[name] = e.target.value;
                })
                node.value = vm.data[name]; // 初始化元素节点 input 的值
                node.removeAttribute('v-model') // 移除input的v-model属性
            }
        }
        return
    }
    var reg = /\{\{(.*)\}\}/    // 定义正则,用于匹配 {{...}}
    // 节点类型为文本
    if (node.nodeType === 3) {
        var str = node.nodeValue.trim(); // 删除字符串的头尾空白符
        if (reg.test(str)) {
            var name = RegExp.$1;   // 属性名 text
            new Watcher(vm, node, name);    // 实例化观察者对象,传入(Vue实例, 文本节点, 属性名text)
        }
        return
    }
}
# 注:

这个实现还是很粗糙的,有太多场景没有考虑,更完善的可以看看源码分析这篇源码分析vue响应式原理 (opens new window)