Appearance
基本介绍
核心设计思想
ue3.0 注重模块上的拆分 Vue3 的模块之间耦合度低,模块可以独立使用。拆分模块 通过构建工具 Tree-shaking 机制实现按需引入,减少用户打包后体积。组合式 API Vue3 允许自定义渲染器,扩展能力强。扩展更方便 使用 RFC 来确保改动和设计都是经过 Vuejs 核心团队探讨并得到确认的。也让用户可以了解每 一个功能采用或废弃的前因后果。
整体架构
Monorepo 管理项目
Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包 (package)。Vue3 源码采用 monorepo 方式进行管理,将模块拆分到 package 目录中。作为一 个个包来管理,这样职责划分更加明确。 一个仓库可维护多个模块,不用到处找仓库 方便版本管理和依赖管理,模块之间的引用,调用都非常方便
Vue3 采用 Typescript
复杂的框架项目开发,使用类型语言非常有利于代码的维护,在编码期间就可以帮我们做 类型检查,避免错误。所以 TS 已经是主流框架的标配~
Vue2 早期采用 Flow 来进行类型检测(Vue2 中对 TS 支持并不友好),Vue3 源码采 用 Typescript 来进行理写。同时 Vue2.7 也采用 TS 进行重写。TS 能对代码提供良好的 类型检查,同时也支持复杂的类型推导。
Vue3响应式原理
reactive
核心响应式模块 reactive(让数据变成响应式,可以检测数据的变化)、effect(数据变化后可以让 effect 执行更新视图,组件、watch、computed 都是基于 effect 来实现)
html
<div id="app"></div>
<script type="module">
import { reactive,effect }from"/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
const state = reactive({name:"jw",age:30 });
// effect 会默认执行一次
effect(()=>{
app.innerHTML=姓名${state.name}年龄${state.age};
);
setTimeout(()=>
state.age++;//数据变化后effect会再次重新执行
,1000)reactive 实现
1.判断传入的参数是否为对象类型
js
//统一做判断,响应式对象必须是对象才可以
if(!isobject(target)){
return target;
}2.使用 WeakMap 缓存代理后的结果,对同一数据代理多次返回的还是同一个代理对象
js
// 用于记录我们的代理后的结果,可以复用
const reactiveMap = new WeakMap();
const exitsProxy = reactiveMap.get(target);
if(exitsProxy)return exitsProxy;
let proxy = new Proxy(target,mutableHandlers);
// 根据对象缓存代理后的结果
reactiveMap.set(target,proxy)3.在代理数据的 get 方法写入 IS_REACTIV 判断,用于返回当前数据是否被代理过(用于应对 reactive(reactive(obj)) 这种情况),防止多次代理
js
// 获取当前数据是否被代理过
if(target[ReactiveFlags.IS_REACTIVE]){
return target;
}
// 用于应对如下情况
let obj = {name:“jw",age:30};
const state1 = reactive(obj);
//如果 state1 被代理过后一定有 get 和 set 了
const state2 = reactive(state1);
console.log(state1=== state2) // true4.使用 Reflect 书写代理对象更改逻辑
js
const person = {
name: 'jw',
get aliasName() {
console.log(this) // 未使用 Reflect 时 这里的 this 指向 person 而不是 proxyPerson
return this.name + 'handsome'
}
}
let proxyPerson = new Proxy(person, {
get(target, key, recevier) {
// return target[key]
// return recevier[key] // 报错 Maximum call stack size exceeded
return Reflect.get(target, key, recevier)
}
})
console.log(proxyPerson.aliasName)5.实现 effect。定义一个全局变量用于保存当前执行的 effect ,并在执行 effect 的 回调函数(effect 的参数)时设置当前执行的 effect,effect 默认会自动执行一次
6.创建一个全局的 WeakMap(一种数据结构,可以防止内存泄漏。也就是 dep 依赖映射表)根据代理对象存放对应属性,在对应属性新建一个 Map 存放涉及该属性的 effect
7.进行依赖收集。这时就可以在代理对象的 get 方法里拿到代理对象和对应的属性以及与之对应的 effect (上一步通过保存的全局变量获取)进行依赖收集。
8.优化依赖收集。在每个 effect 中定义一个 _trackId 属性用于记录执行次数,对于多次出现的响应式数据保证只收集一次。
拿到上一次的依赖的与这次比较,如果不一致则会删掉上一次的依赖(尽量复用)替换成新的依赖,避免出现不需要触发 effect 执行的依赖却触发了 effect 重新执行
js
// 依赖收集时会记录在此 effect 使用用的响应式数据(即 state.flag 与 state.name),每个响应式数据发生变化时都会重新执行 effect
effect(()=>{
// 这里出现了两次 state.flag 需要优化只需要收集一次 state.flag 依赖即可
app.innerHTML = state.flag + state.flag + state.name;
)
effect(() =>
// 这里 state.name 和 state.age 只使用了其一,收集依赖时也应该收集实际使用到的依赖
app.innerHTML = state.flag ? state.name: state.age;
);9.当监听到代理对象变化时会触发代理对象的 set 方法,这里就可以拿到对应的代理对象和键以及老值与新值,然后根据上一步保存的 dep 中查找对应代理对象的对应属性,再将对应属性身上保存的所有 effect 依次调用一下即可完成页面更新
10.进行一些优化。
(1.在 effect 中定义一个变量用于记录当前 effect 是否正在执行,然后在执行 effect 之前判断 当前 effect 是否正在执行,如果是则不执行。
(2.解决深层次数据变化时 effect 不执行。在 proxy 代理对象上获取值时会触发代理对象的 get 方法,此时判断该值是否为基础数据类型,如果不是则将该值用 reactive 方法递归代理(即懒代理,当取用的值不为基础数据类型时,会深层代理)
js
effect(()=>{
// 当修改页面后又再次修改响应式数据,会导致死循环
app.innerHTML = state.name;
state.name = Math.random();
})
// 如果只代理响应式数据的第一层则深层数据变化时并不会触发 effect 执行
effect(()=>{
app.innerHTML = state.address.n;
});
setTimeout(()=>{
state.address.n= 602
},1000)ref 实现
1.创建一个 RefImpl 类用于创建 ref 的响应式数据。
1)在类中定义一个 __v_isRef 属性用于区分某个数据是否是 ref 创建的响应式数据。
2)再定义一个 _value 属性用于保存用户传递进来的数据,重写类的 get 和 set 方法(set方法中会去重,即修改的最新值与保存的值不一致时才会修改 ref 的值)
3)如果传入的值不为基础数据类型时,会调用 reactive 方法包裹实现响应式
2.依赖收集。定义一个 dep 属性用于收集依赖,在 get 方法中将当前 effect 与 ref 值保存到 dep 中,然后在 set 方法拿出 dep 中的依赖 effect 依次执行
ts
class RefImpl {
public _v_isRef = true // 增加ref标识
public _value // 用来保存ref的值的
public dep // 用于收集对应的effect
constructor(public rawValue) {
// 如果传入的值不为基础数据类型时,会调用 reactive 方法包裹实现响应式
this._value = toReactive(rawValue);
}
get value() {
// 收集依赖
trackRefValue(this)
return this._value
}
set value(newValue) {
if (newValue !== this.rawValue) {
this.rawValue = newValue // 更新值
this._value = newValue
// 拿出 dep 中的依赖 effect 依次执行
triggerRefValue()
}
}
}toRefs、proxyRef 实现
toRef、toRefs 实现
功能:将一个 reactive 数据的某个或多个值变成 Ref 数据
与 ref 实现较为类似
ts
// 以下为简化代码
class ObjectRefImpl {
public _v_isRef = true // 增加ref标识
constructor(public _object, public _key) {}
get value() {
return this._object[this._key]
}
set value(newWalue) {
this._object[this._key] = newWalue
}
}
export function toRef(object, key) {
return new ObjectRefImpl(object, key)
}
// toRefs 实际上就是循环调用 toRef
export function toRefs(object) {
const res = {}
// 这里简化了代码,只考虑对象类型
for (let key in object) {
res[key] = toRef(object, key)
}
return res
}proxyRef 实现
proxyRefs 用于应对 ref 数据自动解包,开发中很少用,是 Vue 内部在模板中将 ref 自动解包使用,即 ref 数据在模板中无需 .value
ts
let state = reactive({name:"jw",age: 30 });
let proxy = proxyRefs({...toRefs(state)});
effect(()=>{
// 无需 .value 即可访问数据,相当于 proxy.name.value,proxy.sge.value
console.log(proxy.name,proxy.sge); // jw 30
})用 Proxy 代理并在 get 方法中判断是否是 ref 数据,如果是则自动解包(即 .value)
ts
// 简单实现
export function proxyRefs(objectwithRef) {
return new Proxy(objectwithRef, {
get(target, key, receiver) {
let r = Reflect.get(target, key, receiver)
return r._v_isRef ? r.value : r //自动脱ref
},
set(target, key, value, receiver) {
const oldValue = target[key]
if (oldValue._v_isRef) {
oldValue.value = value // 如果老值是ref需要给ref赋值
return true
} else {
return Reflect.set(target, key, value, receiver)
}
}
})
}计算属性实现
computed 执行后的结果是一个 ref 。
计算属性具备收集能力的,可以收集对应的 effect,依依赖的值变化后会触发 effect 重新执行。
1.计算属性维护了一个 dirty 属性(表示是否执行过),默认就是 true,稍后运行过一次会将 dirty 变为 false,并且稍后依赖的值变化后会再次让 dirty 变为 true 2.计算属性也是一个 effect,依赖的属性会收集这个计算属性,当前值变化后,会让 computedEffect 里面dirty 变为 true
具体实现
1.定义两个变量 Dirty 与 NoDirty 。Dirty 是一个数字类型,表示脏值,意味着取值要运行计算属性。NoDirty 也是一个数字类型,表示不脏就用上一次的返回结果。并在 effect 属性上添加一个属性 _dirtyLevel 表示脏值。
ts
export enum DirtyLevels{
Dirty = 4,// 脏值,意味着取值要运行计算属性
NoDirty = 0,// 不脏就用上-次的返回结果
}2.在运行 effect 时会将该 effect 的 _dirtyLevel 置为 0 表示不脏(便于实现 computed 缓存功能)。
3.定义一个方法 computed ,判断参数是否为函数(computed 参数可直接传递一个函数也可以传递 getter 和 setter)最后返回一个 ComputedRefImpl 类
ts
export function computed(getteroroptions) {
let onlyGetter = isFunction(getteroroptions)
let getter
let setter
if (onlyGetter) {
getter = getteroroptions
setter = () => {}
} else {
getter = getteroroptions.get
setter = getteroroptions.set
}
return new ComputedRefImpl(getter,setter);// 计算属性 ref
}4.在 ComputedRefImpl 类中通过 ReactiveEffect 类(创建 effect 的类)创建一个关联的 effect,再通过定义一个属性 _value 记录老值,并重写 getter (获取 computed 值时执行 effect) 和 setter。
5.依赖收集。定义一个 dep 属性用于将 computed 与相关的 effect 关联(依赖收集),然后在获取值时将对应 effect 存入 dep 中。循环取出 dep 中的 effect 依次执行并将该 effect 变脏(即用户修改了 computed 依赖的值时变脏,需要重新执行)。
ts
class ComputedRefImpl {
// 记录当前 computed 的老值
public _value
// 关联当前 computed 对应的 effect
public effect
// 用于收集依赖
public dep
constructor(getter, public setter) {
// 我们需要创建一个 effect 来关联当前计算属性的 dirty 属性
this.effect = new ReactiveEffect(
() => getter(this._value),
() => {
// 计算属性依赖的值变化了,我们应该触发渲染。即循环取出 dep 中的 effect 依次执行并将该 effect 变脏
}
)
}
get value() {
// 只有当前 effect 不脏时才会重新执行,否则直接返回老值
if(this.effect.dirty){
// 默认取值一定是脏的,但是执行一次 run 后就不脏了
this._value = this.effect.run();
// 在此进行依赖收集,将对应 effect 存入 dep 中
}
// 直接返回老值
return this._value;
}
set value(v) {
//这个就是ref的setter
this.setter(v)
}
}watch
可以监听数据的变化。
watch 实现
1.定义一个 watch 方法,返回 dowatch 方法
ts
export function watch(source,cb,options = {}){
// watchEffect 也是基于 dowatch 来实现的
return dowatch(source,cb,options);
}2.先根据 deep (用户传递的是否深度监听参数)递归遍历 source 中的每一个数据。遍历时就会触发每个属性的 getter
ts
// depth 选项可能还处在测试阶段,在后续版本才可使用
// depth 表示深度监听到第几层,currentDepth 表示当前已经遍历的层数,每遍历一个数据就会往 seen 中保存一份,如果下次再遇到相同的直接返回保存的值,防止递归查询死循环(即一个对象中的值又指向对象本身)
function traverse(source, depth, currentDepth = 0, seen = new Set()) {
if (!isobject(source)) {
return source
}
if (depth) {
if (currentDepth >= depth) {
return source
}
currentDepth++ //根据deep属性来看是否是深度
}
if (seen.has(source)) {
return source
}
seen.set(source)
for (let key in source) {
traverse(source[key], depth, currentDepth, seen)
}
return source
}
function dowatch(source, cb, options) {
const reactiveGetter = source => traverse(source,options.deep === false?1:undefined)
}3.根据上一步遍历的数据产生一个 getter 并传递给 ReactiveEffect (之前实现的类,只要访问监听的值就会重新执行回调),这样在遍历用户数据时就相当于是依赖收集了(递归遍历时就已经访问了所有数据),只要数据发生变化就会重新执行回调。
ts
function dowatch(source, cb, options) {
// 递归遍历 source 中的每一个数据
const reactiveGetter = source => traverse(source,options.deep === false?1:undefined)
// 判断当前数据是否是响应式数据,是的话才会做监听
let getter
if(isReactive(source)){
//产生一个可以给ReactiveEffect来使用的getter,需要对这个对象进行取值操作,会关联当前 reactiveEffect
getter = () => reactiveGetter(source)
}else if(isRef(source)){
// 如果是 ref 的值会将其转换为 getter 并解包以实现如下写法
getter = () => source.value
}else if(isFunction(source)){
// 如果是函数类型直接赋值
getter = source
}
// 保存老值
let oldValue
function job(){
if(cb){
// 当数据变化后会执行此回调,拿到新值并保存
const newValue = effect.run();
// 调用用户传递的回调并将新老值传递过去
cb(newValue,oldvalue)
oldValue = newValue
}else{
// watchEffect 实现逻辑
}
}
const effect = new ReactiveEffect(getter, job)
if(cb){
// 用户如果传递了 inmediate 属性则立即执行一次。
if(options.inmediate){
job()
}else{
// 保存老值
oldValue = effect.run()
}
}else{
// watchEffect 实现逻辑
}
}4.其他一些补充
ts
if(isReactive(source)){
// 代码...
}else if(isRef(source)){
// 如果是 ref 的值会将其转换为 getter 并解包
getter = () => source.value
}
// 上面判断是否是 ref 的的逻辑是为了实现如下写法。即实现对 ref 数据的监听
const name = ref('xxx')
watch(name, (newVal, oldVal) => {
console.log('1')
}
if(isReactive(source)){
// 代码...
}else if(isRef(source)){
// 代码...
}else if(isFunction(source)){
// 如果是函数类型直接赋值
getter = source
}
// 上面判断是否是函数的的逻辑是为了实现如下写法。即实现对 reactive 中某个值单独的监听
const state = reactive({age:30})
watch(()=>state,age, (newVal, oldVal) => {
console.log('1')
}watchEffect
watchEffect 就类似于一个 reactiveEffect 只要依赖的数据变化就重新执行
ts
const state = reactive({name:"jw",age:30,address:{n:1}})
watchEffect(()=>{
console.log(state.name + state.age);
});1.定义一个 watchEffect 方法,返回 dowatch 方法
ts
export function watchEffect(source,options = {}){
// 没有 watch 中的 cb(用户传递的回调函数)就是 watchEffect
return dowatch(source,null,options);
}2.在上一步实现 watch 的 inmediate 属性逻辑判断没有 cb 时,直接执行 effect 即可
ts
const effect = new ReactiveEffect(getter, job)
if(cb){
// watch API 相关代码......
}else{
effect.run() // 直接执行即可
}3.在上一步实现 watch 的 job 方法逻辑判断没有 cb 时,直接执行 effect 即可
ts
const effect = new ReactiveEffect(getter, job)
function job(){
if(cb){
// watch API 相关代码......
}else{
effect.run() // 直接执行即可
}
}渲染器
使用
vue 中提供了两种渲染器(如何渲染虚拟DOM),render(借助 DOM API 渲染,vue 内置渲染器)、createRenderer(自定义渲染)。此功能在 vue 源码中的 runtime-dom 包中,主要提供一些浏览器端 API
ts
import{ createRenderer,render,h }from"/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js";
// 使用内置渲染器
// 创建一个虚拟节点 h1 元素文本为 handsome jw
let ele = h("h1","handsome jw");
// 表示将此虚拟节点变为真实节点插入到 app 这个容器中
render(ele,app);
// 使用自定义渲染器。如 uniapp 开发小程序
let ele = h("h1","handsome jw");
const renderer = createRenderer({
// 自定义如何创建元素
createElement(type){
return document.createElement("h1")
},
// 如何处理文本元素
setElementText(el,text){
el.textContent = text;
},
// 如何插入节点
insert(el,container){
container.appendChild(el);
}
)}
// 将此虚拟节点变为真实节点插入到 app 这个容器中
renderer.render(ele,app);具体实现
1.实现 nodeOps。实现对节点得增删改等操作。
ts
export const nodeops = {
// 如果第三个元素不传递则等价于 appendChild
insert(el, parent, anchor) {
//appendchild parent.insertBefore(el,null)
parent.insertBefore(el, anchor || null)
},
remove(el) {
//移除dom元素
const parent = el.parentNode;
if(parent){
parent.removeChild(el);
}
},
createElement(type) {
return document.createElement(type)
},
setElementText(el, text) {
el.textContent = text
}
}2.实现 patchProp 。实现对节点属性的(class、style、事件、class、普通属性)操作
ts
export function patchProp(el, key, prevValue, nextValue) {
// 处理 class
if (key === 'class') return patchclass(el, nextValue)
// 处理 style
if (key === 'style') return patchStyle(el, prevValue, nextValue)
// 处理事件以 on 开头,如 onClick
if (/on[^a-z]/.test(key)) return patchEvent(el, key, nextValue)
// 处理普通属性
return patchAttr(el, key, nextValue)
}
// 处理 class
function patchclass(el, value) {
if (value == null) {
// 移除class
el.removeAttribute('class')
} else {
el.className = value
}
}
// 处理 style
function patchStyle(el, prevValue, nextvalue) {
let style = el.style
for (let key in nextvalue) {
style[key] = nextvalue[key] // 新样式要全部生效
if (prevValue) {
for (let key in nextvalue) {
// 看以前的属性,现在有没有,如果没有删除掉
if (nextvalue && nextvalue[key] == null) style[key] = null
}
}
}
}
// 处理事件
function patchEvent(el, name, nextValue) {
// 将用户绑定的事件回调保存起来,下次回调更新后无需解绑事件再绑定,直接修改 .value 属性保存的函数即可
function createInvoker(value) {
const invoker = e => invoker.value(e)
// 更改 invoker 中的 value 属性可以修改对应的调用函数
invoker.value = value
return invoker
}
// 给当前元素绑定缓存区,用于记录绑定过哪些事件
const invokers = el._vei || (el._vei = {})
const exisitingInvokers = invokers[name]
const eventName = name.slice(2).toLowerCase()
if (nextValue & exisitingInvokers) {
// 事件换绑定 当缓存中存在同名事件如:之前是 onClick = fn1,更新后是 onClick = fn2 直接替换用户回调即可
return (exisitingInvokers.value = nextValue)
}
if (nextValue) {
// 创建一个调用函数,并且内部会执行 nextValue,并将用户回调缓存
const invoker = (invokers[name] = createInvoker(nextValue))
// 绑定事件
return el.addEventListener(eventName, invoker)
}
if (exisitingInvokers) {
// 现在没有,以前有
el.removeEventListener(eventName, exisitingInvokers)
invokers[name] = undefined
}
}
// 处理普通属性
function patchAttr(el, key, value) {
if (value === null) {
el.removeAttribute(key)
} else {
el.setAttribute(key, value)
}
}3.将前两步组装的选项合并变为最终传递给渲染器的选项
ts
let ele = h("h1","handsome jw");
// 组装选项
const renderer = createRenderer(Object.assign({patchProp},node0ps))
renderer.render(ele,app);虚拟DOM渲染
vue 内部就是按照上一个大步骤创建渲染器并渲染的。
ts
const render =(vnode,container)=> {
// render0ptions 是上一个大步骤中的组装的选项 vnode 表示虚拟 DOM container 表示要将 vnode 渲染的容器
return createRenderer((render0ptions).render(vnode,container);
}
// 用户使用代码
// 第一次的 vdom
let ele1 = h(
"h1",
{style:{color:'red'}},
"handsome jw"
)
// 第二次的 vdom
let ele2 = h(
"h2",
{style:{color:'#000'}},
"handsome jw"
)
render(ele1,app);
// 更新
setTimeout(()=>{
render(ele2,app);
},3000);实现
1.创建 createRenderer 方法,将外部传入的渲染选项重命名并返回一个 render 函数,此函数会采用外部传递进来的 API 将 vnode 渲染到指定容器中。此功能在 vue 源码中的 runtime-core 包中,此包完全不关心渲染时用的是哪些 API ,可以跨平台
ts
export function createRenderer({
insert: hostInsert,
remove: hostRemove,
createElement: hostCreateElement,
createText: hostCreateText,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
patchProp: hostPatchProp
}) {
const render = (vnode, container) => {
// 这里会采用上面的 API 将 vnode 渲染到 container 中
// ...实现逻辑...
}
return {
render
}
}2.创建 patch 方法用于渲染和更新 vnode ,并保存此次的 vnode 方便后续更新操作时比对差异
ts
export function createRenderer(renderoptions) {
// ...其他逻辑...
// 初次渲染走这里,更新也走这里
const patch = (n1, n2, container) => {}
// 多次调用 render 会进行虚拟节点的比较,再进行更新
const render = (vnode, container) => {
// 第一个参数表示旧的 vnode 第二个表示最新的 vnode
patch(container._vnode || null, vnode, container)
// 保存此次的 vnode 方便后续更新操作时比对差异
container._vnode = vnode
}
return {
render
}
}3.创建 patch 方法,多次调用时过滤相同的 vnode ,如果没有旧的 vnode 就执行初次渲染
ts
const patch = (n1, n2, container) => {
// 两次渲染同一个元素直接跳过即可
if (n1 === n2) return
if (n1 === null) {
// 初始化操作
mountElement(n2, container)
}
}4.实现 mountElement 方法,根据外部提供的 API 将虚拟节点变为真实节点并插入到页面中
ts
const mountElement = (vnode, container) => {
const mountChildren = (children, container) => {
for (let i = 0; i < children.length; i++) {
patch(null, children[i], container)
}
}
// type:当前节点的类型,如 h1、div等,children:此节点的子节点,可能是文本,props:此节点上的属性,shapeFlas:子节点的类型,是一个数字,通过位运算得出
const { type, children, props, shapeFla } = vnode
// 根据类型创建真实元素
let el = hostCreateElement(type)
// 当此元素有属性时,循环所有属性并依次添加到元素上
if (props) {
for (let key in props) {
hostPatchProp(el, key, null, props[key])
}
}
// 判断子节点是否是单个文本
if (shapeFla & ShapeFlags.TEXT_CHILDREN) {
// 创建文本节点并插入
hostSetElementText(el, children)
// 当子节点有多个时递归遍历依次创建
}else if(shapeFla & ShapeFlags.ARRAY_CHILDREN){
mountElement(children,el)
}
// 将此元素插入到指定容器中
hostInsert(el, container)
}h 方法
h 方法是一个用于创建虚拟节点的函数,它是创建渲染函数的核心工具。用法如下
ts
// 1.两个参数第二个参数可能是属性,或者虚拟节点(v_isVnode 有此属性表示是虚拟节点)
const ele1 = h("div",{a:1}) // 表示创建一个 div 节点,节点属性为 a 其值为 1
const ele1 = h("div",h("a")) // 表示创建一个 div 节点,再为其创建一个子节点 a 标签
// 2.第二个参数是一个数组表示儿子
const ele1 = h("div",["a","b"]) // 表示创建一个 div 节点,其子节点为 a 和 b(都是文本)
// 3.其他情况就是属性
// 4.直接传递非对象的,文本
const ele1 = h("div",'abc') // 表示创建一个 div 节点,再为其创建一个子节点 abc(文本)
// 5.不能出现三个参数的时候第二个只能是属性
const ele1 = h("div",{a:1},'abc') // 表示创建一个 div 节点,节点属性为 a 其值为 1,再为其创建一个子节点 abc(文本)
// 6.如果超过三个参数,后面的都是儿子
const ele1 = h("div",{a:1},"a","b","c","d")// 表示创建一个 div 节点,节点属性为 a 其值为 1,再为其创建一个子节点 abcd (文本)实现
1.创建一个 h 函数,分别根据参数的个数进行不同的创建方式对应虚拟节点。写法比较随意。
ts
export function h(type, propsorchildren?, children?) {
let l = arguments.length
if (l === 2) {
//h(h1,虚拟节点|属性)
if (isobject(propsorChildren) && !Array.isArray(propsorChildren)) {
// 判断当前节点是否是虚拟节点,用 _v_isVnode 判断
// h("div",h('a'))
if (isVnode(propsorChildren)) return createVnode(type, null, [propsorchildren])
// 属性
return createVnode(type, propsorchildren)
}
// 儿子 是数组丨文本
return createVnode(type, null, propsorchildren)
} else {
if (l > 3) {
// 直接截取后面的参数作为子节点
children = Array.from(arguments).slice(2)
}
if (l == 3 && isVnode(children)) {
// 如果参数为 3 直接将 children 包装成数组作为子节点
children = [children]
}
// 参数为 3 或 1 的情况
return createVnode(type, propsorchildren, children)
}
}2.实现 createVnode 方法,返回虚拟节点,用法严格。vue 为了处理后面的优化,源码中编译后的结果全部采用了createVnode,靶向更新。至此就完成了页面初次渲染。
ts
function createVnode(type, props, children?) {
// 判断当前节点类型是否是字符串,是的话就创建对应标签的虚拟节点,否则置为 0
const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0
const vnode = {
_v_isVnode: true, // 标识是否是虚拟节点
type, // 节点类型
props, // 属性
children, // 子节点
key: props?.key, //diff算法后面需要的key
el: null, //虚拟节点需要对应的真实节点是谁
shapeFlag
}
if (children) {
if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
} else {
children = String(children)
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
}
}
return vnode
}3.当页面更新后,需要对比节点的变化。
ts
let vnode1 = h('h1‘,{key:1},’hello')
let vnode2 = h('div', { style: { color: 'red' },key:2 }, 'world')
// 初次渲染
render(vnode1, app)
// 两个节点之间是有diff的看两个元素是否是同一个元素,如果是则可以复用
setTimeout(() => {
// 更新
render(vnode2, app)
}, 1000)4.需要修改前几步中 render 方法中的 patch 方法,当新旧节点不同时直接删除旧节点,重新挂载新节点。
ts
// 判断当前新旧节点是否相同,主要是判断节点的 type(h1、div 等) 和 key 是否相同
function isSameVnode(n1,n2){
return n1.type == n2.type && n1.key === n2.key
}
const patch = (n1, n2, container) => {
// 其他逻辑代码......
// 判断当前新旧节点是否相同
if(n1 &!isSameVnode(n1,n2)){
/*
当新旧节点如下时
let vnode1 = h('h1‘,{key:1},’hello')
let vnode2 = h('div', { style: { color: 'red' },key:2 }, 'world')
直接删除老的节点,重新渲染新节点即可
*/
unmount(n1); // 删除老的节点
n1 = null;·// 就会执行后续的 n2 的初始化
}
// 其他逻辑代码......
}5.当新旧节点相同,属性不同时,需要比对差异进行更新
ts
const patch = (n1, n2, container) => {
// 其他逻辑代码......
// 判断当前新旧节点是否相同
if(n1 &!isSameVnode(n1,n2)){
// 其他逻辑代码......
}
if(n1 === null){
// 其他逻辑代码......
}else{
/*
当新旧节点相同,属性不同时
let vnode1 = h('h1‘,{key:1},’hello')
let vnode2 = h('h1', { style: { color: 'red' },key:2 }, 'world')
*/
patchElement(n1,n2,container)
}
// 其他逻辑代码......
}
function patchElement(n1,n2,container){
// 将老节点的 dom 赋值给新节点,并将此节点缓存
let el = n2.el = n1.el;
// 取出新旧节点的属性
let oldProps = n1.props || {}
let newProps = n2.props || {};
// 循环处理节点上的所有属性
patchProps(oldProps,newProps,el)
// 处理节点上的子节点
patchchildren(n1,n2,container)
}
function patchProps(oldProps,newProps,el){
// 循环处理新节点上的所有属性,让其全部生效
for (let key in newProps){
// 对某一条属性进行更新操作
hostPatchProp(el,key,oldProps[key],newProps[key])
}
// 循环处理老节点上的所有属性
for (let key in oldProps){
// 此属性如果在新节点没有则需要删除
if(!(key in newProps)){
hostPatchProp(el,key,oldProps[key],null)
}
}
}6.比对节点的子节点差异,实现除新旧子节点都是数组(多个子节点,需要 diff) 情况外的渲染
子元素比较情况
| 新儿子 | 旧儿子 | 操作方式 |
|---|---|---|
| 文本 | 数组 | 删除老儿子,设置文本内容 |
| 文本 | 文本 | 更新文本即可 |
| 文本 | 空 | 更新文本即可,与上面的类似 |
| 数组 | 数组 | diff 算法 |
| 数组 | 文本 | 清空文本,进行挂载 |
| 数组 | 空 | 进行挂载,与上面的类似 |
| 空 | 数组 | 删除所有儿子 |
| 空 | 文本 | 清空文本 |
| 空 | 空 | 无需处理 |
ts
function patchElement(n1,n2,container){
// 其他逻辑......
// 处理节点上的子节点
patchchildren(n1,n2,container)
}
function patchchildren(n1,n2,el){
// 有三种情况 儿子是文本、数组、null(没有子节点)
const c1= n1.children;
const c2= n2.children;
// 旧节点的节点类型(文本、元素等)
const prevShapeFlag = n1.shapeFlag;
// 新节点的节点类型
const shapeFlag = n2.shapeFlag;
// 新的是文本,老的是数组
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 循环老的子节点并将全部其删除
unmountChildren(c1)
}
// 如果新的是文本,老的也是文本,直接替换成新的文本
if (c1 !== c2) {
hostSetElementText(el, c2)
}
} else {
// 老的是数组,新的也是数组
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 全量 diff 算法两个数组的比对
// 放到下一个步骤实现
} else {
// 老的是数组,新的不是数组,循环老的子节点并将全部其删除
unmountChildren(c1);
}
} else {
// 老的是文本,新的是空
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 直接删除文本即可
hostSetElementText(el, '')
}
// 老的是文本,新的是数组,直接将数组元素依次挂载到容器中
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(c2, el)
}
}
}
}diff 算法
1.紧接上一步当新旧节点都是数组(多个节点),就需要 diff 算法来计算差异了。
ts
function patchchildren(n1,n2,el){
// 其他逻辑代码......
const c1 = n1.children
const c2 = n2.children
// 老的是数组,新的也是数组
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 全量 diff 算法两个数组的比对
patchKeyedChildren(c1,c2,el)
}
// 其他逻辑代码......
}
}2.实现 patchKeyedChildren 方法。先从头开始比,再从尾部开始比,同层比较(新旧节点同一层级)
ts
// 1.减少比对范围,从头开始比,确定新旧子节点不一样的范围
function patchKeyedChildren(n1,n2,el){
let i = 0 // 开始比对的索引
let e1 = c1.length - 1 // 第一个数组的尾部索引
let e2 = c2.length - 1 // 第二个数组的尾部索引
while (i <= e1 && i <= e2) {
// 有任何一方循环结束了就要终止比较
const n1 = c1[i]
const n2 = c2[i]
if (isSameVnode(n1, n2)) {
patch(n1, n2, el) // 更新当前节点的属性和儿子(递归比较子节点)
} else {
break
}
i++
}
}
// 用户代码
// 旧节点
let vnode1 = h('h1', [
h('div', { key: 'a' }, 'a'),
h('div', { key: 'b' }, 'b'),
h('div', { key: 'c' }, 'c')
])
// 新节点
let vnode2 = h('h1', [
h('div', { key: 'a' ,style:{color:'red'}}, 'a'),
h('div', { key: 'b' }, 'b'),
h('div', { key: 'd' }, 'd'),
h('div', { key: 'e' }, 'e')
])3.从尾部开始对比
ts
// 用户代码(尾部有相同节点)
// 旧节点
let vnode1 = h('h1', [
h('div', { key: 'a' }, 'a'),
h('div', { key: 'b' }, 'b'),
h('div', { key: 'c' }, 'c')
])
// 新节点
let vnode2 = h('h1', [
h('div', { key: 'd' ,style:{color:'red'}}, 'd'),
h('div', { key: 'e' }, 'e'),
h('div', { key: 'b' }, 'b'),
h('div', { key: 'c' }, 'c')
])ts
function patchKeyedChildren(n1,n2,el){
let i = 0 // 开始比对的索引
let e1 = c1.length - 1 // 第一个数组的尾部索引
let e2 = c2.length - 1 // 第二个数组的尾部索引
while (i <= e1 && i <= e2) {
// 上一步从头开始比较逻辑代码......
}
// 从尾部开始比较
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVnode(n1, n2)) {
patch(n1, n2, el) // 更新当前节点的属性和儿子(递归比较子节点)
} else {
break
}
e1--
e2--
}
}4.比对不变的节点。新节点比老节点多,依次从尾部开始插入新节点
ts
function patchKeyedChildren(n1, n2, el) {
let i = 0 // 开始比对的索引
let e1 = c1.length - 1 // 第一个数组的尾部索引
let e2 = c2.length - 1 // 第二个数组的尾部索引
while (i <= e1 && i <= e2) {
// 上一步从头开始比较逻辑代码......
}
// 从尾部开始比较
while (i <= e1 && i <= e2) {
// 上一步从尾部开始比较逻辑代码......
}
// 新的节点多
if (i > el) {
// 有插入的部分
if (i <= e2) {
// 看一下当前下-个元素是否存在
letnextPos = e2 + 1
let anchor = c2[nextPos]?.el
// 有的话,从尾部开始插入新节点
while (i <= e2) patch(null, c2[i], el, anchor)
i++
}
}
}5.比对不变的节点。老节点比新节点多,将元素一个个删除
ts
function patchKeyedChildren(n1, n2, el) {
let i = 0 // 开始比对的索引
let e1 = c1.length - 1 // 第一个数组的尾部索引
let e2 = c2.length - 1 // 第二个数组的尾部索引
while (i <= e1 && i <= e2) {
// 上一步从头开始比较逻辑代码......
}
// 从尾部开始比较
while (i <= e1 && i <= e2) {
// 上一步从尾部开始比较逻辑代码......
}
if (i > el) {
// 新的节点多处理逻辑代码......
} else if (i > e2) {
// 旧的节点多处理逻辑代码......
if (i <= e1) {
while (i <= e1) unmount(c1[i]) // 将元素一个个删除
i++
}
}
}6.乱序比对。拿老的节点到新的节点中寻找,有就更新或复用没有就删除,最后还有剩余的新节点就新增。
以上步骤确认不变化的节点,并对这部分进行移除或插入。这步将对比新老节点发生变化时做处理。
ts
function patchKeyedChildren(n1, n2, el) {
let i = 0 // 开始比对的索引
let e1 = c1.length - 1 // 第一个数组的尾部索引
let e2 = c2.length - 1 // 第二个数组的尾部索引
while (i <= e1 && i <= e2) {
// 上一步从头开始比较逻辑代码......
}
// 从尾部开始比较
while (i <= e1 && i <= e2) {
// 上一步从尾部开始比较逻辑代码......
}
if (i > el) {
// 新的节点多处理逻辑代码......
} else if (i > e2) {
// 旧的节点多处理逻辑代码......
} else {
let s1 = i
let s2 = i
// 根据新节点列表做一个映射表用于快速查找,看老的是否在新的里面还有,没有就删除,有的话就更新
const keyToNewIndexMap = newMap()
let toBePatched = e2 - s2 + 1 // 要倒序插入的个数
for (leti = s2; i <= e2; i++) {
const vnode = c2[i]
}
// 遍历老节点
for (leti = s1; i <= e1; i++) {
const vnode = c1[i]
const newIndex = keyToNewIndexMap.get(vnode.key) // 通过 key 找到对应的索引
if (newIndex == undefined) {
// 如果新的里面找不到则说明老的有的要删除掉
unmount(vnode)
} else {
// 比较前后节点的差异,更新属性和儿子
patch(vnode, c2[newIndex], el) //服用
}
}
// 调整节点顺序
// 按照新的队列倒序插入 insertBefore 通过参照物往前面插入
// 插入的过程中,可能新的元素的多,需要创建
for (leti = toBePatched - 1; i >= 0; i--) {
let newIndex = s2 + i // 找下-个元素作为参照物,来进行插入
let anchor = c2[newIndex + 1]?.el
let vnode = c2[newIndex]
// 当不存在此节点时,表示新加入的节点需要创建
if (!vnode.el) {
// 新列表中新增的元素
patch(null, vnode, el, anchor) // 创建这个节点并插入
} else {
hostInsert(vnode.el, el, anchor) // 倒序插入节点
}
}
}
}优化
以上步骤性能较差(倒序插入,但某些节点其实是不需要动的),还需优化,利用贪心算法 + 二分查找(最长递增子序列)
记录新的元素索引映射老的索引(元素的位置),再根据最长递增子序列算法求出哪些元素是不用移动的(优化点)。
1.加入最长递增子序列算法,当当前节点索引出现在最长递增子序列中则跳过插入,以提高性能。
ts
function patchKeyedChildren(n1, n2, el) {
// 前一个步骤的其他逻辑代码......
if (i > el) {
// 前一个步骤的其他逻辑代码......
} else if (i > e2) {
// 前一个步骤的其他逻辑代码......
} else {
// 前一个步骤的其他逻辑代码......
// 根据要倒序插入的个数创建对应的数组,用新索引映射老的索引,再利用算法求出哪些元素是不用移动的
let nnewIndexTo0ldMapIndex = new Array(toBePatched).fill(0) // [0,0,0,0]
for (leti = s1; i <= e1; i++) {
if (newIndex == undefined) {
// 前一个步骤的其他逻辑代码......
} else {
// 每次 patch 比对后记录索引,i 可能出现是 0 的情况,为了保证 0 是没有比对过的元素,直接 i+1
newIndexTo0ldMapIndex[newIndex - s2] = i + 1
// 比较前后节点的差异,更新属性和儿子
patch(vnode, c2[newIndex], el)
}
}
// 获取最长递增子序列
let increasingSeqq = getSequence(newIndexToOldMapIndex)
let j = increasingSeq.length - 1
for (leti = toBePatched - 1; i >= 0; i--) {
// 前一个步骤的其他逻辑代码......
if (!vnode.el) {
// 前一个步骤的其他逻辑代码......
} else {
// 如果当前索引在最长递增子序列中,则不需要移动
if (i == increasingSeq[j]) {
j--
} else {
hostInsert(vnode.el, el, anchor) // 倒序插入节点
}
}
}
}
}2.实现核心最长递增子序列算法函数 getSequence。利用贪心算法 + 二分查找求出哪些元素无需移动。
ts
function getSequence(arr) {
const result = [0]
const len = arr.length // 数组长度
const p = result.slice(0) // 存放当前节点的上一个节点的索引的,用于回溯
// 双指针,用于二分查找,查找性能更高
let start
let end
let middle
for (leti = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
// 为了 vue3 而处理掉数组中 0 的情况
// 拿出结果集对应的的最后一项,和我当前的这一项来作比对
let resultLastIndex = result[result.length - 1]
// 这是处理比较简单的情况,数组就是依次递增的。如果当前项比结果集最后一项大,则直接放入结果集即可
if (arr[resultLastIndex] < arrI) {
result.push(i) // 直接将当前的索引i放入到结果集即可
p[i] = result[result.length - 1] // 记录前一个节点的索引
continue
}
}
start = 0
end = result.length - 1
// 二分查找,用于当数组数字乱序的情况
while (start < end) {
// 取整,取中间值
middle = ((start + end) / 2) | 0
if (arr[result[middle]] < arrI) {
start = middle + 1
} else {
end = middle
}
}
if (arrI < arr[result[start]]) {
p[i] = result[start - 1] // 记录前一个节点的索引
result[start] = i
}
// p为前驱节点的列表,需要根据最后一个节点做追溯
let l = result.length
let last = result[l - 1] // 取出最后-项
while (l-- > 0) {
result[l] = last
last = p[last] // 在数组中找到最后-个
}
}
return result
}至此 vue3 中的全量 diff(递归diff)已经完成。但 vue3 中分为两种:全量 diff(递归 diff)以及快速diff(靶向更新)-> 基于模板编译实现(即在编译时就给那些静态节点当上标记(静态标记),到时候运行时只需要对动态节点进行 diff 即可,进一步提高性能)
Fragment
1.创建 Fragment 类型,并在 patch 时添加 Fragment 情况的处理函数 processFragment
ts
// 创建 Fragment 类型,方便后续对比
export const Fragment = Symbol("Fragment")
// 比对(patch 函数)时处理 Fragment
switch (type) {
case Text:
processText(n1, n2, container)
break
case Fragment:
processFragment(n1, n2, container)
break
default:
processElement(n1, n2, container, anchor) // 对元素处理
}2.实现 processFragment
ts
const processFragment = (n1, n2, container) => {
// 没有老节点时直接将 Fragment 包裹的子节点渲染到指定容器钟
if (n1 == null) {
mountChildren(n2.children, container)
} else {
// 如果有老节点进行 diff 比较
patchchildren(n1, n2, container)
}
}3.处理卸载的情况
ts
const unmount = vnode => {
// 当 Fragment 需要卸载时,直接循环遍历 Fragment 包裹的子节点依次卸载
if (vnode.type === Fragment) {
unmountChildren(vnode.children)
}else{
hostRemove(vnode.el)
}
}组件渲染
使用
ts
// 这就是一个组件
const VueComponent = {
data() {
return { name: 'jw', age: 30 }
},
render() {
//this ==组件的实例内部不会通过类来产生实例了
return h(Fragment, [h(Text, 'my name is ' + this.name), h('a', this.age)])
}
}
// 组件两个虚拟节点组成 h(VueComponent) = vnode 产生的是组件内的虚拟节点
// render 函数返回的虚拟节点,这个虚拟节点才是最终要渲染的内容 = subTree
render(h(VueComponent), app)实现
1.创建 Vnode 时打上组件标识,并针对其做不同处理
ts
export function createVnode(type, props, children?) {
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT // 元素
: isobject(type)
// 如果传入的是一个对象(即 h 函数第一个参数)则为组件
? ShapeFlags.STATEFUL_COMPONENT // 组件
: 0
}
// path 函数
const patch = (n1, n2, container, anchor = null) => {
// 其他逻辑......
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(n1, n2, container, anchor) // 对元素处理
} else if (shapeFlag & ShapeFlags.COMPONENT) {
//对组件的处理,vue3中函数式组件,已经废弃了,没有性能节约
processComponent(n1, n2, container, anchor)
}
}2.组件挂载与更新逻辑
ts
const processComponent = (n1, n2, container, anchor) => {
if (n1 === null) {
// 组件的挂载
mountComponent(n2, container, anchor)
} else {
//组件的更新
}
}3.实现组件挂载 mountComponent
ts
const mountComponent = (vnode, container, anchor) => {
// 组件可以基于自己的状态重新渲染,组件就类似 effect
// 组件创建时获取组件状态以及渲染函数
const { data = () => {}, render } = vnode.type
const state = reactive(data()) // 将组件的状态变成响应式
// 创建组件实例
const instance = {
state,
vnode,
// 该组件的子节点
subTree: null,
// 是否挂载完成
isMounted: false,
// 组件更新方法
update: null
}
// 组件更新方法,数据更新后会重新执行
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 组件初次挂载
// 指定 render 函数的 this 为 state (用户在可以使用 this 访问数据),
const subTree = render.call(state, state)
// 创建子节点
patch(null, subTree, container, anchor)
instance.isMounted = true
instance.subTree = subTree
} else {
// 组件更新e
const subTree = render.call(state, state)
// diff 比较前后差异进行更新
patch(instance.subTree, subTree, container, anchor)
instance.subTree = subTree
}
}
// 创建一个 effect,当状态发生改变的时候,重新执行 effect
const effect = new ReactiveEffect(componentUpdateFn, () => update())
const update = (instance.update = () => effect.run())
// 组件挂载时后立即执行一次
update()
}异步更新
4.借助 gueueJob 批处理优化,即将多次更新合并为一次统一更新。
ts
const mountComponent = (vnode, container, anchor) => {
// 其他逻辑......
// 使用 gueueJob 函数包裹 update 实现批处理操作
const effect = new ReactiveEffect(componentUpdateFn, () => gueueJob(update))
}5.实现 gueueJob 方法。声明一个队列,多次执行 updata 时只会入队一次,再开启一个微任务将更新操作放入微任务中执行(不会立即执行)。
ts
const queue = [] // 缓存当前要执行的队列
let isFlushing = false // 是否正在执行队列
const resolvePromise = Promise.resolve()
// 如果同时在一个组件中更新多个状态 job 肯定是同一个
function gueueJob(job) {
if (!queue.includes(job)) {
queue.push(job) // 让任务入队列
}
if (!isFlushing) {
// 同时开启一个异步任务
// 通过 js 事件环的机制(先走宏任务再执行微任务),延迟更新操作,将更新操作放在微任务中
isFlushing = true
resolveProise.then(() => {
isFlushing = false
const copy = queue.slice(0) // 先拷贝再执行
queue.length = 0
copy.forEach(job => job())
copy.length = 0
})
}
}props及attrs实现
使用
ts
// 属性分两种:props(响应式的,需要组件显式声明,未显式声明的属性会放入 attrs 中,且会绑定到组件根容器之上)、attrs(开发环境是响应式的,但生产环境是非响应式的)
// 就相当于:<VueComponent :a=1 :b=2 name='jw' :age='30'/>
render(h(VueComponent, { a: 1, b: 2, name: 'jw', age: 30 }), app)6.修改上方第三步中的代码(mountComponent 函数),添加 props、attrs 等属性,并初始化组件 props
ts
const mountComponent = (vnode, container, anchor) => {
const { data = () => {}, render,props:propsoptions={} } = vnode.type
const instance = {
// ...其他属性...
props:{},
attrs:{},
propsoptions,
component:null
}
// 根据 propsoptions 来区分出 props,attrs
vnode.component = instance
// 初始化组件 props
initProps(instance, vnode.props)
}7.实现 initProps。将组件中声明接收的 props 赋值给 props,其余的赋值给 attrs,并将 props 变为响应式数据。
ts
function initProps(instance, rawProps) {
const props = {}
const attrs = {}
const propsoptions = instance.propsoptions || {} // 组件中定义的
if (rawProps) {
// 遍历组件 props 将组件中声明接收的 props 赋值给 props,其余的赋值给 attrs
for (let key in rawProps) {
if (key in propsoptions) {
props[key] = value
} else {
attrs[key] = value
}
}
}
instance.attrs = attrs
// 将 props 变为响应式(浅层)
instance.props = shalloReactive(props)
}组件中的代理对象
1.修改前面实现 mountComponent 函数的步骤中的代码。通过 proxy 创建一个代理对象,让用户能够通过 proxy 更方便的访问属性。
ts
render(proxy) {
// 用户可以通过 proxy 快速访问 props
// a 为该组件未声明接受的 props ,age 为组件通过 props 选项声明接受的属性
return h('div', [h(Text, 'my name is ' + proxy.$attrs.a), h('a', proxy.age)])
}ts
const mountComponent = (vnode, container, anchor) => {
// 其他逻辑代码......
const instance = {
state,
vnode,
// 其他属性......
// 用来代理 props attrs,data 让用户更方便的使用
proxy: null
}
// 做一个映射
const publicProperty = {
$attrs: instance => instance.attrs
// ...其他属性:$slots 等
}
instance.proxy = new Proxy(instance, {
// 由此可见开发时 data 和 props 属性中的名字不要重名
get(target, key) {
const { state, props } = target
// 查看 state 中是否有 key
if (state && hasOwn(state, key)) {
return state[key]
// 查看 props 中是否有 key
} else if (props && hasOwn(props, key)) {
return props[key]
}
const getter = publicProperty[key] // 通过不同的策略来访问对应的方法
if (getter) {
return getter(target)
}
},
set(target, key, value) {
const { state, props } = target
// 查看 state 中是否有 key
if (state && hasOwn(state, key)) {
state[key] = value
// 查看 props 中是否有 key
} else if (props && hasOwn(props, key)) {
// 我们用户可以修改属性中的嵌套属性(内部不会报错)但是不合法
console.warn('props are readonly')
return false
}
return true
}
})
// 组件更新方法,数据更新后会重新执行
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 将代理对象传入,用户就能够通过 proxy 更方便的访问属性
const subTree = render.call(instance.proxy, instance.proxy)
// 其他逻辑代码......
} else {
// 将代理对象传入,用户就能够通过 proxy 更方便的访问属性
const subTree = render.call(instance.proxy, instance.proxy)
// 其他逻辑代码......
}
}
// 其他逻辑代码......
}通过属性进行组件更新
组件更新分为三种情况:组件内 data 、组件 props、组件插槽。前面的步骤已经实现第一种情况。接下来实现第二种。
用户代码
ts
// VueComponent 组件中渲染了一个按钮和 RenderComponent 组件并传递了 address 属性,当 VueComponent 组件发生改变即传递的 props 发生改变那么 RenderComponent 也需要更新
const RenderComponent = {
props: {
address: String
},
render() {
return h(Text, this.address)
}
}
const VueComponent = {
data() {
return { flag: true }
},
render() {
return h(Fragment,null, [
h('button', { onClick: () => (this.flag = !this.flag) }, '点我啊'),
h(RenderComponent, { address: this.flag ? '北京' : '上海' })
])
}
}1.回到组件渲染实现的第一步的 processComponent 函数中添加 updateComponent 方法实现组件更新。
ts
const processComponent = (n1, n2, container, anchor) => {
if (n1 === null) {
// 组件的挂载
mountComponent(n2, container, anchor)
} else {
// 组件的更新
updateComponent(n1,n2)
}
}2.实现 updateComponent。比较新老属性变化并将其更新,多余的删除,由于 props 在前面的步骤已经被包装为响应式数据,此时比较更新后,组件也会更新。
ts
function updateComponent(n1, n2) {
const instance = (n2.component = n1.component) // 复用组件的实例
// 获取老新属性
const { props: prevProps } = n1
const { props: nextProps } = n2
// 比较老新属性差异
updataProps(instance, prevProps, nextProps)
}
function hasPropsChange(prevProps, nextProps) {
const nKeys = Object.keys(nextProps)
// 先看属性个数是否发生变化
if (nKeys.length !== Object.keys(prevProps).length) return true
// 再遍历依次比较属性值是否发生变化
for (let i = 0; i < nKeys.length; i++) {
const key = nKeys[i]
if (nextProps[key] !== prevProps[key]) {
return true
}
}
return false
}
function updataProps(instance, prevProps, nextProps) {
// 比较属性是否发生变化
if (hasPropsChange(prevProps, nextProp)) {
//看属性是否存在变化
for (let key in nextProps) {
// 用新的覆盖掉所有老的
instance.props[key] = nextProps[key]
}
for (let key in instance.props) {
// 删除老的多于的
if (!(key in nextProps)) {
delete instance.props[key]
}
}
}
}setup 函数
每个组件只会执行一次,可以书写 compositionApi
ts
// 基本使用
// 平时开发时一般很少直接写 render 函数,一般通过模板书写内容,再通过构建工具将模板转换成 render 函数进行渲染
const VueComponent = {
setup(props, { emit, attrs, expose, slots }) {
const a = ref(1)
// 提供渲染逻辑
setTimeout(() => {
a.value = 2
}, 1000)
return {
// setup 可以返回函数亦可以返回对象
a: a.value
}
// 可返回函数
// return () => {
// return h('div', a.value)
// }
},
// 当 setup 返回函数时,该组件以内容以 setup 返回内容渲染优先级高于此处组件的 render
render(proxy) {
return h('div', proxy.a)
}
}1.修改 setupComponent 函数。结构出组件传递的 setup 函数,并根据其返回值渲染组件
ts
export function setupComponent(instance) {
const { vnode } = instance
//赋值属性
initProps(instance, vnode.props)
//赋值代理对象
instance.proxy = new Proxy(instance, handler)
const { data = () => {}, render, setup } = vnode.type
if (setup) {
const setupcontext = {
// 这里组合 setup 的上下文,即用户使用时 setup 第二个参数,里面放着:attrs、slots、emit、expose 等
}
const setupResult = setup(instance.props, setupcontext)
if (isFunction(setupResult)) {
// 如果 setup 返回一个函数,则认为这个函数是 render 函数
instance.render = setupResult
} else {
// 否则,认为 setup 返回的就是组件的 state,并通过 proxyRefs 方法将数据自动解包(自动 .value)
instance.setupState = proxyRefs(setupResult)
}
}
if (!isFunction(data)) {
console.warn('data option must be a function')
} else {
//data 中可以拿到props
instance.data = reactive(data.call(instance.proxy))
}
// 如果该组件实例上没有 render 函数(setup 可能返回函数取代组件 render 函数),才会取用组件的 render 函数
if (!instance.render) {
instance.render = render
}
}插槽
基本使用
ts
const RenderComponent = {
render(proxy) {
// proxy.$slots.footer 传参表示作用域插槽
return h(Fragment, [proxy.$slots.footer('fff'), proxy.$slots.header('hhh')])
}
}
const VueComponent = {
setup(props, { emit, attrs, expose, slots }) {
return proxy => {
return h(RenderComponent, null, {
header: t => h('header', 'header' + t),
footer: t => h('footer', 'footer' + t)
})
}
}
}1.修改 createVnode 方法(上方 h 方法实现的第二步),添加组件插槽标识
ts
function createVnode(type, props, children?) {
// 其他逻辑......
if (children) {
if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
}else if(isobject(children)){
vnode.shapeFlag |= ShapeFlags.SLOTS_CHILDREN;// 添加插槽标识
}else {
children = String(children)
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
}
}
}2.修改 mountComponent 方法(上方组件中的代理对象步骤),在组件实例中添加 slots 属性用于保存组件插槽
ts
const mountComponent = (vnode, container, anchor) => {
// 其他逻辑代码......
const instance = {
// 在组件实例中添加 slots 属性用于保存组件插槽
slots:{}
// 其他属性......
}
// 其他逻辑代码......
}3.修改 setupComponent 方法(上方 setup 函数实现第一个步骤),添加 initSlots 方法
ts
export function setupComponent(instance) {
const { vnode } = instance
initProps(instance, vnode.props)
// 添加 initSlots 方法
initSlots(instance, vnode.children)
instance.proxy = new Proxy(instance, handler)
// 其他逻辑代码......
}4.实现 initSlots 方法。判断当前虚拟节点上是否有插槽标识,如果有就将 children 赋值给 instance.slots
ts
export function initslots(instance, children) {
// 判断当前虚拟节点上是否有插槽标识,如果有就将 children 赋值给 instance.slots。组件的儿子就是插槽
if (instance.Vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
instance.slots = children
} else {
instance.slots = {}
}
}5.修改 mountComponent 方法(上方组件中的代理对象步骤)。添加 $slots 映射,方便用户直接通过 $slots 快速访问组件插槽
ts
const mountComponent = (vnode, container, anchor) => {
// 其他逻辑代码......
const publicProperty = {
$attrs: instance => instance.attrs
$slots:instance => instance.slots
}
// 其他逻辑代码......
}6.修改 mountComponent 方法(上方组件中的代理对象步骤)。修改 setupcontext (也就是用户使用 setup 函数的第二个参数)添加 slots 供用户快速访问。
ts
export function setupComponent(instance) {
// 其他逻辑代码......
if (setup) {
// 这里组合 setup 的上下文,即用户使用时 setup 第二个参数,里面放着:attrs、slots、emit、expose 等
const setupcontext = {
slots: instance.slots,
attrs: instance.attrs
}
const setupResult = setup(instance.props, setupcontext)
}
}emit
用户代码。写入了一个自定义事件 onMyEvent 并在组件中通过 emit 触发
ts
const VueComponent = {
setup(props, { emit }) {
return proxy => {
return h('button', { onClick: () => emit('myEvent', 100) },"按钮")
}
}
}
// 给组件绑定事件<div@myEvent>
render(h(VueComponent, { onMyEvent: value => alert(value) }), app)1.修改 mountComponent 方法(上方组件中的代理对象步骤)。在 setupcontext 中添加 emit ,先获取用户传递的函数名,再去组件实例上找到该函数执行。
ts
export function setupComponent(instance) {
// 其他逻辑代码......
if (setup) {
const setupcontext = {
slots: instance.slots,
attrs: instance.attrs,
emit(event, ...payload) {
// 先拼接 “on” 再将函数名首字母大写,最终就会得到 “onMyEvent”,再去组件实例上找到该函数执行
const eventNamee = `on${event[0].toUpperCase() + event.slice(1)}`
const handler = instance.vnode.props[eventName]
handler && handler(...payload)
}
}
const setupResult = setup(instance.props, setupcontext)
}
}组件生命周期
使用
ts
// 父初始化 => 子初始化 => 父完成
// 父更新 => 子更新 => 子完成 => 父完成
const my = {
props: {
value: String
},
setup(props) {
console.log('子 setup')
onBeforeMount(() => {
console.log('子 beforemount')
})
onMounted(() => {
console.log('子 mounted')
})
onBeforeUpdate(() => {
console.log('子 beforeupdate')
})
onUpdated(() => {
cnsole.log('子 updated')
})
return () => {
return h('div', props.value)
}
}
}
const VueComponent = {
setup() {
console.log('setup')
onBeforeMount(() => {
console.log('beforemount')
})
onMounted(() => {
console.log('mounted')
})
onBeforeUpdate(() => {
console.log('beforeupdate')
})
onUpdated(() => {
cnsole.log('updated')
})
const val = ref('a')
setTimeout(() => {
val.value = 'b'
}, 1000)
return () => {
return h(my, { value: val.value })
}
}
}1.先列举出所有生命周期钩子名称,再通过 createHook (函数柯里化)创建对应钩子函数。
ts
// 列举生命周期钩子名称
export const enum Lifecycle {
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u'
}
function createHook(type) {}
// 创建生命周期钩子
export const onBaforeMount = createHook(LifeCycles.BEFORE_MOUNT)
export const onMounted = createHook(LifeCycles.MouNTED)
export const onBeforeUpdate = createHook(LifeCycles.BEFORE_UPDATE)
export const onUpdated = createHook(LifeCycles.UPDATED)2.实现 getCurrentInstance 、setCurrentInstance 、unsetCurrentInstance 方法
ts
// 定义一个全局变量,用来存储当前实例
export let currentInstance = null
// 获取当前组件实例
export const getCurrentInstance = () => {
return currentInstance
}
// 设置当前组件实例
export const setCurrentInstance = instance => {
currentInstance = instance
}
// 重置
export const unsetCurrentInstance = () => {
currentInstance = null
}3.修改 setupComponent 方法(上方 实现 setup 函数的步骤)。在执行组件 setup 函数之前保存当前组件实例,这样在 setup 中就可以获取到当前组件实例
ts
function setupComponent(instance) {
// 其他逻辑代码......
// 在执行组件 setup 函数之前将当前组件实例(instance)记录下来
setCurrentInstance(instance)
const setupResult = setup(instance.props, setupcontext)
// 执行完之后再重置
unsetCurrentInstance()
}4.实现 createHook 方法。此时可以获取到组件实例让其和钩子产生关联。将用户写入的对应生命周期保存至当前组件实例上。
ts
function createHook(type) {
// 获取全局保存的组件实例,利用闭包将当前的实例存到了此钩子上
return (hook, target = currentInstance) => {
if (target) {
// 确保当前钩子是在组件中运行的,因为只有这样才能获取当前组件实例
// 看当前钩子是否在当前组件实例上存放过
const hooks = target[type] || (target[type] = [])
// 让currentInstance 存到这个函数内容
const wrapHook = () => {
// 在钩子执行前,对实例进行校正处理,即重新设置一下当前组件实例,以解决用户在生命周期钩子中调用 getCurrentInstance 获取组件实例时获取错误问题
setCurrentInstance(target)
// 确保生命周期钩子内的 this 指向为当前组件实例
hook.call(target);
// 钩子执行完毕后,再重置
unsetCurrentInstance()
}
// 存入数组中,用户可能会在组件中写入多个相同的钩子,所以需要用数组保存
hooks.push(wrapHook(hook))
}
}
}5.修改 mountComponent (上方组件渲染实现步骤三),分别在组件创建、更新阶段获取当前组件实例上的对应生命周期钩子依次遍历执行。
ts
const mountComponent = (vnode, container, anchor) => {
// 其他逻辑代码......
// 组件更新方法,数据更新后会重新执行
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 组件初次挂载后从实例上获取生命周期钩子(用户写入的钩子)
const { bm, m } = instance
if (bm) {
// 获取用户组件中写入的所有 onBaforeMount 钩子遍历依次执行
invokeArray(bm)
}
// 其他逻辑代码......
} else {
const { next, bu, u } = instance
if (bu) {
// 获取用户组件中写入的所有 onBeforeUpdate 钩子遍历依次执行
invokeArray(bu)
}
// 组件更新。diff 比较前后差异进行更新逻辑代码......
if (u) {
// 获取用户组件中写入的所有 onBeforeUpdate 钩子遍历依次执行
invokeArray(u)
}
}
}
}