写给小白(自己)的vue3源码导读!

2,368 阅读24分钟

大纲

目前社区有很多 Vue3 的源码解析文章,但是质量层次不齐,不够系统和全面,总是一个知识点一个知识点的解读,这样我在拜读中,会出现断层,无法将整个vue3的知识体系融合,于是只能自己操刀来了

并且自己建了一个github 我会将我在源码中的一些注释,以及我对源码的一些理解,尽数放到此github中,既是为了自己能温故而知新,也能方便后来的小伙伴能更快的了解vue的整个体系

github地址奉上vue 源码解析 v3.2.26

image.png

项目中包含思维导图(后续会慢慢更新),源码注释,简版手写原理,以及帮助理解的文章,希望大家手动star

言归正传,我们这是要做源码导读,这次源码导读,将会分为一下几个方面:

  1. 工程化
  2. 组件化
  3. 渲染器
  4. 响应式系统
  5. 编译器
  6. 为什么要看vue源码

我们一个个来分析

工程化

learn(monorepo)

Monorepo的意思是在版本控制系统的单个代码库里包含了许多项目的代码。这些项目虽然有可能是相关的,但通常在逻辑上是独立的,并由不同的团队维护。 想要理解他到底是个是,我们用一张图彻底理解

image.png 如上图所示,大致意思就是一个仓库管理者很多的包,他们可以统一打包统一发布,也可以分开打包分开发布,并且由不同的人维护,这也是目前很多开源库所采用的方案

roullp

Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,也是目前库的打包首选bundler

Typescirpt

Typescirpt自不用多说,社区火爆

CICD

每个项目不一样,感兴趣的可以参考

依赖包管理

pnpm - 速度快、节省磁盘空间的软件包管理器

单元测试

Jest 是一个令人愉快的 JavaScript 测试框架,专注于 简洁明快

代码校验

ESLint可组装的JavaScript和JSX检查工具

目录结构

image.png

这里我们主要介绍几个主要的核心库,本身看源码的就无需事无巨细,我们只需要了解主要的原理,以及能吸收一些优秀的代码设计和思想

learn 构建工程,所有的目录散在packages中,并且packages中的包也能单独使用比较重要的包

  • compiler-core(编译器的核心逻辑)
  • compiler-dom(dom 平台的编译器)
  • vue-compiler-sfc(解析SFC组件类似vue2中的vue-lorder)
  • reactivity(响应式)
  • runtime-core(运行时代码,包含渲染器,vnode ,调度器 )
  • runtime-dom (dom 平台相关)
  • vue(最后打出不同包的目录)
  • shared(初始化的一些变量啊,工具函数等等)

组件化

Vue3 另一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。

我们在用 Vue3开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。在 Vue.js 的官网中,也是花了大篇幅来介绍什么是组件,如何编写组件以及组件拥有的属性和特性。

当然组件化这个概念也不是vue3独有的,从很早以前就有这个就流传开来

组件的本质

JQuery 年代,模板引擎的概念,干的年头长的都应该听过。

举个例子

import { template } from 'lodash'

const compiler = template('<h1><%= title %></h1>')
const html = compiler({ title: 'My Component' })

document.getElementById('app').innerHTML = html

上述代码中就是lodash的一个模板引擎他的本质就是:模板+数据=HTML 相信很多老前端都知道当年前后端不分离的时代,套模板是大多数web站点的宿命。那个年代的模板srr方案其实本质就是模板引擎

而在现在的vue、react年代模板引擎的变了

模板+数据=Vdom 他引入了Virtual DOM的概念

在vue 3 中,我们的模板就会给抽象成render函数,这个render函数就是我们的模板,举个例子:

<div id="demo">
  <div @click="handle">
    点击切换
  </div>
  <div @click="handle1">
    点击切换1
  </div>
  <div v-if="falg">{{num}}</div>
  <div v-else>{{num1}}</div>
</div>

他最后编译的结果就会是这个样子

const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue

const _hoisted_1 = ["onClick"]
const _hoisted_2 = ["onClick"]
const _hoisted_3 = { key: 0 }
const _hoisted_4 = { key: 1 }

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, Fragment: _Fragment } = _Vue

    return (_openBlock(), _createElementBlock(_Fragment, null, [
      _createElementVNode("div", { onClick: handle }, " 点击切换 ", 8 /* PROPS */, _hoisted_1),
      _createElementVNode("div", { onClick: handle1 }, " 点击切换1 ", 8 /* PROPS */, _hoisted_2),
      falg
        ? (_openBlock(), _createElementBlock("div", _hoisted_3, _toDisplayString(num), 1 /* TEXT */))
        : (_openBlock(), _createElementBlock("div", _hoisted_4, _toDisplayString(num1), 1 /* TEXT */))
    ], 64 /* STABLE_FRAGMENT */))
  }
}

而render 函数执行的结果就应该是一个vdom

Virtual DOM

Virtual DOM 他就是个js 对象,比如

{
    tag: "div",
    props: {},
    children: [
        "Hello World", 
        {
            tag: "ul",
            props: {},
            children: [{
                tag: "li",
                props: {
                    id: 1,
                    class: "li-1"
                },
                children: ["第", 1]
            }]
        }
    ]
}

他对应的就是表达的dom

<div>
    Hello World
    <ul>
        <li id="1" class="li-1">
            第1
        </li>
    </ul>
</div>

至于为何组件要从直接产出 html 变成产出 Virtual DOM 呢?其原因是 Virtual DOM 带来了 分层设计,它对渲染过程的抽象,使得框架可以渲染到 web(浏览器) 以外的平台,以及能够实现 SSR 等 ,并不是Virtual DOM 的性能好

