通过120行代码理解Vue的双向绑定原理

一. 通过简单代码模拟Vue的实现

    在网上找到一个Vue简单实现的例子, 为了更好的理解这个例子,
通过对这段代码下断点调试, 理解了每一行代码, 并且对代码做了详细的注释, 
方便以后阅读.

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"></head>
<body>
    <div id="test_bind">
        <input v-model="xxx">
        <input v-model="xxx">
        <div>{{xxx}}</div>
    </div>
    <script>
        function nodeContainer(node, vm, flag) {      // 分析模板, 需要填充和需要绑定的
            var flag = flag || document.createDocumentFragment();    // 准备一个节点用于返回
            var child;
            while (child = node.firstChild) {              // 遍历
                compile(child, vm);                          // 分析当前子模板节点
                flag.appendChild(child);                     // 将已分析的节点挂在返回节点下
                if (child.firstChild) {                      // 还有子节点就递归
                    nodeContainer(child, vm, flag);          // 递归
                }
            }
            return flag;                              // 返回挂了分析后的节点, 他们会被watcher更新
        }
        function compile(node, vm) {                  // 分析单个模板节点
            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;            // 绑定的变量名
                        node.addEventListener('input', function (e) {     // 监听输入事件
                            vm[name] = e.target.value;           // 触发这个变量名的set事件
                        });
                        node.value = vm[name];                // 将实例中的data数据赋值给节点
                        node.removeAttribute('v-model');      // 移除该属性
                        new Watcher(vm, node, name);          // 添加watcher
                    }
                }
            }
            if (node.nodeType === 3) {                         // 文本类型
                if (/\{\{(.*)\}\}/g.test(node.nodeValue)) {      // 发现{{xxx}}
                    var name = RegExp.$1;
                    name = name.trim();                          // 取出xxx名字
                    new Watcher(vm, node, name);                 // 添加watcher
                }
            }
        }
        function defineReactive(obj, key, value) {   // 监听data的get和set
            var dep = new Dep();                     // 每个key单独提供一个闭包的Dep对象存watcher
            Object.defineProperty(obj, key, {        // 替换get, set
                get: function () {                   // 监听到get
                    if (Dep.global) {                  // 判断是否有新的watcher
                        dep.add(Dep.global);           // 存入新的watcher
                        Dep.global = null;             // 避免重复存入
                    }
                    return value;                      // 返回value, 先是默认值, set后就是新值
                },
                set: function (newValue) {           // 监听到set
                    if (newValue === value) return;    // 相同值不处理
                    value = newValue;                  // 设置新值
                    dep.notify();                      // 通知当前key对应的watcher
                }
            })
        }
        function observe(obj, vm) {                       // 监听data每个属性的get,set
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            })
        }
        function Vue(options) {                           // Vue构造函数
            this.data = options.data;                       // 保存数据
            var data = this.data;
            observe(data, this);                            // 对data每个属性监听get,set
            var id = options.el;
            var element = document.getElementById(id);      // 取出目标节点对应的dom元素
            var dom = nodeContainer(element, this);         // 分析dom模板, 填充dom模板, 返回
            element.appendChild(dom);                       // 将新的子树附加到目标节点
        }
        function Dep() {                                 // Dep构造函数 
            this.subs = [];                              // 含数组缓存watcher
        }
        Dep.prototype = {
            add: function (sub) {                       // 添加新的watcher
                this.subs.push(sub);                      
            },
            notify: function () {                       // 循环通知每一个watcher
                this.subs.forEach(function (sub) {
                    sub.update();
                })
            }
        }
        function Watcher(vm, node, name) {             // Watcher构造函数
            Dep.global = this;                         // 准备传递watcher
            this.name = name;                          // xxx的名字
            this.node = node;                          // 待填充的dom元素
            this.vm = vm;                              // 当前的Vue对象
            this.update();                             // 首次更新
        }
        Watcher.prototype = {
            update: function () {                      // 将值更新到需要填充的node
                this.value = this.vm[this.name];         // 先触发get获取
                switch (this.node.nodeType) {
                    case 1:
                        this.node.value = this.value;          // 填充到节点
                        break;
                    case 3:
                        this.node.nodeValue = this.value;      // 填充到文本
                        break;
                    default: break;
                }
            }
        }
        var Demo = new Vue({                                 // 构造Vue对象
            el: 'test_bind',                                 // 绑定id
            data: {                                          // 数据
                xxx: '123',                                  // 给xxx默认值"123"
            }
        })
    </script>
</body>
</html>

二. 整体流程描述

  1. 编写dom模板:
      dom写id, input写v-model绑定data.xxx, div显示data.xxx;
  2. 创建Vue对象: 
      填入dom的id, data.xxx的默认值;
  3. 定义Vue的构造函数:
      保存data数据, 
      调用observe对data的全部子属性都监听get, set;
      调用nodeContainer递归遍历分析目标节点下的模板, 对每个需要填充xxx数据的节点添加watcher;
  5. 实现函数defineReactive
      对每个属性创建闭包的变量dep, 每个dep有一个数组存watcher;
  6. 实现函数nodeContainer
      遍历+递归,
      解析目标dom子节点包含的xxx名字需要填充的, 或者v-model绑定了事件的. 
      compile解析v-model, 监听输入事件, 将xxx名字和对应的node添加watcher,
      compile解析目标中的{{xxx}}, 将xxx名字添加watcher;
      创建临时节点, 把目标id的子节点作为临时节点的子节点返回. 
  7. 定义Watcher的构造函数:
      通过Dep.global传递当前watcher,  记下xxx, node, vm, 触发第一次update,
      update访问vm[xxx]触发了get (将Dep.global缓存的watcher存入xxx对应dep内的数组),
      update根据节点类型设置node.value;

三. 验证

  • 操作:

       将以上代码保存为html, 在浏览器打开, input控件输入文本;

  • 代码层面的处理:
  1. 触发complie时设置的对input事件的监听函数;
  2. 监听函数设置vm[xxx], 触发defineReactive定义的set;
  3. set触发闭包的dep的notify, notify对数组内的watcher触发update;
  4. update操作更新node.value, 文本node.nodeValue;
  • 看到的现象:

       两个输入框不管输入哪个, 三个dom元素子显示的内容都同步发生变化.

四. 总结

以上代码详细分析了Vue的实现过程, 
首先是创建Vue对象, 监听data的属性的get, set;
然后是分析模板中需要填充的node用watcher记下, 后续触发watcher更新该节点;
分析模板中的v-model, 对input输入事件做监听, 当input发生时, 触发set, set又触发watcher;
watcher对缓存的xxx对应的node更新; 最终实现了双向绑定;

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值