【vue 学习】深入学习 vue 原理

基于 Vue + Vuex + Vue-Router 的博客也算是基本完成了,对 Vue 的使用也算是比较熟练了。但是,作为一个目标全栈的前端开发工程师来说,目标可不只是仅仅会使用而已,我们要熟悉框架原理,知道其思路,甚至于能手写一个简易框架。这篇博文的目的就是深入理解 Vue 的双向绑定原理,分析思路,最终我们要自己手写一个简易的 mvvm 框架,效果如下:

本人自认为不是一个适合写作的人,但我还是尝试去写出来,很多技术方面的东西在记录的过程中,会发现自己没有主要到的盲点,能帮助自己进一步学习,所以我会继续尝试写点什么。

这篇文章针对的是 Vue 1.x,虽然在这里我建议大家直接学习 Vue 2.x,但是理解原理上来说,Vue 1.x 够了。

因为是个人的学习总结,难免会出现错误,如果有什么不对的地方,欢迎大家指正。

原理

Vue 的数据绑定可以是单向的,也可以是双向的,无论单向还是双向,其实都属于下面两个过程:

  • 数据变化 --> DOM 更新
  • DOM 更新 --> 数据变化

这种模式是不是很熟悉,就是 观察者(订阅/发布者)模式,我们平时在对 DOM 操作中都会用到过,比如对 input 执行事件监听,如果有输入就执行一系列操作。但 Vue 的实现可不是基于 DOM 事件,它是一套更加高级的事件系统。下面我们就针对上面两个过程来一个一个的分析。

数据变化 --> DOM 更新

数据的变化怎么能通知 DOM 呢,这里的实现有多种,比如 angular 的脏值检查,在特定的事件发生时执行数据新旧值的检查,如果值发生改变了,那就执行通知。Vue 使用了一种不同的方式 -- setter,通过 Object.defineProperty() 来定义 setter,这样数据改变时就会触发 setter,就能通知 DOM 进行更新了。

但是,当数据更新时,怎么知道要通知哪些 DOM 进行数据更新?这里就需要存储依赖关系,将那些用到数据的引用保存起来,当数据更新时,逐一通知更新。现在就是在哪里进行依赖收集处理了,Vue 是在 getter 中处理的,当触发 getter 读取时,就保存引用数据的源头。

DOM 更新 --> 数据变化

这里就很好处理了,就是对 DOM 添加事件监听,当有值变化时,通知数据进行更新。这里包括 Vue 本身的 model 绑定,也包括我们自己的 DOM 操作,其本质都是一样的,读取新值,调用数据 setter 进行数据更新。

但上面只是很简单的原理分析,实际上 Vue 的实现要复杂得多,下面是 Vue 官方文档中的一张图。

最右侧的绿色的圆代表数据 data,数据的每个属性对象都被定义了 getter 和 setter 两个属性,当通过 watcher 读取数据时,会先执行依赖收集;而通过 setter 更新数据时,会通知 watcher。

最左侧的黄色的方框代表指令集 directive,当 watcher 接收到数据更新时,会通知 directive 执行 update,由 directive 内部实现怎么执行 DOM 更新。

中间的紫色圆就是 watcher,它是连接 data 和 directive 的桥梁,也就是说 directive 不会直接读写 data 里面的数据,数据 data 的变化也不会直接更新到 DOM,watcher 在这里起到一个中介的作用。

从原理的角度上来说,上图的内容是不全的,上图是为了使用者更好的理解 Vue 的响应式,并不适合用来深入研究原理。下图是《Vue.js 权威指南》中源码篇的一个章节中画的图,专门画给研究者看的:

上图比之前的图多了 Dep 和 Observer。一个是上面我们提到的依赖保存,一个是对数据的处理。下面我们聊聊 Directive、Watcher、Dep 和 Observer 的关系及处理逻辑。

首先是 Observer,正如其名 -- 观察者,就是用来监听数据变化并做相应的通知处理。

在 Vue 中,Observer 会观察两种类型的数据,Object 与 Array。

