【4858美高梅】js响应式原理分析与贯彻,js动态数据绑定学习笔记

By admin in 4858美高梅 on 2019年3月25日

上一节我们曾经分析了vue.js是经过Object.defineProperty以及发布订阅情势来进展数据威胁和监听,并且落成了二个大概的demo。后天,我们就依据上一节的代码,来落实二个MVVM类,将其与html结合在联合署名,并且达成v-model以及{{}}语法。

无意接触前端的时日已经过去半年了,越来尤其现对知识的读书不应当只停留在会用的范畴,这在自家学jQuery的一段时间后便有如此的咀嚼。

对此vue.js的动态数据绑定,经过再三地看源码和博客讲解,总算能够知道它的落实了,心累~
分享一下读书收获,同时也算是做个记录。完整代码GitHub地址:。也得以到库房的
README 阅读本文,容作者厚脸皮地求 star,求 follow。

MVC模式

MVC模式

陈年的MVC形式是单向绑定,即Model绑定到View,当大家用JavaScript代码更新Model时,View就会自动更新

tips:本节新增代码(去除注释)在第一百货公司行左右。使用的Observer和沃特cher都是延用上一节的代码,没有改动

虽说jQuery只是贰个JS的代码库,只要会有个别JS的基本操作学习一两日就能快捷掌握jQuery的着力语法并熟识应用,不过一旦持续解jQUery库背后的完毕原理,相信只要您一段时间不再行使jQuery的话就会把jQuery忘得一清二白,那大概正是知其然不知其所以然的结局。

总体思路

MVVM模式

MVVM方式正是Model–View–ViewModel形式。它达成了View的变更,自动反映在
ViewModel,反之亦然。
作者对于双向绑定的知晓,就是用户更新了View,Model的数量也自动被更新了,那种景况便是双向绑定。再说细点,即是在一边绑定的功底上给可输入成分(input、textare等)添加了change(input)事件,(change事件触发,View的气象就被更新了)来动态修改model。

MVVM模式

接下去,让大家一步步来,完成一个MVVM类。

多年来在学vue的时候又再一遍经历了那般的迷惑,即使能够相比熟悉的牵线vue的主导接纳,也能够对MV*情势、数据勒迫、双向数据绑定、数据代理侃上两句。然则要是某个尖锐一些就有点困难了。所以这几天痛下决心商量大批量技艺文章(起先尝试看早期源码,无奈vue与jQuery不是1个层级的,相比较于jQuery,vue是实在含义上的前端框架。只好无奈弃坑转而看技术博客),对vue也算有了三个管窥蠡测的认识。最终尝试推行一下友好学到的知识,基于数据代理、数据勒迫、模板解析、双向绑定达成了1个袖珍的vue框架。

不亮堂有没有同桌和自个儿同样,看着vue的源码却不知从何早先,真叫人头大。硬生生地看了observer,
watcher,
compile这几有的的源码,只觉得一脸懵逼。最后,从此处获得启发,小编写得很好,值得一读。

双向绑定原理

vue数据双向绑定是经过数据勒迫结合发表者-订阅者形式的不二法门来完毕的。
笔者们早已知道完毕多少的双向绑定,首先要对数码进行恐吓监听,所以大家须求设置二个监听器Observer,用来监听全体属性。要是属性发上扭转了,就需求报告订阅者沃特cher看是不是要求更新。因为订阅者是有过多少个,所以大家要求有1个音讯订阅器Dep来特别收集这一个订阅者,然后在监听器Observer和订阅者沃特cher之间进行合并管理的。接着,大家还索要有贰个发令解析器Compile,对各类节点成分进行扫描和剖析,将相关指令(如v-model,v-on)对应开端化成3个订阅者沃特cher,并替换模板数据依旧绑定相应的函数,此时当订阅者沃特cher接收到相应属性的变通,就会履行相应的翻新函数,从而创新视图。因而接下去大家实践以下二个步骤,达成数据的双向绑定:

1.完成一个监听器Observer,用来威吓并监听全部属性,即便有改动的,就通告订阅者。

2.兑现多个订阅者沃特cher,每四个沃特cher都绑定三个翻新函数,watcher能够接到属性的变更文告并实施相应的函数,从而创新视图。

3.完成3个解析器Compile,能够扫描和剖析每一个节点的连带指令(v-model,v-on等一声令下),借使节点存在v-model,v-on等一声令下,则解析器Compile先河化那类节点的模版数据,使之能够显示在视图上,然后起先化相应的订阅者(沃特cher)。

构造函数

率先,3个MVVM的构造函数如下(和vue.js的构造函数一样):

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }
}

和vue.js一样,有它的data属性以及el成分。

友好提醒:小说是依照每一种模块的贯彻依靠关系来进行剖析的,不过在翻阅的时候能够遵从vue的施行顺序来分析,那样对初学者越发的温馨。推荐的读书顺序为:实现VMVM、数据代理、完成Observe、达成Complie、完结Watcher。

关于动态数据绑定呢,要求消除的是 Dep , Observer , 沃特cher , Compile
那多少个类,他们中间有着各个关系,想要搞懂源码,就得先理解她们中间的联系。上边来理一理:

1.完毕3个Observer

Observer是三个多少监听器,其落到实处核心措施就是Object.defineProperty(
)。就算要对拥有属性都实行监听的话,那么能够透过递归方法遍历全部属性值,并对其展开Object.defineProperty(
)处理
一般来说代码实现了1个Observer。

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

Observer.prototype = {
    walk: function(data) {
        var self = this;
        //这里是通过对一个对象进行遍历,对这个对象的所有属性都进行监听
        Object.keys(data).forEach(function(key) {
            self.defineReactive(data, key, data[key]);
        });
    },
    defineReactive: function(data, key, val) {
        var dep = new Dep();
      // 递归遍历所有子属性
        var childObj = observe(val);
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function getter () {
                if (Dep.target) {
                  // 在这里添加一个订阅者
                  console.log(Dep.target)
                    dep.addSub(Dep.target);
                }
                return val;
            },
           // setter,如果对一个对象属性值改变,就会触发setter中的dep.notify(),通知watcher(订阅者)数据变更,执行对应订阅者的更新函数,来更新视图。
            set: function setter (newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
              // 新的值是object的话,进行监听
                childObj = observe(newVal);
                dep.notify();
            }
        });
    }
};

