vue.js动态数据绑定进修

关于vue.js的动态数据绑定,经由重复地看源码和博客解说,总算能够明白它的完成了,心累~ 分享一下进修效果,同时也算是做个纪录。完全代码GitHub地点:https://github.com/hanrenguang/Dynamic-data-binding。也能够到堆栈的 README 浏览本文。

团体思绪

不晓得有无同砚和我一样,看着vue的源码却不知从何最先,真叫人头大。硬生生地看了observer, watcher, compile这几部分的源码,只以为一脸懵逼。终究,从这里取得启示,作者写得很好,值得一读。

关于动态数据绑定呢,须要搞定的是 Dep , Observer , Watcher , Compile 这几个类,他们之间有着种种联络,想要搞懂源码,就得先相识他们之间的联络。下面来理一理:

  • Observer 所做的就是挟制监听一切属性,当有更改时关照 Dep

  • WatcherDep 增添定阅,同时,属性有变化时,Observer 关照 DepDep 则关照 Watcher

  • Watcher 取得关照后,挪用回调函数更新视图

  • Compile 则是剖析所绑定元素的 DOM 组织,对一切须要绑定的属性增添 Watcher 定阅

由此能够看出,当属性发作变化时,是由Observer -> Dep -> Watcher -> update viewCompile 在最最先剖析 DOM 并增添 Watcher 定阅后就急流勇退了。

从递次实行的递次来看的话,即 new Vue({}) 以后,应该是如许的:先经由过程 Observer 挟制一切属性,然后 Compile 剖析 DOM 组织,并增添 Watcher 定阅,再以后就是属性变化 -> Observer -> Dep -> Watcher -> update view,接下来就说说详细的完成。

从new一个实例最先谈起

网上的许多源码解读都是从 Observer 最先的,而我会从 new 一个MVVM实例最先,依据递次实行递次去诠释或许更随意马虎明白。先来看一个简朴的例子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <div class="test">
        <p>{{user.name}}</p>
        <p>{{user.age}}</p>
    </div>

    <script type="text/javascript" src="hue.js"></script>
    <script type="text/javascript">
        let vm = new Hue({
            el: '.test',
            data: {
                user: {
                    name: 'Jack',
                    age: '18'
                }
            }
        });
    </script>
</body>
</html>

接下来都将以其为例来剖析。下面来看一个简单的 MVVM 的完成,在此将其命名为 hue。为了随意马虎起见,为 data 属性设置了一个代办,经由过程 vm._data 来接见 data 的属性显得贫苦且冗余,经由过程代办,能够很好地处置惩罚这个题目,在解释中也有申明。增添完属性代办后,挪用了一个 observe 函数,这一步做的就是 Observer 的属性挟制了,这一步详细怎样完成,临时先不睁开。先记着他为 data 的属性增添了 gettersetter

function Hue(options) {
    this.$options = options || {};
    let data = this._data = this.$options.data,
        self = this;

    Object.keys(data).forEach(function(key) {
        self._proxyData(key);
    });

    observe(data);

    self.$compile = new Compile(self, options.el || document.body);
}

// 为 data 做了一个代办,
// 接见 vm.xxx 会触发 vm._data[xxx] 的getter,取得 vm._data[xxx] 的值,
// 为 vm.xxx 赋值则会触发 vm._data[xxx] 的setter
Hue.prototype._proxyData = function(key) {
    let self = this;
    Object.defineProperty(self, key, {
        configurable: false,
        enumerable: true,
        get: function proxyGetter() {
            return self._data[key];
        },
        set: function proxySetter(newVal) {
            self._data[key] = newVal;
        }
    });
};

再往下看,末了一步 new 了一个 Compile,下面我们就来讲讲 Compile

Compile

new Compile(self, options.el || document.body) 这一行代码中,第一个参数是当前 Hue 实例,第二个参数是绑定的元素,在上面的示例中为class为 .test 的div。

关于 Compile,这里只完成最简朴的 textContent 的绑定。而 Compile 的代码没什么难点,很随意马虎就可以读懂,所做的就是剖析 DOM,并增添 Watcher 定阅。关于 DOM 的剖析,先将根节点 el 转换成文档碎片 fragment 举行剖析编译操纵,剖析完成后,再将 fragment 增添回本来的实在 DOM 节点中。来看看这部分的代码:

function Compile(vm, el) {
    this.$vm = vm;
    this.$el = this.isElementNode(el)
        ? el
        : document.querySelector(el);

    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}

Compile.prototype.node2Fragment = function(el) {
    let fragment = document.createDocumentFragment(),
        child;

    // 或许有同砚不太明白这一步,无妨着手写个小例子视察一下他的行动
    while (child = el.firstChild) {
        fragment.appendChild(child);
    }

    return fragment;
};

