Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

看了多篇Vue内核文章原理还是不太明白,看看这篇试试? #20

Open
jackiewillen opened this issue Dec 12, 2019 · 1 comment

Comments

@jackiewillen
Copy link
Owner

No description provided.

@jackiewillen jackiewillen changed the title 看了多篇Vue内核原理文章还是不太明白,来看看这篇试试? 看了多篇Vue内核原理文章还是不太明白,看看这篇试试? Dec 12, 2019
@jackiewillen
Copy link
Owner Author

jackiewillen commented Dec 12, 2019

    作者:殷荣桧@腾讯

    Vue最精华的部分就是双向绑定,在此基础上,又添加了computed,watch, methods等方法。所以要看懂Vue内核,那第一步肯定就是要了解Vue双向绑定的原理,但是说实话,看了网上很多,好多代码都是经过重构优化后的代码,失去了代码原始的面貌,不太易于理解。所以决定写一个原始一点但是又尽可能简洁一点的,但是原理绝对是Vue双向绑定的原理,确保你看懂这篇文章,就能够了解Vue内核。采用最少的代码,来实现一个个功能。有什么写的不妥的地方,烦请在仓库issue中指出,我好及时修正。
  这个项目的github地址为build-your-own-vue 欢迎star

如果你对当前流行的轮子的原理感兴趣,下面这些都是我用尽可能少的代码,尽可能易于理解的方式实现的框架的原理,这些你也可以看看,有疑问欢迎在各个仓库下留言:

build-your-own-react

build-your-own-vuex

build-your-own-redux

build-your-own-flux

接下来所讲的这些就为了实现下面这个简单的双向绑定:

<div id="app">
    {{name}}
</div>
<script type="text/javascript">
    let vue = new Vue({
        el: '#app',
        data: {
            name: 'jackieyin'
        }
    })
    window.vue = vue;
</script>

在chrome devtools控制台中通过this.vue.name = 'willen'可以自动更新页面中的name为’willen‘。看看结果:

双向绑定结果

(1)从最容易的Dependency.js开始说。

先来看代码:

let Watcher = null; // 用来表明有没有监视器实例,这会你可能不懂,下面会遇到它,然后讲解
class Dep { // 把与一个变量相关的监听器都存在subs这个变量中
    constructor() {
        this.subs = []; // 定义一个subs容器
    }
    notify() {
        // 执行所有与变量相关的回调函数,容器中的watcher一个个都执行掉(看不懂watcher没关系,第二结中就会讲解)
        this.subs.forEach(sub => sub.update());
    }
    addSub(watcher) { // 将一个一个的watcher放入到sub的容器中(看不懂watcher没关系,第二结中就会讲解)
        // 添加与变量相关的订阅回调
        this.subs.push(watcher);
    }
}

从代码看下来,Dep就是subs容器,是一个数组,将一个个的watcher都放到subs容器中。watcher就是一个个的回调函数,都放在subs的容器中等待触发。addSub中的this.subs.push(watcher)就是将一个个的watcher回调函数放入到其中。notify就是用来将subs中的watcher都触发掉。watcher中就是一个一个更新页面中对应的变量的函数。这个下面会说到。

(2)接下来就看看这个watcher是什么?

class Watch {
    constructor(vue, exp, cb) {
        this.vue = vue; // 将vue实例传入到watcher中
        this.exp = exp; // 需要对那个表达式进行监控,比如对上例中的'name'进行监控,那么这里的exp就是'name'
        this.cb = cb; // 一但监听到上述exp表达式子的值发生变化,需要通知到的cb(callback)回调函数
        this.hasAddedAsSub = false; // 有没有被添加到Dep中的Subscriber中去,有的话就不需要重复添加
        this.value = this.get(); // 得到当前vue实例上对应表达式exp的最新的值
    }
    get() {
        Watcher = this; // 这边的Watcher为什么需要放入this,并在下面又置空,你需要继续向下看,暂且先记着,这边把现在的watcher实例放到了Watcher中了。
            var value = this.vue[this.exp]; // 得到表达式的值,就是得到'name'表达式的值为‘willen’(通过chrome devtools控制台中通过this.vue.name = 'willen'修改了name为’willen‘。)
        Watcher = null; // 将Watcher置空,让给下一个值
        return value; // 将获取到的表达式的值返回出去
    }
    update() {
        let value = this.get(); // 通过get()函数得到当前的watcher监听的表达式的值,例如上面的‘willen’
        let oldVal = this.value; // 获取旧的值
        if(value !== oldVal) {  // 对比新旧表达式‘name’的值,发现修改前为'jackieyin',修改后为'willen',说明需要更新页面
            this.value = value; // 把现在的值记录下来,用于和下次比较。
            this.cb.call(this.vue, value); // 用现在的值willen去执行回调函数,其实就是更新一下页面中的{{name}}从‘jackieyin’ 为‘willen’
        }
    }
}

(3) 接下来看一下Observer,这个类是做什么工作的。