function observe(value, vm) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
};

// 消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数
function Dep () {
    this.subs = [];
}
Dep.prototype = {
  /**
   * [订阅器添加订阅者]
   * @param  {[Watcher]} sub [订阅者]
   */
    addSub: function(sub) {
        this.subs.push(sub);
    },
  // 通知订阅者数据变更
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;

在Observer中,当初本人看别人的源码时,作者有几许不清楚的地点就是Dep.target是从哪个地方来的,相信有些人和作者会有雷同的疑点。那里不急急,当写到Watcher的时候,你就会发觉,那么些Dep.target是发源沃特cher。

开首化操作

vue.js能够经过this.xxx的格局来直接待上访问this.data.xxx的性质,那或多或少是怎么完结的啊?其实答案很简短,它是通过Object.defineProperty来做动作,当您拜访this.xxx的时候,它回到的莫过于是this.data.xxx。当你改改this.xxx值的时候,其实修改的是this.data.xxx的值。具体能够看如下代码:

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }
  // 初始化
  init() {
    // 对this.data进行数据劫持
    new Observer(this.data);
    // 传入的el可以是selector,也可以是元素,因此我们要在这里做一层处理,保证this.$el的值是一个元素节点
    this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
    // 将this.data的属性都绑定到this上,这样用户就可以直接通过this.xxx来访问this.data.xxx的值
    for (let key in this.data) {
      this.defineReactive(key);
    }
  }

  defineReactive(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.data[key];
      },
      set(newVal) {
        this.data[key] = newVal;
      }
    })
  }
  // 是否是属性节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
}

在完毕开端化操作后,大家必要对this.$el的节点开始展览编写翻译。近来大家要贯彻的语法有v-model和{{}}语法,v-model那个天性只或者会油不过生在要白藏点的attributes里,而{{}}语法则是现身在文书节点里。

源码:

  • Observer 所做的便是勒迫监听全体属性,当有改观时通报 Dep
  • 沃特cher 向 Dep 添加订阅,同时,属性有变化时,Observer 通告 Dep,Dep
    则通告 沃特cher
  • Watcher 获得关照后,调用回调函数更新视图
  • Compile 则是分析所绑定成分的 DOM 结构,对全部须要绑定的性质添加
    沃特cher 订阅

2.贯彻二个沃特cher

Watcher正是二个订阅者。用于将Observer发来的update音讯处理,执行沃特cher绑定的革新函数。
一般来说代码达成了二个沃特cher

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    this.value = this.get();  // 将自己添加到订阅器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this;  // 缓存自己
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null;  // 释放自己
        return value;
    }
};

在自作者探究代码的长河中,作者觉得最复杂的就是精晓这么些函数的参数,后来在本身出口了那一个参数之后,函数的那些功用也不难驾驭了。vm,正是今后要写的SelfValue对象,约等于Vue中的new
Vue的3个目的。exp是node节点的v-model或v-on:click等一声令下的属性值。如v-model=”name”,exp便是”name”。cb,正是沃特cher绑定的翻新函数。
【4858美高梅】js响应式原理分析与贯彻,js动态数据绑定学习笔记。地点的代码中就能够看出来,在Watcher的getter函数中,Dep.target指向了自个儿,也正是沃特cher对象。在getter函数中,

var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数。

此处获得vm.data[this.exp]
时,会调用Observer中Object.defineProperty中的get函数

get: function getter () {
                if (Dep.target) {
                  // 在这里添加一个订阅者
                  console.log(Dep.target)
                    dep.addSub(Dep.target);
                }
                return val;
            },

从而把watcher添加到了订阅器中,也就化解了下面Dep.target是何地来的那一个难题。

fragment

在对节点进行编写翻译在此之前,大家先考虑三个切实可行难点:假使我们在编写翻译进程中央直机关接操作DOM节点的话,每二回修改DOM都会促成DOM的回流或重绘,而这一有的品质损耗是很没有须求的。因而,我们能够运用fragment,将节点转化为fragment,然后在fragment里编写翻译完成后,再将其放回到页面上。

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }

  initDom() {
    const fragment = this.node2Fragment();
    this.compile(fragment);
    // 将fragment返回到页面中
    document.body.appendChild(fragment);
  }
  // 将节点转为fragment,通过fragment来操作DOM,可以获得更高的效率
  // 因为如果直接操作DOM节点的话,每次修改DOM都会导致DOM的回流或重绘,而将其放在fragment里,修改fragment不会导致DOM回流和重绘
  // 当在fragment一次性修改完后,在直接放回到DOM节点中
  node2Fragment() {
    const fragment = document.createDocumentFragment();
    let firstChild;
    while(firstChild = this.$el.firstChild) {
      fragment.appendChild(firstChild);
    }
    return fragment;
  }
}

效益演示如下所示:

由此能够看看,当属性产生变化时,是由Observer -> Dep -> 沃特cher
-> update view,Compile 在最伊始解析 DOM 并累加 沃特cher
订阅后就功成身退了。

3.兑现二个Compile

new SelfVue 绑定的dom节点

Compile重要的意义是把new SelfVue
绑定的dom节点,(也正是el标签绑定的id)遍历该节点的全体子节点,找出里面有着的v-指令和”
{{}} “.
1.若是实节点含有v-指令,便是成分节点,则对那一个成分添加监听事件。(借使是v-on,则node.add伊芙ntListener(‘click’),假若是v-model,则node.add伊芙ntListener(‘input’))。接着开首化模板成分,创制一个沃特cher绑定那个因穷秋点。
2.尽管子节点是文件节点,即” {{ data }} “,则用正则表明式取出” {{ data }}
“中的data,然后var initText =
this.vm[exp],用initText去替代其中的data。
切实代码参见作者的github:
vue-MVVM
其间有详实的注释。

实现v-model

在将node节点转为fragment后,我们来对内部的v-model语法进行编写翻译。

是因为v-model语句只恐怕会并发在要商节点的attributes里,因而,大家先判断该节点是还是不是为成分节点,若为成分节点,则判断其是不是是directive(近年来唯有v-model),若都满意的话,则调用CompileUtils.compileModelAttr来编写翻译该节点。

编写翻译含有v-model的节点首要有两步:

  1. 为要早秋点注册input事件,在input事件触发的时候,更新vm(this.data)上相应的属性值。
  2. 对v-model注重的性质注册叁个沃特cher函数,当重视的习性发生变化,则更新成分节点的value。

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }

  initDom() {
    const fragment = this.node2Fragment();
    this.compile(fragment);
    // 将fragment返回到页面中
    document.body.appendChild(fragment);
  }

  compile(node) {
    if (this.isElementNode(node)) {
      // 若是元素节点,则遍历它的属性,编译其中的指令
      const attrs = node.attributes;
      Array.prototype.forEach.call(attrs, (attr) => {
        if (this.isDirective(attr)) {
          CompileUtils.compileModelAttr(this.data, node, attr)
        }
      })
    }
    // 若节点有子节点的话,则对子节点进行编译
    if (node.childNodes && node.childNodes.length > 0) {
      Array.prototype.forEach.call(node.childNodes, (child) => {
        this.compile(child);
      })
    }
  }
  // 是否是属性节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
  // 检测属性是否是指令(vue的指令是v-开头)
  isDirective(attr) {
    return attr.nodeName.indexOf('v-') >= 0;
  }
}

const CompileUtils = {
  // 编译v-model属性,为元素节点注册input事件,在input事件触发的时候,更新vm对应的值。
  // 同时也注册一个Watcher函数,当所依赖的值发生变化的时候,更新节点的值
  compileModelAttr(vm, node, attr) {
    const { value: keys, nodeName } = attr;
    node.value = this.getModelValue(vm, keys);
    // 将v-model属性值从元素节点上去掉
    node.removeAttribute(nodeName);
    node.addEventListener('input', (e) => {
      this.setModelValue(vm, keys, e.target.value);
    });

    new Watcher(vm, keys, (oldVal, newVal) => {
      node.value = newVal;
    });
  },
  /* 解析keys,比如,用户可以传入
  *  <input v-model="obj.name" />
  *  这个时候,我们在取值的时候,需要将"obj.name"解析为data[obj][name]的形式来获取目标值
  */
  parse(vm, keys) {
    keys = keys.split('.');
    let value = vm;
    keys.forEach(_key => {
      value = value[_key];
    });
    return value;
  },
  // 根据vm和keys,返回v-model对应属性的值
  getModelValue(vm, keys) {
    return this.parse(vm, keys);
  },
  // 修改v-model对应属性的值
  setModelValue(vm, keys, val) {
    keys = keys.split('.');
    let value = vm;
    for(let i = 0; i < keys.length - 1; i++) {
      value = value[keys[i]];
    }
    value[keys[keys.length - 1]] = val;
  },
}

4858美高梅 1

从程序执行的逐条来看的话,即 new Vue({}) 之后,应该是这么的:先经过
Observer 威吓全体属性,然后 Compile 解析 DOM 结构,并累加 沃特cher
订阅,再之后正是性质变化 -> Observer -> Dep -> 沃特cher ->
update view,接下去就说说具体的兑现。

4.落实二个MVVM

能够说MVVM是Observer,Compile以及沃特cher的“boss”了,他索要安顿给Observer,Compile以及沃特che做的工作如下

a、Observer完成对MVVM本身model数据吓唬,监听数据的属性别变化更,并在变更时展开notify
b、Compile实现指令解析,初步化视图,并订阅数据变化,绑定好更新函数
c、沃特cher一方面接收Observer通过dep传递过来的多寡变化,一方面公告Compile实行view
update。
最后,把这几个MVVM抽象出来,便是vue中Vue的构造函数了,能够组织出3个vue实例。

实现{{}}语法

{{}}语法只或许会现出在文件节点中,因而,我们只需求对文本节点做拍卖。借使文本节点中出现{{key}}那种话语的话,我们则对该节点开展编译。在那里,大家能够透过上边那些正则表明式来对文本节点开始展览处理,判断其是不是带有{{}}语法。

const textReg = /\{\{\s*\w+\s*\}\}/gi; // 检测{{name}}语法
console.log(textReg.test('sss'));
console.log(textReg.test('aaa{{  name  }}'));
console.log(textReg.test('aaa{{  name  }} {{ text }}'));

若含有{{}}语法,大家则足以对其处理,由于3个文书节点大概现身多少个{{}}语法,由此编写翻译含有{{}}语法的文件节点首要有以下两步:

  1. 找出该公文节点中拥有信赖的习性,并且保留原来文本消息,依照原有文本音讯还有属性值,生成最后的文件音讯。比如说,原始文本新闻是”test
    {{test}}
    {{name}}”,那么该文件音讯信赖的性质有this.data.test和this.data.name,那么我们得以依照原本信息和属性值,生成最后的文件。
  2. 为该文件节点有所依赖的习性注册沃特cher函数,当信赖的性子发生变化的时候,则更新文本节点的内容。

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }

  initDom() {
    const fragment = this.node2Fragment();
    this.compile(fragment);
    // 将fragment返回到页面中
    document.body.appendChild(fragment);
  }

  compile(node) {
    const textReg = /\{\{\s*\w+\s*\}\}/gi; // 检测{{name}}语法
    if (this.isTextNode(node)) {
      // 若是文本节点,则判断是否有{{}}语法,如果有的话,则编译{{}}语法
      let textContent = node.textContent;
      if (textReg.test(textContent)) {
        // 对于 "test{{test}} {{name}}"这种文本,可能在一个文本节点会出现多个匹配符,因此得对他们统一进行处理
        // 使用 textReg来对文本节点进行匹配,可以得到["{{test}}", "{{name}}"]两个匹配值
        const matchs = textContent.match(textReg);
        CompileUtils.compileTextNode(this.data, node, matchs);
      }
    }
    // 若节点有子节点的话,则对子节点进行编译
    if (node.childNodes && node.childNodes.length > 0) {
      Array.prototype.forEach.call(node.childNodes, (child) => {
        this.compile(child);
      })
    }
  }
  // 是否是文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }
}

const CompileUtils = {
  reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
  // 编译文本节点,并注册Watcher函数,当文本节点依赖的属性发生变化的时候,更新文本节点
  compileTextNode(vm, node, matchs) {
    // 原始文本信息
    const rawTextContent = node.textContent;
    matchs.forEach((match) => {
      const keys = match.match(this.reg)[1];
      console.log(rawTextContent);
      new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
    });
    this.updateTextNode(vm, node, matchs, rawTextContent);
  },
  // 更新文本节点信息
  updateTextNode(vm, node, matchs, rawTextContent) {
    let newTextContent = rawTextContent;
    matchs.forEach((match) => {
      const keys = match.match(this.reg)[1];
      const val = this.getModelValue(vm, keys);
      newTextContent = newTextContent.replace(match, val);
    })
    node.textContent = newTextContent;
  }
}

数量代理

从new2个实例初叶谈起

最终写3个html测试一下大家的坚守

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>self-vue</title>
</head>
<style>
    #app {
        text-align: center;
    }
</style>
<body>
    <div id="app">
        <h2>{{title}}</h2>
        <input v-model="name">
        <h1>{{name}}</h1>
        <button v-on:click="clickMe">click me!</button>
    </div>
</body>
<script src="js/observer.js"></script>
<script src="js/watcher.js"></script>
<script src="js/compile.js"></script>
<script src="js/mvvm.js"></script>
<script type="text/javascript">

     var app = new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: 'canfoo'
        },
        methods: {
            clickMe: function () {
                this.title = 'hello world';
            }
        },
        mounted: function () {
            window.setTimeout(() => {
                this.title = '你好';
            }, 1000);
        }
    });

</script>
</html>

先执行mvvm中的new SelfVue(…),在mvvm.js中,

observe(this.data);
new Compile(options.el, this);

先先导化八个监听器Observer,用于监听该指标data属性的值。
接下来开首化二个解析器Compile,绑定那么些节点,并分析在那之中的v-,” {{}}
“指令,(每叁个发令对应一个沃特cher)并开首化模板数据以及起头化相应的订阅者,并把订阅者添加到订阅器中(Dep)。那样就落到实处双向绑定了。
假诺v-model绑定的成分,

<input v-model="name">

即输入框的值发生变化,就会触发Compile中的

node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }
            self.vm[exp] = newValue;
            val = newValue;
        });

self.vm[exp] =
newValue;这一个语句会触发mvvm中SelfValue的setter,以及触发Observer对该对象name属性的监听,即Observer中的Object.defineProperty()中的setter。setter中有文告订阅者的函数dep.notify,沃特cher收到文告后就会实施绑定的换代函数。
最终的结尾就是功效图啦:

双向绑定

有关参考链接:http://www.cnblogs.com/canfoo/p/6891868.html

结语

这么,一个独具v-model和{{}}成效的MVVM类就已经到位了。代码地址点击那里。有趣味的同伙能够上去看下(也得以star
or fork下哈哈哈)。

此间也有二个差不离的样例(忽略样式)。

接下去的话,可能会三番五次贯彻computed属性,v-bind方法,以及帮衬在{{}}里面放表明式。假设认为这些小说对你有协理的话,麻烦点个赞,嘻嘻。

末段,贴上具有的代码:

class Observer {
  constructor(data) {
    // 如果不是对象,则返回
    if (!data || typeof data !== 'object') {
      return;
    }
    this.data = data;
    this.walk();
  }

  // 对传入的数据进行数据劫持
  walk() {
    for (let key in this.data) {
      this.defineReactive(this.data, key, this.data[key]);
    }
  }
  // 创建当前属性的一个发布实例,使用Object.defineProperty来对当前属性进行数据劫持。
  defineReactive(obj, key, val) {
    // 创建当前属性的发布者
    const dep = new Dep();
    /*
    * 递归对子属性的值进行数据劫持,比如说对以下数据
    * let data = {
    *   name: 'cjg',
    *   obj: {
    *     name: 'zht',
    *     age: 22,
    *     obj: {
    *       name: 'cjg',
    *       age: 22,
    *     }
    *   },
    * };
    * 我们先对data最外层的name和obj进行数据劫持,之后再对obj对象的子属性obj.name,obj.age, obj.obj进行数据劫持,层层递归下去,直到所有的数据都完成了数据劫持工作。
    */
    new Observer(val);
    Object.defineProperty(obj, key, {
      get() {
        // 若当前有对该属性的依赖项,则将其加入到发布者的订阅者队列里
        if (Dep.target) {
          dep.addSub(Dep.target);
        }
        return val;
      },
      set(newVal) {
        if (val === newVal) {
          return;
        }
        val = newVal;
        new Observer(newVal);
        dep.notify();
      }
    })
  }
}

// 发布者,将依赖该属性的watcher都加入subs数组,当该属性改变的时候,则调用所有依赖该属性的watcher的更新函数,触发更新。
class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    if (this.subs.indexOf(sub) < 0) {
      this.subs.push(sub);
    }
  }

  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    })
  }
}

Dep.target = null;

// 观察者
class Watcher {
  /**
   *Creates an instance of Watcher.
   * @param {*} vm
   * @param {*} keys
   * @param {*} updateCb
   * @memberof Watcher
   */
  constructor(vm, keys, updateCb) {
    this.vm = vm;
    this.keys = keys;
    this.updateCb = updateCb;
    this.value = null;
    this.get();
  }

  // 根据vm和keys获取到最新的观察值
  get() {
    // 将Dep的依赖项设置为当前的watcher,并且根据传入的keys遍历获取到最新值。
    // 在这个过程中,由于会调用observer对象属性的getter方法,因此在遍历过程中这些对象属性的发布者就将watcher添加到订阅者队列里。
    // 因此,当这一过程中的某一对象属性发生变化的时候,则会触发watcher的update方法
    Dep.target = this;
    this.value = CompileUtils.parse(this.vm, this.keys);
    Dep.target = null;
    return this.value;
  }