对于 Array 类型的数据,会先重写 Array 的原型方法,重写后能达到两个目的,

  • 当数组发生变化时,触发 notify
  • 如果是 push,unshift,splice 这些添加新元素的操作,则会使用 observer 观察新添加的数据

重写完原型方法后,遍历拿到数组中的每个数据,使用 observer 观察它。

而对于 Object 类型的数据,则遍历它的每个 key,使用 defineProperty 设置 getter 和 setter,当触发 getter 的时候,observer 则开始收集依赖,而触发 setter 的时候,observer 则触发 notify。

然后是依赖的收集,这时就需要 Dep 出场了。

前面我们说到数据的依赖收集是在 getter 里,没错,但不是所有,因为只有和 DOM 绑定的 getter 才应该被添加到依赖中,Vue 中是只收集通过 watcher 触发 getter 的依赖,而被收集的依赖,其实就是 watcher 实例本身。这里我再强调一次:

当数据的 getter 触发后,会收集依赖,但也不是所有的触发方式都会收集依赖,只有通过watcher 触发的 getter 会收集依赖,而所谓的被收集的依赖就是当前 watcher。

因为只有watcher触发的 getter 才会收集依赖,所以 DOM 中的数据必须通过 watcher 来绑定,就是说 DOM 中的数据必须通过 watcher 来读取!

关于 Dep 的实现,这里提出一部分代码实现:

var uid = 0;

function Dep() {
    this.id = uid++;
    this.subs = [];
}

Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },

    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    },
    // some code
};

当通过 watcher 触发 getter 时,watcher 会使用 dep.addSub(this) 把自己的实例推到 subs 中。

当触发 setter 的时候,会触发 notify,而 notify 则会把 watcher 的 update 方法执行一遍。

到这里 observer dep watcher 的逻辑关系已经清楚了。

至于Directive 如何通过 watcher 的 update 方法改变视图,下面就开始分析 Vue 在模板渲染的过程。

在上图中,左边是上面讲的内容,下面我们讲讲右边的内容。

对于编译部分 vue 分了两种类型,一种是文本节点,一种是元素节点。

文本节点

hello {{name}}

这就是一个文本节点,它包含两部分,普通文本节点 hello 和一个特殊的节点 {{name}}。

接下来 Vue 会通过正则来解析文本节点,解析后的结果如下面结构:

[{
  "value": "hello "
}, {
  "value": "name",
  "tag": true,
  "html": false,
  "oneTime": false
}]

然后就是遍历 Array,将所有 tag 为 true 的添加扩展对象,扩展属性包括指令方法。

像文本节点的特殊节点只有两种类型,text 和 html,所以简单判断 html 的值就可以知道,应该给扩展类型添加那种指令的接口。

添加扩展对象后大概长成下面的样子。

[{
  "value": "hello "
}, {
  "value": "name",
  "tag": true,
  "html": false,
  "oneTime": false,
  "descriptor": {
    "def": {
      "update": function(){
          // some code
      },
      "bind": function(){
          // some code
      }
    },
    expression: "xx",
    filters: "xx",
    name: "text"
  }
}]

vue 内置的指令都会抛出两个接口 bind 和 update,这两个接口的作用是,编译的最后一步是执行所有用到的指令的 bind 方法,而 update 方法则是当 watcher 触发 update 时,Directive 会触发指令的 update 方法。

observe -> 触发setter -> watcher -> 触发update -> Directive -> 触发update -> 指令。

再然后将所有 tag 为 true 的数据中的扩展对象拿出来生成一个 Directive 实例并添加到 _directives 中(_directives是当前vm中存储所有directive实例的地方)。

this._directives.push(
  new Directive(descriptor, this, node, host, scope, frag)
)

最后就是循环 _directives 执行所有 directive实例的 _bind 方法。

Directive 中 _bind 方法的作用有几点:

  • 调用所有已绑定的指令的 bind 方法
  • 实例化一个 Watcher,将指令的 update 与 watcher 绑定在一起(这样就实现了 watcher 接收到消息后触发的 update 方法,指令可以做出对应的更新视图操作)
  • 调用指令的 update,首次初始化视图