class Observer {
    constructor(data) {
        this.defineReactive(data); // 将用户自定义的data中的元素都进行劫持观察,从而来实现双向绑定
    }
    defineReactive(data) { // 开始对用户定义的数据进行劫持
        var dep  = new Dep(); //这个就是第一节中提及到的Dependency类。用来收集双向绑定的各个数据变化时都有的依赖watcher
        Object.keys(data).forEach(key => { // 遍历用户定义的data,其实现在也就一个‘name’字段
            var val = data[key]; // 得到data['name']的值为jackieyin
            Object.defineProperty(data, key, {
                get() { // 使用get对data中的name字段进行劫持
                    if(Watcher) { // 这个就是第二结中提及的Watcher了,(第二结中Watcher = this赋值后这边才会进入if)
                        if(!Watcher.hasAddedAsSub) { // 对于已经添加到订阅列表中的监视器则无需再重复添加了,防止将watcher重复添加到subs容器中,没有意义,因为一会儿更新{{name}}从‘jackieyin’到‘willen’,更新两三次也还还是一个结果
                            dep.addSub(Watcher); // 将监视器watcher添加到subs订阅列表中
                            Watcher.hasAddedAsSub = true;  // 表明这个结果已经添加到subs容器中了
                        }
                    }
                    return val; // 将name中的值返回出去
                },
                set(newVal) { // 对this.vue.name = 'willen'这个set行为进行劫持
                    if(newVal === val) { // 新值(例如还是this.vue.name = 'jackieyin')与之前的值相同,不做任何修改
                        return;
                    }
                    val = newVal; // 将vue实例上对应的值(name的值)修改为新的值
                    dep.notify(); // 通知subs中watcher都触发来对页面进行更新,将页面中的{{name}}处的‘jackieyin’更新为'willen'
                }
            })
        });
    }
}

(4) 最后再一起来看看编译类Compile,这个是用来对{{name}}进行编译,说白了就是在你的实例的data对象中,找到name: 'jackieyin',然后在页面上将{{name}}替换为‘jackieyin’

class Compile {
    constructor(el, vue) {
        this.$vue = vue; // 拷贝vue实例,之所以加$符号,表示暴露给用户的,经常在Vue中看到这种带$标志的,说明是暴露给用户使用的。
        this.$el = document.querySelector(el); // 获取到dom对象,其实就是document.querySelector('#app'); 
        if(this.$el) { // 如果存在可以挂在的实例
            // 在$fragment中操作,比this.$el中操作节省很多性能,所以要赋值给fragment
            let $fragment = this.node2Fragment(this.$el); // 将获取到的el的地方使用片段替代,这是为了便于在内存中操作,使得更新页面更加快速
            this.compileText($fragment.childNodes[0]); // 将模板中的{{}}替换成对应的变量,如{{name}}替换为'jackieyin'
            this.$el.appendChild($fragment); // 将el获取到的dom节点使用内存中的片段进行替换
        }
    }
    node2Fragment(el) { // 用来把dom中的节点赋值到内存fragment变量中去
        // 将node节点都放到fragment中去
        var fragment  = document.createDocumentFragment();
        fragment.appendChild(el.firstChild);// 将el中的元素放到fragment中去,并删除el中原有的,这个是appendChild自带的功能
        return fragment;
    }

    compileText(node) {
        // 对包含可能出现vue标识的部分进行编译,主要是将{{xxx}}替换成对应的值,这边是用正则表达式检测{{}}进行替换
        var reg = /\{\{(.*)\}\}/; // 用来判断有没有vue的双括号的
        if(reg.test(node.textContent)) {
            let matchedName = RegExp.$1;
            node.textContent = this.$vue[matchedName];
            new Watch(this.$vue, matchedName, function(value) { // 对当前的表达式‘name’添加watcher监听器,其实后来就是把这个watcher放入到了dep中的subs的数组中了。当'name'更新为‘willen’后,其实就是执行了这边的node.textContent = value就把页面中的jackieyin替换成了willen了。这就是双向绑定了。node其实就是刚才存放在内存中的$fragement的节点,所以相当于直接操作了内存,所以更新页面就比修改DOM更新页面快多了。
                node.textContent = value;
            });
        }
    }
}

(5)这个时候就可以来组装出一个我们自己的小型的Vue了。

class Vue {
    constructor(options) {
        let data = this._data = options.data || undefined;
        this._initData(); // 将data中的数据都挂载到this上去,使得this.name 相当于就是得到了this._data.name
        new Observer(data); // 将data中的数据进行劫持
        new Compile(options.el, this); // 将{{name}}用data中的’jackieyin‘数据替换掉
    }
    _initData() {
        // 这个函数的功能很简单,就是把用户定义在data中的变量,都挂载到Vue实例(this)上
        let that = this;
	        Object.keys(that._data).forEach((key) => {
	            Object.defineProperty(that, key, {
	                get: () => {
	                    return that._data[key];
	                },
	                set: (newVal) => {
	                    that._data[key] = newVal;
	                }
	            })
	        });
	    }
	}

(6)大功告成,把我们所写的零件组装在一起试一下我们的小型的vue是否工作正常。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <div id="app">
        {{name}}
    </div>
    <script src="./js/Dependency.js"></script>
    <script src="./js/Observer.js"></script>
    <script src="./js/Watch.js"></script>
    <script src="./js/Compile.js"></script>
    <script src="./js/Vue.js"></script>
    <script type="text/javascript">
        let vue = new Vue({
            el: '#app',
            data: {
                name: 'jackie'
            }
        })
        window.vue = vue;
    </script>
</body>
</html>

双向绑定结果

怎么样,搞定了,其实,这只是Vue的冰山一角(下图中的绿色框框的部分),在这个仓库中还实现了一系列vue的功能,如果你有兴趣可以一个commit一个commit的往上看,每个commit都只实现一个完整的细小的功能,而且代码量都尽可能的少,你如果想看一定能看懂。这仓库都是没有使用虚拟DOM来实现,更新颗粒度细,现在的Vue降低了更新的颗粒度,用了虚拟DOM,但是Vue中双向绑定的原理始终未变,所以这篇文章还是需要看懂的,老弟。以后有时间我再研究研究虚拟DOM写个仓库。

123

如发现文章有什么错误,可以在 我的github中进行评论留言。如果你觉的文章写的还可以, 欢迎star

文章vue内核仓库地址

@jackiewillen jackiewillen changed the title 看了多篇Vue内核原理文章还是不太明白,看看这篇试试? 看了多篇Vue内核文章原理还是不太明白,看看这篇试试? Dec 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant