Vue3源码
Vue.js 2.x 的源码在 src 目录下,依据功不同分成 compiler(模板编译的相关代码)、core(与平台无关的通用运行时代码)、platforms(平台专有代码)、server(服务端渲染的相关代码)、sfc(.vue 单文件解析相关代码)、shared(共享工具代码) 等目录。
Vue.js 3.0中 ,源码通过 monorepo 的方式管理,根据功能将不同的模块拆分到 packages 目录下。
可以看出相对于 Vue.js 2.x 的源码组织方式,monorepo 把这些模块拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性。
问题
- 克隆下git的代码之后,pnpm安装相关依赖,并且需要node版本12以上。
- 需要执行
pnpm run dev,打包dist,才可以使用packages下面的examples文件夹下的例子。
阅读步骤
应用初始化
// 在 Vue.js 3.0 中,初始化一个应用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
1、创建app对象
-
createApp方法 packages/runtime-dom/src/index.ts
const createApp = ((...args) => { // 创建 app 对象 const app = ensureRenderer().createApp(...args) const { mount } = app // 重写 mount 方法 app.mount = (containerOrSelector) => { // ... } return app }) -
文件目录 vue-next/packages/runtime-dom/src/index.ts
-
总的来说就是vue3中通过 ensureRenderer 创建一个渲染器,这个渲染器内部层层调用,最终会找到一个叫 createApp 的方法,它是执行 createAppAPI 方法返回的函数。 createApp 方法接受了 rootComponent 和 rootProps 两个参数,我们在应用层面执行 createApp(App) 方法时,会把 App 组件对象作为根组件传递给 rootComponent。
const app = createApp(App) -
使用 ensureRenderer().createApp() 来创建 app 对象
-
首先使用ensureRenderer()来创建一个渲染器,其内部包括rendererOptions配置和createRenderer方法。初始时会调用createRenderer方法来延时创建渲染器 。
function ensureRenderer() { return renderer || (renderer = createRenderer(rendererOptions)) } -
createRenderer方法里面调用baseCreateRenderer方法。
-
baseCreateRenderer方法里面包括很多基础方法。比如patch,render等方法。其中render方法中就是组件渲染的核心逻辑。
patch、procesText、processCommentNode、mountStaticNode、patchStaticNode、moveStaticNode、removeStaticNode、processElement、mountElement、setScopeId、mountChildren、patchElement、patchBlockChildren、patchProps、processFragment、processComponent、mountComponent、updateComponent、setupRenderEffect、updateComponentPreRender、patchChildren、patchUnkeyedChildren、patchKeyedChildren、move、unmount、remove、removeFragment、unmountComponent、unmountChildren、getNextHostNode、render
-
baseCreateRenderer 的返回值, 也就是 ensureRenderer() 的返回值,返回上一步的CreateApp。CreateApp由createAppAPI实现。
-
return { render, hydrate, createApp: createAppAPI(render, hydrate) }
-
-
createAppAPI内部会返回一个叫createApp的方法。
-
return function createApp(rootComponent, rootProps = null) { const app = { _component: rootComponent, _props: rootProps, mount(rootContainer) { // 创建根组件的 vnode const vnode = createVNode(rootComponent, rootProps) // 利用渲染器渲染 vnode render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy }, mixin(mixin: ComponentOptions) { if (!context.mixins.includes(mixin)) { context.mixins.push(mixin) } return app }, } return app } -
这个function中定义了一个app实例,这个实例里面包括use、mixin、component、directive、mount、unmount、provide方法,为全局app实例添加一些扩展。
-
createApp执行完之后会返回定义的app实例。
-
-
-
-
-
在整个 app 对象创建过程中,Vue.js 利用闭包和函数柯里化的技巧,很好地实现了参数保留。
- 比如,在执行 app.mount 的时候,并不需要传入渲染器 render,这是因为在执行 createAppAPI 的时候渲染器 render 参数已经被保留下来了。
2、创建app.mount方法
const { mount } = app 要渲染到页面上,还需要调用mount,执行app.mount()= (containerOrSelector) => { ... }重写mount方法。
- 为什么要重写这个方法、而不把相关逻辑放到app实例上的mount方法内部去实现?
- ( createApp 返回的app对象里面已经有了mount方法,但是在入口函数中,下面还要对app.mount()进行重写)
- 原因:
- 因为Vue.js不仅仅服务web平台,他的目标是支持跨平台渲染,而createApp函数内部的
app.mount()方法是一个标准的可跨平台的组件渲染流程。 - 标准的渲染流程是不包含任何特定平台相关的逻辑,也就是说这些代码的执行逻辑都是与平台无关的。
- 我们需要在外部重写这个方法,来完善web平台下的逻辑渲染。
- 因为Vue.js不仅仅服务web平台,他的目标是支持跨平台渲染,而createApp函数内部的
- 一个标准化渲染流程会做什么?
- 创建根组件的 vnode
- 利用渲染器渲染 vnode
- 重写mount做了啥?
- 通过 normalizeContainer 标准化容器(可以传字符串选择器或者dom对象,如果是字符串选择器,就需要把他转换成dom对象,作为最终挂载容器。)
- 接着做一个if判断,如果组件对象没有定义 render 函数和 template 模板,则提取容器的 innerHTML 作为组件模板的内容。
- 接着在挂载之前清空容器内容,最终在调用
app.mount的方法走标准的组件渲染流程。
其他问题
这些事情发生在mount中。 const { mount } = app
-
执行 render(vnode, rootContainer, isSVG) , 这个render其实是在baseCreateRenderer中定义的。那么baseCreateRenderer里面的render做了什么?
render 是组件渲染的核心逻辑。
判断vnode是否为空, 如不为空。执行patch方法。patch方法同样定义在baseCreateRenderer内部。
patch(container._vnode || null, vnode, container, null, null, null, isSVG) -
baseCreateRenderer内部的patch方法做了什么? 在 patch 中会判断
vnode的type或shapeFlag执行对应的操作。 第一次patch的时候,vnode是一个组件,会进入shapeFlags.COMPONENT的判断, 执行processComponent方法进行处理。 -
baseCreateRenderer内部的processComponent方法做了什么? processComponent 里面触发 mountComponent。
-
baseCreateRenderer内部的mountComponent方法做了什么?
- 定义了一个实例instance,并执行
setupComponent(instance)。 - setupComponent方法在
packages/runtime-core/src/component.ts中定义,具体做了什么?- 触发 setupComponent 房中初始化组件的 props slots setup等(initProps,initSlots),将需要proxy代理的数据处理好,返回setupResult。
- 执行完了 setupComponent之后, 会执行setupRenderEffect。
- 定义了一个实例instance,并执行
-
baseCreateRenderer内部的 setupRenderEffect 方法做了什么?
- setupRenderEffect 内部首先定义了componentUpdateFn方法,接着会通过setupRenderEffect内部定义的effect方法使用componentUpdateFn方法。最后定义了update方法,执行effect方法。
- componentUpdateFn里面实现了什么?
- 第一次执行时, instance.isMounted 是 undefined。
- 执行patch方法,通过patch递归创建子节点。
- 结束后将instance.isMounted设置为true。
- 在 patch 中会判断 vnode 的 type 或 shapeFlag 执行对应的操作。
const setupRenderEffect: () => { const componentUpdateFn = () => { if (!instance.isMounted) { // 第一次执行时, instance.isMounted 是 undefined , 走这块逻辑 // ... // 执行patch方法,通过patch递归创建子节点。 patch() // 结束后将instance.isMounted设置为true。 instance.isMounted = true } else { } } const effect = new ReactiveEffect( componentUpdateFn, () => queueJob(instance.update), instance.scope // track it in component's effect scope ) const update = (instance.update = effect.run.bind(effect) as SchedulerJob) update() }
核心渲染流程
1、创建vnode
- Vnode 本质上是用来描述DOM的 JavaScript对象。在Vue.js中可以描述不同类型的节点,比如普通元素节点、组件节点等。
- vnode的优势?
- 对抽象事物的描述。Vue.js 3.0 内部还针对 vnode 的 type,做了更详尽的分类,包括 Suspense、Teleport 等,且把 vnode 的类型信息做了编码,以便在后面的 patch 阶段,可以根据不同的类型执行相应的处理逻辑。
- 可以把渲染过程抽象化,从而使得组件的抽象能力提升。
- 跨平台的优势,patch vnode 的过程对于不同的平台可以有自己的实现。
- vnode的性能一定比操作原生DOM好吗? 不一定。首先基于vnode实现的MVVM框架,在每次render to vnode的过程中,渲染组件会有一定的JavaScript耗时。
- 通过 createVNode 方法创建
- 对 props 做标准化处理
- 对 vnode 的类型信息编码
- 创建 vnode 对象,标准化子节点 children 。
2、渲染vnode
render(vnode, rootContainer)
const render = (vnode, container) => {
if (vnode == null) {
// 销毁组件
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 创建或者更新组件
patch(container._vnode || null, bnode. container)
}
// 缓存 vnode 节点, 表示已经渲染
container._vnode = node
}
-
render 渲染函数。逻辑简单,第一个参数vnode为空时,执行销毁组件的逻辑。否则执行创建或更新组件的逻辑。
-
渲染代码中的patch方法,是如何实现的。(render和patch都在baseCreateRenderer里面定义的)
- patch 本意是打补丁的意思,这个函数有两个功能.
- 一个是根据 vnode 挂载 DOM
- 一个是根据新旧 vnode 更新 DOM。
- patch 本意是打补丁的意思,这个函数有两个功能.
-
在创建的过程中,patch 函数接受多个参数,这里我们目前只重点关注前三个:
第一个参数 n1 表示旧的 vnode,当 n1 为 null 的时候,表示是一次挂载的过程;
第二个参数 n2 表示新的 vnode 节点,后续会根据这个 vnode 类型执行不同的处理逻辑;
第三个参数 container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,会挂载到 container 下面。
-
对于渲染的节点,我们这里重点关注两种类型节点的渲染逻辑:对组件的处理和对普通 DOM 元素的处理。
-
先来看对组件的处理。由于初始化渲染的是 App 组件,它是一个组件 vnode。
- 首先是用来处理组件的 processComponent 函数的实现。如果 n1 为 null,则执行挂载组件的逻辑,否则执行更新组件的逻辑。
- 挂载组件的 mountComponent 函数主要做三件事情。创建组件实例、设置组件实例、设置并运行带副作用的渲染函数。
- 首先是创建组件实例,Vue.js 3.0 虽然不像 Vue.js 2.x 那样通过类的方式去实例化组件,但内部也通过对象的方式去创建了当前渲染的组件实例。
- 其次设置组件实例,instance 保留了很多组件相关的数据,维护了组件的上下文,包括对 props、插槽,以及其他实例的属性的初始化处理。
- 最后是运行带副作用的渲染函数 setupRenderEffect。
- 该函数利用响应式库的 effect 函数创建了一个副作用渲染函数 componentEffect。副作用effect,当组件的数据发生变化时,effect 函数包裹的内部渲染函数 componentEffect 会重新执行一遍,从而达到重新渲染组件的目的。
- 渲染函数内部也会判断这是一次初始渲染还是组件更新。
- 初始渲染主要做两件事情:渲染组件生成 subTree、把 subTree 挂载到 container 中。
- 首先,是渲染组件生成 subTree,它也是一个 vnode 对象。这里要注意别把 subTree 和 initialVNode 弄混了(其实在 Vue.js 3.0 中,根据命名我们已经能很好地区分它们了,而在 Vue.js 2.x 中它们分别命名为 _vnode 和 $vnode)。
- 渲染生成子树 vnode 后,接下来就是继续调用 patch 函数把子树 vnode 挂载到 container 中了。
- 再次回到了 patch 函数,会继续对这个子树 vnode 类型进行判断。
- 初始渲染主要做两件事情:渲染组件生成 subTree、把 subTree 挂载到 container 中。
-
对普通 DOM 元素的处理流程。
-
processElement 函数。如果 n1 为 null,走挂载元素节点的逻辑,否则走更新元素节点逻辑。
-
mountElement 函数。创建 DOM 元素节点、处理 props、处理 children、挂载 DOM 元素到 container 上。
-
创建 DOM 元素节点,通过 hostCreateElement 方法创建。调用了底层的 DOM API document.createElement 创建元素,所以本质上 Vue.js 强调不去操作 DOM ,只是希望用户不直接碰触 DOM,它并没有什么神奇的魔法,底层还是会操作 DOM。
-
创建完 DOM 节点后,接下来要做的是判断如果有 props 的话,给这个 DOM 节点添加相关的 class、style、event 等属性,并做相关的处理,这些逻辑都是在 hostPatchProp 函数内部做的。
-
接下来是对子节点的处理,我们知道 DOM 是一棵树,vnode 同样也是一棵树,并且它和 DOM 结构是一一映射的。
- 如果子节点是纯文本,则执行 hostSetElementText 方法。
- 如果子节点是数组,则执行 mountChildren 方法。
子节点的挂载逻辑同样很简单,遍历 children 获取到每一个 child,然后递归执行 patch 方法挂载每一个 child 。
mountChildren 函数的第二个参数是 container,而我们调用 mountChildren 方法传入的第二个参数是在 mountElement 时创建的 DOM 节点,这就很好地建立了父子关系。
通过递归 patch 这种深度优先遍历树的方式,我们就可以构造完整的 DOM 树,完成组件的渲染。
-
处理完所有子节点后,最后通过 hostInsert 方法把创建的 DOM 元素节点挂载到 container 上。
-
-
其他类型。
-
完整的diff流程
更新组件主要做三件事情:更新组件 vnode 节点、渲染新的子树 vnode、根据新旧子树 vnode 执行 patch 逻辑。
重点关注一下patch 逻辑:
- 首先判断新旧节点是否是相同的 vnode 类型,如果不同,比如一个 div 更新成一个 ul,那么最简单的操作就是删除旧的 div 节点,再去挂载新的 ul 节点。
- 如果是相同的 vnode 类型,就需要走 diff 更新流程了,接着会根据不同的 vnode 类型执行不同的处理逻辑,这里我们只分析普通元素类型和组件类型的处理过程。
组件更新
-
组件的更新最终还是要转换成内部真实 DOM 的更新,而实际上普通元素的处理流程才是真正做 DOM 的更新。主要依据子组件 vnode 是否存在一些会影响组件更新的属性变化进行判断,如果存在就会更新子组件。
-
processComponent 作为处理组件更新的方法,主要通过执行 updateComponent 函数来更新子组件,updateComponent 函数在更新子组件的时候,会先执行 shouldUpdateComponent 函数,根据新旧子组件 vnode 来判断是否需要更新子组件。
- processComponent 处理组件 vnode,本质上就是去判断子组件是否需要更新,如果需要则递归执行子组件的副作用渲染函数来更新,否则仅仅更新一些 vnode 的属性。
- processComponent 处理组件 vnode,让子组件实例保留对组件 vnode 的引用,用于子组件自身数据变化引起组件重新渲染的时候,在渲染函数内部可以拿到新的组件 vnode。
-
在 shouldUpdateComponent 函数的内部,主要是通过检测和对比组件 vnode 中的 props、chidren、dirs、transiton 等属性,来决定子组件是否需要更新。
一个组件重新渲染的两种场景:
- 一种是组件本身的数据变化,这种情况下 next 是 null。
- 一种是父组件在更新的过程中,遇到子组件节点,先判断子组件是否需要更新,如果需要则主动执行子组件的重新渲染方法,这种情况下 next 就是新的子组件 vnode。
子组件对应的新的组件 vnode 是什么时候创建的
- 在父组件重新渲染的过程中,通过 renderComponentRoot 渲染子树 vnode 的时候生成。
- 子树 vnode 是个树形结构,通过遍历它的子节点就可以访问到其对应的组件 vnode。
元素更新
processElement
更新元素(patchElement)的过程主要做两件事情:
- 更新 props
patchProps - 更新子节点
patchChildren - 一个 DOM 节点元素就是由它自身的一些属性和子节点构成的。
patchChildren
-
旧子节点是纯文本
-
如果新子节点也是纯文本,那么做简单地文本替换即可;
-
如果新子节点是空,那么删除旧子节点即可;
-
如果新子节点是 vnode 数组,那么先把旧子节点的文本清空,再去旧子节点的父容器下添加多个新子节点。
-
-
旧子节点是空
- 如果新子节点是纯文本,那么在旧子节点的父容器下添加新文本节点即可;
- 如果新子节点也是空,那么什么都不需要做;
- 如果新子节点是 vnode 数组,那么直接去旧子节点的父容器下添加多个新子节点即可
-
旧子节点是 vnode 数组
- 如果新子节点是纯文本,那么先删除旧子节点,再去旧子节点的父容器下添加新文本节点
- 如果新子节点是空,那么删除旧子节点即可
- 如果新子节点也是 vnode 数组,那么就需要做完整的 diff 新旧子节点了,这是最复杂的情况,内部运用了核心 diff 算法。
diff
diff主要发生在patch逻辑中,用来找出新旧子树 vnode 的不同,并找到一种合适的方式更新 DOM。(patch里面的processElement里面的patchChildren)
diff这次就先不展开写了,先给个思维导图。有时间写完了再更新文章~ assets.processon.com/chart_image…
组件渲染前的初始化过程
mountComponent
( packages/runtime-core/src/renderer.ts 的 baseCreateRenderer 中定义的)
vue3 不同于 vue2 通过类的方式去实例化组件了,但是内部也是通过设置对象的方式去创建了当前渲染的组件实例。
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 创建组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
// 设置组件实例
setupComponent(instance)
// 设置并运行带副作用的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}
主要流程
-
创建组件实例 createComponentInstance。里面定义了instance , 包括很多属性。props、attrs、refs、slots等。
-
组件实例的设置流程 setupComponent // Composition API 的核心 setup 函数的实现。
- 解构出一些属性 props children
- 判断是否是一个有状态的组件
- 初始化props
- 初始化插槽
- 返回 setupResult (执行 setupStatefulComponent)
- setupStatefulComponent
- 创建渲染代理的属性访问缓存 instance.accessCache = {}
- 创建渲染上下文代理 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
- 在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性。
- 所以我们也要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改,代理到对 setupState、ctx、data、props 中的数据的访问和修改。
- 明确了代理的需求后,我们接下来就要分析 proxy 的几个方法: get、set 和 has
- PublicInstanceProxyHandlers (packages/runtime-core/src/componentPublicInstance.ts) 中包括get、set、has
- 判断处理setup函数
const { setup } = Componentif (setup) { }- 如果 setup 函数带参数,则创建一个 setupContext。
- 判断 setup 函数的参数长度,如果大于 1,则创建 setupContext 上下文。
const setupContext = (instance.setupContext =setup.length > 1 ? createSetupContext(instance) : null)
- 执行 setup 函数,获取结果
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext]) - 处理 setup 执行结果
handleSetupResult(instance, setupResult, isSSR)
- 如果 setup 函数带参数,则创建一个 setupContext。
- 完成组件实例设置
finishComponentSetup(instance)if (!setup) { }-
做了两件事: 标准化模板或者渲染函数和兼容 Options API
-
- setupStatefulComponent
响应式实现
响应式机制的主要功能就是,可以把普通的 JavaScript 对象封装成为响应式对象,拦截数据的获取和修改操作,实现依赖数据的自动化更新。
reactive
reactive 内部通过 createReactiveObject 函数把 target 变成了一个响应式对象。
createReactiveObject
-
首先判断target是不是数组或对象类型,如果不是直接返回target。 这里同时也说明了只有对象或者数组才能reactive。
-
target 已经是 Proxy 对象,直接返回。
if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target }注意如果是readonly作用于一个响应式对象上,不返回,继续执行。
-
如果target 已经有对应的 Proxy 了, 直接返回。(target already has corresponding Proxy)
const existingProxy = proxyMap.get(target) if (existingProxy) { return existingProxy } // ... proxtMap.set(target, proxy) -
只有在白名单里面的数据类型才能变成响应式。
const targetType = getTargetType(target) if (targetType === TargetType.INVALID) { // 以前的canObserve 就是对target对象做进一步限制 return target }TargetType.INVALID不在白名单。也可能会看到有通过canObeserve 版本进行白名单判断的版本,道理都是一样的。
-
通过 Proxy API 劫持 target 对象,把它变成响应式。
const proxy = new Proxy(target, targetType = TargetType.COLLECTION ? collectionHandlers : baseHandlers ) return proxy这里Proxy 对应的处理器会根据数据类型的不同而分成不同类型的处理器。
mutableHandlers,readonlyHandlers,shallowReactiveHandlers,shallowReadonlyHandlers
baseHandlers
处理器对象类型
-
mutableHandlers
-
可变类型的处理器对象 (基本数据类型)
-
const mutableHandlers = { get, // 属性读取操作的捕捉器。 set, // 属性设置操作的捕捉器。 deleteProperty, // delete 操作符的捕捉器。 has, // in 操作符的捕捉器。 ownKeys // Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 } -
实现基本操作的拦截和自定义。
-
-
readonlyHandlers
-
只读类型的处理器对象
-
const readonlyHandlers: ProxyHandler<object> = { get: readonlyGet, // createGetter(true) set(target, key) { return true }, deleteProperty(target, key) { return true } } -
set 和 deleteProperty 失效。 开发模式下有报错提示。
-
-
shallowReactiveHandlers
-
const shallowReactiveHandlers = Object.assign( {}, mutableHandlers, { get: shallowGet, set: shallowSet } )
-
-
shallowReadonlyHandlers
-
const shallowReadonlyHandlers = Object.assign( {}, readonlyHandlers, { get: shallowReadonlyGet } )
-
mutableHandlers
重点分析一下比较全面的基本数据类型的Proxy处理器对象 mutableHandlers。
无论进行哪种处理,最终都会进行依赖收集track或者派发trigger通知两件事之一。
get
-
访问对象属性会触发 get 函数
-
就是对象直接
.的时候 -
get函数就是执行createGetter()的返回值
const res = Reflect.get(target, key, receiver)- 首先对特殊的key进行处理,对有
ReactiveFlags.RAW属性的对象进行判断,如果有就返回对象本身。 - 如果对象不是只读的并且target是数组类型
Reflect.get(arrayInstrumentations, key, receiver)对数组的每个元素做了依赖收集,保证可以监听到数组每个元素的变化 - 如果对象不是只读的,通过 trak 触发依赖收集。
track(target, TrackOpTypes.GET, key) - 最后判断res是否是一个对象,是否需要递归执行reactive把res变成响应式的。
return isReadonly ? readonly(res) : reactive(res)- 这么做的好处是,在初始化响应式对象时,通过Proxy劫持的是对象本身,并不劫持子对象的变化。
- Proxy 只有在访问对象属性的时候才会进行递归操作,而Vue2.x中的Object.defineProperty触发对象劫持的时候就执行了递归操作。
- 相比较之下,通过Proxy实现的只有在访问时才会触发递归会有性能上的提升。
- 首先对特殊的key进行处理,对有
set
-
设置对象属性会触发 set 函数
-
执行set操作,获取最新的value的值。
const result = Reflect.set(target, key, value, receiver)
-
通过trigger触依赖更新。
if (target === toRaw(receiver)) { if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } -
如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了。
const hadKey = hasOwn(target, key)const hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key)- 判断key是否存在于target上来确定通知的类型。
- TriggerOpTypes.ADD
- TriggerOpTypes.SET
-
最后返回result。核心是如何通过trigger来派发通知,触发依赖。
deleteProperty
删除对象属性会触发 deleteProperty 函数
- 确认target是否有这个属性。
const hadKey = Object.prototype.hasOwnProperty.call(val, key)。 - 执行删除操作
const result = Reflect.deleteProperty(target, key) - 派发通知,
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) return result
has
in 操作符会触发 has 函数
- 执行has操作。
const result = Reflect.has(target, key) - 触发依赖收集
track(target, TrackOpTypes.HAS, key) return result
ownKeys
通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数
- 进行依赖收集。
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY) Reflect.ownKeys(target)返回一个由目标对象自身的属性键组成的数组。
track 🌟
收集依赖
track(target, TrackOpTypes.GET, key)
function track(target: object, type: TrackOpTypes, key: unknown){
...
}
const enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
我们要收集的依赖就是数据变化后执行的副作用函数。
具体如何实现的:
-
创建全局的
targetMap()用于存储target依赖,它的键是target,值是depsMap。const targetMap = new WeakMap()- depsMap中键是target的key值,值是依赖收集的容器Dep,是一个Set。
dep = createDep()。 - Dep存放了依赖的副作用函数effect。
-
!(isTracking = shouldTrack && activeEffect !== undefined)-
shouldTrack 是否应该收集依赖
-
activeEffect 当前激活的 effect
-
当shouldTrack为false且activeEffect 为undefined时,track直接返回。
-
-
每个 target 对应一个 depsMap
-
let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) }
-
-
每个key对应一个dep 集合
-
let dep = depsMap.get(key) if (!dep) { depsMap.set(key, (dep = createDep())) }
-
-
收集当前激活的**
effect**作为依赖,也就是存储依赖的操作。trackEffects(dep)-
if (!dep.has(activeEffect!)) { // 收集当前激活的 effect 作为依赖 dep.add(activeEffect!) // 当前激活的 effect 收集 dep 集合作为依赖 activeEffect!.deps.push(dep) } -
activeEffect 是当前正在执行的effect
-
trigger 🌟
派发通知
trigger(target, TriggerOpTypes.ADD, key, value)
function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
){
...
}
const enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
执行trigger就是根据target和key从 targetMap中找到相关的所有副作用函数遍历执行一遍。
具体如何实现的:
-
通过 targetMap 拿到 target 对应的依赖集合 depsMap。
-
const depsMap = targetMap.get(target) if (!depsMap) { // 如果没有依赖,直接返回。 return }
-
-
创建依赖集合deps,根据 key 从 depsMap 中找到对应的 effects 添加到deps中。
-
当type为clear时。
deps = [ ...depsMap.values()] -
如果target为数组类型。
depsMap.forEach((dep, key) => { if (key === 'length' || key >= (newValue as number)) { deps.push(dep) } }) -
SET | ADD | DELETE 操作,添加对应的effects。
deps.push(depsMap.get(key))- 会有一些针对不同类型的操作,不赘述了在这里。
-
-
收集到依赖集合之后,创建运行的 effects 集合
const effects: ReactiveEffect[] = []。遍历依赖集合所有的依赖,然后添加到effects中。最后通过triggerEffects执行effects。-
for (const dep of deps) { if (dep) { effects.push(...dep) } } triggerEffects(createDep(effects)) -
triggerEffects 执行effects
for (const effect of isArray(dep) ? dep : [...dep]) { if (effect !== activeEffect || effect.allowRecurse) { if (effect.scheduler) { effect.scheduler() } else { effect.run() } } }
-
effect 🌟
ReactiveEffect
-
作用:用于生成effect实例,存放回调函数。其实就是响应式的副作用函数,当trigger派发通知时,执行的effect就是它。
-
使用:例如trigger中定义的依赖集合effects,函数effect中的新建
const _effect = new ReactiveEffect(fn)。 -
具体实现:
-
实现思路
- 把全局的activeEffect 指向它
- 执行被包装的原始函数fn
-
参数
- active 是否激活 true
- deps 依赖集合
cleanupEffect()时会从其中删除对应的effect,并清空它。 - constructor中
- fn
- Scheduler
- scope (非必填)
- ...
-
方法 effectStack是根据ReactiveEffect定义的。
const effectStack: ReactiveEffect[] = []-
run
run() { if (!this.active) { return this.fn() // 执行fn 但是不收集依赖 } if (!effectStack.includes(this)) { try { effectStack.push((activeEffect = this)) // 进行压栈操作,并设置activeEffect = effect enableTracking() // 开启 shouldTrack,设置为true,允许收集依赖 trackOpBit = 1 << ++effectTrackDepth if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this) } else { cleanupEffect(this) } return this.fn() // 执行传入的fn } finally { if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth resetTracking() // 恢复开始之前的状态 effectStack.pop() // 出栈操作 const n = effectStack.length activeEffect = n > 0 ? effectStack[n - 1] : undefined // 给activeEffect赋值,只想最后一个effect } } }- 首先判断effect的状态active,在active不为true时,直接执行原始函数fn。
- 接着判断 effectStack 中是否包含 effect,如果没有就把 effect 压入栈内。
if (!effectStack.includes(effec))effectStack.push((activeEffect = this))就相当于effectStack.push(effect)和activeEffect = effect。- 入栈操作同时将activeEffect设置为当前的effect函数
- 执行
enableTracking(),设置shouldTrack为true,进行依赖收集。 - 最后进行出栈操作,恢复开启依赖收集之前的状态,activeEffect指向栈内最后一个effect。
-
stop
- 执行
cleanupEffect()操作,清空 reactiveEffect 函数对应的依赖,删除deps中每一项的effect,最后清空deps。 - 设置active状态为false,变为飞机货的状态。
- 执行
-
-
toReactive()
判断是否是对象,是的话进行响应式处理
isObject(value) ? reacive(value) : value
ref
实现
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
RefImpl
class RefImpl<T> {
private _value: T // 响应式处理后的值
private _rawValue: T // 存储原始值,主要用于与 newVal 作比较
public dep?: Dep = undefined // 依赖收集的容器,用于存放effect
public readonly __v_isRef = true
constructor(value: T, public readonly _shallow: boolean) {
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : toReactive(value) // 响应式处理
}
get value() {
trackRefValue(this) // 依赖收集
return this._value
}
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : toReactive(newVal) // 发生改变时重新赋值
triggerRefValue(this, newVal) // 派发通知,触发依赖,通知更新
}
}
}
trackRefValue
ref依赖收集
-
判断
ref.dep是否有值,没有的话初始化创建一个dep -
执行
trackEffects(ref.dep)进行依赖收集 -
实现
export function trackRefValue(ref: RefBase<any>) { if (isTracking()) { ref = toRaw(ref) if (!ref.dep) { ref.dep = createDep() } if (__DEV__) { trackEffects(ref.dep, { target: ref, type: TrackOpTypes.GET, key: 'value' }) } else { trackEffects(ref.dep) } }
triggerRefValue
-
当
ref.dep有值时,执行triggerEffects(ref.dep)触发依赖,派发通知 -
实现
export function triggerRefValue(ref: RefBase<any>, newVal?: any) { ref = toRaw(ref) if (ref.dep) { if (__DEV__) { triggerEffects(ref.dep, { target: ref, type: TriggerOpTypes.SET, key: 'value', newValue: newVal }) } else { triggerEffects(ref.dep) } } }