这里有一个点需要注意一下,实例化 Watcher 的时候,Watcher会将自己主动的推入Dep依赖中。

总结

响应式原理共有四个部分,observe、Dep、watcher、Directive。

observer可以监听数据的变化。

Dep 可以知道数据变化后通知给谁。

Watcher 可以做到接收到通知后将执行指令的update操作。

Directive 可以把 Watcher 和 指令 连在一起。

不同的指令都会有update方法来使用自己的方式更新 dom。

必须使用watcher触发getter,Dep才会收集依赖。

执行流:

当数据触发 setter 时,会发消息给所有 watcher,watcher 会跟执行指令的 update 方法来更新视图。

当指令在页面上修改了数据会触发 watcher 的 set 方法来修改数据。

实现DEMO

因为实现部分思想逻辑不想再写一遍了,所以直接上代码,看注释理解。

Observer

Observer 的实现主要有下面几点:

  1. 对数据的劫持。通过 getter 和 setter 实现。
  2. 依赖的添加。依赖添加的时机应该在读取数据时,也就是调用 getter 方法的时候,但不是所有的读取都会添加依赖,需要在编译模板时判断。
  3. 依赖添加方式。因为 Observer 和 Watcher 是独立的,所以添加依赖的判断是通过一个全局变量 Dep.target 实现的。
/**
 * Created by Jay-W on 2017/2/12.
 */

function Observer(data) {
    this._data = data;
    this.covertData();
}

Observer.prototype = {
    covertData: function () {
        Object.keys(this._data).forEach(function (key) {
            this.defineReactive(key, this._data[key]);
        }, this);
    },
    defineReactive: function (key, value) {
        // 监听的是数据,依赖应该直接和数据关联,所以在这里生成依赖处理对象
        var dep = new Dep();

        // 继续监听子数据
        if(value && typeof value === "object") new Observer(value);

        // 改写 getter、setter
        Object.defineProperty(this._data, key, {
            enumerable: true,
            configurable: false,
            get: function () {
                if(Dep.target) {
                    dep.depend();
                }
                return value;
            },
            set: function (newVal) {
                if(newVal === value) return;
                value = newVal;
                dep.notify();
            }
        })
    }
};

function Dep() {
    this.deps = [];
}
Dep.prototype = {
    depend: function () {
        // 调用 Watch 方法,并将 Dep 实例传递过去
        Dep.target.addDep(this);
    },
    addDep: function (dep) {
        this.deps.push(dep);
    },
    notify: function () {
        // 当数据更新时,依次调用 watch 的 update 方法
        this.deps.forEach(function (dep) {
            dep.update();
        })
    }
};

Watcher

watcher 是连接 Observer 和 Compiler 的桥梁,其实现也比较简单,主要要注意下面两点:

  1. 生成 Watcher 实例的时候,通过读取 vm 的数据主动触发 getter 方法,实现依赖的添加。
  2. 将添加依赖和更新 DOM 的方法暴露给 Observer。
/**
 * Created by Jay-W on 2017/2/12.
 */

function Watcher(vm, exp, updater) {
    this.vm = vm;
    this.exp = exp;
    this.updater = updater;
    this.value = this._getVmValue();
}

Watcher.prototype = {
    addDep: function (dep) {
        // 调用传递过来的 Dep 实例方法,将 watch 自身添加到依赖中
        if(dep.deps.indexOf(this) === -1){
            dep.addDep(this);
        }
    },
    update: function () {
        var oldValue = this.value;
        var value = this._getVmValue();
        if(oldValue === value) return;
        this.updater && this.updater.call(this.vm, this._getVmValue());
        this.value = value;
    },
    _getVmValue: function () {
        // 通过主动调用 getter 方法来将自身添加到 Dep
        Dep.target = this;
        var val = this.vm.$data;
        this.exp.split(".").forEach(function (key) {
            val = val[key];
        });
        Dep.target = null;
        return val;
    }
};

Compiler

这块主要就是编译模板,将属性转化成指令,同时在添加指令的时候生成 Watcher 实例。

/**
 * Created by Jay-W on 2017/2/12.
 */

