美文网首页
原来vue的原理是这样的

原来vue的原理是这样的

作者: 红英当头 | 来源:发表于2019-09-29 11:50 被阅读0次

vue.js中有两个核心功能:响应式数据绑定,组件系统。主流的mvc框架都实现了单向数据绑定,而双向绑定无非是在单向绑定基础上给可输入元素添加了change事件,从而动态地修改model和view。
介绍vue原理之前,我们先简单回顾一下什么是mvc。
[阮老师mvc详解链接点这里]

MVC

阮链接

mvc模型

视图(View):用户界面。
控制器(Controller):业务逻辑
模型(Model):数据保存

通信方式

View 传送指令到 Controller
Controller 完成业务逻辑后,要求 Model 改变状态
Model 将新的数据发送到 View,用户得到反馈

MVP

mvp模型
  1. 各部分之间的通信,都是双向的。

  2. View 与 Model 不发生联系,都通过 Presenter 传递。

  3. View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

MVVM

MVVM模型
可以看到,MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。AngularEmber 都采用这种模式。

好了,接下来来介绍本章的重点,vue的原理。

1.vuejs双向绑定

html代码

 <input type="text" id="a">
    <span id="b"></span>

js代码

       var obj = {};
        Object.defineProperty(obj, 'hello', {
            set: function (newVal) {
              document.getElementById('a').value = newVal;
              document.getElementById('b').innerHTML = newVal;
            }
        })
        document.addEventListener('keyup', function (e) {
          obj.hello = e.target.value;
        });

效果1:


效果1

这个效果就是在文本框中输入的值会显示在旁边的<span>标签里。这个例子就是双向绑定的实现,但是仅仅为了说明原理,这个和我们平时用的vue.js还有差距,下面是我们常见的vue.js写法

html代码

    {{ text }} 

js代码

    el: 'app',
    data: {
      text: 'hello world'
    }
  }) 

为了实现这样的容易理解的代码vue.js背后做了很多工作,我们一一分解。

  1. 输入框以及文本节点与data中的数据绑定
  2. 输入框变化的时候,data中的数据同步变化。即MVVM中 view => viewmodel的变化
  3. data中的数据变化时,文本节点的内容同步变化。即MVVM中viewmode => view的变化

2 数据初始化绑定

介绍数据初始化绑定之前先说一下DocumentFragment。DocumentFragment(文档片段)可以看做是节点容器,它可以包含多个子节点,可以把它插入到DOM中,只有它的子节点会插入目标节点,所以可以把它看做是一组节点容器。使用DocumentFragment处理节点速度和性能优于直接操作DOM。Vue进行编译的时候就是将挂载目标的所有子节点劫持到DocumentFragment中,经过处理后再将DocumentFragment整体返回到挂载目标。实例代码如下:

 var dom = nodeToFragment(document.getElementById("app"));
        console.log(dom);
        function nodeToFragment (node, vm) {
            var flag = document.createDocumentFragment();
            var child;
            while (child = node.firstChild) {
                flag.appendChild(child); // 劫持node的所有节点
            }
            return flag;
        }
        document.getElementById("app").appendChild(dom);

有了文档片段之后再看看初始化绑定。

html代码:

    <input type="text" v-model="text">
    {{text}}
</div> 

js代码

function compile (node, vm) {
        var reg = /\{\{(.*)\}\}/;
        // 节点类型为元素
        if (node.nodeType === 1) {
            var attr = node.attributes;
            // 解析属性
            for (var i = 0; i < attr.length; i++) {
                if (attr[i].nodeName === 'v-model') {
                    var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                    node.value = vm.data[name]; // 将data的值赋给该node
                    node.removeAttribute('v-model');
                }
            }
        }
        // 节点类型为text
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 获取匹配到的字符串
                name = name.trim()
                node.nodeValue = vm.data[name]; // 将该data的值付给该node
            }
        }
    }

    function nodeToFragment (node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            // 将子节点劫持到文档片段中
            flag.appendChild(child);
        }
        return flag;
    }

    // 构造函数
    function Vue (options) {
        this.data = options.data;
        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
        // 编译完成后把dom返回到app中
        document.getElementById(id).appendChild(dom);
    }

    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world'
        }
    }); 

