目录
react 架构分三层
- 1:schedule 调度层
创建一个全局的任务队列,浏览器空闲时会调用任务队列中的任务执行 初始化时根据root的virtualDom创建rootFibler
- 2:fibler Reconcilliation层
- 1: 初始化时通过schedule调度出rootFibler,同步的创建fibler树。
- 2: 将每一个的fiber节点挂载在对应的staticNode上。(staticNode:可能是dom节点,可能是组件实例,可能是函数式组件的函数本身)
- 3: 创建完成后将rootFibler挂载带root dom节点上。方便后续获取这个fibler树。
- 4: 非初始化时,某个组件的内部状态发生改变。通过组件实例this到staticNode下获取对应的fibler节点
- 5: 将当前改变的状态与取出来fibler节点的状态合并
- 6: 通过当前的fibler节点的parent属性,递归向上寻找,直到找到rootFilber。
- 7: 将6中找到的rootFilber作为workInProcess fiberTree的跟节点
- 8: 将workInProcess fiberTree的rootFibler节点push到全局的任务队列中,待浏览器空闲时取出这个rootFibler进行fibler Reconcilliation。
- 9: 进行递归生成fibler时,会将第5步中重新合并的数据传入到函数组件中,返回新的virtualDom,依据新的virtualDom创建新的fibler节点。
- 10:将新的fibler节点与old filberTree进行中对应的fibler节点进行比对。将比对后的workInProcess fiber打上对应的effectTag标签(update, delete...等标签)
- 11:使用递归的先序遍历fiber树的回推阶段,从叶子节点开始收集所有fibler中的effect数组到root节点的effect数组中。
- effect数中保存的是所有子fibler节点对象,为什么要收集到root fibler的effect中呢?
- 典型的以空间换时间,之后根据每个fibler中的effectTag操作对应fibler的dom时就不用重新遍历fibler tree了。直接采用一个循环就能遍历和处理所有fibler对应的dom操作
- 12: 出发commit render
- 3:commit render层
- 11:最后采用一个循环的方式遍历所有的fiber节点,根据fibler节点的effectTag标记对dom进行操作。这一层的操作时同步的且中断优先级时最好的,不会被其他的中断任务打断。
vue架构
实例化vue内部的操作
- 获取options挂载带this.options
- 将optins中的data挂载到当前this上,便于后续vue组件内部通过this.的方式就能获取到数据
- 进行数据的劫持和响应式,调用observer进行数据劫持,创建观察者模式的dep对象
- 编译模板,调用compile编译模板的插值表达式,都编译为指令和文本节点。为每个插值表达式创建观察者模式的watch对象。将watch订阅到对应数据的dep对象中,实现响应式。
- 1:observer实现defineReactive,进行数据劫持,
观察者模式:创建观察者模式中的Dep对象
- 每个数据实例化Dep对象,在get中做依赖搜集,在set中调用dep.notify出发订阅的依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 编译模板时会将所有的插值表达式对应的dom操作,实例化为watch对象,
// 将watch对象挂载到Dep构造函数下
// 在wacth对象被实例化时会调用this[key](key)为插值表达式的字符串。触发当前这里的get函数调用
// 执行后判断Dep.target存在,会将当前watch作为当前dep对象中订阅
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 当触发当前数据的set时会触发dep.notify,通知所有订阅当前数据的wacth执行cb函数,进行批量更新
dep.notify()
}
})
- 2: 调用compile编译模板
观察者模式:创建观察者模式的Watch对象
- 初始化编译模板的插值时,编译指令和文本插值。
- 依据指令和文本插值使用的数据作为key,为每一个key对应的数据实例化Watch对象,
- 实例化对象时获取data对应key的值,触发observer绑定的get函数,将当前Watch实例this绑定在Dep下。
- 触发get时,在get中执行Dep.target && dep.addSubs(dep.target)。
- 将wacth的实例订阅到对应数据dep的subs数组中
- data下每一个数据触发,都会调用对应的dep.notify,调用dep下所有watch中的cb函数。达到实时更新的效果。
架构上的区别时最大的。react和vue在virtualDom和diff的在底层的原理上都类似,只是实现的方式不一样而已。
- vue中virtualDom的实现和diff算法
- vue中的virtualDom采用的Snabbdom的h函数来实现virtualDom
- 初始化时采用Snabbdom的patch得到let oldNode = patch(rootElement, vnode)得到oldVnode
patch函数的功能 需要传入两个参数,
- 第一个参数可以是dom节点,也可以是vnode
- 第一个参数是vnode。 patch会根据传入的参数惊喜diff比对,更新diff比对后的dom
- 当有组件有数据更新时,调用h函数生成新的virtualDom也就是newVnode。
- 在次调用oldNode = patch(newVnode, oldNode)。对比新旧vnode并返回一份oldVnode 以上生生成vnode的过程及vnode比对和dom更新的过程
- 那Snabbdom种时如何进行diff的呢?
- vue 采用的diff算法是从两端开始向中间比对。
- diff比对关键的四个数据oldStartIndex,oldEndIndex,newStartIndex,newEndIndex。
先进行oldStartIndex和oldEndIndex若为sameNode则,调用patchNode更新dom节点的属性或子节点。
oldStartIndex ++ ,oldEndIndex ++
// 判断时sameNode后会调用patchVnode更加节点内的文本等。
// 在vue中vnode的text和children是互斥的,vnode存在chlidren,则不存在text文本节点旧不可能存在。
// 反之text文本节点存在,则children旧不可能存在。
// 根据dom节点的特性也可以理解上面的互斥行为
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
// 新旧节点完全相等,则不需要更新,直接返回
if (oldVnode === vnode) return
// 若新旧节点只是文本不同则更新文本
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
// 若新旧节点多有cildren节点,则递归调用updateChildren进行比较
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
// 若只有新节点有children则直接创建这些children节点
} else if (ch){
createEle(vnode) //create el's children dom
} else if (oldCh){
// 只有老节点存在children节点,则删除这些children节点。
api.removeChildren(el)
}
}
}
// 判断是否同一个节点
function sameVnode (a, b) {
return (
a.key === b.key && // key相同
a.tag === b.tag && // 标签名相同
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
// 从oldStartVnode和newStartVnode开始比较,如果是相同节点,则调用patchVnode,更新节点
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 如果oldStartVnode和newStartVnode对应的不是同一个节点,则从oldEndVnode, newEndVnode开始比较。
// 如果oldEndVnode和newEndVnode对应的节点时sameNode则调用patchVnode更新节点
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode, newEndVnode对应的节点也不相同,则比较oldStartVnode, newEndVnode节点
// 如果相同则调用patchVnode更新节点
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldStartVnode和newEndVnode对应的节点也不相同,则比较oldEndVnode和newStartVnode对应的节点
// 相同的话则更新节点
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 上面的条件都不匹配,则从两边到中间的匹配结束,先将所有的带key的节点索引保存到map中
if (oldKeyToIdx === undefined) {
// 有key生成index表
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
idxInOld = oldKeyToIdx[newStartVnode.key]
// 如果当前新节点的key在老的key索引表中不存在则新增这个新的节点
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
} else {
// 如果新增的key在老的索引表中存在
// 则移动oldChildren中这个key所对应的节点到oldStartVnode之前
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
} else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
// 上面循环结束后,若oldStartIdx > oldEndIdx。则代表新的节点还有每匹配的。需要调用addVnodes添加这些新增的节点
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
// 否则的话则说明老的节点经过匹配后还有剩余,则需要调用removeVnodes删除这些节点
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
- react的virtualDom由自己的React.reactElement(options)生成,options由babel编译jsx 得到。 react如何进行diff?
- react采用的从做往右进行diff,diff比对的逻辑会比vue简单些
- 取出当前层的所有fibler节点,取出oldFiblerTree当前层的所有fibler节点。
- 若不存在key
- 则遍历workInProcess fiblerTree当前层的fibler节点,依据索引取出oldFiblerTree当前层中的fibler节点
- 将workInProcess中的fibler节点与oldFiblerTree取出fibler节点做比对。
- 如果新旧两个vnode的type相同,这调用updateProps更新prop
- 如果不相同则移除给旧fibler节点的effectTag打上删除标签
- 循环对比结束后,如果oldFiblerTree还有剩余节点没有比对,则说明这些节点已经在新的workInProcessFiblerTree中不存在了,则打上删除标签
- 如果workInProcess fiblerTree当前层还有fibler节点没比对,则添加将这些节点打上添加的标签
- 若存在key(则与vue中存在key的对比有点类似,也需要先创建个key映射表,不过react中时key与整个Vnode的索引,而vue中key与index的索引)
- 先遍历oldFiblerTree当前层的所有fibler,将key与vnode映射到一个map中
- 遍历遍历workInProcess fiblerTree当前层的fibler节点,遍历的时候取出newVnode的key到map中寻找,若找到则复用之前的节点
- 若是在map中每找到当前遍历节点的key,则添加这个fibler节点,effectTag打上添加的标签
- 遍历循环结束后,在遍历oldFiblerTree查看那些带有key的节点,在workInProcess fiblerTree当前层中不存在,若不不在则说明这些节点已经没有用到过,需要打上删除标签