响应式原理
数据劫持+发布订阅模式,通过以下初始化和更新的过程来实现双向绑定,也就是响应式原理
初始化:
- 1.Observer 对数据进行响应式绑定
- 2.Compiler 编译解析模块指令,初始化渲染页面,并将每个指令的节点绑上更新函数,实例化监听监听数据的订阅者 Watcher。
- 3.数据 getter 时,执行对应数据的 dep 收集所有 watcher 依赖
更新:
- 1.更新时触发 dep.notify(),派发通知所有订阅者 watcher
- 2.订阅者 watcher 执行 update() 回调函数
- 3.调用对应 Compiler 编译解析模块,重新更新视图
Observer:数据观察者,把数据设置为响应式数据.(发布者)
数组: 遍历元素绑定方法(改造后的七个数组方法,这个七个方法会改变数组本身).有原型把方法给到改写后的原型.没原型直接给到元素身上.因为数组的length在使用里面用defineProperty会报错,所以不用legnth.
对象: 遍历对象(深度遍历),给每一个属性值使用defineReactive函数,里面用defineProperty对数据进行劫持.在getter中用 dep.depend()收集依赖,在setter中用dep.notify()通知更新
数组的方法改造
- 新建了一个对象,此对象的原型对象为Array的原型对象(避免污染array原来的原型对象).
- 通过Object.defineProperty给新建的对象的七个方法返回一个新函数,函数内部就是改变数组原型对象的的this指向
- 把新建的对象挂在数组的原型对象上
Dep:依赖管理(调度中心)
作用:收集Watcher和通知观察者目标更新
每个属性都有自己的dep,用于存放所有订阅了该属性的Watcher
Dep 中有实例属性subs,它是一个数组,用于存放依赖.Dep的原型上有add和remove两个操作subs的方法用于增加依赖和移除依赖.
还有两个函数:给watcher增加添加当前Dep(实现双向绑定)的depend方法;数据变化通知watcher执行update的notify方法
watcher:订阅者
一个组件一个watcher
watcher中有get, addDep(收集watcher实现双向绑定),cleanDeps,update,run,evaluate,depend,teardown方法.
update方法:分三种情况: 懒执行,同步执行(调用run方法),其他(使用queueWatcher(this),让其进入异步更新队列)
run方法使用get方法获取新值来更新旧值,并执行cb返回新旧值
watcher的实例分为三种:computed-watcher, normal-watcher,render-watcher
computed-watcher:在computed中定义的属于这种
normal-watcher:我们在watch中定义的都属于这种
render-watcher:每个组件都有一个.当 data/computed 中的属性改变的时候,会调用该 render-watcher 来更新组件的视图
三者的执行顺序:computed-watcher -> normal-watcher -> render-watcher.
这样就能尽可能的保证,在更新组件视图的时候,computed 属性已经是最新值.
Dep和Watcher是一种观察者模式.Watcher会订阅属性的变化,从而更新视图。Dep用来收集Watcher当Observer监听到更新时,通过dep.notify() 统一派发给 Watcher,实现了双向绑定。
vnode
定义
一个js对象,里面有tag,data,children等属性.
tag:存标签名
data:里面存了 style,class,以及标签的其他属性
children:存放子节点
创建
使用render函数去创建,分为了普通dom元素,还有组件
普通dom
使用VNode 构造函数
组件
使用createComponent函数,这个函数里面也是用的VNode构造函数
diff 算法
核心:递归+双指针
在进行递归加双指针前,会先排除一些情况
1.diif算法只会进行同层级比较.
2.标签名不同,直接替换
3.key值相同且标签名相同(sameVnode方法),认为是同节点,不再深度比较.
patch函数:
if(!isVnode(oldVnode)){// 判断是不是vnode
// 不是就创建一个空的vnode且关联一个dom元素
}
if(sameVnode(oldVnode,vnode)){ // sameVnode方法:判断是不是key值相同且选择器相同
// 同一节点,则使用patchVnode函数
}else{
// 不同节点,直接创建新的dom且移出老的dom
}
patchVnode函数:
// 把新的vnode的元素设置为老的vnode的元素
// 分别获取新老vnode的children
// vnode===oldVnode,直接return
if(isUndef(vnode.text)){ // 新的vnode没有text的情况
if(isDef(oldCh)&&isDef(ch)){// 新老vnode均有children的情况
if(oldCh!==ch){ // 使用updateChildren函数 }
}else if(isDef(ch)){ // 新vnode有children,老vnode没children
if(isDef(oldVnode.text)){ // 这种情况就是老的有text,直接清空text}
// 增加children
}else if(isDef(oldCh)){ // 老vonde有children,新vnode无children
// 移除children
}else if(isDef(oldVnode.text)){
// 清空text
}
}else if(oldVnode.text!==vnode.text){ // 新老vnode text不同
if(isDef(oldCh)){
// 老vnode有children的情况则移出老vnode的children
}
// 设置新vnode的text
}
updateChildren函数(diff):
此时新老children肯定是有不同之处的.对所有新老children都分别定义开始和结束的索引.进行循环比较,每次比较新老索引是往中间移动的.当新索引大于老索引时结束比较.
循环中有四种对比vnode的方式
老开始和新开始对比 , 老结束和新结束对比,老开始和新结束对比, 老结束和新开始对比.对比都是通过sameVnode方法判断是否是同一节点.
只要符合其中一种对比的情况,进去后里面都是对新老vnode使用patchVnode函数(递归).然后就是还有对指针(索引)的移动情况.
符合 老开始和新开始对比 新老开始索引都增大1
符合 老结束和新结束对比 新老结束索引都减小1
符合 老开始和新结束对比 老开始增大1,新结束减小1,把老开始节点放到老的结束后面
符合 老结束和新开始对比 老结束减小1,新开始增大1,把老的结束放到老的开始前面
上面四种比对都没符合,则会进行一种特殊处理:
拿到新开始的这个节点的key,在老的children里面去找有没有某个节点的key与其相等.
若在老children中找到:判断元素的标签是否相等.若相等:使用patchVode方法;若不相等:创建新元素
若没在老的children里找到:创建新元素
nextTick
定义
nextTick是vue中的批量异步更新策略。监听到组件的变化时,不会立即更新,会开启一个队列,同一watcher只入队一次。在事件循环结束后时,刷新队列执行执行dom更新操作
通过微任务高优先级确保在这个事件循环中就能完成数据更新. 如果不支持微任务就降为宏任务,可能需要多次渲染.
实现
把传入的 cb 回调函数用 try-catch 包裹后放在一个匿名函数中推入callbacks数组中,
这么做是因为防止单个 cb 如果执行错误不至于让整个JS线程挂掉,
每个 cb 都包裹是防止这些回调函数如果执行错误不会相互影响,比如前一个抛错了后一个仍然可以执行
把传入的回调函数用try-catch包裹后推入回调函数数组,
然后执行timerFunc函数去判断是把回调函数放到微任务队列还是宏任务队列.
然后执行flushCallbacks函数(就是循环执行回调函数数组)
computed
缓存问题:
计算得到的值保存到一个变量(watcher.value)中,使用缓存的时候直接返回这个变量,更新时重新赋值这个变量.
通过watcher.dirty去控制是否需要重新计算.
当computed创建自己的watcher时,会设置dirty为true.
当依赖的数据发生变化时,会使dirty为true,,以便于其他地方重新渲染,从而重新读取 computed 时,此时 computed 重新计算
计算完以后把dirty 改为false
更新问题:
用一个对象存计算属性的watcher以及页面的watcher.
computed-watcher先于页面的watcher执行.
首先调用 computed - watcher 的 update 方法,将 dirty 更改为 true ,表示缓存已无效,注意:此时不会重新计算
再调用页面 - watcher,通知页面更新。页面更新时,会重新读取 computed 的值。此时,由于 dirty = true, 执行 computed - evaluate 方法,重新计算computed
watch
watch在初始化时,会读取监听的数据,这样getter就会收集依赖(收集此watcer),把设置的方法(handler)放入wather的upate函数中. 数据更新时,dep.notify通知watcher调用uapte更新就执行了handler
immediate
会在初始化watch,读取值以后立即调用一遍handler.
deep
会递归这个对象,把所有的属性都做一遍依赖收集.所以无论套得多深,只要改了就会通知对应的watcher去更新