Git Product home page Git Product logo

mini-vue's Introduction

CN / EN

mini-vue github

实现最简 vue3 模型,用于深入学习 vue3, 让你更轻松的理解 vue3 的核心逻辑

Usage

B 站 提供了视频讲解使用方式

Why

当我们需要深入学习 vue3 时,我们就需要看源码来学习,但是像这种工业级别的库,源码中有很多逻辑是用于处理边缘情况或者是兼容处理逻辑,是不利于我们学习的。

我们应该关注于核心逻辑,而这个库的目的就是把 vue3 源码中最核心的逻辑剥离出来,只留下核心逻辑,以供大家学习。

How

基于 vue3 的功能点,一点一点的拆分出来。

代码命名会保持和源码中的一致,方便大家通过命名去源码中查找逻辑。

Tasking

runtime-core

  • 支持组件类型
  • 支持 element 类型
  • 初始化 props
  • setup 可获取 props 和 context
  • 支持 component emit
  • 支持 proxy
  • 可以在 render 函数中获取 setup 返回的对象
  • nextTick 的实现
  • 支持 getCurrentInstance
  • 支持 provide/inject
  • 支持最基础的 slots
  • 支持 Text 类型节点
  • 支持 $el api
  • 支持 watchEffect

reactivity

目标是用自己的 reactivity 支持现有的 demo 运行

  • reactive 的实现
  • ref 的实现
  • readonly 的实现
  • computed 的实现
  • track 依赖收集
  • trigger 触发依赖
  • 支持 isReactive
  • 支持嵌套 reactive
  • 支持 toRaw
  • 支持 effect.scheduler
  • 支持 effect.stop
  • 支持 isReadonly
  • 支持 isProxy
  • 支持 shallowReadonly
  • 支持 proxyRefs

compiler-core

  • 解析插值
  • 解析 element
  • 解析 text

runtime-dom

  • 支持 custom renderer

runtime-test

  • 支持测试 runtime-core 的逻辑

infrastructure

  • support monorepo with pnpm

build

pnpm build

example

通过 server 的方式打开 packages/vue/example/* 下的 index.html 即可

� 推荐使用 Live Server

初始化

流程图

初始化流程图

可加 vx:cuixr1314 获取所有脑图(备注:github mini-vue 领取脑图)

关键函数调用图

关键函数调用图2

可以基于函数名快速搜索到源码内容

update

流程图

image

关键函数调用图

image

可以基于函数名快速搜索到源码内容

从零到一实现一遍

自从有了 mini-vue 之后 很多同学都问我 能不能带着他从零到一敲一遍

因为对于源码的学习来讲 看在多遍也不如自己写一遍

为此我把 mini-vue 做成了一套视频课 从零到一带着大家实现一遍 不跳过任何一行代码

当然除了功能上的实现还有编程**融入到了课程内

比如 TDD、小步走、重构手法、TPP

TDD 测试驱动开发 影响了我整个技术生涯 可以说在我认识到 TDD 之后 技术才有了质的飞跃

课程目录如下:

  1. vue3 源码结构的介绍
  2. reactivity 的核心流程
  3. runtime-core 初始化的核心流程
  4. runtime-core 更新的核心流程
  5. setup 环境 -> 集成 jest 做单元测试 & 集成 typescript
  6. 实现 effect 返回 runner
  7. 实现 effect 的 scheduler 功能
  8. 实现 effect 的 stop 功能
  9. 实现 readonly 功能
  10. 实现 isReactive 和 isReadonly
  11. 优化 stop 功能
  12. 实现 reactive 和 readonly 嵌套对象转换功能
  13. 实现 shallowReadonly 功能
  14. 实现 isProxy 功能
  15. 实现 isProxy 功能
  16. 实现 ref 功能
  17. 实现 isRef 和 unRef 功能
  18. 实现 proxyR 功能
  19. 实现 computed 计算属性功能
  20. 实现初始化 component 主流程
  21. 实现 rollup 打包
  22. 实现初始化 element 主流程
  23. 实现组件代理对象
  24. 实现 shapeFlags
  25. 实现注册事件功能
  26. 实现组件 props 功能
  27. 实现组件 emit 功能
  28. 实现组件 slots 功能
  29. 实现 Fragment 和 Text 类型节点
  30. 实现 getCurrentInstance
  31. 实现依赖注入功能 provide/inject
  32. 实现自定义渲染器 custom renderer
  33. 更新 element 流程搭建
  34. 更新 element 的props
  35. 更新 element 的 children
  36. 双端对比 diff 算法1
  37. 双端对比 diff 算法2 - key 的作用
  38. 双端对比 diff 算法3 - 最长子序列的作用
  39. 学习尤大解决 bug 的处理方式
  40. 实现组件更新功能
  41. 实现 nextTick 功能
  42. 编译模块概述
  43. 实现解析插值功能
  44. 实现解析 element 标签
  45. 实现解析 text 功能
  46. 实现解析三种联合类型 template
  47. parse 的实现原理&有限状态机
  48. 实现 transform 功能
  49. 实现代码生成 string 类型
  50. 实现代码生成插值类型
  51. 实现代码生成三种联合类型
  52. 实现编译 template 成 render 函数
  53. 实现 monorepo & 使用 vitest 替换 jest

课程内部包含了 vue3 的三大核心模块:reactivity、runtime 以及 compiler 模块

等你自己手写一遍之后 在去看 vue3 源码或者再去看分析解析 vue3 源码的书籍时你会有不同的体验

除此之外 还录制了课程介绍以及课程试听课

可以直接购买 也可以加我 wx: cuixr1314 来咨询这门课是否合适你

除了课程内容以外 还有专门的社群来答疑大家在学习上的问题 😊

mini-vue's People

Contributors

1172208932 avatar bigfatdone avatar cuixiaorui avatar dependabot[bot] avatar kunogi avatar leno23 avatar liubei90 avatar meglody avatar qinran0423 avatar ranieryu avatar talljack avatar wxsms avatar xuhongbo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

mini-vue's Issues

建议优化

不知道如何打开example/index.html,个人建议将vscode 中拓展程序 搜索live server步骤放上去

issues初尝试

for (let index = 0; index < newIndexToOldIndexMap.length; index++) {

for (let index = 0; index < newIndexToOldIndexMap.length; index++) {
      // 源码里面是用 0 来初始化的
      // 但是有可能 0 是个正常值
      // 我这里先用 -1 来初始化
      newIndexToOldIndexMap[index] = -1;
    }

这里初始化的index,在判断是否为新加节点的时候,无法对应上后面使用的c2[]的Index。
在patchChildren用例中,把测试用例改为
const prevChildren = [
  h("div", { key: "jjj", id: "old-jjj" }, "jjj"),
  h("div", { key: "z", id: "old-z" }, "z"),
  h("div", { key: "a", id: "old-a" }, "a"),
  h("div", { key: "b", id: "old-b" }, "b"),
  h("div", { key: "c", id: "old-c" }, "c"),
];
const nextChildren = [
  h("div", { key: "jjj", id: "new-jjj" }, "jjj"),
  h("div", { key: "z", id: "new-z" }, "z"),
  h("div", { key: "b", id: "new-b" }, "b"),
  h("div", { key: "a", id: "new-a" }, "a"),
  h("div", { key: "add-j", id: "new-j" }, "add-j"),
  h("div", { key: "add-jj", id: "new-jj" }, "add-jj"),
  h("div", { key: "c", id: "new-c" }, "c"),
];
就会报错。
render代码段改为
 for (let value of keyToNewIndexMap.values()) {
      newIndexToOldIndexMap[value] = -1
    }
后报错消失

expected allow nested effects ?

Hi~ Dear xiaorui, run this follow code got some error

const foo = reactive({
    count: 0,
    name: "cui",
  });

  effect(() => {
    effect(() => {
      console.log("inside effect: ", foo.name);
    });
    console.log("outside effect: ", foo.count);
  });

  foo.count++;

running example code catch an error like this:

Uncaught TypeError: dep is not iterable (cannot read property undefined)

So. expected author add feature to allow nested effects ?

组件更新逻辑没写

// 组件的更新
function updateComponent(n1, n2, container) {
// TODO
console.log("更新组件", n1, n2);
}

component.ts里面的一点问题

function setupStatefulComponent(instance) {
  // todo
  // 1. 先创建代理 proxy
  console.log("创建 proxy");
  // 用户声明的对象就是 instance.type
  // const Component = {setup(),render()} ....
  const Component = instance.type;
  // 2. 调用 setup

  // 调用 setup 的时候传入 props
  const { setup } = Component;
  if (setup) {
    const setupContext = createSetupContext(instance);
    const setupResult = setup && setup(instance.props, setupContext);
  }

  // 3. 处理 setupResult
  handleSetupResult(instance, setupResult);
}

这段代码处理setupResult的位置,是不是应该在if(setup)内部,else直接 finishComponentSetup(instance)?

isReactive & isReadonly test failed case

add these case in reactive test file:

    const evil = {
      __v_isReactive: true,
      __v_isReadonly: true,
    }
    expect(isReactive(evil)).toBe(false)
    expect(isReadonly(evil)).toBe(false)

expect

test pass

result

test failed

reactivity/baseHandler.ts createGetter中的优化点

const isExistInReactiveMap = () =>
      key === ReactiveFlags.RAW && receiver === reactiveMap.get(target);

const isExistInReadonlyMap = () =>
      key === ReactiveFlags.RAW && receiver === readonlyMap.get(target);

const isExistInShallowReadonlyMap = () =>
      key === ReactiveFlags.RAW && receiver === shallowReadonlyMap.get(target);

这三个常量写为函数对象似乎并没有什么实质性作用,vue3现在的代码已经实现为:

      (key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target))

也许可以优化下。

【看的见的思考】compiler-core

看的见的思考

先从compile 看起, compile 里面的 baseCompile 是整个 core 的入口函数

先看看单元测试吧

  test('function mode', () => {
    const { code, map } = compile(source, {
      sourceMap: true,
      filename: `foo.vue`
    })

    expect(code).toMatchSnapshot()
    expect(map!.sources).toEqual([`foo.vue`])
    expect(map!.sourcesContent).toEqual([source])

    const consumer = new SourceMapConsumer(map as RawSourceMap)

    expect(
      consumer.originalPositionFor(getPositionInCode(code, `id`))
    ).toMatchObject(getPositionInCode(source, `id`))

这个测试相当的大,先一点点的看

先看input source 是

  const source = `
<div id="foo" :class="bar.baz">
  {{ world.burn() }}
  <div v-if="ok">yes</div>
  <template v-else>no</template>
  <div v-for="(value, index) in list"><span>{{ value + index }}</span></div>
</div>
`.trim()

这个就是 template 了。 然后第二个参数是对应的 CompilerOptions 先不用管

在去看看输出 output

下面是 code

 const _Vue = Vue
    
    return function render(_ctx, _cache) {
      with (_ctx) {
        const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList, createElementVNode: _createElementVNode, normalizeClass: _normalizeClass } = _Vue
    
        return (_openBlock(), _createElementBlock("div", {
          id: "foo",
          class: _normalizeClass(bar.baz)
        }, [
          _createTextVNode(_toDisplayString(world.burn()) + " ", 1 /* TEXT */),
          ok
            ? (_openBlock(), _createElementBlock("div", { key: 0 }, "yes"))
            : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
                _createTextVNode("no")
              ], 2112 /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */)),
          (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (value, index) => {
            return (_openBlock(), _createElementBlock("div", null, [
              _createElementVNode("span", null, _toDisplayString(value + index), 1 /* TEXT */)
            ]))
          }), 256 /* UNKEYED_FRAGMENT */))
        ], 2 /* CLASS */))
      }
    }

就是生成的 render 函数

那map 是什么?

  {
      version: 3,
      sources: [ 'foo.vue' ],
      names: [],
      mappings: ';;;;;;0BAAA,oBAKM;MALD,EAAE,EAAC,KAAK;MAAE,KAAK,EAApB,gBAAsB,OAAO;;MAA7B,kCACK,YAAY,IAAG,GAClB;MAAW,EAAE;yBAAb,oBAAwB,SAF1B,KAAA,KAEiB,KAAG;yBAClB,oBAA8B,aAHhC,KAAA;YAAA,iBAGmB,IAAE;;yBACnB,oBAA0E,iBAJ5E,YAIgC,IAAI,EAJpC,CAIe,KAAK,EAAE,KAAK;8BAAzB,oBAA0E;UAAtC,oBAAgC,+BAAvB,aAAa',
      sourcesContent: [
        '<div id="foo" :class="bar.baz">\n' +
          '  {{ world.burn() }}\n' +
          '  <div v-if="ok">yes</div>\n' +
          '  <template v-else>no</template>\n' +
          '  <div v-for="(value, index) in list"><span>{{ value + index }}</span></div>\n' +
          '</div>'
      ]
    }

应该是 sourcemap

接下来这个 单元测试所以的点都是测试 sourcemap 的点,所以我们先不看了


那接着我们看看 baseCompile 函数是通过几个步骤生成的 code

baseCompile

