Skip to content

Vue3源码

三大核心系统

虚拟 DOM 优势

  • 首先是可以对真实的元素节点进行抽象,抽象成 VNode(虚拟节点),这样方便后续对其进行各种操作
    • 对于直接操作 DOM 来说是有很多限制的,比如:diff、clone 等,但是使用 JavaScript 来操作就变得非常简单
    • 我们可以使用 JavaScript 来表达非常多的逻辑,对于 DOM 本身来说是非常不方便的
  • 其次是方便实现跨平台,包括你可以将 VNode 节点渲染成任意你想要的节点
    • 如渲染在 canvas、WebGL、SSR、Native(IOS、Android)上
    • 并且 Vue 允许你开发属于自己的渲染器(renderer),在其他的平台上渲染

虚拟 DOM 优势渲染过程

image-20220805111511820

image-20220805111516913

三大核心系统

  • Compiler 模块:编译模板系统
  • Runtime 模块:也可以称之为 Renderer 模块,真正渲染的模块
  • Reactivity 模块:响应式系统

image-20220805111651788

三个系统之间如何协调工作?

image-20220805141457586

Mini-Vue

Vue 包括三个模块:

  • 渲染系统模块
  • 响应式系统模块
  • 应用程序入口模块

渲染系统实现

  • 功能一:h 函数,用于返回一个 VNode 对象
  • 功能二:mount 函数,用于将 VNode 挂载到 DOM 上
  • 功能三:patch 函数,用于对两个 VNode 进行对比,决定如何处理新的 VNode

h 函数

  • 直接返回一个 VNode 对象即可
js
const h = (tag, props, children) => {
  // vnode -> javascript 对象
  return {
    tag,
    props,
    children
  }
}

mount 函数(挂载 VNode)

  • 第一步:根据 tag,创建 HTML 元素,并且存储到 vnode 的 el中
  • 第二步:处理 props 属性
    • 如果以 on 开头,那么监听事件
    • 普通属性直接通过 setAttribute 添加即可
  • 第三步:处理子节点
    • 如果是字符串节点,那么直接设置 textContent
    • 如果是数组节点,那么遍历调用 mount 函数
js
const mount = (vnode, container) => {
  // vnode -> element
  // 1.创建出真实的原生, 并且在 vnode 上保留 el
  const el = (vnode.el = document.createElement(vnode.tag))

  // 2.处理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      const value = vnode.props[key]

      if (key.startsWith('on')) {
        // 对事件监听的判断
        el.addEventListener(key.slice(2).toLowerCase(), value)
      } else {
        el.setAttribute(key, value)
      }
    }
  }

  // 3.处理 children
  if (vnode.children) {
    if (typeof vnode.children === 'string') {
      el.textContent = vnode.children
    } else {
      vnode.children.forEach(item => {
        mount(item, el)
      })
    }
  }

  // 4.将 el 挂载到 container 上
  container.appendChild(el)
}

patch 函数(对比两个 VNode)

patch 函数的实现,分为两种情况

  • n1 和 n2 是不同类型的节点
    • 找到 n1 的 el 父节点,删除原来的 n1 节点的 el
    • 挂载 n2 节点到 n1 的 el 父节点上
  • n1 和 n2 节点是相同节点
    • 处理 props 的情况
      • 先将新节点的 props 全部挂载到 el 上
      • 判断旧节点的 props 是否不需要在新节点上,如果不需要,那么删除对应的属性
    • 处理 children 的情况
      • 如果新节点是一个字符串类型,那么直接调用 el.textContent = newChildren
      • 如果新节点不是一个字符串类型
        • 旧节点是一个字符串类型
          • 将 el 的 textContent 设置为空字符串
          • 旧节点是一个字符串类型,那么直接遍历新节点,挂载到 el 上
        • 旧节点也是一个数组类型
          • 取出数组的最小长度
          • 遍历所有的节点,新节点和旧节点进行 patch 操作
          • 如果新节点的 length 更长,那么剩余的新节点进行挂载操作