效果2

我们看到hello word已经绑定到input标签和节点中了

先看compile方法,这个方法主要负责给node节点赋值

  1. compile方法接收两个参数,第一个是DOM节点,第二个vm是当前对象
  2. 判断dom节点类型,如果是1,表示元素(这里判断不太严谨,只是为了说明原理),在node节点的所有属性中查找nodeName为“v-model”的属性,找到属性值,这里是“text”。用当前对象中名字为“text”的属性值给节点赋值,最后删除这个属性,就是删除节点的v-model属性。
  3. 判断dom节点类型,如果是3,表示是节点内容,用正则表达式判断是“{{text}}”这样的字符串,用当前对象中名字为“text”的属性值给节点赋值,直接覆盖掉“{{text}}”

nodeToFragment方法负责创建文档片段,并将compile处理过的子节点劫持到这个文档片段中

  1. 创建一个文档片段
  2. 循环查找传入的node节点,调用compile方法给节点赋值
  3. 将赋值后的节点劫持到文档片段中

Vue构造函数

  1. 用传入参数的data属性给当前对象的data属性赋值
  2. 用传入参数的id标记查找挂载节点,调用nodeToFragment方法获取劫持后的文档片段,这个过程称为编译
  3. 编译完成后,将文档片段插入到指定的当前节点中

实例化vue

  1. 实例化一个vue对象,el属性为挂载节点的id,data属性为要绑定的属性及属性值

响应式数据绑定

初始化绑定只是实现了第一步,然后我们要实现的是在文本框中输入内容的时候,vue实例中的属性值也跟着变化。思路是在文本框中输入数据的时候,触发文本框的input事件(也可以是keyup,change),在相应的事件处理程序中,获取输入内容赋值给当前vue实例vm的text属性。这里利用上面介绍的Object.defeinProperty()方法来给vue实例中data中的属性重新定义为访问器属性,就是在定义这个属性的时候添加get,set这两个存取描述符,这样给vm.text赋值的时候就会触发set方法。然后在set方法中更新vue实例属性的值。看下面的html,js代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>响应式数据绑定</title>
</head>
<body>
<div id="app">
    <input type="text" v-model="text"/>
    {{ text }}
</div>
<script>
    /**
     * 使用defineProperty将data中的text设置为vm的访问器属性
     * @param obj 对象
     * @param 属性名
     * @param 属性值
     * */
    function defineReactive (obj, key, val) {
        Object.defineProperty(obj, key, {
            get: function () {
                return val
            },
            set: function (newVal) {
                if (newVal === val) {
                    return
                }
                val = newVal
                // 输出日志
                console.log(`set方法触发属性值变化${val}`)
            }
        })
    }
    /**
     * 给vue实例定义访问器属性
     * @param obj vue实例中的数据
     * @param vm vue对象
     * */
    function observe (obj, vm) {
        Object.keys(obj).forEach(function (key) {
            defineReactive(vm, key, obj[key]);
        })
    }
    /**
     * 编译过程,给子节点初始化绑定vue实例中的属性值
     * @param node 子节点
     * @param vm vue实例
     * */
    function compile (node, vm) {
        let reg = /\{\{(.*)\}\}/
        // 节点类型为元素
        if (node.nodeType === 1) {
            let attr = node.attributes
            // 解析属性
            for (let i = 0; i < attr.length; i++) {
                if (attr[i].nodeName === 'v-model') {
                    // 获取v-model绑定的属性名
                    let name = attr[i].nodeValue
                    // 添加监听事件
                    node.addEventListener('input', function (e) {
                        // 给相应的data属性赋值,进而触发该属性的set方法
                        vm[name] = e.target.value;
                    });
                    // 将data的值赋给该node
                    node.value = vm.data[name];
                    node.removeAttribute('v-model')
                }
            }
        }
        // 节点类型为text
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                // 获取匹配到的字符串
                let name = RegExp.$1
                name = name.trim()
                // 将data的值赋给该node
                node.nodeValue = vm.data[name]
            }
        }
    }
    /**
     * DocumentFragment文档片段,可以看作节点容器,它可以包含多个子节点,当将它插入到dom中时只有子节点插入到目标节点中。
     * 使用documentfragment处理节点速度和性能要高于直接操作dom。vue编译的时候,就是将挂载目标的所有子节点劫持到documentfragment
     * 中,经过处理后再将documentfragment整体返回到挂载目标中。
     * @param node 节点
     * @param vm vue实例
     * */
    function nodeToFragment (node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            flag.appendChild(child);
        }
        return flag;
    }
    /*vue类*/
    function Vue (options) {
        this.data = options.data
        let data = this.data
        // 给vue实例的data定义访问器属性,覆盖原来的同名属性
        observe(data, this)
        let id = options.el
        let dom = nodeToFragment(document.getElementById(id), this)
        // 编译,劫持完成后将dom返回到app中
        document.getElementById(id).appendChild(dom)
    }

    /*定义一个vue实例*/
    let vm = new Vue({
        el: 'app',
        // 这里的data属性不是访问器属性
        data: {
            text: 'hello world!'
        }
    })
