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--,然后不做操作,继续遍历下一个.
- 上边,虽然可以复用dom,但会移动所有dom(除开为0的为insert外),有时候其实是不必要的
lifeCycle
- beformount mounted before unmount unmounted,函数都是会在setUP被执行的时候存到当前正在render的instance上,存成数组或者链表都可以。在render前后执行。 setup可以返回state,也可以返会函数,返回函数将作为render函数。