js
const patch = (n1, n2) => {
  if (n1.tag !== n2.tag) {
    const n1ElParent = n1.el.parentElement
    n1ElParent.removeChild(n1.el)
    mount(n2, n1ElParent)
  } else {
    // 1.取出 element 对象,并在 n2 中进行保存
    const el = (n2.el = n1.el)

    // 2.处理 props
    const oldProps = n1.props || {}
    const newProps = n2.props || {}
    // 2.1.获取所有的 newProps 添加到 el
    for (const key in newProps) {
      const oldValue = oldProps[key]
      const newValue = newProps[key]
      if (newValue !== oldValue) {
        if (key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), newValue)
        } else {
          el.setAttribute(key, newValue)
        }
      }
    }
    // 2.2.删除旧的 props
    for (const key in oldProps) {
      if (key.startsWith('on')) {
        const value = oldProps[key]
        el.removeEventListener(key.slice(2).toLowerCase(), value)
      }
      if (!key in newProps) {
        el.removeAttribute(key)
      }
    }

    // 3.处理children
    const oldChildren = n1.children || []
    const newChildren = n2.children || []

    if (typeof newChildren === 'string') {
      // 情况一: newChildren 本身是一个 string
      // 边界情况 (edge case)
      if (typeof oldChildren === 'string') {
        if (newChildren !== oldChildren) {
          el.textContent = newChildren
        }
      } else {
        el.innerHTML = newChildren
      }
    } else {
      // 情况二: newChildren 本身是一个数组
      if (typeof oldChildren === 'string') {
        el.innerHTML = ''
        newChildren.forEach(item => {
          mount(item, el)
        })
      } else {
        // oldChildren: [v1, v2, v3, v8, v9]
        // newChildren: [v1, v5, v6]
        // 1.前面有相同节点的元素进行 patch 操作
        const commonLength = Math.min(oldChildren.length, newChildren.length)
        for (let i = 0; i < commonLength; i++) {
          patch(oldChildren[i], newChildren[i])
        }

        // 2.newChildren.length > oldChildren.length
        if (newChildren.length > oldChildren.length) {
          newChildren.slice(oldChildren.length).forEach(item => {
            mount(item, el)
          })
        }

        // 3.newChildren.length < oldChildren.length
        if (newChildren.length < oldChildren.length) {
          oldChildren.slice(newChildren.length).forEach(item => {
            el.removeChild(item.el)
          })
        }
      }
    }
  }
}

响应式系统模块

Dep

js
class Dep {
  constructor() {
    this.subscribers = new Set()
  }
  addEffect(effect) {
    this.subscribers.add(effect)
  }
  notify() {
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}

const info = { counter: 100 }
const dep = new Dep()

function doubleCounter() {
  console.log(info.counter * 2)
}
function powerCounter() {
  console.log(info.counter * info.counter)
}
dep.addEffect(doubleCounter)
dep.addEffect(powerCounter)
info.counter++
dep.notify()

添加 Effect 方法重构

js
class Dep {
  constructor() {
    this.subscribers = new Set()
  }
  /* addEffect(effect) {
    this.subscribers.add(effect)
  } */
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  notify() {
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}

let activeEffect = null
function watchEffect(effect) {
  activeEffect = effect
  dep.depend()
  effect()
  activeEffect = null
}

const info = { counter: 100 }
const dep = new Dep()

watchEffect(function () {
  console.log(info.counter * 2)
})
watchEffect(function () {
  console.log(info.counter * info.counter)
})
info.counter++
dep.notify()

reactive 和 getDep 的实现

Map({ key: value }):key 可以是任意类型

WeakMap({ key(对象): value }):key只能将对象作为键名

  • 对 key 的引用是弱引用,不影响垃圾回收器的工作

    只要对应 key 置为 null,其 value 就不存在了,规避内存泄露的风险

    js
    let button = document.getElementById('button')
    let wm = new WeakMap()
    wm.set(button, { count: 0 })
    
    function buttonClick() {
      let data = wm.get(button)
      data.count += 1
      console.log(data.count)
    }
    button.addEventListener('click', buttonClick)
    button.removeEventListener('click', buttonClick)

    正因为 weackMap 对键名引用的对象是弱引用关系,因此 weakMap 内部成员是会取决于垃圾回收机制有没有执行,运行前后成员个数可能是不一样的,而垃圾回收机制的执行又是不可预测的,因此不可遍历

  • 可以做私有成员