具体的请参考网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?

vue中的组件

在我们日常写的组件如下:


 <template>
    <div>
   	 这是一个组件{{num}}
    </div>
</template>
<script>
export default {
name:home
  setup(){
    const num=ref(1)
    return {num}
  }
};
</script>

编译后的结果如下:

const home={
	 setup(){
    const num=ref(1)
     function handleClick() {
        num.value++
      }
    	return {num,handleClick}
  },
  render(){
   with (_ctx) {
    const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
    return (_openBlock(), _createElementBlock("div", { onClick: handleClick }, " 这是一个组件" + _toDisplayString(num), 9 /* TEXT, PROPS */, _hoisted_1))
  }
  }
}

其实你发现他就是个配置对象,里面包含了数据,操作数据的方法,以及编译后的模板模板函数

而在我们使用的时候 给抽象成标签引用

<template>
  <div>
    <home></home>
    </div>
</template>

最后编译后的结果

const elementVNode = {
  tag: 'div',
  data: null,
  children: {
    tag: home,
    data: null
  }
}

如此以来,我们的组件就能参与到vdom中来,我们的整个页面就能渲染成一个包含组件的vdom树,这样就能通过搭积木的方式将很多个组件拼装为一个一个页面,就像下图一样:

image.png

渲染器

所谓渲染器,简单的说就是将 搭积木形成Virtual DOM 渲染成特定平台下真实 DOM 的工具(就是一个函数,通常叫 render),渲染器的工作流程分为两个阶段:mountpatch,如果旧的 VNode 存在,则会使用新的 VNode 与旧的 VNode 进行对比,试图以最小的资源开销完成 DOM 的更新,这个过程就叫 patch,或“打补丁”。如果旧的 VNode 不存在,则直接将新的 VNode 挂载成全新的 DOM,这个过程叫做 mount