</script>
</body>
</html> 

效果3

下面不再逐句分析,只说重点的。

  1. 在defineReactive方法中,vue实例中的data的属性重新定义为访问器属性,并在set方法中将新的值更新到这个属性上
  2. 在observe方法中,遍历vue实例中data的属性,逐一调用defineReactive方法,把他们定义为访问器属性
  3. 在compile方法中,如果是input这样的标签,给它添加事件(也可以是keyup,change),监听input值变化,并给vue实例中相应的访问器属性赋值
  4. 在Vue类方法中,调用observer方法,传入当前实例对象和对象的data属性,将data属性中的子元素重新定义为当前对象的访问器属性

set方法被触发之后,vue实例的text属性跟着变化,但是<span>的内容并没有变化,下面的内容将会介绍“订阅/发布模式”来解决这个问题。

4 双向绑定的实现

在实现双向绑定之前要先学习一下“订阅/发布模式”。订阅发布模式(又称为观察者模式)定义一种一对多的关系,让多个观察者同时监听一个主题对象,主题对象状态发生改变的时候通知所有的观察者。

发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作

看下面的代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>订阅/发布模式</title>
</head>
<body>
<script>
    /**
     * 定义一个发布者publisher
     * */
    var pub = {
        publish: function () {
            dep.notify();
        }
    }
    /**
     * 三个订阅者
     * */
    var sub1 = {
        update: function () {
            console.log(1);
        }
    };
    var sub2 = {
        update: function () {
            console.log(2);
        }
    };
    var sub3 = {
        update: function () {
            console.log(3);
        }
    }
    /**
     * 一个主题对象
     * */
    function Dep () {
        this.subs = [sub1, sub2, sub3];
    }
    Dep.prototype.notify = function () {
        this.subs.forEach(function (sub) {
            sub.update();
        })
    }
    // 发布者发布消息,主题对象执行notifiy方法,触发所有订阅者响应,执行update
    var dep = new Dep();
    pub.publish();
</script>
</body>
</html>
效果4
  1. 定义发布者对象pub,对象中定义publish方法,方法调用主题对象实例dep的notify()方法
  2. 定义三个订阅者对象,对象中定义update方法,三个对象的update方法分别输出1,2,3
  3. 定义一个主题方法类,主题对象中定义数组属性subs,包含三个订阅者对象
  4. 在主题方法类的原型对象上定义通知方法notify,方法中循环调用三个订阅者对象的update()方法
  5. 实例化主题方法类得到实例dep
  6. 调用发布者对象的通知方法notifiy(),分别输出1,2,3

每当创建一个Vue实例的时候,主要做了两件事情,第一是监听数据:observe(data),第二个是编译HTML:nodeToFragment(id)。
在监听数据过程中,为data的每一个属性生成主题对象dep。
在编译HTML的过程中,为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
前面已经实现了:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性set方法。
接下来我们要实现的是:发出通知dep.notify() => 触发订阅者的updata方法 => 更新视图,实现这个目标的关键是如何将watcher添加到关联属性的dep中去。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>双向绑定的实现</title>
</head>
<body>
<div id="app">
    <input type="text" v-model="text">
    {{ text }}
