cisen / blog

Time waits for no one.
134 stars 20 forks source link

vue3 源码相关 #744

Open cisen opened 4 years ago

cisen commented 4 years ago

总结

总体流程

  1. proxy拦截接受到改变
  2. 触发trigger/track,从targetMap[target][depMaps][key]里面找到所有依赖该dep的effect,并执行
  3. 执行过程中出现递归,则将effect压入effectStack

资源收集

总体流程

mount

属性

data下面的数据可以直接被模板使用

computed 封装html变量,data下面的数据改变才会重新运算computed(依赖data)

watch 监控data

computed VS watch

methods render时执行

指令

指令缩写

v-bind

v-on

v-model

component

directive 自定义指令

cisen commented 4 years ago

问答

如何解析html的指令?

如何拦截html的解释?

如何将html转化为AST的?

createApp函数在哪里?

computed是如何实现的 ? mount

render的时候

watch是如何实现的? mount

udpate

从哪里开始循环遍历vnode的?

remaining (todomvc-classic.html:115)
run (vue.global.js:3975)
reactiveEffect (vue.global.js:3958)
get value (vue.global.js:4161)
get (vue.global.js:3567)
get (vue.global.js:7039)
get (todomvc-classic.html:120)
run (vue.global.js:3975)
reactiveEffect (vue.global.js:3958)
get value (vue.global.js:4161)
get (vue.global.js:3567)
get (vue.global.js:7039)
render (VM68:51)
renderComponentRoot (vue.global.js:4501)
componentEffect (vue.global.js:6062)
run (vue.global.js:3975)
reactiveEffect (vue.global.js:3958)
effect (vue.global.js:3943)
setupRenderEffect (vue.global.js:6060)(这里如果有子则从新调用patch,开始遍历vnode树,没有则执行下去)
mountComponent (vue.global.js:6052)
processComponent (vue.global.js:5981)
patch (vue.global.js:5712)(循环的入口)
render (vue.global.js:6543)
mount (vue.global.js:5621)
app.mount (vue.global.js:8309)

(anonymous) (todomvc-classic.html:199)

effect是如何存储到Vnode的?

vnode和instance的关系是什么?

effect是什么?

effect是用来做什么的?

会有全局的effect-list嘛?

effect跟computed/watcher有什么关系?

PublicInstanceProxyHandlers做什么用的?

为什么要把computed/watch/methods/inject的effect都存入renderContext?

因为有上一条,说以computed/watch/methods/inject的名称key不能相同?

target/dep/effect有什么关系? 捕获235432432

Proxy是如何到effect的?

为什么effect要存一份到instance.effects?

proxy的创建和effect的创建有何不同?

computed/methon是什么时候被存入targetMap?

computed的effect如何跟dep关联的?

dep是一个Set?dep从哪里添加effect的?

effectStack是什么?跟targetMap有什么关系

track(target, type, key)函数的作用

父子组件共用targetMap和effectStack

为什么要使用effectStack栈来存储effect?

vue3的schedule是怎样的?

vue3如何将vnode变更应用到真实dom?

vue3 diff 算法在哪里?

ssrTransformIf做了什么?

cisen commented 4 years ago

数据结构

instance

const instance = {
        vnode,
        parent,
        appContext,
        type: vnode.type,
        root: null,
        next: null,
        subTree: null,
        update: null,
        render: null,
        renderProxy: null,
        propsProxy: null,
        setupContext: null,
        // 脏操作数组
        effects: null,
        provides: parent ? parent.provides : Object.create(appContext.provides),
        accessCache: null,
        renderCache: null,
        // setup context properties
        renderContext: EMPTY_OBJ,
        data: EMPTY_OBJ,
        props: EMPTY_OBJ,
        attrs: EMPTY_OBJ,
        slots: EMPTY_OBJ,
        refs: EMPTY_OBJ,
        // per-instance asset storage (mutable during options resolution)
        components: Object.create(appContext.components),
        directives: Object.create(appContext.directives),
        // async dependency management
        asyncDep: null,
        asyncResult: null,
        asyncResolved: false,
        // user namespace for storing whatever the user assigns to `this`
        // can also be used as a wildcard storage for ad-hoc injections internally
        sink: {},
        // lifecycle hooks
        // not using enums here because it results in computed properties
        isUnmounted: false,
        isDeactivated: false,
        bc: null,
        c: null,
        bm: null,
        m: null,
        bu: null,
        u: null,
        um: null,
        bum: null,
        da: null,
        a: null,
        rtg: null,
        rtc: null,
        ec: null,
        emit: (event, ...args) => {
            const props = instance.vnode.props || EMPTY_OBJ;
            const handler = props[`on${event}`] || props[`on${capitalize(event)}`];
            if (handler) {
                callWithAsyncErrorHandling(handler, instance, 6 /* COMPONENT_EVENT_HANDLER */, args);
            }
        }
    };