Compile.prototype.init = function() {
    // 剖析 fragment
    this.compileElement(this.$fragment);
};

以上面示例为例,此时若打印出 fragment,可视察到其包括两个p元素:

<p>{{user.name}}</p>
<p>{{user.age}}</p>

下一步就是剖析 fragment,直接看代码及解释吧:

Compile.prototype.compileElement = function(el) {
    let childNodes = Array.from(el.childNodes),
        self = this;

    childNodes.forEach(function(node) {
        let text = node.textContent,
            reg = /\{\{(.*)\}\}/;

        // 若为 textNode 元素,且婚配 reg 正则
        // 在上例中会婚配 '{{user.name}}' 及 '{{user.age}}'
        if (self.isTextNode(node) && reg.test(text)) {
            // 剖析 textContent,RegExp.$1 为婚配到的内容,在上例中为 'user.name' 及 'user.age'
            self.compileText(node, RegExp.$1);
        }

        // 递归
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);
        }
    });
};

Compile.prototype.compileText = function(node, exp) {
    // this.$vm 即为 Hue 实例,exp 为正则婚配到的内容,即 'user.name' 或 'user.age'
    compileUtil.text(node, this.$vm, exp);
};

let compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },

    bind: function(node, vm, exp, dir) {
        // 猎取更新视图的回调函数
        let updaterFn = updater[dir + 'Updater'];

        // 先挪用一次 updaterFn,更新视图
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));

        // 增添 Watcher 定阅
        new Watcher(vm, exp, function(value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },

    // 依据 exp,取得其值,在上例中即 'vm.user.name' 或 'vm.user.age'
    _getVMVal: function(vm, exp) {
        let val = vm;
        exp = exp.trim().split('.');
        exp.forEach(function(k) {
            val = val[k];
        });
        return val;
    }
};

let updater = {
    // Watcher 定阅的回调函数
    // 在此即更新 node.textContent,即 update view
    textUpdater: function(node, value) {
        node.textContent = typeof value === 'undefined'
            ? ''
            : value;
    }
};

正如代码中所看到的,Compile 在剖析到 {{xxx}} 后便增添了 xxx 属性的定阅,即 new Watcher(vm, exp, callback)。明白了这一步后,接下来就须要相识怎样完成相干属性的定阅了。先从 Observer 最先谈起。

Observer

从最简朴的状况来斟酌,即不斟酌数组元素的变化。临时先不斟酌 DepObserver 的联络。先看看 Observer 组织函数:

function Observer(data) {
    this.data = data;
    this.walk(data);
}

Observer.prototype.walk = function(data) {
    const keys = Object.keys(data);
    // 遍历 data 的一切属性
    for (let i = 0; i < keys.length; i++) {
        // 挪用 defineReactive 增添 getter 和 setter
        defineReactive(data, keys[i], data[keys[i]]);
    }
};

接下来经由过程 Object.defineProperty 要领给一切属性增添 gettersetter,就达到了我们的目标。属性有能够也是对象,因而须要对属性值举行递归挪用。

function defineReactive(obj, key, val) {
    // 对属性值递归,对应属性值为对象的状况
    let childObj = observe(val);

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            // 直接返回属性值
            return val;
        },
        set: function(newVal) {
            if (newVal === val) {
                return;
            }
            // 值发作变化时修正闭包中的 val,
            // 保证在触发 getter 时返回准确的值
            val = newVal;

            // 对新赋的值举行递归,防备赋的值为对象的状况
            childObj = observe(newVal);
        }
    });
}

末了补充上 observe 函数,也即 Hue 组织函数中挪用的 observe 函数:

function observe(val) {
    // 若 val 是对象且非数组,则 new 一个 Observer 实例,val 作为参数
    // 简朴点说:是对象就继承。
    if (!Array.isArray(val) && typeof val === "object") {
        return new Observer(val);
    }
}

如许一来就对 data 的一切子孙属性(不知有无这类说法。。)都举行了“挟制”。明显到目前为止,这并没什么用,或者说假如只做到这里,那末和什么都不做没差异。因而 Dep 上场了。我以为明白 DepObserverWatcher 之间的联络是最主要的,先来谈谈 DepObserver 里做了什么。

Observer & Dep

在每一次 defineReactive 函数被挪用以后,都会在闭包中新建一个 Dep 实例,即 let dep = new Dep()Dep 供应了一些要领,先来说说 notify 这个要领,它做了什么事?就是在属性值发作变化的时刻关照 Dep,那末我们的代码能够增添以下:

function defineReactive(obj, key, val) {
    let childObj = observe(val);
    const dep = new Dep();

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

            val = newVal;
            childObj = observe(newVal);

            // 发作更改
            dep.notify();
        }
    });
}