</div>
<script>
    /**
     * 使用defineProperty将data中的text设置为vm的访问器属性
     * @param obj 对象
     * @param key 属性名
     * @param val 属性值
     */
    function defineReactive (obj, key, val) {
        var dep = new Dep();
        Object.defineProperty(obj, key, {
            get: function () {
                //  如果主题对象类的静态属性target有值, 此时Watcher方法被调用,给主题对象添加订阅者
                if (Dep.target) dep.addSub(Dep.target);
                return val;
            },
            set: function (newVal) {
                if (newVal === val) return
                val = newVal;
                // 主题对象作为发布者收到通知推送给订阅者
                dep.notify();
            }
        })
    }
    /**
     * 给vue实例定义访问器属性
     * @param obj vue实例中的数据
     * @param vm vue对象
     */
    function observe (obj, vm) {
        Object.keys(obj).forEach(function (key) {
            defineReactive(vm, key, obj[key])
        })
    }
    /**
     * DocumentFragment文档片段,可以看作节点容器,它可以包含多个子节点,当将它插入到dom中时只有子节点插入到目标节点中。
     * 使用documentfragment处理节点速度和性能要高于直接操作dom。vue编译的时候,就是将挂载目标的所有子节点劫持到documentfragment
     * 中,经过处理后再将documentfragment整体返回到挂载目标中。
     * @param node 节点
     * @param vm vue实例
     * */
    function nodeToFragment (node, vm) {
        var flag = document.createDocumentFragment();
        var child;
        while (child = node.firstChild) {
            compile(child, vm);
            flag.appendChild(child);
        }
        return flag;
    }

    /**
     * 给子节点初始化绑定vue实例中的属性值
     * @param node 子节点
     * @param vm vue实例
     */
    function compile (node, vm) {
        var reg = /\{\{(.*)\}\}/;
        // 节点类型为元素
        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;
                    node.addEventListener('input', function (e) {
                        // 给相应的data属性赋值,触发set方法
                        vm[name] = e.target.value
                    });
                    // 将data的值赋给该node
                    node.value = vm[name];
                    node.removeAttribute('v-model');
                }
            }
            new Watcher(vm, node, name, 'input')
        }
        if (node.nodeType === 3) {
            if (reg.test(node.nodeValue)) {
                var name = RegExp.$1; // 获取匹配到的字符串
                name = name.trim();
                // 将data的值赋给该node
                new Watcher(vm, node, name, 'text');
            }
        }
    }

    /**
     * 编译 HTML 过程中,为每个与 data 关联的节点生成一个 Watcher
     * @param vm
     * @param node
     * @param name
     * @param nodeType
     * @constructor
     */
    function Watcher (vm, node, name, nodeType) {
        // 将当前对象赋值给全局变量Dep.target
        Dep.target = this;
        this.name = name;
        this.node = node;
        this.vm = vm;
        this.nodeType = nodeType;
        this.update();
        Dep.target = null;
    }
    Watcher.prototype = {
        update: function () {
            this.get();
            if (this.nodeType === 'text') {
                this.node.nodeValue = this.value;
            }
            if (this.nodeType === 'input') {
                this.node.value = this.value;
            }
        },
        get: function () {
            this.value = this.vm[this.name];
        }
    }

    /**
     * 定义一个主题对象
     * @constructor
     */
    function Dep () {
        this.subs = [];
    }

    /**
     * 定义主题对象的添加方法和通知变化方法
     * @type {{addSub: Dep.addSub, notify: Dep.notify}}
     */
    Dep.prototype = {
        addSub: function (sub) {
            this.subs.push(sub);
        },
        notify: function () {
            this.subs.forEach(function (sub) {
                sub.update();
            });
        }
    };

    /**
     * 定义Vue类
     * @param options Vue参数选项
     * @constructor
     */
    function Vue (options) {
        this.data = options.data;
        var data = this.data;
        observe(data, this);
        var id = options.el;
        var dom = nodeToFragment(document.getElementById(id), this);
        // 编译完成后,将dom返回到app中
        document.getElementById(id).appendChild(dom);
    }
    // 定义Vue实例
    var vm = new Vue({
        el: 'app',
        data: {
            text: 'hello world'
        }
    })