effect的数据结构

 // effect就是添加这些属性的computed的getter函数。fn就是getter
  function createReactiveEffect(fn, options) {
      const effect = function reactiveEffect(...args) {
          return run(effect, fn, args);
      };
      effect._isEffect = true;
      effect.active = true;
      effect.raw = fn;
      effect.deps = [];
      effect.options = options;
      return effect;
  }
cisen commented 4 years ago

浅谈vue3中effect与computed的亲密关系 https://www.jb51.net/article/171534.htm

在我刚看完vue3响应式的时候,心中就有一个不可磨灭的谜团,让我茶不思饭不想,总想生病。那么这个谜团是什么呢?就是在响应式中一直穿行在tranger跟track之间的effect。如果单纯的响应式原理根本就用不上effect,那么effect到底是干什么的呢?

船到桥头自然直,柳岸花明又一村。苦心人天不负,偶然间我看到了effect测试代码用例!

it('should observe basic properties', () => {
 let dummy
 const counter = reactive({ num: 0 })
 effect(() => (dummy = counter.num))

 expect(dummy).toBe(0)
 counter.num = 7
 expect(dummy).toBe(7)
})

解释一下,这段代码

首先声明dummy变量,然后在effect的回调中把已响应的对象counter的num属性赋值给dummy 然后做断言判断 dummy是否等于 0 将 counter.num 赋值 7 ,然后 dummy 也变成了 7 ! 这,,,让我想到了什么??

这就是computed的吗? 赶紧看下 computed 的测试用例!

const value = reactive<{ foo?: number }>({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)

哈哈哈

阿哈哈哈哈

hhhhhhhhhhhhhhhhhhhh

忍不住想仰天长啸!!

果然跟我猜想的一样!!!我终于直到effect是个什么鬼了,顾名思义effect是副作用的意思,也就是说它是响应式副产品,每次触发了 get 时收集effect,每次set时在触发这些effects。这样就可以做一些响应式数据之外的一些事情了,比如计算属性computed。

让我们用effect实现一个computed 可能会更清晰一点 我就不写一些乱七八糟的判断了,让大家能够看的更加清楚

function computed (fn) {
 let value = undefined
 const runner = effect(fn, {
  // 如果lazy不置为true的话,每次创建effect的时候都会立即执行一次
  // 而我们要实现computed显然是不需要的
  lazy: true
 })
 // 为什么要使用对象的形式,是因为我们最后需要得到computed的值
 // 如果不用对象的 get 方法的话我们就需要手动再调用一次 computed() 
 return {
  get value() {
   return runner()
  }
 }
}

// 使用起来是这样的

const value = reactive({})
const cValue = computed(() => value.foo)
value.foo = 1

console.log(cValue.value) // 1

这也太简单了吧,那么重点来了,effect怎么实现的呢? 别着急,我们先捋一下逻辑

首先 如果 effect 回调内有已响应的对象被触发了 get 时,effect就应该被储存起来 然后,我们需要一个储存effect的地方,在effect函数调用的时候就应该把effect放进这个储存空间,在vue中使用的是一个数组activeReactiveEffectStack = [] 再后,每个target被触发的时候,都可能有多个effect,所以每个target需要有一个对应的依赖收集器 deps,等到 set 时遍历 deps 执行 effect() 然而,这个依赖收集器 deps 不能放在 target 本身上,这样会使数据看起来不是很简洁,还会存在多余无用的数据,所以我们需要一个 map 集合来储存 target 跟 deps 的关系, 在vue中这个储存集合叫 targetMap 。

几个概念 track 追踪器,在 get 时调用该函数,将所有 get 的 target 跟 key 以及 effect 建立起对应关系

// 比如 const react = reactive({a: { b: 2 })
// react.a 时 target -> {a: { b: 2 } key -> a 
// targetMap 储存了 target --> Map --> key --> Set --> dep --> effect 
// 当调用 react.a.b.c.d.e 时 depsMap
// {"a" => Set(1)} --> Set --> effect
// {"b" => Set(1)}
// {"c" => Set(1)}
// {"d" => Set(1)}
// {"e" => Set(1)}
export function track(target: any, key: string) {
 const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1];
 if (effect) {
  let depsMap = targetMap.get(target);
  if (depsMap === void 0) {
   targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key!);
  if (!dep) {
   depsMap.set(key!, (dep = new Set()));
  }
  if (!dep.has(effect)) {
   dep.add(effect);
   effect.deps.push(dep);
  }
 }
}

