Vue3 响应式原理个人笔记

171 阅读5分钟

reactive(obj)把obj用proxy包一下,get里会记录做这样的一个数据结构, target -> Map(key -> effectSet), set里会去之前的存的map里map.get(target).get(key) 拿到effect,执行。

  • effect(fn)会在执行前把fn存起来,然后执行(执行的时候,fn里会取值,于是就会出发get,记录映射关系target key effectset, 此时的effect就是这个fn),完了再pop出来。

我们规定,取值一定发生在副作用函数执行的时候。

  • effect里的fn的内容会执行组件的render函数(render函数一定会取被reactive的对象的值。),然后拿到render的返回值,去做patch or update。

只要会取reactive的对象的值,那就会走get,那么就能拿到target key, 以及当前的effect, 就可以记录下来。

当该属性被修改的时候,set被调用,直接map.get(target).get(key) 拿到effect,执行。

    effect(function () {
      if (!instance.isMounted) {
        let subTree = (instance.subTree = instance.render());
        patch(null, subTree, container);
        instance.isMounted = true;
      } else {
        let prevTree = instance.subTree;
        let nextTree = instance.render();
        patch(prevTree, nextTree, container);
      }
    });
    
    传给effect里的函数被存起来,当再次被执行的时候,instance对象的引用还在,
    所以就能拿着新老vnode做diff,至于diff方案,头头依次,尾尾依次。
  • 为什么proxy比definePropert性能好,因为proxy不会递归的去设置get set,而是在被get的时候,检查get出来的是否是个对象或者数组,然后对这个进行proxy。

  • computed 原理,首先是new一个对象,里面有get value set value, 在构造函数里,把传来的get函数,放到effect里,传了lazy,不会立即执行。在被取值的时候(取值一定发生在副总用函数中),才对执行传入的get函数拿到值(里面一定会取普通属性的值)(造成的直接结果:普通属性的副作用函数,除了组件render的那个,还多了一个compute的get,但是计算属性在effect更新的时候,是调用effect的scheduler), 然后还会track当前this对象的value,相当于把用到该computed的组件的render副作用干到自己的value映射里.
    于是就形成了一条链路,普通是属性修改,组件渲染副作用执行,computed的effect的scheduler执行,导致 修改dirty,然后trigger(this, "set", "value"),导致组件渲染副作用执行.

v3 dom diff 核心

  • oldVnode,newVnode, 先比较自己type key一样不,若不一样,直接干掉重建

  • 若一样, 更新属性,然后比较孩子,这里只记录都有几个孩子且孩子都带key的情况:

    • 老c d e
    • 新d e c h
  • 一个map记录新的节点d e c h在父节点(即newVnode)中的索引.(+1的目的是为了区分开哪些是需要新增的, 索引如果使用0,就撞车了嘛)

map.set(d.key, 0 + 1)
map.set(e.key, 1 + 1)
map.set(c.key, 2 + 1)
map.set(h.key, 3 + 1)
  • 核心: 新节点的索引与老节点索引的对应关系

    • 先初始化一个长度等于新子节点个数的数组填满0, arr[0,0,0,0],然后遍历老c d e , 去map里取看是否能取到,取不到直接node.el.parentNode.remove(el), 取到了记录对应的老节点的索引,同时还要继续patch可复用的这俩节点,得到 arr[1+1,2+1,0+1,0],arr表示:新节点的索引与老节点索引的对应关系。eg: 0即d 1e 2c 3e,分别对应老节点的1,2,0索引,加一是为了把0区别开,害怕撞车了。因为arr元素为0就是添加的元素。
    • 这样一来,我们在遍历arr的时候就可以同时知道新节点 与 老节点的对应关系, 同时拿对应的dom元素引用,好做insert
     // 为了讲解方便,就没考虑abcdew  abdechw,前后有可复用节点的情况,其实都是一样的。
     // 多一个起点终点坐标变量而已。非核心不多说
     倒序遍历
     for (let i = arr.length - 1; i >= 0; i--) {
         newChildren[i]vNode(key 相同)
         oldChildren[arr[i] - 1] 老vNode(key 相同)
         
         //insertBefore()方法在参考节点之前插入一个拥有指定父节点的子节点。如果给定的子节点是对文档中现有节点的引用,`insertBefore()` 会将其从当前位置移动到新位置
         insertBefore(newChildren[i].el, parentEl, newChildren[i+1] || null)
     }
    
  • 继续优化的点

    • 上边,虽然可以复用dom,但会移动所有dom(除开为0的为insert外),有时候其实是不必要的
      • 老c d e
      • 新d e c h
      • 这种我们就希望插入h后, 只移动c,就好了,其他不变就好了。倒序精粹!
    • arr[2,3,1,0]的最长递增子序列
      • 2
      • 2 3(添加3,因为比上一个元素2大)
      • 2 3(丢弃1,因为比上一个元素3小)
      • 2 3(丢弃0,因为比上一个元素3小)
      • ps: 假如有个2.5,会干掉3,用2.5补上
      • 最后返回留下的元素的索引,此处:maxSub[0,1], 代表倒叙遍历arr[2,3,1,0]的时候,i如果不等于maxSub[j],就做插入移动,等于就j--,然后不做操作,继续遍历下一个.

lifeCycle

  • beformount mounted before unmount unmounted,函数都是会在setUP被执行的时候存到当前正在render的instance上,存成数组或者链表都可以。在render前后执行。 setup可以返回state,也可以返会函数,返回函数将作为render函数。