export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'
  /* istanbul ignore if */
  const ast = isString(template) ? baseParse(template, options) : template
  const [nodeTransforms, directiveTransforms] =
    getBaseTransformPreset(prefixIdentifiers)
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

好,最主要的就是三个部分

  1. 生成 ast - 通过 baseParse

  2. 调用 transform - 来处理 ast

  3. 使用 generate 生成 code


baseParse

那我们先看是如何生成ast的把

也就是baseParse,还是先看单元测试

下面的逻辑是只测试的 TextNode

    test('simple text', () => {
      const ast = baseParse('some text')
      const text = ast.children[0] as TextNode

      expect(text).toStrictEqual({
        type: NodeTypes.TEXT,
        content: 'some text',
        loc: {
          start: { offset: 0, line: 1, column: 1 },
          end: { offset: 9, line: 1, column: 10 },
          source: 'some text'
        }
      })
    })

可以看到 node 对象的关键的几个属性了

接着看看是如何解析出来的把

export function baseParse(
  content: string,
  options: ParserOptions = {}
): RootNode {
  const context = createParserContext(content, options)
  const start = getCursor(context)
  return createRoot(
    parseChildren(context, TextModes.DATA, []),
    getSelection(context, start)
  )
}

这里的重点是 context 是什么

继续去看 createParserContext

function createParserContext(
  content: string,
  rawOptions: ParserOptions
): ParserContext {
  const options = extend({}, defaultParserOptions)

  let key: keyof ParserOptions
  for (key in rawOptions) {
    // @ts-ignore
    options[key] =
      rawOptions[key] === undefined
        ? defaultParserOptions[key]
        : rawOptions[key]
  }
  return {
    options,
    column: 1,
    line: 1,
    offset: 0,
    originalSource: content,
    source: content,
    inPre: false,
    inVPre: false,
    onWarn: options.onWarn
  }
}

只是生成了一个配置对象

    {
      options: {
        delimiters: [ '{{', '}}' ],
        getNamespace: [Function: getNamespace],
        getTextMode: [Function: getTextMode],
        isVoidTag: [Function: NO],
        isPreTag: [Function: NO],
        isCustomElement: [Function: NO],
        decodeEntities: [Function: decodeEntities],
        onError: [Function: defaultOnError],
        onWarn: [Function: defaultOnWarn],
        comments: true
      },
      column: 1,
      line: 1,
      offset: 0,
      originalSource: 'some text',
      source: 'some text',
      inPre: false,
      inVPre: false,
      onWarn: [Function: defaultOnWarn]
    }

接着是调用了 getCursor

function getCursor(context: ParserContext): Position {
  const { column, line, offset } = context
  return { column, line, offset }
}

数据是来自 context 里面的

继续最后一个逻辑

  return createRoot(
    parseChildren(context, TextModes.DATA, []),
    getSelection(context, start)
  )

先看 parseChildren

function parseChildren(
  context: ParserContext,
  mode: TextModes,
  ancestors: ElementNode[]
): TemplateChildNode[] {
  const parent = last(ancestors)
  const ns = parent ? parent.ns : Namespaces.HTML
  const nodes: TemplateChildNode[] = []

  while (!isEnd(context, mode, ancestors)) {
    __TEST__ && assert(context.source.length > 0)
    const s = context.source
    let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined

    if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
      if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
        // '{{'
        node = parseInterpolation(context, mode)
      } else if (mode === TextModes.DATA && s[0] === '<') {

太多了,就不copy过来了。不过激动的是这里和我们之前去刷编译原理时候处理语法的时候逻辑是一致的,解析成功的话,那么就创建一个 node 节点对象

而且是用 nodes 把所有node对象都收集起来

至于解析的规则的话,就是按照 html 的规则来的

回头去刷编译原理就好了,这里去解析 html 的套路都是一样的

现在我们只需要知道返回一个处理完的 nodes就ok了

继续去看下一个逻辑点


getSelection

function getSelection(
  context: ParserContext,
  start: Position,
  end?: Position
): SourceLocation {
  end = end || getCursor(context)
  return {
    start,
    end,
    source: context.originalSource.slice(start.offset, end.offset)
  }
}

这里就是返回对应这段代码的信息的


createRoot

export function createRoot(
  children: TemplateChildNode[],
  loc = locStub
): RootNode {
  return {
    type: NodeTypes.ROOT,
    children,
    helpers: [],
    components: [],
    directives: [],
    hoists: [],
    imports: [],
    cached: 0,
    temps: 0,
    codegenNode: undefined,
    loc
  }
}

这里的 createRoot 就是直接创建一个 root 节点给外面就ok了

而关键的 children 就是通过parseChildren生成的 nodes。

transform

看看 transform 阶段是做了什么

  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

是基于 ast 来做处理,第二个参数就是 transformOptions 了

export function transform(root: RootNode, options: TransformOptions) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
  if (options.hoistStatic) {
    hoistStatic(root, context)
  }
  if (!options.ssr) {
    createRootCodegen(root, context)
  }
  // finalize meta information
  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached

  if (__COMPAT__) {
    root.filters = [...context.filters!]
  }
}

还是先处理配置 context

看看长什么样子

 {
      selfName: null,
      prefixIdentifiers: false,
      hoistStatic: false,
      cacheHandlers: false,
      nodeTransforms: [ [Function: plugin] ],
      directiveTransforms: {},
      transformHoist: null,
      isBuiltInComponent: [Function: NOOP],
      isCustomElement: [Function: NOOP],
      expressionPlugins: [],
      scopeId: null,
      slotted: true,
      ssr: false,
      inSSR: false,
      ssrCssVars: '',
      bindingMetadata: {},
      inline: false,
      isTS: false,
      onError: [Function: defaultOnError],
      onWarn: [Function: defaultOnWarn],
      compatConfig: undefined,
      root: {
        type: 0,
        children: [ [Object] ],
        helpers: [],
        components: [],
        directives: [],
        hoists: [],
        imports: [],
        cached: 0,
        temps: 0,
        codegenNode: undefined,
        loc: {
          start: [Object],
          end: [Object],
          source: '<div>hello {{ world }}</div>'
        }
      },
      helpers: Map(0) {},
      components: Set(0) {},
      directives: Set(0) {},
      hoists: [],
      imports: [],
      constantCache: Map(0) {},
      temps: 0,
      cached: 0,
      identifiers: [Object: null prototype] {},
      scopes: { vFor: 0, vSlot: 0, vPre: 0, vOnce: 0 },
      parent: null,
      currentNode: {
        type: 0,
        children: [ [Object] ],
        helpers: [],
        components: [],
        directives: [],
        hoists: [],
        imports: [],
        cached: 0,
        temps: 0,
        codegenNode: undefined,
        loc: {
          start: [Object],
          end: [Object],
          source: '<div>hello {{ world }}</div>'
        }
      },
      childIndex: 0,
      inVOnce: false,
      helper: [Function: helper],
      removeHelper: [Function: removeHelper],
      helperString: [Function: helperString],
      replaceNode: [Function: replaceNode],
      removeNode: [Function: removeNode],
      onNodeRemoved: [Function: onNodeRemoved],
      addIdentifiers: [Function: addIdentifiers],
      removeIdentifiers: [Function: removeIdentifiers],
      hoist: [Function: hoist],
      cache: [Function: cache],
      filters: Set(0) {}
    }

这里的好多属性看起来都是 vue 特有的

第二步的时候就是 调用 traverseNode

后面的逻辑是处理一些特殊key 的

  root.helpers = [...context.helpers.keys()]
  root.components = [...context.components]
  root.directives = [...context.directives]
  root.imports = context.imports
  root.hoists = context.hoists
  root.temps = context.temps
  root.cached = context.cached

这里是把一些额外的方法给到了 root 上面,而root 是 AST 的root node

traverseNode

在看这个函数之前,先找个测试看看

  test('context state', () => {
    const ast = baseParse(`<div>hello {{ world }}</div>`)

    // manually store call arguments because context is mutable and shared
    // across calls
    const calls: any[] = []
    const plugin: NodeTransform = (node, context) => {
      calls.push([node, { ...context }])
    }

    transform(ast, {
      nodeTransforms: [plugin]
    })

    const div = ast.children[0] as ElementNode
    expect(calls.length).toBe(4)
    expect(calls[0]).toMatchObject([
      ast,
      {
        parent: null,
        currentNode: ast
      }
    ])
    expect(calls[1]).toMatchObject([
      div,
      {
        parent: ast,
        currentNode: div
      }
    ])
    expect(calls[2]).toMatchObject([
      div.children[0],
      {
        parent: div,
        currentNode: div.children[0]
      }
    ])
    expect(calls[3]).toMatchObject([
      div.children[1],
      {
        parent: div,
        currentNode: div.children[1]
      }
    ])
  })

通过这个测试可以知道,transform 是在本身的 AST 的基础上直接修改数据的

而这里的执行模式应该和 babel 的 plugin 的形式也差不多,通过 visit 的处理方式来调用

上面的 nodeTransforms:[plugin] 就是处理方式,看起来是当所有的node调用的时候,就会执行这个 nodeTransforms 里面给的函数

在看看第二个测试

  test('context.replaceNode', () => {
    const ast = baseParse(`<div/><span/>`)
    const plugin: NodeTransform = (node, context) => {
      if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
        // change the node to <p>
        context.replaceNode(
          Object.assign({}, node, {
            tag: 'p',
            children: [
              {
                type: NodeTypes.TEXT,
                content: 'hello',
                isEmpty: false
              }
            ]
          })
        )
      }
    }
    const spy = jest.fn(plugin)
    transform(ast, {
      nodeTransforms: [spy]
    })

    expect(ast.children.length).toBe(2)
    const newElement = ast.children[0] as ElementNode
    expect(newElement.tag).toBe('p')
    expect(spy).toHaveBeenCalledTimes(4)
    // should traverse the children of replaced node
    expect(spy.mock.calls[2][0]).toBe(newElement.children[0])
    // should traverse the node after the replaced node
    expect(spy.mock.calls[3][0]).toBe(ast.children[1])
  })

在 plugin 的实现里面可以看到就是通过替换node来达到修改代码的效果

而context 是什么?哦,看起来 context 是用来处理 node 的

traverseNode

使用的基本逻辑明白了 那接着看看 traverseNode 内部是如何实现的把

export function traverseNode(
  node: RootNode | TemplateChildNode,
  context: TransformContext
) {
  context.currentNode = node
  // apply transform plugins
  const { nodeTransforms } = context
  const exitFns = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    const onExit = nodeTransforms[i](node, context)
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
    if (!context.currentNode) {
      // node was removed
      return
    } else {
      // node may have been replaced
      node = context.currentNode
    }
  }

  switch (node.type) {
    case NodeTypes.COMMENT:
      if (!context.ssr) {
        // inject import for the Comment symbol, which is needed for creating
        // comment nodes with `createVNode`
        context.helper(CREATE_COMMENT)
      }
      break
    case NodeTypes.INTERPOLATION:
      // no need to traverse, but we need to inject toString helper
      if (!context.ssr) {
        context.helper(TO_DISPLAY_STRING)
      }
      break

    // for container types, further traverse downwards
    case NodeTypes.IF:
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
  }

  // exit transforms
  context.currentNode = node
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

这里的 context 就是通过 createTransformContext 生成的,里面有好多方法可以处理 node

第一步是先调用用户通过 config 注入的 nodeTransforms 里面的函数,也就是单测里面给的 plugin 函数

参数就是把 node 和 context 给到,所以用户可以在 plugin 里面通过 context 提供的方法来处理 node

这里的细节是, plugin 是可以返回一个函数的,这个函数就做 onExit

接着会基于 node 的类型做不同的处理

switch (node.type) {
    case NodeTypes.COMMENT:
      if (!context.ssr) {
        // inject import for the Comment symbol, which is needed for creating
        // comment nodes with `createVNode`
        context.helper(CREATE_COMMENT)
      }
      break
    case NodeTypes.INTERPOLATION:
      // no need to traverse, but we need to inject toString helper
      if (!context.ssr) {
        context.helper(TO_DISPLAY_STRING)
      }
      break

    // for container types, further traverse downwards
    case NodeTypes.IF:
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break
    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
  }
  • NodeTypes.COMMENT → context.helper(CREATE_COMMENT)

  • NodeTypes.INTERPOLATION → context.helper(TO_DISPLAY_STRING)

  • NodeTypes.IF → traverseNode(node.branches[i], context)

  • NodeTypes.IF_BRANCH || NodeTypes.FOR || NodeTypes.ELEMENT || NodeTypes.ROOT:

    traverseChildren(node, context)

这个处理完成后在统一的调用 exitFn

  // exit transforms
  context.currentNode = node
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }

这里应该是方便让用户做一些清理逻辑


createTransformContext - context

继续来分析一下 context

