在vue3中,因为reactive创建的响应式对象是通过Proxy来实现的,所以传入数据不能为基础类型,所以 ref 对象是对reactive不支持的数据的一个补充。
在 ref 和 reactive 中还有一个重要的工作就是收集、触发依赖,那么依赖是什么呢?怎么收集触发?一起来看一下吧:
我们先来看一下 ref 的源码实现:
export function ref(value?: unknown) {
return createRef(value, false)
}
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
const toReactive = (value) => isObject(value) ? reactive(value) : value;
function createRef(rawValue: unknown, shallow: boolean) {
// 如果是ref则直接返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
// 存放 raw 原始值
private _rawValue: T
// 存放依赖
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
// toRaw 拿到value的原始值
this._rawValue = __v_isShallow ? value : toRaw(value)
// 如果不是shallowRef,使用 reactive 转成响应式对象
this._value = __v_isShallow ? value : toReactive(value)
}
// getter拦截器
get value() {
// 收集依赖
trackRefValue(this)
return this._value
}
// setter拦截器
set value(newVal) {
// 如果是需要深度响应的则获取 入参的raw
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 新值与旧值是否改变
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
// 更新value 如果是深入创建并且是对象的话 还需要转化为reactive代理
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 触发依赖
triggerRefValue(this, newVal)
}
}
}
RefImpl 采用ES6类的写法,包含 get 、 set ,其实大家可以用 webpack 等打包工具打包成 ES5 的代码, 发现其实就是 Object.defineProperty 。
可以看到, shallowRef 和 ref 都调用了 createRef ,只是传入的参数不同。当使用 shallowRef 时,不会调用 toReactive 去将对象转换为响应式,由此可见,shallowRef对象只支持对value值的响应式,ref对象支持对value深度响应式,ref.value.a.b.c中的修改都能被拦截,举个:chestnut::
<template>
<p>{{ refData.a }}</p>
<p>{{ shallowRefData.a }}</p>
<button @click="handleChange">change</button>
</template>
let refData = ref({
a: 'ref'
})
let shallowRefData = shallowRef({
a: 'shallowRef'
})
const handleChange = () => {
refData.value.a = "ref1"
shallowRefData.value.a = "shallowRef1"
}
当我们点击按钮修改数据后,界面上的 refData.a 的值会变为 ref1 ,而 shallowRefData.a 应该会不发生变化,但其实在这个例子里, shallowRefData.a 在视图上也会发生变化的:dog:,因为修改 refData.a 时候,触发了setter函数,内会去调用 triggerRefValue(this, newVal) 从而触发了 视图更新 ,所以shallow的最新数据也会被更新到了视图上 (把 refData.value.a = "ref1" 去掉它就不会变了)。
在 ref 里最关键的还是 trackRefValue 和 triggerRefValue ,负责收集触发依赖。
function trackRefValue(ref) {
// 判断是否需要收集依赖
// shouldTrack 全局变量,代表当前是否需要 track 收集依赖
// activeEffect 全局变量,代表当前的副作用对象 ReactiveEffect
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
{
// 如果没有 dep 属性,则初始化 dep,dep 是一个 Set<ReactiveEffect>,存储副作用函数
// trackEffects 收集依赖
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: "get",
key: 'value'
});
}
}
}
为什么要判断 shouldTrack 和 activeEffect ,因为在Vue3中有些时候不需要收集依赖:
ref.dep 用于储存 依赖 (副作用对象),ref 被修改时就会触发,那么依赖是什么呢?依赖就是 ReactiveEffect :
为什么要收集依赖(副作用对象),因为在Vue3中,一个响应式变量的变化,往往会触发一些副作用,比如视图更新、计算属性变化等等,需要在响应式变量变化时去触发其它一些副作用函数。
在我看来 ReactiveEffect 其实就和 Vue2 中的 Watcher 的作用差不多,我之前写的 《Vue源码学习-响应式原理》 里做过说明:
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
// 传入一个副作用函数
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
// 存储 Dep 对象,如上面的 ref.dep
// 用于在触发依赖后, ref.dep.delete(effect),双向删除依赖)
this.deps = [];
this.parent = undefined;
recordEffectScope(this, scope);
}
run() {
// 如果当前effect已经被stop
if (!this.active) {
return this.fn();
}
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
if (parent === this) {
return;
}
parent = parent.parent;
}
try {
// 保存上一个 activeEffect
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
// trackOpBit: 根据深度生成 trackOpBit
trackOpBit = 1 << ++effectTrackDepth;
// 如果不超过最大嵌套深度,使用优化方案
if (effectTrackDepth <= maxMarkerBits) {
// 标记所有的 dep 为 was
initDepMarkers(this);
}
// 否则使用降级方案
else {
cleanupEffect(this);
}
// 执行过程中重新收集依赖标记新的 dep 为 new
return this.fn();
}
finally {
if (effectTrackDepth <= maxMarkerBits) {
// 优化方案:删除失效的依赖
finalizeDepMarkers(this);
}
// 嵌套深度自 + 重置操作的位数
trackOpBit = 1 << --effectTrackDepth;
// 恢复上一个 activeEffect
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = undefined;
if (this.deferStop) {
this.stop();
}
}
}
}
ReactiveEffect 是副作用对象,它就是被收集依赖的实际对象,一个响应式变量可以有多个依赖,其中最主要的就是 run 方法,里面有两套方案,当 effect 嵌套次数不超过最大嵌套次数的时候,使用优化方案,否则使用降级方案。
function cleanupEffect(effect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
// 从 ref.dep 中删除 ReactiveEffect
deps[i].delete(effect);
}
deps.length = 0;
}
}
这个很简单,删除全部依赖,然后重新收集。在各个 dep 中,删除该 ReactiveEffect 对象,然后执行 this.fn() (副作用函数) 时,当获取响应式变量触发 getter 时,又会重新收集依赖。之所以要先删除然后重新收集,是因为随着响应式变量的变化,收集到的依赖前后可能不一样。
const toggle = ref(false)
const visible = ref('show')
effect(() = {
if (toggle.value) {
console.log(visible.value)
} else {
console.log('xxxxxxxxxxx')
}
})
toggle.value = true
全部删除,再重新收集,明显太消耗性能了,很多依赖其实是不需要被删除的,所以优化方案的做法是:
// 响应式变量上都有一个 dep 用来保存依赖
const createDep = (effects) => {
const dep = new Set(effects);
dep.w = 0;
dep.n = 0;
return dep;
};
其实就是一个筛选的过程,我们现在来第一步,如何加上 was 标记:
// 在 ReactiveEffect 的 run 方法里
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
}
const initDepMarkers = ({ deps }) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit;
}
}
};
这里使用了位运算,快捷高效。trackOpBit是什么呢?代表当前嵌套深度 (effect可以嵌套) ,在Vue3中有一个全局变量 effectTrackDepth
// 全局变量 嵌套深度
let effectTrackDepth = 0;
// 在 ReactiveEffect 的 run 方法里
// 每次执行 effect 副作用函数前,全局变量嵌套深度会自增1
trackOpBit = 1 << ++effectTrackDepth
// 执行完副作用函数后会自减
trackOpBit = 1 << --effectTrackDepth;
当深度为 1 时,trackOpBit为 2(二进制:00000010),这样执行 deps[i].w |= trackOpBit 时,操作的是第二位,所以第一位是用不到的。
为什么Vue3中嵌套深度最大是 30 ?
1 << 30
// 0100 0000 0000 0000 0000 0000 0000 0000
// 1073741824
1 << 31
// 1000 0000 0000 0000 0000 0000 0000 0000
// -2147483648 溢出
因为js中位运算是以32位带符号的整数进行运算的,最左边一位是符号位,所以可用的正数最多只能到30位。
可以看到,在执行副作用函数之前,使用 deps[i].w |= trackOpBit ,对依赖在不同深度是否被依赖( w )进行标记,然后执行 this.fn() ,重新收集依赖,上面说到收集依赖调用 trackRefValue 方法,该方法内会调用 trackEffects :
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false;
if (effectTrackDepth <= maxMarkerBits) {
// 查看是否记录过当前依赖
if (!newTracked(dep)) {
dep.n |= trackOpBit;
// 如果 w 在当前深度有值,说明effect之前已经收集过
// 不是新增依赖,不需要再次收集
shouldTrack = !wasTracked(dep);
}
}
else {
shouldTrack = !dep.has(activeEffect);
}
if (shouldTrack) {
// dep添加当前正在使用的effect
dep.add(activeEffect);
// effect的deps也记录当前dep 双向引用
activeEffect.deps.push(dep);
}
}
可以看到再重新收集依赖的时候,使用 dep.n |= trackOpBit 对依赖在不同深度是否被依赖( n )进行标记,这里还用到两个工具函数:
const wasTracked = (dep) => (dep.w & trackOpBit) > 0;
const newTracked = (dep) => (dep.n & trackOpBit) > 0;
使用 wasTracked 和 newTracked,判断 dep 是否在当前深度被标记。比如判断依赖在深度 1 时 (trackOpBit第二位是1) 是否被标记,采用按位与:
最后,如果已经超过最大深度,因为采用降级方案,是全部删除然后重新收集的,所以肯定是最新的,所以只需要把 trackOpBit 恢复,恢复上一个 activeEffect:
finally {
if (effectTrackDepth <= maxMarkerBits) {
// 优化方案:删除失效的依赖
finalizeDepMarkers(this);
}
trackOpBit = 1 << --effectTrackDepth;
// 恢复上一个 activeEffect
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = undefined;
if (this.deferStop) {
this.stop();
}
}
如果没超过最大深度,就像之前说的把失效的依赖删除掉,然后更新一下deps的顺序:
const finalizeDepMarkers = (effect) => {
const { deps } = effect;
if (deps.length) {
let ptr = 0;
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
// 把有 w 没有 n 的删除
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect);
}
else {
// 更新deps,因为有的可能会被删掉
// 所以要把前面空的补上,用 ptr 单独控制下标
deps[ptr++] = dep;
}
// 与非,恢复到进入时的状态
dep.w &= ~trackOpBit;
dep.n &= ~trackOpBit;
}
deps.length = ptr;
}
};
举个简单的:chestnut:,理解起来可能简单点,有两个组件,一个父组件,一个子组件,子组件接收父组件传递的 toggle 参数显示在界面上, toggle 还控制着 visible 的显示,点击按钮切换 toggle 的值:
// Parent
<script setup lang="ts">
const toggle = ref(true)
const visible = ref('show')
const handleChange = () => {
toggle.value = false
}
</script>
<template>
<div>
<p v-if="toggle">{{ visible }}</p>
<p v-else>xxxxxxxxxxx</p>
<button @click="handleChange">change</button>
<Child :toggle="toggle" />
</div>
</template>
// Child
<script setup lang="ts">
const props = defineProps({
toggle: {
type: Boolean,
},
});
</script>
<template>
<p>{{ toggle }}</p>
</template>
第一次渲染,因为toggle 默认为 true,我们可以收集到 toggle 、 visible 的依赖,
执行 run 方法中的 this.fn ,重新收集依赖,触发 trackEffects:
执行 run 方法中的 this.fn ,重新收集依赖,触发 trackEffects:
这样首次进入页面的收集依赖就结束了,然后我们点击按钮,把 toggle 改为 false:
Parent 组件: 执行 run 方法中的 initDepMarkers 方法,之前在 Parent 组件里收集到了两个变量的依赖,所以将他们 w 标记:
dep = {n: 0, w: 2}
dep = {n: 0, w: 2}
执行 run 方法中的 this.fn ,重新收集依赖,触发 trackEffects:
执行 run 方法中的 this.fn ,重新收集依赖,触发 trackEffects:
最后发现 visible 有 w 没有 n ,在 finalizeDepMarkers 中删除掉失效依赖。
在一开始讲到的 ref 源码里,可以看到在 setter 时会调用 triggerRefValue 触发依赖:
function triggerRefValue(ref, newVal) {
ref = toRaw(ref);
if (ref.dep) {
{
triggerEffects(ref.dep, {
target: ref,
type: "set",
key: 'value',
newValue: newVal
});
}
}
}
function triggerEffects(
dep: Dep | ReactiveEffect[]
) {
// 循环去取每个依赖的副作用对象 ReactiveEffect
for (const effect of isArray(dep) ? dep : [...dep]) {
// effect !== activeEffect 防止递归,造成死循环
if (effect !== activeEffect || effect.allowRecurse) {
// effect.scheduler可以先不管,ref 和 reactive 都没有
if (effect.scheduler) {
effect.scheduler()
} else {
// 执行 effect 的副作用函数
effect.run()
}
}
}
}
触发依赖最终的目的其实就是去执行 依赖 上 每个的副作用对象 的 副作用函数 ,这里的副作用函数可能是执行更新视图、watch数据监听、计算属性等。
我个人再看源码的时候还遇到了一个问题,不知道大家遇到没有(我看的代码版本算是比较新v3.2.37),一开始我也是上网看一些源码的解析文章,看到好多讲解 effect 这个函数的,先来看看这个方法的源码:
function effect(fn, options) {
if (fn.effect) {
fn = fn.effect.fn;
}
const _effect = new ReactiveEffect(fn);
if (options) {
extend(_effect, options);
if (options.scope)
recordEffectScope(_effect, options.scope);
}
if (!options || !options.lazy) {
_effect.run();
}
const runner = _effect.run.bind(_effect);
runner.effect = _effect;
// 返回一个包装后的函数,执行收集依赖
return runner;
}
这个函数看上去挺简单的,创建一个 ReactiveEffect 副作用对象,将用户传入的参数附加到对象上,然后调用 run 方法收集依赖,如果有 lazy 配置不会自动去收集依赖,用户主动执行 effect 包装后的函数,也能够正确的收集依赖。
但我找了一圈,发现源码里一个地方都没调用,于是我就在想是不是以前用到过,现在去掉了,去commit记录里找了一圈,还真找到了:
这次更新把 ReactiveEffect 改为用类来实现,避免不必要时也创建 effect runner ,节省了17%的内存等。
原来的 effect 方法包括了现在的 ReactiveEffect ,在视图更新渲染、watch等地方都直接引用了这个方法,但更新后都是直接 new ReactiveEffect ,然后去触发 run 方法,不走 effect 了,可以说现在的 ReactiveEffect 类就是之前的 effect 方法 。
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { const effect = createReactiveEffect(fn, options) return effect } let uid = 0 function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { if (!effect.active) { return fn() } if (!effectStack.includes(effect)) { cleanup(effect) try { enableTracking() effectStack.push(effect) activeEffect = effect return fn()
} finally {
effectStack.pop() resetTracking() const n = effectStack.length activeEffect = n > 0 ? effectStack[n - 1] : undefined } } } as ReactiveEffect effect.id = uid++ effect.allowRecurse = !!options.allowRecurse effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect }
我是周小羊,一个前端萌新,写文章是为了记录自己日常工作遇到的问题和学习的内容,提升自己,如果您觉得本文对你有用的话,麻烦点个赞鼓励一下哟~
来自:https://segmentfault.com/a/1190000042054691
在日常 Coding 中,码农们肯定少不了对数组的操作,其中很常用的一个操作就是对数组进行遍历,查看数组中的元素,然后一顿操作猛如虎。今天暂且简单地说说在 JavaScript 中 forEach。
克隆项目代码到本地(git应该都要会哈,现在源码几乎都会放github上,会git才方便,不会的可以自学一下哦,不会的也没关系,gitHub上也提供直接下载的链接);打开微信开发者工具;
随着这些模块逐渐完善, Nodejs 在服务端的使用场景也越来越丰富,如果你仅仅是因为JS 这个后缀而注意到它的话, 那么我希望你能暂停脚步,好好了解一下这门年轻的语言,相信它会给你带来惊喜
在 Vue 内部,有一段这样的代码:上面5个函数的作用是在Vue的原型上面挂载方法。initMixin 函数;可以看到在 initMixin 方法中,实现了一系列的初始化操作,包括生命周期流程以及响应式系统流程的启动
nextTick的使用:vue中dom的更像并不是实时的,当数据改变后,vue会把渲染watcher添加到异步队列,异步执行,同步代码执行完成后再统一修改dom,我们看下面的代码。
React更新的方式有三种:(1)ReactDOM.render() || hydrate(ReactDOMServer渲染)(2)setState(3)forceUpdate;接下来,我们就来看下ReactDOM.render()源码
在React中,为防止某个update因为优先级的原因一直被打断而未能执行。React会设置一个ExpirationTime,当时间到了ExpirationTime的时候,如果某个update还未执行的话,React将会强制执行该update,这就是ExpirationTime的作用。
算法对于前端工程师来说总有一层神秘色彩,这篇文章通过解读V8源码,带你探索 Array.prototype.sort 函数下的算法实现。来,先把你用过的和听说过的排序算法都列出来:
extend是jQuery中一个比较核心的代码,如果有查看jQuery的源码的话,就会发现jQuery在多处调用了extend方法。作用:对任意对象进行扩;’扩展某个实例对象
state也就是vuex里的值,也即是整个vuex的状态,而strict和state的设置有关,如果设置strict为true,那么不能直接修改state里的值,只能通过mutation来设置
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!