</script>
</body>
</html> 
效果5

这里不再逐句分析,只把重点说明一下

  1. 定义主题对象Dep,对象中有addSub和notify两个方法,前者负责向当前对象中添加订阅者,后者轮询订阅者,调用订阅者的更新方法update()
  2. 定义观察者对象方法Watcher,在方法中先将自己赋给一个全局变量Dep.target,其实是给主题类Dep定义了一个静态属性target,可以直接使用Dep.target访问这个静态属性。然后给类定义共有属性name(vue实例中的访问器属性名“text”),node(html标签,如<input>,{{text}}),vm(当前vue实例),nodeType(html标签类型),其次执行update方法,进而执行了原型对象上的get方法,get方法中的this.vm[this.name]读取了vm中的访问器属性,从而触发了访问器属性的get方法,get方法中将wathcer添加到对应访问器属性的dep中,同时将属性值赋给临时变量value。再者,获取属性的值(保存在临时变量value中),然后更新视图。最后将Dep.target设为空。因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。
  3. 在编译方法compile中,劫持子节点的时候,在节点上定义一个观察者对象Watcher
  4. defineReactive方法中,定义访问器属性的时候,在存取描述符get中,如果主题对象类的静态属性target有值, 此时Watcher方法被调用,给主题对象添加订阅者。

data中的数据重新定义为访问器属性,get中将当前数据对应的节点添加到主题对象中,set方法中通知数据对应的节点更新。编译过程将data数据生成数据节点,并生成一个观察者来观察节点变化。

总结

本文介绍了vue.js的简单实现以及相关的知识,包含MVC,MVP,MVVM的原理,对象的访问器属性,html的文档片段(DocumentFragment),观察者模式。vue.js的实现主要介绍数据编译(compile),通过文档片段实现数据劫持挂载,通过观察者模式(订阅发布模式)的实现数据双向绑定等内容。

参考

https://www.cnblogs.com/tylerdonet/p/9893065.html
http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html

相关文章

  • 原来vue的原理是这样的

    vue.js中有两个核心功能:响应式数据绑定,组件系统。主流的mvc框架都实现了单向数据绑定,而双向绑定无非是在单...

  • vue系列--- vue响应式原理

    vue响应式原理 要说vue响应式原理首先要说的是Object.defindProperty(),这个是响应式原理...

  • VUE(面试题)

    1、vue双向绑定原理vue2.x:vue双向绑定的原理是利用了Object.defineProperty()这个...

  • vue-router原理分析与实践

    今天大师兄跟大家简单聊聊Router的实现原理,以及我们如何去实现这样一个插件。 Vue Router 是Vue....

  • vue总结

    vue路由守卫 vue实现双向绑定原理 Vue生命周期: vue跨域方式 vue-router实现原理 vue-r...

  • 原来机械表的运行工作原理是这样的

    很多消费者不仅仅是佩戴机械表,还希望能够弄清楚机械表到底是怎样运行的,机械表的运行工作原理是什么。这大概就是我们常...

  • 面试总结之基础(2)

    Vue2响应式原理 Vue3响应式原理

  • Vue面试题集锦

    VUE的双向绑定原理 原理:在创建Vue实例时,Vue会遍历data选项的属性,利用Object.definePr...

  • vue原理面试题资源整理

    vuex面试题 Vue生命周期 周期2 Vue通信 父子 兄弟 Vue响应式原理 MVVM Vue axios原理...

  • 🥤 简述:Vue2和Vue3开发区别

    响应式原理api的改变Vue2响应式原理采用的是defineProperty,而vue3选用的是proxy。这两者...

网友评论

      本文标题:原来vue的原理是这样的

      本文链接:https://www.haomeiwen.com/subject/fahfpctx.html