  update() {
    const oldValue = this.value;
    const newValue = this.get();
    if (oldValue !== newValue) {
      this.updateCb(oldValue, newValue);
    }
  }
}

class MVVM {
  constructor({ data, el }) {
    this.data = data;
    this.el = el;
    this.init();
    this.initDom();
  }

  // 初始化
  init() {
    // 对this.data进行数据劫持
    new Observer(this.data);
    // 传入的el可以是selector,也可以是元素,因此我们要在这里做一层处理,保证this.$el的值是一个元素节点
    this.$el = this.isElementNode(this.el) ? this.el : document.querySelector(this.el);
    // 将this.data的属性都绑定到this上,这样用户就可以直接通过this.xxx来访问this.data.xxx的值
    for (let key in this.data) {
      this.defineReactive(key);
    }
  }

  initDom() {
    const fragment = this.node2Fragment();
    this.compile(fragment);
    document.body.appendChild(fragment);
  }
  // 将节点转为fragment,通过fragment来操作DOM,可以获得更高的效率
  // 因为如果直接操作DOM节点的话,每次修改DOM都会导致DOM的回流或重绘,而将其放在fragment里,修改fragment不会导致DOM回流和重绘
  // 当在fragment一次性修改完后,在直接放回到DOM节点中
  node2Fragment() {
    const fragment = document.createDocumentFragment();
    let firstChild;
    while(firstChild = this.$el.firstChild) {
      fragment.appendChild(firstChild);
    }
    return fragment;
  }

  defineReactive(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.data[key];
      },
      set(newVal) {
        this.data[key] = newVal;
      }
    })
  }

  compile(node) {
    const textReg = /\{\{\s*\w+\s*\}\}/gi; // 检测{{name}}语法
    if (this.isElementNode(node)) {
      // 若是元素节点,则遍历它的属性,编译其中的指令
      const attrs = node.attributes;
      Array.prototype.forEach.call(attrs, (attr) => {
        if (this.isDirective(attr)) {
          CompileUtils.compileModelAttr(this.data, node, attr)
        }
      })
    } else if (this.isTextNode(node)) {
      // 若是文本节点,则判断是否有{{}}语法,如果有的话,则编译{{}}语法
      let textContent = node.textContent;
      if (textReg.test(textContent)) {
        // 对于 "test{{test}} {{name}}"这种文本,可能在一个文本节点会出现多个匹配符,因此得对他们统一进行处理
        // 使用 textReg来对文本节点进行匹配,可以得到["{{test}}", "{{name}}"]两个匹配值
        const matchs = textContent.match(textReg);
        CompileUtils.compileTextNode(this.data, node, matchs);
      }
    }
    // 若节点有子节点的话,则对子节点进行编译。
    if (node.childNodes && node.childNodes.length > 0) {
      Array.prototype.forEach.call(node.childNodes, (child) => {
        this.compile(child);
      })
    }
  }

  // 是否是属性节点
  isElementNode(node) {
    return node.nodeType === 1;
  }
  // 是否是文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }

  isAttrs(node) {
    return node.nodeType === 2;
  }
  // 检测属性是否是指令(vue的指令是v-开头)
  isDirective(attr) {
    return attr.nodeName.indexOf('v-') >= 0;
  }

}

const CompileUtils = {
  reg: /\{\{\s*(\w+)\s*\}\}/, // 匹配 {{ key }}中的key
  // 编译文本节点,并注册Watcher函数,当文本节点依赖的属性发生变化的时候,更新文本节点
  compileTextNode(vm, node, matchs) {
    // 原始文本信息
    const rawTextContent = node.textContent;
    matchs.forEach((match) => {
      const keys = match.match(this.reg)[1];
      console.log(rawTextContent);
      new Watcher(vm, keys, () => this.updateTextNode(vm, node, matchs, rawTextContent));
    });
    this.updateTextNode(vm, node, matchs, rawTextContent);
  },
  // 更新文本节点信息
  updateTextNode(vm, node, matchs, rawTextContent) {
    let newTextContent = rawTextContent;
    matchs.forEach((match) => {
      const keys = match.match(this.reg)[1];
      const val = this.getModelValue(vm, keys);
      newTextContent = newTextContent.replace(match, val);
    })
    node.textContent = newTextContent;
  },
  // 编译v-model属性,为元素节点注册input事件,在input事件触发的时候,更新vm对应的值。
  // 同时也注册一个Watcher函数,当所依赖的值发生变化的时候,更新节点的值
  compileModelAttr(vm, node, attr) {
    const { value: keys, nodeName } = attr;
    node.value = this.getModelValue(vm, keys);
    // 将v-model属性值从元素节点上去掉
    node.removeAttribute(nodeName);
    new Watcher(vm, keys, (oldVal, newVal) => {
      node.value = newVal;
    });
    node.addEventListener('input', (e) => {
      this.setModelValue(vm, keys, e.target.value);
    });
  },
  /* 解析keys,比如,用户可以传入
  *  let data = {
  *    name: 'cjg',
  *    obj: {
  *      name: 'zht',
  *    },
  *  };
  *  new Watcher(data, 'obj.name', (oldValue, newValue) => {
  *    console.log(oldValue, newValue);
  *  })
  *  这个时候,我们需要将keys解析为data[obj][name]的形式来获取目标值
  */
  parse(vm, keys) {
    keys = keys.split('.');
    let value = vm;
    keys.forEach(_key => {
      value = value[_key];
    });
    return value;
  },
  // 根据vm和keys,返回v-model对应属性的值
  getModelValue(vm, keys) {
    return this.parse(vm, keys);
  },
  // 修改v-model对应属性的值
  setModelValue(vm, keys, val) {
    keys = keys.split('.');
    let value = vm;
    for(let i = 0; i < keys.length - 1; i++) {
      value = value[keys[i]];
    }
    value[keys[keys.length - 1]] = val;
  },
}

以上面那一个模板为例,要替换的根成分“#mvvm-app”内唯有三个文本节点#text,#text的始末为{{name}}。大家就以上边那么些模板详细询问一下VUE框架的大致完毕流程。

