精华内容
下载资源
问答
  • 双向绑定的原理

    2020-12-27 17:58:15
    双向绑定的核心原理 核心是采用数据劫持结合发布者订阅者模式,通过Object.defineProperty()对每个属性的get和set进行拦截。在数据发生变化的时候发布消息给订阅者,触发相应的监听回调。 仅仅使用Object,...

    双向绑定的核心原理

    核心是采用数据劫持结合发布者订阅者模式,通过Object.defineProperty()对每个属性的get和set进行拦截。在数据发生变化的时候发布消息给订阅者,触发相应的监听回调。
    仅仅使用Object,defineProperty()就能完成一个简单的双向绑定,但是效率比较低。观察者模式让双向绑定更有效率,它是一对多的模式,一指的是修改的一处数据,多是凡是用了这个数据的地方都更新。数据劫持就是利用Object.defineProperty()读操作数据的set,get。

    极简的双向绑定

    Object.defineProperty()有三个参数,第一个是属性所在的对象。第二个是要操作的属性,第三个是被操作的属性的特性,是一个对象{},一般是两个get:读取属性时触发,set写入属性时触发。

    <input type="text" id="inputtext">
    <p id="ptext"></p>
    
    let obj={}
    Object.defineProperty(obj,'val',{
    	get:function(){},
    	set:function(_val){
    		document.getElementById("inputtext").value=_val
    		document.getElementById('ptext').innerHTML=_val
    	}
    })
    document.addEventListener("keyup",function(e){
    	obj.val=e.target.value
    })
    

    简单的发布订阅模式

    1.Dep类
    负责进行依赖收集
    首先要有一个数组,存放订阅信息
    其次提供一个向数组中追加订阅信息的方法
    然后提供一个循环,循环触发数组中的每个订阅信息
    2.watcher类
    负责订阅一些事件

    //收集依赖/收集订阅者
    class Dep{
      constructor(){
        //存放订阅者的数组
        this.subs=[]
      }
      //将订阅者存放到数组中
      add(watcher){
        this.subs.push(watcher)
      }
      //发布通知的方法
      notify(){
         this.subs.forEach(watcher=>{
           return watcher.update()
         })
      }
    }
    //订阅者的类
    class Watcher{
      //接收一个回调函数,将回调函数挂载到自己身上
      constructor(callback){
        this.callback=callback
      }
      //触发回调的方法
      update(){
        this.callback()
      }
    }
    let w1=new Watcher(()=>{console.log('第一个订阅者')})
    let w2=new Watcher(()=>{console.log('第二个订阅者')})
    let dep=new Dep()
    dep.add(w1) 
    dep.add(w2)
    //只要我们为vue中data的数据重新赋值,vue会监听到,并把数据的变化通知到订阅者,订阅者(DOM元素)会根据最新的数据,更新内容
    dep.notify()
    

    Object.defineProperty()

    let obj={
      name:'lisa',
      age:23
    }
    Object.defineProperty(obj,'name',{
      enumerable:true, //当前属性允许被循环
      configurable:true,//当前属性允许被配置
      get(){  //getter
        console.log("name属性被访问")
      },
      set(newVal){ //setter
        console.log("name属性被修改为"+newVal)
      }
    })
    obj.name  //访问
    obj.name="zhao"  //修改
    

    实现数据劫持

    <div id="app">
    		姓名是:{{name}}
    		年龄:{{age}}
    	</div>
    
    const vm = new Vue({
    			el: '#app',
    			data: {
    				name: 'lisa',
    				age: 21,
    				info: {
    					a: "a1",
    					b: "b1"
    				}
    			},
    		})
    		console.log(vm)
    
    class Vue {
    			constructor(options) {
    				this.$data = options.data
    				//调用数据劫持的方法
    				Observer(this.$data)
    				//属性代理,不使用代理时,访问数据this.$data.xx
    				//使用代理后访问this.xxx
    				Object.keys(this.$data).forEach(key=>{
    					//为this添加了key属性
    					Object.defineProperty(this,key,{
    						enumerable:true,
    						configurable:true,
    						get(){
    							return this.$data[key]
    						},
    						set(newVal){
    							this.$data[key]=newVal
    						}
    					})
    				})
    			}
    		}
    		//定义一个数据劫持的方法
    		function Observer(obj) {
    			//递归的终止条件
    			if (!obj || typeof obj !== "object") return
    			//获取obj的属性并添加set,get
    			Object.keys(obj).forEach((key) => {
    				let value = obj[key]
    				//把value这个子节点进行递归
    				Observer(value)
    				Object.defineProperty(obj, key, {
    					enumerable: true,
    					configurable: true,
    					//属性被访问时,返回value
    					get() {
    						console.log(`有人获取了${key}的值`)
    						return value
    					},
    					set(newVal) {
    						value = newVal
    						//给属性重新赋值后,新的属性值没有了get和set,需要重新递归
    						Observer(value)
    					}
    				})
    			})
    		}
    

    模板编译

    创建文档碎片,提高DOM操作的性能

    在浏览器中,一旦把节点插入到document.body(或其他节点)中,页面就会更新并反映出这种变化。但是当要向document添加大量数据时,这个过程就可能会十分缓慢。
    为了解决这个问题,我们可以引入createDocumentFragment()方法,它的作用是创建一个文档碎片,把要插入的新节点先附加在它上面,然后再一次性添加到document中

    function Compile(el,vm){
    			//获取el对应的DOM元素
    			vm.$el=document.querySelector(el)
    			//创建文档碎片
    			const fragment=document.createDocumentFragment()
    			//当存在子节点时,将节点加入到文档碎片中
    			while(childNode=vm.$el.firstChild){
    				fragment.appendChild(childNode)
    			}
    			//进行模板编译
    			replace(fragment);
    			//将文档碎片中的节点加入到DOM节点中
    			vm.$el.appendChild(fragment)
    			//创建一个对模板进行编译的方法
    			function replace(node){
    				//定义匹配插值表达式的正则
    				const regMustache=/\{\{\s*(\S+)\s*\}\}/
    				//判断如果为纯文本节点,则进行文本的替换
    				if(node.nodeType ===3){
    					const text=node.textContent
    					//数组第一个值为替换前的值,第二个值为替换后的值
    					const exec=regMustache.exec(text)
    					console.log(exec)
    					//如果内容存在(空格也为一个文本节点)
    					if(exec){
    						//对值进行分割,再拿到最下面的值
    						let value=exec[1].split('.').reduce((newObj,k)=>{
    							return newObj[k]
    						},vm)
    						node.textContent=text.replace(regMustache,value)
    					}
    					return
    				}
    				//不是纯文本节点,可能是DOM元素需要进行递归处理
    				node.childNodes.forEach(child=>{
    					replace(child)
    				})
    			}
    		}
    

    创建Dep累进行依赖收集

    class Dep{
    			constructor(){
    				this.subs=[]
    			}
    			addSub(watcher){
    				this.subs.push(watcher)
    			}
    			//负责通知每个watcher的方法
    			notify(){
    				this.subs.forEach(watcher=>
    					watcher.update()
    				)
    			}
    		}
    

    完整的双向绑定

    <div id="app">
    		<h3>姓名是:{{name}}</h3>
    		<h3>年龄:{{age}}</h3>
    		<h3>a的值是:{{info.a}}</h3>
    		姓名是:<input type="text" v-model="name">
    	</div>
    
    const vm = new Vue({
    			el: '#app',
    			data: {
    				name: 'lisa',
    				age: 21,
    				info: {
    					a: "a1",
    					b: "b1"
    				}
    			},
    		})
    
    class Vue {
    			constructor(options) {
    				this.$data = options.data
    				//调用数据劫持的方法
    				Observer(this.$data)
    				//属性代理,不使用代理时,访问数据this.$data.xx
    				//使用代理后访问this.xxx
    				Object.keys(this.$data).forEach(key=>{
    					//为this添加了key属性
    					Object.defineProperty(this,key,{
    						enumerable:true,
    						configurable:true,
    						get(){
    							return this.$data[key]
    						},
    						set(newVal){
    							this.$data[key]=newVal
    						}
    					})
    				})
    				//调用模板编译的函数
    				Compile(options.el,this)
    			}
    		}
    		//定义一个数据劫持的方法
    		function Observer(obj) {
    			//递归的终止条件
    			if (!obj || typeof obj !== "object") return
    			const dep=new Dep()
    			//获取obj的属性并添加set,get
    			Object.keys(obj).forEach((key) => {
    				let value = obj[key]
    				//把value这个子节点进行递归
    				Observer(value)
    				Object.defineProperty(obj, key, {
    					enumerable: true,
    					configurable: true,
    					//属性被访问时,返回value
    					get() {
    						// console.log(`有人获取了${key}的值`)
    						//将watcher的实例放到dep.subs数组中
    						Dep.target && dep.addSub(Dep.target)
    						return value
    					},
    					set(newVal) {
    						value = newVal
    						//给属性重新赋值后,新的属性值没有了get和set,需要重新递归
    						Observer(value)
    						//通知每个订阅者更新自己的文本
    						dep.notify()
    					}
    				})
    			})
    		}
    		//对HTML结构进行模板编译的方法
    		function Compile(el,vm){
    			//获取el对应的DOM元素
    			vm.$el=document.querySelector(el)
    			//创建文档碎片
    			const fragment=document.createDocumentFragment()
    			//当存在子节点时,将节点加入到文档碎片中
    			while(childNode=vm.$el.firstChild){
    				fragment.appendChild(childNode)
    			}
    			//进行模板编译
    			replace(fragment);
    			//将文档碎片中的节点加入到DOM节点中
    			vm.$el.appendChild(fragment)
    			//创建一个对模板进行编译的方法
    			function replace(node){
    				//定义匹配插值表达式的正则
    				const regMustache=/\{\{\s*(\S+)\s*\}\}/
    				//判断如果为纯文本节点,则进行文本的替换
    				if(node.nodeType ===3){
    					const text=node.textContent
    					//数组第一个值为替换前的值,第二个值为替换后的值
    					const exec=regMustache.exec(text)
    					//如果内容存在(空格也为一个文本节点)
    					if(exec){
    						//对值进行分割,再拿到最下面的值
    						let value=exec[1].split('.').reduce((newObj,k)=>{
    							return newObj[k]
    						},vm)
    						node.textContent=text.replace(regMustache,value)
    						//在这个时候,创建watcher类的实例
    						new Watcher(vm,exec[1],(newVal)=>{
    							node.textContent=text.replace(regMustache,newVal)
    						})
    					}
    					//终止递归的条件
    					return
    				}
    				//判断是否为一个元素,并且是否为input输入框
    				if(node.nodeType ===1 && node.tagName.toUpperCase() =="INPUT"){
    					//得到当前元素的所有属性节点
    					const attrs=Array.from(node.attributes)
    					const findResult=attrs.find(x=>x.name ==="v-model"
    					)
    					if(findResult){
    						//获取到当前v-model属性的值,v-modal="name",v-modal="info.a"
    						const expStr=findResult.value
    						const value=expStr.split(".").reduce((newObj,k)=>newObj[k],vm)
    						node.value=value
    						//创建watcher的实例
    						new Watcher(vm,expStr,(newValue)=>{
    							node.value=newValue
    						})
    						//监听文本框的input输入事件,拿到文本框最新的值,更新到vm上
    						node.addEventListener('input',(e)=>{
    							const keyArr=expStr.split('.')
    							const obj=keyArr.slice(0,keyArr.length-1).reduce((newObj,k)=>newObj[k],vm)
    							console.log(obj)
    							obj[keyArr[keyArr.length-1]]=e.target.value
    						})
    					}
    				}
    				//不是纯文本节点,可能是DOM元素需要进行递归处理
    				node.childNodes.forEach(child=>{
    					replace(child)
    				})
    			}
    		}
    		//收集依赖/收集订阅者
    		class Dep{
    			constructor(){
    				this.subs=[]
    			}
    			addSub(watcher){
    				this.subs.push(watcher)
    			}
    			//负责通知每个watcher的方法
    			notify(){
    				this.subs.forEach(watcher=>
    					watcher.update()
    				)
    			}
    		}
    		//订阅者的类
    		class Watcher{
    			//cb回调函数中,记录着当前watcher如何更新自己的文本内容,
    			//但是,知道如何更新自己还不行,还必须拿到最新的数据
    			//因此,还需要在new Watcher期间,把vm也传进来(因为vm中保存着最新的数据)
    			//除此之外,还需要知道在vm的数据中,哪个才是自己需要的数据
    			//因此,必须在new Watcher期间,指定watcher对应数据的名字
    			constructor(vm,key,cb){
    				this.vm=vm
    				this.key=key
    				this.cb=cb
    				//下面3行负责把创建的watcher实例存放到subs数组中
    				Dep.target=this //this指向watcher实例
    				//取值目的是去执行get()
    				key.split(".").reduce((newObj,key)=>newObj[key],vm)
    				Dep.target=null
    			}
    			//watcher的实例,需要有update函数,从而让发布者能够通知我们进行更新
    			update(){
    				const value=this.key.split(".").reduce((newObj,key)=>newObj[key],this.vm)
    				this.cb(value)
    			}
    		}
    
    展开全文
  • 双向绑定的原理(vue) 简介: 双向绑定:指的就是,绑定对象属性的改变到用户界面的变化的能力,反之亦然。换种说法,如果我们有一个user对象和一个name属性,一旦我们赋了一个新值给user.name,在UI上就会显示新的...

    双向绑定的原理(vue)

    简介:

    双向绑定:指的就是,绑定对象属性的改变到用户界面的变化的能力,反之亦然。换种说法,如果我们有一个user对象和一个name属性,一旦我们赋了一个新值给user.name,在UI上就会显示新的姓名了。同样地,如果UI包含了一个输入用户姓名的输入框,输入一个新值就应该会使user对象的name属性做出相应的改变。

    vue双向绑定的实现(2.0以上)

    一:通过这个方法 Object.defineProperty 来实现 双向绑定 ( vue 2.x)

    二:Object.proxy(代理)来实现Vue3.x的双向绑定,使用代理能够解决一些性能问题,解决了defineProperty这个方法的历史遗留问题(监听数组会产生监听失效的问题)

    Object.defineProperty 来实现 双向绑定的原理代码简写

    //代码如下
    <script>
            var obj = {
                value: 0
            }
    		//Object.defineProperty给对象绑定方法,有obj 和 obj需要设置属性的值
            Object.defineProperty(obj, "value", {
                // setter 方法,设置值的方法
                set(val) {
                    console.log('current value:', val)
                    document.getElementById('valueNode').innerText = val
                },
                // getter 方法,获取值的方法,必须实现放回值
                get() {
                    console.log('dispatch getter function')
                    // return obj.value
                    return document.getElementById('valueNode').innerText
                }
            })
    
            function setValueEvt() {
                obj.value = window.event.currentTarget.value
            }
        </script>
    

    Object.defineProperty 来实现 双向绑定的原理总结

    ​ 通过Object.defineProperty这个方法,给对象某个属性(如:value)设置set()方法和get()方法,当我们给对象属性value设置值的时候,就会调用set方法。当我们取value值得时候,就会触发get方法。get()方法必须要有返回值

    展开全文
  • vue双向绑定的原理

    2020-03-10 14:51:37
    vue双向绑定的原理 为什么要学习双向绑定的原理? 面试 提升自我学习能力 VUE实现双向数据绑定的原理就是利用了 Object.defineProperty() 这个方法,重新定义了对象获取属性值(get)和设置属性值(set)的操作来...

    vue双向绑定的原理

    为什么要学习双向绑定的原理?

    1. 面试

    2. 提升自我学习能力

      VUE双向数据绑定的原理利用Object.defineProperty() ,重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的。
      用法: Object.defineProperty( 要操作的对象,要定义或修改的对象属性名,属性描述符)
      属性描述符:是一个对象,主要有两种形式:数据描述符和存取描述符。这两种对象只能选择一种使用,不能混合两种描述符的属性同时使用。上面说的get和set就是属于存取描述符对象的属性。

    在面试中如何应对?

    面试官: 说一下VUE双向绑定的原理?
    答: VUE实现双向数据绑定利用Object.defineProperty() 重新定义了对象获取属性值(get)和设置属性值(set)的操作来实现的。
    代码演示:defineProperty的用法

    var obj = { };var name;
    //第一个参数:定义属性的对象。
    //第二个参数:要定义或修改的属性的名称。
    //第三个参数:将被定义或修改的属性描述符。
    Object.defineProperty(obj, "data", {
    	//获取值get: 
    	function () {
    		return name;
        },
    	//设置值set: 
    	function (val) {
    		name = val;console.log(val)
    	}
    })
    //赋值调用setobj.data = 'aaa';
    //取值调用getconsole.log(obj.data);
    //代码演示:defineProperty的双向绑定
    var obj={};
    Object.defineProperty(obj, 'val',{
    	set:function (newVal) {
    		document.getElementById("a").value=newVal==undefined?'':newVal;
    		document.getElementById("b").innerHTML=newVal==undefined?'':newVal;}});
    		document.getElementById("a").addEventListener("keyup",function (e) {
    			obj.val = e.target.value;
    	})
    
    展开全文
  • 主要介绍了Vue实现双向绑定的原理以及响应式数据的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • vue双向绑定的原理及实现双向绑定MVVM源码分析 双向数据绑定的原理是:可以将对象的属性绑定到UI,具体的说,我们有一个对象,该对象有一个name属性,当我们给这个对象name属性赋新值的时候,新值在UI上也会得到...

    vue双向绑定的原理及实现双向绑定MVVM源码分析

        双向数据绑定的原理是:可以将对象的属性绑定到UI,具体的说,我们有一个对象,该对象有一个name属性,当我们给这个对象name属性赋新值的时候,新值在UI上也会得到更新。同样的道理,当我们有一个输入框或者textarea的时候,我们输入一个新值的时候,也会在该对象的name属性得到更新。

    实现数据绑定的做法有如下几种:
    1. 发布者-订阅模式(http://www.cnblogs.com/tugenhua0707/p/7471381.html)
    2. 脏值检查(angular.js)
    3. 数据劫持(vue.js)

    脏值检查:是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval()定时轮询检测数据的变动。

    数据劫持:vue.js 则是采用数据劫持结合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的setter,getter。在数据变动时发布消息给订阅者,触发响应的监听回调。

    下面是一个通过 Object.defineProperty 来实现一个简单的数据双向绑定。通过该方法来监听属性的变化。
    实现的效果简单如下:页面上有一个input输入框和div显示框,当在input输入框输入值的时候,div也会显示对应的值,当我打开控制台改变 obj.name="输入任意值"的时候,按回车键运行下,input输入框的值也会跟着变,可以简单的理解为 模型-> 视图的 改变,以及 视图 -> 模型的改变。如下代码:

    <!DOCTYPE html>
     <html>
      <head>
        <meta charset="utf-8">
        <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">
        <meta content="yes" name="apple-mobile-web-app-capable">
        <meta content="black" name="apple-mobile-web-app-status-bar-style">
        <meta content="telephone=no" name="format-detection">
        <meta content="email=no" name="format-detection">
        <title>标题</title>
        <link rel="shortcut icon" href="/favicon.ico">
      </head>
      <body>
        <h3>使用Object.defineProperty实现简单的双向数据绑定</h3>
        <input type="text" id="input" />
        <div id="div"></div>
        <script>
            var obj = {};
            var inputVal = document.getElementById("input");
            var div = document.getElementById("div");
    
            Object.defineProperty(obj, "name", {
              set: function(newVal) {
                inputVal.value = newVal;
                div.innerHTML = newVal;
              }
            });
            inputVal.addEventListener('input', function(e){
              obj.name = e.target.value;
            });
        </script>
      </body>
    </html>

    查看效果

    vue是通过数据劫持的方式来做数据绑定的,最核心的方法是通过 Object.defineProperty()方法来实现对属性的劫持,达到能监听到数据的变动。要实现数据的双向绑定,需要实现如下几点
    1. 需要实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动拿到最新值并通知订阅者。
    2. 需要实现一个指令解析器Compile, 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
    3. 需要实现一个Watcher,作为链接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应的回调函数,从而更新视图。

    一: 实现Observer
    我们可以使用Object.defineProperty()来监听属性的变动,我们需要对属性对象进行递归遍历,包括子属性对象的属性。再加上getter和setter方法,我们可以和上面的demo一样,监听input值的变化,即 视图 -> 模型。且当我们给某个对象的属性赋值的话,会自动调用setter方法,来动态修改数据,即 模型 -> 视图。
    引入我们可以使用 object.defineProperty来实现监听属性的变动,如下代码:

    <!DOCTYPE html>
     <html>
      <head>
        <meta charset="utf-8">
        <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">
        <meta content="yes" name="apple-mobile-web-app-capable">
        <meta content="black" name="apple-mobile-web-app-status-bar-style">
        <meta content="telephone=no" name="format-detection">
        <meta content="email=no" name="format-detection">
        <title>标题</title>
        <link rel="shortcut icon" href="/favicon.ico">
      </head>
      <body>
        <script>
          function Observer(data) {
            this.data = data;
            this.init();
          }
          Observer.prototype = {
            init: function() {
              var self = this;
              var data = self.data;
              // 遍历data对象
              Object.keys(data).forEach(function(key) {
                self.defineReactive(data, key, data[key]);
              });
            },
            defineReactive: function(data, key, value) {
    
              // 递归遍历子对象
              var childObj = observer(value);
    
              // 对对象的属性使用Object.defineProperty进行监听
              Object.defineProperty(data, key, {
                enumerable: true,  // 可枚举
                configurable: false, // 不能删除目标属性或不能修改目标属性
                get: function() {
                  return value;
                },
                set: function(newVal) {
                  if (newVal === value) {
                    return;
                  }
                  console.log('已经监听到值的变化了', value, '==>', newVal);
                  value = newVal;
                }
              });
            }
          }
    
          function observer(value) {
            if (!value || typeof value !== 'object') {
              return;
            }
            return new Observer(value);
          }
    
          // 测试demo
          var data = {name: 'kongzhi'};
          observer(data);
          data.name = 'kongzhi2';  // 控制台打印出  已经监听到值的变化了,kongzhi ==> kongzhi2
        </script>
      </body>
    </html>

    如上代码我们可以监听每个属性对象数据的变化了,那么监听到属性值变化后我们需要把消息通知到订阅者,因此我们需要实现一个消息订阅器,该订阅器的作用是收集所有的订阅者,当有属性值发生改变的时候,就把该消息通知给所有订阅者。因此我们可以实现如下代码:

    // 对所有的属性数据进行监听
    function Observer(data) {
      this.data = data;
      this.init();
    }
    
    Observer.prototype = {
      init: function() {
        var self = this;
        var data = self.data;
        // 遍历data对象
        Object.keys(data).forEach(function(key) {
          self.defineReactive(data, key, data[key]);
        });
      },
      defineReactive: function(data, key, value) {
        var dep = new Dep();
        // 递归遍历子对象
        var childObj = observer(value);
    
        // 对对象的属性使用Object.defineProperty进行监听
        Object.defineProperty(data, key, {
          enumerable: true,  // 可枚举
          configurable: false, // 不能删除目标属性或不能修改目标属性
          get: function() {
            if (Dep.target) {
              dep.depend();
            }
            return value;
          },
          set: function(newVal) {
            if (newVal === value) {
              return;
            }
            value = newVal;
            // 如果新值是对象的话,递归该对象 进行监听
            childObj = observer(newVal);
            // 通知订阅者 
            dep.notify();
          }
        });
      }
    }
    
    function observer(value) {
      if (!value || typeof value !== 'object') {
        return;
      }
      return new Observer(value);
    }
    
    function Dep() {
      this.subs = [];
    }
    
    Dep.prototype = {
      addSub: function(sub) {
        this.subs.push(sub);
      },
      depend: function() {
        Dep.target.addDep(this);
      },
      removeSub: function(sub) {
        var index = this.subs.indexOf(sub);
        if (index != -1) {
          this.subs.splice(index, 1);
        }
      },
      notify: function() {
        // 遍历所有的订阅者 通知所有的订阅者
        this.subs.forEach(function(sub) {
          sub.update();
        })
      }
    };
    
    Dep.target = null;

    从上面的分析得到,需要实现一个Watcher,作为链接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,所以订阅者就是Watcher,var dep = new Dep();是在 defineReactive方法内部定义的,是想通过dep添加订阅者,通过Dep定义一个全局的target属性,暂存watcher,添加完后会移除,因此最后一句代码设置 Dep.target = null;
    执行getter方法会返回值,执行setter方法会判断新旧值是否相等,如果不相等的话,再次递归遍历设置的对象的所有子对象,然后会通知所有的订阅者。

    二: 实现 Compile

    compile做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数。

    如下图所示:

    // 具体看如下代码,代码中有对应的注释
    
    /*
     * @param {el} 元素节点容器 如:el: '#mvvm-app'
     * @param {vm} Object 对象传递数据 
    */
    function Compile(el, vm) {
      this.$vm = vm;
      // 判断是元素节点 还是 选择器
      this.$el = this.isElementNode(el) ? el : document.querySelector(el);
      if (this.$el) {
        /*
         因为遍历解析过程有多次操作dom节点,为了提高性能和效率,会先将根节点el转换成文档碎片fragment进行解析编译操作,
         解析完成后,再将fragment添加回原来真实的dom节点中。有关 DocumentFragment 请看 http://www.cnblogs.com/tugenhua0707/p/7465915.html
         */
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
      }
    }
    
    Compile.prototype = {
      node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(),
          child;
        // 将原生节点拷贝到fragment中
        while (child = el.firstChild) {
          fragment.appendChild(child);
        }
        return fragment;
      },
      init: function() {
        // 进行解析编译操作
        this.compileElement(this.$fragment);
      },
      compileElement: function(el) {
        var childNodes = el.childNodes;
        // 遍历所有的子节点 判断子节点是 元素节点还是文本节点 分别进行编译解析操作
        [].slice.call(childNodes).forEach(function(node) {
    
          // 获取节点的文本内容和它的所有后代
          var text = node.textContent;
    
          // 正则匹配 {{xx}} 这样的xx文本值
          var reg = /\{\{(.*)\}\}/;
          // 判断是否是元素节点,然后进行编译
          if (this.isElementNode(node)) {
            this.compile(node);
    
          } else if(this.isTextNode(node) && reg.test(text)) {
            // 判断node是否是文本节点 且 符合正则匹配的 {{xx}} 那么就会进行编译解析
            this.compileText(node, RegExp.$1);
          }
    
          // 如果该节点还有子节点的话,那么递归进行判断编译
          if (node.childNodes && node.childNodes.length) {
            this.compileElement(node);
          }
        });
      },
      // 元素节点编译
      compile: function(node) {
        // 获取节点的所有属性
        var nodeAttrs = node.attributes;
        var self = this;
        /*
         遍历该节点的所有属性,判断该属性是事件指令还是普通指令
         */
        [].slice.call(nodeAttrs).forEach(function(attr) {
          // 获取属性名
          var attrName = attr.name;
          // 先判断属性名是否是 以 v- 开头的
          if (self.isDirective(attrName)) {
            var attrValue = attr.value;
            // 获取 v-xx 中从xx开始的所有字符串
            var dir = attrName.substring(2);
            // 判断是否是事件指令
            if (self.isEventDirective(dir)) {
              compileUtil.eventHandler(node, self.$vm, attrValue, dir);
            } else {
              // 普通指令
              compileUtil[dir] && compileUtil[dir](node, self.$vm, attrValue);
            }
            // 循环完成一次后 删除该属性
            node.removeAttribute(attrName);
          }
        });
      },
      // 编译文本
      compileText: function(node, exp) {
        compileUtil.text(node, this.$vm, exp);
      },
      // 是否是v- 开始的指令
      isDirective: function(attrName) {
        return attrName.indexOf('v-') === 0;
      },
      /*
       * 是否是事件指令 事件指令以 v-on开头的
       */
       isEventDirective: function(dir) {
         return dir.indexOf('on') === 0;
       },
       // 是否是元素节点
       isElementNode: function(node) {
         return node.nodeType === 1;
       },
       // 是否是文本节点
       isTextNode: function(node) {
         return node.nodeType === 3;
       }
    };
    
    // 指令处理
    var compileUtil = {
      text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
      },
      html: function(node, vm, exp) {
        this.bind(node, vm, exp, 'html');
      },
      /*
       * 普通指令 v-model 开头的,调用model方法
       * @param {node} 容器节点
       * @param {vm} 数据对象
       * @param {exp} 普通指令的值 比如 v-model="xx" 那么exp就等于xx
       */
      model: function(node, vm, exp) {
        this.bind(node, vm, exp, 'model');
        var self = this;
        var val = this.getVMVal(vm, exp);
        // 监听input的事件
        node.addEventListener('input', function(e) {
          var newValue = e.target.value;
          // 比较新旧值是否相同
          if (val === newValue) {
            return;
          }
          // 重新设置新值
          self.setVMVal(vm, exp, newValue);
          val = newValue;
        });
      },
      /*
       *  返回 v-mdoel = "xx" 中的xx的值, 比如data对象会定义如下:
        data: {
          "xx" : "111"
        }
        * @param {vm} 数据对象
        * @param {exp} 普通指令的值 比如 v-model="xx" 那么exp就等于xx
       */
      getVMVal: function(vm, exp) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(k) {
          val = val[k];
        });
        return val;
      },
      /*
        设置普通指令的值
        @param {vm} 数据对象
        @param {exp} 普通指令的值 比如 v-model="xx" 那么exp就等于xx
        @param {value} 新值
       */
      setVMVal: function(vm, exp, value) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(key, index) {
          // 如果不是最后一个元素的话,更新值
          /*
           数据对象 data 如下数据
           data: {
            child: {
              someStr: 'World !'
            }
          },
           如果 v-model="child.someStr" 那么 exp = ["child", "someStr"], 遍历该数组,
           val = val["child"]; val 先储存该对象,然后再继续遍历 someStr,会执行else语句,因此val['someStr'] = value, 就会更新到对象的值了。
           */
          if (i < exp.length - 1) {
            val = val[key];
          } else {
            val[key] = value;
          }
        });
      },
      class: function(node, vm, exp) {
        this.bind(node, vm, exp, 'class');
      },
      /* 
       事件处理
       @param {node} 元素节点
       @param {vm} 数据对象
       @param {attrValue} attrValue 属性值
       @param {dir} 事件指令的值 比如 v-on:click="xx" 那么dir就等 on:click
       */
      eventHandler: function(node, vm, attrValue, dir) {
        // 获取事件类型 比如dir=on:click 因此 eventType="click" 
        var eventType = dir.split(':')[1];
        /*
         * 获取事件的函数 比如 v-on:click="clickBtn" 那么就会对应vm里面的clickBtn 函数。
         * 比如 methods: {
            clickBtn: function(e) {
              console.log(11)
            }
          }
         */
        var fn = vm.$options.methods && vm.$options.methods[attrValue];
        if (eventType && fn) {
          // 如果有事件类型和函数的话,就绑定该事件
          node.addEventListener(eventType, fn.bind(vm), false);
        }
      },
      /*
       @param {node} 节点
       @param {vm} 数据对象
       @param {exp} 正则匹配的值
       @param {dir} 字符串类型 比如 'text', 'html' 'model'
       */
      bind: function(node, vm, exp, dir) {
        // 获取updater 对象内的对应的函数
        var updaterFn = updater[dir + 'Updater'];
    
        // 如有该函数的话就执行该函数 参数node为节点,第二个参数为 指令的值。 比如 v-model = 'xx' 那么返回的就是xx的值
        updaterFn && updaterFn(node, this.getVMVal(vm, exp));
    
        // 调用订阅者watcher
        new Watcher(vm, exp, function(newValue, oldValue) {
          updaterFn && updaterFn(node, newValue, oldValue);
        });
      }
    };
    
    var updater = {
      textUpdater: function(node, value) {
        node.textContent = typeof value === 'undefined' ? '' : value;
      },
      htmlUpdater: function(node, value) {
        node.innerHTML = typeof value === 'undefined' ? '' : value;
      },
      classUpdater: function(node, newValue, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');
        var space = className && String(newValue) ? ' ' : '';
        node.className = className + space + newValue;
      },
      modelUpdater: function(node, newValue) {
        node.value = typeof newValue === 'undefined' ? '' : newValue;
      }
    };

    三: 实现Watcher
    Watcher是订阅者是作为 Observer和Compile之间通信的桥梁。
    相关代码如下:

    /*
     * @param {vm} 数据对象
     * @param {expOrFn} 属性值 比如 v-model='xx' v-on:click='yy' v-text="tt" 中的 xx, yy, tt 
     * @param {cb}  回调函数
     */
    function Watcher(vm, expOrFn, cb) {
      this.vm = vm;
      this.expOrFn = expOrFn;
      this.cb = cb;
      this.depIds = {};
    
      // expOrFn 是事件函数的话
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
      } else {
        this.getter = this.parseGetter(expOrFn);
      }
      // 为了触发属性getter,从而在dep添加自己作为订阅者
      this.value = this.get();
    }
    
    Watcher.prototype = {
      update: function() {
        this.run();    // observer 中的属性值发生变化 收到通知
      },
      run: function() {
        var value = this.get();
        var oldValue = this.value;
        // 新旧值不相等的话
        if (value !== oldValue) {
          // 把当前的值赋值给 this.value 更新this.value的值
          this.value = value;
          this.cb.call(this.vm, value, oldValue);  // 执行Compile中绑定的回调 更新视图
        }
      },
      get: function() {
        Dep.target = this; // 将当前订阅者指向自己
        var value = this.getter.call(this.vm, this.vm); // 触发getter,添加自己到属性订阅器中
        Dep.target = null;  // 添加完成后 清空数据
        return value;
      },
      parseGetter: function(exp) {
        var reg = /[^\w.$]/;
        if (reg.test(exp)) {
          return;
        }
        var exps = exp.split('.');
        return function(obj) {
          for(var i = 0, len = exps.length; i < len; i++) {
            if (!obj) {
              return;
            }
            obj = obj[exps[i]];
          }
          return obj;
        }
      }
    }

    四: 实现MVVM
    MVVM是数据绑定的入口,整合 Observer, Compile, 和 Watcher,通过Observer来监听model数据变化,通过Compile来解析编译模板指令,最后使用watcher搭起Observer和Compile之间的通信桥梁。 达到数据变化 --》视图更新,视图变化 --》 数据model更新的 双向绑定效果。
    代码如下:

    function MVVM(options) {
      this.$options = options || {};
      var data = this._data = this.$options.data;
      var self = this;
    
      /*
       数据代理,实现 vm.xxx 
       上面的代码看出 监听数据的对象是 options.data, 因此每次更新视图的时候;如:
       var vm = new MVVM({
         data: {name: 'kongzhi'}
       });
       那么更新数据就变成 vm._data.name = 'kongzhi2'; 但是我们想实现这样更改 vm.name = "kongzhi2";
       因此这边需要使用属性代理方式,利用Object.defineProperty()方法来劫持vm实列对象属性的读写权,使读写vm实列的属性转成vm.name的属性值。
       */
       Object.keys(data).forEach(function(key) {
         self._proxyData(key);
       });
    
       this._initComputed();
    
       // 初始化Observer
       observer(data, this);
    
       // 初始化 Compile
       this.$compile = new Compile(options.el || document.body, this);
    }
    
    MVVM.prototype = {
      $watch: function(key, cb, options) {
        new Watcher(this, key, cb);
      },
    
      _proxyData: function(key) {
        var 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;
          }
        })
      },
    
      _initComputed: function() {
        var self = this;
        var computed = this.$options.computed;
        if (typeof computed === 'object') {
          Object.keys(computed).forEach(function(key) {
            Object.defineProperty(self, key, {
              get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
              set: function() {}
            })
          })
        }
      }
    }

    html代码初始化如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>MVVM</title>
    </head>
    <body>
    
    <div id="mvvm-app">
      <input type="text" v-model="someStr">
      <input type="text" v-model="child.someStr">
      <p>{{getHelloWord}}</p>
      <p v-html="child.htmlStr"></p>
      <button v-on:click="clickBtn">change model</button>
    </div>
    <script src="./observer.js"></script>
    <script src="./watcher.js"></script>
    <script src="./compile.js"></script>
    <script src="./mvvm.js"></script>
    <script>
        var vm = new MVVM({
          el: '#mvvm-app',
          data: {
            someStr: 'hello ',
            className: 'btn',
            htmlStr: '<span style="color: #f00;">red</span>',
            child: {
              someStr: 'World !'
            }
          },
          computed: {
            getHelloWord: function() {
              return this.someStr + this.child.someStr;
            }
          },
          methods: {
            clickBtn: function(e) {
              var randomStrArr = ['childOne', 'childTwo', 'childThree'];
              this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
            }
          }
        });
        vm.$watch('child.someStr', function() {
            console.log(arguments);
        });
    </script>
    </body>
    </html>

    代码分析:

    1. 代码初始化执行

    首先html代码如下:

    <div id="mvvm-app">
      <input type="text" v-model="someStr">
      <input type="text" v-model="child.someStr">
      <p>{{getHelloWord}}</p>
      <p v-html="child.htmlStr"></p>
      <button v-on:click="clickBtn">change model</button>
    </div>

    数据调用如下:

    var vm = new MVVM({
      el: '#mvvm-app',
      data: {
        someStr: 'hello ',
        className: 'btn',
        htmlStr: '<span style="color: #f00;">red</span>',
        child: {
          someStr: 'World !'
        }
      },
      computed: {
        getHelloWord: function() {
          return this.someStr + this.child.someStr;
        }
      },
      methods: {
        clickBtn: function(e) {
          var randomStrArr = ['childOne', 'childTwo', 'childThree'];
          this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
        }
      }
    });
    vm.$watch('child.someStr', function() {
      console.log(arguments);
    });

    执行 new MVVM 后 mvvm.js 实列化,mvvm.js 代码如下:

    function MVVM(options) {
      this.$options = options || {};
      var data = this._data = this.$options.data;
      var self = this;
      /*
       数据代理,实现 vm.xxx 
       上面的代码看出 监听数据的对象是 options.data, 因此每次更新视图的时候;如:
       var vm = new MVVM({
         data: {name: 'kongzhi'}
       });
       那么更新数据就变成 vm._data.name = 'kongzhi2'; 但是我们想实现这样更改 vm.name = "kongzhi2";
       因此这边需要使用属性代理方式,利用Object.defineProperty()方法来劫持vm实列对象属性的读写权,使读写vm实列的属性转成vm.name的属性值。
       */
       Object.keys(data).forEach(function(key) {
         self._proxyData(key);
       });
    
       this._initComputed();
    
       // 初始化Observer
       observer(data, this);
    
       // 初始化 Compile
       this.$compile = new Compile(options.el || document.body, this);
    }

    因此 参数options值为对象(Object):

    this.$options = {
      el: '#mvvm-app',
      data: {
        someStr: 'hello ',
        className: 'btn',
        htmlStr: '<span style="color: #f00;">red</span>',
        child: {
          someStr: 'World !'
        }
      },
      computed: {
        getHelloWord: function() {
          return this.someStr + this.child.someStr;
        }
      },
      methods: {
        clickBtn: function(e) {
          var randomStrArr = ['childOne', 'childTwo', 'childThree'];
          this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
        }
      }
    }
    var data = this._data = this.$options.data = {
      someStr: 'hello ',
      className: 'btn',
      htmlStr: '<span style="color: #f00;">red</span>',
      child: {
        someStr: 'World !'
      }
    }

    然后 遍历data对象,如下:

    Object.keys(data).forEach(function(key) {
      self._proxyData(key);
    });

    _proxyData 方法代码如下:

    _proxyData: function(key) {
      var 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;
        }
      })
    }

    使用Object.defineProperty来监听对象属性的变化,使vm实列的属性转变成vm._data的属性值。
    接着初始化 this._initComputed();方法;代码如下:

    _initComputed: function() {
      var self = this;
      var computed = this.$options.computed;
      if (typeof computed === 'object') {
        Object.keys(computed).forEach(function(key) {
          Object.defineProperty(self, key, {
            get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
            set: function() {}
          })
        })
      }
    }

    首先判断 computed 是否为对象,然后遍历computed,判断如果computed[key] 键是否是一个函数,如果是一个函数的话,就执行该函数,否则的话,就执行该get调用。所以我们看到使用vue的
    时候会看到 computed方法初始时调用。

    然后 初始化Observer
    observer(data, this);

    因此会实例化 Observer;代码如下:

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

    因此 this.data值为:

    this.data = {
      someStr: 'hello ',
      className: 'btn',
      htmlStr: '<span style="color: #f00;">red</span>',
      child: {
        someStr: 'World !'
      }
    }

    然后调用init方法,如下代码:

    init: function() {
      var self = this;
      var data = self.data;
      // 遍历data对象
      Object.keys(data).forEach(function(key) {
        self.defineReactive(data, key, data[key]);
      });
    },

    遍历data,获取每一个key,也就是 key 可以为 someStr, className, htmlStr, child等。
    然后执行 self.defineReactive 方法,参数如下三个

    data = {
      someStr: 'hello ',
      className: 'btn',
      htmlStr: '<span style="color: #f00;">red</span>',
      child: {
        someStr: 'World !'
      }
    }

    key 分别为 someStr, className, htmlStr, child,
    data[key]值分别为:

    'hello ', 'btn', '<span style="color: #f00;">red</span>', 和 
    {
      someStr: 'World !'
    }

    调用 defineReactive  代码如下:

    defineReactive: function(data, key, value) {
      var dep = new Dep();
      // 递归遍历子对象
      var childObj = observer(value);
    
      // 对对象的属性使用Object.defineProperty进行监听
      Object.defineProperty(data, key, {
        enumerable: true,  // 可枚举
        configurable: false, // 不能删除目标属性或不能修改目标属性
        get: function() {
          if (Dep.target) {
            dep.depend();
          }
          return value;
        },
        set: function(newVal) {
          if (newVal === value) {
            return;
          }
          value = newVal;
          // 如果新值是对象的话,递归该对象 进行监听
          childObj = observer(newVal);
          // 通知订阅者 
          dep.notify();
        }
      });
    }

    同理 使用 Object.defineProperty 监听对象属性值的变化。执行完成后,下一步代码执行如下:
    // 初始化 Compile
    this.$compile = new Compile(options.el || document.body, this);

    options.el参数就是传进去的Id为 #mvvm-app, 如果没有的话,就是 document.body;
    compile实列化代码如下:

    /*
     * @param {el} 元素节点容器 如:el: '#mvvm-app'
     * @param {vm} Object 对象传递数据 
    */
    function Compile(el, vm) {
      this.$vm = vm;
      // 判断是元素节点 还是 选择器
      this.$el = this.isElementNode(el) ? el : document.querySelector(el);
      if (this.$el) {
        /*
         因为遍历解析过程有多次操作dom节点,为了提高性能和效率,会先将根节点el转换成文档碎片fragment进行解析编译操作,
         解析完成后,再将fragment添加回原来真实的dom节点中。有关 DocumentFragment 请看 http://www.cnblogs.com/tugenhua0707/p/7465915.html
         */
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
      }
    }

    调用init方法会进行compileElement 执行解析编译操作,找到id为#mvvm-app的childNodes, 遍历子节点首先会判断是元素节点还是文本节点,如果是元素节点会调用 该如下方法:
    this.compile(node); 进行元素节点编译和解析操作, 如果是文本节点的话,就会 调用 this.compileText(node, RegExp.$1); 方法编译文本节点。

    理清思路:
    视图 --> 模型的改变:

    在输入框输入新值的时,会在 compile.js里面会监听input事件,如下代码:

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

    首先会获取当前的值,然后执行 setVMVal该方法,如下代码:

    /*
        设置普通指令的值
        @param {vm} 数据对象
        @param {exp} 普通指令的值 比如 v-model="xx" 那么exp就等于xx
        @param {value} 新值
       */
      setVMVal: function(vm, exp, value) {
        var val = vm;
        exp = exp.split('.');
        exp.forEach(function(key, index) {
          // 如果不是最后一个元素的话,更新值
          /*
           数据对象 data 如下数据
           data: {
            child: {
              someStr: 'World !'
            }
          },
           如果 v-model="child.someStr" 那么 exp = ["child", "someStr"], 遍历该数组,
           val = val["child"]; val 先储存该对象,然后再继续遍历 someStr,会执行else语句,因此val['someStr'] = value, 就会更新到对象的值了。
           */
          if (index < exp.length - 1) {
            val = val[key];
          } else {
            val[key] = value;
          }
        });
      }

    执行完setVMVal方法后,会拿到最新值,然后会调用 Object.defineProperty 里面的setter方法,如下代码:

    set: function(newVal) {
        if (newVal === value) {
          return;
        }
        value = newVal;
        // 发布消息
        dep.public();
      }

    从而发布消息, 执行方法 dep.public(); 因此会调用Dep里面的public方法,代码如下:

    public: function() {
        this.subs.forEach(function(sub){
          sub.update();
        });
      }

    从而调用 watcher.js 里面的update方法,代码如下:

    update: function() {
      this.run();
    },

    接着会调用run方法,代码如下:

    run: function() {
        var value = this.get();
        var oldVal = this.value;
        if (value !== oldVal) {
          this.value = value;
          this.cb.call(this.vm, value, oldVal);
        }
     },

    先调用get方法拿到当前的值,代码如下:

    get: function() {
        Dep.target = this;
        var value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    },

    如上代码,会调用 this.getter方法,this.getter方法初始化代码在watcher如下:

    // expOrFn 是事件函数的话
      if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
      } else {
        this.getter = this.parseGetter(expOrFn);
      }

    接着调用 parseGetter 方法代码:

    parseGetter: function(exp) {
        if (/[^\w.$]/.test(exp)) return; 
        var exps = exp.split('.');
        return function(obj) {
          for (var i = 0, len = exps.length; i < len; i++) {
            if (!obj) return;
            obj = obj[exps[i]];
          }
          return obj;
        }
    }

    用完成后,会执行到get方法里面代码 var value = this.getter.call(this.vm, this.vm); 因此会继续调用到 parseGetter 返回函数的代码,所以最后返回新值。
    返回新值后,在执行函数run方法后,判断新旧值是否相等,不相等的话,就会执行 compile.js里面bind的方法的回调,如下代码:

    new Watcher(vm, attrValue, function(value, oldValue) {
        updaterFn && updaterFn(node, value, oldValue);
    });

    compile.js里面的updater对象如下:

    var updater = {
      textUpdater: function(node, value) {
        node.textContent = typeof value === 'undefined' ? '' : value;
      },
      htmlUpdater: function(node, value) {
        node.innerHTML = typeof value === 'undefined' ? '' : value;
      },
      classUpdater: function(node, newValue, oldValue) {
        var className = node.className;
        className = className.replace(oldValue, '').replace(/\s$/, '');
        var space = className && String(newValue) ? ' ' : '';
        node.className = className + space + newValue;
      },
      modelUpdater: function(node, newValue) {
        node.value = typeof newValue === 'undefined' ? '' : newValue;
      }
    };

    从而更新视图。

    模型 -> 视图的改变

    当页面初始化时,会判断是普通指令 v-model, v-text, or v-html,还是事件指令 v-on:click; compile.js相对应的代码如下:

    // 判断是否是事件指令
    if (self.isEventDirective(dir)) {
      // 事件指令 比如 v-on:click 这样的
      compileUtil.eventHandler(node, self.$vm, attrValue, dir);
    }

    所以会对事件进行处理: 如下函数:

    // 事件处理
    eventHandler: function(node, vm, attrValue, dir) {
      var eventType = dir.split(':')[1],
        fn = vm.$options.methods && vm.$options.methods[attrValue];
        if (eventType && fn) {
          node.addEventListener(eventType, fn.bind(vm), false);
        }
    }

    因此会对该节点进行绑定 'click'事件,代码 node.addEventListener(eventType, fn.bind(vm), false); 就是对绑定的事件 进行监听,demo里面是使用 v-on:click, 因此eventType就是click事件,因此会对 点击事件 进行监听。然后执行相对应的回调函数。该demo中的回调函数为 clickBtn(函数名)。
    clickBtn函数代码如下:

    clickBtn: function(e) {
      var randomStrArr = ['childOne', 'childTwo', 'childThree'];
      this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
    }

    因此 this.child.someStr 会重新赋值一个随机数,也就是说值会得到更新,因此首先会调用 mvvm.js的_proxyData中的get方法;代码如下:

    _proxyData: function(key, setter, getter) {
      var me = this;
      setter = setter || 
      Object.defineProperty(me, key, {
          configurable: false,
          enumerable: true,
          get: function proxyGetter() {
              return me._data[key];
          },
          set: function proxySetter(newVal) {
              me._data[key] = newVal;
          }
      });
    }

    页面初始化的时候,vm.data.child.someStr 会被mvvm.js 里面的 代理方法转换成 vm.child.someStr, 因此给 vm.child.someStr 设置新值的时候,会调用 Object.defineProperty方法来监听属性值的改变,因此需要返回一个新值,所以先调用mvvm.js中的get方法,之后会调用 Observer.js代码 的Object.defineProperty(obj, key, {})中的get方法,返回页面初始化的值,然后会调用Observer.js该对应的set方法,获取新值,然后判断新旧值是否相等,如果不相等的话,就会把消息发布出去该订阅者,订阅者会接收该消息。从而和上面 视图 -》模型 步骤一样 渲染视图页面。
    git代码

    转载于:https://www.cnblogs.com/tugenhua0707/p/7589602.html

    展开全文
  • 当然对这个问题,不同的面试官可能会有不同的问法,比如,直接提问 Vue 中数据双向绑定的原理是什么,提问 v-model 的实现原理是什么,提问 Vue 是如何实现当我们更新数据时能够实现页面响应式渲染的,等等。...
  • Vue 双向绑定的原理

    2018-05-29 14:32:02
    Vue双向绑定的原理 大致思路: 首先Vue会使用documentfragment劫持根元素里包含的所有节点,这些节点不仅包括标签元素,还包括文本,甚至换行的回车。  然后Vue会把data中所有的数据,用defindProperty()变成Vue的...
  • vue数据双向绑定的原理 vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者 数据劫持 Object.defineProperty() 当...
  • Vue数据双向绑定的原理解析 思路     所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据。如图  也就是说,输入框内容变化时,data 中的数据同步变化。即 view —> model 的变化。data ...
  • Vue实现数据双向绑定的原理 (一)Vue实现数据双向绑定主要是:采用数据劫持结合发布者–订阅者模式的方式,通过object.defineProperty()来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的...
  • 转-Vue双向绑定的原理

    2017-06-21 20:24:15
    Vue双向绑定的原理一篇很不错的文章:Vue.js双向绑定的实现原理原文:(http://blog.csdn.net/creabine/article/details/59201837)主要的知识点: 1.Vue双向绑定原理(一)文档片段DocumentFragment 2.Vue双向绑定...
  • 第一次写,CSDN博客,希望这篇文章对大家的学习有所...vue实现双向绑定的原理是数据劫持结合发布订阅模式和Object.defineProperty中的get和set方法实现的。 自己实现一个数据双向绑定 主要思路是先的定义一个对象...
  • vue 数据双向绑定的原理 VUE实现双向数据绑定的原理就是利用了 Object.defineProperty() 这个方法重新定义了对象**获取属性值(get)和设置属性值(set)**的操作来实现的。 它会接收三个参数 第一个参数:定义属性的...
  • 数据双向绑定的原理 var inputText = document.querySelector('#input') var h1 = document.querySelector('h1') var h3 = document.querySelector('#username') var age = document.querySelector('#age') ...
  • Vue 双向绑定的原理及实现Demo

    千次阅读 2017-03-01 23:41:39
    Vue双向绑定的原理Vue用了一段时间了,一直没有纠结过它的原理,今天看了一篇很不错的文章:Vue.js双向绑定的实现原理,跟着敲了一遍,发现其中有意思的地方还是很多的,一些知识我之前都没有接触过,这里要好好整理...
  • 前几天,阿W在宿舍跟我提到在面试的时候被问到的一个问题,是关于Vue双向绑定的原理的。当我听到之后,表示一头雾水,于是上网查了一下相关的资料。其中遇到了一个关键词: Object.defineProperty其实这个关键词之前...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,353
精华内容 1,341
关键字:

双向绑定的原理