react架构 和 vue架构的对比(笔记)

189 阅读8分钟

目录

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当前层中不存在,若不不在则说明这些节点已经没有用到过,需要打上删除标签