网上的众多源码解读都以从 Observer 初叶的,而小编会从 new
二个MVVM实例初始,根据程序执行顺序去解释可能更便于通晓。先来看3个简约的例证:

<body>
 <div id="mvvm-app">
  {{name}}
 </div>
 <script src="./js/observer.js"></script>
 <script src="./js/watcher.js"></script>
 <script src="./js/compile.js"></script>
 <script src="./js/mvvm.js"></script>
 <script>
  let vm = new MVVM({
   el: "#mvvm-app",
   data: {
    name: "hello world"
   },  
  })

 </script>
</body>
<!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
的习性添加了 getter 和 setter。

一 、什么是数据代理

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;
  }
 });
};

在vue里面,大家将数据写在data对象中。可是大家在做客data里的数目时,既能够通过vm.data.name访问,也能够经过vm.name访问。那便是数码代理:在二个对象中,能够动态的拜会和设置另贰个指标的属性。

再往下看,最终一步 new 了三个 Compile,上面大家就来讲讲 Compile。

贰 、完结原理

4858美高梅 ,Compile

大家明白静态绑定(如vm.name =
vm.data.name)能够三回性的将结果赋给变量,而选取Object.defineProperty()方法来绑定则足以因此set和get函数完毕赋值的中间进程,从而实现多少的动态绑定。具体落实如下:

new Compile(self, options.el || document.body)
这一行代码中,第二个参数是眼前 Hue
实例,第一个参数是绑定的成分,在地点的演示中为class为 .test 的div。

let obj = {};
let obj1 = {
 name: 'xiaoyu',
 age: 18,
}
//实现origin对象代理target对象
function proxyData(origin,target){
 Object.keys(target).forEach(function(key){
  Object.defineProperty(origin,key,{//定义origin对象的key属性
   enumerable: false,
   configurable: true,
   get: function getter(){
    return target[key];//origin[key] = target[key];
   },
   set: function setter(newValue){
    target[key] = newValue;
   }
  })
 })
}

至于 Compile,那里只兑现最简便易行的 textContent 的绑定。而 Compile
的代码没什么难题,很轻易就能读懂,所做的正是分析 DOM,并累加 沃特cher
订阅。关于 DOM 的分析,先将根节点 el 转换来文书档案碎片 fragment
进行辨析编写翻译操作,解析完成后,再将 fragment 添加回原来的诚实 DOM
节点中。来看看这一部分的代码:

vue中的数据代理也是由此那种格局来促成的。

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);
};
function MVVM(options) {
 this.$options = options || {};
 var data = this._data = this.$options.data;
 var _this = this;//当前实例vm

 // 数据代理
 // 实现 vm._data.xxx -> vm.xxx 
 Object.keys(data).forEach(function(key) {
  _this._proxyData(key);
 });
 observe(data, this);
 this.$compile = new Compile(options.el || document.body, this);

}

MVVM.prototype = {
_proxyData: function(key) {
 var _this = this;
 if (typeof key == 'object' && !(key instanceof Array)){//这里只实现了对对象的监听,没有实现数组的
  this._proxyData(key);
 }
 Object.defineProperty(_this, key, {
  configurable: false,
  enumerable: true,
  get: function proxyGetter() {
   return _this._data[key];
  },
  set: function proxySetter(newVal) {
   _this._data[key] = newVal;
  }
 });
},
};

以地点示例为例,此时若打字与印刷出 fragment,可观望到其含有多个p元素:

实现Observe

<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 沃特cher(vm, exp,
callback)。明白了这一步后,接下去就须要理解怎么落实相关属性的订阅了。先从
Observer 开首谈起。

要想达成当数码变动时视图更新,首先要做的正是什么样领悟数码变动了,能够经过Object.defineProperty()函数监听data对象里的数码,当数码变动了就会触发set()方法。所以大家必要完成叁个数量监听器Observe,来对数码对象中的全数属性举行监听,当某一属性数据产生变化时,得到新型的数据公告绑定了该属性的订阅器,订阅器再实践相应的数量更新回调函数,从而达成视图的基础代谢。

Observer

当设置this.name = ‘hello
vue’时,就会履行set函数,公告订阅器里的订阅者执行相应的回调函数,实现多少变动,对应视图更新。

从最简便的事态来设想,即不考虑数组成分的生成。近年来先不考虑 Dep 与
Observer 的调换。先看看 Observer 构造函数:

function observe(data){
 if (typeof data != 'object') {
  return ;
 }
 return new Observe(data);
}

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

Observe.prototype = {
 walk: function(data){
  let _this = this;
  for (key in data) {
   if (data.hasOwnProperty(key)){
    let value = data[key];
    if (typeof value == 'object'){
     observe(value);
    }
    _this.defineReactive(data,key,data[key]);
   }
  }
 },
 defineReactive: function(data,key,value){
  Object.defineProperty(data,key,{
   enumerable: true,//可枚举
   configurable: false,//不能再define
   get: function(){
    console.log('你访问了' + key);return value;
   },
   set: function(newValue){
    console.log('你设置了' + key);
    if (newValue == value) return;
    value = newValue;
    observe(newValue);//监听新设置的值
   }
  })
 }
}
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 方法给拥有属性添加 getter 和
setter,就高达了我们的目标。属性有大概也是目的,由此必要对属性值实行递归调用。

要想文告订阅者,首先得要有三个订阅器(统一保管全体的订阅者)。为了方便管理,大家会为每一个data对象的习性都增加二个订阅器(new
Dep)。

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);
  }
 });
}

订阅器里存着的是订阅者Watcher(后边会讲到),由于订阅者只怕会有三个,大家供给建立1个数组来保养。一旦数据变化,就会触发订阅器的notify()方法,订阅者就会调用自己的update方法完成视图更新。

最后补充上 observe 函数,也即 Hue 构造函数中调用的 observe 函数:

function Dep(){
 this.subs = [];
}
Dep.prototype = {
 addSub: function(sub){this.subs.push(sub);
 },
 notify: function(){
  this.subs.forEach(function(sub) {
   sub.update();
  })
 }
}
function observe(val) {
 // 若 val 是对象且非数组,则 new 一个 Observer 实例,val 作为参数
 // 简单点说:是对象就继续。
 if (!Array.isArray(val) && typeof val === "object") {
  return new Observer(val);
 }
}