他里面有几个关键的方法

  helper(name) {
 
    },
    removeHelper(name) {
  
    },
    helperString(name) {
    
    },
    replaceNode(node) {
    },
    removeNode(node) {
    onNodeRemoved: () => {},
    addIdentifiers(exp) {
     
    },
    removeIdentifiers(exp) {
      
    },
    hoist(exp) {
     
    },
    cache(exp, isVNode = false) {
      
    }

先来看看 helper

    helper(name) {
      const count = context.helpers.get(name) || 0
      context.helpers.set(name, count + 1)
      return name
    },

逻辑是加一个 count ,那么是用在哪里的呢?

没找到 继续看看 removeHelper

    removeHelper(name) {
      const count = context.helpers.get(name)
      if (count) {
        const currentCount = count - 1
        if (!currentCount) {
          context.helpers.delete(name)
        } else {
          context.helpers.set(name, currentCount)
        }
      }
    },

和 helper 是对应的,这里是删除一个 count

在看 helperString

    helperString(name) {
    return `_${helperNameMap[context.helper(name)]}`
    },

这里的重点是 helperNameMap ,而 context.helper(name ) 是基于 name 计数了一下,然后把 name 返回。

那看看 helperNameMap

export const helperNameMap: any = {
  [FRAGMENT]: `Fragment`,
  [TELEPORT]: `Teleport`,
  [SUSPENSE]: `Suspense`,
  [KEEP_ALIVE]: `KeepAlive`,
  [BASE_TRANSITION]: `BaseTransition`,
  [OPEN_BLOCK]: `openBlock`,
  [CREATE_BLOCK]: `createBlock`,
  [CREATE_ELEMENT_BLOCK]: `createElementBlock`,
  [CREATE_VNODE]: `createVNode`,
  [CREATE_ELEMENT_VNODE]: `createElementVNode`,
  [CREATE_COMMENT]: `createCommentVNode`,

太多了,截取了一部分,可以看到,这里存储的都是对应的处理函数,也就是所谓的 helper

那可以说是 helperString 就是返回对应 helper 的名称

继续看replaceNode

    replaceNode(node) {
      context.parent!.children[context.childIndex] = context.currentNode = node
    },

替换节点, 替换的是当前 context 的父级的孩子节点(基于 childIndex 获取的孩子)

下面的是removeNode

    removeNode(node) {

      const list = context.parent!.children
      const removalIndex = node
        ? list.indexOf(node)
        : context.currentNode
        ? context.childIndex
        : -1

      if (!node || node === context.currentNode) {
        // current node removed
        context.currentNode = null
        context.onNodeRemoved()
      } else {
        // sibling node removed
        if (context.childIndex > removalIndex) {
          context.childIndex--
          context.onNodeRemoved()
        }
      }
      context.parent!.children.splice(removalIndex, 1)
    },

处理的也是 context 父级的孩子节点, 直接给删除当前的节点

这里有个回调,删除完成后会执行 context 里面的 onNodeRemoved

addIdentifiers

    addIdentifiers(exp) {
      // identifier tracking only happens in non-browser builds.
      if (!__BROWSER__) {
        if (isString(exp)) {
          addId(exp)
        } else if (exp.identifiers) {
          exp.identifiers.forEach(addId)
        } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
          addId(exp.content)
        }
      }
    },

这里的关键是 addId 函数,等会在看 TODO

后面是 removeIdentifiers

    removeIdentifiers(exp) {
      if (!__BROWSER__) {
        if (isString(exp)) {
          removeId(exp)
        } else if (exp.identifiers) {
          exp.identifiers.forEach(removeId)
        } else if (exp.type === NodeTypes.SIMPLE_EXPRESSION) {
          removeId(exp.content)
        }
      }
    },

这里是对Id 的删除

hoist 处理静态提升

把静态的标签都缓存起来

    hoist(exp) {
      if (isString(exp)) exp = createSimpleExpression(exp)
      context.hoists.push(exp)
      const identifier = createSimpleExpression(
        `_hoisted_${context.hoists.length}`,
        false,
        exp.loc,
        ConstantTypes.CAN_HOIST
      )
      identifier.hoisted = exp
      return identifier
    },

这个主要是做优化的,可以作为TODO

cache 方法也是一样的,后续在看

    cache(exp, isVNode = false) {
      return createCacheExpression(context.cached++, exp, isVNode)
    }
 

总结来看的话,context 有这么几个职责

  • 处理 helper

    • helper 是对应的处理函数,这个应该是由 runtime-core 导出的
  • 处理 node

    • node就是 AST 节点,可以替换和删除
  • 处理 Identifiers

    • identifiers 是做什么的,还不知道

    • 添加和删除

  • 以及2个用于优化的方法

    • hoist

    • cache


接着我们回到traverseNode 函数内看下面的逻辑

    case NodeTypes.IF:
      for (let i = 0; i < node.branches.length; i++) {
        traverseNode(node.branches[i], context)
      }
      break

如果是 if 分支的话,那么需要2个分支都处理继续调用

在看 下面

    case NodeTypes.IF_BRANCH:
    case NodeTypes.FOR:
    case NodeTypes.ELEMENT:
    case NodeTypes.ROOT:
      traverseChildren(node, context)
      break
export function traverseChildren(
  parent: ParentNode,
  context: TransformContext
) {
  let i = 0
  const nodeRemoved = () => {
    i--
  }
  for (; i < parent.children.length; i++) {
    const child = parent.children[i]
    if (isString(child)) continue
    context.parent = parent
    context.childIndex = i
    context.onNodeRemoved = nodeRemoved
    traverseNode(child, context)
  }
}

traverseChildren 的逻辑很简单,就是标准的 for children 然后再调用 traverseNode

至此,所有的 node 都会被执行到 nodeTransforms 这个里面的函数内

并且还收集完了 helper , 以及做好了 count 计数


generate

看看如何做代码生成

export function generate(
  ast: RootNode,
  options: CodegenOptions & {
    onContextCreated?: (context: CodegenContext) => void
  } = {}
): CodegenResult {
  const context = createCodegenContext(ast, options)
  if (options.onContextCreated) options.onContextCreated(context)
  const {
    mode,
    push,
    prefixIdentifiers,
    indent,
    deindent,
    newline,
    scopeId,
    ssr
  } = context

  const hasHelpers = ast.helpers.length > 0
  const useWithBlock = !prefixIdentifiers && mode !== 'module'
  const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
  const isSetupInlined = !__BROWSER__ && !!options.inline

  // preambles
  // in setup() inline mode, the preamble is generated in a sub context
  // and returned separately.
  const preambleContext = isSetupInlined
    ? createCodegenContext(ast, options)
    : context
  if (!__BROWSER__ && mode === 'module') {
    genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
  } else {
    genFunctionPreamble(ast, preambleContext)
  }
  // enter render function
  const functionName = ssr ? `ssrRender` : `render`
  const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
  if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
    // binding optimization args
    args.push('$props', '$setup', '$data', '$options')
  }
  const signature =
    !__BROWSER__ && options.isTS
      ? args.map(arg => `${arg}: any`).join(',')
      : args.join(', ')

  if (isSetupInlined) {
    push(`(${signature}) => {`)
  } else {
    push(`function ${functionName}(${signature}) {`)
  }
  indent()

  if (useWithBlock) {
    push(`with (_ctx) {`)
    indent()
    // function mode const declarations should be inside with block
    // also they should be renamed to avoid collision with user properties
    if (hasHelpers) {
      push(
        `const { ${ast.helpers
          .map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
          .join(', ')} } = _Vue`
      )
      push(`\n`)
      newline()
    }
  }

  // generate asset resolution statements
  if (ast.components.length) {
    genAssets(ast.components, 'component', context)
    if (ast.directives.length || ast.temps > 0) {
      newline()
    }
  }
  if (ast.directives.length) {
    genAssets(ast.directives, 'directive', context)
    if (ast.temps > 0) {
      newline()
    }
  }
  if (__COMPAT__ && ast.filters && ast.filters.length) {
    newline()
    genAssets(ast.filters, 'filter', context)
    newline()
  }

  if (ast.temps > 0) {
    push(`let `)
    for (let i = 0; i < ast.temps; i++) {
      push(`${i > 0 ? `, ` : ``}_temp${i}`)
    }
  }
  if (ast.components.length || ast.directives.length || ast.temps) {
    push(`\n`)
    newline()
  }

  // generate the VNode tree expression
  if (!ssr) {
    push(`return `)
  }
  if (ast.codegenNode) {
    genNode(ast.codegenNode, context)
  } else {
    push(`null`)
  }

  if (useWithBlock) {
    deindent()
    push(`}`)
  }

  deindent()
  push(`}`)

  return {
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.code : ``,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }
}

去找个单元测试

  test('module mode preamble', () => {
    const root = createRoot({
      helpers: [CREATE_VNODE, RESOLVE_DIRECTIVE]
    })
    const { code } = generate(root, { mode: 'module' })
    expect(code).toMatch(
      `import { ${helperNameMap[CREATE_VNODE]} as _${helperNameMap[CREATE_VNODE]}, ${helperNameMap[RESOLVE_DIRECTIVE]} as _${helperNameMap[RESOLVE_DIRECTIVE]} } from "vue"`
    )
    expect(code).toMatchSnapshot()
  })

code 长这个样子

import { createVNode as _createVNode, resolveDirective as _resolveDirective } from "vue"
    
    export function render(_ctx, _cache) {
      return null
    }

那问题

import 上面的代码是怎么生成的?

在这里

  // preambles
  // in setup() inline mode, the preamble is generated in a sub context
  // and returned separately.
  const preambleContext = isSetupInlined
    ? createCodegenContext(ast, options)
    : context
  if (!__BROWSER__ && mode === 'module') {
    genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
  } else {
    genFunctionPreamble(ast, preambleContext)
  }

这里是基于不同的 mode 来生成不同的代码

而代码这里其实就是 string 的拼接

我们先看 genModulePreamble

