Vue八股文之分之切换和嵌套函数

875 阅读5分钟

打开了尘封已久的Vue,在上一篇中讲解了Vue3的响应式原理。接下来考虑一些缺陷问题。

在上一篇中,我们可以封装trigger和track去分别设置set和get方法。

function track(target, key) {
  if (!reactiveFns) return target[key];
  let targetDepend = busket.get(target);
  if (!targetDepend) {
    targetDepend = new Map();
    busket.set(target, targetDepend);
  }
  let depend = targetDepend.get(key);
  if (!depend) {
    depend = new Set();
    targetDepend.set(key, depend);
  }
  depend.add(reactiveFns);
}

function trigger(target, key) {
  let targetDepend = busket.get(target);
  if (!targetDepend) return;
  let depend = targetDepend.get(key);
  if (!depend) return;
  depend.forEach((fn) => {
    fn();
  });
}

整个proxy代理就非常简洁:

const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    track(target, key); //存储当前副作用函数
    return target[key];
  },
  set: function (target, key, newVal, receiver) {
    target[key] = newVal;
    trigger(target, key);取出副作用函数执行
  },
});

基本的响应式完成,当watchFn副作用函数执行,收集响应式依赖的时候会自动收集到bukket中。这一点第一篇也有阐述过。

那如果是这一种情况就另外考虑了:

let obj = {
  name: "mjc",
  age: 18,
  ok: true,
  text: "text",
};

watchFn(function () {
  document.innerHTML = objProxy.ok ? objProxy.text : "not";
    console.log("副作用执行---------");
});

watchFn的执行会收集objProxy.ok副作用函数,同时也会收集objProxy.text的桶中。那么如果我对objProxy.ok设置为false,那么接下来的副作用执行的时候objProxy.text无法被收集依赖。

也就是,一旦objProxy.ok为false,无论副作用如何执行, document.innerHTML的值永远不变。那既然这样,就不叫副作用了,根本没有对任何做出影响。可是,此时我们根本没有取消这个busket的依赖。

如果设置objProxy.text,依然会触发副作用,这是不应该的。

图2

很明显objProxy.text无法造成副作用了,但是set的时候仍然触发副作用,这是多此一举了。

对watcnFn再次改造:

function watchFn(fn) {
  const effect = () => { //新增
    reactiveFns = effect;//新增
    fn && fn();
  };
  effect.dep = [];//新增 用来存储当前副作用的依赖集合
  effect();//新增
}

effect虽然是一个函数,本质其实也是一个对象,所以可以设置dep属性。

effect.deps=[]用来收集包含当副作用函数的所有依赖的集合

什么意思?待会就知道了这样的意义。

对watcnFn再次改造:

function track(target, key) {
  ...
  if (!depend) {
    depend = new Set();
    targetDepend.set(key, depend);
  }
    depend.add(reactiveFns);
    reactiveFns.deps.push(depend); //新增
}

effect.deps存储了当前的Map中的某个属性的set对象

接下来我们写清理函数,再每次重新执行Fn的时候,先取消所有副作用和响应式对象属性的联系,再通过Fn()重新创建联系,避免分支切换导致的影响。

cleanup函数

function clearup(effectFn) {
  //遍历effectFn.deps的数组
  for (let i = 0; i < effectFn.length; i++) {
    const deps = effectFn.deps[i];
    deps.delete(effectFn);
    effectFn.deps.length = 0;
  }
}

我们只需要每次副作用执行之前,先执行这个cleanup再执行Fn,确保每次断开联系再重新建立联系。

function watchFn(fn) {
  const effectFn = () => {
    clearup(effectFn); //新增
    reactiveFns = effectFn; 
    fn();
  };
  effectFn.dep = []; 
  effectFn();
}

行,发现一直在循环..,终究是一个forEach遍历的问题。

在forEach遍历Set中,如果一个值已经被访问过了,但是该值被删除后重新加入到集合中,那么本次遍历没有结束,仍然继续被重新访问这个值。

我们得不让原来的set对象来遍历,构建一个新的set来遍历

function trigger(target, key) {
  let targetDepend = busket.get(target);
  if (!targetDepend) return;
  let depend = targetDepend.get(key);
  if (!depend) return;
  const newDepend = new Set(depend)
  newDepend.forEach((fn) => {
    fn();
  });
}

这样depend的删除和添加不影响newDepend的遍历结束。

至此,分支切换的副作用遗留函数问题就解决了。

我们都知道Vue是组件的嵌套来渲染的,在根组件下,继续嵌套子组件和孙子组件。如:

<App.vue
<template>
<MjcFather>
<MjcSon>
...
</MjcSon>
</MjcFather>

</template>

App.vue下渲染了子组件MjcFather,MjcFather又渲染了子组件MjcSon。那么在vue2中,是用h函数来嵌套渲染的。在vue3中,组件的渲染成了effect函数执行。

比如MjcFather,是由如下:

const MjcFather = {
	render(){
		return /*... */
	}
}

当渲染的时候用effect来渲染

effect(()=>{
	MjcFather.redner()
})

那再嵌套呢?

//MjcSon
const MjcSon = {
 	render(){
 		/* ... */
 	}
}
//MjcFather
const MjcFather = {
 	render(){
 		return (
 		<MjcSon />
 		)
 		
 	}
}

用effect来表示它的结构:

effect(()=>{
	MjcFather.render()
	effect(()=>{
		MjcSon.render()
	})
})

这就很神奇是吧,组件的嵌套居然是effec函数的嵌套渲染,vue3的响应式系统之所以比vue2能在效率上提升,这里也是有独特新奇之处。effect函数不单是响应式数据的收集,也是渲染的一种方式。

但是我们所写的effect函数还不行做到嵌套,问题在于我们的effect函数用全局变量reactiveFns去注册当前激活的副作用,如果发生嵌套,会是这样的效果。

举个例子:

let temp1,temp2;
let reactiveFns= null;
effect(function effect1(){
	console.log('effect1')
	effect(function effect2(){
		console.log('effect2')
		temp2 = obj.bar  //假设obj为响应式数据
		//读取依赖触发收集
	})
	temp1 = obj.foo//再次读取
})

问题在于effect2入栈后,当前的reactiveFns存储值由effect1变为effect2,obj.bar的桶收集了effect2,接下来的obj.foo的桶收集却依然收集了effect2而不是effec1!这就是嵌套导致的问题。

那么这种方式的处理方法用栈的先入后出的方式去实现。 是不是用栈去解决这种问题很少见?我们在原来的effect副作用设计上进行改造

let reactiveFns
const effectStack = [] //声明一个栈用数组
function watchFn(fn) {
  const effectFn = () => {
    clearup(effectFn);
    reactiveFns = effectFn; 
    effectStack.push(effectFn) //新增  副作用执行前,推入栈
    fn();
    effectStack.pop(effectFn) //新增   副作用执行后,弹出栈
    reactiveFns = effectStack[effectStack.length -1] //新增 让当前存储副作用函数的重新从栈内退一位
  };
  effectFn.dep = []; 
  effectFn();
}

在JavaScript中使用栈,我也是第一次学会。本文思路和设计来源于《Vue设计与实现》的作者霍春阳,非本人原创思路,个人学习思想实操学习,如有侵权,可联系我删除本文。

总结

1.effect嵌套与分支切换的理解和思想。

2.effect的组件嵌套渲染以及Js中栈的概念使用

参考资料

[1] 《Vue设计与实现》作者霍春阳

[2] 博客 blog
mjcelaine.top