function Compiler(el, vm) {
    this._el = this.isElementNode(el) ? el : document.querySelector(el);
    this._vm = vm;

    // 开始编译模板
    var fragment = this.node2Fragment(this._el);
    this.compile(fragment);
    this._el.appendChild(fragment);
}

Compiler.prototype = {
    node2Fragment: function (el) {
        var fragment = document.createDocumentFragment();
        while (el.firstChild) fragment.appendChild(el.firstChild);
        return fragment;
    },
    compile: function (el) {
        [].slice.apply(el.childNodes).forEach(function (node) {
            var nodeContent = node.textContent.trim(),
                reg = /\{\{(.*?)\}\}/,
                isTextNode = nodeContent.match(reg);

            if (this.isElementNode(node)) { // 当子节点为元素节点
                this._compileElementNode(node);
            } else if (this.isTextNode(node) && isTextNode) { // 当子节点为文本节点
                this._compileTextNode(node, isTextNode[1]);
            }

            // 当仍然存在子节点时,继续迭代
            if (node.childNodes && node.childNodes.length) {
                this.compile(node);
            }
        }, this);
    },
    _compileElementNode: function (node) {
        [].slice.apply(node.attributes).forEach(function (attr) {
            var attrName = attr.name,
                reg = /v-([^\:]+)\:?(.*)/,
                isDirective = attrName.match(reg);
            if (isDirective) {
                var dir = isDirective[1],
                    extra = isDirective[2],
                    exp = attr.value;

                Directive[dir] && Directive[dir](node, this._vm, dir, exp, extra);

                // 移除指令属性
                node.removeAttribute(attrName);
            }
        }, this);
    },
    _compileTextNode: function (node, exp) {
        Directive.textNode(node, this._vm, exp);
    },
    isElementNode: function (el) {
        return el.nodeType === 1;
    },
    isTextNode: function (el) {
        return el.nodeType === 3;
    }
};


// 指令集
var Directive = {
    _bind: function (node, vm, dir, exp) {
        var updateFn = this._updates[dir];
        updateFn && updateFn(node, this._getVmValue(vm ,exp));
        new Watcher(vm, exp, function (value) {
            updateFn && updateFn(node, value);
        });
    },
    html: function (node, vm, exp) {
        this._bind(node, vm, "html", exp);
    },
    text: function (node, vm, exp) {
        this._bind(node, vm, "nodeText", exp);
    },
    textNode: function (node, vm, exp) {
        this._bind(node, vm, "nodeText", exp);
    },
    model: function (node, vm, dir, exp) {
        this._bind(node, vm, dir, exp);

        var oldVal = this._getVmValue(vm, exp),
            self = this;
        node.addEventListener("input", function () {
            var value = this.value;
            if(value === oldVal) return;
            self._setVmValue(vm, exp, value);
        })
    },
    on: function (node, vm, dir, exp, extra) {
        var eventFn = vm.$options.methods && vm.$options.methods[exp];
        if (extra && eventFn) {
            node.addEventListener(extra, eventFn.bind(vm), false);
        }
    },
    _getVmValue: function (vm, exp) {
        var data = vm.$data;
        exp.split(".").forEach(function (k) {
            data = data[k];
        });
        return data;
    },
    _setVmValue: function (vm, exp, value) {
        var data = vm.$data,
            keys = exp.split(".");
        keys.forEach(function (k, i) {
            if(i < keys.length - 1){
                data = data[k];
            } else {
                data[k] = value;
            }
        });
    },

    // update 更新规则列表
    _updates: {
        nodeText: function (node, value) {
            node.textContent = typeof value === 'undefined' ? '' : value;
        },
        model: function (node, value) {
            node.value = typeof value === "undefined" ? "" : value;
        },
        html: function (node, value) {
            node.innerHTML = typeof value === "undefined" ? "" : value;
        }
    }
};

最后

就这个简单的 MVVM 框架,我反复看了、写了很多遍,学到了很多。整体来说,实现不难,但这个设计思想,我觉得我现在是肯定想不出来的。。。要走的路还很长嘛

全部代码地址:github