    js
    let Stack = (function () {
      let vm = new WeakMap()
      return class {
        constructor() {
          vm.set(this, [])
        }
        push(el) {
          vm.get(this).push(el)
        }
        toString(el) {
          console.log(vm.get(this))
        }
      }
    })()
    let stack = new Stack()
    stack.push(1)
    stack.push(2)
    console.log(stack) // {}
    stack.toString() // [1, 2]
js
class Dep {
  constructor() {
    this.subscribers = new Set()
  }
  depend() {
    if (activeEffect) {
      this.subscribers.add(activeEffect)
    }
  }
  notify() {
    this.subscribers.forEach(effect => {
      effect()
    })
  }
}

let activeEffect = null
function watchEffect(effect) {
  activeEffect = effect
  effect()
  activeEffect = null
}

const targetMap = new WeakMap()
function getDep(target, key) {
  // 1.根据对象取出对应的Map对象
  let depsMap = target.get(target)
  if (depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  // 2.取出具体的dep对象
  let dep = depsMap.get(key)
  if (!dep) {
    dep = new Dep()
    depsMap.set(key, dep)
  }
  return dep
}

function reactive(raw) {
  Object.keys(raw).forEach(key => {
    const dep = new Dep()
    let value = raw[key]
    Object.defineProperty(raw, key, {
      get() {
        dep.depend()
        return value
      },
      set(newValue) {
        if (value !== newValue) {
          value = newValue
          dep.notify()
        }
      }
    })
  })
  return raw
}

const info = reactive({ counter: 100, name: 'cat' })
const foo = reactive({ height: 1.88 })

watchEffect(function () {
  console.log('effect1', info.counter * 2, info.name)
})
watchEffect(function () {
  console.log('effect2', info.counter * info.counter)
})
watchEffect(function () {
  console.log('effect3', info.counter * 2, info.name)
})
watchEffect(function () {
  console.log('effect4', foo.height)
})
// info.counter++
info.name = 'bird'
/*
effect1 200 cat
effect2 10000
effect3 200 cat
effect4 1.88
effect1 200 bird
effect3 200 bird
*/

proxy

vue2 使用 Object.defineProperty劫持 data 属性中的 getter 和 setter,在数据变动时发布消息给订阅者

Object.defineProperty 劫持对象的属性时,如果新增元素:

  • Vue2 需要再次调用 defineProperty,而 Proxy 劫持的是整个对象,不需要做特殊处理

修改对象的不同:

  • 使用 defineProperty 时,我们修改原来的 obj 对象就可以触发拦截
  • 而使用 Proxy,就必须修改代理对象,即 Proxy 的实例才可以触发拦截

Proxy 能观察的类型比 defineProperty 更丰富

  • has:in 操作符的捕获器
  • deleteProperty:delete 操作符的捕捉器

Proxy 作为新标准将受到浏览器厂商重点持续的性能优化

  • 缺点:不兼容 IE,也没有 polyfill
  • deleteProperty 能支持到 IE9
js
function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const dep = getDep(target, key)
      dep.depend()
      return Reflect.get(target, key)
    },
    set(target, key, newValue) {
      const dep = getDep(target,key)
      const result = Reflect.set(target, key)
      dep.notify()
      return result
    }
  })
}

应用程序入口模块

js
function createApp(rootComponent) {
  return {
    mount(selector) {
      const container = document.querySelector(selector)
      let isMounted = false
      let oldVNode = null

      watchEffect(function () {
        if (!isMounted) {
          oldVNode = rootComponent.render()
          mount(oldVNode, container)
          isMounted = true
        } else {
          const newVNode = rootComponent.render()
          patch(oldVNode, newVNode)
          oldVNode = newVNode
        }
      })
    }
  }
}

源码阅读

createApp 入口

image-20220809175617424

packages/runtime-dom/src/index.ts

  • 先拿到渲染器(对象),里面有一个属性:createApp

image-20220810093027665

packages/runtime-core/src/renderer.ts

  • 渲染器对象,最终会返回一个对象,里面带有:createApp

image-20220810093308839

packages/runtime-core/src/apiCreateApp.ts

  • 里面定义了 app 对象,有 mixin、component、mount 等方法

image-20220810093728121

调用 mount 时,首先会使用 createVNode 来创建 vnode 对象,之后调用 render 渲染 vnode

  • 根据传入的 App 创建一个 VNode

    组件 -> VNode -> 真实 DOM

  • 渲染 VNode

    render(vnode, container)

image-20220810095441323

mount 流程

image-20220815105448023