老是响应属性的set()函数调用的时候,都会触发订阅器,所以代码补充完整。

那样一来就对 data
的具有子孙属性(不知有没有那种说法。。)都进展了“吓唬”。明显到最近停止,这并没什么用,只怕说固然只完结那里,那么和怎么都不做没差距。于是
Dep 上场了。作者觉着精通 Dep 与 Observer 和 沃特cher
之间的关系是最主要的,先来探究 Dep 在 Observer 里做了哪些。

Observe.prototype = {
 //省略的代码未作更改
 defineReactive: function(data,key,value){
  let dep = new Dep();//创建一个订阅器,会被闭包在key属性的get/set函数内,因此每个属性对应唯一一个订阅器dep实例
  Object.defineProperty(data,key,{
   enumerable: true,//可枚举
   configurable: false,//不能再define
   get: function(){
    console.log('你访问了' + key);
    return value;
   },
   set: function(newValue){
    console.log('你设置了' + key);
    if (newValue == value) return;
    value = newValue;
    observe(newValue);//监听新设置的值
    dep.notify();//通知所有的订阅者
   }
  })
 }
}

Observer & Dep

实现Complie

在每二回 defineReactive 函数被调用之后,都会在闭包中新建3个 Dep
实例,即 let dep = new Dep()。Dep 提供了有个别艺术,先来说说 notify
这一个法子,它做了什么样事?正是在属性值爆发变化的时候通知Dep,那么大家的代码能够追加如下:

compile首要做的政工是分析模板指令,将模板中的data属性替换来data属性对应的值(比如将{{name}}替换到data.name值),然后开首化渲染页面视图,并且为每种data属性添加三个监听数据的订阅者(new
沃特cher),一旦数据有转移,收到公告,更新视图。

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();
  }
 });
}

遍历解析要求替换的根成分el下的HTML标签必然会提到到数次的DOM节点操作,由此不可制止的会吸引页面包车型地铁重排或重绘,为了增强质量和成效,大家把根成分el下的保有节点转换为文书档案碎片fragment实行剖析编写翻译操作,解析完结,再将fragment添加回原来的真实dom节点中。

假使仅考虑 Observer 与 Dep 的沟通,即有变动时通报
Dep,那么那里就是完了,可是在 vue.js 的源码中,大家还能观察一段增加在
getter 中的代码:

注:文档碎片本身也是叁个节点,但是当将该节点append进页面时,该节点标签作为根节点不会展现html文书档案中,其内部的子节点则足以完全展现。

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

Compile解析模板,将模板内的子成分#text添加进文档碎片节点fragment。

以此 depend 方法吧,它又做了吗?答案是为闭包中的 Dep 实例添加了一个Watcher 的订阅,而 Dep.target 又是什么?他实在是二个 沃特cher
实例,???一脸懵逼,先记住就好,先看一部份的 Dep 源码:

function Compile(el,vm){
 this.$vm = vm;//vm为当前实例
 this.$el = document.querySelector(el);//获得要解析的根元素 
 if (this.$el){
  this.$fragment = this.nodeToFragment(this.$el);
  this.init();
  this.$el.appendChild(this.$fragment);
 } 
}
Compile.prototype = {
 nodeToFragment: function(el){
  let fragment = document.createDocumentFragment();
  let child;
  while (child = el.firstChild){
   fragment.appendChild(child);//append相当于剪切的功能
  }
  return fragment;

 },
};
// 标识符,在 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;

compileElement方法将遍历全数节点及其子节点,举行扫描解析编译,调用对应的授命渲染函数举行数量渲染,并调用对应的吩咐更新函数实行绑定,详看代码及注释表达:

或是看到那依旧一脸懵逼,没涉及,接着往下。差不离有同学会疑心,为何要把添加
Watcher 订阅放在 getter 中,接下去大家的话说那 沃特cher 和 Dep 的好玩的事。

因为我们的模版只含有三个文本节点#text,因而compileElement方法执行后会进入_this.compileText(node,reg.exec(node.textContent)[1]);//#text,’name’

Watcher & Dep

Compile.prototype = {
 nodeToFragment: function(el){
  let fragment = document.createDocumentFragment();
  let child;
  while (child = el.firstChild){
   fragment.appendChild(child);//append相当于剪切的功能
  }
  return fragment;

 },

 init: function(){
  this.compileElement(this.$fragment);
 },

 compileElement: function(node){
  let childNodes = node.childNodes;
  const _this = this;
  let reg = /\{\{(.*)\}\}/g;
  [].slice.call(childNodes).forEach(function(node){

   if (_this.isElementNode(node)){//如果为元素节点,则进行相应操作
    _this.compile(node);
   } else if (_this.isTextNode(node) && reg.test(node.textContent)){
    //如果为文本节点,并且包含data属性(如{{name}}),则进行相应操作
    _this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'
   }

   if (node.childNodes && node.childNodes.length){
    //如果节点内还有子节点,则递归继续解析节点
    _this.compileElement(node);

   }
  })
 },
 compileText: function(node,exp){//#text,'name'
   compileUtil.text(node,this.$vm,exp);//#text,vm,'name'
 },};

先让我们回想一下 Compile 做的事,解析
fragment,然后给相应属性添加订阅:new 沃特cher(vm, exp, cb)。new 了那一个沃特cher 之后,沃特cher 如何做呢,就有了上面那样的对话:

CompileText()函数达成起始化渲染页面视图(将data.name的值通过#text.textContent

data.name展现在页面上),并且为各类DOM节点添加2个监听数据的订阅者(那里是为#text节点新增三个沃特her)。

let updater = {
 textUpdater: function(node,value){ 
  node.textContent = typeof value == 'undefined' ? '' : value;
 },
}

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

 bind: function(node,vm,exp,dir){//#text,vm,'name','text'
  let updaterFn = updater[dir + 'Updater'];
  updaterFn && updaterFn(node,this._getVMVal(vm,exp));
  new Watcher(vm,exp,function(value){
   updaterFn && updaterFn(node,value)
  });
  console.log('加进去了');
 }
};