function genModulePreamble(
  ast: RootNode,
  context: CodegenContext,
  genScopeId: boolean,
  inline?: boolean
) {
  const { push, newline, optimizeImports, runtimeModuleName } = context

  if (genScopeId) {
    ast.helpers.push(WITH_SCOPE_ID)
    if (ast.hoists.length) {
      ast.helpers.push(PUSH_SCOPE_ID, POP_SCOPE_ID)
    }
  }

  // generate import statements for helpers
  if (ast.helpers.length) {
    if (optimizeImports) {
      // when bundled with webpack with code-split, calling an import binding
      // as a function leads to it being wrapped with `Object(a.b)` or `(0,a.b)`,
      // incurring both payload size increase and potential perf overhead.
      // therefore we assign the imports to variables (which is a constant ~50b
      // cost per-component instead of scaling with template size)
      push(
        `import { ${ast.helpers
          .map(s => helperNameMap[s])
          .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
      )
      push(
        `\n// Binding optimization for webpack code-split\nconst ${ast.helpers
          .map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
          .join(', ')}\n`
      )
    } else {
      push(
        `import { ${ast.helpers
          .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
          .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
      )
    }
  }

  if (ast.ssrHelpers && ast.ssrHelpers.length) {
    push(
      `import { ${ast.ssrHelpers
        .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
        .join(', ')} } from "@vue/server-renderer"\n`
    )
  }

  if (ast.imports.length) {
    genImports(ast.imports, context)
    newline()
  }

  genHoists(ast.hoists, context)
  newline()

  if (!inline) {
    push(`export `)
  }
}

最简单的分支是

      push(
        `import { ${ast.helpers
          .map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
          .join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
      )

ast.helpers 是调用的时候给的,然后通知 helperNameMap 映射对应的函数名,最后拼成一个字符串即可

push 的话是 context 内部的方法,其实就是吧所有的 string 都收集进去


接着看 render 函数的生成

  // enter render function
  const functionName = ssr ? `ssrRender` : `render`
  const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
  if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
    // binding optimization args
    args.push('$props', '$setup', '$data', '$options')
  }
  const signature =
    !__BROWSER__ && options.isTS
      ? args.map(arg => `${arg}: any`).join(',')
      : args.join(', ')

  if (isSetupInlined) {
    push(`(${signature}) => {`)
  } else {
    push(`function ${functionName}(${signature}) {`)
  }

基于参数来生成不同的 render string

具体后面所有生成代码的逻辑,都是基于 ast 上面的options 来做处理,有什么就生成什么样子的代码

现在基本上明白了。在回头看看 codegen return 的数据

  return {
    ast,
    code: context.code,
    preamble: isSetupInlined ? preambleContext.code : ``,
    // SourceMapGenerator does have toJSON() method but it's not in the types
    map: context.map ? (context.map as any).toJSON() : undefined
  }

ast 对象不用说了

code 就是基于 ast 生成的代码

preamble 是前导码头部的代码

map 是处理 sourcemap

patchProp函数为元素添加监听函数时是不是要先用createInvoker函数包装监听函数?

function patchProp(el, key, preValue, nextValue) {
if (isOn(key)) {
const invokers = el._vei || (el._vei = {});
const existingInvoker = invokers[key];
if (nextValue && existingInvoker) {
existingInvoker.value = nextValue; //这个invoker是一个函数,直接修改function.value是不是有点问题?我没有找到类似写法
} else {
const eventName = (key).slice(2).toLowerCase();
if (nextValue) {
const invoker = (invokers[key] = nextValue); //没用createInvoker包装函数去包装,所以invoker是一个函数
el .addEventListener(eventName, invoker);
} else {
el.removeEventListener(eventName, existingInvoker);
invokers[key] = undefined;
}
}
} else {
if (nextValue === null || nextValue === "") {
el.removeAttribute(key);
} else {
el.setAttribute(key, nextValue);
}
}
}

dep和effect之间的对应关系

崔老师好,最近在研究reactivity模块,对应dep和effect之间知道有如下的关系,但当嵌套依赖复杂点,就搞不清楚了,请问你是怎么理解这一块的?

// effect 和dep之间的对应关系
targetMap: {
    target1: {
        key1: [effect1, effect2, ...],
        key2: [effect1, effect2, ...],
        key3: [effect1, effect2, ...],
        ...
    } 
}
effect.deps = [
    [effect1, effect2, ...],
    [effect1, effect2, ...],
    [effect1, effect2, ...],
    ...
]

但当加入computed,ref后,感觉有点绕不清了,比如:

const count = ref(0) 
const plusOne = computed(() => { 
  return count.value + 1 
}) 
effect(() => { 
  // plusOne.value 和 count.value顺序不一样,输出结果不一样
  // console.log( plusOne.value + count.value) 
  console.log(count.value + plusOne.value) 
}) 
function plus() { 
  count.value++ 
} 
plus()

再比如:

const count = ref(0) 
const plusOne = computed(() => { 
  return count.value + 1 
}) 
const plusTwo = computed(() => { 
  return plusOne.value + 1 
}) 
console.log(plusTwo.value)

cypress install failed

cypress 依赖包下载一直有问题,google翻了下也不行,是项目哪有特殊配置吗

Cannot read properties of null (reading 'insertBefore')

index.ts?t=1670014912902:50 Uncaught TypeError: Cannot read properties of null (reading 'insertBefore')
at insert (index.ts?t=1670014912902:50:10)
at mountElement (renderer.ts?t=1670014912902:243:5)
at processElement (renderer.ts?t=1670014912902:63:7)
at patch (renderer.ts?t=1670014912902:35:11)
at ReactiveEffect.componentUpdateFn [as fn] (renderer.ts?t=1670014912902:295:9)
at ReactiveEffect.run (effect.ts:23:25)
at effect (effect.ts:47:11)
at setupRenderEffect (renderer.ts?t=1670014912902:319:23)
at mountComponent (renderer.ts?t=1670014912902:282:5)
at processComponent (renderer.ts?t=1670014912902:256:7)

main.js

const rootContainer = document.querySelector("#root");

index.html

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

id不匹配导致,错误信息不友好

【看的见的思考】reactivity

看的见的思考

reactive

  test('Object', () => {
    const original = { foo: 1 }
    const observed = reactive(original)
    expect(observed).not.toBe(original)
    expect(isReactive(observed)).toBe(true)
    expect(isReactive(original)).toBe(false)
    // get
    expect(observed.foo).toBe(1)
    // has
    expect('foo' in observed).toBe(true)
    // ownKeys
    expect(Object.keys(observed)).toEqual(['foo'])
  })
 

从这个单元测试的 demo 开始

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

先从 createReactiveObject 开始

这里别的什么都不需要管,先看看他做了什么事

不需要关心 mutableHandlers、mutableCollectionHandlers、reactiveMap 都做了什么

在 createReactiveObject 里面最核心的逻辑是

function createReactiveObject(target){

  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  return proxy
 } 

最核心的逻辑其实就是用 Proxy 给包裹一下,然后这里是需要基于一个 targetType 的类型去选择用不同的 handlers 的

那我们看看 targetType 都有哪几种?

看 getTargetType

  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))

无效的和正常了 type 做了区分

无效的情况是:

有 ReactiveFlags.SKIP 字段 或者 对象是不可以扩展的

那在什么情况下需要对象用到扩展呢?

TODO

剩下的点都在 targetTypeMap 里面声明好了

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

所以总结一下就是:

  • TargetType.COMMON:

    • Array

    • Object

  • TargetType.COLLECTION

    • Map

    • Set

    • WeakMap

    • WeakSet

  • TargetType.INVALID

    • 上面都不符合的话

好 ,类型知道了,那我们的参数是一个对象 {foo:xxx} 所以看看 handlers 应该是 baseHandlers

那么 handlers 就是

TargetType.COMMON → baseHandlers → mutableHandlers

TargetType.COLLECTION → collectionHandlers → mutableCollectionHandlers

mutableHandlers

先看这个方法

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

这里把所有的处理都封装到了具体的函数内了,这次在看要比之前好很多

接着看看 get

get

get 就是调用了 createGetter

function createGetter(isReadonly = false, shallow = false) {
  return function get (target: Target, key: string | symbol, receiver: object)=>{
   const res = Reflect.get(target, key, receiver)
     if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
     if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }
  }



}

这里是使用了闭包的概念,让调用者可以少写几个参数,而且从概念上也做了分离,在create 的时候就可以标注是不是 readonly 的 或者是 shallow 的

这里的 get 调用的时机是在触发 proxy.xxx 的 get 操作的时候

而在调用 get 的时候是需要做依赖收集的,而依赖收集的动作就是在 track 里面做的!

而最终的结果就是返回 Reflect.get 的值就可以了

这里有个点,就是如何处理嵌套的 Object, 就是递归的调用 reactive 即可

track 依赖收集

重点:那是如何做依赖收集的呢???

export function track(target: object, type: TrackOpTypes, key: unknown) {
 
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

这里的重点就是如何存储 dep

这里有两层关系,

target → depsMap

key → dep

例如:

const user = {age: 1}

那 target 就是 user

target 对应的就是 depsMap

age 对应一个 dep

那 activeEffect 是什么呢?其实就是依赖(通过 effect 给到的 function)

effect(()=>{
// 这个 函数就是 activeEffect
})

而把 activeEffect 添加到dep里面的操作就是依赖收集

到这里依赖收集的动作就已经都搞定了

那注意这里的 dep 只是一个 set 数据结构

需要看看 activeEffect 是从哪里过来的

这里的 activeEffect 是在 createReactiveEffect 赋值的

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
        activeEffect = effect
}

所以这个 activeEffect 确实是调用 effect 时候的依赖函数

在来分析分析这几个参数

export function track(target: object, type: TrackOpTypes, key: unknown) {

target 就是对应的对象

type 的作用是后续给 debug 的时候调用的,方便用户知道当前是什么类型

    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }

key 是什么?

key 按照以前的理解是 target 对应的 key ,但是下面的 trigger 的时候发现这个key 可能并不只有一种情况,那么我们看看key 都有什么情况把

啊哦,其实大多数情况下都是有key 的,但是有一些特殊的操作是没有key 的,那怎么办? 自己搞一个被,所以这个就是 ITERATE_KEY 的作用

比如在 ownkeys 的时候,在 size 的时候

function ownKeys(target: object): (string | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
  return Reflect.ownKeys(target)
}

function size(target: IterableCollections, isReadonly = false) {
  target = (target as any)[ReactiveFlags.RAW]
  !isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
  return Reflect.get(target, 'size', target)
}

Set

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

这里的核心逻辑是 trigger 也就是触发依赖

但是触发依赖的时候是分情况的,一个是 ADD 一个是 SET

TODO 如何区分触发依赖的时候是 ADD 还是 SET?

先看trigger 的逻辑实现

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
    const effects = new Set<ReactiveEffect>()
    ……



}

先看 Add 的逻辑

    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'))
        }

这里是基于不同的类型来从获取 depsMap 里面获取值

那看看 add 干了啥?

  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

啊哦, 这里是用 effects 来把 effect 给添加进来的,

那为什么要在这里添加呢? 不是应该在收集依赖的时候添加吗? 看看收集依赖做了啥?

track 的时候确实是收集了 effect 里面的依赖函数,没有问题

那看看 effects 后续是用在哪里了吧

// trigger 
 effects.forEach(run)

在最后一行调用了 effects 执行 run 函数,那这里应该是在 trigger 的时候把之前所有的依赖又重新收集了一遍,那为什么又重新收集了一遍呢?方便后续的统一处理,因为需要在多个地方把所有的 effect 都收集起来

add(depsMap.get(ITERATE_KEY))

这里的 ITERATE_KEY 是什么? 他是在哪里赋值的?是在什么时候存给 depsMap 的呢?

export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')

只是一个唯一标识

那看看在哪里存的 ,存的又是什么, 啊哦

在调用 track 的时候会吧这个值给过去,比如:在 ownKeys 的时候 ,那这里需要看看 track 的几个关键的参数

那么如果我们精简一下 set 做的事的话,那么就是:

  1. 收集所有的 effect (依赖)

  2. 使用的是 add 函数来添加

  3. 调用所有的 effect

  4. 调用的 run 函数来处理

那么继续去看看 run 函数都做了什么吧

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

也是非常简单,就是调用一下收集过来的 effect (依赖)

而这里有个特殊处理就是 scheduler 的逻辑实现,这里其实就是调用 scheduler ,让用户自己处理调用的时机(这里先略过)

effect

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options)
  return effect
}

fn就是传入的 function ,这里的核心就是创建一个 effect 对象,然后返回即可

接下来看 createReactiveEffect

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

effect 本身就是一个函数,然后给了它很多的属性

没有用对象也没有用 class , 只是用了 function 来表示的 effect 。这是因为 effect 本身是需要执行的

这里是使用 effectStack 来存储所有的 effect ,以及使用 activeEffect 这个全局的变量来保存当前的 effect 这里的 activeEffect 就和 track 依赖收集关联起来了, 因为 effect 这个函数就是响应式对象的依赖

这里需要探索的是

enableTracking

resetTracking

cleanup

先看 enableTracking

export function enableTracking() {
  trackStack.push(shouldTrack)
  shouldTrack = true
}

咦,这里的 trackStack 又是一个栈,他是做什么的呢?

看看他都是在哪里调用了,trackStack 只是会影响 shouldTrack ,那重点是 shoudlTrack

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

最终的使用是会影响到 track 的逻辑

那为什么需要用 stack 来管理一个布尔变量呢?

这里的关键点是 pauseTracking 函数的调用,但是使用它的地方太多了

结合pauseTracking enableTracking resetTracking 来判断的话,应该是需要返回上一个 shouldTrack 的状态

那我们只分析 try 内部的代码的话

 try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
       ……
      }

就是先允许 track 然后把 effect 记录到 effectStack 内,接着执行 fn()

而执行 fn 的时候会触发 track 的逻辑,所以正好把当前的 effect 给收集进去了

那我们在看看 finally 的逻辑

try{
……
}finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }

这里的 finally 逻辑是肯定会执行的

所以也就是在收集完依赖后,就需要把之前的 effect 给弹出去了,然后还 resetTracking,但是这里的 resetTracking 是回到上一个状态。 最后把 activeEffect 制成栈顶的值

但是这里是应对什么场景的呢???

可能是嵌套的。TODO

接着看看 cleanup 的逻辑,因为在跟断点的时候发现每次收集到的 dep 都是会被清理的。也就是执行一次,清理一次。

这个逻辑之前大概听尤大讲过,是为了处理一些边缘case ,必须需要每次都清空,我需要找到这个 边缘case 的 demo

function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

这里的 deps 是 effect 依赖对应的响应式对象的 dep,之前在 track 的时候 收集进来的。

这里的目的就是把响应式对象里面的依赖都清空。至于为什么,只能是找到对应的 demo 才明白。(应该是为了处理 边缘 case)

看单元测试:

  it('should observe multiple properties', () => {
    let dummy
    const counter = reactive({ num1: 0, num2: 0 })
    effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))

    expect(dummy).toBe(0)
    counter.num1 = counter.num2 = 7
    expect(dummy).toBe(21)
  })

这里本来以为赋值2个key 的话,只会触发一次,但是其实是修改一个key就会触发一次 fn的

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

    expect(dummy).toBe(0)
    counter.nested.num = 8
    expect(dummy).toBe(8)
  })

如果是个嵌套的对象的话,这里的 counter.nested 的依赖会是 fn,而 counter.nested.num 的依赖也会是 fn

那这个调用的链路都会收集 fn

 it('should not be triggered by mutating a property, which is used in an inactive branch', () => {
    let dummy
    const obj = reactive({ prop: 'value', run: true })

    const conditionalSpy = jest.fn(() => {
      dummy = obj.run ? obj.prop : 'other'
    })
    effect(conditionalSpy)

    expect(dummy).toBe('value')
    expect(conditionalSpy).toHaveBeenCalledTimes(1)
    obj.run = false
    expect(dummy).toBe('other')
    expect(conditionalSpy).toHaveBeenCalledTimes(2)
    obj.prop = 'value2'
    expect(dummy).toBe('other')
    expect(conditionalSpy).toHaveBeenCalledTimes(2)
  })

看到这个测试的时候,明白了为什么每次都需要 cleanup 了,这里的重点是在 conditionalSpy 内,这里 track 的前提是需要触发 get 之类的操作,而如果说在 effect 的 fn 里面因为有条件逻辑存在的话,就是会有触发不到某个 get 的时候。

比如 conditionalSpy ,在 obj.run 为 true 的时候,这里会触发 obj.run 也会触发 obj.prop。

而 obj.run 变成 false 的时候,这里只会触发 obj.run 。 而只触发 obj.urn 就会意味着 obj.prop 没有收集进来,那当我们去修改了 obj.prop 的值的时候,就不会在执行 effect fn 了。

那如果说我们把代码的执行 path 比作绘画的话,因为这个 path 会改变,所以我们就需要每次都 清空掉,然后重新绘制

哈哈,这里是用游戏逻辑的 reset 来做的比喻

阅读 3.2 版本

变化点在 effect.ts 里面

首先 effect 是用 class 来表示了

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []

  // can be attached after creation
  computed?: boolean
  allowRecurse?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    recordEffectScope(this, scope)
  }

  run() {
  }

  stop() {
  }
}

只有2个行为,run 和 stop 了

在看 effect 的逻辑

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  const _effect = new ReactiveEffect(fn)
  if (options) {
  // 参数的处理
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

这里的 scope 是做什么用的

执行的时候就是用 _effect.run() 调用即可

这时候的 effect 函数会返回一个新的概念 ReactiveEffectRunner 类型。

那看看这个类型都是做了什么事把

  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner

这里的 runner 也就是 ReactiveEffectRunner 就是 _effect.run.bind 之后的这个函数

接着看看 effect 里面的 run 都是做了什么

  run() {
    if (!this.active) {
      return this.fn()
    }
    if (!effectStack.includes(this)) {
      try {
        effectStack.push((activeEffect = this))
        enableTracking()

        trackOpBit = 1 << ++effectTrackDepth

        if (effectTrackDepth <= maxMarkerBits) {
          initDepMarkers(this)
        } else {
          cleanupEffect(this)
        }
        return this.fn()
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          finalizeDepMarkers(this)
        }

        trackOpBit = 1 << --effectTrackDepth

        resetTracking()
        effectStack.pop()
        const n = effectStack.length
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }

这里和之前的版本的变化点是控制 track 的布尔值的逻辑变了。别的都是和以前一样的

那重点就看看新增加的逻辑是什么

 try {
        effectStack.push((activeEffect = this))
        enableTracking()

        trackOpBit = 1 << ++effectTrackDepth

        if (effectTrackDepth <= maxMarkerBits) {
          initDepMarkers(this)
        } else {
          cleanupEffect(this)
        }
        return this.fn()
      } finally {

这里的 trackOpBit 和 initDepMarkers 和 cleanupEffect 方法

对比之前的实现的话,每次都是必须会调用 cleanupEffect 来清理依赖的。而现在不是了

先看看 effectTrackDepth

// The number of effects currently being tracked recursively.
let effectTrackDepth = 0

对effectTrackDepth 的赋值是在 try 里面的时候 effectTrackDepth 会执行 ++effectTrackDepth,

而 effectTrackDepth 会影响到initDepMarkers和cleanupEffect 以及finalizeDepMarkers 的执行

这里还有一个关键的控制变量是maxMarkerBits

感觉是个控制值

/**
 * The bitwise track markers support at most 30 levels op recursion.
 * This value is chosen to enable modern JS engines to use a SMI on all platforms.
 * When recursion depth is greater, fall back to using a full cleanup.
 */
const maxMarkerBits = 30

这里的 SMI ,是什么?TODO

目前猜测是基于v8的某些点做的优化策略

目前是

if (effectTrackDepth <= maxMarkerBits) {
          initDepMarkers(this)
        } else {
          cleanupEffect(this)
        }

那看看小于 maxMarkerBits 的时候的 initDepMarkers 吧

export const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // set was tracked
    }
  }
}

这里也是用了二进制的处理方式来做的标记逻辑。

看看这里的 dep.w 都用在了哪里

以上逻辑的关键点是优化了之前每次都调用 cleanup 的点

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      dep.n |= trackOpBit // set newly tracked
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

逻辑线索在 shouldTrack , 都是为了算出是不是需要 track,那么看看有没有 demo 可以验证这个猜测

  it('should observe multiple properties', () => {
    let dummy
    const counter = reactive({ num1: 0, num2: 0 })
    effect(() => (dummy = counter.num1 + counter.num1 + counter.num2))

    expect(dummy).toBe(0)
    counter.num1 = counter.num2 = 7
    expect(dummy).toBe(21)
  })

在这个单测里面证明了上面的猜测,现在我们知道了解决的是什么问题(why)

接着看看是如何做到的 how

如果从优化角度自己思考的话,是希望

  1. track 收集依赖只收集一次就好了

  2. 但是只收集一次的话,code path 变化了要如何解决?

好了,这里使用二进制的原因是因为会有多层级,而用二进制来表示每一个层级的标识

而对于一个 dep 来讲会有2个标识

  • n → 就是代表在当前的递归层级中是不是初始化过的

    • 我们在简化一下,不考虑递归层级的问题

      • n 就标识为是不是初始化过的
  • w→在当前的递归层级中是不是已经被 track 的

    • 是不是已经被 track 的

接着我们看看 dep.n 和 dep.w 都是分别在什么时候被赋值的

stop 的实现

  it('stop', () => {
    let dummy
    const obj = reactive({ prop: 1 })
    const runner = effect(() => {
      dummy = obj.prop
    })
    obj.prop = 2
    expect(dummy).toBe(2)
    stop(runner)
    obj.prop = 3
    expect(dummy).toBe(2)

    // stopped effect should still be manually callable
    runner()
    expect(dummy).toBe(3)
  })

看看stop 的功能是如何实现的

这里 stop 是停止侦听,而 runner 是重新run起来

看看stop 都做了什么事

export function stop(runner: ReactiveEffectRunner) {
  runner.effect.stop()
}

  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
function cleanupEffect(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

这里的关键是调用了 cleanupEffect 这个函数,是会把当前的 effect 的 deps 都清空掉

那么也就是说响应式对象里面是没有依赖了,所以当触发 effect 的 trigger 的时候,是没有依赖可以执行的

而再次可以运行的逻辑其实就是重新调用一遍 effect.run

这里的 runner 就是指向的 effect.run 函数

所以后续的操作就是和初始化的逻辑一样了,需要重新的执行 fn ,然后再重新的收集依赖

lazy 的实现

lazy 是为了让用户自己选择调用的时机

  it('lazy', () => {
    const obj = reactive({ foo: 1 })
    let dummy
    const runner = effect(() => (dummy = obj.foo), { lazy: true })
    expect(dummy).toBe(undefined)

    expect(runner()).toBe(1)
    expect(dummy).toBe(1)
    obj.foo = 2
    expect(dummy).toBe(2)
  })

如果 lazy 为true 的话,那么在执行 effect 的时候,是不会主动执行 run 的,(不会执行 run 就意味着不会执行用户给的 fn),然后执行 effect 之后是会返回 runner 的,这里用户可以自己选择在什么时候去执行

实现也比较简单

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {

  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

EffectScope 是什么

暂时看不出来是在哪里使用的

但是他的职责就是收集所有的 effect

计算属性的实现 computed

先从 test 入手

  it('should return updated value', () => {
    const value = reactive<{ foo?: number }>({})
    const cValue = computed(() => value.foo)
    expect(cValue.value).toBe(undefined)
    value.foo = 1
    expect(cValue.value).toBe(1)
  })

computed 的逻辑

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

// 初始化的逻辑
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  )

  return cRef as any
}

一开始先找到 getter 和 setter

接着看 ComputedRefImpl 的实现

class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  private _dirty = true
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}/

computed 的核心就是 effect ,而这里是使用了 effect 的 scheduler 的功能

后续执行 effect.run 的时候需要做一些额外的处理

看看 dirty 和 triggerRefValue 是做了什么

因为我们知道 computed 的一个核心点是可以有缓存的,就是没有变化的话,会返回之前的值

主要也是看看 computed 的这个核心功能是如何实现的

先看看 triggerRefValue 的实现

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)
    }
  }
}

这里涉及到了 ref 的代码实现了, 哦 算了 , 计算属性先等等在看,先看 ref ,

这个函数在 ref 的时候都分析完了,可以去看看 ref 的分析

继续

计算属性的话,只有在执行 get 操作的时候才会触发后续的逻辑

构造器里面只是创建了 effect,但是并没有执行

接着就看看 get value 的逻辑吧

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

这里的重点是搞清楚 _dirty 的逻辑

选择一个单元测试,使用断点

  it('should compute lazily', () => {
    const value = reactive<{ foo?: number }>({})
    const getter = jest.fn(() => value.foo)
    const cValue = computed(getter)

    // lazy
    expect(getter).not.toHaveBeenCalled()

    expect(cValue.value).toBe(undefined)
    expect(getter).toHaveBeenCalledTimes(1)

    // should not compute again
    cValue.value
    expect(getter).toHaveBeenCalledTimes(1)

    // should not compute until needed
    value.foo = 1
    expect(getter).toHaveBeenCalledTimes(1)

首先是调用了 new ComputedRefImpl

然后再初始化的时候创建了 effect ,这里要注意的是

    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this[ReactiveFlags.IS_READONLY] = isReadonly

第二个参数是scheduler 的实现,当执行 effect.run 的时候会执行 fn,当触发了 trigger 逻辑的时候会执行 scheduler 内部的实现

顺序是:

  1. 执行 new ReactiveEffect

  2. 用户执行 get value 的操作

  3. 触发 get

1. _dirty 成false ,锁上了

2. 这里只有在触发了 trigger 的时候才会开锁
  1. 执行 trackRefValue 收集依赖?这里是处理未知情况的逻辑

  2. 触发 effect.run()

  3. 执行用户给的 fn

  4. 触发收集依赖

  5. 用户修改了内部响应式对象的值

  6. 触发 trigger

1. 执行 scheduler 的实现

2. 开锁
  1. 再次调用 get 的时候 dirty 成 ture 所以可以再次执行 effect.run 也就是执行用户给的 fn

ref 的实现

先从这里看起

export function ref(value?: unknown) {
  return createRef(value)
}
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

用户实际使用的就是 RefImpl 的实例

而 refimpl 一共就2个行为, get value 和 set value

class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(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 : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }
}

而基于我们之前看 reactive的经验,知道在 get 的时候是会 track 的,在 set 的时候会 trigger 的

而 ref 这里的实现也是一样的

先看 trackRefValue

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)
    }
  }
}

这里的实现就简单了,因为一个 ref 只会对应一个值,所以实现的时候会创建一个 dep 赋值给 ref.dep 上

然后后面的执行和 reactive 一样了。都是执行 trackEffects来处理

这里有个问题,如果说传入的值是 object 或者是 array 的话,那怎么办?

答案就是会用 reactive 包裹一下,这个逻辑是在 构造器里面实现的

  constructor(value: T, public readonly _shallow = false) {
    this._rawValue = _shallow ? value : toRaw(value)
    this._value = _shallow ? value : convert(value)
  }

看 convert 的实现

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

啊哦,如果是个 object 的话,那么就用 reactive 包裹了

那继续去看 set逻辑

  set value(newVal) {
    newVal = this._shallow ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      triggerRefValue(this, newVal)
    }
  }

这里处理了2个点

  1. 看看新的值和老的值一样不,如果不一样的话,需要更改之前的值
1. 细节就是会把新的值也 convert 一下
  1. 触发 triggerRefValue
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)
    }
  }
}

这里也和之前是一样的,如果有 dep 的话,那么就 trigger 一下就可以了

DeferredComputed 的实现

先从单元测试看起

  test('should only trigger once on multiple mutations', async () => {
    const src = ref(0)
    const c = deferredComputed(() => src.value)
    const spy = jest.fn()
    effect(() => {
      spy(c.value)
    })
    expect(spy).toHaveBeenCalledTimes(1)
    src.value = 1
    src.value = 2
    src.value = 3
    // not called yet
    expect(spy).toHaveBeenCalledTimes(1)
    await tick
    // should only trigger once
    expect(spy).toHaveBeenCalledTimes(2)
    expect(spy).toHaveBeenCalledWith(c.value)
  })

大概先猜测的话,这里的 deferredComputed 和 computed 大概差不多,一上来就会执行,但是不同的是后续在改变他的值的时候他不会立即执行了。而是等到 await tick 之后才会执行

那看看是如何实现的

程序的入口

export function deferredComputed<T>(getter: () => T): ComputedRef<T> {
  return new DeferredComputedRefImpl(getter) as any
}
class DeferredComputedRefImpl<T> {
  public dep?: Dep = undefined
  private _value!: T
  private _dirty = true
  constructor(getter: ComputedGetter<T>) {
    let compareTarget: any
    let hasCompareTarget = false
    let scheduled = false
    this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
      if (this.dep) {
        if (computedTrigger) {
          compareTarget = this._value
          hasCompareTarget = true
        } else if (!scheduled) {
          const valueToCompare = hasCompareTarget ? compareTarget : this._value
          scheduled = true
          hasCompareTarget = false
          scheduler(() => {
            if (this.effect.active && this._get() !== valueToCompare) {
              triggerRefValue(this)
            }
            scheduled = false
          })
        }
        // chained upstream computeds are notified synchronously to ensure
        // value invalidation in case of sync access; normal effects are
        // deferred to be triggered in scheduler.
        for (const e of this.dep) {
          if (e.computed) {
            e.scheduler!(true /* computedTrigger */)
          }
        }
      }
      this._dirty = true
    })
    this.effect.computed = true
  }

  private _get() {
    if (this._dirty) {
      this._dirty = false
      return (this._value = this.effect.run()!)
    }
    return this._value
  }

  get value() {
    trackRefValue(this)
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    return toRaw(this)._get()
  }
}

这里的几个属性都是和 computed 是一样的,比如有 dep 、 dirty、和 value

看看区别是什么

get 函数的实现和 computed 是差不多的,但是他没有 set 。

  get value() {
    trackRefValue(this)
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    return toRaw(this)._get()
  }
  private _get() {
    if (this._dirty) {
      this._dirty = false
      return (this._value = this.effect.run()!)
    }
    return this._value
  }

同样都是使用 dirty 给锁上

那重点应该就是在 trigger 的实现里面

  constructor(getter: ComputedGetter<T>) {
    let compareTarget: any
    let hasCompareTarget = false
    let scheduled = false
    this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
      if (this.dep) {
        if (computedTrigger) {
          compareTarget = this._value
          hasCompareTarget = true
        } else if (!scheduled) {
          const valueToCompare = hasCompareTarget ? compareTarget : this._value
          scheduled = true
          hasCompareTarget = false
          scheduler(() => {
            if (this.effect.active && this._get() !== valueToCompare) {
              triggerRefValue(this)
            }scheduler
            scheduled = false
          })
        }
        // chained upstream computeds are notified synchronously to ensure
        // value invalidation in case of sync access; normal effects are
        // deferred to be triggered in scheduler.
        for (const e of this.dep) {
          if (e.computed) {
            e.scheduler!(true /* computedTrigger */)
          }
        }
      }
      this._dirty = true
    })
    this.effect.computed = true
  }

这里的 computedTrigger 参数是什么?

这里第二个参数是 scheduler ,我们看看当调用 scheduler 的时候给传入的什么参加就可以了

咦,看了看 effect 里面当调用 scheduler 的时候 并没有什么参数呀?

那执行看看

这个逻辑实在是太绕了。先放弃

这里有个有价值的是,进队列,在异步后执行的实现逻辑

const tick = Promise.resolve()
const queue: any[] = []
let queued = false

const scheduler = (fn: any) => {
  queue.push(fn)
  if (!queued) {
    queued = true
    tick.then(flush)
  }
}

const flush = () => {
  for (let i = 0; i < queue.length; i++) {
    queue[i]()
  }
  queue.length = 0
  queued = false
}

这里有个缺点就是没有看看进来的 fn 是不是已经收集过的。

effectScope 的实现

当 reactivity 这个库单独拿出去使用的时候,就会出现一系列的问题。

比如创建出来的 effect 变多了,如何去统一的 stop 掉。这个 api 就是解决这个问题的

之前在 vue 中使用的话,所有的 effect 都是和组件绑定在一起的。所以当组件销毁的时候,他会自动的把组件内所有的 effect 都 stop 掉。

先看看他的几个功能点

  1. 收集所有的 effect

  2. 包含了 effect

  3. watch

  4. computed

  5. watchEffect

  6. 可以统一的停止所有的 effect

  7. 也就是调用收集起来的 effect.stop

  8. 可以有多个 scope 联合起来使用

  9. 多个 scope 就会涉及到树结构

1. 看看多个 scope 是如何管理的
  1. 当 parent scope 清理的时候所有的 children 都会被清理掉

  2. 也可以设置一下 具体的那个 scope 可以不会被清理

从 api 上来看的话分为以下几个行为:

1. Basic Usage

  1. Nested Scopes

  2. Detached Nested Scopes

4.onScopeDispose

好了,接下来就依次来看看是如何实现的

看看是如何收集所有的 effect 的

  run<T>(fn: () => T): T | undefined {
    if (this.active) {
      try {
        this.on()
        return fn()
      } finally {
        this.off()
      }
    } else if (__DEV__) {
      warn(`cannot run an inactive effect scope.`)
    }
  }

关键函数是 this.on 和 this.off

//this.on
  on() {
    if (this.active) {
      effectScopeStack.push(this)
      activeEffectScope = this
    }
  }

咦,这里只做到了把 effectScope 自己收集起来,那在什么时候收集的 run 里面的 effect 呢?

这个问题的答案是在这里 recordEffectScope

export function recordEffectScope(
  effect: ReactiveEffect,
  scope?: EffectScope | null
) {
  scope = scope || activeEffectScope
  if (scope && scope.active) {
    scope.effects.push(effect)
  }
}

scope 就是 effectScope 了。而里面的逻辑也很简单 就是把 effect 给收集起来,而这个函数是在哪里调用的呢?

export class ReactiveEffect<T = any> {
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    recordEffectScope(this, scope)
  }

哈哈,是在 new ReactiveEffect 的时候,所以只要是 effect 创建了,那么就会收集到当前的 scope 里面了,这样就完成了收集 effect 的动作

这里有个发现是,在创建 effect 的时候,你可以指定具体的 scope 。不然的话 就是 activeEffectScope (当前的 scope 来收集 effect 了)

我们继续去看看 this.off 的实现

  off() {
    if (this.active) {
      effectScopeStack.pop()
      activeEffectScope = effectScopeStack[effectScopeStack.length - 1]
    }
  }

可以看到,这里是用 stack 来管理收集递归调用的,这里也和程序执行用 stack 来管理是一样的道理

stop 功能是如何实现的

  stop(fromParent?: boolean) {
    if (this.active) {
      this.effects.forEach(e => e.stop())
      this.cleanups.forEach(cleanup => cleanup())
      if (this.scopes) {
        this.scopes.forEach(e => e.stop(true))
      }
      // nested scope, dereference from parent to avoid memory leaks
      if (this.parent && !fromParent) {
        // optimized O(1) removal
        const last = this.parent.scopes!.pop()
        if (last && last !== this) {
          this.parent.scopes![this.index!] = last
          last.index = this.index!
        }
      }
      this.active = false
    }
  }

如果只忽略其他功能的话,那么核心只是一行代码

 this.effects.forEach(e => e.stop())

把收集起来的所有的 effect 都执行 stop 就完事了

剩下的逻辑是处理其他功能的。我们接着依次去看看

scope 里面会嵌套 scope,当执行 stop 的时候,内部的 scope 里面所有的 effect 也会都 stop,这个是怎么实现的?

如果让我自己来实现的话,那么首先是当前的 scope 应该把内部的所有 scope 都存起来,然后再 stop 的时候在调用内部 scope.stop 逻辑

那么先看看如何存的把:

这里是用 scopes 字段来存储的

  scopes: EffectScope[] | undefined

那看看在哪里 push 的把

  constructor(detached = false) {
    if (!detached && activeEffectScope) {
      this.parent = activeEffectScope
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }
  }

是在构造器里面执行的 push ,只不过这里判断了 detached 变量,因为在功能的使用上,如果当然的 scope 的 detached 为true 的话,那么表示当前的 scope 是不可以被清理的

又因为 activeEffectScope 是在执行 this.on 的时候才会被赋值,现在还是之前的也就是上一个 scope 所以可以是当前的 scope 的 parent

然后使用 parent 把当前的 scope 给收集起来,这样就完成了收集的处理,

Index 是做什么的, 好像是和后面优化的逻辑相关,暂时不关心

在去看看 stop 时候的处理

 if (this.scopes) {
        this.scopes.forEach(e => e.stop(true))
      }

简单 ,和刚刚上面猜测的一样,调用 scope 的 stop 方法就可以了

Detached Nested Scopes 如果 scope 是独立的话,那么当 parent scope 调用 stop 的时候 不应该被清理

这里的实现也很简单了,想一想,如果是 Detached 的话,那么就不应该被清除,那么只需要不收集到 scopes 里面不就完事了吗。 所以在构造器里面的实现就是判断一下

    if (!detached && activeEffectScope) {
      this.parent = activeEffectScope
      this.index =
        (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
          this
        ) - 1
    }

detached 是 true 的话,那么你就别给我收集了

onScopeDispose 是类似于组件的 onUnMounted 的功能的

也就是在 stop 执行完成后,被调用。让用户可以处理一些副作用(比如清空一些事件侦听)

export function onScopeDispose(fn: () => void) {
  if (activeEffectScope) {
    activeEffectScope.cleanups.push(fn)
  } else if (__DEV__) {
    warn(
      `onDispose() is called when there is no active effect scope ` +
        ` to be associated with.`
    )
  }
}

这里要注意的是, 清理函数是可以有多个的,所以这里是用 数组来存 cleanups

onScopeDispose 的逻辑就是收集所有的处理函数

接着是在 stop 完成之后调用即可

  stop(fromParent?: boolean) {
    if (this.active) {
      this.effects.forEach(e => e.stop())
      this.cleanups.forEach(cleanup => cleanup())
      if (this.scopes) {
        this.scopes.forEach(e => e.stop(true))
      }
      this.active = false
    }
  }

可以看到,这里的 cleanups 的执行是在 当前 scope 所有的 effect都执行完 stop 之后调用的

知识点

可以利用 stack 来处理递归的每一个状态

使用 Stack 来处理递归嵌套

可以使用二进制的方式来管理多个状态

这里的限制是这个状态只能是个 boolean

问题

targetType 都有哪几种?✅

如何区分触发依赖的时候是 ADD 还是 SET?✅

为什么清空 cleanup ,清空 effect 里面的 deps ✅

执行的代码变了,所以需要全部重新执行

effectsStack 和 trackStack 是为了处理什么场景的?✅

嵌套的场景

一个 stack 需要对应一个 effect

多层级递归调用的 demo 是哪一个 ✅

v8里面的 SMI 是什么?

总结

track 都做了什么

收集依赖

trigger 都做了什

调用收集到的所有的依赖

function ownKeys(target: object): (string | symbol)[] {

track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)

return Reflect.ownKeys(target)

}

effect trigger error

问题

image

image

user.age++ 触发依赖 trigger 中没有考虑到 depsMap.get(key) 为空的情况 导致语法错误

通过reactive 创建 响应式对象并没有 被track .

`const value = reactive({
foo: 1,
});

const getter = computed(() => {
return value.foo;
});

value.foo = 2 // 触发setter 函数时,会去调用trigger 函数,但发现 targetmap 是空的:原因就是 先要访问value.foo 才会track
console.log(getter.value)`

props update 比较时 出现的两个问题

Q1

import { h, ref } from "mini-vue.esm-bundler.js";
export const App = {
  name: "APP",
  render() {
    return h("div", { id: "root", ...this.props }, [
      h(
        "button",
        {
          onClick: this.change1,
        },
        "修改值修改"
      ),
    ]);
  },
  setup() {
    const props = ref({a:10});
    const change1 = () => {
      props.value.foo = "new foo";
    };
    return {
      change1,
      props,
    };
  },
};

点击按钮时
error:  dep is not iterable (cannot read property undefined)


Q2:
import { h, ref } from "mini-vue.esm-bundler.js";
export const App = {
  name: "APP",
  render() {
    return h("div", { id: "root", ...this.props }, [
      h(
        "button",
        {
          onClick: this.change1,
        },
        "修改值修改"
      ),
    ]);
  },
  setup() {
    const props = ref({ });
    const change1 = () => {
      props.value.foo = "new foo";
    };
    return {
      change1,
      props,
    };
  },
};

点击修改时更改的按钮
这时候 按理说 props 改变了 视图应该更新,但因为 之前没有收集到依赖,视图没有重新渲染

【看的见的思考】compiler-sfc

sfc 这个模块是由 vue-loader 来调用的,目的是解析 SFC文件

主入口是 parse

接着来分析一下

一开始的时候还是基于 compiler.parse 去生成 ast 对象

  const ast = compiler.parse(source, {
    // there are no components at SFC parsing level
    isNativeTag: () => true,
    // preserve all whitespaces
    isPreTag: () => true,
    getTextMode: ({ tag, props }, parent) => {
      // all top level elements except <template> are parsed as raw text
      // containers
      if (
        (!parent && tag !== 'template') ||
        // <template lang="xxx"> should also be treated as raw text
        (tag === 'template' &&
          props.some(
            p =>
              p.type === NodeTypes.ATTRIBUTE &&
              p.name === 'lang' &&
              p.value &&
              p.value.content &&
              p.value.content !== 'html'
          ))
      ) {
        return TextModes.RAWTEXT
      } else {
        return TextModes.DATA
      }
    },
    onError: e => {
      errors.push(e)
    }
  })

接着遍历 ast 来处理 template、script、style

    switch (node.tag) {
      case 'template':
        if (!descriptor.template) {
          const templateBlock = (descriptor.template = createBlock(
            node,
            source,
            false
          ) as SFCTemplateBlock)
          templateBlock.ast = node

        break

是创建一个 block 然后存放到 descriptor.template 内

 case 'script':
        const scriptBlock = createBlock(node, source, pad) as SFCScriptBlock
        const isSetup = !!scriptBlock.attrs.setup
        if (isSetup && !descriptor.scriptSetup) {
          descriptor.scriptSetup = scriptBlock
          break
        }
        if (!isSetup && !descriptor.script) {
          descriptor.script = scriptBlock
          break
        }
        errors.push(createDuplicateBlockError(node, isSetup))
        break

也是创建一个 block 放到 descriptor.scriptSetup

      case 'style':
        const styleBlock = createBlock(node, source, pad) as SFCStyleBlock
       
        descriptor.styles.push(styleBlock)
        break

style 和上面不同的话,是可以有多个

最后是处理自定义的

        descriptor.customBlocks.push(createBlock(node, source, pad))

都不是的话,那么就是 custom类型的blocks

最后是返回了 result 对象

 const result = {
    descriptor,
    errors
  }
  sourceToSFC.set(sourceKey, result)
  return result

这个东西就需要和 vue-loader 结合去看了

接着我们从单元测试看

  test('nested templates', () => {
    const content = `
    <template v-if="ok">ok</template>
    <div><div></div></div>
    `
    const { descriptor } = parse(`<template>${content}</template>`)
    console.log(descriptor)

    expect(descriptor.template!.content).toBe(content)
  })

然后我们看看 descriptor 长什么样子

 {
      filename: 'anonymous.vue',
      source: '<template>\n' +
        '    <template v-if="ok">ok</template>\n' +
        '    <div><div></div></div>\n' +
        '    </template>',
      template: {
        type: 'template',
        content: '\n    <template v-if="ok">ok</template>\n    <div><div></div></div>\n    ',
        loc: {
          source: '\n    <template v-if="ok">ok</template>\n    <div><div></div></div>\n    ',
          start: [Object],
          end: [Object]
        },
        attrs: {},
        ast: {
          type: 1,
          ns: 0,
          tag: 'template',
          tagType: 0,
          props: [],
          isSelfClosing: false,
          children: [Array],
          loc: [Object],
          codegenNode: undefined
        },
        map: {
          version: 3,
          sources: [Array],
          names: [],
          mappings: ';IACI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC',
          file: 'anonymous.vue',
          sourceRoot: '',
          sourcesContent: [Array]
        }
      },
      script: null,
      scriptSetup: null,
      styles: [],
      customBlocks: [],
      cssVars: [],
      slotted: false
    }

compileTemplate

先看看是如何解析 template 的

先从单元测试看起

test('should work', () => {
  const source = `<div><p>{{ render }}</p></div>`

  const result = compile({ filename: 'example.vue', source })

  expect(result.errors.length).toBe(0)
  expect(result.source).toBe(source)
  // should expose render fn
  expect(result.code).toMatch(`export function render(`)
})

compile 就是调用的 compileTemplate

export function compileTemplate(
  options: SFCTemplateCompileOptions
): SFCTemplateCompileResults {
  const { preprocessLang, preprocessCustomRequire } = options

  const preprocessor = preprocessLang
    ? preprocessCustomRequire
      ? preprocessCustomRequire(preprocessLang)
      : require('consolidate')[preprocessLang as keyof typeof consolidate]
    : false
  if (preprocessor) {
    try {
      return doCompileTemplate({
        ...options,
        source: preprocess(options, preprocessor)
      })
    } catch (e) {
      return {
        code: `export default function render() {}`,
        source: options.source,
        tips: [],
        errors: [e]
      }
    }
  } else if (preprocessLang) {
    return {
      code: `export default function render() {}`,
      source: options.source,
      tips: [
        `Component ${options.filename} uses lang ${preprocessLang} for template. Please install the language preprocessor.`
      ],
      errors: [
        `Component ${options.filename} uses lang ${preprocessLang} for template, however it is not installed.`
      ]
    }
  } else {
    return doCompileTemplate(options)
  }
}

接着我们去把逻辑给拆分

  const { preprocessLang, preprocessCustomRequire } = options

是给用户做扩展用的接口,可以先忽略掉

接着是调用了 doCompileTemplate

else {
    return doCompileTemplate(options)
  }
function doCompileTemplate({
  filename,
  id,
  scoped,
  slotted,
  inMap,
  source,
  ssr = false,
  ssrCssVars,
  isProd = false,
  compiler = ssr ? (CompilerSSR as TemplateCompiler) : CompilerDOM,
  compilerOptions = {},
  transformAssetUrls
}: SFCTemplateCompileOptions): SFCTemplateCompileResults {
  const errors: CompilerError[] = []
  const warnings: CompilerError[] = []

  let nodeTransforms: NodeTransform[] = []
  if (isObject(transformAssetUrls)) {
    const assetOptions = normalizeOptions(transformAssetUrls)
    nodeTransforms = [
      createAssetUrlTransformWithOptions(assetOptions),
      createSrcsetTransformWithOptions(assetOptions)
    ]
  } else if (transformAssetUrls !== false) {
    nodeTransforms = [transformAssetUrl, transformSrcset]
  }

  const shortId = id.replace(/^data-v-/, '')
  const longId = `data-v-${shortId}`

  let { code, ast, preamble, map } = compiler.compile(source, {
    mode: 'module',
    prefixIdentifiers: true,
    hoistStatic: true,
    cacheHandlers: true,
    ssrCssVars:
      ssr && ssrCssVars && ssrCssVars.length
        ? genCssVarsFromList(ssrCssVars, shortId, isProd)
        : '',
    scopeId: scoped ? longId : undefined,
    slotted,
    ...compilerOptions,
    nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
    filename,
    sourceMap: true,
    onError: e => errors.push(e),
    onWarn: w => warnings.push(w)
  })

  // inMap should be the map produced by ./parse.ts which is a simple line-only
  // mapping. If it is present, we need to adjust the final map and errors to
  // reflect the original line numbers.
  if (inMap) {
    if (map) {
      map = mapLines(inMap, map)
    }
    if (errors.length) {
      patchErrors(errors, source, inMap)
    }
  }

  const tips = warnings.map(w => {
    let msg = w.message
    if (w.loc) {
      msg += `\n${generateCodeFrame(
        source,
        w.loc.start.offset,
        w.loc.end.offset
      )}`
    }
    return msg
  })

  return { code, ast, preamble, source, errors, tips, map }
}

继续拆分

  let nodeTransforms: NodeTransform[] = []
  if (isObject(transformAssetUrls)) {
    const assetOptions = normalizeOptions(transformAssetUrls)
    nodeTransforms = [
      createAssetUrlTransformWithOptions(assetOptions),
      createSrcsetTransformWithOptions(assetOptions)
    ]
  } else if (transformAssetUrls !== false) {
    nodeTransforms = [transformAssetUrl, transformSrcset]
  }

这里依然是给用户做扩展的

最终会影响 nodeTransforms 的值

也就是说 不同的 transformAssetUrls 会有不同的 transform

下面是核心代码

  let { code, ast, preamble, map } = compiler.compile(source, {
    mode: 'module',
    prefixIdentifiers: true,
    hoistStatic: true,
    cacheHandlers: true,
    ssrCssVars:
      ssr && ssrCssVars && ssrCssVars.length
        ? genCssVarsFromList(ssrCssVars, shortId, isProd)
        : '',
    scopeId: scoped ? longId : undefined,
    slotted,
    ...compilerOptions,
    nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
    filename,
    sourceMap: true,
    onError: e => errors.push(e),
    onWarn: w => warnings.push(w)
  })

这里的 compiler 是来自 compiler-dom ,执行 compile 开始编译

所以 template 就是收集一些特有的数据 ,然后给到 compiler.compile 进行编译,最后得到数据完事。


看看 template 如果是 pug 的话,是如何处理的

test('preprocess pug', () => {
  const template = parse(
    `
<template lang="pug">
body
  h1 Pug Examples
  div.container
    p Cool Pug example!
</template>
`,
    { filename: 'example.vue', sourceMap: true }
  ).descriptor.template as SFCTemplateBlock

  const result = compile({
    filename: 'example.vue',
    source: template.content,
    preprocessLang: template.lang
  })

  expect(result.errors.length).toBe(0)
})

如果template 是 pug 的话,那么 vue3 会调用 consolidate 这个库来处理解析

consolidate 是个template 大杂烩,做了一个中间层

那么换个角度来讲的话,只要是 consolidate 支持的template ,vue3 的template 就会支持

compileScript

接着看看如何处理 script 的

还是先从单元测试入手

  test('should expose top level declarations', () => {
    const { content, bindings } = compile(`
      <script setup>
      import { x } from './x'
      let a = 1
      const b = 2
      function c() {}
      class d {}
      </script>

      <script>
      import { xx } from './x'
      let aa = 1
      const bb = 2
      function cc() {}
      class dd {}
      </script>
      `)
    expect(content).toMatch('return { aa, bb, cc, dd, a, b, c, d, xx, x }')
    expect(bindings).toStrictEqual({
      x: BindingTypes.SETUP_MAYBE_REF,
      a: BindingTypes.SETUP_LET,
      b: BindingTypes.SETUP_CONST,
      c: BindingTypes.SETUP_CONST,
      d: BindingTypes.SETUP_CONST,
      xx: BindingTypes.SETUP_MAYBE_REF,
      aa: BindingTypes.SETUP_LET,
      bb: BindingTypes.SETUP_CONST,
      cc: BindingTypes.SETUP_CONST,
      dd: BindingTypes.SETUP_CONST
    })
    assertCode(content)
  })

这里的 compile 实际是调用了 compileSFCScript

这里最终实现的就是把 script 代码编译成可以让 runtime 执行的js代码

import { x } from './x'\n      \nexport default {\n  setup(__props, { expose }) {\n  expose()\n\n      let a = 1\n      const b = 2\n      function c() {}\n      class d {}\n      \nreturn { aa, bb, cc, dd, a, b, c, d, xx, x }\n}\n\n}\n      import { xx } from './x'\n      let aa = 1\n      const bb = 2\n      function cc() {}\n      class dd {}

接着看看调用的主流程

会调用compileScript,代码量很大,我们分拆这来看

export function compileScript(
  sfc: SFCDescriptor,
  options: SFCScriptCompileOptions
): SFCScriptBlock {
  const { descriptor } = parse(src)

先看看 input ,这里的 sfc 是通过 parse 过得到的对象,是已经把src也就是string代码编译成对象了

接着就是通过这个对象上面的数据信息来进行处理就可以了

在一开始的时候先收集数据,进行初始化

  const scopeId = options.id ? options.id.replace(/^data-v-/, '') : ''
  const cssVars = sfc.cssVars
  const scriptLang = script && script.lang
  const scriptSetupLang = scriptSetup && scriptSetup.lang
  const isTS =
    scriptLang === 'ts' ||
    scriptLang === 'tsx' ||
    scriptSetupLang === 'ts' ||
    scriptSetupLang === 'tsx'
  const plugins: ParserPlugin[] = [...babelParserDefaultPlugins]
  if (!isTS || scriptLang === 'tsx' || scriptSetupLang === 'tsx') {
    plugins.push('jsx')
  }
  if (options.babelParserPlugins) plugins.push(...options.babelParserPlugins)
  if (isTS) plugins.push('typescript', 'decorators-legacy')

然后先处理的是 普通的 scirpt

处理普通的 script

    try {
      let content = script.content
      let map = script.map
      const scriptAst = _parse(content, {
        plugins,
        sourceType: 'module'
      }).program
      const bindings = analyzeScriptBindings(scriptAst.body)
      if (enableRefTransform && shouldTransformRef(content)) {
        const s = new MagicString(source)
        const startOffset = script.loc.start.offset
        const endOffset = script.loc.end.offset
        const { importedHelpers } = transformRefAST(scriptAst, s, startOffset)
        if (importedHelpers.length) {
          s.prepend(
            `import { ${importedHelpers
              .map(h => `${h} as _${h}`)
              .join(', ')} } from 'vue'\n`
          )
        }
        s.remove(0, startOffset)
        s.remove(endOffset, source.length)
        content = s.toString()
        map = s.generateMap({
          source: filename,
          hires: true,
          includeContent: true
        }) as unknown as RawSourceMap
      }
  1. 先解析 script里面的代码,搞成 ast 对象

  2. 这里是基于 babel 来解析的 JS 代码

  3. 基于 ast.body 生成bindings

  4. 看看有没有开启 enableRefTransform,开启的话处理

      if (cssVars.length) {
        content = rewriteDefault(content, `__default__`, plugins)
        content += genNormalScriptCssVarsCode(
          cssVars,
          bindings,
          scopeId,
          !!options.isProd
        )
        content += `\nexport default __default__`
      }
      return {
        ...script,
        content,
        map,
        bindings,
        scriptAst: scriptAst.body
      }
  1. 处理 cssVars

完事就直接返回了,可以看到普通的 script 的处理过程是比较简单的

处理 script setup

处理 setup 代码就多了去了

  // metadata that needs to be returned
  const bindingMetadata: BindingMetadata = {}
  const defaultTempVar = `__default__`
  const helperImports: Set<string> = new Set()
  const userImports: Record<string, ImportBinding> = Object.create(null)
  const userImportAlias: Record<string, string> = Object.create(null)
  const setupBindings: Record<string, BindingTypes> = Object.create(null)

  let defaultExport: Node | undefined
  let hasDefinePropsCall = false
  let hasDefineEmitCall = false
  let hasDefineExposeCall = false
  let propsRuntimeDecl: Node | undefined
  let propsRuntimeDefaults: Node | undefined
  let propsTypeDecl: TSTypeLiteral | TSInterfaceBody | undefined
  let propsTypeDeclRaw: Node | undefined
  let propsIdentifier: string | undefined
  let emitsRuntimeDecl: Node | undefined
  let emitsTypeDecl:
    | TSFunctionType
    | TSTypeLiteral
    | TSInterfaceBody
    | undefined
  let emitsTypeDeclRaw: Node | undefined
  let emitIdentifier: string | undefined
  let hasAwait = false
  let hasInlinedSsrRenderFn = false
  // props/emits declared via types

先初始化后面需要用到的数据


总结

目标:compiler-sfc 是把 sfc 里面的string生成对应的用 js 表达的组件代码

基本的逻辑是先解析 SFC 文件,生成对应的对象。这个对象里面包含了所有 SFC 的信息

然后在把 template script style 分别交给各自的编译器来编译成对象

template 就是用的compiler-dom 来解析的

script 用的是 babel 来解析的

style 暂时还不知道 TODO

然后基于ast 对象就可以对代码做手术了,比如获取到某些信息,然后基于这些信息生成对应的 js 代码

runtime-core 中 updateElement 无法处理 Text to Array 的情况

测试用例

// @example/patchChildren/TextToArray.js
const prevChildren = 'oldChild';
const nextChildren = [h('div', {}, 'A'), h('div', {}, 'B')];

export default {
  name: 'TextToArray',
  setup() {
    const isChange = ref(false);
    window.isChange = isChange;

    return {
      isChange,
    };
  },
  render() {
    const self = this;

    return self.isChange === true
      ? h('div', {}, nextChildren)
      : h('div', {}, prevChildren);
  },
};

深度截图_选择区域_20221014225546

trackEffects()方法,会有‘循环引用’ (不知道这个词是否恰当)吗?

image
export function trackEffects(dep) {
//看看 dep 之前有没有添加过,添加过的话 那么就不添加了
if (dep.has(activeEffect)) return;
dep.add(activeEffect)
activeEffect.deps.push(dep); // 反向搜集

// start --- test 
let count = 0;
if( activeEffect.deps.length ){
    testMethod(activeEffect);
}
function testMethod(activeEffect){
    const dep = activeEffect.deps[0]
    for (const effect of dep) {            // debugger
        count++;
        if(count > 1000){
            debugger
            console.log('循环结束')
            return;
        }
        testMethod(effect);
        
    }
}
// end -- test 

}

patchKeyedChildren实现中,hostInsert的顺序问题

for (let i = e2; i >= s2; i--) {

  // ...
  const anchor = i + 1 >= e2 + 1 ? null : c2[i + 1];
  hostInsert(newChild.el, container, anchor && anchor.el)
}

会不会造成插入节点是反向的?appendChild不是顺序插入的么

【看的见的思考】nextTick 的实现

问题

  1. 为什么需要先执行父组件

看的见的思考

vue3 的 nextTick 的实现是在 scheduler.ts 实现的

scheduler 中文翻译过来是调度器,但是这个调度器这个词还是有点抽象啊。[[调度器]]

先找到核心执行逻辑

function flushJobs(seen?: CountMap) { isFlushPending = false
  isFlushing = true
  let job
  if (__DEV__) {
    seen = seen || new Map()
  }

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child so its render effect will have smaller
  //    priority number)
  // 2. If a component is unmounted during a parent component's update,
  //    its update can be skipped.
  // Jobs can never be null before flush starts, since they are only invalidated
  // during execution of another flushed job.
  queue.sort((a, b) => getId(a!) - getId(b!))

  while ((job = queue.shift()) !== undefined) {
    if (job === null) {
      continue
    }
    if (__DEV__) {
      checkRecursiveUpdates(seen!, job)
    }
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  }
  flushPostFlushCbs(seen)
  isFlushing = false
  // some postFlushCb queued jobs!
  // keep flushing until it drains.
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen)
  }
}

这里有几个关键的点,

  1. queue
  2. flushPostFlushCbs

先看看 queue 这个队列是干什么的? 队列里面存的是什么?在什么时候存的

export function queueJob(job: Job) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

是通过 queueJob 来收集 job 的,那接着看看 job 都是什么东西

const prodEffectOptions = {
  scheduler: queueJob
}

function createDevEffectOptions(
  instance: ComponentInternalInstance
): ReactiveEffectOptions {
  return {
    scheduler: queueJob,
    onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc!, e) : void 0,
    onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg!, e) : void 0
  }
}
    instance.update = effect(function componentEffect() {

啊哦,原来是传给了做 update 时的 effect 的配置里面

注意这个 scheduler:queueJob

在回顾一下 如果给了 effect options 里面有 scheduler 的话,effect 会有什么行为

  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()
    }
  }

啊哦,当响应式对象发生改变后,执行 effect 的时候,如果有 scheduler 这个 option 的话,会执行这个 scheduler 函数,并且把 effect 传入

那其实这里的 scheduler 函数就是 上面的 queueJob 函数,并且 effect 就是 update 函数

那也就是说,每次更新的时候都会把 update 这个函数推入到 queueJob 内

那推入后呢?

在回顾一下 queueJob 函数

export function queueJob(job: Job) {
  if (!queue.includes(job)) {
    queue.push(job)
    queueFlush()
  }
}

这里的 job 就是 update 函数,那么这里只会推入一次,也就是说,当后续执行 queueFlush 之后,才会执行 update 函数。

嗯,这样的话就可以避免修改了数据之后就会立马渲染页面了! 棒!

其实 nextTick 的目的也是如此,而且这个做法在游戏里面也有,我自己在写物理引擎的时候也用过,但是没有现在理解的更清晰

继续继续,看看接下来做什么了

function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    nextTick(flushJobs)
  }
}

那其实接着就是执行 queueFlush 了,这里的关键是调用了 nextTick 给了 flushJobs 函数

而 flushJobs 函数就是我们最开始看到的入口函数,在那里会最终执行我们传入的 job 函数

在看 flushJobs 之前 ,先看看 nextTick 是怎么实现的

const p = Promise.resolve()

export function nextTick(fn?: () => void): Promise<void> {
  return fn ? p.then(fn) : p
}

这里还挺简单的,就是用的 Promise.resolve() ,把要执行的函数延迟到 微任务队列里面执行。

接着我们看看在最终执行到微任务队列的时候是怎么执行 flushJobs 函数的把
这里暂时只关注和 queue 队列有关的逻辑点

  queue.sort((a, b) => getId(a!) - getId(b!))

  while ((job = queue.shift()) !== undefined) {
    if (job === null) {
      continue
    }
    if (__DEV__) {
      checkRecursiveUpdates(seen!, job)
    }
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  }
    isFlushing = false
  // some postFlushCb queued jobs!
  // keep flushing until it drains.
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen)
  }

看起来很简单,先排序(为什么需要排序呢? 这里的 id 是什么时候给的,)

然后取队列的头部 job 执行就完事了

这里是个递归的操作

好了,那接着要搞懂的问题就是这个 id 的问题

在 sort 的逻辑上有详细的注释

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child so its render effect will have smaller
  //    priority number)
  // 2. If a component is unmounted during a parent component's update,
  //    its update can be skipped.
  // Jobs can never be null before flush starts, since they are only invalidated
  // during execution of another flushed job.

尝试着翻译翻译,然后理解一下是啥意思

有可能会涉及到设置 id 的信息

  1. 组件更新从父级到孩子,因为父级总是在子组件之前创建好的,所以它渲染 effect 有较小的优先级(因为初始化的时候先创建的父组件,所以父组件的 id 是小的,换句话说也就是会先执行父组件)
  2. 如果一个组件在父级组件更新时是 unmounted 的,那么它的更新会被跳过(这个没太理解,找一找对应的 demo 来验证一下)

好,到这里的时候其实我们能理解调用的优先级是先调用父组件

因为创建 update = effect(fn) 的时候,这里的 effect 的 id 是从零开始计算的,又因为先初始化父级组件,所以后面更新的时候基于 id 排序,会先执行父级组件的 update

那其实我想知道的是,为什么需要先执行父组件?

暂时想不出来 先记录一下


还有一个队列就是 queuePostRenderEffect 的应用了

export function queuePostFlushCb(cb: Function | Function[]) {
  if (!isArray(cb)) {
    postFlushCbs.push(cb)
  } else {
    postFlushCbs.push(...cb)
  }
  queueFlush()
}

和 queue 队列不同,它把收集的 job 都添加到 postFlushCbs 数组里面去

  queue.sort((a, b) => getId(a!) - getId(b!))

  // 处理 queue 队列
  while ((job = queue.shift()) !== undefined) {
    if (job === null) {
      continue
    }
    if (__DEV__) {
      checkRecursiveUpdates(seen!, job)
    }
    callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  }
  // 处理完 queue 队列后处理 postFlushCbs 队列
  flushPostFlushCbs(seen)
  isFlushing = false
  // some postFlushCb queued jobs!
  // keep flushing until it drains.
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen)
  }

然后是在处理完 queue 之后再处理 postFlushCbs ,从命名上也能体现出来 post (后刷新)

接着的重点是看看都是把什么任务添加到这个 postFlushCbs 队列来呢?

export const queuePostRenderEffect = __FEATURE_SUSPENSE__
  ? queueEffectWithSuspense
  : queuePostFlushCb

在 renderer.js 使用的时候给它改了个名称叫做 queuePostRenderEffect ,从命名上我们能猜到是在 渲染 effect 之后调用的队列

对 Suspense 组件的处理我们以后单独搞一个章节来分析

 queuePostRenderEffect(() => {
        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
        transition && !transition.persisted && transition.enter(el)
        dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
      }, parentSuspense)
 if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
      queuePostRenderEffect(() => {
        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
        dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated')
      }, parentSuspense)
    }
  // mounted hook
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        // onVnodeMounted
        if ((vnodeHook = props && props.onVnodeMounted)) {
          queuePostRenderEffect(() => {
            invokeVNodeHook(vnodeHook!, parent, initialVNode)
          }, parentSuspense)
        }
        // activated hook for keep-alive roots.
        if (
          a &&
          initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        ) {
          queuePostRenderEffect(a, parentSuspense)
        }
        instance.isMounted = true

会发现在处理一些 hook 的时候都会用这个方法,也就是说 hook 的处理都要等到处理完 update 逻辑。

至于为什么,我还没有猜到

补充:

我发现如果是触发 beforeXxx 的逻辑的时候直接调用 hook 即可

而如果是触发 xxxed 的逻辑的时候需要等到渲染之后

那么这样就能理解了为什么要等到渲染之后

不渲染完的话,怎么能叫 xxxed 呢? 哈哈

这里的 xxxed 指的是 mounted updated 等等


到此为止,其实 nextTick 已经被分析的差不多了,还剩下几个为什么这么做的问题,当然了着就是思考的价值所在,也是看源码的意义,就是它是解决什么问题的,也就是 why 层面的东西。


总结

问题

我们先聊一个场景,当响应式对象发生改变会,会触发 update 逻辑,当触发 update 逻辑后立马重新渲染视图的话 ok 没有问题

但是我们需要考虑这么一个场景

var count = ref(10)

for(let i=0; i<100; i++){
	count = i
}

我把响应式对象在 for 循环中(同一帧)更新了 100 次,如果按照我们上面的更新策略,那么就需要更新 100 次视图(响应式数据变更就会触发重新渲染视图)

那我们怎么去优化这个问题呢?

其实通过观察我们会发现,最后的结果就是渲染 count 为 100 的情况。

那么我们就要想办法做到响应式完全变更完之后再渲染视图了

那怎么做呢?

怎么做

我们可以利用 js 的事件循环机制,上面的这个 for 循环的操作是在当前执行栈内执行(同步的),当前执行栈执行完成后,js 会检查事件队列里面的异步任务 (会先执行 微任务,然后在执行宏任务),所以我们完全可以把渲染的逻辑延迟到微任务里执行

这样就可以解决上面说的问题了,而着其实就是 vue.nextTick 要解决的问题,以及它的解决方案

vue 中维护了一个队列,当响应式对象发生变更后,会把 update 函数 push 到队列内,在入队列的时候还做了个检查,如果添加过就不会在添加了。确保同一个 update 函数只执行一次。然后利用 Promise.resolve(), 在微任务执行的时候在去执行这个队列里面所有的函数。

readme中的图片不能打开

您好,我在访问readme中的图片时都不能打开
浏览器版本:
chrome版本84,firefox版本85
报错信息:
建立安全连接失败
连接到 camo.githubusercontent.com 时发生错误。PR_CONNECT_RESET_ERROR

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.