假如仅斟酌 ObserverDep 的联络,即有更改时关照 Dep,那末这里就算完了,然而在 vue.js 的源码中,我们还能够看到一段增添在 getter 中的代码:

// ...
get: function() {
    if (Dep.target) {
        dep.depend();
    }
    return val;
}
// ...

这个 depend 要领呢,它又做了啥?答案是为闭包中的 Dep 实例增添了一个 Watcher 的定阅,而 Dep.target 又是啥?他实际上是一个 Watcher 实例,???一脸懵逼,先记着就好,先看一部分的 Dep 源码:

// 标识符,在 Watcher 中有用到,先不必管
let uid = 0;

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

Dep.prototype.depend = function() {
    // 这一步相当于做了这么一件事:this.subs.push(Dep.target)
    // 即增添了 Watcher 定阅,addDep 是 Watcher 的要领
    Dep.target.addDep(this);
};

// 关照更新
Dep.prototype.notify = function() {
    // this.subs 的每一项都为一个 Watcher 实例
    this.subs.forEach(function(sub) {
        // update 为 Watcher 的一个要领,更新视图
        // 没错,实际上这个要领终究会挪用到 Compile 中的 updaterFn,
        // 也即 new Watcher(vm, exp, callback) 中的 callback
        sub.update();
    });
};

// 在 Watcher 中挪用
Dep.prototype.addSub = function(sub) {
    this.subs.push(sub);
};

// 初始时援用为空
Dep.target = null;

或许看到这照样一脸懵逼,没紧要,接着往下。大概有同砚会迷惑,为何要把增添 Watcher 定阅放在 getter 中,接下来我们来说说这 WatcherDep 的故事。

Watcher & Dep

先让我们回忆一下 Compile 做的事,剖析 fragment,然后给响应属性增添定阅:new Watcher(vm, exp, cb)new 了这个 Watcher 以后,Watcher 怎样办呢,就有了下面如许的对话:

Watcher:hey Dep,我须要定阅 exp 属性的更改。

Dep:这我可做不到,你得去找 exp 属性中的 dep,他能做到这件事。

Watcher:但是他在闭包中啊,我没法和他联络。

Dep:你拿到了全部 Hue 实例 vm,又晓得属性 exp,你能够触发他的 getter 啊,你在 getter 里动些四肢不就好了。

Watcher:有原理,但是我得让 dep 晓得是我定阅的啊,不然他关照不到我。

Dep:这个简朴,我帮你,你每次触发 getter 前,把你的援用通知 Dep.target 就好了。记得办完预先给 Dep.target 置空。

因而就有了上面 getter 中的代码:

// ...
get: function() {
    // 是不是是 Watcher 触发的
    if (Dep.target) {
        // 是就增添进来
        dep.depend();
    }
    return val;
}
// ...

如今再回头看看 Dep 部分的代码,是不是是好明白些了。如此一来, Watcher 须要做的事变就简朴明了了:

function Watcher(vm, exp, cb) {
    this.$vm = vm;
    this.cb = cb;
    this.exp = exp;
    this.depIds = new Set();

    // 返回一个用于猎取响应属性值的函数
    this.getter = parseGetter(exp.trim());

    // 挪用 get 要领,触发 getter
    this.value = this.get();
}

Watcher.prototype.get = function() {
    const vm = this.$vm;
    // 将 Dep.target 指向当前 Watcher 实例
    Dep.target = this;
    // 触发 getter
    let value = this.getter.call(vm, vm);
    // Dep.target 置空
    Dep.target = null;
    return value;
};

Watcher.prototype.addDep = function(dep) {
    const id = dep.id;
    if (!this.depIds.has(id)) {
        // 增添定阅,相当于 dep.subs.push(this)
        dep.addSub(this);
        this.depIds.add(id);
    }
};

function parseGetter(exp) {
    if (/[^\w.$]/.test(exp)) {
        return;
    }

    let exps = exp.split(".");

    return function(obj) {
        for (let i = 0; i < exps.length; i++) {
            if (!obj)
                return;
            obj = obj[exps[i]];
        }
        return obj;
    };
}

末了还差一部分,即 Dep 关照变化后,Watcher 的处置惩罚,详细的函数挪用流程是如许的:dep.notify() -> sub.update(),直接上代码:

Watcher.prototype.update = function() {
    this.run();
};

Watcher.prototype.run = function() {
    let value = this.get();
    let oldVal = this.value;

    if (value !== oldVal) {
        this.value = value;
        // 挪用回调函数更新视图
        this.cb.call(this.$vm, value, oldVal);
    }
};

结语

到这就算写完了,本人程度有限,如有不足之处迎接指出,一同讨论。

参考资料

https://github.com/DMQ/mvvm

    原文作者:hanrenguang
    原文地址: https://segmentfault.com/a/1190000009469448
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