往期文章:
前言
期期盼盼,vue3终于来了,好用与否,见仁见智,不过目前而言,个人还是更倾向于使用 react
偏好 react 理由(仅为个人观点)
- 1.react 有更加完善的hooks;vue3借鉴了react的hooks,但不完全
- 2.Fiber调度机制,优秀的一匹~
- 3.vue2 GC 不是很好,因为模版编译使用 with(ctx)、vue3已经修正过来,值得👍
- 4.vue 数据流管理并不如 react 般严谨,有人会说,vue我也可以做的很严格,好吧,但实际项目开发中,为了实现某些需求(尤其接手别人的代码),你不得不写一些语法糖式代码,严重破坏的自上而下的数据流,项目质量增加了一定的风险,被坑过几次,已无力吐槽~
- 5.vue 对 typescript 的支持不如 react,坑的一匹~,可以参考 Vue3-Vuex-TypeScript 踩坑之旅
- 6.react 抽象公共组件、高阶组件相较于 vue 更加灵活、方便,JSX 相较于 template,更令话,便于控制
- 7.vue 生态圈不如 react~
-
- 组件库,不言而喻,目前 antd vue 支持 Vue 3.0 的 2.0.0 测试版 已发布,香不香,看疗效,最重要一点,antd vue 是由个人维护,antd react 是由大团队技术社区维护,尤其是公司的项目,可持续性是基础 ~
-
- 无论是 recoil 还是 mobx,实际应用都优于 vuex
vuex 只能 vue 使用、因为 vuex 是 挂载在 vue上,监听数据是基于 新 new Vue 上
recoil 只能 react 使用,因为 recoil 基于react 中的 Provide、useEffect 基础上的,一共就四个API,非常简单有效,
mobx 谁都可以用,因为基于 observable 来实现的,并且 mobx 拥有自己数据流管理的功能,比如一个store,它可以自己管理自己的流程,比如 constructor中可以执行数据的一些基本化操作、when、autorun等钩子来监听数据,实际应用中,数据流完全可以在 mobx 中自行管理,外面只是引用和调用store方法,数据流管理,清晰可见,好不好,看疗效。
言归正传,还是的得学,那么咱们开始吧...
vue3 VS vue2
1.源码结构发生改变
-
vue2的所有方法都是挂载在 vue 实例上面,无论你使用与否,它们都会被打包到产出物中,监听数据是基于浏览器 Object.observer 能力,并且 observer API没有暴露出来
-
vue3 从代码结构发生改变,由 typescript 编写,每个模块可以作为一个单独的功能被外部使用,这样利于tree-shaking,并且 reactive等可以暴露给外部使用
packages 目录结构
- reactivity 响应式数据处理
-
- reactive ref effect 可以在不引用 vue 的情况下单独引用
- runtime-core 与平台无关的 runtime(VNode、渲染器 vue组件实例...)
- runtime-dom 针对于浏览器的 runtime
- complier-core 与平台无关的编译器(render)
- complier-dom 针对浏览器编译时
- complier-sfc 编译处理单文件
-
- 一个vue文件包含三部分:sfc将它们拆分出来分别处理,template => vue、script => js、style => css
- complier-srr 服务端渲染编译器
- server-renderer 服务端渲染
- shared 内部实用函数和常量共享
- size-check 私有包,用于检查 tree-shaking 后的运行时大小
- template-explorer 在线模版编译工具
- vue 主入口
2.模版编译
vue2
- template => AST => 遍历AST(查找静态节点并打上标记) => render => VNode => diff
-
- 不区分动态还是静态模版,从上到下diff,开销很大
-
- with 导致内存无法 GC,但 vue2 不得已用 with, 因为浏览器识别不了动态字符串 {{ greeting }}
vue2 template-explorer
- with 导致内存无法 GC,但 vue2 不得已用 with, 因为浏览器识别不了动态字符串 {{ greeting }}
vue2 template-explorer
-
- vue2 是在编译时进行分析优化,而react 使用 jsx => react.createElment 在运行时进行分析优化(fiber)
-
- 模版编译比较耗性能,在 template 编译成 AST的过程中,是通过正则匹配进行实现的,而正则匹配的回夙性导致性能降低
例如:匹配/xf{1,3}fz/
xffz xfffz xffffz 不停的回溯直至匹配为止,即正则贪婪特性
- optionsAPI 配置型API
new Vue({
data: {},
mounted: {},
methods: {}
})
vue3
-
template => AST => 遍历AST => 生成新的AST=> render
-
- vue3 不用with,是因为vue3采用 @babel/parser 进行编译
Vue 3 Template Explorer
- vue3 不用with,是因为vue3采用 @babel/parser 进行编译
Vue 3 Template Explorer
-
- vue3 做了静态编译优化:编译时做静态分析,把能提升的静态节点,提升到渲染函数外面,这样就不需要再创建一遍节点 例如:
<div>
<section>
<span> {{ name }}</span>
</section>
</div>
--- 动态节点 <span>{{ name }}</span> ---
当 name 发生改变,不要从上至下递归进行 diff,只需要通过 dynamicChildren 进行定向 update,这大大提高了编译性能
const vnode = {
tag: 'div',
children: [
{
tag: 'section',
children: [
{
tag: 'span',
children: ctx.name,
patchFlag: 1 // 动态的 textContent
},
],
},
],
// 这个数组收集当前 VNode 下所有的动态子节点
dynamicChildren: [
{ tag: 'span', children: ctx.name, patchFlag: 1 },
],
}
但有的情况不能采用定向更新模式,比如 v-if、v-for,它会重新创建一层 block
2.静态节点提升
3.静态属性的提升
4.事件缓存
5.静态节点超过6个,会对静态节点字符串化,这个NB了!!!
-
- 模版编译时,基于状态机来遍历字符串
- comppositionAPI: hooks型API
createApp({
setup() {}
}).mount('#app')
3.响应式系统
vue2
- 基于浏览器能力 Object.definePrototype,而它无法直接监听新增Key、深度嵌套(递归) 、数组(导致多次触发get/set)
vue3
-
创建实例开销大,每次创建一个实例,需要在this上暴露很多东西,每个暴露的属性都需要在Object.defineProperty去定义,开销大,proxy把数据定义的过程丢掉,暴露的是proxy
-
- Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,可以译为“代理器”
-
- Proxy可以监听对象本身的变化,并在变化后执行相应的操作。可以实现追踪对象,同时在数据绑定方面也很有用处 var proxy = new Proxy(target, handler);
new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为 注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象进行操作
-
基于 proxy,可以监听新增key,无法直接通过get监听深嵌套和数组,需要额外处理
-
- 数组,通过重写数组方法,拦截这些方法从而获得监听的能力
-
- 对象深嵌套,采用只有获取某一个key时,才 reactive(懒代理),解决深度嵌套的性能问题
new Proxy(target, baseHandler);
const baseHandler = {
get(target, key) {
// 收集依赖
track(target, key);
const res = Reflect.get(target, key);
return typeof res === 'object' ? reactive(res) : res;
},
set(target, key, val) {
const res = Reflect.set(target, key, val);
// 触发更新
return res;
},
apply (target, ctx, args) {
return Reflect.apply(...args);
}
};
vue3 整体流程
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
app 挂载流程
1.调用 createApp(App) 生成应用实例,然后调用mount方法
2.调用 mount 的阶段,首先根据入口组件 App 创建VNode,接着触发 render(reactive+effect) 函数,整个render的阶段,就是 Patch 所有 Vnode 的阶段,当中分为初始化挂载与更新组件 mount
3.对于内部 patch 的阶段,会对不同节点类型有不同的处理,主要是组件与element,组件就继续执行创建、挂载等等
data/props 挂到 ctx 上
代码分析:createApp => App
-
- runtime-dom/src/index:createApp -> ensureRenderer -> createRenderer
export const createApp = ((...args) => {
// 创建 app 实例对象
const app = ensureRenderer().createApp(...args)
// 重写mount方法(可以独立到不的平台实现
const { mount } = app
app.mount = (containerOrSelector) => {
// ...
// 容器处理
const proxy = mount(container)
ontainer.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
return proxy
}
return app
})
// 动态加载执行render,这样可以import 可以保证tree shake
function ensureRenderer() {
return renderer || (renderer = createRendere(rendererOptions))
}
-
- runtime-core/src/renderer: createRenderer -> baseCreateRenderer => { render, hydrate, createApp: createAppAPI(render, hydrate) }
node与element区别:所有的element元素都是node元素,继承node,node不一定是element,因为文本节点不是一个element,element是node的一种
//创建的逻辑
export function createRenderer(options) {
return baseCreateRenderer(options)
}
// 返回render渲染与createApp方法
function baseCreateRenderer(options,createHydrationFns) {
const { dom操作方法... } = options
/**
patch方法,接受新老VNode节点与对应的容器,节点的兄弟节点,父组件,是否压缩优化等等,
整个patch的过程其实就是对应的VNode深度优先遍历的过程;最终构造出一颗完整的树
*/
const patch: PatchFn = (n1,n2,container,anchor = null,parentComponent = null,
parentSuspense = null,isSVG = false,optimized = false) => {
// 存在老的VNode,并且新的VNode类型与之不同,销毁对应的旧结点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
//n1设置为null,保证后面走整个节点的mount逻辑
n1 = null
}
//节点属于没有优化的类型
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
/**
patch内部有两种情况:
1. 挂载 n1 为null
2. 更新
*/
const { type, ref, shapeFlag } = n2
switch (type) {
case Text://处理文本
case Comment://处理注释
case Static://处理静态节点
case Fragment://处理Fragment元素</>
default: //elemment 处理DOM元素
if (shapeFlag & ShapeFlags.ELEMENT) {
// 元素处理逻辑
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 组件处理逻辑
/**
内部主要做了以下几件事情:
STEP: 1.判断是挂载组件,还是更新组件,挂载调用mountComponent,更新调用updateComponent
STEP: 2.对于首次挂载的吗mountedComponent内部,
1.创建组件实例,根据组件VNode
2.设置组件实例,调用setup方法,以及处理options等等、
3.触发并且运行带有副作用的渲染函数,内部会结合使用Effect与render的调用
STEP: 3. 渲染真实逻辑
1. 生成对应的subTree,因为组件会是其他子组件的情况,这个过程是调用render的过程,然后生成对应的subTree
2. 把对应的subTree渲染到container中,通过执行对应的patch,然后再次进入到这个函数,如果patch是element,就开始触发渲染任务
*/
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理TELEPORT
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理SUSPENSE
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
}
}
// 处理文本
const processText = (n1, n2, container, anchor) => {}
// 处理注释
const processCommentNode: ProcessTextOrCommentFn (n1,n2,container,anchor)
// 处理静态
const mountStaticNode = (n2,container,anchor,isSVG)
// 处理Fragment元素</>
const processFragment = (...) {}
// 处理element: 1.首次初始化挂载 2.更新
const processElement = (n1, n2) => {
if (n1 == null) {
// 初始化挂载节点element
mountElement(n2,container,...)
} else {
// 更新节点的过程
patchElement(n1, n2, ...)
}
}
// 挂载element节点
const mountElement = (vnode,container,...) => {
// 新建 DOM 元素节点,直接调用options的对应平台方法
el = vnode.el = hostCreateElement(vnode.type,isSVG,props && props.is)
// 处理文本节点
hostSetElementText(el, vnode.children as string);
// 子节点是数组
mountChildren(vnode.children,el,null,parentComponent,...)
// 处理props的情况,class style event等等
hostPatchProp(...)
// 把创建的 element,插入到节点中
hostInsert(el, container, anchor)
//
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
needCallTransitionHooks && transition!.enter(el)
dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
// 当子节点是数组的时候,需要遍历处理patch
const mountChildren = (children,container,anchor,...) => {
patch(....)
}
// 更新element节点,主要是更新props和子节点,内部还涉及到动态patch更新
const patchElement()
// 通过动态black tree触发的修改
const patchBlockChildren = (...) => {}
// 更新一个节点的props,可能是class style、event等等
const patchProps = (...) => {}
// 组件处理逻辑,判断子组件是否需要更新,如果是则递归执行子组件的副作用渲染函数,否则直接更新VNode
const processComponent() {
// 组件初始化挂载
if (是否需要更新) {
mountComponent(...)
} else {
// 更新
updateComponent(...)
}
}
// 挂载组件
const mountComponent = (...) {
// 1.创建组件实例
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
))
// 2.设置组件实例
setupComponent(instance)
// 3.设置并且运行带有副作用的渲染函数
setupRenderEffect(instance,...)
}
// 更新组件:dom diff更新dom来触发
const updateComponent = (n1: VNode, n2: VNode,...) => {
const instance = (n2.component = n1.component)!
// 根据新旧子组件VNode判断是否需要跟新子组件
if (shouldUpdateComponent(n1, n2, optimized)) {
// 更新对应组件实例的next为新的VNode
//调用子组件的渲染函数
instance.update()
} else {
// 不需要更新,只需要复制新老组件的属性
}
}
// 运行带有副作用的render函数
const setupRenderEffect = (instance,...) => {
// 创建响应式的副作用渲染函数,当组件内部数据发生变化,会重新执行componentEffect方法内部逻辑,并且挂载到组件实例的update方法上,后续更新执行
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 第一次组件挂载,初始化渲染
// 1.生成对应的subTree
// 2.将对应的subTree渲染到container中
// 渲染组件生成子树VNode,
const subTree = (instance.subTree = renderComponentRoot(instance))
} else {
// 把对应的subTree渲染到container中,子树可能是element、text、component等
path(null,subTree,container,...)
}
})
}
// 更新组件实例的对应信息,比如组件的props、Vnode指向,同时更新VNode节点指向的组件实例
const updateComponentPreRender = (instance,nextVNode,...) => {}
/** 更新子节点:
1.如果旧结点是文本
1.新节点是文本,直接替换
2.新节点是空,直接删除
3.新节点是数组,清空文本,添加数组
2.如果旧结点为空
1.新节点是文本,添加文本
2.新节点是空,不做处理
3.新节点是数组,添加多个节点
3.如果旧结点是数组
1.新节点是文本,删除节点,添加文本
2.新节点是空,删除旧节点
3.新节点是数组,需要完整diff子节点
*/
const patchChildren: PatchChildrenFn = () => {}
// diff,数组子节点发生变更,主要是,更新、删除、添加、移动几种方式处理
const patchKeyedChildren = () => {}
// 渲染和挂载的流程
const render = (vnode, container) => {
// 卸载
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 创建或者更新组件
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs();
// 缓存VNode节点(已经渲染过)
container._vnode = vnode
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
-- runtime-core/src/apiCreateApp: createAppAPI
export function createAppAPI(render,hydrate) {
// 接受两个参数,根组件的对象与props,默认为null
return function createApp(rootComponent, rootProps = null) {
// 创建应用context
const context = createAppContext();
/**
ctx = { app, config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
isCustomElement: NO,
errorHandler: undefined,
warnHandler: undefined
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
}
*/
// 所有插件
const installedPlugins = new Set();
let isMounted = false
// app实例
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent,
_props: rootProps,//props
_container: null,//挂载容器
_context: context,
version,
get config() {
return context.config
},
// 通过 use 挂载中间件,
use(plugin, ...options) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
return app;
},
// 用mixin
mixin(mixin) {
context.mixins.push(mixin)
return app
},
// 在 app 实例上定义组件
component(name, component) {
if (!component) {
return context.components[name]
}
context.components[name] = component
return app
},
//定义指令
directive(name, directive) {
if (!directive) {
return context.directives[name]
}
context.directives[name] = directive
return app
},
// 挂载,组件核心渲染逻辑
mount(rootContainer, isHydrate) {
if (!isMounted) {
// 没有挂载,创建根节点的VNode
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
);
// 绑定上下文
vnode.appContext = context
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 实例化触发render,使用渲染器渲染VNode,传入VNode与container容器
render(vnode, rootContainer)
}
isMounted = true
app._container = rootContainer
return vnode.component!.proxy
}
},
//卸载
unmount() {
if (isMounted) {
render(null, app._container)
}
}
provide(key, value) {
context.provides[key as string] = value
return app
}
}
}
数据响应式分析
- reactive: 把复杂(object、array等)类型数据并返回响应式数据proxy
- ref: 把基本(number、string等)类型数据并返回响应式数据proxy
- effect(fn):监听数据变化处理函数
当初始化时,执行fn,收集依赖 => 渲染页面
过程:render => VNode => patch => mount
当fn中的响应式数据发生变化时,effect 重新执行 fn => 更新 => 渲染页面
过程:render => VNode => patch => diff => update,例如
const state = reactive({ count: 1 })
effect(() => {
const count = state.count;
console.log(count);
})
reactive
- get: 收集依赖 track
- set: 触发更新 trigger
// 将原始数据转化成响应数据
export const proxyMap = new WeakMap<Target, any>()/
export function reactive(target: object) {
// 只读数据,直接返回
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
// 创建响应式数据
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject(target, isReadonly, mutableHandlers, mutableCollectionHandlers) {
// 如果不是 Object,直接返回target,也就说明,你要监听的数据传值必须是 {}
// ...
// 如果已经是响应式数据,并且不是只读的,直接返回
// ...
// 如果原始数据已经有代理数据,从 proxyMap中找到相应代理后的数据
const existingProxy = proxyMap.get(target)
if (existingProxy) { return existingProxy }
// 没找到,就处理代理数据
const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers)
// 将 proxy 插入 proxyMap 中留存
proxyMap.set(target, proxy);
return proxy
}
// ------- handler --------
// mutableHandlers
export const base: ProxyHandler<object> = {
get:createGetter,// 收集依赖
set, // 发布订阅,触发更新
deleteProperty,
has,
ownKeys
}
function createGetter() {
return function get(target: Target, key: string | symbol, receiver: object) {
// 如果是数组,则需要特殊处理,因为数组变化可能导致多次set、get触发
const targetIsArray = isArray(target);
// 数组调用内置方法,而非原型链上的方法
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 其他类型
const res = Reflect.get(target, key, receiver)
// 如果是对象
/**
data: {
bar: {
foo: 1
},
car: {
price: 100
}
}
1.当执行 data.bar 时,只会将 bar 转化为 reactive
2.当执行 data.bar.foo 时,才会将 foo 转化 reactive
*/
if (isObject(res)) {
// 如果是对象,reactive,说白了就是递归 reactive,但并不是一开始就递归所有key => reactive,只有当获取某一个key时,才 reactive
return isReadonly ? readonly(res) : reactive(res)
}
// 不是只读数据,开始收集依赖
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
return res;
}
}
// 重写数组,直接重写方法,规避多次触发 get/set 问题
const arrayInstrumentations: Record<string, Function> = {}
// 重写获取数据方法
(['includes', 'indexOf', 'lastIndexOf'] as const).forEach((key) => {
// 获取 数组 原本的方法
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 获取代理数据的原始数据
const arr = toRaw(this)
// 收集依赖
for (let i = 0, l = this.length; i < l; i++) {
track(arr, ‘get’, i + '')
}
// 第一次执行,可能参数是响应式数据的情况
const res = method.apply(arr, args)
if (res === -1 || res === false) {
// 数据被代理的情况,查找原始数据,重新处理
return method.apply(arr, args.map(toRaw))
} else {
return res
}
})
(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach((key) => {
// ....
}
function createSetter(shallow = false) {
return function set(target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 获取到原来的值
const oldValue = (target as any)[key]
if (!shallow) {
// 处理对应 ref 数据的情况
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// ref的时候,需要修改value来触发对应数据的内部方法
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 判断这个 key 是新添加的,还是修改的
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 触发修改逻辑
const result = Reflect.set(target, key, value, receiver)
// receiver 为 Proxy 或者继承 Proxy 的对象,
// 这里需要处理原型链的情况,因为如果原型链继承的也是一个proxy,通过Reflect.set修改原型链上的属性会触发两次setter
if (target === toRaw(receiver)) {
if (!hadKey) {
//trigger 派发通知
trigger(target, 'add', key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, 'set', key, value, oldValue)
}
}
return result
}
}
effect
- effect(() => state.name) 过程
-
- 初始化执行,fn => 包装 createReactiveEffect(fn) => activeEffect
-
- fn() => 触发get => track收集依赖
/**
* effect 包装依赖函数的逻辑,返回 activeEffect
* fn 具体要执行的函数
* options 配置项
*/
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
// 如果fn已经是一个被 effect 包装过的函数,那么就直接指向原始函数
if (isEffect(fn)) {
fn = fn.raw
}
/**
* 创建一个包装逻辑
* 因为需要在数据获取的时候,收集依赖,那就应该在执行之前,把处理逻辑赋值给reactiveEffect
* 当 effect 内部函数执行的时,内部获取数据的逻辑,就可以直接添加依赖
*/
const effect = createReactiveEffect(fn, options)
//判断是否需要lazy,如果不是lazy,直接执行一次
if (!options.lazy) {
effect()
}
//返回包装后的函数
return effect
}
// effect栈
const effectStack: ReactiveEffect[] = [
// 包装函数
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
// 在 effect 没有被激活并且没有调度选项,则直接执行fn
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
// 在初始化执行时,effectStack 会先收集 effect,再执行 fn
// 因为 在执行 fn 的过程中,修改对应数据会触发执行 effect,通过 effectStack 可以避免这个问题
if (!effectStack.includes(effect)) {
// console.log(effect.deps, '当前数据执行过程中的依赖')
// 清空 effect.dep 中所依赖的 effect,避免多次渲染,
/** 为什么要清空 effect.dep 依赖
const state = reactive({
show:true,
name:'xfz',
default: 'girl'
});
// 监听 开始执行,依赖收集,收集了show与name的字段,映射到effect上,
effect(function(){
if(state.show){
console.log(state.name)
} else {
console.log(state.default)
}
})
// 修改 show
state.show = false // 输出 girl
// 此时如果不清空dep里面的所有依赖,修改 name
state.name = 'zc' // 会再次输出 girl,其实不应该再次执行fn,因为这样会造成多次渲染
*/
cleanup(effect)
try {
// 开启允许依赖收集
enableTracking()
// effect 入栈
effectStack.push(effect)
// 设置激活的 effect
activeEffect = effect
// 执行fn逻辑,触发内部数据依赖的收集,开始收集 activeEffect
return fn(); // fn() => 触发get => track收集依赖
} finally {
// 出栈
effectStack.pop()
// 恢复之前的状态
resetTracking()
// 指向栈最后一个effect
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true // 表示是一个 effect 函数
effect.active = true // 激活状态
effect.raw = fn // 原始的函数
effect.deps = [] // effect对应的依赖 ,一个effect里面,可能存在多个监听数据
effect.options = options
return effect
}
// 清除对应的依赖执行
function cleanup(effect: ReactiveEffect) {
const { deps } = effect; // 一个effect可能有多个数据依赖
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
let shouldTrack = true // 是否应该收集依赖
const trackStack: boolean[] = [] // 控制收集依赖的多次嵌套的状态
// 暂停依赖的收集
export function pauseTracking() {
trackStack.push(shouldTrack)
shouldTrack = false
}
// 开始依赖的收集
export function enableTracking() {
trackStack.push(shouldTrack)
shouldTrack = true
}
// 回退到上一次依赖的收集
export function resetTracking() {
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
}
track 收集依赖
// { [’target1‘]: depsMap }
const targetMap = new WeakMap<any, KeyToDepMap>();
/**
* 收集依赖
* target 原始数据
* type 触发的类型,如 get、add
* key 获取数据具体的某一个key值
*/
export function track(target: object, type: TrackOpTypes, key: unknown) {
// activeEffect 数据发生变化之后,需要做的事情
// 当前依赖不应该收集,或者没有对应激活的effect,直接返回,不收集
if (!shouldTrack || activeEffect === undefined) {
return
}
/*
{
target<原始数据>:{
key<对应的key>:[effect]<当前key数据,所有关联的监听使用的地方,一个key,可能多个地方使用,就会存在一个key对应对个effect>
}
}
*/
let depsMap = targetMap.get(target)
if (!depsMap) {
// 每一个target,对应一个depsMap
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// depsMap中维护了对应的key到dep的集合
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的effect作为依赖
dep.add(activeEffect)
// 当前激活的effect,收集dep作为依赖,当前effect内部有可能触发其他effect
activeEffect.deps.push(dep)
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
trigger 触发更新,派发通知
export function trigger(target, type, key, newValue, oldValue, oldTarget) {
// 获取到对应数据原始数据的依赖集合
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有被收集,直接返回
return
}
// 创建需要运行的effect集合
const effects = new Set<ReactiveEffect>()
// 定义遍历添加effect的函数
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
//触发了对应的操作,修改、删除、添加,都添加effect依赖到effects中
if (type === TriggerOpTypes.CLEAR) {
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
add(depsMap.get(key))
}
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
// 定义执行函数
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
// 如果有调度函数,就先执行调度函数
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
//没有的话,直接执行
effect()
}
}
effects.forEach(run);//遍历执行effect
}
ref
// 创建ref数据,处理基本类型数据的监听
export function ref(value?: unknown) {
return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
//如果是ref,直接返回
if (isRef(rawValue)) {
return rawValue
}
// 创建ref
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue) //处理数据的转换,如果是复杂数据,修改为Reactive的情况
}
//通过设置.value的情况,来触发整个track的过程
get value() {
//收集依赖
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
if (hasChanged(toRaw(newVal), this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
// 派发通知
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
模版编译
compile -> parse -> AST -> render
export function compile(template, options {
return baseCompile(template, extend({}, parserOptions, options, {...})
}
export function baseCompile(template,options) {
// 通过baseParse编译生成AST
const ast = isString(template) ? baseParse(template, options) : template
// 转换对应的AST
transform(ast,extend({}, options, {...}));
return ast;
}
调度
- 维护一个promise队列,通过 push 加入任务,flush执行任务
const queue: SchedulerJob[] = []
export function nextTick(
this: ComponentPublicInstance | void,
fn?: () => void
): Promise<void> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
---------------------------------------------
export function queueJob(job: SchedulerJob) {
if (
(!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
job !== currentPreFlushParentJob
) {
queue.push(job)
queueFlush()
}
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
尾声
感觉好累,感觉代码好乱,不想再看了,以后希望不要用这玩意,我还是研究 react 去把~~
后续文章
❤️ 加入我们
字节跳动 · 幸福里团队
Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者
期待您的加入,一起用技术改变生活!!!