在此之前我们先来看 Virtual DOM的种类(引用大佬的图

image-20220310195250119.png

对应的在vue中也通过二进制位的方式来表示vnode类型

export const enum ShapeFlags {
  ELEMENT = 1, // 普通节点
  FUNCTIONAL_COMPONENT = 1 << 1,//2 // 函数组件
  STATEFUL_COMPONENT = 1 << 2,//4 // 普通组件
  TEXT_CHILDREN = 1 << 3,//8 // 文本子节点
  ARRAY_CHILDREN = 1 << 4,//16 // 数组子节点
  SLOTS_CHILDREN = 1 << 5,//32
  TELEPORT = 1 << 6,//64 // 传送门
  SUSPENSE = 1 << 7,//128 // 可以在组件中异步
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,//256
  COMPONENT_KEPT_ALIVE = 1 << 9,//512// keepALIVE
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 6 表示函数组件和普通组件
}

而有了这些类型区分,我们就能通过不同的类型来执行不同的挂载逻辑以及patch 逻辑

  switch (type) {
      // 文本节点
      case Text:
        processText(n1, n2, container, anchor)
        break
      // 注释节点
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      // 静态节点, 这个应该是在ssr的时候用到的
      // 因为只有在ssr的时候才会常见static类型的vnode
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      // Fragment 片段
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
        // 去除了特殊情况节点的渲染,就是正常的vnode 渲染
        // 如果是个节点类型
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          // 如果是个组件类型
          // 第一次执行挂载时候也被当做组件类型初始化的
          // vue3改版之后直接用配置去常见对象去创建组件vnode
          // 这个配置需要用一个函数去拿,也是动态加载的
          // 传入名字在运行时去去通过resolvecompinent 来拿
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          // 如果是个传送门
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ; (type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
          // Suspense  实验性内容
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ; (type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        }
    }

image.png

那么到此,整个渲染器的主要方法patch的核心逻辑就解释到这了,我们意在了解整个源码的体系,以及脉络,不纠结具体实现,具体实现我们还需要在源码中找寻。

组件挂载

组件挂载 就是组件类型的vnode 节点的初始化 包含响应式初始化,依赖收集,生命周期,编译等,所以我们有必要来了解一下整个组件挂载的全过程,来研究一下组件挂载的流程,让我们更清晰的明白整个vue中的数据怎样和render函数绑定的。

我们抽离了渲染器的的核心逻辑意在解释整个组件的初始化的流程


        const nodeOps = {
            insert: (child, parent) => {
                parent.insertBefore(child, null)
            },
            createElement: (tag) => {
                return document.createElement(tag)
            },
            setElementText: (el, text) => {
                el.textContent = text
            },
        }
        // 这里我们只考虑 事件patch 的情况,其他暂不处理
        function patchEvent(el, key, val) {
            el.addEventListener(key.slice(2).toLowerCase(), function () {
                val()
            })
        }
        // 处理props
        function patchProp(el, key, val) {
            patchEvent(el, key, val)
        }
        // 判断是不是ref
        function isRef(r) {
            return Boolean(r && r.__v_isRef === true)
        }
        // 返回正确的值
        function unref(ref) {
            return isRef(ref) ? ref.value : ref
        }
        const toDisplayString = (val) => {
            return val == null
                ? ''
                : String(val)
        }
        const computed = Vue.computed
        Vue.computed = function (fn) {
            const val = computed(fn)
            // 模拟拿到effect
            recordEffectScope(val.effect)
            return val
        }
        const ref = Vue.ref
        Vue.ref = function (val) {
            const newRef = ref(val)
            // 模拟拿到effect
            recordEffectScope(val.effect)
            return newRef
        }
        // 建立关联的函数,当前函数在computed、watch、watchEffect等函数中使用
        function recordEffectScope(effect, scope) {
            scope = scope || activeEffectScope
            if (scope && scope.active) {
                scope.effects.push(effect)
            }
        }
        // vue的内部使用
        // 在页面内部使用EffectScope是为了在组件销毁的时候卸载依赖

        //当前实例
        let currentInstance = null
        // 设置当前实例
        const setCurrentInstance = (instance) => {
            currentInstance = instance
            instance.scope.on()
        }
        function parentNode(node) {
            return node.parentNode
        }
        // 容错处理
        function callWithErrorHandling(
            fn,
            instancel,
            args
        ) {
            let res
            try {
                res = args ? fn(...args) : fn()
            } catch (err) {
                console.error(err)
            }
            return res
        }
        // 建立响应式
        function proxyRefs(objectWithRefs) {
            return new Proxy(objectWithRefs, {
                get: (target, key, receiver) => {
                    return unref(Reflect.get(target, key, receiver))
                },
                set: (target, key, value, receiver) => {
                    return Reflect.set(target, key, value, receiver)
                }
            })
        }
        // 赋值render函数,咱们这里暂不处理编译相关,只赋值render 函数即可
        function finishComponentSetup(instance) {
            Component = instance.type
            // 中间可能有编译过程,暂时省略不处理
            instance.render = Component.render
        }
        function handleSetupResult(instance, setupResult) {
            instance.setupState = proxyRefs(setupResult)
            // 下方还有编译的内容
            finishComponentSetup(instance)
        }
        // 创建组件实例
        function createComponentInstance(vnode, parent) {
            // 拿到类型
            const type = vnode.type
            const instance = {
                vnode,
                type,
                parent,
                // 此时已经创建了一个EffectScope 为了批量处理依赖
                scope: new EffectScope(true /* detached */),
                render: null,
                subTree: null,
                effect: null,
                update: null,
            }
            return instance
        }
        // 组件类型的处理,里面包含mount和update 
        function processComponent(n1, n2, container) {
            if (n1 == null) {
                mountComponent(n2, container)
            } else {
                updateComponent()
            }

        }
        const processText = (n1, n2, container, anchor) => {
            if (n1 == null) {
                nodeOps.insert(
                    (n2.el = createText(n2.children)),
                    container
                )
            }
        }
        // 节点初始化
        function mountElement(n2, container, parentComponent) {
            const { type, props, children, shapeFlag } = n2
            let el = nodeOps.createElement(type)

            // 判断出来带文本子节点的内容
            if (n2.shapeFlag === 9) {
                nodeOps.setElementText(el, children)
            }
            // 如果有props 的情况
            if (props) {
                for (key in props) {
                    // 注意这里只处理事件的情况,暂不处理样式等情况
                    patchProp(el, key, props[key])
                }
            }
            nodeOps.insert(el, container)
        }
        // 节点类型的处理
        function processElement(n1, n2, container, parentComponent) {
            if (n1 == null) {
                // 如果第一次没有,那么就是走mountelement的类型
                mountElement(
                    n2,
                    container,
                    parentComponent
                )
            } else {
                // 接下来就是组件更新,走的是patch的内容,内部包含diff,也就是最核心的内容
            }
        }
        // 组件销毁方法
        function unmountComponent() {

        }
        //创建vnode  
        function createVNode(type, props, children, shapeFlag = 1) {
            if (typeof children === "string") {
                shapeFlag = 9
            }
            const vnode = {
                type,
                props,
                children,
                shapeFlag,
                component: null,
            }

            return vnode
        }
        // patch 包含各种组件,节点,注释等内容的挂载,这里为了分析依赖追踪,我们只分析组件的挂载
        function patch(n1, n2, container, parentComponent = null) {
            // 如果相等就返回表示没变
            if (n1 === n2) {
                return
            }
            const { type, shapeFlag } = n2
            switch (type) {

                // 前面逻辑省略,我们只处理组件类型和普通节点类型

                default:
                    if (shapeFlag & 1) {
                        processElement(n1, n2, container, parentComponent)
                    } else {
                        processComponent(n1, n2, container)
                    }


            }
        }
        function setupStatefulComponent(instance) {
            const Component = instance.type
            const { setup } = Component
            if (setup) {
                setCurrentInstance(instance)
                // 拿到setup结果
                const setupResult = callWithErrorHandling(
                    setup,
                    instance,
                    [instance.props]
                )
                // 这里我们只判断返回对象的情况,不考虑别的情况
                handleSetupResult(instance, setupResult)
            }
        }
        // 执行setup函数
        function setupComponent(instance) {
            // 上方的一些不重要的逻辑暂不处理,执行核心逻辑
            const setupResult = setupStatefulComponent(instance)
            return setupResult
        }
        // 拿到vnode
        function renderComponentRoot(instance) {
            // 源码中使用一个Proxy来代理所有的响应式内容的访问和修改 并且存入 instance.proxy中,统一处理
            // 我们这里只是为了梳理主流程 只需要setupState即可 porps 暂不考虑
            const {
                type: Component,
                render,
                setupState,
            } = instance
            debugger
            result = render.call(
                setupState,
                setupState,
            )
            return result
        }
        // 依赖收集 生成effect
        function setupRenderEffect(instance, n2, container) {
            const componentUpdateFn = () => {
                const nextTree = renderComponentRoot(instance)
                const prevTree = instance.subTree
                instance.subTree = nextTree
                // 这是组件内部的patch走diff 
                patch(
                    prevTree,
                    nextTree,
                    container,
                    instance
                )
            }
            // 这里直接调用内部的ReactiveEffect即可不用自己重写
            const effect = (instance.effect = new Vue.ReactiveEffect(
                componentUpdateFn,
                null,
                instance.scope // track it in component's effect scope 在组件的影响范围内跟踪它 依赖追踪使用
            ))
            const update = (instance.update = effect.run.bind(effect))
            update()
        }

        // 组件初始化方法
        function mountComponent(initialVNode, container, parentComponent = null) {
            const instance =
                (initialVNode.component = createComponentInstance(
                    initialVNode,
                    parentComponent,
                ))
            // 执行setup 
            setupComponent(instance)
            // 依赖收集
            setupRenderEffect(instance, initialVNode, container)
        }

        // 组件更新 
        function updateComponent() {
            // 更新逻辑咱暂时不看
        }
        // render 组件的初始化主要就是执行path
        function createApp(rootComponent, rootProps = null) {
            // 这里我们直接传值,源码中是根据type 去判断的
            const vnode = createVNode(rootComponent, rootProps, null, 4)
            const mount = (container) => {
                if (typeof container === 'string') {
                    container = document.querySelector(container)
                }
                patch(null, vnode, container)
            }
            return { mount }

        }
        // 初始化
        createApp({
            setup() {
                const num = Vue.ref(1)
                console.log(num)
                const double = Vue.computed(() => {
                    console.log(1111)
                    debugger
                    return num.value * 2
                })
                function handleClick() {
                    num.value++
                }
                return {
                    num,
                    double,
                    handleClick
                }
            },
            render(_ctx) {
                with (_ctx) {
                    return createVNode("div", { onClick: handleClick }, toDisplayString(double))
                }

            }
        }).mount('#app')
        

上述代码大家可以自行粘贴运行,也可在github上查看

简单解释一下,核心逻辑我们首先createApp之后,其实就是传入当前组件配置,然后就开始走patch方法来执行组件的初始化,然后走setupComponent 执行setup的初始化 注意这个时候只是初始化响应式相关,实例this并没有通过call传入,所以函数中拿不到this 。

接下来执行setupRenderEffect 开始依赖收集,在当前方法中执行了render方法开始了触发在setup 中的响应式的get从而触发依赖收集,具体的依赖收集过程,我们后续讲响应式这块会详细分析

ReactiveEffect

ReactiveEffect 是响应式系统的核心,而响应式系统又是 vue3 中的核心,所以我们必须要理解ReactiveEffect到底是干什么的

首先我们要理解的是ReactiveEffect 作为 reactive 的核心,主要负责收集依赖,更新依赖,在vue2中他叫Watcher,我们来看源码:

// 记录当前活跃的对象
let activeEffect
// 标记是否追踪
let shouldTrack = false

class ReactiveEffect{
active = true // 是否为激活状态
deps = [] // 所有依赖这个 effect 的响应式对象
onStop = null // function
constructor(fn, scheduler) {
  this.fn = fn // 回调函数,如: computed(/* fn */() => { return testRef.value ++ })
  // function类型,不为空那么在 TriggerRefValue 函数中会执行 effect.scheduler,否则会执行 effect.run
  this.scheduler = scheduler
}

run() {
  // 如果这个 effect 不需要被响应式对象收集
  if(!this.active) {
    return this.fn()
  }

  // 源码这里用了两个工具函数:pauseTracking 和 enableTracking 来改变 shouldTrack的状态
  shouldTrack = true
  activeEffect = this
  
  // 在设置完 activeEffect 后执行,在方法中能够对当前活跃的 activeEffect 进行依赖收集
  const result = this.fn()
  
  shouldTrack = false
  // 执行完副作用函数后要清空当前活跃的 effect
  activeEffect = undefined

  return result
}

// 暂停追踪
stop() {
  if (this.active) {
    // 找到所有依赖这个 effect 的响应式对象
    // 从这些响应式对象里面把 effect 给删除掉
    cleanupEffect(this)
    // 执行onStop回调函数
    if (this.onStop) {
      this.onStop();
    }
    this.active = false;
  }
}
}

this.fn() 在组件级别就是一个渲染的render函数,也是通过重新执行当前方法,来达到触发更新的目的

而在整个响应式系统中,他通过一个dep 建立了响应式数据和 ReactiveEffect 的关系,dep 中保存了ReactiveEffect ,而一个响应式数据又会有个dep 小管家管理了与之相关的ReactiveEffect 这样的时候当响应式数据变化的时候,会触发dep小管家,去批量处理他里面的ReactiveEffect 去做更新,注意在组件化中,一个组件只有一个渲染ReactiveEffect。

提起执行render 不得不想到diff

渲染器中的diff

我们上文说道,之所以采用Virtual DOM 的目的不是为了性能,而是为了跨平台,所以,当页面大量的内容更新的时候性能就没法保证,就需要有一种算法来减小DOM操作的性能开销

市面上的diff 算法基本原理的核心是Diff同层对比,不做跨层级对比,这样能大大减少js 的计算,而在同层对比的核心算法上出现了不同的流派

  • React 系列的diff 算法 ---从前到后找到需要移动的节点
  • 双端diff---从两端往中间遍历找到需要移动的节点
  • 最长递归子序列diff---通过求解最长递归子序列找到需要移动的节点

vue3目前使用的是 inferno 其中的核心算法就是最长递归子序列

在这里我们不在赘述,整个对比过程,很多大佬也都分析了,咱们只是导论,不做深入探究,意在和大家一起掌握真个vue 源码的体系

响应式系统

响应性--这个术语在程序设计中经常被提及,但这是什么意思呢?响应性是一种允许我们以声明式的方式去适应变化的编程范例。

而在vue3中的响应性原理就离不开proxy 文档中是这样描述的

  • 当一个值被读取时进行追踪:proxy 的 get 处理函数中 track 函数记录了该 property 和当前副作用。
  • 当某个值改变时进行检测:在 proxy 上调用 set 处理函数。
  • 重新运行代码来读取原始值trigger 函数查找哪些副作用依赖于该 property 并执行它们。

该被代理的对象对于用户来说是不可见的,但是在内部,它们使 Vue 能够在 property 的值被访问或修改的情况下进行依赖跟踪和变更通知。

那他通知的是什么呢?这里就用到了我们前面ReactiveEffect 我们通知当前响应式变量中的小管家deps 依次执行其中的ReactiveEffect.run 方法,来达到触发更新的目的 ,原理图如下(感谢大佬提供的图)

image.png

我们也准备了可运行的代码,各位大佬们也可粘贴下来,实际体验

 // 保存临时依赖函数用于包装
        const effectStack = [];

        // 依赖关系的map对象只能接受对象
        let targetMap = new WeakMap();
        // 判断是不是对象
        const isObject = (val) => val !== null && typeof val === 'object';
        // ref的函数
        function ref(val) {
            // 此处源码中为了保持一致,在对象情况下也做了用value 访问的情况value->proxy对象
            // 我们在对象情况下就不在使用value 访问
            return isObject(val) ? reactive(val) : new refObj(val);
        }

        //创建响应式对象
        class refObj {
            constructor(val) {
                this._value = val;
            }
            get value() {
                // 在第一次执行之后触发get来收集依赖
                track(this, 'value');
                return this._value;
            }
            set value(newVal) {
                console.log(newVal);
                this._value = newVal;
                trigger(this, 'value');
            }
        };

        // 对象的响应式处理 在这里我们为了理解原理原理暂时不考虑对象里嵌套对象的情况
        // 其实对象的响应式处理也就是重复执行reactive 
        function reactive(target) {
            return new Proxy(target, {
                get(target, key, receiver) {
                    // Reflect用于执行对象默认操作,更规范、函数式
                    // Proxy和Object的方法Reflect都有对应
                    const res = Reflect.get(target, key, receiver);
                    track(target, key);
                    return res;
                },
                set(target, key, value, receiver) {
                    const res = Reflect.set(target, key, value, receiver);
                    trigger(target, key);
                    return res;
                },
                deleteProperty(target, key) {
                    const res = Reflect.deleteProperty(target, key);
                    trigger(target, key);
                    return res;
                }
            });
        }

        // 到此处,当前的ref 对象就已经实现了对数据改变的监听
        const newRef = ref(0);
        // 但是还是没有响应式的能力,那么他是怎样实现响应式的呢----依赖收集,触发更新=
        // 用来做依赖收集 
        // 在源码中为为了方法的通用性,他还传入了很多参数用于兼容不同情况
        // 我们意在理解原理,只需要包装fn 即可
        function effect(fn) {
            // 包装当前依赖函数
            const effect = function reactiveEffect() {
                // 模拟源码中也加入错误处理,为了避免你瞎写出现错误的情况,这就是框架的高明之处
                if (!effectStack.includes(effect)) {
                    try {
                        // 给当前函数放入临时栈中,为在下面执行中,触发get,在依赖收集中能找到当前变量的依赖项来建立关系
                        effectStack.push(fn);
                        // 执行当前函数,开始依赖收集了
                        return fn();
                    } finally {
                        // 执行成功了出栈
                        effectStack.pop();
                    }
                };
            };

            effect();
        }
        //  在收集的依赖中建立关系
        function track(target, key) {
            // 取出最后一个数据内容
            const effect = effectStack[effectStack.length - 1];
            // 如果当前变量有依赖
            if (effect) {
                //判断当前的map中是否有target
                let depsMap = targetMap.get(target);
                // 如果没有
                if (!depsMap) {
                    // new map存储当前weakmap
                    depsMap = new Map();
                    targetMap.set(target, depsMap);
                }
                // 获取key对应的响应函数集
                let deps = depsMap.get(key);
                if (!deps) {
                    // 建立当前key 和依赖的关系,因为一个key 会有多个依赖
                    // 为了防止重复依赖,使用set
                    deps = new Set();
                    depsMap.set(key, deps);
                }
                // 存入当前依赖
                if (!deps.has(effect)) {
                    deps.add(effect);
                }
            }
        }
        // 用于触发更新
        function trigger(target, key) {
            // 获取所有依赖内容
            const depsMap = targetMap.get(target);
            // 如果有依赖的话全部拉出来执行
            if (depsMap) {
                // 获取响应函数集合
                const deps = depsMap.get(key);
                if (deps) {
                    // 执行所有响应函数 
                    const run = (effect) => {
                        // 源码中有异步调度任务,我们在这里省略
                        effect();
                    };
                    deps.forEach(run);
                }
            }
        }
        effect(() => {
            console.log(11111);
            // 在自己实现的effect中,由于为了演示原理,没有做兼容,不能来触发set,否则会死循环
            // vue源码中触发对effect中的做了兼容处理只会执行一次
            newRef.value;
        });

        newRef.value++;

上述代码中,没有渲染的effect 因为,如果使用渲染effect 篇幅太大,大家容易迷糊,我们使用一个相当于是vue3的Composition API中的用户effect, 和渲染effect 异曲同工,不同的是渲染effect 是一个ReactiveEffect 而用户effect 是我们传入的一个函数,他们最终都会被放进deps小管家中去

编译器

vue3之所以会有很大的性能提升,编译器起到了很大的作用,由于模板的可遍历性,所以在编译阶段可以做很多优化,在此之前我们先大致简述一下整个编译器的基本流程

如果了解过编译器的工作流程的同学应该知道,一个完整的编译器的工作流程会是这样:

  • 首先,parse 解析原始代码字符串,生成抽象语法树 AST。
  • 其次,transform 转化抽象语法树,让它变成更贴近目标「DSL」的结构。
  • 最后,codegen 根据转化后的抽象语法树生成目标「DSL」(一种为特定领域设计的,具有受限表达性的编程语言)的可执行代码。

那放到vue中来也是一样的

Parse 阶段

parse函数就是发生在把template转换成ast的这过程,具体是通过一些正则表达式的匹配template中的字符串,parse解析之后得到的是一个粗糙的ast对象,所谓ast它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。在我们vue 编译中,就是个js 对象

比如如下模板:

<template>
  <p>hello World!</p>
</template>

变成ast

{
  "type": 0,
  "children": [
    {
      "type": 1,
      "ns": 0,
      "tag": "template",
      "tagType": 0,
      "props": [],
      "isSelfClosing": false,
      "children": [
        {
          "type": 1,
          "ns": 0,
          "tag": "p",
          "tagType": 0,
          "props": [],
          "isSelfClosing": false,
          "children": [
            {
              "type": 2,
              "content": "hello World!",
              "loc": {
                "start": {
                  "column": 6,
                  "line": 2,
                  "offset": 16
                },
                "end": {
                  "column": 18,
                  "line": 2,
                  "offset": 28
                },
                "source": "hello World!"
              }
            }
          ],
          "loc": {
            "start": {
              "column": 3,
              "line": 2,
              "offset": 13
            },
            "end": {
              "column": 22,
              "line": 2,
              "offset": 32
            },
            "source": "<p>hello World!</p>"
          }
        }
      ],
      "loc": {
        "start": {
          "column": 1,
          "line": 1,
          "offset": 0
        },
        "end": {
          "column": 12,
          "line": 3,
          "offset": 44
        },
        "source": "<template>\n  <p>hello World!</p>\n</template>"
      }
    }
  ],
  "helpers": [],
  "components": [],
  "directives": [],
  "hoists": [],
  "imports": [],
  "cached": 0,
  "temps": 0,
  "loc": {
    "start": {
      "column": 1,
      "line": 1,
      "offset": 0
    },
    "end": {
      "column": 1,
      "line": 4,
      "offset": 45
    },
    "source": "<template>\n  <p>hello World!</p>\n</template>\n"
  }
}

如果有兴趣可以去 AST explorer 可以在线看到不同的 parser 解析 js 代码后得到的 AST。

Transform 阶段

transform阶段,Vue 将对 AST 执行一些转换操作,进行深加工ue的一些的hoistStatic 静态提升 、cacheHandlers 缓存函数 PatchFlags补丁标识 都是在这个阶段处理的

Codegen阶段

生成render函数

Vue3编译器优化策略

我们之前说过,Transform 阶段 做了很多优化,我们就来具体梳理一下,先来一段编译后的代码

const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue

const _hoisted_1 = ["onClick"]
const _hoisted_2 = ["onClick"]
const _hoisted_3 = /*#__PURE__*/_createElementVNode("div", null, "静态节点", -1 /* HOISTED */)

return function render(_ctx, _cache) {
 with (_ctx) {
   const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue

   return (_openBlock(), _createElementBlock(_Fragment, null, [
     _createCommentVNode(" <div>\n  注释\n</div> "),
     _createElementVNode("div", { onClick: handle }, " 点击切换 ", 8 /* PROPS */, _hoisted_1),
     _createElementVNode("div", { onClick: handle1 }, " 点击切换1 ", 8 /* PROPS */, _hoisted_2),
     _hoisted_3,
     _createElementVNode("div", null, _toDisplayString(num1) + "动态节点", 1 /* TEXT */)
   ], 64 /* STABLE_FRAGMENT */))
 }
}

1.静态节点提升

所谓静态节点提升为了在diff 的时候防止patch 从而在编译的时候标记处理,使得在将静态内容排除在render之外,防止render 执行的时候,被再次创建

   // 通过闭包将执行结果放在render之内,防止重复执行render再次执行当前静态节点的创建
 const _hoisted_3 = /*#__PURE__*/_createElementVNode("div", null, "静态节点", -1 /* HOISTED */)

2.补丁标记和动态属性记录

 _createElementVNode("div", { onClick: handle }, " 点击切换 ", 8 /* PROPS */, _hoisted_1),

上述代码中有个8 这就是 patchFlag,他的所有类型展示如下:

export const enum PatchFlags {
// 动态文本节点
TEXT = 1,
// 2 动态class
CLASS = 1 << 1,
// 4 动态style
STYLE = 1 << 2,
// 动态props
PROPS = 1 << 3,
/**
*指示带有带有动态关键点的道具的元素。当钥匙更换时,一个完整的
*总是需要diff来移除旧密钥。这面旗是相互的
*独家与阶级,风格和道具。
*/
// 具有动态key属性,当key改变时,需要进行完整的diff
FULL_PROPS = 1 << 4,
// 32 带有监听事件的节点
HYDRATE_EVENTS = 1 << 5,
// 64 一个不会改变子节点顺序的fragment
STABLE_FRAGMENT = 1 << 6,
// 128 带有key的fragment
KEYED_FRAGMENT = 1 << 7,
// 256 没有key的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 512 一个子节点只会进行非props比较
NEED_PATCH = 1 << 9,
// 1024 动态插槽
DYNAMIC_SLOTS = 1 << 10,
// 下面是特殊的,即在diff阶段会被跳过的
// 2048 表示仅因为用户在模板的根级别放置注释而创建的片段,这是一个仅用于开发的标志,因为注释在生产中被剥离
DEV_ROOT_FRAGMENT = 1 << 11,
// 静态节点,它的内容永远不会改变,不需要进行diff
HOISTED = -1,
// 用来表示一个节点的diff应该结束
BAIL = -2
}

有个patchFlag之后 就能根据patchFlag的类型,执行特殊的diff 逻辑,能防止全量的diff 造成的性能浪费

3.缓存事件处理程序

默认情况下onClick会被视为动态绑定,所以每次都会去追踪它的变化 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可

// 模板
<div> <button @click = 'onClick'>点我</button> </div>

// 编译后
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("button", {
      onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.onClick && _ctx.onClick(...args)))
    }, "点我")
  ]))
}

