Skip to content

基本介绍

核心设计思想

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) // true

4.使用 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 不脏时才会重新执行,否则直接返回老值
    ifthis.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(sourcecb,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(sourceoptions = {}){
    // 没有 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(eltext){
    	el.textContent = text;
    },
    // 如何插入节点
    insert(elcontainer){
    	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)
            }
        }
    }
    }