packages/runtime-core/src/renderer.ts

patch 函数

  • 判断类型,因为第一次传的是一个根组件(<App/>),会走 processComponent 来处理组件函数

image-20220810170528994

  • processComponent 会判断旧的 vnode 是否挂载过,第一次是没有挂载的,会调用 mountComponent 来处理组件函数

image-20220810171158001

组件的 VNode 和 instance 区别:

  • 组件的 VNode:虚拟 DOM 里的虚拟节点
  • 组件的 instance:保存组件的各种状态

mountComponent 会创建组件实例,并且初始化 props、data 等数据,接下来会调用 setupRenderEffect

image-20220810141337640

setupRenderEffect 这里会调用 effet 函数(当数据发生变化时就会重新执行),函数里面判断组件是否挂载,没有则挂载组件,有则更新组件。第一次没有挂载会走挂载操作

image-20220815102627313

挂载操作会拿到组件 subTree,在调用 patch 方法

image-20220815103221789

patch 方法会判断节点的类别,如果有根会直接调用 processElement 挂载元素,如果没有根会当成 Fragment 来处理

image-20220815103610369

有根会调用 mountElement 挂载节点

image-20220815103759036

mountElement 首先会调用 hostCreateElement 创建元素

image-20220815104005602

如果里面有 children 会调用 mountChildren 挂载,最终会把 el 挂在到 container 上

image-20220815104307691

组件的初始化

image-20220816085744114

packages/runtime-core/src/component.ts

  1. 处理 props 和 attrs

    instance.propsinstance.attrs

  2. 处理 slots

    instance.slots

  3. 执行 setup

    instance.setupState = proxyRefs(result)

  4. 编译 template

    template -> render

  5. 对 vue2 的 options api 进行支持

image-20220816090422391

处理完 props、slots 会设置有状态的组件,调用 setup 函数,执行 handleSetupResult

image-20220816091049474

setup 会把结果放到 setupState 里

image-20220816091218816

调用 compiler 函数

image-20220816092250624

对 vue2.x 的 options api 进行支持

  • applyOptions 里处理生命周期、methods、computed...

image-20220816092322882

Compile 过程

image-20220816094017751

image-20220816104831709

Scope Hoisting、Block Tree -> Vue3 性能优化

  • 在 Vue 中更新是组件级别的,在下一次更新时会重新执行 render 函数,如果在 render 函数里面写 createVNode,每次都会执行会浪费性能
  • 所以 Vue3 对此进行优化,对于不会改变的静态节点进行了作用域的提升
  • render 函数在 return 时进行了 openBlock 操作,它会创建一个数组,会将可能修改的元素放到这个数组中 [divVNode, buttonVNode] -> dynamicChildren,之后 diff 时只 diff 可能会修改的元素即可

生命周期回调

image-20220816112556064

image-20220816112855743

v2 v3 对比

https://v3-migration.vuejs.org/

template 中数据的使用顺序

vue3

  • packages/runtime-core/src/component.ts

image-20220816141515107

这里解构 ctx、setupState 等变量

image-20220816142657015

那这里的 ctx 是什么呢?

  • packages/runtime-core/src/componentOptions.ts
  • methods、data、computed 等都放在 ctx 中

setupState 是 setup 的返回结果

image-20220816142031574

最后通过一个 if 进行判断,取值优先级顺序如下:

  1. setup
  2. data
  3. props
  4. context

image-20220816142601505

Vue3 可以进行代码测试:

html
<div id="app"></div>

<script src="https://unpkg.com/vue@3"></script>
<script>
  const Son = {
    template: `<div>{{message}}</div>`,
    props: {
      message: String
    },
    data() {
      return {
        message: 'data'
      }
    },
    computed: {
      message() {
        return 'computed'
      }
    },
    setup() {
      const message = Vue.ref('setup')
      return { message }
    }
  }

  const app = Vue.createApp({
    components: { Son },
    template: `<Son message="props"></Son>`,
  }).mount('#app')
</script>

vue2

  • src/core/instance/state.ts

初始化顺序:propssetupmethodsdatacomputedwatch

image-20221104151050114

v-for v-if 优先级

vue2

  • src/compiler/codegen/index.ts

v-for 优先级比 v-if

image-20221104152043917

vue3

v-if 优先级比 v-for

常备不懈,才能有备无患