Vue双向数据绑定,Model是如何改变View,View又是如何改变Model的
1. 原理解析
大家可能发现IE8以下的浏览器不兼容Vue.js,只是因为Vue的双向数据绑定核心是利用ES5的Object.defineProperty。
1. Object.defineProperty
对于Object.defineProperty来说,它会直接在对象上定义一个新的属性,或者修改一个对象的现有属性,它的返回值就是这个对象。
Object.defineProperty(obj, prop, descriptor)
属性解释
- obj 定义属性的对象
- prop 要定义或修改的属性的名称
- descriptor将被定义或修改属性的描述符(这是核心)
2. observe
对于observe来说,它的功能就是用来检测数据的变化。实现方式是给非VNode的对象类型数据添加一个Observer,如果已经添加了就直接返回,否则去实例化一个Observer对象实例,但是需要满足一定的条件.
对于Observer来说,它是一个类,通过给对象添加getter和setter来实现依赖收集和派发更新.
- 依赖收集(getter)
在get函数中通过dep.depend做依赖收集const dep= new Dep() //实例化一个Dep实例
下面说说Dep:
Dep是一个Class,定义了一些属性和方法,它有一个静态属性target,这是一个全局唯一Watcher。对于Watcher来说,同一时间内只有一个全局的Watcher被计算。这么看来Dep就是对Watcher的一种管理,Dep如果脱离了Watcher是没有意义的。而Watcher和Dep的配合就是一个典型的观察者设计模式。
下面说说Watcher:
Watcher是一个Class,在它的构造函数中定义了一些和Dep相关的属性:
this.dep=[]
this.newDeps=[]
this.depIds = new Set()
this.newDepIds = new Set()
- 收集过程:
在实例化一个渲染watcher的时候,首先进入watcher的构造函数逻辑,然后执行this.get()
方法,进入get函数把Dep.target赋值为当前渲染的watcher并且将它压栈。然后使用vm._render()
方法,生成渲染VNode,并且在在这个过程中对vm上的数据访问,这个时候就触发数据对象的getter,在这个过程内执行Dep.target.addDep(this)
方法,将watcher订阅到这个数据持有的deo的subs中,为后续数据变化时通知到哪些subs做准备。最后递归遍历添加所有的子项的getter。
Watcher在构造函数中初始化两个Dep实例数组。newDeps代表添加的Dep实例数组,deps代表上一次添加的Dep实例数组。
依赖清空:在执行清空依赖(cleanupDeps)函数时,会首先遍历deps,移除对dep的订阅,然后把newDepIds和depIds交换,newDeps和deps交换,并且把newDepsIds和newDeps清空,在条件渲染时,即使对不用渲染的数据订阅移除,减少性能浪费。
由于Vue是数据驱动的,所以在每次数据变化都会重写Render,那么vm._render()方法会再次执行,并再次触发数据
收集依赖的目的是为了当这些响应式数据发生变化时,触发它们的setter的时候,能知道应该通过哪些订阅者去做相应的逻辑处理(也就是派发更新) - 派发更新(setter)
childOb = !shallow &&observe(newVal) //如果shallow为false的情况,会对新设置的值变成一个响应式对象 dep.notify() // 通知所有订阅者
- 派发过程
当我们组件中对相应的数据做了修改,就会触发setter的逻辑,最后调用dep.notify()方法,它是Dep的一个实例方法。具体的做法是遍历依赖收集中建立的subs,也就是Watcher的实例数组。subs数组在依赖收集getter中被添加,期间会通过一些逻辑处理判断保证同意数组不会被添加多次,然后调用每一个watcher的update方法.
update函数中有多个queueWatcher(this)方法引入了队列的概念,是vue在做派发更新时优化的一个点,它并不会每次数据改变都会触发watcher回调,而是把watcher先添加到一个队列中,然后在nextTick后执行watcher的run函数- 队列排序保证
- 组件的=更新由父到子,父组件的创建要早于子组件,watcher的创建也是如此
- 自定义的watcher要早于watcher执行,原因是自定义的watcher是在渲染watcher前创建的
- 队列遍历:排序完成后,对队列进行遍历,拿到对应的watcher,执行watcher.run()
- run函数解析
先通过this.get()得到它的当前值,然后做判断,如果满足新旧值不相等、新值是对象类型、deep模式任何一个条件,则执行watcher的回调,注意回调函数执行时会把第一个参数和第二个参数传入新值value和旧值oldValue,这就是当我们自己添加watcher时可以在参数中取得新旧值的来源。对应渲染watcher而言,在执行this.get()方法求值的时候会执行getter方法。因此,修改组件相关数据的时候,会触发组件重新渲染,接着执行patch的过程。
对本人来说,如此篇幅的纯文字博文还是头一次,难免会有一些语言表达不规范和难以阅读之处,希望大家可以多多包涵。没有代码的博文不是一篇好博文,下面给各位老铁表演一个手写数据绑定。
<input id="input" type="text">
<div id="text"></div>
let data={value:''};
Object.defineProperty(data,'value',{
set:function(val){
$("#text").innerHTML = val;
$("#input").value = val;
},
get:function(val){
return $("#input").value;
}
})
$("#input").onkeyup = function(e){
data.value=e.target.value;
}
function $(str){
if(str.charAt(0)=="#"){
return document.getElementById(str.substr(1));
}else if(str.charAt(0)=="."){
return document.getElementsByClassName(str.substr(1));
}else{
return document.getElementsByTagName(str.substr(1));
}
}