// Check the console for the AST

上述代码中,没有了patchFlag的类型 , 也就是patchFlag是一个默认值为0,这样的话就不走diff

  if (patchFlag > 0) {
    
      //patchFlag的存在意味着该元素的渲染代码
      //由编译器生成,可以采取快速路径。
      //在该路径中,旧节点和新节点保证具有相同的形状
      //(即,在源模板中完全相同的位置)
      if (patchFlag & PatchFlags.FULL_PROPS) {
       
        patchProps(
          el,
          n2,
          oldProps,
          newProps,
          parentComponent,
          parentSuspense,
          isSVG
        )
      } else {
        // class
   
        if (patchFlag & PatchFlags.CLASS) {
          if (oldProps.class !== newProps.class) {
            hostPatchProp(el, 'class', null, newProps.class, isSVG)
          }
        }

        // style
        // this flag is matched when the element has dynamic style bindings
        if (patchFlag & PatchFlags.STYLE) {
          hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG)
        }

     
        if (patchFlag & PatchFlags.PROPS) {
          // if the flag is present then dynamicProps must be non-null
          const propsToUpdate = n2.dynamicProps!
          for (let i = 0; i < propsToUpdate.length; i++) {
            const key = propsToUpdate[i]
            const prev = oldProps[key]
            const next = newProps[key]
            // #1471 force patch value
            if (next !== prev || key === 'value') {
              hostPatchProp(
                el,
                key,
                prev,
                next,
                isSVG,
                n1.children as VNode[],
                parentComponent,
                parentSuspense,
                unmountChildren
              )
            }
          }
        }
      }

      // text
      // This flag is matched when the element has only dynamic text children.
      if (patchFlag & PatchFlags.TEXT) {
        if (n1.children !== n2.children) {
          hostSetElementText(el, n2.children as string)
        }
      }
    } else if (!optimized && dynamicChildren == null) {
      // unoptimized, full diff
      patchProps(
        el,
        n2,
        oldProps,
        newProps,
        parentComponent,
        parentSuspense,
        isSVG
      )
    }