trigger 触发器,这个就比较好理解了,拿到target key下的对应的所有 effect,然后遍历执行 effect()

export function trigger(target: any, key?: string | symbol) {
 const depsMap: any = targetMap.get(target);
 const effects: any = new Set()
 if (depsMap && depsMap.get(key)) {
  depsMap.get(key).forEach((dep: any) => {
   effects.add(dep)
  });
  effects.forEach((e: any) => e())
 }
}

effect 函数实现

// 暴露的 effect 函数
export function effect(
 fn: Function,
 options: any = EMPTY_OBJ
): any {
 if ((fn as any).isEffect) {
  fn = (fn as any).raw
 }
 const effect = createReactiveEffect(fn, options)
 // 如果不是 lazy,则会立即执行一次
 if (!options.lazy) {
  effect()
 }
 return effect
}

// 创建 effect
function createReactiveEffect(
 fn: Function,
 options: any
): any {
 const effect = function effect(...args: any): any {
  return run(effect as any, fn, args)
 } as any
 effect.isEffect = true
 effect.active = true
 effect.raw = fn
 effect.scheduler = options.scheduler
 effect.onTrack = options.onTrack
 effect.onTrigger = options.onTrigger
 effect.onStop = options.onStop
 effect.computed = options.computed
 effect.deps = []
 return effect
}

// 执行函数,执行完之后会将储存的 effect 删除
// 这是函数 effect 的所有执行,所经历的完整的声明周期
function run(effect: any, fn: Function, args: any[]): any {
 if (!effect.active) {
  return fn(...args)
 }
 if (activeReactiveEffectStack.indexOf(effect) === -1) {
  try {
   activeReactiveEffectStack.push(effect)
   return fn(...args)
  } finally {
   activeReactiveEffectStack.pop()
  }
 }
}

一口气写了这么多,最后总结一下。在大家看源码的时候,如果发现有哪个地方无从下手的话,可以先从测试用例开始看。因为测试用例可以很清楚的知道这个函数想要达到什么效果,然后从效果上想,为什么这么做,如果我自己写的话应该怎么写,这样一点点就能揣摩出作者的意图了。再根据源码结合自己的想法你就能够学到很多。

cisen commented 3 years ago

SSR

hydration是什么?

为什么要用Function('require', code)(require))

travels的实现在哪里?

transform是在哪里分发

html的ast结构是怎样的?

export interface Position {
  // 文件开始的便宜
  offset: number // from start of file
  // 第几行
  line: number
  // 第几列
  column: number
}
export interface SourceLocation {
   // 代码开始的位置
  start: Position
  end: Position
  source: string
}
export interface BaseElementNode extends Node {
  // 代码位置对象
  loc: SourceLocation
  type: NodeTypes.ELEMENT
  ns: Namespace
  // html标签名
  tag: string
  tagType: ElementTypes
  isSelfClosing: boolean
  props: Array<AttributeNode | DirectiveNode>
  children: TemplateChildNode[]
}

指令是在哪里?如何解析执行的?

traverseNode做了什么?

为什么会有codegen?

codegen之后是做什么?

SSR生命周期在哪里执行的?