Vue3 源码构建

12/6/2021 vuesetup

# Vue3源码构建

# 应用初始化

createApp(App).mount(rootContainer)

# 渲染器

创建一个渲染器对象,包含平台核心逻辑。

function ensureRenderer() {
  // 如果 renderer 有值的话,那么以后都不会初始化了
  return (
    renderer ||
    (renderer = createRenderer({
      createElement,
      createText,
      setText,
      setElementText,
      patchProp,
      insert,
      remove,
    }))
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

通过createRenderer创建渲染器,

createRenderer(oprions){
    //...
    return {
        createApp:createAppAPI(render)
    }
}
1
2
3
4
5
6

将app对象作为根组件传递,app组件提供一个mount方法挂载对象

function createAppAPI(render) {
  return function createApp(rootComponent) {
    const app = {
      _component: rootComponent,
      mount(rootContainer) {
        console.log("基于根组件创建 vnode");
        const vnode = createVNode(rootComponent);
        console.log("调用 render,基于 vnode 进行开箱");
        render(vnode, rootContainer);
      },
    };

    return app;
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这里mount方法会针对跨平台做出适配,先创建vnode再去根据不同平台render

# vnode

vnode将渲染过程抽象化,使组件抽象能力提升,以适配不同平台。

类型

  • 普通元素vnode
  • 组件vnode
  • 纯文本vnode,注释vnode

查找vnode类型(实际更多类型)

function getShapeFlag(type: any) {//{name:API,setup(){...}}
  return typeof type === "string"
    ? ShapeFlags.ELEMENT//普通元素
    : ShapeFlags.STATEFUL_COMPONENT;//组件
}
1
2
3
4
5

创建vnode

createVNode = function (
  type: any,
  props?: any,
  children?: string | Array<any>
) {
  if(props){
      //处理props相关逻辑,标准化class等
  }

  // 基于 children 再次设置 shapeFlag
  if (Array.isArray(children)) {
    vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN;
  } else if (typeof children === "string") {
    vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN;
  }

  const vnode = {
    el,
    component,
    key,
    type,
    props,
    shapeFlag,
    children,
  };
    
  normalizeChildren(vnode, children);

  return vnode;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

渲染vnode

const render = (vnode, container) => {
    if(vnode==null){
        //销毁组件umount
    }else{
        patch(container._vnode||null, vnode, container);
    }
    
};
1
2
3
4
5
6
7
8

path挂载和更新vnode

function patch(n1, n2, container = null, parentComponent = null) {
    // n1为null表示挂载
    // 基于 n2 的类型来判断
    // 因为 n2 是新的 vnode
    // container是vnode渲染成DOM后挂载的地方
    const { type, shapeFlag } = n2;
    switch (type) {
      case Text:
        processText(n1, n2, container);
        break;
      // 其中还有几个类型比如: static fragment comment
      case Fragment:
        processFragment(n1, n2, container);
        break;
      default:
        // 这里就基于 shapeFlag 来处理
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(n1, n2, container);
        } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
          processComponent(n1, n2, container, parentComponent);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 组件vnode处理

组件处理逻辑processComponent

function processComponent(n1, n2, container, parentComponent) {
    // 如果 n1 没有值的话,那么就是 mount
    if (!n1) {
      // 初始化 component
      mountComponent(n2, container, parentComponent);
    } else {
      updateComponent(n1, n2, container);
    }
  }
1
2
3
4
5
6
7
8
9

挂载组件mountComponent

function mountComponent(initialVNode, container, parentComponent) {
    // 1. 先创建一个组件实例
    const instance = (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent
    ));
    // 2. 处理组件实例,instance保留了很多组件数据,维护上下文(props、slot...)
    setupComponent(instance);
    // 3. 运行带副作用的渲染函数
    setupRenderEffect(instance, initialVNode, container);
}
1
2
3
4
5
6
7
8
9
10
11

setupRenderEffect中初始渲染流程

		// 组件初始化的时候会执行这里
        // 调用 render 函数template也会编译成render函数
		// 源码中是调用renderComponentRoot,这里直接调用render
        // 是因为在 effect 内调用 render 才能触发依赖收集
        // 等到后面响应式的值变更后会再次触发这个函数
        const proxyToUse = instance.proxy;
        // 可在 render 函数中通过 this 来使用 proxy
        const subTree = (instance.subTree = instance.render.call(
          proxyToUse,
          proxyToUse
        ));
        // 这里基于 subTree 再次调用 patch
        // 基于 render 返回的 vnode ,再次进行渲染
        // 这里我把这个行为隐喻成开箱
        // 一个组件就是一个箱子
        // 里面有可能是 element (也就是可以直接渲染的)
        // 也有可能还是 component
        // 这里就是递归的开箱
        // 而 subTree 就是当前的这个箱子(组件)装的东西
        // 箱子(组件)只是个概念,它实际是不需要渲染的
        // 要渲染的是箱子里面的 subTree
        patch(null, subTree, container, instance);
        // 把 root element 赋值给 组件的vnode.el ,为后续调用 $el 的时候获取值
        initialVNode.el = subTree.el;
        instance.isMounted = true;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

subTree实际也是一个vnode(子树vnode),实际上是一个迭代过程

# 元素vnode处理

元素处理逻辑processElement

function processElement(n1, n2, container) {
    if (!n1) {
        mountElement(n2, container);
    } else {
        updateElement(n1, n2, container);
    }
}
1
2
3
4
5
6
7

元素挂载mountElement

function mountElement(vnode, container){
    const { shapeFlag, props } = vnode;
    // 1. 先创建 element
    // 基于可扩展的渲染 api
    const el = (vnode.el = hostCreateElement(vnode.type));
    // 支持单子组件和多子组件的创建
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // 举个栗子
      // render(){
      //     return h("div",{},"test")
      // }
      // 这里 children 就是 test ,只需要渲染一下就完事了
      hostSetElementText(el, vnode.children);
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 举个栗子
      // render(){
      // Hello 是个 component
      //     return h("div",{},[h("p"),h(Hello)])
      // }
      // 这里 children 就是个数组了,就需要依次调用 patch 递归来处理
      mountChildren(vnode.children, el);
    }
    // 处理 props
    if (props) {
      for (const key in props) {
        const nextVal = props[key];
        hostPatchProp(el, key, null, nextVal);
      }
    }
    // 插入
    hostInsert(el, container);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

hostCreateElement这个是更具平台变化的(闭包)

function createElement(tag,isSVG,is){
	isSVG?document.createElementNS(svgNS,tag):document.createElement(tag,is?{is}:undefined)
}
1
2
3

mountChildren也是一个递归patch的过程

hostInsert

function insert(child,parent,anchor){
    //anchor 参考元素
    if(anchor){
        parent.insertBefore(child,anchor)
    }else{
        parent.appendChild(child)
    }
}
1
2
3
4
5
6
7
8

至此组件渲染大体流程完成


# 组件更新

update

组件的渲染过程中创建了一个带副作用的渲染函数(setupRenderEffect),当数据变化时执行这个渲染函数触发更新

setupRenderEffect中更新渲染流程

function setupRenderEffect(instance,initialVNode,container){
    function componentUpdateFn() {
      if (!instance.isMounted) {
          //见上
      } else {
        // 响应式的值变更后会从这里执行逻辑
        // 主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
        // 拿到最新的 subTree
        const { next, vnode } = instance;

        // 如果有 next 的话, 说明需要更新组件的数据(props,slots 等)
        // 先更新组件的数据,然后更新完成后,在继续对比当前组件的子元素
        if (next) {
          next.el = vnode.el;
          updateComponentPreRender(instance, next);
        }

        const proxyToUse = instance.proxy;
        const nextTree = instance.render.call(proxyToUse, proxyToUse);
        // 替换之前的 subTree
        const prevTree = instance.subTree;
        instance.subTree = nextTree;

        // 触发 beforeUpdated hook
        console.log("beforeUpdated hook");
        console.log("onVnodeBeforeUpdate hook");

        // 用旧的 vnode 和新的 vnode 交给 patch 来处理
        patch(prevTree, nextTree, prevTree.el, instance);

        // 触发 updated hook
        console.log("updated hook");
        console.log("onVnodeUpdated hook");
      }
    }
		
    instance.update = effect(componentUpdateFn, {
        scheduler: () => {
            // 把 effect 推到微任务的时候在执行
            // queueJob(effect);
            queueJob(instance.update);
        },
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

进入patch流程->processComponent->updateComponent

function updateComponent(n1, n2, container) {
    // 更新组件实例引用
    const instance = (n2.component = n1.component);
    // 先看看这个组件是否应该更新更具porps、children等属性
    if (shouldUpdateComponent(n1, n2)) {
      // 那么 next 就是新的 vnode 了(也就是 n2)
      instance.next = n2;
      // 这里的 update 是在 setupRenderEffect 里面初始化的,update 函数除了当内部的响应式对象发生改变的时候会调用
      // 还可以直接主动的调用(这是属于 effect 的特性)
      // 调用 update 再次更新调用 patch 逻辑
      // 在update 中调用的 next 就变成了 n2了
      instance.update();
    } else {
      // 不需要更新的话,那么只需要覆盖下面的属性即可
      n2.component = n1.component;
      n2.el = n1.el;
      instance.vnode = n2;
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

组件重新渲染的两种可能场景

  • 组件本身数据变化,next=null
  • 父组件在更新过程中,遇到子组件节点,先判断是否需要更新,需要则主动执行子组件的重新渲染方法,此时next=新的子组件vnode

# 普通元素更新

processElement->updateElement

function updateElement(n1, n2, container) {
    const oldProps = (n1 && n1.props) || {};
    const newProps = n2.props || {};
    // 应该更新 element
    // 需要把 el 挂载到新的 vnode
    const el = (n2.el = n1.el);

    // 更新 props
    patchProps(el, oldProps, newProps);

    // 更新 children
    patchChildren(n1, n2, el);
  } 
1
2
3
4
5
6
7
8
9
10
11
12
13

更新子节点,子节点(文本,数组,空)新老vnode组合9种处理

function patchChildren(n1, n2, container) {
    const { shapeFlag: prevShapeFlag, children: c1 } = n1;
    const { shapeFlag, children: c2 } = n2;

    // 如果 n2 的 children 是 text 类型的话
    // 就看看和之前的 n1 的 children 是不是一样的
    // 如果不一样的话直接重新设置一下 text 即可
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        if (c2 !== c1) {
            console.log("类型为 text_children, 当前需要更新");
            hostSetElementText(container, c2 as string);
        }
    } else {
        // 如果之前是 array_children
        // 现在还是 array_children 的话
        // 那么我们就需要对比两个 children 啦
        if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
            if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
                patchKeyedChildren(c1, c2, container);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# diff算法处理新老vnode都是数组的情况
function patchKeyedChildren(c1: any[], c2: any[], container) {
    let i = 0;
    let e1 = c1.length - 1;
    let e2 = c2.length - 1;

    const isSameVNodeType = (n1, n2) => {
        return n1.type === n2.type && n1.key === n2.key;
    };
	//1. 同步头部相同节点,相同就patch
    while (i <= e1 && i <= e2) {
        const prevChild = c1[i];
        const nextChild = c2[i];

        if (!isSameVNodeType(prevChild, nextChild)) {
            //两个 child 不相等(从左往右比对)
            break;
        }

        //两个 child 相等,接下来对比着两个 child 节点(从左往右比对)");
        patch(prevChild, nextChild, container);
        i++;
    }
	//2. 从尾部开始同步尾部节点,相同就patch
    while (i <= e1 && i <= e2) {
        // 从右向左取值
        const prevChild = c1[e1];
        const nextChild = c2[e2];

        if (!isSameVNodeType(prevChild, nextChild)) {
            //两个 child 不相等(从右往左比对)
            break;
        }
        //两个 child 相等,接下来对比着两个 child 节点(从右往左比对)
        patch(prevChild, nextChild, container);
        e1--;
        e2--;
    }
	//3. 新子节点需要添加
    if (i > e1 && i <= e2) {
        while (i <= e2) {
            //需要新创建一个 vnode
            patch(null, c2[i], container);
            i++;
        }
    } 
    //4. 旧子节点需要删除
    else if (i > e2 && i <= e1) {
        while (i <= e1) {
            //需要删除当前的 vnode
            hostRemove(c1[i].el);
            i++;
        }
    }
    //5. 未知子序列
    else {
        let s1 = i;
        let s2 = i;
        //6. 根据key建立索引图
        const keyToNewIndexMap = new Map();
        // 先把 key 和 newIndex 绑定好,方便后续基于 key 找到 newIndex
        for (let i = s2; i <= e2; i++) {
            const nextChild = c2[i];
            keyToNewIndexMap.set(nextChild.key, i);
        }

        // 需要处理新节点的数量
        const toBePatched = e2 - s2 + 1;
        const newIndexToOldIndexMap = new Array(toBePatched);
        for (let index = 0; index < newIndexToOldIndexMap.length; index++) {
            // 源码里面是用 0 来初始化的
            // 但是有可能 0 是个正常值
            // 我这里先用 -1 来初始化
            newIndexToOldIndexMap[index] = -1;
        }
        // 7. 遍历老节点
        // 7.1. 需要找出老节点有,而新节点没有的 -> 需要把这个节点删除掉
        // 7.2. 新老节点都有的,—> 需要 patch
        for (i = s1; i <= e1; i++) {
            const prevChild = c1[i];
            const newIndex = keyToNewIndexMap.get(prevChild.key);
            newIndexToOldIndexMap[newIndex] = i;

            // 因为有可能 nextIndex 的值为0(0也是正常值)
            // 所以需要通过值是不是 undefined 来判断
            // 不能直接 if(newIndex) 来判断
            if (newIndex === undefined) {
                // 当前节点的key 不存在于 newChildren 中,需要把当前节点给删除掉
                hostRemove(prevChild.el);
            } else {
                // 新老节点都存在
                console.log("新老节点都存在");
                patch(prevChild, c2[newIndex], container);
            }
        }

        // 8 遍历新节点
        // 8.1. 需要找出老节点没有,而新节点有的 -> 需要把这个节点创建
        // 8.2. 最后需要移动一下位置,比如 [c,d,e] -> [e,c,d]
        for (i = e2; i >= s2; i--) {
            const nextChild = c2[i];

            if (newIndexToOldIndexMap[i] === -1) {
                // 说明是个新增的节点
                patch(null, c2[i], container);
            } else {
                // 有可能 i+1 没有元素 没有的话就直接设置为 null
                // 在 hostInsert 函数内如果发现是 null 的话,会直接添加到父级容器内
                const anchor = i + 1 >= e2 + 1 ? null : c2[i + 1];
                hostInsert(nextChild.el, container, anchor && anchor.el);
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

针对5这种情况可以理解为

用最少的步骤,使[1,2,3,4,5,6]转化为[1,3,2,6,4,5]需要求解最长递增子序列

# setup函数

创建组件实例createComponentInstance

function createComponentInstance(vnode, parent) {
  const instance = {
    type: vnode.type,
    vnode,
    next: null, // 需要更新的 vnode,用于更新 component 类型的组件
    props: {},
    parent,
    provides: parent ? parent.provides : {}, //  获取 parent 的 provides 作为当前组件的初始化值 这样就可以继承 parent.provides 的属性了
    proxy: null,
    isMounted: false,
    attrs: {}, // 存放 attrs 的数据
    slots: {}, // 存放插槽的数据
    ctx: {}, // context 对象
    setupState: {}, // 存储 setup 的返回值
    emit: () => {},
  };

  // 在 prod 坏境下的 ctx 只是下面简单的结构
  // 在 dev 环境下会更复杂
  instance.ctx = {
    _: instance,
  };

  // 赋值 emit
  // 这里使用 bind 把 instance 进行绑定
  // 后面用户使用的时候只需要给 event 和参数即可
  instance.emit = emit.bind(null, instance) as any;

  return instance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

设置组件实例setupComponent

function setupComponent(instance) {
  // 1. 处理 props
  // 取出存在 vnode 里面的 props
  const { props, children } = instance.vnode;
  initProps(instance, props);
  // 2. 处理 slots
  initSlots(instance, children);

  // 源码里面有两种类型的 component
  // 一种是基于 options 创建的
  // 还有一种是 function 的
  // 这里处理的是 options 创建的
  // 叫做 stateful 类型
  setupStatefulComponent(instance);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Vue3中为了方便维护,把组件不同状态的数据存储到不同的属性中,比如setupState、ctx、data、props。

在执行组件渲染函数的时候,直接访问渲染上下文instance.ctx的属性做一层proxy后对渲染instance.ctx属性的访问和修改代理到对setupState、ctx、data、props中的数据访问和修改

setupStatefulComponent(instance) {
  // 1. 先创建代理 proxy
  // proxy 对象其实是代理了 instance.ctx 对象
  // 我们在使用的时候需要使用 instance.proxy 对象
  // 因为 instance.ctx 在 prod 和 dev 坏境下是不同的
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
  // 用户声明的对象就是 instance.type
  // const Component = {setup(),render()} ....
  const Component = instance.type;
  
  // 2. 调用 setup
  // 调用 setup 的时候传入 props
  const { setup } = Component;
  if (setup) {
    // 设置当前 currentInstance 的值
    // 必须要在调用 setup 之前
    setCurrentInstance(instance);

    const setupContext = createSetupContext(instance);
    // 真实的处理场景里面应该是只在 dev 环境才会把 props 设置为只读的
    const setupResult =
      setup && setup(shallowReadonly(instance.props), setupContext);

    setCurrentInstance(null);

    // 3. 处理 setupResult
    handleSetupResult(instance, setupResult);
  } else {
    finishComponentSetup(instance);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 创建代理

代理函数PublicInstanceProxyHandlers

 get({ _: instance }, key) {
    // 用户访问 proxy[key]
    // 这里就匹配一下看看是否有对应的 function
    // 有的话就直接调用这个 function
    const { setupState, props } = instance;

    if (key !== "$") {
        // 说明不是访问 public api
        // 先检测访问的 key 是否存在于 setupState 中, 是的话直接返回
        if (hasOwn(setupState, key)) {
            return setupState[key];
        } else if (hasOwn(props, key)) {
            // 看看 key 是不是在 props 中
            // 代理是可以访问到 props 中的 key 的
            return props[key];
        }
    }
 
    const publicGetter = publicPropertiesMap[key];

    if (publicGetter) {
        return publicGetter(instance);
    }
},
set({ _: instance }, key, value) {
    const { setupState } = instance;

    if (setupState !== {} && hasOwn(setupState, key)) {
        // 有的话 那么就直接赋值
        setupState[key] = value;
    }
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

setup中的变量优先级要高于data。存在setup>data>props判断优先级mini-vue中没有体现此外还有个has

porps事单向数据流,修改会收到警告。

# 处理setup

创建setupContext,对应setup的第二个参数

function createSetupContext(instance) {
  return {
    attrs: instance.attrs,
    slots: instance.slots,
    emit: instance.emit,
    expose: () => {},
  };
}
1
2
3
4
5
6
7
8

处理结果handleSetupResult

function handleSetupResult(instance, setupResult) {
  // setup 返回值不一样的话,会有不同的处理
  // 1. 看看 setupResult 是个什么
  if (typeof setupResult === "function") {
    // 如果返回的是 function 的话,那么绑定到 render 上
    // 认为是 render 逻辑
    // setup(){ return (ctx)=>(h("div",ull,ctx.msg)) }
    instance.render = setupResult;
  } else if (typeof setupResult === "object") {
    // 返回的是一个对象的话
    // 先存到 setupState 上
    // 先使用 @vue/reactivity 里面的 proxyRefs
    // 后面我们自己构建
    // proxyRefs 的作用就是把 setupResult 对象做一层代理
    // 方便用户直接访问 ref 类型的值
    // 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value
    // 这里也就是官网里面说到的自动解构 Ref 类型
    instance.setupState = proxyRefs(setupResult);
  }

  finishComponentSetup(instance);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

finishComponentSetup完成组件实例的设置

function finishComponentSetup(instance) {
  // 给 instance 设置 render
  // 先取到用户设置的 component options
  const Component = instance.type;
  if (!instance.render) {
    // 调用 compile 模块来编译 template
    // Component.render = compile(Component.template, {
    //     isCustomElement: instance.appContext.config.isCustomElement || NO
    //   })
    instance.render = Component.render;
  }
  applyOptions()兼容vue2.x的option API
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. compile和组件template存在,render不存在。runtime-compiled会在js运行时进行模板编译,生成render函数。
  2. 组件template存在,compile和render不存在。使用runtime-only版本,会给个警告说想要运行时编译使用runtime-compiled。

组件渲染时就可以运行instance.render函数生成组件的子树vnode了

applyOptions的工作

处理mixin,extend,mixins,inject,methods,data,computed,watch,provide,component,指令,生命周期option

# 响应式

image-20211109165109662

object.defineProperty API缺点。不能监听对象属性新增和删除;defineProperty的性能负担。

data中的数据,vue会自动(黑盒)把他们变成响应式的。

# reactive

简要原理见Vue3响应式

  1. 如果不是数组和对象直接返回
  2. 响应式对象,reactive直接返回。通过target.__v_raw标识判断是否是响应式属性
  3. 原始对象如果已经响应式过,reactive直接返回响应式对象。通过target.__v_reactive标识判断
  4. 代理(get,set,deleteProperty,has,ownKeys)
  5. 打标识

# effect

每次effect stack入栈操作时,将全局的activeEffect指向reactiveEffect,执行被包装的原始函数fn。

组件在render effct执行前,通过cleanup清理依赖。(比如不再显示的数据)

# computed

dirty表示是否需要重新计算,value表示最后的值

class ComputedRefImpl {
  public dep: any;
  public effect: ReactiveEffect;

  private _dirty: boolean;
  private _value

  constructor(getter) {
    this._dirty = true;
    this.dep = createDep();
    this.effect = new ReactiveEffect(getter, () => {
      // scheduler
      // 只要触发了这个函数说明响应式对象的值发生改变了
      // 那么就解锁,后续在调用 get 的时候就会重新执行,所以会得到最新的值
      if (this._dirty) return;

      this._dirty = true;
      triggerRefValue(this);
    });
  }

  get value() {
    // 收集依赖
    trackRefValue(this);
    // 锁上,只可以调用一次
    // 当数据改变的时候才会解锁
    // 这里就是缓存实现的核心
    // 解锁是在 scheduler 里面做的
    if (this._dirty) {
      this._dirty = false;
      // 这里执行 run 的话,就是执行用户传入的 fn
      this._value = this.effect.run();
    }

    return this._value;
  }
}

function computed(getter) {
  return new ComputedRefImpl(getter);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

只要依赖不变化,就可以使用缓存的value,而不用每次在组件渲染的时候执行函数计算,先执行computed的依赖再执行普通effect

Last Updated: 12/6/2021, 3:30:27 PM