上述代码中就是根据patchFlag类型来走不同的diff 逻辑,而一旦没有了patchFlag diff也就不走了,直接复用老得vnode ,而老的vnode 的事件中有了缓存,我们直接取用即可,省去了重新创建包装函数的开销,很多大佬可能不明白我说的啥意思,贴上vue 代码,大家就明白了:

export function patchEvent(
 el: Element & { _vei?: Record<string, Invoker | undefined> },
 rawName: string,
 prevValue: EventValue | null,
 nextValue: EventValue | null,
 instance: ComponentInternalInstance | null = null
) {
 // vei = vue event invokers
 const invokers = el._vei || (el._vei = {})
 const existingInvoker = invokers[rawName]
 if (nextValue && existingInvoker) {
   // patch
   existingInvoker.value = nextValue
 } else {
   const [name, options] = parseName(rawName)
   if (nextValue) {
     // 先对事件函数来一层包装,在将包装函数绑定到dom 上去
     // 如果开启缓存,就会将当前包装函数缓存,省去了diff 的 开销,直接复用
     const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
     addEventListener(el, name, invoker, options)
   } else if (existingInvoker) {
     // remove
     removeEventListener(el, name, existingInvoker, options)
     invokers[rawName] = undefined
   }
 }
}

4.块block

创建一个openBlock(),意思是先打开一个块,再createBlock创建一个块,把那些动态的部分保存在一个叫dynamicChildren的属性里,将来这个模块更新的时候,只做dynamicChildren里更新,其他不再处理,这样patch只会处理动态的子节点,从而提高性能,我们也梳理了一下代码:

   /**
         * 主要思想就是巧妙的利用栈的结构将动态节点放入dynamicChildren中
         **/
        let blockStack = []
        let currentBlock = null
        //初始化将快放入栈中利用栈的结构放入block 的子动态元素
        function _openBlock(disableTracking = false) {
            blockStack.push((currentBlock = disableTracking ? null : []))
        }
        function closeBlock() {
            blockStack.pop()
            currentBlock = blockStack[blockStack.length - 1] || null
        }
        function setupBlock(vnode) {
            vnode.dynamicChildren = currentBlock
            closeBlock()
            return vnode
        }
        function createVnode(type, porps, children, isBlockNode = false,
        ) {
            const vnode = {
                type,
                porps,
                children,
                isBlockNode,
                dynamicChildren: null
            }
            if (!isBlockNode && currentBlock) {
                currentBlock.push(vnode)
            }
            return vnode
        }
        function _createElementBlock(type, porps, children) {
            // 传入true直接表示当前vnode是一个当前block 的开始
            return setupBlock(createVnode(type, porps, children, true))
        }
        // 我们假设source是一个number 
        function _renderList(source, renderItem,) {
            ret = new Array(source)
            for (let i = 0; i < source; i++) {
                ret[i] = renderItem(i + 1)
            }
            return ret
        }
        // 生成vnode 
        function render(ctx) {
            return (_openBlock(), _createElementBlock('Fragment', null, [
                createVnode("div", null, " 11111 ", true/* CLASS */),
                createVnode("div", null, ctx.currentBranch, /* TEXT */),
                (_openBlock(true), _createElementBlock('Fragment', null, _renderList(5, (i) => {
                    return (_openBlock(), _createElementBlock("div", null, i, /* TEXT */))
                })))
            ]))
        }
        console.log(render({ currentBranch: "老骥伏枥" }))

