数据实现响应式
vue2 使用的Object.defineProperty,利用observe递归和遍历属性 进行属性的绑定,设置值的时候 同样会进行绑定,如果是对象就会递归,进行Object.defineProperty。
数组的绑定
先复习js知识点Object.create方法将一个对象,绑定到另一个对象的原型上面
判断值如果是数组了,就会执行数据的特殊的双向绑定的方法。并且为数据的每一个属性都判断,如果为对象就双向绑定,否则不绑定。这里需要注意一下ObserveArray方法,这个是判断列表中的元素是否为对象,是的话给对象里面的属性进行双向绑定,而不是直接给list[0]这个进行绑定。list[0]='123’是不会触发set的。如果list[0]是对象的话会触发。
list[0]={name:‘q’}
list[0].name='w’这样则会触发更新
在数组对象上新定义 push,shift等方法,并且利用Object.create方法将Array.propotype绑定到新的对象的原型上,这样新的对象就能使用数据的所有方法了,包括foreach等等, 然后对push等方法进行重写,添加上双向绑定的逻辑,内部再调用数组原先的方法即可
在push方法中怎样得到的observeArray方法的,就是给数组添加新的属性,数组data.__ob__将当前类this赋值给 数组data.ob=this
这样在重写的push方法中就可以用__ob__得到observe 进而实现双向绑定了
newArrayProto
const oldArrayProto = Array.prototype
export newArrayProto = Object.create(oldArrayProto)//导出此变量
newArrayProto.push = function(...args){
const result = oldArrayProto.push.call(this,..args)
...省略
this.__ob__.observeArray(...args)
/**
对新增的数据进行数据劫持使用observeArray方法,
在上面已经对数据添加了__ob__属性,这个属性指向vm
所以使用
this.__ob__.observeArray(...args))就可以对新的内容进行双向绑定
**/
}
解析模板参数
大概思路步骤如下 1、采用虚拟dom,数据变化后比较虚拟dom的差异,更新需要更新的地方。
2、核心是将模板变成js语法,通过js生成虚拟dom。
**template模板,
将template转化为render函数
1.解析的时候第一步先看是否有render
如果有
就取render函数
如果没有
就取template属性或者从el上得到整个模板
**
取到整个模板之后 就要将模板变为render函数返回
2、编译的过程
compileToFunction方法
如果使用script标签的话是在浏览器中运行的,
如果是脚手架则是启动前由loader提前编译好的。
以下是compilToFunction的具体实现
**
上面compilToFunction函数的详细步骤为下面的1和2
1、将template转化为ast语法树
2、根据ast语法树生成render方法,
,
3、执行render方法生成虚拟dom
4、根据虚拟dom生成真实dom**
1、如下图就是ast对象语法树,使用js对象描述了整个template模板,利用正则表达式
2、 如下图就是编译后生成的render函数,里面可以将模板中的js语法进行执行,可以使用相应的变量,注意jsx最终也会被便以为render函数
会先将下面的内容变为字符串,然后使用new Function(with(this){字符串render})变为真正的render函数。放在vm上面去执行。
3、将render函数变为虚拟dom
只需要调用vm.$options.render函数即可得到一个虚拟dom,
然后使用vm._update函数,将虚拟dom变为真实dom
ast本身是描述html语法的,虚拟dom是对应真实dom的,正好相反的,ast是html到js,虚拟dom是 js到html。dom可以增加属性,增加功能。
_update方法中是调用的patch方法,利用nodeType判断是否为真节点,如果是的话 说明是初渲染,就将老节点删除 换为新生成的真实dom
如上图的patch方法就是创建新的dom节点,然后插入,将返回的新的dom节点替换给vm.$el
到上面 初始化就完成了,接下来是更新
将渲染逻辑放到watcher类中,watcher存放渲染逻辑的代码而已。
每个组件中都有一个watcher实例,watcher实例中存放着dep列表,dep中又存放着watcher,每当数据更新的时候,就会去找对应的watcher实例进行更新。
为什么组件化,可以复用,方便维护,局部更新。
dep收集watcher,
当new watcher初始化的时候(在mount的时候调用的图一和图二)。
图二中的new Watcher执行的时候会执行Watcher类中的this.get(图四)
(图四调用get的时候给Dep.target进行赋值为当前wather实例)Dep.target存放的是new watcher的this,然后执行this.getter(就是render函数)初始化渲染的时候必然会调用值。
调用值的时候调用双向绑定的get方法(图三),从而调用dep.depend方法(图五),调用Watcher类的addDeps方法(别忘了上面Dep.target的值为watcher实例)。实现 保存watcher的this到dep的subs列表中,同时也把dep存到了watcher的deps当中(图四)。
图一
图二
图三
图四
图五
应该做一套流程图
初始化 调用值(执行
m
o
u
n
t
里面的
n
e
w
W
a
t
c
h
e
r
的时候会调
用
u
p
d
a
t
e
然后会调用
v
m
.
mount里面的new Watcher的时候会调用_update然后会调用vm.
mount里面的newWatcher的时候会调用update然后会调用vm.data.属性值的get方法)的时候
1、执行dep的depend方法,depend内部执行watcher的addDep(dep)方法
2、watcher的addDep方法会记录不重复的dep实例 并且,dep也会同时记录对应的watcher,因为watcher里面没有重复的dep,所以dep不会重复记录,dep也不会重复记录watcher。参见上面watcher类里面的addDep方法
更新视图
值变化调用dep.notify()然后遍历dep存储的watcher,调用每个watcher里面的更新方法,更新视图
执行watcher的更新方法,遍历存储的所有watcher 并且进行更新
watcher的update方法
观察者模式的应用,
watcher是观察者,属性是被观察者,属性变化了会通知 所有的观察者,进行视图的更新。
就好像订阅天气app一样,我订阅了天气app我就是观察者,天气app就是被观察者, 被观察者数据变了就会通知我,我去做一些改变。
在代码中,
watcher是观察者,dep就是被观察者,当dep的值变了,就会通知watcher,然后watcher进行视图的更新。以上就是观察者模式在实例中的应用。
订阅发布者模式可以理解为观察者模式的变种,订阅发布模式,不再需要单独的增加程序,订阅者(观察者)只需要订阅相对应的消息即可,但是观察者则需要将自己添加到目标对象当中。这就是区别,发布内容的时候都一样。 vue2中使用的是观察者模式。
上面的更新有缺点,也就是同一个watcher如果有多个变量更新,那么这个watcher就会更新多次 。 接下来使用异步更新解决这个问题。
异步更新
以下是代码,请先大致阅读代码,结合代码看下面解析。
dep中
watcher中
watcher渲染逻辑,因为watcher是观察者,同时是负责渲染的。
首先,这里使用的是任务队列,也就是先调用了queueWatcher
每次值更新的时候都会调用notify,获取dep存储的watcher列表,然后调用所有watcher实例的更新方法。(watcher里面的get方法则是调用了不同的render函数,并且生成不同的虚拟dom并且进行替换。)
更新的时候queueWatcher 直接创建了一个队列,将不同的watcher进行存储。相同watcher会被过滤掉。
例如 改变了name的值两次,又改变了age的值,这时候notify会调用三次。(假如这三个属性内的subs存储的都是同一个watcher)
接下来的queueWatcher方法执行三次且传相同参数。queue队列中只有一个watcher实例。因为相同的watcher.id被过滤掉了。
接下来执行flushSchedulerQueue方法,这个方法从始至终都只会执行一次。因为防抖的限制只会在第一次向queue队列添加watcher时触发一次。因为是异步的,他会等queue队列添加完数据后再执行flushSchedulerQueue。
最后因为queue只有一个,所以只执行了一次刷新方法。
nextTick,对上面的异步更新进行了优化。
因为这里使用的是setTimeout设计的是宏任务。如果在获取模板的时候,代码使用了微任务,这时候会获取到旧的模板。例如
vm.name='tom'
Promise.resolve().then(()=>{
获取dom //只会获取到旧的dom模板。
})
发生以上原因都是因为setTimeout会宏任务,会等微任务执行完才会被执行。也就是会获取到旧的模板,为了解决以上问题,就产生了,nextTick 任务队列。如果想异步操作就使用nextTick去获取。保证了任务的执行顺序。
nextTick代码如下
同样是一个异步任务队列,将原本只执行一次的刷新操作flushSchedulerQueue函数放入nextTick中,而不是再放入setTimeout里面了,这样就会导致,nextTick中的callbacks队列是有顺序的。因为都是同步执行的。所以只要改变值了,刷新操作就会放在callback 列表中的第一位,而异步获取值的操作使用nextTick去替代后,就会一次放在callbacks的后面。
例如:
改变属性值vm.name=“tom”
callbacks列表=[flushSchedulerQueue]
使用nextTick获取dom
callbacks列表=[flushSchedulerQueue,获取dom函数]
调用callbacks中的所有函数
这样就可以获取到正常的值了,无论是否异步。
(nextTick也并非单纯的setTimeout实现的而是使用的异步的优雅降级。如下方法替换了上面的setTimeout。)
数组的更新原理
数组push之后是不会被自动更新的,因为上面的代码中 push重写的时候,只是对新增的对象进行了双向绑定而已。
例如
let list=[]
list.push({name:'tom'})//只是对属性进行了双向绑定,push的时候,并不会触发set方法。所以push 的时候不会更新数组。
list[0].name='jack'
重新给name修改值的时候才会触发更新操作。
因为之前的defineReactive是给每个属性添加的双向绑定,例如
let a = {mesage:{name:‘tom’}}中,是给message属性添加的双向绑定,对于外部的对象并没有添加双向绑定,所以当a.b='123’的时候是不会触发set更新的,
解决办法就是给对象也添加Dep属性,收集watcher,因为在observe方法里面return new Observe(data)创建这个的前提是data是对象,所以在Observe类里面创建了dep = new Dep()属性,并且当外部属性被调用的时候也让内部的对象进行收集watcher。
例如
data:{
message:{name:'tom’},
list:[1,2,3]
}
当message被调用的时候会让{name:'tom'}对象对应的Observe实例中的dep实例收集watcher并且,将Observe实例赋值给对象的__ob__,
所以想更新的话,就可以使用对象.__ob__.dep.notify()进行模板更新。如下如,给对象增加了dep属性,并且给对象添加了__ob__属性,值赋为对应的Observer对象,然后访问对象里面对应的dep属性,遍历所有的watcher强制进行视图的更新。
**在这里新增属性的时候就需要手动的去更新模板了。$set的原理**
![在这里插入图片描述](https://img-blog.csdnimg.cn/5f3b47dccc2c411fb9ebfbfda2e1e215.png)
当list列表被调用的时候会让Observe实例对象的Dep实例属性收集watcher
所以当push 的时候可以利用上面说的数组的__ob__属性指向了数组对应的Observe实例,进而获取到dep实例属性,进而获取watcher,进行在push的时候 使用notify通知更新。
![在这里插入图片描述](https://img-blog.csdnimg.cn/35eff94a6af64646b491dff5b0c13869.png)
但是又因为在获取值的时候只会触发外部的属性,不会触发内部的列表,就会导致,内部的列表对象,依旧不能dep.depend进行watcher的收集,所以就需要一个递归,手动的将内部的其他列表元素对象进行收集了。
后面是对象在更新值得时候因为对每一个属性都增加了dep,并且改变值的时候会触发set就会更新模板了,
这里的列表 因为更新的时候不会触发set 就需要获取的时候创建dep实例收集watcher,然后在push等方法的时候去触发dep的notify方法去更新模板。
如上图,就是,给列表对象添加dep实例,并且get调用的时候去双向绑定,循环给每一个列表中的列表都添加上dep实例即可。