现今大家完毕了二个能落到实处文件节点解析的Compile()函数,接下去大家落到实处2个沃特cher()函数。

实现Watcher

咱俩后边讲过,Observe()函数落成data对象的本性威胁,并在属性值改变时触发订阅器的notify()文告订阅者沃特cher,订阅者就会调用本身的update方法达成视图更新。

Compile()函数负责解析模板,早先化页面,并且为各类data属性新增三个监听数据的订阅者(new
沃特cher)。

Watcher订阅者作为Observer和Compile之间通讯的桥梁,所以大家得以差不多知道沃特cher的效应是何许。

重在做的事务是:

在自个儿实例化时往订阅器(dep)里面添加自身。

自身必须有2个update()方法 。

待属性别变化动dep.notice()通告时,能调用自个儿的update()方法,并触发Compile中绑定的回调。

先交由全体代码,再分析现实的功用。

//Watcher
function Watcher(vm, exp, cb) {
 this.vm = vm;
 this.cb = cb;
 this.exp = exp;
 this.value = this.get();//初始化时将自己添加进订阅器
};

Watcher.prototype = {
 update: function(){
  this.run();
 },
 run: function(){
  const value = this.vm[this.exp];
  //console.log('me:'+value);
  if (value != this.value){
   this.value = value;
   this.cb.call(this.vm,value);
  }
 },
 get: function() { 
  Dep.target = this; // 缓存自己
  var value = this.vm[this.exp] // 访问自己,执行defineProperty里的get函数   
  Dep.target = null; // 释放自己
  return value;
 }
}

//这里列出Observe和Dep,方便理解
Observe.prototype = {
 defineReactive: function(data,key,value){
  let dep = new Dep();
  Object.defineProperty(data,key,{
   enumerable: true,//可枚举
   configurable: false,//不能再define
   get: function(){
    console.log('你访问了' + key);
    //说明这是实例化Watcher时引起的,则添加进订阅器
    if (Dep.target){
     //console.log('访问了Dep.target');
     dep.addSub(Dep.target);
    }
    return value;
   },
  })
 }
}

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

咱俩清楚在Observe()函数执行时,大家为各种属性都添加了三个订阅器dep,而以此dep被闭包在性质的get/set函数内。所以,咱们得以在实例化沃特cher时调用this.get()函数访问data.name属性,那会触发defineProperty()函数内的get函数,get方法执行的时候,就会在质量的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcher实例就能接收更新通告。

那正是说沃特cher()函数中的get()函数内Dep.taeger =
this又有哪些十分的含义呢?大家期待的是在实例化沃特cher时将相应的Watcher实例添加一回进dep订阅器即可,而不希望在随后每一回访问data.name属性时都参预一回dep订阅器。所以我们在实例化执行this.get()函数时用Dep.target
= this来标识当前沃特cher实例,当添加进dep订阅器后安装Dep.target=null。

实现VMVM

MVVM作为数据绑定的入口,整合Observer、Compile和沃特cher三者,通过Observer来监听本身的model数据变动,通过Compile来分析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通讯桥梁,达到数据变动
-> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

function MVVM(options) {
 this.$options = options || {};
 var data = this._data = this.$options.data;
 var _this = this;
 // 数据代理
 // 实现 vm._data.xxx -> vm.xxx 
 Object.keys(data).forEach(function(key) {
  _this._proxyData(key);
 });
 observe(data, this);
 this.$compile = new Compile(options.el || document.body, this);
}

沃特cher:hey Dep,小编索要订阅 exp 属性的变动。

你大概感兴趣的篇章:

  • Vue的MVVM完成形式
  • Vue原理分析
    实现双向绑定MVVM
  • JS组件种类之MVVM组件 vue
    2捌分钟解决前端增加和删除改查
  • JS组件连串之MVVM组件营造筑组织调的Vue组件
  • 又一款MVVM组件
    塑造本身的Vue组件(2)
  • 前端 Vue.js 和 MVVM
    详细介绍
  • 又一款MVVM组件
    Vue基础语法和常用命令(1)
  • Vue.js 和 MVVM 的注意事项
  • JavaScript的MVVM库Vue.js入门学习笔记
  • vue,angular,avalon那二种MVVM框架优缺点

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

沃特cher:不过她在闭包中啊,小编一筹莫展和他联络。

Dep:你得到了整个 Hue 实例 vm,又精晓属性 exp,你能够触发他的 getter
啊,你在 getter 里动些小动作不就行了。

沃特cher:有道理,但是笔者得让 dep 知道是自个儿订阅的哟,不然她关照不到我。

Dep:这些大约,笔者帮您,你每一回触发 getter 前,把你的引用告诉 Dep.target
就行了。记得办完事后给 Dep.target 置空。

于是乎就有了地点 getter 中的代码:

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

最近再回头看看 Dep 部分的代码,是还是不是好驾驭些了。如此一来, 沃特cher
需求做的政工就不难明了了:

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 通知变化后,沃特cher
的拍卖,具体的函数调用流程是那般的: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);
 }
};

结语

到那即便写完了,本身水平有限,若有不足之处欢迎提出,一起探索。

参考资料

上述便是本文的全体内容,希望对我们的求学抱有帮忙,也期待咱们多多帮助脚本之家。

你也许感兴趣的篇章:

  • vue.js数据绑定的办法(单向、双向和1次性绑定)
  • Vue.js基础指令实例讲解(各类数码绑定、表单渲染大计算)
  • Vue.js数据绑定之data属性
  • Vue.js中多少绑定的语法教程
  • Vue.js+Layer表格数据绑定与完成立异的实例
  • Vue.js每一天必学之数据双向绑定
  • 详解Vue.js基于$.ajax获取数据并与组件的data绑定
  • Vue.js第3天学习笔记(数据的双向绑定、常用命令)
  • vue.js利用defineProperty达成多少的双向绑定
  • 详解Vue.js之视图和数目标双向绑定(v-model)
  • vue.js数据绑定操作详解

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 美高梅手机版4858 版权所有