大家可以复制下来,体会一下,怎么生成的动态dynamicChildren

为什么要看vue源码

  • 1、可能是市面上最先进的工程化方案
  • 2、ts 的最佳学习教材
  • 3、规范可维护的代码质量,优雅的代码技巧,
  • 4、开发时能针对性的性能优化,以及封装
  • 5、更快的定位工作中遇到的问题

1、2、5 我们不在过多解释,大家都明白,我们重点看一下3和4举两个例子

规范可维护的代码质量,优雅的代码技巧

  // 插件机制技巧
     // 一个全局变量
       let compile
       function registerRuntimeCompiler(_compile) {
           //将当前模板编译器赋值方便当前模板内别的函数能调用到
           //之所以要有这个注册方法,是为了让runtime 中使用
           compile = _compile
       }
       // 如此一来在代码导出时,只需要在在非runtime版本中使用注册方法注册即可
       //finishComponentSetup 内部包含编译逻辑
       function finishComponentSetup(instance) {
           Component = instance.type
           // 只需要判断有没有compile并且有没有render 即可
           if (compile && !Component.render) {
               // 执行编译逻辑
           }

       }
//vue3中的柯理化技巧
function makeMap ( str, expectsLowerCase ) {
       var map = Object.create(null);
       var list = str.split(',');
       for (var i = 0; i < list.length; i++) {
           map[list[i]] = true;
       }
       return expectsLowerCase
           ? function (val) { return map[val.toLowerCase()]; }
           : function (val) { return map[val]; }
   }
   var isHTMLTag = makeMap(
       'html,body,base,head,link,meta,style,title,' +
       'address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,' +
       'div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,' +
       'a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,' +
       's,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,' +
       'embed,object,param,source,canvas,script,noscript,del,ins,' +
       'caption,col,colgroup,table,thead,tbody,td,th,tr,' +
       'button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,' +
       'output,progress,select,textarea,' +
       'details,dialog,menu,menuitem,summary,' +
       'content,element,shadow,template,blockquote,iframe,tfoot'
   );
   var isHTMLTag = isHTMLTag('div');
