- vue3中的h函数是辅助创建虚拟DOM的工具函数,h函数的返回值就是一个对象。
- vue3中的render函数最终结果是虚拟DOM,而用h函数可以更简单的传参创建,当然也可以直接用对象描述出来。
-
vue3中的组件渲染只有根据render函数的返回值,也就是虚拟DOM,才能渲染页面。
-
渲染器:渲染函数,将虚拟DOM渲染成真实DOM
/**
* 渲染函数,将虚拟DOM渲染成真实DOM
* @param {*} vnode 虚拟DOM对象
* @param {*} container 一个真实DOM,渲染器会把虚拟DOM渲染到该挂载点下
*/
function renderer(vnode, container) {
const el = document.createElement(vnode.tag)
for (const key in vnode.props) {
if (/^on/.test(key)) {
el.addEventListener(key.substring(2).toLowerCase(), vnode.props[key])
}
}
if (typeof vnode.children === 'string') {
el.appendChild(document.createTextNode(vnode.children))
} else {
vnode.children.forEach(child => renderer(child, el))
}
container.appendChild(el)
}
- 组件本质上就是一组虚拟DOM的集合,他的返回值依旧是虚拟DOM。他是一个对象,有一个render方法,调用render方法的返回值就是虚拟DOM
const myComponent = {
render() {
return {
tag: "div",
props: {
onClick() {
console.log("324234234");
},
},
children: "555",
};
},
};
- 编译器的本质就是将写在template里面的声明式UI,编译成渲染函数如下。:
<template>
<div @click="handler">
我是div
</div>
</tamplate>
上述编译成渲染函数后就是:
<script>
export default {
methods:{
handler(){}
},
render(){
return h('div',{onClick:handler},'我是div')
}
}
</script>
-
所以一个组件需要展示的内容就是通过渲染函数产生的,渲染函数产生的虚拟DOM再由渲染器转化成真实DOM。
-
Vue3的响应式系统通过proxy来做。当读取一个代理对象时,触发get。设置一个代理对象时,触发set。Vue3中的响应式系统在渲染函数被调用时,就会完成每一个组件的每一个属性的依赖收集。Vue3设计了weakMap -> Map -> Set的数据结构,分别对应了Vue实例 -> 属性 -> 读取了这个属性对应的值的副作用函数(简称依赖),副作用函数就是render函数内的一些函数,因为render函数的返回值是虚拟DOM,虚拟DOM转化成真实DOM时读取了响应式数据。
-
当渲染函数调用时,所有的依赖就会被收集,并且以上述的数据结构存储。每一个依赖都是被包装过后的副作用函数,他存在deps属性,用于记录这个依赖都被收集到哪一些依赖集合中,在属性对应的依赖集合收集依赖时,依赖也会收集依赖集合。他们是相互记录的关系。
-
当属性对应的值发生变化,那么这些依赖就会被重新触发实现页面的更新。在触发更新前,会将依赖被收集到的所有依赖集合中,删除自己,达到依赖更新的目的,因为可能存在当前的依赖被2个key所对应,但是当一些值变化后,只有1个key对应该依赖,那就就需要依赖更新。
const bucket = new WeakMap();
const data = {
text: "hello world",
};
let activeEffect;
function effect(fn) {
const effectFn = () => {
cleanUp(effectFn);
activeEffect = effectFn;
fn();
};
effectFn.deps = [];
effectFn();
}
function cleanUp(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn);
}
effectFn.deps.length = 0;
}
function track(target, key) {
if (!activeEffect) {
return target[key];
}
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
activeEffect.deps.push(deps);
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
const effectsToRun = new Set(effects)
effectsToRun && effectsToRun.forEach((fn) => fn());
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newValue) {
target[key] = newValue;
trigger(target, key);
return true;
},
});
// console.log(obj);
effect(() => {
console.log(obj.text, "我被触发了");
});
setTimeout(() => {
obj.noExist = "big";
}, 1000);
- Vue3的render渲染函数实际上被包含在effect函数内执行,所以当render渲染函数执行时,对响应式数据的读取,就完成了依赖收集的过程,依赖就是render渲染函数。如果响应式数据发生变化,会重新调用render函数,生成虚拟DOM树,新旧两个树对比仅将差异的部分渲染到页面上。
effect(()=>{
Foo.render() // 组件的render渲染函数
})
- Vue3多次对同一个属性值的修改,只会引起页面一次更新,这是因为响应式数据实现了调度器,调度器决定了副作用函数调用的方式与时机。模版读取了响应式数据,依赖被记录。重新对响应式数据修改,会触发render函数重新执行,多次对响应式数据的修改,会多次触发render函数重新调用。但由于调度器的存在,每一次触发的render函数会被缓冲到同一个队列中,并且多次开启调用该队列的微任务,通过是否开启过第一次调用的标识,限流调用该队列函数只能开启一次。由于微任务的特性,等到多次同步的对响应式数据的修改完成,然后此时微任务才会触发,一次性调用被缓冲的render函数,达到高性能的更新页面。
const effectStack = [];
const jobQueue = new Set();
const p = Promise.resolve();
// 是否正在刷新队列
// 利用微任务队列,做到了同一个时刻是有一个flushJob任务开启了,并且当同步任务都add进来了,微任务队伍就开始一次性执行
let isFlushing = false;
function flushJob() {
if (isFlushing) return;
isFlushing = true;
p.then(() => {
jobQueue.forEach((job) => job());
}).finally(() => {
isFlushing = false;
});
}
effect(
() => {
console.log(obj.foo);
},
{
scheduler(fn) { // 调度
jobQueue.add(fn);
flushJob();
},
}
);
- 计算属性如何实现,原理是什么? 定义一个计算属性时,实际上是创建了一个特殊的函数,我们称之为“响应式getter函数”,这个函数的作用时追踪函数内所依赖的响应式数据,并在这些数据发生变化时自动重新计算。
- getter函数会被包装成reactive effect函数,这样就能够追踪它所依赖的响应式数据,并在这些数据发生变化时自动触发重新计算。
- 计算属性本质是一个函数,但是可以被用属性的方式读取,是因为计算属性返回一个带有value属性的对象,这个value属性具有访问器属性的特性。被读取时,会执行该函数getter函数,依赖的响应式数据与getter包装的reactive effect函数对应,返回的对象的value属性与模版读取时的渲染函数对应,然后Vue会完成各自的依赖收集的过程,他的内部有一个属性标识了是否需要重新计算。
- 当被依赖的响应式数据发生变化,会将computed函数内部的标识改为需要重新计算,触发value的副作用函数,告诉vue需要重新调用render函数生成新的虚拟DOM树,此时会对computed重新读值。就会重新执行被包装成reactive effect的getter函数,取得最新的值。否则读取的值永远都是缓存值。这利用了闭包的原理,确保只在需要重新计算时才执行计算逻辑。
function computed(getter) {
let value;
let dirty = true;
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true;
trigger(obj, "value");
},
});
const obj = {
get value() {
if (dirty) {
value = effectFn();
dirty = false;
}
track(obj, "value");
return value;
},
};
return obj;
}
function effect(fn, options = {}) {
const effectFn = () => {
cleanUp(effectFn);
activeEffect = effectFn;
effectStack.push(effectFn);
const res = fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
return res;
};
effectFn.options = options;
effectFn.deps = [];
if (!options.lazy) {
effectFn();
}
return effectFn;
}
- watch的实现原理,watch是当要监控的数据发生变化,执行回调函数,其实这里跟调度器的作用很相似,调度器就是当响应式数据发生变化后,控制依赖的执行时机,那么可以利用该机制,去实现watch。首先就是读取响应式数据,只有响应式数据才有依赖,当响应式数据发生变化,然后在调度器内去执行回调函数cb。这样就实现了最简单的watch。监控的数据可能是确切的,也可能是一个大对象,就需要traverse递归函数,递归的将对象的属性都加到Set中,统一读取一遍,这样无论哪个属性变化了,都可以触发回调函数。新旧值的获取,就需要lazy属性控制,利用闭包的方式,将新旧值都保留起来,初次手动执行被包装的副作用函数,可以得到旧值,新值的获取需要在调度器内的回调函数内,然后把旧值改为新值。
function watch(source, cb, options = {}) {
let getter;
if (typeof source === "function") {
getter = source;
} else {
getter = () => traverse(source);
}
let oldValue, newValue;
const job = () => {
newValue = effectFn();
cb(newValue, oldValue);
oldValue = newValue;
};
const effectFn = effect(() => getter(), {
scheduler() {
if (options.flush === "post") {
const p = Promise.resolve();
p.then(job);
} else {
job();
}
},
lazy: true,
});
if (options.immediate) {
job();
} else {
oldValue = effectFn();
}
}