双向绑定如何实现:
1、我们需要一个方法来识别视图中哪个元素被设置了双向绑定。
2、我们需要监视视图和数据的变化。
3、我们需要将所有变化传播到绑定视图或者数据。
几种实现数据双向绑定的做法:1、发布者-订阅者模式(backbone.js)、脏值检查(angular.js)、数据劫持(vue.js)。
发布者-订阅者模式:
视图驱动数据变化,主要应用于input、select、textarea等元素,通过监听dom的change、keypress、keyup等事件来触发事件改变数据层数据。
数据驱动视图变化,一个数组对象el存放支持双绑的dom对象,一个储存 绑定值的对象data,一个储存 设置值函数的对象fun,一个调用函数set(循环每个el,再循环每个el的属性,如果某个指定的属性存在,就调用对应fun),一旦改变数据就调用set函数。
脏值检查:
和上种方式类似,但在数据驱动视图变化,不是改完值手动触发set函数触发视图更新,而是通过setInterval()定时轮询检测数据变化触发set函数。
数据劫持:
vue.js是通过数据劫持(object.defineProperty()的set和get)结合发布者-订阅者方式,在数据变动时发布消息给订阅者,触发相应回调监听。
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者。
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模版替换数据,以及绑定相应的更新函数。
3、实现一个watcher,做为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知。执行指令绑定的相应回调函数,从而更新视图。
4、mvvm入口函数,整合以上三者。
分析
实现mvvm主要包括两个方面,数据变化更新视图,视图变化更新数据。关键在于前者数据变化更新视图,后者可以通过事件监听即可。所以我们着重看前者。我们可以通过Object.defineProperty()对属性设置一个set函数来观察数据变化做出反应,我们来具体实现一下。
实现
我们需要一个观察者Observer,以 遍历递归 方式 为每个对象属性通过Object.defineProperty()添加观察,在 获取对象属性getter函数中 需要在订阅者Watcher初始化时把他装进订阅器Dep,在 对象属性值更新setter函数中 需要 改变oldVal—newVal,并通知订阅者触发相应函数。
我们需要一个订阅者Watcher,它需要有初始化功能( 结合Observer的getter函数初始化时把自己添加进Dep ),需要在数据属性值变化时,调用相关函数重新赋予订阅者新值。
我们需要一个解析者Compile,遍历+递归 去解析dom节点,若解析到绑定相关字符串,就将初始化的数据初始化到视图中,实例化一个订阅者并绑定更新函数。
1、实现一个观察者Observer,用来劫持并观察所有属性,如果有变动,就通知订阅者。
2、实现一个订阅者Watcher,可以收到属性的变化并执行相应的函数,从而更新视图。
3、实现一个解析者compile,可以扫描和解析每个节点的相关指令,并根据初始化模版数据去初始化相应的订阅器。
维护订阅器
维护一个订阅器,负责实例化订阅者,当初始化和更新时,调用相关函数。
Dep为一个构造函数,有subs数组储存订阅者,addSub和notify两个函数,addSub负责在初始化订阅者初始化时(当Compile解析dom检测到相关字符串 进行订阅者初始化)添加到订阅器中,notify负责在观察到数据更新时被触发 去调用订阅者的更新函数。
实现Observe
Observer主要是对 对象属性 通过 defineProperty()进行监听,getter时帮助订阅者初始化时加入订阅器,setter时更新对象属性及通知订阅者调用函数更新订阅者值。
一个构造函数Observer,一个触发函数observe。
observe函数判断参数是否是对象,是的话实例化一个Observe对象(对象属性 遍历递归 时判断子属性值 是否是对象)。
Observe接受一个对象,有Walk、defineReactive两个函数,Walk负责遍历对象每个属性调用defineReactive。defineReactive负责递归每个对象属性 设置监听器。
Q/A
每次defineReactive都会new Dep()再在getter中初始化push订阅者,Dep中怎么会有所有订阅者?
其实只有一个订阅者,每次都会实例一个Dep,有多个Dep。
setter时循环订阅器中每个订阅者调用update函数,update函数做了什么事情?
监听器更新数据时触发的更新函数判断 new/old数据是否相同,不相同就把旧Value赋予新值,并在全局执行回调函数(传入新旧值)
实现订阅者
每个订阅者实例有4个对象属性,cb(监听器更新数据时触发的函数),vm(组件对象),exp(绑定的属性key),value(绑定的属性值)。run和get两个函数,run为监听器更新数据时触发的更新函数判断 new/old数据是否相同,不相同就把旧Value赋予新值,并在全局执行回调函数(传入新旧值)。get为初始化时把自己添加进订阅器Dep()中。
实现Compile
解析器主要作用是 遍历递归解析dom节点,解析到双向绑定的指令,将初始化的数据初始化到视图中,实例化订阅器并绑定更新函数。
Compile构造函数有3个属性,vm(全局环境),el(html最高节点),fragment用来存放dom节点(我们数据更新dom时需要多次操作dom,通过createDocumentFragment创建一个虚拟父节点fragment,把dom移入fragment进行操作,操作完了直接替换整个dom(一次性替换操作效率更高比一次次操作块70%)。
init()调用了nodeToFragment、compileElement、compile三个函数。
nodeToFragment,把dom塞入fragment虚拟父节点。
compileElement,遍历递归fragment中dom,判断是元素节点的话执行compile函数,是文本节点且有'{{}}'的话执行compileText函数。如果节点有子节点继续递归执行compileElement。
compile,对dom节点的属性节点进行遍历,若有"v-"相关字段属性name,若有":on"相关字段则绑定的是事件,执行compileEvent事件,否则执行compileMdole事件。
"{{}}"对应的compileText函数,负责初始化节点textContent数据,并新增一个订阅者。
"v-on:"对应的compileEvent函数,负责取得事件名和事件值 通过addEventListener监听函数触发执行对应事件。
"v-model"对应compileModel函数,负责初始化节点value数据,并新增一个订阅者,再通过对node.addEventListener('input', function(...))在input数据变化时实时改变对象数据
mvvm入口
我们把整个流程结合起来看一遍
入口构造函数,需要一个数据对象data,需要一个函数对象methods(当data中数据变化时调用)。
有一个proxyKeys函数,作用,在访问selfVue的属性时代理为selfVue.data属性(this.data.name = 'canfoo'我们可以用更简洁的方式 this.name = 'canfoo' ),也是通过遍历每个data属性为每个属性添加监听器object.defineProperty(),在get内把对this.key的访问替换成this.data.key的属性值来处理。
监听器observe对数据对象进行监听。
实例化compile对象,把节点传入,在compile会对dom节点进行遍历递归,处理3种情况。1、"{{}}",初始化节点texteContent数值,实例化一个订阅者。2、"v-model",初始化节点value数值,实例化一个订阅者,并监听input事件实时对数据更新。3、"v-on:"把对应事件名和methods中事件进行绑定监听addEventListener。
实例化订阅者Watcher,会在初始化时把自己添加进订阅器Dep(),在数据更新时会通过this.c.call()触发传进来的函数 处理数据。
所有事情处理好后执行mounted函数。
二次理解:
我们定义好基本数据
会有一个入口
入口及之后做的事情:1、取到定义好的数据。2、把对this.xxx的访问代理到this.data.xxx(写法更简洁)。3、我们需要一个订阅器Dep,来收集订阅者Watcher,在观察器检测到数据变化时,调用订阅者Watcher的相关处理函数。4、执行观察者(Observe)遍历递归 data属性,通过访问器属性Object.defineProperty() 给他们都绑定观察器。5、执行编译者(Compile)分析编译Dom结构,检测到 相应字符串 实例化一个订阅者(Watcher),实例化时它会主动触发观察者的getter函数把自己push进订阅器Dep。
首次数据赋值在什么时候?
首次数据赋值在解析者Compile解析到相关字符串时 实例化订阅者Watcher的同时进行了赋值。
之后数据更新 执行流程如何?
观察者Observe会在数据更新时观察到,会执行setter函数调用dep中notify函数,notify函数再调用订阅者的Update函数去调用相关方法处理。
订阅器Dep存在的意义?
不知道???? 每个Observer.prototype.defineReactive都会new Dep。相当于每个Watcher订阅者都有一个Dep()。