// 函数的拓展性技巧

       function createAppAPI(rootComponent, rootProps = null) {
           const mount = (container) => {
           }
           return { mount }

       }
       function createRenderer() {
           return {
               createApp: createAppAPI()
           }
       }
       function createApp(...args) {
           const app = createRenderer().createApp(...args)
           const { mount } = app
           app.mount = () => {
               console.log('执行自己的逻辑')
               mount()
           }
       }

开发时能针对性的性能优化,以及封装

// 实现一个弹窗的封装技巧  
// 通过createVNode render 生成真实dom
const { createVNode,  render, ref } = Vue
       const message = {
           setup() {
               const num = ref(1)
               return {
                   num
               }
           },
           template: `<div>
                       <div>{{num}}</div>
                       <div>这是一个弹窗</div>
                     </div>`
       }
       // 生成实例
       const vm = createVNode(message)
       const container = document.createElement('div')
       //通过patch 变成dom
       render(vm, container)
       document.body.appendChild(container.firstElementChild)

最后

整个vue 源码的体系导读,本人才疏学浅,所有的理解都到这了,如有错误之处,请大佬们批评指正。

到这,在来个升华,给大家一点诚实的灌输

就是我最近发现社区里,很多人都特别的浮躁,认为看懂源码就有多了不得,不懂源码的都是菜鸡,在这里我想说一点我看源码的一点小小的感受:

  • 1、 看懂源码不一定说明你多强,因为他又不是你写的,他就跟读书不一定能让你赚钱多一样的道理

  • 2、 看源码能让我们知道什么代码是好的,毫无疑问vue 源码的代码基线,是很多人可能写一辈子都无法达到的高度

  • 3、看源码的目的,是为了让我们防止闭门造车,朝着一个正确的方向去努力

  • 4、vue源码只是我们这学科门类的一部分,整个前端生态是非常庞杂的,如果实在对源码没兴趣(原理还是要了解,应付面试),也没必要焦虑,去学习你感兴趣的磨练好你的手艺就好。