《Vue.js 设计与实现》 笔记

目录

书本信息

1. 权衡的艺术

1.1. 命令式与声明式

命令式关注于 过程

const div = document.querySelector('div');
div.innerText = "hello world";
div.onclick = ()=>console.log("ok");

声明式关注于 结果, Vue 封装了命令式的过程

<template>
  <div @click="alert('ok')">hello world</div>
</template>

1.2. 性能和可维护性的权衡

  • 声明式的更新性能损耗 = 找出差异的性能损耗 + 直接修改的性能损耗
  • 声明式代码的性能 不优于 命令式代码的性能

1.3. 虚拟DOM的性能到底如何

1.3.1. 比较纯js运算与dom运算

console.time('pure');
let app = [];
for (let i = 0; i < 10000; i++) {
    const div = { tag: 'div' };
    app.push(div);
}
console.timeEnd('pure'); // pure: 2.31689453125 ms

console.time('dom');
let container = document.createElement('div');
for (let i = 0; i < 10000; i++) {
    container.appendChild(document.createElement('div'));
}
console.timeEnd('dom'); // dom: 8.001220703125 ms

可见dom操作比纯js操作慢几倍

1.3.2. 对比虚拟DOM和innerHTML 创建 的效率

  虚拟DOM innerHTML
纯js运算 创建js对象(VNode) 拼接html字符串
DOM运算 新建所有DOM元素 新建所有DOM元素

可见使用 innerHTML 可以更快的创建

1.3.3. 对比虚拟DOM和innerHTML 更新 效率

  虚拟DOM innerHTML
纯js运算 创建js对象+Diff出更新VNode 拼接html字符串
DOM运算 必要的DOM更新 销毁所有旧的DOM元素, 新建所有新的DOM元素
性能因素 与数据量变化相关 与模板大小相关

可见,更新的节点越多,使用虚拟DOM的性能越佳

1.4. 运行时和编译时

1.4.1. 纯运行时

const Render = (obj, root) => {
    const el = document.createElement(obj.tag);
    if (typeof obj.children === 'string') {
        el.appendChild(document.createTextNode(obj.children));
    } else if (obj.children) {
        obj.children.forEach((childObj) => {
            Render(childObj, el);
        });
    }
    root.appendChild(el);
};

如上,设计一个 Render 函数,在运行时解析对象渲染成DOM

1.4.2. 运行时+编译时

<div>
  <p>hello</p>
  <ol>
    <li>list item</li>
  </ol>
</div>

实现一个 Compile 函数,将上述的html转成obj对象,然后传递给 Render ,这就是 运行时编译 。该方式在代码执行编译步骤时,会产生性能损耗。 如果把编译步骤放到构建阶段,那么就可以省略该性能消耗。

1.4.3. 编译时

如果上述的 Compile 直接把html转成命令式的过程,那么就变成了纯编译时。

其中 Vue3 使用的是 运行时+编译时

2. 框架设计的核心要素

2.1. 提升用户的开发体验

源码中提供的 warn 函数,打印了更丰富的信息

warn.png

图1  丰富的组件栈信息

Vue 提供了 initCustomFormatter ,当在 DevTools 勾选 Setting -> Perference -> Console -> Enable custom formatters

const count = ref(0)
console.log(count)

ref.png

图2  更直观的输出内容

initCustomFormatter 的作用,就是向 window.devtoolsFormatters 数组添加 formatter

2.2. 控制框架代码体积

Vue3 中有大量的 __DEV__

if (__DEV__ && __BROWSER__ && dir.exp) {
  validateBrowserExpression(dir.exp as SimpleExpressionNode, context)
}

当构建生产环境时,其被替换成 false ,其块内的代码变成 dead code, 那么就不会包含到最终的包中,从而减少体积

2.3. 框架要做到良好的 Tree-Shaking

Tree-Shaking 指的就是消除那些永远不会被执行的代码

实现 Tree-Shaking 的几个要点:

  • 模块必须是 ESM (ES Module), 因为其依赖于ESM的静态结构
  • 如果函数调用没有 副作用 ,那么显示指定 /*#__PURE__*/ 也可以将其移除
// b.js
export function foo() {
    console.log('hello');
}

export function bar() {
    console.log(1);
}
export function baz() {
    console.log(2);
}
export const a = /*#__PURE__*/ bar();
export const b = baz();
// a.js
import { foo } from './b';
foo();
// output.js
function foo() {
    console.log('hello');
}
function baz() {
    console.log(2);
}
baz();

foo();

如上,输出结果不会包含 bar 相关的内容

Vue3 中包含大量如下代码

export const isSpecialBooleanAttr = /*#__PURE__*/ makeMap(specialBooleanAttrs)

如果没有引用 isSpecialBooleanAttr 这类变量,那么该变量相关内容就不会包含在最终包中

2.4. 框架应该输出怎样的构建产物

文件名 说明
vue.global.js 可在 <script> 中引用,会注入 Vue 到 window
vue.esm-browser.js 可以 <script type="module"> 引用
vue.esm-bundler.js 在 package.json 的 module 中引用
vue.cjs.js 给node环境使用,用于SSR

vue.esm-bundler.js 构建时其 __DEV__(process.env.NODE_ENV !== 'production') ,这样子包使用者就可以控制是否使用 prod 版本

2.5. 特性开关

通过控制全局变量,显式关闭某些特性, 从而减少体积

// vite.config.js
export default {
  define: {
    __VUE_OPTIONS_API__: JSON.stringify(false),
  },
}

2.6. 错误处理

Vue会通过 callWithErrorHandling 来调用函数,从而统一异常处理。

异步函数抛出的异常不能处理

同时可以注册全局的异常处理函数

import App from 'App.vue'
const app = createApp(App)
app.config.errorHandler = (err) => {
    console.log(err)
}

3. Vue.js 3的设计思路

3.1. 声明式地描述UI

编写前端页面涉及的内容:

  • DOM元素
  • 属性
  • 事件
  • 元素的层级结构

3.1.1. 通过模板

<template>
  <div class="wrap" @click="handleClick" :id="dynamicId">
    hello world
  </div>
</template>

3.1.2. 通过 js object

let div = {
    tag: 'div',
    children: 'hello world',
    onClick: handleClick,
    attr: {
        className: 'wrap',
        id: dynamicId
    }
}

使用 js 对象会被使用模板更灵活,而虚拟DOM其实就是 js 对象

Vue 组件可通过手写 渲染函数 使用虚拟DOM来描述UI

import { h } from 'vue'
export default {
    render(){
        return h('h1', { onClick: handler}) // 虚拟DOM
    }
}

如果 Vue 组件中同时存在 <template>render ,那么会忽略 render

Vue 根据组件的 render 的返回值拿到虚拟DOM, 然后渲染出内容

3.2. 初识渲染器

渲染器的作用,就是将虚拟DOM转化成真实的DOM

渲染器的工作原理就是递归遍历 vnode, 并调用原生DOM API完成真实DOM的创建。在更新阶段,它会通过 Diff 算法找出变更点,并且只更新需要更新的内容

虚拟DOM h('div', 'hello') -> 渲染器 -> 真实DOM

例如,将下面的 vnode 转化成真实的DOM

const vnode = {
    tag: 'div',
    props: {
        id: 'container',
    },
    children: [
        {
            tag: 'button',
            props: {
                onClick: () => alert('hello'),
            },
            children: 'click me',
        },
    ],
};

可实现一个 renderer

const renderer = (vnode, container) => {
    // 创建元素
    const el = document.createElement(vnode.tag);
    // 设置属性和事件监听器
    for (const key in vnode.props) {
        if (key.startsWith('on')) {
            el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key]);
        } else {
            el.setAttribute(key, vnode.props[key]);
        }
    }
    // 处理 children
    if (typeof vnode.children === 'string') {
        el.appendChild(document.createTextNode(vnode.children));
    } else {
        vnode.children.forEach((child) => {
            renderer(child, el);
        });
    }
    // 挂载父节点
    container.appendChild(el);
};

这就是一个简单的渲染器,其主要工作如下:

  • 创建元素
  • 为该元素添加属性和事件
  • 设置子节点
  • 挂载父节点

3.3. 组件的本质

组件就是一组DOM元素的封装

3.3.1. 使用 function 表示组件

假设组件的表现形式为返回 vnode 的函数

const Component = function () {
    return {
        tag: 'div',
        children: 'hello',
    };
};

可见,该函数,实质就是一个 渲染函数.

改造一下 renderer

const renderer = (vnode, container) => {
    if (typeof vnode.tag === 'string') {
        mountElement(vnode, container);
    } else if (typeof vnode.tag === 'function') {
        mountComponent(vnode, container);
    }
};

其中 mountElement 就是上述的 renderer

const mountElement = (vnode, container) => {
    const el = document.createElement(vnode.tag);
    for (const key in vnode.props) {
        if (key.startsWith('on')) {
            el.addEventListener(key.slice(2).toLowerCase(), vnode.props[key]);
        } else {
            el.setAttribute(key, vnode.props[key]);
        }
    }
    if (typeof vnode.children === 'string') {
        el.appendChild(document.createTextNode(vnode.children));
    } else {
        vnode.children.forEach((child) => {
            renderer(child, el);
        });
    }
    container.appendChild(el);
};

mountComponent 可如下实现

const mountComponent = (vnode, container) => {
    renderer(vnode.tag(), container);
};

3.3.2. 使用 object 表示组件

组件为包含渲染函数 render 的对象

const ComponentB = {
    render() {
        return {
            tag: 'div',
            children: 'hello',
        };
    },
};

那么 mountComponent 改成

const mountComponent = (vnode, container) => {
    renderer(vnode.tag.render(), container);
};

3.3.3. 总结

不管是 object 还是 function,mount 的时候,都是调用函数,得到一个新的 vnode 对象实例,这样子组件间就不会相互影响

3.4. 模板的工作原理

编译器的作用,就是把模板编译成渲染函数

如下组件

<template>
  <div @click="handler">hello</div>
</template>

<script>
export default {
  data() {
    return {};
  },
  methods: {
    handler() {},
  },
};
</script>

编译器会编译 <template> 中的内容为渲染函数(render),并添加到 script 的组件对象上

export default {
  data() {
    return {};
  },
  methods: {
    handler() {},
  },
  render() {
    return h("div", { onClick: this.handler }, "hello");
  },
};

3.5. Vue 是各个模块组件的有机整体

编译器和渲染器之间是可以通过虚拟DOM进行信息交流的,例如

<div id="id1" :class="cls"></div>

上述模板,可以看出,id不可变,而 class 可变,因此编译后的渲染函数可以为

render() {
    return {
        tag: 'div',
        props: {
            id: 'id1',
            class: cls
        },
        patchFlag: 1 // 假设1表示 class 是动态的
    }
}

由此,渲染器当看到 patchFlags 时,就知道只有class属性会变更,从而减少了寻找变更点的时间,以此提升了性能

4. 响应系统的作用和实现

4.1. 响应式数据与副作用函数

let obj = {
    text: 'hello'
}
function effect(){
    document.body.innerText = obj.text;
}
  • 当函数的执行,会导致外部状态的改变,那么该函数就是 副作用函数
  • obj.text 被修改时,函数 effect 会自动重新执行,那么 obj 就是响应式数据

4.2. 响应式数据的基本实现

  • effect 执行时,会触发 obj , 对 obj.text 赋值时,会触发
  • 如果可以拦截其读写, 当触发 时, 将该 effect 收集到一个桶中, 触发 时, 从桶中取处该 effect 并执行。 那么 obj 就可以成为响应式数据。

    const bucket = new Set();
    
    let activeEffect;
    
    function reactive(obj) {
        let reactiveObj = new Proxy(obj, {
            get() {
                bucket.add(activeEffect)
                return Reflect.get(...arguments)
            },
            set(target, prop, value) {
                target[prop] = value;
                bucket.forEach(activeEffectFn => {
                    activeEffectFn()
                })
                return true
            }
        })
        return reactiveObj
    }
    
    
    const obj = reactive({ text: 'hello' })
    function effect() {
        activeEffect = effect;
        console.log('obj.text is ', obj.text)
    }
    effect()
    obj.text += ' world'
    obj.noExist = 1
    
    obj.text is  hello
    obj.text is  hello world
    obj.text is  hello world
    

4.3. 设计一个完善的响应系统

上述例子有几个缺陷:

  • effect 函数名硬编码了
  • 响应式对象与副作用函数之间没有建立一一对应关系
  • 当给未存在的属性赋值时,依然执行了副作用函数

对此的解决方案:

  • 用一个注册函数,接受一个匿名的副作用函数,解决硬编码问题
  • 使用 WeakmMap 收集副作用函数
  • 使用 Map 将属性与副作用函数对应
const bucket = new WeakMap()
let activeEffect;
function effect(fn) {
    activeEffect = fn
    // 为了触发依赖收集
    fn()
    activeEffect = null
}

function track(target, key) {
    if (!activeEffect) {
        return
    }
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)
}

function trigger(target, key) {
    let depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    let deps = depsMap.get(key)
    if (!deps) {
        return
    }
    deps.forEach(effectFn => {
        effectFn()
    })
}

function reactive(obj) {
    const reactiveObj = new Proxy(obj, {
        get(target, prop) {
            track(target, prop)
            return Reflect.get(...arguments)
        },
        set(target, prop, val) {
            target[prop] = val;
            trigger(target, prop)
            return true
        },
    })
    return reactiveObj
}

let obj1 = reactive({ a: 1 })
effect(() => {
    console.log('obj1.a is', obj1.a)
})
let obj2 = reactive({ b: 10 })
effect(() => {
    console.log('obj2.b is', obj2.b)
})

obj1.a = 2;
obj2.b = 4
obj2.c = 3
obj1.a is 1
obj2.b is 10
obj1.a is 2
obj2.b is 4

4.4. 分支切换与 cleanup

考虑下面场景

const obj = reactive({ ok: true, text: 'hello' })
effect(() => {
    document.innerHTML = obj.ok ? obj.text : 'empty'
})
obj.ok = false;
obj.text = "world"
  • obj.oktrue 时, obj 中的 oktext 都收集到依赖中
  • 如果这时, obj.ok 被赋值为 false , 对应到副作用函数重新执行时,其不会收集 text 到依赖中。 但是由于之前收集的 text 依赖, 依然残留在 bucket 中。 故给 text 赋值时, 依然会执行副作用函数
  • 问题是, okfalse 时, text 的值的变更,不会有任何副作用, 理想情况下, 应该不执行副作用函数

解决方案:

  • 每次执行副作用函数前,先将该副作用函数从上次的 依赖集合 中移除
const bucket = new WeakMap()
let activeEffect;
function effect(fn) {
    // 新增
    const effectFn = () => {
        // 执行副作用函数时, 将其设置为激活状态
        activeEffect = effectFn
        fn()
    }
    effectFn.deps = []
    // 为了触发依赖收集
    effectFn()
    activeEffect = null
}

function track(target, key) {
    if (!activeEffect) {
        return
    }
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)
    // 新增
    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    let depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    let deps = depsMap.get(key)
    if (!deps) {
        return
    }
    // 新增
    let depsToRun = new Set(deps)
    depsToRun.forEach(effectFn => {
        cleanup(effectFn)
        effectFn()
    })
}

// 新增
function cleanup(effectFn) {
    let deps = effectFn.deps.pop();
    while (deps) {
        deps.delete(effectFn)
        deps = effectFn.deps.pop();
    }
}

function reactive(obj) {
    const reactiveObj = new Proxy(obj, {
        get(target, prop) {
            track(target, prop)
            return Reflect.get(...arguments)
        },
        set(target, prop, val) {
            target[prop] = val;
            trigger(target, prop)
            return true
        },
    })
    return reactiveObj
}

let obj1 = reactive({ ok: true, text: 'hello' })
effect(() => {
    console.log('obj1 is', obj1.ok ? obj1.text : 'empty')
})
obj1.ok = false;
obj1.text = 'world'

obj1 is hello
obj1 is empty

4.5. 嵌套的 effect 与 effect 栈

在嵌套 effect 的情况, 当里层的 effect 执行完成, activeEffect 丢失外层的 effect 的引用, 从而导致不能 track 副作用函数到依赖集合中(比如组件嵌套渲染)。 如下:

effect(() => {
    effect(() => {
        console.log(reactiveObj.a)
    })
    console.log(reactiveObj.b)
})

解决方案: activeEffect 的数据结构改成

const bucket = new WeakMap()
let effectStack = [];
let activeEffect;
function effect(fn) {
    const effectFn = () => {
        // 新增
        // 执行副作用函数时, 将其设置为激活状态
        effectStack.push(activeEffect = effectFn)
        cleanup(effectFn)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    // 为了触发依赖收集
    effectFn()
}

function track(target, key) {
    if (!activeEffect) {
        return
    }
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)

    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    let depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    let deps = depsMap.get(key)
    if (!deps) {
        return
    }

    let depsToRun = new Set(deps)
    depsToRun.forEach(effectFn => {
        effectFn()
    })
}

function cleanup(effectFn) {
    let deps = effectFn.deps.pop();
    while (deps) {
        deps.delete(effectFn)
        deps = effectFn.deps.pop();
    }
}

function reactive(obj) {
    const reactiveObj = new Proxy(obj, {
        get(target, prop) {
            track(target, prop)
            return Reflect.get(...arguments)
        },
        set(target, prop, val) {
            target[prop] = val;
            trigger(target, prop)
            return true
        },
    })
    return reactiveObj
}

let obj1 = reactive({ ok: true, text: 'hello', num: 2 })
effect(() => {
    effect(() => {
        console.log('num is', obj1.num)
    })
    console.log('obj1 is', obj1.ok ? obj1.text : 'empty')
})
console.log('----')
obj1.ok = false;
obj1.text = 'world'
obj1.num = 10
num is 2
obj1 is hello
----
num is 2
obj1 is empty
num is 10
num is 10

4.5.1. TODO 副作用函数重复注册问题

如上述结果, 该方案有个问题, 嵌套的副作用函数,会被重复收集,从而执行多次(因为每次执行外层 effect , 传给里层 effect 的副作用函数都是新的, 因此在依赖集合里不能被去重),故 num is 10 被打印了两次

4.6. 避免无限递归循环

上述方案有个缺陷,当在副作用函数中同时存在读和写时,会导致无限递归循环

解决方案: 在 trigger 中加个判断, 如果将要执行读 effectFnactiveEffect 一致,那么就跳过执行

const bucket = new WeakMap()
let effectStack = [];
let activeEffect;
function effect(fn) {
    const effectFn = () => {
        // 执行副作用函数时, 将其设置为激活状态
        effectStack.push(activeEffect = effectFn)
        cleanup(effectFn)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    // 为了触发依赖收集
    effectFn()
}

function track(target, key) {
    if (!activeEffect) {
        return
    }
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)

    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    let depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    let deps = depsMap.get(key)
    if (!deps) {
        return
    }

    let depsToRun = new Set(deps)
    depsToRun.forEach(effectFn => {
        activeEffect !== effectFn && effectFn()
    })
}

function cleanup(effectFn) {
    let deps = effectFn.deps.pop();
    while (deps) {
        deps.delete(effectFn)
        deps = effectFn.deps.pop();
    }
}

function reactive(obj) {
    const reactiveObj = new Proxy(obj, {
        get(target, prop) {
            track(target, prop)
            return Reflect.get(...arguments)
        },
        set(target, prop, val) {
            target[prop] = val;
            trigger(target, prop)
            return true
        },
    })
    return reactiveObj
}

let obj1 = reactive({ ok: true, text: 'hello', num: 2 })
effect(() => {
    console.log('obj1 is', obj1.ok ? obj1.text : 'empty')
    console.log(obj1.num++)
})
console.log('----')
obj1.ok = false;
obj1.text = 'world'
obj1.num = 44
obj1 is hello
2
----
obj1 is empty
3
obj1 is empty
44

4.7. 调度执行

可调度 指当 trigger 触发副作用函数执行时, 外部环境有能力决定副作用函数执行当时机、次数以及方式

考虑下面场景

const obj = reactive({ foo: 1 })
effect(() => {
    console.log(obj.foo)
})
obj.foo++
console.log('end')

假设想要让 obj.foo++ 导致当副作用函数当执行, 放在下一个 tick 中执行, 也就是说,结果为:

1
end
2

该如何?

此时可以给 effect 传递一个 options.scheduler ,将副作用函数的执行权反转到外部。

const bucket = new WeakMap()
let effectStack = [];
let activeEffect;
function effect(fn, options) {
    const effectFn = () => {
        // 执行副作用函数时, 将其设置为激活状态
        effectStack.push(activeEffect = effectFn)
        cleanup(effectFn)
        fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = []
    effectFn.options = options // 新增
    // 为了触发依赖收集
    effectFn()
}

function track(target, key) {
    if (!activeEffect) {
        return
    }
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)

    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    let depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    let deps = depsMap.get(key)
    if (!deps) {
        return
    }

    let depsToRun = new Set(deps)
    depsToRun.forEach(effectFn => {
        if (activeEffect !== effectFn) {
            // 新增
            effectFn?.options?.scheduler ?
                effectFn?.options?.scheduler(effectFn) : effectFn()
        }
    })
}

function cleanup(effectFn) {
    let deps = effectFn.deps.pop();
    while (deps) {
        deps.delete(effectFn)
        deps = effectFn.deps.pop();
    }
}

function reactive(obj) {
    const reactiveObj = new Proxy(obj, {
        get(target, prop) {
            track(target, prop)
            return Reflect.get(...arguments)
        },
        set(target, prop, val) {
            target[prop] = val;
            trigger(target, prop)
            return true
        },
    })
    return reactiveObj
}


let obj1 = reactive({ foo: 1 })
effect(() => {
    console.log(obj1.foo)
}, {
    scheduler(fn) {
        Promise.resolve().then(fn)
    }
})
console.log('----')
obj1.foo++
console.log('end')

console.log('---排除过渡状态')
const jobQueue = new Set()
let isFlushing = false;
function flushJob() {
    if (isFlushing) return
    isFlushing = true;
    Promise.resolve().then(() => {
        jobQueue.forEach(fn => fn())
        isFlushing = false
    })
}
let obj2 = reactive({ foo: 2 })
effect(() => {
    console.log('obj2', obj2.foo)
}, {
    scheduler(fn) {
        jobQueue.add(fn)
        flushJob()
    }
})
console.log('----')
obj2.foo++
obj2.foo++

Promise.resolve().then(() => obj2.foo++)
Promise.resolve().then(() => obj2.foo++)
1
----
end
---排除过渡状态
obj2 2
----
2
obj2 4
obj2 6

4.8. 计算属性 computed 与 lazy

假如想要注册副作用函数时,不先执行,而是把执行权给外部,可如下操作:

function effect(fn, options) {
    const effectFn = () => {
        cleanup(effectFn)
        effectFnStack.push(activeEffectFn = fn)
        let result = fn(); // 新增
        effectFnStack.pop()
        activeEffectFn = effectFnStack[effectFnStack.length - 1]
        return result // 新增
    }
    effectFn.options = options
    effectFn.deps = []
    if (!options.lazy) { // 新增
        effectFn()
    }
    return effectFn
}

此时, 如果把 fn 看成一个 getter ,那么就可以利用此实现一个 computed , 如下:

function computed(getter) {
    const effectFn = effect(getter, {
        lazy: true
    })
    const obj = {
        get value() {
            // 仅在获取值时,才调用副作用函数
            return effectFn()
        }
    }
    return obj
}

同时,可以加入缓存功能:

function computed(getter) {
    let dirty = true;
    const effectFn = effect(getter, {
        lazy: true,
        scheduler(fn) {
            // 此处不需要执行 fn , 因为执行权放到了下面到 get value中

            // getter 中的响应式数据变化时, 就说明值的结果已过期
            dirty = true;

        }
    })
    let cached;
    const obj = {
        get value() {
            if (dirty) {
                cached = effectFn()
                dirty = false
            }
            return cached
        }
    }
    return obj
}

解决在 effect 中的 computed 未响应的问题,如下:

const sumRes = computed(() => obj.a + obj.b)
effect(() => {
    // 之前的响应式值,是通过 reactive 创建的, 其 getter 中会执行 track, setter 中执行 trigger
    // 但是该对象由 computed 创建, sumRes.value 的 getter 和 setter 并没有调用 track 和 trigger
    // 故 sumRes 与 value 其对应到副作用作用函数,并没有收集到依赖中
    console.log(sumRes.value)
})
obj.a++ // 不会执行上面到副作用函数

解决方案, 在 computed 中调用 tracktrigger

function computed(getter) {
    let dirty = true;
    let cached;
    const effectFn = effect(getter, {
        lazy: true,
        scheduler() {
            dirty = true
            // 执行了调度函数, 说明触发了依赖的响应式数据的 setter, 那么obj.value 也应该重新 set
            // 故可以把该处当成是 obj.value 的 setter
            trigger(obj, 'value')
        }
    })
    let obj = {
        get value() {
            if (dirty) {
                cached = effectFn()
                dirty = false
            }
            track(obj, 'value')
            return cached
        }
    }
    return effectFn
}

完整到演示效果:

const bucket = new WeakMap()
let effectStack = [];
let activeEffect;
function effect(fn, options) {
    const effectFn = () => {
        // 执行副作用函数时, 将其设置为激活状态
        effectStack.push(activeEffect = effectFn)
        cleanup(effectFn)
        let res = fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res
    }
    effectFn.deps = []
    effectFn.options = options // 新增
    if (!options?.lazy) {
        // 为了触发依赖收集
        effectFn()
    }
    return effectFn
}

function track(target, key) {
    if (!activeEffect) {
        return
    }
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)

    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    let depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    let deps = depsMap.get(key)
    if (!deps) {
        return
    }

    let depsToRun = new Set(deps)
    depsToRun.forEach(effectFn => {
        if (activeEffect !== effectFn) {
            // 新增
            effectFn?.options?.scheduler ?
                effectFn?.options?.scheduler(effectFn) : effectFn()
        }
    })
}

function cleanup(effectFn) {
    let deps = effectFn.deps.pop();
    while (deps) {
        deps.delete(effectFn)
        deps = effectFn.deps.pop();
    }
}

function reactive(obj) {
    const reactiveObj = new Proxy(obj, {
        get(target, prop) {
            track(target, prop)
            return Reflect.get(...arguments)
        },
        set(target, prop, val) {
            target[prop] = val;
            trigger(target, prop)
            return true
        },
    })
    return reactiveObj
}

function computed(getter) {
    let cached;
    let dirty = true;
    let effectFn = effect(getter, {
        lazy: true,
        scheduler() {
            dirty = true
            trigger(obj, 'value')
        }
    })
    let obj = {
        get value() {
            if (dirty) {
                cached = effectFn()
                dirty = false
            }
            track(obj, 'value')
            return cached
        }
    }
    return obj
}

let obj = reactive({ a: 1, b: 2 })
let sumRes = computed(() => obj.a + obj.b)
console.log('sum is', sumRes.value)
effect(() => {
    console.log('sum', sumRes.value)
})
console.log('---')
obj.a++
console.log('new sum is', sumRes.value)

sum is 3
sum 3
---
sum 4
new sum is 4

4.9. watch 的实现原理

watch 的使用如下:

watch(obj, () => {
    console.log('obj change')
})
obj.foo++

其本质是, 响应式数据变更时, 执行回调函数。 effect 中注册的副作用函数,会在每次 set 时触发执行, 故可以利用此来执行回调。

function watch(source, cb) {
    let oldVal;
    let getter;
    if (typeof source === 'function') {
        getter = source
    } else {
        // 遍历触发其属性到读取
        getter = () => traverse(source)
    }

    let effectFn = effect(() => getter(), {
        lazy: true,
        scheduler(fn) {
            let newVal = fn()
            cb(newVal, oldVal)
            oldVal = newVal
        }
    })
    oldVal = effectFn()

}
function traverse(obj, seen = new Set()) {
    // 去重, 防止嵌套自引用导致到无限循环
    if (seen.has(obj)) return
    if (typeof obj !== 'object' && obj !== null) {
        return
    }
    seen.add(obj)
    for (let k in obj) {
        traverse(obj[k], seen)
    }
    return obj
}
const bucket = new WeakMap()
let effectStack = [];
let activeEffect;
function effect(fn, options) {
    const effectFn = () => {
        // 执行副作用函数时, 将其设置为激活状态
        effectStack.push(activeEffect = effectFn)
        cleanup(effectFn)
        let res = fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res
    }
    effectFn.deps = []
    effectFn.options = options // 新增
    if (!options?.lazy) {
        // 为了触发依赖收集
        effectFn()
    }
    return effectFn
}

function track(target, key) {
    if (!activeEffect) {
        return
    }
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)

    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    let depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    let deps = depsMap.get(key)
    if (!deps) {
        return
    }

    let depsToRun = new Set(deps)
    depsToRun.forEach(effectFn => {
        if (activeEffect !== effectFn) {
            // 新增
            effectFn?.options?.scheduler ?
                effectFn?.options?.scheduler(effectFn) : effectFn()
        }
    })
}

function cleanup(effectFn) {
    let deps = effectFn.deps.pop();
    while (deps) {
        deps.delete(effectFn)
        deps = effectFn.deps.pop();
    }
}

function reactive(obj) {
    const reactiveObj = new Proxy(obj, {
        get(target, prop) {
            track(target, prop)
            return Reflect.get(...arguments)
        },
        set(target, prop, val) {
            target[prop] = val;
            trigger(target, prop)
            return true
        },
    })
    return reactiveObj
}

function computed(getter) {
    let cached;
    let dirty = true;
    let effectFn = effect(getter, {
        lazy: true,
        scheduler() {
            dirty = true
            trigger(obj, 'value')
        }
    })
    let obj = {
        get value() {
            if (dirty) {
                cached = effectFn()
                dirty = false
            }
            track(obj, 'value')
            return cached
        }
    }
    return obj
}
function watch(source, cb) {
    let oldVal;
    let getter;
    if (typeof source === 'function') {
        getter = source
    } else {
        // 遍历触发其属性到读取
        getter = () => traverse(source)
    }

    let effectFn = effect(() => getter(), {
        lazy: true,
        scheduler(fn) {
            let newVal = fn()
            cb(newVal, oldVal)
            oldVal = newVal
        }
    })
    oldVal = effectFn()

}
function traverse(obj, seen = new Set()) {
    // 去重, 防止嵌套自引用导致到无限循环
    if (seen.has(obj)) return
    if (typeof obj !== 'object' && obj !== null) {
        return
    }
    seen.add(obj)
    for (let k in obj) {
        traverse(obj[k], seen)
    }
    return obj
}

let obj = reactive({ a: 1, b: 2 })
watch(() => obj.a, (v) => console.log('obj.a is', v))
obj.a++
obj.a++
watch(obj, (newV, oldV) => console.log('newV', newV))
obj.b++
obj.b++
obj.a is 2
obj.a is 3
newV { a: 3, b: 3 }
newV { a: 3, b: 4 }

4.10. 立即执行的 watch 与回调执行时机

上述的方案有两个问题:

  • 回调函数不能马上执行一次(实现 immediate )
  • 不能控制执行时机(实现 flush )(DOM 更新前(sync)执行还是更新后(post)执行)

可以如下实现:

function watch(source, cb, options = {}) {
    let getter;
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }
    let oldV;
    let job = () => {
        let newV = effectFn();
        if (options.flush === 'pre') {
            cb(newV, oldV)
        } else if (options.flush === 'post') {
            Promise.resolve().then(() => {
                cb(newV, oldV)
            })
        }
        oldV = newV
    }
    let effectFn = effect(() => getter(), {
        lazy: true,
        scheduler() {
            job()
        }
    })
    if (options.immediate) {
        job()
    }
    else {
        oldV = effectFn()
    }
}

完整示例如下:

const bucket = new WeakMap()
let effectStack = [];
let activeEffect;
function effect(fn, options) {
    const effectFn = () => {
        // 执行副作用函数时, 将其设置为激活状态
        effectStack.push(activeEffect = effectFn)
        cleanup(effectFn)
        let res = fn()
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
        return res
    }
    effectFn.deps = []
    effectFn.options = options // 新增
    if (!options?.lazy) {
        // 为了触发依赖收集
        effectFn()
    }
    return effectFn
}

function track(target, key) {
    if (!activeEffect) {
        return
    }
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key)
    if (!deps) {
        depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)

    activeEffect.deps.push(deps)
}

function trigger(target, key) {
    let depsMap = bucket.get(target)
    if (!depsMap) {
        return
    }
    let deps = depsMap.get(key)
    if (!deps) {
        return
    }

    let depsToRun = new Set(deps)
    depsToRun.forEach(effectFn => {
        if (activeEffect !== effectFn) {
            // 新增
            effectFn?.options?.scheduler ?
                effectFn?.options?.scheduler(effectFn) : effectFn()
        }
    })
}

function cleanup(effectFn) {
    let deps = effectFn.deps.pop();
    while (deps) {
        deps.delete(effectFn)
        deps = effectFn.deps.pop();
    }
}

function reactive(obj) {
    const reactiveObj = new Proxy(obj, {
        get(target, prop) {
            track(target, prop)
            return Reflect.get(...arguments)
        },
        set(target, prop, val) {
            target[prop] = val;
            trigger(target, prop)
            return true
        },
    })
    return reactiveObj
}

function computed(getter) {
    let cached;
    let dirty = true;
    let effectFn = effect(getter, {
        lazy: true,
        scheduler() {
            dirty = true
            trigger(obj, 'value')
        }
    })
    let obj = {
        get value() {
            if (dirty) {
                cached = effectFn()
                dirty = false
            }
            track(obj, 'value')
            return cached
        }
    }
    return obj
}

function traverse(obj, seen = new Set()) {
    // 去重, 防止嵌套自引用导致到无限循环
    if (seen.has(obj)) return
    if (typeof obj !== 'object' && obj !== null) {
        return
    }
    seen.add(obj)
    for (let k in obj) {
        traverse(obj[k], seen)
    }
    return obj
}

function watch(source, cb, options = {}) {
    let getter;
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }
    let oldV;
    let job = () => {
        let newV = effectFn();
        if (options.flush === 'pre') {
            cb(newV, oldV)
        } else if (options.flush === 'post') {
            Promise.resolve().then(() => {
                cb(newV, oldV)
            })
        }
        oldV = newV
    }
    let effectFn = effect(() => getter(), {
        lazy: true,
        scheduler() {
            job()
        }
    })
    if (options.immediate) {
        job()
    }
    else {
        oldV = effectFn()
    }
}

let obj = reactive({ a: 1, b: 2 })
watch(() => obj.a, (v) => console.log('obj.a is', v), { immediate: true, flush: 'post' })
obj.a++
console.log('end')

end
obj.a is 1
obj.a is 2

4.11. 过期的副作用

watch 的回调是一个异步函数时, 可能会存在 竞态问题 。如下:

watch(obj, async () => {
    let resp = await fetch(url, { data: obj })
    finalData = resp
})

假设 obj 变更了两次, 期望 finalData 是第二次变更的结果。 但是由于不能保证两次请求会按照请求次序返回,故有可能导致 finalData 是一个过期的值。

解决方案: 可传入一个注册函数给回调函数, 让其内部可以监听 过期的情景

watch(obj, async (newV, oldV, onCleanup) => {
    let expired = false;
    onCleanup(() => {
        expired = true
    })
    const resp = await fetch(url, { data: obj })
    if (!expired) {
        finalData = resp;
    }
})

对应的 watch 的实现逻辑:

function watch(source, cb, options = {}) {
    let getter;
    if (typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }
    let oldV;
    let clean;
    let onCleanup = (fn) => {
        clean = fn
    }
    let job = () => {
        // 每次变更前, 清理一下
        clean && clean()
        let newV = effectFn();
        cb(newV, oldV, onCleanup)
        oldV = newV
    }
    let effectFn = effect(() => getter(), {
        lazy: true,
        scheduler() {
            if (options.flush === 'pre') {
                job()
            } else if (options.flush === 'post') {
                Promise.resolve().then(job)
            }
        }
    })
    if (options.immediate) {
        job()
    }
    else {
        oldV = effectFn()
    }
}

4.12. 总结

实现一个响应式系统,需要处理下面几个问题:

  • effect 注册副作用函数
  • Proxygetter 中实现 track 将对应的 targetkeyeffectFn 收集到依赖集合中 (数据结构依次是, WeakMap Map Set
  • 实现 cleanup 清理掉因分支切换导致的无效的副作用函数(通过 effectFn.deps 反向收集对该副作用函数的依赖集合, 从而进行清理)
  • 处理在 effect 同时执行 get 和 set 操作,导致无限递归问题(同时执行时, activeEffect 会与 trigger 中的需要执行的 effectFn 相同)
  • 处理 effect 嵌套导致 activeEffect 丢失外层引用 effectFn 的问题 (使用 effectStack 来收集 activeEffect )
  • effect 实现一个 scheduler ,把 effectFn 的执行权交给外部

实现 computed

  • effect 实现 lazy ,可选地延时调用副作用函数, 同时把副作用函数的控制权返回给调用方
  • 可缓存计算结果 (内部缓存一个值, 同时储存一个flag dirty , 当触发依赖值更新时, 设置 dirtytrue 从而再次计算)
  • 可在 effect 中收集该计算对象 (显示地调用 tracktrigger

实现 watch :

  • effect 的副作用函数中遍历监听对象的属性,即可将该对象收集到依赖集合中(依赖的响应式数据,每次 set 都会导致副作用函数的执行, 从而导致 scheduler 的执行)
  • scheduler 中来执行回调
  • 实现 immediate (立刻执行)和 flush (DOM更新前还是更新后)
  • 处理过期的副作用, 传入 onCleanup 给副作用函数, 执行下次 cb 前, 都执行一下 cleanup

5. 非原始值的响应式方案

5.1. 理解 Proxy 和 Reflect

  • Proxy 仅拦截对对象的 基本操作 , 例如 get set apply
  • 对于 复合操作 , 例如 obj.foo() , Proxy 不会递归拦截

考虑下面场景

const obj = {
    foo: 1,
    get bar() {
        return this.foo
    }
}
const reactiveObj = new Proxy(obj, {
    get(target, key) {
        track(target, key)
        return target[key]
    },
    set(target, key) {
        trigger(target, key)
        return true
    }
})
effect(() => {
    console.log(reactiveObj.bar)
})
reactiveObj.foo++
  • 期望的结果是, foo 自增时,会执行副作用函数
  • 实际结果并没有, 因为 gettarget[key]target 指代的是 obj , 其并没有代理拦截,所以导致 foo 并没有收集到依赖中

解决方案:

const reactiveObj = new Proxy(obj, {
    get(target, key, receiver) {
        track(target, key)
        return Reflect.get(target, key, receiver)
    },
})
  • receiver 指代的是 reactiveObj , 而不是原始的 obj
  • Reflect.get 中设置 receiver 可以显示制定 target[key]this 指向

5.2. JS 对象和 Proxy 的工作原理

在 ECMA2022 中,对于一个对象,其定义了下列内部方法 ecma2022 :

内部方法 签名
[​[GetPrototypeof]] () => Object | Null
[​[SetPrototypeof]] (Object | null) => Boolean
[​[IsExtensible]] () => Boolean
[​[PreventExtensions]] () => Boolean
[​[GetOwnProperty]] (propKey) => Undefined | Property Descriptor
[​[DefineOwnProperty]] (propKey, descriptor) => Boolean
[​[HasProperty]] (propKey) => Boolean
[​[Get]] (propKey, receiver) => any
[​[Set]] (propKey, value, receiver) => Boolean
[​[Delete]] (propKey) => Boolean
[​[OwnPropertyKeys]] ()=> list of propKey

对于函数对象,有额外两个

内部方法 签名
[​[Call]] (any, list of any) => any
[​[Construct]] (list of any) => Object
  • 常规对象 (Ordinary Object): 上述的内置方法由 emca-10.1ecma-10.2 中定义
  • 异质对象 (Exotic Object): 常规对象以外的对象, 比如 Proxy

代理对象有与常规对象相同的内置方法, 但是其定义遵守 ecma-10.5

内置方法 handler
[​[GetPrototypeOf]] getPrototypeOf
[​[SetPrototypeOf]] setPrototypeOf
[​[IsExtensible]] isExtensible
[​[PreventExtensions]] preventExtensions
[​[GetOwnProperty]] getOwnPropertyDescriptor
[​[DefineOwnProperty]] defineProperty
[​[HasProperty]] has
[​[Get]] get
[​[Set]] set
[​[Delete]] deleteProperty
[​[OwnPropertyKeys]] ownKeys
[​[Call]] apply
[​[Construct]] construct

创建一个代理对象时, 其 handler 定义了代理对象本身的内置方法的处理行为, 对于被代理对象的内置方法, 并没有影响

5.3. 如何代理 Object

对对象的代理操作,涉及到下面几种:

  • 属性访问 [​[Get]]
  • in 操作符
  • for...in 遍历
  • delete 属性

5.3.1. in

根据规范 ecma-13.10 , in 涉及到 [​[HasProperty]] 内置方法,故可以如下拦截

new Proxy(obj, {
    has(target, key) {
        track(target, key)
        return Reflect.has(...arguments)
    }
})

5.3.2. for...in

例如要响应下面示例:

effect(() => {
    for (key in reactiveObj) {
        console.log(key)
    }
})

根据规范 14.7.5.614.7.5.9 , for...in 涉及到 [​[OwnPropertyKeys]] 内置方法,故可以如下写:

const ITERATE_KEY = Symbol()

new Proxy(obj, {
    ownKeys(target) {
        track(target, ITERATE_KEY)
        return Reflect.ownKeys(target)
    }
})

其中,在添加属性时, 需要运行副作用函数, 故需要改写触发时机:

new Proxy(obj, {
    set(target, key, value) {
        let res = Reflect.set(...arguments)
        const triggerType = target.hasOwnProperty(key) ? 'ADD' : 'SET'
        trigger(target, key, triggerType)
        return res
    }
})

function trigger(target, key, triggerType) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key) || new Set()
    const effectsToRun = new Set([...effects, ...(triggerType === 'ADD' ? depsMap.get(ITERATE_KEY) || [] : [])])
    effectsToRun.forEach(effectFn => {
        effectFn.options.scheduler ? effectFn.options.scheduler(effectFn) : effectFn()
    })

}

5.3.3. delete

当删除属性时, 会同时影响到 infor...in 的结果, 故也要触发 ITERATE_KEY 的副作用函数。 根据规范 ecma-13.5.1 其涉及到 [​[Delete]] 内置方法, 故如下实现:

new Proxy(obj, {
    deleteProperty(target, key) {
        const res = Reflect.deleteProperty(...arguments)
        if (res && Reflect.hasOwnProperty(target, key)) {
            trigger(target, key, 'DELETE')
        }
        return res
    }
})

function trigger(target, key, triggerType) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key) || new Set()
    const effectsToRun = new Set([...effects, ...(
        triggerType === 'ADD' || triggerType === 'DELETE' ? depsMap.get(ITERATE_KEY) || [] : []
    )])
    effectsToRun.forEach(effectFn => {
        effectFn.options.scheduler ? effectFn.options.scheduler(effectFn) : effectFn()
    })
}

5.4. 合理地触发响应

触发响应,需要处理下面几个问题:

  • 如果新值和旧值相等,那么应该不执行副作用函数
  • 假设 parentchild 的原型, 且两者都是响应式对象, 当从原型链上设置值时,应该只触发一次副作用函数

对于问题1, 可如下解决:

function reactive(obj) {
    return new Proxy(obj, {
        set(target, key, value, receiver) {
            const oldVal = target[key]
            const res = Reflect.set(target, key, value, receiver)
            const exists = target.hasOwnProperty(key)
            if (oldVal !== val &&
                !(Number.isNaN(value) && Number.isNaN(oldVal))) {
                trigger(target, key, exists ? 'SET' : 'ADD')
            }
            return res
        }
    })
}

对于问题2, 考虑以下场景:

const obj = {}
const proto = { foo: 1 }
const child = reactive(obj)
const parent = reactive(proto)
Object.setPrototypeOf(child, parent)

effect(() => {
    console.log(child.foo)
})
child.foo++
  • 因为 child.foo 是从 parent 上读取的, 所以会调用 childparent 的内置方法 [​[GET]] , 因此副作用函数会分别收集到 child.fooparent.foo 的依赖中
  • 当触发 child.foo 时,根据 ecma-10.1.92 , 因为 child 没有 foo 这个属性,所以也会调用原型上的 [​[Set]] (注意原型链上对应的属性的值并不会被设置),因此导致该副作用函数会执行两遍
  • 两次拦截的 set 中的 receiver 都是 child ,利用此点,可以只在第一次 set 时才触发执行 (即 targetreceiver 的代理原始对象相等时)
const RAW = Symbol()
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            if (key === RAW) {
                return obj
            }
            track(target, key)
            return Reflect.get(...arguments)

        },
        set(target, key, value, receiver) {
            const oldVal = target[key]
            const res = Reflect.set(...arguments)
            if (oldVal !== value
                && !(Number.isNaN(value) && Number.isNaN(oldVal))
                && receiver[raw] === target) {
                const type = target.hasOwnProperty(key) ? 'SET' : 'ADD'
                trigger(target, key, type)
            }
            return res
        }
    })
}

5.5. 浅响应与深响应

目前创建的响应式对象,都是浅响应的,如果要做到深层响应,只要递归执行 reactive 即可, 如下

function createReactive(obj, shallow = false) {
    return new Proxy(obj, {
        get(target, key) {
            if (key === RAW) {
                return obj
            }

            const val = target[key]
            const res = Reflect.get(...arguments)
            track(target, key)
            if (shallow) {
                return res
            }
            if (typeof val === 'object' && val !== null) {
                return createReactive(val, shallow)
            }
            return res
        }
    })
}
function reactive(obj) {
    return createReactive(obj, false)
}
function shallowReactive(obj) {
    return createReactive(obj, true)
}

5.6. 只读和浅只读

实现只读,需注意下面几点:

  • 拦截 set , 不设置值
  • 拦截 deleteProperty , 不进行操作
  • 只读对象不会变更, 那么就不需要 track
  • 如果要做到深层只读, 那么递归调用 readonly 即可
function createReactive(obj, isShallow, isReadonly) {
    return new Proxy(obj, {
        get(target, key) {
            if (key === RAW) {
                return obj
            }
            if (!isReadonly) {
                track(target, key)
            }
            const value = target[key]
            const res = Reflect.get(...arguments)
            if (isShallow) {
                return res
            }
            if (typeof value === 'object' && value !== null) {
                return createReactive(obj, isShallow, isReadonly)
            }
            return res
        },
        set(target, key, value, receiver) {
            if (!isReadonly) {
                console.warn(`${key} is readonly`)
                return true
            }
            const oldVal = target[key]
            const res = Reflect.set(...arguments)
            const type = target.hasOwnProperty(key) ? 'SET' : 'ADD'
            if (oldVal !== value
                && !(Number.isNaN(oldVal) && Number.isNaN(value))
                && receiver[RAW] === target
            ) {
                trigger(target, key, type)
            }
            return res
        },
        deleteProperty(target, key) {
            if (isReadonly) {
                console.warn(`${key} is readonly`)
                return true
            }
            const hadKey = target.hasOwnProperty(key)
            const res = Reflect.deleteProperty(...arguments)
            if (hadKey && res) {
                trigger(target, key, 'DELETE')
            }
            return res
        }
    })
}

function readonly(obj) {
    return createReactive(obj, false, true)
}
function shallowReadonly(obj) {
    return createReactive(obj, true, true)
}

5.7. 代理数组

代理数组需要处理下面几个问题:

  • 通过数组索引赋值, 导致 length 变更
  • 修改 length 导致超过 length 的索引的值为 undefined
  • 数组的查询方法涉及到的对象相等性的比较
  • 数组的栈方法导致数组的变更

5.7.1. 数组索引与 length

示例

const arr = reactive([1])
effect(() => {
    console.log(arr.length)
})
arr[10] = 12 // 期望可以触发

effect(() => {
    console.log(arr[0])
})
arr.length = 0 // 期望可以触发
  • 数组的 [​[DefineOwnProperty]]10.4.2.1 定义, 所以数组也是 异质对象
  • 当数组索引赋值的 indexlength 时,会导致 length 的变更
  • length 的变更, 也会影响到数组元素

根据上面几点,可如下处理

function isObject(val) {
    return typeof val === 'object' && val !== null
}
const isArray = Array.isArray

function isEqual(a, b) {
    return a === b || (Number.isNaN(a) && Number.isNaN(b))
}

function createReactive(obj, isShallow, isReadonly) {
    return new Proxy(obj, {
        set(target, key, value, receiver) {
            if (isReadonly) {
                console.log(`${key} is readonly`)
                return
            }
            const oldVal = target[key]
            const type = isArray(target) ? Number(key) >= target.length ? 'ADD' : 'SET'
                : target.hasOwnProperty(key) ? 'SET' : 'ADD'
            const res = Reflect.set(...arguments)
            if (target === receiver[RAW]
                && !isEqual(oldVal, value)) {
                trigger(target, key, type, value)
            }
            return res
        }
    })
}

function trigger(target, key, type, newVal) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key) || []
    const effectsToRun = new Set(effects)
    if (isArray(target)) {
        const effectsLength = depsMap.get('length')
        if (type === 'ADD') {
            effectsLength && effectsLength.forEach(effect => effectsToRun.add(effect))
        }
        if (key === 'length') {
            depsMap.forEach((effectsIndex, idx) => {
                if (idx >= newVal) {
                    effectsIndex.forEach(effect => effectsToRun.add(effect))
                }
            })
        }
    } else {
        if (type === 'ADD' || type === 'DELETE') {
            (depsMap.get(ITERATE_KEY) || []).forEach(fn => effectsToRun.add(fn))
        }
    }

    effectsToRun.forEach(effectFn => {
        if (effectFn === activeEffect) return
        effectFn.options.scheduler ? effectFn.options.scheduler(effectFn) : effectFn()
    })

}

5.7.2. for...infor...of

问题示例:

const arr = reactive([1, 2])
effect(() => {
    for (const i in arr) {
        console.log(arr[i])
    }
})
// 期望都能执行
arr[10] = 3;
arr.length = 0
  • 处理对象的 for...in 时,之前是在 ownKeystrack ITERATE_KEY 。对于数组来说, ADDDELETE 都会引起 length 的变更, 所以可以直接 track length
  • 对于 for...of , 根据 23.1.5 , 迭代过程中会访问 length 以及所有索引, 所以不需要做更改就已经 track 了对应的副作用函数
  • 为了避免意外错误, 不 track 类似 Symbol.iterator 这种 symbol 值
function createReactive(obj, isShallow, isReadonly) {
    return new Proxy(obj, {
        get(target, key, receiver) {
            if (key === RAW) {
                return target
            }
            const res = Reflect.get(...arguments)

            if (!isReadonly && typeof key !== 'symbol') {
                track(target, key)
            }

            if (!isShallow && isObject(res)) {
                return createReactive(res, isShallow, isReadonly)
            }
            return res
        },
        ownKeys(target) {
            track(target, isArray(target) ? 'length' : ITERATE_KEY)
            return Reflect.ownKeys(target)
        }
    })
}

5.7.3. 处理查询方法

对于 includes indexOf lastIndexOf 这类方法,要处以下问题

示例

const obj = {}
const arr = reactive([obj])

console.log(arr.includes(obj)) // 期望是 true
  • 当代理的对象数组上,使用 includes 原始对象时, 要返回 true , 故需要重写对应的方法
  • 处理每次递归 track 都会新建代理对象的问题
function toRaw(observed) {
    const raw = observed && observed[RAW]
    return raw ? toRaw(observed) : raw // 即使是嵌套创建响应式对象, 也可以拿到最初的原始对象
}


const arrayInstrumentations = createArrayInstrumentations()
function createArrayInstrumentations() {
    const instrumentations = {}
    ['includes', 'lastIndexOf', 'indexOf'].forEach(method => {
        const originMethod = Array.prototype[method]
        instrumentations[method] = function (...args) {
            let result = originMethod.apply(this, args)
            if (result === false || result === -1) {
                return originMethod.apply(toRaw(this), args.map(toRaw))
            }
            return result
        }
    })

    return instrumentations
}
const proxyMap = new WeakMap()
function createReactive(obj, isShallow, isReadonly) {
    const existProxy = proxyMap.get(target)
    if (existProxy) {
        return existProxy
    }
    const proxy = new Proxy(obj, {
        get(target, key, receiver) {
            if (key === RAW) {
                return target
            }
            if (isArray(target) && arrayInstrumentations.hasOwnProperty(key)) {
                return Reflect.get(arrayInstrumentations, key, receiver)
            }
            // ...省略
        }
    })
    proxyMap.set(obj, proxy)
    return proxy
}

5.7.4. 处理会修改数组长度的方法

诸如 push pop shift unshift splice 这类方法,需要处理以下问题

示例:

const arr = reactive([])
effect(() => {
    arr.push(1)
})
effect(() => {
    arr.push(2)
})
  • 依据 23.1.3.20 , 方法运行时, 会读取 length 和写入 length
  • 当一个响应式数组,在两个副作用函数中都使用了 push 时, 会导致来回执行, 从而堆栈溢出
  • 解决思路: 把这类方法看成是写操作, 屏蔽读取 lengthtrack 调用
const arrayInstrumentations = createArrayInstrumentations()
let shouldTrack = true
function createArrayInstrumentations() {
    const instrumentations = {}
    ['includes', 'lastIndexOf', 'indexOf'].forEach(method => {
        instrumentations[method] = function (...args) {
            const raw = toRaw(this)
            const result = raw[method].apply(this, args)
            if (result === false || result === -1) {
                return raw[method].apply(raw, args.map(toRaw))
            }
            return result
        }
    })
    ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
        instrumentations[method] = function (...args) {
            shouldTrack = false
            const result = toRaw(this)[method].apply(raw, args)
            shouldTrack = true
            return result
        }
    })

    return instrumentations
}


function track(target, key) {
    if (!activeEffect || !shouldTrack) return
    // ... 省略
}

5.8. 代理 Set 和 Map

5.8.1. 处理 this 指向问题

SetMap 的公共原型属性/方法

  • size
  • clear()
  • delete()
  • has()
  • keys()
  • values()
  • entries()
  • forEach()

Set 特有

  • add()

Map 特有

  • set()
  • get()

代理 SetMap 需要解决的问题:

const s = new Set([1])
const p = new Proxy(s, {})

p.size // throw error , 因为在不兼容的 receiver 上调用 get size

根据 24.2.3.9 , 调用 get size 的 receiver 需要有 [​[SetData]] 这个内部槽 (同理, Map 需要有 [​[MapData]] ), 而 Proxy 不会有。

解决方法:

  • 把 receiver 指向原始的对象
function reactive(obj) {
    const p = new Proxy(obj, {
        get(target, key) {
            track(target, key)
            if (key === 'size') {
                return Reflect.get(target, key, obj)
            }
            return Reflect.get(...arguments)
        }
    })
    return p
}

调用方法时:

const s = reactive(new Set([1]))
s.delete(1) // throw error

因为上述方法调用的 this 指向的时 Proxy , 所以也不存在内部槽 [​[SetData]] 或 [​[MapData]] 。

解决方法:

  • 重新绑定 this
const p = new Proxy(new Set(), {
    get(target, key) {
        if (key === 'size') {
            return Reflect.get(target, key, target)
        }
        return target[key].bind(target)
    }
})

将其封装好:

const bucket = new WeakMap()
function reactive(obj) {
    let cached = bucket.get(obj)
    if (!cached) {
        cached = createReactive(obj, false, false)
        bucket.set(obj, cached)
    }
    return cached
}

function createReactive(obj, shallow, readonly) {
    return new Proxy(obj, {
        get(target, key) {
            if (key === 'size') {
                return Reflect.get(target, key, target)
            }
            return target[key].bind(target)
        }
    })
}

5.8.2. 建立响应关系

const m = reactive(new Map([['key', 1]]))
effect(() => {
    m.get('m')
    m.size
})
m.set('m', 12) // 没有触发副作用函数的执行
m.set('a', 12) // 没有触发
  • 访问时 track ITERATE_KEY
  • 重写 get 以 track
  • add , set , delete 时 trigger, 即重写对应的方法
const instruments = createInstruments()
function createInstruments() {
    return {
        delete(key) {
            const target = toRaw(this)
            const hadKey = target.has(key)
            const res = target.delete(key)
            if (hadKey) {
                trigger(target, key, 'DELETE')
            }
            return res
        }
        clear() {
            const target = toRaw(this)
            const hadItems = target.size > 0
            target.clear()
            if (hadItems) {
                trigger(target, ITERATE_KEY, 'CLEAR')
            }
        }
        add(key) {
            const target = toRaw(this)
            const hadKey = target.has(key)
            const res = target.add(key)
            if (!hadKey) {
                trigger(target, key, 'ADD')
            }
            return res
        }
        get(key) {
            const target = toRaw(this)
            track(target, key)
            const hadKey = target.has(key)
            const res = target.get(key)
            if (hadKey) {
                return isObject(res) ? reactive(res) : res // 仍然返回响应式对象
            }
        }
        set(key, value) {
            const target = toRaw(this)
            const hadKey = target.has(key)
            const oldValue = target.get(key)
            const res = target.set(key, toRaw(value)) // 不要让响应式数据污染源对象
            if (!hadKey) {
                trigger(target, key, 'ADD')
            } else if (!isEqual(oldValue, value)) {
                trigger(target, key, 'SET')
            }
            return res
        }
    }
}

function createReactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            if (key === 'size') {
                track(target, ITERATE_KEY)
                return Reflect.get(target, key, target)
            }
            return instruments[key]
        }
    })
}

5.8.3. 处理 forEach

  • 因为 delete , add , clear 都会影响到迭代, 故在 forEach 调用时, track ITERATE_KEY
function wrap(v) {
    return isObject(v) ? reactive(v) : v
}
mutationInstrumentaions = {
    forEach(fn, thisArg) {
        const target = toRaw(this)
        track(target, ITERATE_KEY)
        target.forEach((v, k) => {
            fn.apply(thisArg, [wrap(v), wrap(k), this])
        })
    }
}

对于 MapforEach , 即使是重新 set 新值时, 也应该重新执行副作用函数。故如下修改:

function isMap(obj) {
    Object.prototype.toString.call(obj) === '[object Map]'
}

function trigger(target, key, type) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key) || []
    const effectsToRun = new Set(effects)

    if (type === 'DELETE'
        || type === 'ADD'
        || (type === 'SET' && isMap(target))) {
        (depsMap.get(ITERATE_KEY) || []).forEach(fn => effectsToRun.add(fn))
    }
    // ... 省略
}

5.8.4. 处理迭代器方法

  • Map.entries 与 Map[Symbol.iterator] 是相等的
  • for..of 中, 应该返回 响应式数据

实现 可迭代协议 指的是实现了 Symbol.iterator 方法, 实现 迭代器协议 指的是实现了 next 方法

function iterateMethod() {
    const target = toRaw(this)
    track(target, ITERATE_KEY)

    const it = target[Symbol.iterator]()
    // 返回迭代器对象
    return {
        next() {
            const { value, done } = it.next()
            const
            return {
                value: value ? [wrap[value[0], wrap(value[1])]] : value,
                done
            }
        },
        // 实现可迭代协议
        [Symbol.iterator]() {
            return this
        }

    }
}

const mutationInstrumentations = {
    [Symbol.iterator]: iterateMethod,
    entries: iterateMethod
}

5.8.5. 处理 valueskeys

与上述类似

const MAP_KEY_ITERATOR_KEY = Symbol()
const mutationInstrumentations = {
    values() {
        const target = toRaw(this)

        track(target, ITERATE_KEY)

        const it = target.values()
        return {
            next() {
                const { value, done } = it.next()
                return {
                    value: wrap(value),
                    done
                }
            },
            [Symbol.iterator]() {
                return this
            }
        }
    },
    keys() {
        const target = toRaw(this)
        track(target, MAP_KEY_ITERATOR_KEY)
        const it = target.keys()
        return {
            next() {
                const { value, done } = it.next()
                return {
                    value: wrap(value),
                    done
                }
            },
            [Symbol.iterator]() {
                return this
            }
        }
    }
}

function trigger(target, key, type) {
    // ..
    if (type === 'ADD'
        || type === 'DELETE'
        || (type === 'SET' && isMap(target))) {
        (effects.get(MAP_KEY_ITERATOR_KEY) || []).forEach(fn => effectsToRun.add(fn))
    }
}

5.9. 总结

  • 代理对象需要处理属性的添加和删除触发 for..in , in 相关副作用函数的执行
  • 代理数组需要处理
    • length 与索引下表赋值的互相影响
    • 跟踪 for..infor..of (这个不用特殊处理)
    • includes , indexOf , lastIndexOf 传入响应式数据的情况
    • pop push unshift shift splice 同时触发 length 的读写, 导致存在两个副作用函数时,无限递归
  • 代理 Map Set 需要处理
    • size 的 this 指向
    • get set 方法需要收集对应 key 的依赖
    • add delete 等方法对 forEach values keys entries size Symbol.iterator for..of 的影响
  • 实现对象的 深响应/浅响应, 深只读/浅只读

完整示例

const isObject = (v) => typeof v === 'object' && v !== null
const isMap = (v) => Object.prototype.toString.call(v) === '[object Map]'
const isSet = (v) => Object.prototype.toString.call(v) === '[object Set]'
const isArray = Array.isArray
const isEqual = (a, b) => a === b || (Number.isNaN(a) && Number.isNaN(b))
function toRaw(obj) {
    const v = obj && obj[RAW]
    return v ? toRaw(v) : obj
}
const RAW = Symbol()
const ITERATE_KEY = Symbol()
const MAP_KEY_ITERATE_KEY = Symbol()

const activeEffectStack = []
let activeEffect;

const bucket = new WeakMap()

function effect(fn, options = {}) {
    const effectFn = () => {
        cleanup(effectFn)
        activeEffect = effectFn
        activeEffectStack.push(effectFn)
        const res = fn()
        activeEffectStack.pop()
        activeEffect = activeEffectStack[activeEffectStack.length - 1]
        return res
    }
    effectFn.options = options
    effectFn.deps = []
    if (!effectFn.options.lazy) {
        effectFn()
    }

    return effectFn
}
function track(target, key) {
    if (!activeEffect || !shouldTrack) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
        bucket.set(target, depsMap = new Map())
    }
    let effects = depsMap.get(key)
    if (!effects) {
        depsMap.set(key, effects = new Set())
    }
    activeEffect.deps.push(effects)
    effects.add(activeEffect)
}

function trigger(target, key, type, newValue) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key) || []
    const effectsToRun = new Set(effects)

    const iterateEffects = depsMap.get(ITERATE_KEY) || []
    const mapKeyIterateEffects = depsMap.get(MAP_KEY_ITERATE_KEY) || []
    if (type === 'add'
        || type === 'delete') {
        iterateEffects.forEach(fn => effectsToRun.add(fn))
        mapKeyIterateEffects.forEach(fn => effectsToRun.add(fn))
    }
    if (isMap(target) && type === 'set') {
        iterateEffects.forEach(fn => effectsToRun.add(fn))
    }
    if (isArray(target)) {
        if (type === 'add') {
            const lengthEffects = depsMap.get('length') || []
            lengthEffects.forEach(fn => effectsToRun.add(fn))
        }
        if (key === 'length') {
            Array.from(depsMap.keys())
                .forEach(idx =>
                    idx >= newValue && depsMap.get(idx).forEach(fn => effectsToRun.add(fn))
                )
        }
    }

    effectsToRun.forEach(effectFn => {
        if (effectFn !== activeEffect) {
            effectFn.options.scheduler ? effectFn.options.scheduler(effectFn) : effectFn()
        }
    })
}

function cleanup(effectFn) {
    effectFn.deps.forEach(effects => {
        effects.delete(effectFn)
    })
    effectFn.deps.length = 0
}

let shouldTrack = true
const arrayInstrumentations = createArrayInstrumentations()
function createArrayInstrumentations() {
    const instrumentations = {};
    ['includes', 'indexOf', 'lastIndexOf'].forEach(method => {
        instrumentations[method] = function (...args) {
            const target = toRaw(this)
            const res = target[method](...args)
            if (res === false || res === -1) {
                return target[method](...args.map(toRaw))
            }
            return res
        }
    });
    ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(method => {
        instrumentations[method] = function (...args) {
            const target = toRaw(this)
            shouldTrack = false;
            const res = target[method](...args)
            shouldTrack = true
            return res
        }
    })
    return instrumentations
}

function wrap(v) {
    return isObject(v) ? reactive(v) : v
}
const mutationInstrumentations = createMutationInstrumentations()
function createMutationInstrumentations() {
    const iterFn = function () {
        const target = toRaw(this)
        const it = target[Symbol.iterator]()
        track(target, ITERATE_KEY)
        return {
            next() {
                const { done, value } = it.next()
                return {
                    done,
                    value: value ? [wrap(value[0]), wrap(value[1])] : value
                }
            },
            [Symbol.iterator]() {
                return this
            }
        }
    }

    return {
        get(key) {
            const target = toRaw(this)
            track(target, key)
            const res = target.get(key)
            return wrap(res)
        },
        add(key) {
            const target = toRaw(this)
            const hadKey = target.has(key)
            const res = target.add(key)
            if (!hadKey) {
                trigger(target, key, 'add')
            }
            return res
        },
        set(key, val) {
            const target = toRaw(this)
            const hadKey = target.has(key)
            const oldValue = target.get(key)
            const res = target.set(key, toRaw(val))
            if (!hadKey) {
                trigger(target, key, 'add', val)
            } else if (!isEqual(val, oldValue)) {
                trigger(target, key, 'set', val)
            }
            return res
        },
        delete(key) {
            const target = toRaw(this)
            const hadKey = target.has(key)
            const res = target.delete(key)
            if (hadKey) {
                trigger(target, key, 'delete')
            }
            return res
        },
        forEach(fn, thisArg) {
            const target = toRaw(this)
            track(target, ITERATE_KEY)
            target.forEach((v, k) => {
                fn.call(thisArg, wrap(v), wrap(k), this)
            })
        },
        entries: iterFn,
        [Symbol.iterator]: iterFn,
        keys() {
            const target = toRaw(this)
            track(target, MAP_KEY_ITERATE_KEY)
            const it = target.keys()
            return {
                next() {
                    const { done, value } = it.next()
                    return {
                        done,
                        value: wrap(value)
                    }
                },
                [Symbol.iterator]() {
                    return this
                }
            }
        },
        values() {
            const target = toRaw(this)
            track(target, ITERATE_KEY)
            const it = target.values()
            return {
                next() {
                    const { done, value } = it.next()
                    return {
                        done,
                        value: wrap(value)
                    }
                },
                [Symbol.iterator]() {
                    return this
                }
            }
        }

    }
}

function createReactiveObject(obj, proxyMap, isShallow = false, isReadonly = false) {
    let cached = proxyMap.get(obj)
    if (!cached) {
        cached = new Proxy(obj, {
            get(target, key, receiver) {
                if (key === RAW) {
                    return obj
                }
                if (arrayInstrumentations.hasOwnProperty(key)) {
                    return arrayInstrumentations[key]
                }
                if (isMap(target) || isSet(target)) {
                    if (mutationInstrumentations.hasOwnProperty(key)) {
                        return mutationInstrumentations[key]
                    }
                    if (key === 'size') {
                        track(target, ITERATE_KEY)
                        return target[key]
                    }
                }
                if (!isReadonly && typeof key !== 'symbol') {
                    track(target, key)
                }
                const res = Reflect.get(target, key, receiver)
                if (isObject(res)
                    && !isShallow) {
                    return createReactiveObject(obj, proxyMap, isShallow, isReadonly)
                }
                return res
            },
            set(target, key, value, receiver) {
                if (isReadonly) {
                    console.log(`${key} is readonly`)
                    return true
                }
                const hadKey = target.hasOwnProperty(key)
                const oldValue = target[key]
                const type = isArray(target) ? key >= target.length ? 'add' : 'set'
                    : hadKey ? 'set' : 'add'

                const res = Reflect.set(target, key, value, receiver)
                if (!isEqual(oldValue, value)
                    && target === toRaw(receiver) // 忽略原型链上的赋值
                ) {
                    trigger(target, key, type, value)
                }
                return res
            },
            deleteProperty(target, key) {
                const hadKey = target.hasOwnProperty(key)
                const res = Reflect.deleteProperty(target, key)
                if (hadKey) {
                    trigger(target, key, 'delete')
                }
                return res
            },
            has(target, key) {
                track(target, key)
                return Reflect.has(target, key)
            },
            ownKeys(target) {
                isArray(target) ? track(target, 'length') : track(target, ITERATE_KEY)
                return Reflect.ownKeys(target)
            }
        })
        proxyMap.set(obj, cached)
    }
    return cached
}

const reactiveMap = new WeakMap()
function reactive(obj) {
    return createReactiveObject(obj, reactiveMap)
}
const shallowReactiveMap = new WeakMap()
function shallowReactive(obj) {
    return createReactiveObject(obj, shallowReactiveMap, true)
}
const readonlyMap = new WeakMap()
function readonly(obj) {
    return createReactiveObject(obj, readonlyMap, false, true)
}
const shallowReadonlyMap = new WeakMap()
function shallowReadonly(obj) {
    return createReactiveObject(obj, shallowReadonlyMap, true, true)
}


let obj = reactive({ foo: 2, baz: 10 })
effect(() => {
    console.log('1. foo in obj', 'foo' in obj)
})
delete obj.foo

effect(() => {
    for (const key in obj) {
        console.log(`2. ${key} in obj`)
    }
    console.log('---')
})
obj.bar = 3
obj.bar = 5
delete obj.bar
effect(() => {
    console.log('obj.baz', obj.baz)
})
obj.baz = 12
console.log('值没变化,不触发')
obj.baz = 12

console.log('继承的属性赋值应该只触发一次')
obj = {}
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
Object.setPrototypeOf(child, parent)
effect(() => {
    console.log('child.bar', child.bar)
})
child.bar = 12

console.log('深层响应')
obj = reactive({ foo: { bar: 1 } })
effect(() => {
    console.log('obj.foo.bar', obj.foo.bar)
})
obj.foo.bar = 12
console.log('浅响应')
obj = shallowReactive({ foo: { bar: 1 } })
effect(() => {
    console.log('obj.foo.bar', obj.foo.bar)
})
obj.foo = { bar: 3 }
obj.foo.bar = 10
console.log('只读')
obj = readonly({ foo: 1, bar: { baz: 3 } })
obj.foo = 2 // 警告
obj.bar.baz = 12 // 警告
console.log('浅只读')
obj = shallowReadonly({ foo: 1, bar: { baz: 1 } })
obj.foo = 2 // 警告
obj.bar.baz = 3 // 不警告
console.log('代理数组')
let arr = reactive(['foo'])
effect(() => {
    console.log(arr[0])
})
arr[0] = 'bar'
effect(() => {
    console.log('length', arr.length)
})
console.log('设置越界索引能够触发 length 的响应')
arr[1] = 'xxx' // 触发响应
arr = reactive([0, 1])
effect(() => {
    console.log('arr[0]', arr[0])
})
effect(() => {
    console.log('arr[1]', arr[1])
})
console.log('修改length导致的元素被删除,也会触发响应')
arr.length = 1
arr = reactive([1])
effect(() => {
    for (const key in arr) {
        console.log(`arr[${key}]`)
    }
})
console.log('正确触发 for..in')
arr[2] = 'bar'
console.log('---')
arr.length = 1

arr = reactive([1])
effect(() => {
    for (const v of arr) {
        console.log(v)
    }
})
console.log('正确触发 for..of')
arr[1] = 3
console.log('---')
arr.length = 1

obj = {}
arr = reactive([obj])
console.log('includes', arr.includes(obj))

console.log('不会堆栈溢出')
arr = reactive([])
effect(() => {
    arr.push(1)
})
effect(() => {
    arr.push(1)
})

let map = reactive(new Map([['key', 1]]))
effect(() => {
    console.log('map.get(key)', map.get('key'))
})
console.log('Map.set')
map.set('key', 2)
console.log('----')
map.set('key2', 3)
console.log('map.size', map.size)
console.log('map.delete', map.delete('key'))
console.log('不污染原始数据')
const m = new Map()
const p1 = reactive(m)
const p2 = reactive(new Map())
p1.set('p2', p2)
effect(() => {
    console.log(m.get('p2').size)
})
m.get('p2').set('a', 1) // 不触发
let p = reactive(new Map([[{ key: 1 }, { value: 1 }]]))
effect(() => {
    p.forEach((v, k) => {
        console.log(`${k}: ${v}`)
    })
})
console.log('forEach')
p.set({ key: 2 }, { value: 2 })

let key = { key: 1 }
let value = new Set([1, 2, 3])
p = reactive(new Map([[key, value]]))
console.log('----')
effect(() => {
    p.forEach((v) => {
        console.log('v.size', v.size)
    })
})
console.log('forEach获取的应该也是响应式的')
p.get(key).delete(1)
console.log('Map.set 也要触发 forEach')
p = reactive(new Map([['key', 1]]))
effect(() => {
    p.forEach((v, k) => {
        console.log(`${k}: ${v}`)
    })
})
p.set('key', 4)

console.log('迭代器方法')
p = reactive(new Map([['key1', 'value1'], ['key2', 'value2']]))
effect(() => {
    for (const [key, value] of p) {
        console.log(`${key}: ${value}`)
    }
    for (const k of p.keys()) {
        console.log('key: ', k)
    }
    for (const v of p.values()) {
        console.log('value: ', v)
    }
})
p.set('key3', 'value3')

p = reactive(new Map([['key', 'value']]))
effect(() => {
    for (const k of p.keys()) {
        console.log('k: ', k)
    }
})
console.log('应该不执行')
p.set('key', 2)
console.log('----')
1. foo in obj true
1. foo in obj false
2. baz in obj
---
2. baz in obj
2. bar in obj
---
2. baz in obj
---
obj.baz 10
obj.baz 12
值没变化,不触发
继承的属性赋值应该只触发一次
child.bar 1
child.bar 12
深层响应
obj.foo.bar undefined
obj.foo.bar 12
浅响应
obj.foo.bar 1
obj.foo.bar 3
只读
foo is readonly
baz is readonly
浅只读
foo is readonly
代理数组
foo
bar
length 1
设置越界索引能够触发 length 的响应
length 2
arr[0] 0
arr[1] 1
修改length导致的元素被删除,也会触发响应
arr[1] undefined
arr[0]
正确触发 for..in
arr[0]
arr[2]
---
arr[0]
1
正确触发 for..of
1
3
---
1
includes true
不会堆栈溢出
map.get(key) 1
Map.set
map.get(key) 2
----
map.size 2
map.get(key) undefined
map.delete true
不污染原始数据
0
[object Object]: [object Object]
forEach
[object Object]: [object Object]
[object Object]: [object Object]
----
v.size 3
forEach获取的应该也是响应式的
v.size 2
Map.set 也要触发 forEach
key: 1
key: 4
迭代器方法
key1: value1
key2: value2
key:  key1
key:  key2
value:  value1
value:  value2
key1: value1
key2: value2
key3: value3
key:  key1
key:  key2
key:  key3
value:  value1
value:  value2
value:  value3
k:  key
应该不执行
----

6. 原始值的响应式方案

6.1. ref

实现 ref 需要处理的问题:

  • 统一的规范
  • 外部可以识别出其为 ref
function ref(v) {
    const wrapper = {
        value: v
    }
    Object.defineProperty(wrapper, '__v_isRef', {
        value: true
    })
    return reactive(wrapper)
}

6.2. 响应丢失问题

reactive 解构赋值时会存在响应丢失, 如下:

const reactiveObj = reactive({ foo: 1, bar: 1 })
const obj = {
    ...reactiveObj
}
effect(() => {
    obj.foo // 非响应值
})

可以改成下面的模式:

const reactiveObj = reactive({ foo: 1 })
const obj = {
    ...toRefs(reactiveObj)
}
obj.foo.value // 1

实现

function toRef(obj, key) {
    const wrapper = {
        get value() {
            return obj[key]
        },
        set value(v) {
            obj[key] = v
        }
    }
    Object.defineProperty(wrapper, '__v_isRef', {
        value: true
    })
    return wrapper
}
function toRefs(obj) {
    const wrapper = {}
    for (const key in obj) {
        wrapper[key] = toRef(obj, key)
    }
    return wrapper
}

6.3. 自动 unref

传给 template 的响应式对象,以及传给 reactiveref 都有自动 unref 的能力。

实现思路如下, 其中传给 template 的响应式对象都会先经过 proxyRefs 处理

function isRef(ref) {
    return ref && ref.__v_isRef
}

function unref(ref) {
    return isRef(ref) ? ref.value : ref
}

function proxyRefs(target) {
    return new Proxy(target, {
        get(target, key, receiver) {
            return unref(Reflect.get(target, key, receiver))
        },
        set(target, key, value) {
            if (isRef(target, key)) {
                target[key].value = value
                return true
            }
            return Reflect.set(...arguments)
        }
    })
}

6.4. 总结

  • ref 是对 reactive 的二次封装
  • toReftoRefs 的返回值也实现了 *.value 的模式
  • 传给 templatereactiveref 都会自动 unref

7. 渲染器的设计

7.1. 渲染器与响应式系统的结合

在vue中, 其结合方式类似如下

const count = ref(0)

effect(() => {
    renderer(`<div>count: ${count.value}`, document.getElementById('app'))
})
count.value++

function renderer(domString, container) {
    container.innerHTML = domString
}

7.2. 渲染器的基本概念

  • vnode vdom 树形结构的 js 对象
  • renderer 渲染器, 其中的 render 函数可以把 vnode 转成真实的元素。(注意不要和 option api 上的 render 函数混淆, 那个是返回组件 vdom 的)
  • patch 比较新旧节点,然后变更节点。其中 挂载/卸载/更新 都在这里面

实际的使用场景

const renderer = createRenderer()
renderer.render(vnode, $app)
renderer.render(vnode2, $app)
renderer.render(null, $app)

渲染器类似如下

function createRenderer() {
    const render = (vnode, container) => {
        if (vnode) {
            patch(container._vnode, vnode, container)
        } else {
            if (container._vnode) {
                // 旧的存在, 没有新的, 说明时 unmounted
                container.innerHTML = ''
            }
        }
        container._vnode = vnode
    }
    const hydrate = () => { } // SSR 时会用到
    return {
        render,
        hydrate
    }
}

7.3. 自定义渲染器

为了实现跨平台渲染,可如下操作:

  • 涉及到对应平台 API 的操作, 抽象成配置项传给 createRenderer
function createRenderer({ createElement, setElementText, insert }) {
    function render(vnode, container) {
        if (vnode) {
            patch(container._vnode, vnode, container)
        } else {
            if (container._vnode) {
                container.innerHTML = ''
            }
        }
        container._vnode = vnode
    }
    function patch(n1, n2, container) {
        if (!n1) {
            // 挂载
            mountElement(n2, container)
        }
    }
    function mountElement(vnode, container) {
        let el = createElement(vnode.tag)
        if (typeof vnode.children === 'string') {
            setElementText(el, vnode.children)
        } else {
            vnode.children.forEach(childNode => {
                mountElement(childNode, el)
            })
        }
        insert(el, container)
    }
    return {
        render
    }
}
// Nodejs 环境运行的
const renderer = createRenderer({
    createElement(type) {
        console.log(`创建元素 ${type}`)
        return { type }
    },
    setElementText(node, text) {
        console.log(`在 ${JSON.stringify(node)} 设置文本 ${text}`)
        node.textContent = text
    },
    insert(el, parent) {
        console.log(`将 ${JSON.stringify(el)} 添加到 ${JSON.stringify(parent)}`)
        parent.children = el
    }
})

renderer.render({ tag: 'h1', children: 'hello world' }, { type: 'root' })

7.4. 总结

  • 渲染器的作用是将 vdom 转成真实的元素
  • 渲染器的实现与响应式无关
  • patch 用来更新真实元素
  • 为了实现跨平台渲染,可以把具体平台的API抽象成配置项传给 createRenderer

8. 挂载和更新

8.1. 挂载子节点和元素的属性

function mountElement(vnode, container) {
    const el = createElement(vnode.tag)
    if (typeof vnode.children === 'string') {
        setElementText(el, vnode.children)
    }
    else if (isArray(vnode.children)) {
        vnode.children.forEach(child => {
            patch(null, child, el)
        })
    }
    if (vnode.props) {
        // 设置属性
        for (let key in vnode.props) {
            el[key] = vnode.props[key]
        }
    }
    insert(el, container)
}

8.2. HTML Attribute 和 DOM properties

  • Attribute 是 html 上设置的值, properties 是 js dom 的属性值
  • 有些属性, 两者是直接映射关系,例如 id
  • 有些属性, Attribute 上有, properties 没有, 例如 aria-*
  • 有些属性, properties 有, Attribute 没有, 例如 textContent
  • 有些属性, Attribute 是初始值, 而 properties 是实时的值, 例如 value
  • 有些属性, properties 是 Attribute 矫正后的值, 例如 <input type​="foo">= , el.type 得到的是 text
  • 核心原则是, Attribute 设置的是 properties 的初始值

8.3. 正确地设置元素的属性

属性设置遵从下面顺序

  • properties 存在的属性, 优先设置 properties
  • 对于 properties 存在的属性, 但其类型是 boolean 的, 把空字符串自动转成 true (解决类似 <button disabled>btn</button> 中的 disabled 这种特性的问题)
  • 对于只读的 properties 直接设置 Attribute, (例如 <input form​="form1">

大概逻辑如下

function mountElement(vnode, container) {
    const el = createElement(vnode.tag)
    if (typeof vnode.children === 'string') {
        setElementText(el, vnode.children)
    } else if (isArray(vnode.children)) {
        vnode.children.forEach(child => {
            patch(null, child, el)
        })
    }
    if (vnode.props) {
        for (const key in vnode.props) {
            patchProps(el, key, null, vnode.props[key])
        }
    }
    insert(el, container)
}

function shouldSetAsProps(el, key) {
    return (key in el) && (el.type !== 'input' && key !== 'form')
}

createRenderer({
    patchProps(el, key, prevValue, nextValue) {
        if (shouldSetAsProps(el, key)) {
            if (typeof el[key] === 'boolean' && nextValue === '') {
                el[key] = true
            } else {
                el[key] = nextValue
            }
        } else {
            el.setAttribute(key, nextValue)
        }
    }
})

8.4. class 的处理

编译成 vnodeclass 有3种形式

  • class: 'foo bar'
  • class: {foo:true, bar: true}
  • class: ['foo bar', {baz: true}]

因此需要实现一个 normalizeClass , 对其统一处理

props.class = normalizeClass(props.class)

与之相同的还有 style

同时, 因为 set el.className 的性能高于 el.classListel.setAttribute('class',$class), 所以对其进行特殊处理

function patchProps(el, key, prevValue, nextValue) {
    if (key === 'class') {
        el.className = nextValue
    } else if (shouldSetAsProps(el, key)) {
        if (typeof el[key] === 'boolean' && nextValue === '') {
            el[key] = true
        } else {
            el[key] = nextValue
        }
    } else {
        el.setAttribute(key, nextValue)
    }
}

8.5. 卸载操作

直接用 innerHTML​='' 的弊端

  • 不会移除事件监听器
  • 无法触发自定义组件的生命周期函数
  • 无法触发自定义指令的钩子函数

故如下处理

function render(vnode, container) {
    if (vnode) {
        patch(container._vnode, vnode, container)
    } else {
        if (container._vnode) {
            unmount(container._vnode)
        }
    }
    container._vnode = vnode
}

function mountElement(vnode, container) {
    const el = vnode.el = createElement(vnode.type)
    // ...
}

function unmount(vnode) {
    const parent = vnode.el.parentNode
    if (parent) {
        parent.removeChild(vnode.el)
    }
}

8.6. 区分 vnode 的类型

  • 如果新旧 vnode 的type 不一样, 应该直接 umount 旧, 然后 mountElement 新的
  • 同时根据 vnode.type 的类型, 区分不同的操作

修改 patch 函数:

function patch(n1, n2, container) {
    if (n1.type !== n2.type) {
        umount(n1)
        n1 = null
    }
    const { type } = n2
    if (typeof type === 'string') {
        // 标签元素
        if (!n1) {
            mountElement(n2, container)
        } else {
            patchElement(n1, n2, container)
        }
    } else if (typeof type === 'object') {
        // 组件
    }

}

8.7. 事件的处理

  • 约定 ^onXX 类型的 prop 为事件监听器
  • 调用 addEventListenerremoveEventListener 添加/移除事件
  • 使用一个伪造的事件处理函数 invoker , 其内部调用真正的 handler, 从而减少频繁的 add/remove
  • 处理多个不同事件,以及同种事件多个监听器的情况
function patchProps(el, key, prevValue, nextValue) {
    if (/^on/.test(key)) {
        const invokers = el._ver || (el._ver = {})
        const eventName = key.slice(2).toLowerCase()
        let invoker = invokers[key]
        if (nextValue) {
            if (!invoker) {
                invoker = invokers[key] = (e) => {
                    if (isArray(invoker.value)) {
                        invoker.value.forEach(fn => fn(e))
                    } else {
                        invoker.value(e)
                    }
                }
                el.addEventListener(eventName, invoker)
            }
            invoker.value = nextValue
        } else if (invoker) {
            el.removeEvnetListener(eventName, invoker)
            invokers[key] = undefined
        }
    } else if (key === 'class') {
        // ...
    } else if (shouldSetAsProps(el, key, nextValue)) {
        // ...
    } else {
        // ...
    }
}

8.8. 事件冒泡与更新时机问题

考虑下面场景

const bol = ref(false)
effect(() => {
    const vnode = {
        tag: 'div',
        props: {
            onClick: bol.value ? () => alert('parent') : undefined
        },
        children: [
            {
                type: 'p',
                props: {
                    onClick: () => bol.value = true
                }
            }
        ]
    }
    renderer.render(vnode, document.getElementById('app'))
})
  • 首次渲染时, div 没有监听器, p 有监听器
  • 点击 p 时, bol.value 会变成 true, 触发副作用函数,导致 vnode 更新, 从而 div 加上了监听器
  • 然后是事件冒泡到 div 的监听器, 从而执行
  • 但是期望的结果是: div 的监听器不应该执行, 因为点击的时候该监听器还不存在
  • 解决方法是:执行监听器时,过滤掉绑定时间迟于事件触发事件的监听器
function patchProps(el, key, prevValue, nextValue) {
    if (/^on/.test(key)) {
        const invokers = el._ver || (el._ver = {})
        const eventName = key.slice(2).toLowerCase()
        let invoker = invokers[eventName]
        if (nextValue) {
            if (!invoker) {
                invoker = invokers[eventName] = (e) => {
                    if (invoker.attached > e.timestamp) return
                    if (isArray(invoker.value)) {
                        invoker.value.forEach(fn => fn(e))
                    } else {
                        invoker.value(e)
                    }
                }
                el.addEventListener(eventName, invoker)
            }
            invoker.value = nextValue
            invoker.attached = Date.now()
        } else {
            // ...
        }
    } else {
        // ...
    }
}

8.9. 更新子节点

更新子节点为下面几种情况的组合:

  • 旧节点为: 不存在、文本节点、数组
  • 新节点为: 不存在、文本节点、数组
function patchElement(n1, n2) {
    const el = n1.el = n2.el;
    const oldProps = n1.props;
    const newProps = n2.props;
    for (const key in newProps) {
        // 更新 props
        if (newProps[key] !== oldProps[key]) {
            patchProps(el, key, oldProps[key], newProps[key])
        }
    }
    for (const key in oldProps) {
        // 移除 props
        if (!(key in newProps)) {
            patchProps(el, key, oldProps[key], null)
        }
    }
    patchChildren(n1, n2, el)
}

function patchChildren(n1, n2, container) {
    if (typeof n2.children === 'string') {
        if (isArray(n1.children)) {
            // 卸载旧的节点
            n1.children.forEach(child => umount(child))
        }
        setElementText(container, n2.children)
    } else if (isArray(n2.children)) {
        if (isArray(n1.children)) {
            // 这里应该用到 diff 算法, 现在先简单处理, 卸载旧的, 挂载新的
            n1.children.forEach(child => umount(child))
            n2.children.forEach(c => patch(null, c, container))
        } else {
            // 旧的不存在,或者是文本
            setElementText(container, '')
            n2.children.forEach(c => patch(null, c, container))
        }
    } else {
        // 没有新的
        if (isArray(n1.children)) {
            n1.children.forEach(child => umount(child))
        } else if (typeof n1.children === 'string') {
            setElementText(container, '')
        }
    }
}

8.10. 文本节点和注释节点

  • 使用 vnode.type 判断节点类型
  • 对于文本节点和注释节点, 新建 Symbol type 表示
  • createTextcreateCommentsetText 抽象出来
const Text = Symbol()
const Comment = Symbol()
function patch(n1, n2, container) {
    if (n1 && n1.type !== n2.type) {
        umount(n1)
        n1 = null
    }
    const { type } = n2
    if (typeof type === 'string') {
        if (!n1) {
            mountElement(n2, container)
        } else {
            patchElement(n1, n2, container)
        }
    } else if (type === Text) {
        if (!n1) {
            const el = n2.el = createText(n2.children)
            insert(el, container)
        } else {
            if (n1.children != n2.children) {
                setText(n1.el, n2.children)
            }
        }
    } else if (type === Comment) {
        if (!n1) {
            const el = n2.el = createComment(n2.children)
            insert(el, container)
        } else {
            // 不支持动态注释
            n2.el = n1.el
        }
    }

}

8.11. Fragment

  • Fragment 是为了处理多个根节点的问题
  • Fragment 本身不渲染真实的DOM, 所以没有 el
const Fragment = Symbol()
function patch(n1, n2, container) {
    // ..
    const { type } = n2;
    if (typeof type === 'string') {
        // ..
    } else if (type === Fragment) {
        if (!n1) {
            // 将 children 挂载到容器上
            n2.children.forEach(c => patch(null, c, container))
        } else {
            // 都是 Fragment, 更新 children 就可以
            patchChildren(n1, n2, container)
        }
    }
}

function unmount(vnode) {
    if (vnode.type === Fragment) {
        vnode.children.forEach(c => unmount(c))
        return
    }
    const parent = vnode.el.parentNode
    if (parent) {
        parent.removeChild(vnode.el)
    }
}

8.12. 总结

  • 使用 patch 挂载节点
  • 使用 patchProps 处理 props 更新
  • 使用 invokers 让事件监听器不需要频繁 remove
  • 通过比较监听器 attached 时间和事件触发事件, 解决事件冒泡时添加监听器的问题
  • 使用 patchChildren 更新子节点
  • 处理子节点更新,需考虑 children 为 字符串、数组、null 3种情况
  • 当新旧子节点都是数组时,需要用到 diff 算法
  • 对于普通标签元素,使用 mountElement , patchElement
  • 对于组件, 使用 mountComponent , patchComponent
  • 处理 vnode.type 为文本节点和注释节点或 Fragment

9. 简单Diff算法

9.1. 减少 DOM 操作的性能开销

考虑下面场景:

const oldVNode = {
    type: 'div',
    children: [
        { type: 'p', children: '1' },
        { type: 'p', children: '2' },
        { type: 'p', children: '3' },
    ]
}

const newVNode = {
    type: 'div',
    children: [
        { type: 'p', children: '11' },
        { type: 'p', children: '22' },
        { type: 'p', children: '32' },
    ]
}
  • 如果直接 unmount 旧的, patch 新的, 需要 DOM 操作 6次
  • 更新上述节点, 使得 DOM 操作数小, 可以通过比对 children ,相同类型的只需重新设置 children ,不需要卸载了。 这样子 DOM 操作变成了 3次
  • 需要处理两个 children 个数相同或不同的情景
function patchChildren(n1, n2, container) {
    if (isArray(n2.children)) {
        if (isArray(n1.children)) {
            const n1Len = n1.children.length;
            const n2Len = n2.children.length;
            const commonLength = Math.min(n1Len, n2Len);
            for (let i = 0; i < commonLength; i++) {
                // 相同的
                patch(n1.children[i], n2.children[i], container)
            }
            if (n1Len > n2Len) {
                for (let i = commonLength; i < n1Len; i++) {
                    // 卸载旧的
                    unmount(n1.children[i])
                }
            } else if (n2Len > n1Len) {
                for (let i = commonLength; i < n2Len; i++) {
                    // 新增新的
                    patch(null, n2.children[i], container)
                }
            }
        }
    } else {
        // ...
    }
}

9.2. DOM 复用与 key 的作用

考虑下面场景

// oldChildren
[
    { type: 'p' },
    { type: 'div' },
    { type: 'span' }
]
// newChildren
[
    { type: 'span' },
    { type: 'p' },
    { type: 'div' }
]
  • 新旧 children 只是顺序不一样, 如果按照上面的方法, DOM 操作数还是6次
  • 可以给每个 vnodekey 作为标识, 从而复用之前创建的 DOM

例如:

[
    { type: 'p', key: '1' },
    { type: 'div', key: '2' },
    { type: 'span', key: '3' }
]
// newChildren
[
    { type: 'span', key: '3' },
    { type: 'p', key: '1' },
    { type: 'div', key: '2' }
]
  • 当遍历 newChildren, 如果可以在 oldChildren 找到对应的 key 那么就复用之前的
  • 该方案没有解决如何移动
function patchChildren(n1, n2, container) {
    if (isArray(n2.children)) {
        if (isArray(n1.children)) {
            const oldChildren = n1.children
            const newChildren = n2.children
            for (let i = 0; i < newChildren.length; i++) {
                const newChild = newChildren[i]
                for (let j = 0; j < oldChildren.length; j++) {
                    const oldChild = oldChildren[i]
                    if (oldChild.key === newChild.key) {
                        patch(oldChild, newChild, container)
                        break
                    }
                }
            }
        } else {
            // ..
        }
    } else {
        // ..
    }
}

9.3. 找到需要移动的元素

  • 比较新旧节点, 使用 lastIndex 记录最大的复用节点的索引
  • 如果当前内层 oldChildren 的循环中, 索引小于 lastIndex (说明不是递增趋势),说明该 oldChild 需要移动

9.4. 如何移动元素

  • 接着上文, oldChild 移动的位置, 就是当前外层循环的 前一个 newChild 的后面
function patchChildren(n1, n2, container) {
    if (isArray(n2.children)) {
        if (isArray(n1.children)) {
            const oldChildren = n1.children
            const newChildren = n2.children
            let lastIndex = 0;
            for (let i = 0; i < newChildren.length; i++) {
                const newChild = newChildren[i]
                for (let j = 0; j < oldChildren.length; j++) {
                    const oldChild = oldChildren[j]
                    if (oldChild.key === newChild.key) {
                        patch(oldChild, newChild, container)
                        if (j >= lastIndex) {
                            lastIndex = j
                        } else {
                            // 需要移动
                            const prevChild = newChildren[i - 1]
                            if (prevChild) {
                                const anchor = prevChild.el.nextSibling
                                insert(newChild.el, container, anchor)
                            }
                        }
                        break
                    }
                }
            }

        }
        // ..
    }
    // ..
}
// 修改 insert 实现
function insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
}

9.5. 添加新元素

  • 上述算法执行过程中,如果没找到复用节点,那么应该创建 DOM, 并挂载将其挂载到 prevNode 后面
function patchChildren(n1, n2, container) {
    if (isArray(n2.children)) {
        if (isArray(n1.children)) {
            const oldChildren = n1.children
            const newChildren = n2.children
            let lastIndex = 0;
            for (let i = 0; i < newChildren.length; i++) {
                const newNode = newChildren[i]
                let exist = false;
                for (let j = 0; j < oldChildren.length; j++) {
                    const oldNode = oldChildren[j]
                    if (oldNode.key === newNode.key) {
                        exist = true
                        patch(oldNode, newNode, container)
                        if (lastIndex <= j) {
                            // 不需要移动
                            lastIndex = j
                        } else {
                            const prevNode = newChildren[i - 1]
                            if (prevNode) {
                                const anchor = prevNode.el.nextSibling;
                                insert(newNode.el, container, anchor)
                            }
                        }
                        break
                    }
                }
                if (!exist) {
                    // 没找到复用节点, 需要新增
                    const prevNode = newChildren[i - 1]
                    let anchor = null
                    if (prevNode) {
                        anchor = prevNode.el.nextSibling
                    } else {
                        // 新挂载的是 第一个子节点
                        anchor = container.firstChild
                    }
                    patch(null, newNode, container, anchor)
                }
            }
        }
        // ..
    }
    // ..
}


function patch(n1, n2, container, anchor) {
    // ...
    const { type } = n2;
    if (typeof type === 'string') {
        if (!n1) {
            mountElement(n2, container, anchor)
        } else {
            patchElement(n1, n2)
        }
    }
}

function mountElement(vnode, container, anchor) {
    insert(vnode.el, container.anchor)
}

9.6. 移除不存在的元素

  • 遍历旧节点, 如果在新节点中找不到,就 unmount
function parentChildren(n1, n2, container) {
    // .. 

    for (let i = 0; i < oldChildren.length; i++) {
        const oldNode = oldChildren[i]
        const has = newChildren.find(a => a.key === oldNode.key)
        if (!has) {
            unmount(oldNode)
        }
    }
}

9.7. 总结

  • 使用 key 来标识是否为可复用节点
  • 遍历新的节点, 然后拿该新的节点去旧节点中匹配 key , 如果找到了,那么就是存在复用节点, 记录索引。 如果该索引小于记录过的最大索引, 说明该节点需要移动
  • 复用节点移动的位置, 是遍历的 newChildren 的前一个节点的
  • 如果找不到, 说明需要新增节点, 插入到前一个节点的后面。 如果没有前一个节点, 说明第一个节点是新增的, 插入到 parent.firstChild 的前面
  • 使用 el.nextSibling 作为 anchor , 然后 DOM 操作中调用 insertBefore 插入到指定位置
  • 最后遍历旧节点, 如果在新节点中找不到, 那么 unmount

10. 双端Diff算法

10.1. 双端diff比较的原理

  • 每次都比较4个节点:
    • 新开始 - 旧开始
    • 新结尾 - 旧结尾
    • 新结尾 - 旧开始
    • 新开始 - 旧结尾
  • 如果 key 不同,那么不做操作
  • 如果 key 相同,表示可复用
  • 对于可复用节点, 比较新旧位置, 对真实的 DOM el 就行 insert 操作
  • 比较完后, 对于可复用的节点,尾部的 index -1 , 头部的 index + 1

如下:

function patchKeyChildren(n1, n2, container) {
    const oldChildren = n1.children
    const newChildren = n2.children
    let newStartIdx = 0
    let oldStartIdx = 0
    let newEndIdx = newChildren.length - 1
    let oldEndIdx = oldChildren.length - 1
    while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
        const newStartVNode = newChildren[newStartIdx]
        const newEndVNode = newChildren[newEndIdx]
        const oldStartVNode = oldChildren[oldStartIdx]
        const oldEndVNode = oldChildren[oldEndIdx]
        if (newStartVNode.key === oldStartVNode.key) {
            patch(oldStartVNode, newStartVNode, container);
            newStartIdx++
            oldStartIdx++
        } else if (newEndVNode.key === oldEndVNode.key) {
            patch(oldEndVNode, newEndVNode, container);
            newEndIdx--
            oldEndIdx--
        } else if (newEndVNode.key === oldStartVNode.key) {
            patch(oldStartVNode, newEndVNode, container)
            insert(oldStartVNode.el, container, oldEndVNode.el.nextSibling)
            newEndIdx--
            oldStartIdx++
        } else if (newStartVNode.key === oldEndVNode.key) {
            patch(oldEndVNode, newStartVNode, container)
            insert(oldStartVNode.el, container, oldStartVNode.el)
            newStartIdx++
            oldEndIdx--
        }
    }
}

10.2. 双端diff比较的优势

  • 简单Diff算法 是双层嵌套循环
  • 双端Diff算法 是单层循环
  • 且理想情况下 双端Diff算法 需要移动的 DOM 会更少

10.3. 非理想状况的处理方式

假设上述算法, 4步比较之后, 都没有可复用的节点,那么进行下面操作

  • newStartVNode 的key 在 oldChildren 中未处理的节点中寻找相同的key
  • 找到之后, 进行 patch , 然后将该 oldVNodeel 移动到 oldStartVNode 的前面
  • oldChildren 的该位置 置空
  • newStartIdx + 1

如下:

function patchKeyChildren(n1, n2, container) {
    const oldChildren = n1.children
    const newChildren = n2.children
    let newStartIdx = 0
    let oldStartIdx = 0
    let newEndIdx = newChildren.length - 1
    let oldEndIdx = oldChildren.length - 1
    while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
        const newStartVNode = newChildren[newStartIdx]
        const newEndVNode = newChildren[newEndIdx]
        const oldStartVNode = oldChildren[oldStartIdx]
        const oldEndVNode = oldChildren[oldEndIdx]
        if (!oldStartVNode) {
            oldStartIdx++
        } else if (!oldEndVNode) {
            oldEndIdx--
        } else if (newStartVNode.key === oldStartVNode.key) {
            //
        } else if (newEndVNode.key === oldEndVNode.key) {
            //
        } else if (newEndVNode.key === oldStartVNode.key) {
            //
        } else if (newStartVNode.key === oldEndVNode.key) {
            //
        } else {
            for (let i = oldStartIdx + 1; i < oldEndIdx - 1; i++) {
                const oldVNode = oldChildren[i]
                if (oldVNode && oldVNode.key === newStartVNode.key) {
                    patch(oldVNode, newStartVNode, container)
                    insert(oldVNode.el, container, oldStartVNode.el)
                    oldChildren[i] = null
                    newStartIdx++
                    break
                }
            }
        }
    }
}

10.4. 添加新元素

如上在 oldChildren 找不到复用的节点, 说明该节点是新增的,应该如下处理:

  • 创建元素,将该元素 mount 到 oldStartVNode 前面
  • newStartIdx + 1
function patchKeyChildren(n1, n2, container) {
    const oldChildren = n1.children
    const newChildren = n2.children
    let newStartIdx = 0
    let oldStartIdx = 0
    let newEndIdx = newChildren.length - 1
    let oldEndIdx = oldChildren.length - 1
    while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
        const newStartVNode = newChildren[newStartIdx]
        const newEndVNode = newChildren[newEndIdx]
        const oldStartVNode = oldChildren[oldStartIdx]
        const oldEndVNode = oldChildren[oldEndIdx]
        if (!oldStartVNode) {
            oldStartIdx++
        } else if (!oldEndVNode) {
            oldEndIdx--
        } else if (newStartVNode.key === oldStartVNode.key) {
            //
        } else if (newEndVNode.key === oldEndVNode.key) {
            //
        } else if (newEndVNode.key === oldStartVNode.key) {
            //
        } else if (newStartVNode.key === oldEndVNode.key) {
            //
        } else {
            let finded = false
            for (let i = oldStartIdx + 1; i < oldEndIdx - 1; i++) {
                const oldVNode = oldChildren[i]
                if (oldVNode && oldVNode.key === newStartVNode.key) {
                    patch(oldVNode, newStartVNode, container)
                    insert(oldVNode.el, container, oldStartVNode.el)
                    oldChildren[i] = null
                    finded = true
                    break
                }
            }
            if (!finded) {
                patch(null, newStartVNode, container, oldStartVNode.el)
            }
            newStartIdx++
        }
    }
}

10.5. 移除不存在的元素

进行完上述操作后,将 oldChildren 中未复用的节点 unmount

根据算法逻辑, 对于 oldStartIdx 和 oldEndIdx, 如果进行了递增或递减操作, 说明之前的 oldVNode 是可复用的, 同时, 中间的节点如果可复用, 会被置空。 因此不存在的节点, 都位于 oldStartIdx 和 oldEndIdx 之间

function patchKeyChildren(n1, n2, container) {
    const oldChildren = n1.children
    const newChildren = n2.children
    let newStartIdx = 0
    let oldStartIdx = 0
    let newEndIdx = newChildren.length - 1
    let oldEndIdx = oldChildren.length - 1
    while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {
        const newStartVNode = newChildren[newStartIdx]
        const newEndVNode = newChildren[newEndIdx]
        const oldStartVNode = oldChildren[oldStartIdx]
        const oldEndVNode = oldChildren[oldEndIdx]
        if (!oldStartVNode) {
            oldStartIdx++
        } else if (!oldEndVNode) {
            oldEndIdx--
        } else if (newStartVNode.key === oldStartVNode.key) {
            //
        } else if (newEndVNode.key === oldEndVNode.key) {
            //
        } else if (newEndVNode.key === oldStartVNode.key) {
            //
        } else if (newStartVNode.key === oldEndVNode.key) {
            //
        } else {
            //
        }
    }
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
        unmount(oldChildren[i])
    }
}

10.6. 总结

进行双端diff算法,需处理以下步骤

  • 对于新旧 children 的两端进行两两比较,寻找可复用节点并对于移动
  • 处理两端可复用节点不在两端的情况
  • 处理新增元素的情况
  • unmount 不存在的元素

算法总览如下

function patchKeyChildren(n1, n2, container) {
    const oldChildren = n1.children
    const newChildren = n2.children
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldChildren.length - 1
    let newEndIdx = newChildren.length - 1
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        const oldStartVNode = oldChildren[oldStartIdx]
        const newStartVNode = newChildren[newStartIdx]
        const oldEndVNode = oldChildren[oldEndIdx]
        const newEndVNode = oldChildren[newEndIdx]
        if (!oldStartVNode) {
            oldStartIdx++
        } else if (!oldEndVNode) {
            oldEndIdx--
        } else if (newStartVNode.key === oldStartVNode.key) {
            patch(oldStartVNode, newStartVNode, container)
            oldStartIdx++
            newStartIdx++
        } else if (newEndVNode.key === oldEndVNode.key) {
            patch(oldEndVNode, newEndVNode, container)
            oldEndIdx--
            newEndIdx--
        } else if (newEndVNode.key === oldStartVNode.key) {
            patch(oldStartVNode, newEndVNode, container)
            insert(oldStartVNode.el, container, oldEndVNode.el.nextSilbing)
            newEndIdx--
            oldStartIdx++
        } else if (newStartVNode.key === oldEndVNode.key) {
            patch(oldEndVNode, newStartVNode, container)
            insert(oldEndVNode.el, container, oldStartVNode.el)
            newStartIdx++
            oldEndIdx--
        } else {
            let finded = false
            for (let i = oldStartIdx + 1; i < oldEndIdx; i++) {
                const oldVNode = oldChildren[i]
                if (oldVNode?.key === newStartVNode.key) {
                    patch(oldVNode, newStartVNode, container)
                    insert(oldVNode.el, container, oldStartVNode.el)
                    finded = true
                    break
                }
            }
            if (!finded) {
                patch(null, newStartVNode, container, oldStartVNode.el)
            }
            newStartIdx++
        }
    }
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
        unmount(oldChildren[i])
    }
}

11. 快速Diff算法

11.1. 相同的前置元素和后置元素

  • 对于新旧 children 两端 key 重叠的元素,可以不用进行移动操作
  • 处理完前置与后置后, 如果新 children 都处理完了, 说明 oldChildren 的剩余节点都是要被移除的
  • 处理完前置与后置后, 如果就 children 都处理完了, 说明 newChildren 的剩余节点都是要新增的
function patchKeyChildren(n1, n2, container) {
    const newChildren = n2.children
    const oldChildren = n1.children
    let j = 0;
    let newEnd = newChildren.length - 1;
    let oldEnd = oldChildren.length - 1;
    // 比较前置
    let newVNode = newChildren[j]
    let oldVNode = oldChildren[j]
    while (oldVNode && newVNode && newVNode.key === oldVNode.key) {
        patch(oldVNode, newVNode, container)
        j++
        newVNode = newChildren[j]
        oldVNode = oldChildren[j]
    }
    // 比较后置
    newVNode = newChildren[newEnd]
    oldVNode = oldChildren[oldEnd]
    while (newEnd >= j && oldEnd >= j && newVNode.key === oldVNode.key) {
        patch(oldVNode, newVNode, container)
        newEnd--
        oldEnd--
        newVNode = newChildren[newEnd]
        oldVNode = oldChildren[oldEnd]
    }
    if (j > newEnd) {
        // newChildren 处理完毕, oldChildren 有多出来的节点
        while (j <= oldEnd) {
            unmount(oldChildren[j++])
        }
    } else if (j > oldEnd) {
        // oldChilren 处理完毕, newChildren 有多出来的节点
        const anchor = newChildren[newEnd + 1]?.el
        while (j <= newEnd) {
            patch(null, newChildren[j++], container, anchor)
        }
    } else {
        // 两者之间都未处理的节点
    }
}

11.2. 判断是否需要进行DOM移动操作

对于上述尾部 else 的有以下特征:

  • 满足 j <​= newEnd= 且 j <​= oldEnd=
  • 新旧 children 中间的节点都还没有处理

处理方式是:

  • 构建一个 source 数组, 索引为新节点相对于 j 的位置, 值是对应复用的旧节点的位置
  • 如果没有复用节点,那么值为 -1
  • 如果都是 -1 , 说明不需要移动, 都是新增的节点
function patchKeyChildren(n1, n2, container) {
    const newChildren = n2.children
    const oldChildren = n1.children
    let j = 0;
    let newEnd = newChildren.length - 1;
    let oldEnd = oldChildren.length - 1;
    // 比较前置
    // 比较后置
    if (j > newEnd) {
        // newChildren 处理完毕, oldChildren 有多出来的节点
    } else if (j > oldEnd) {
        // oldChilren 处理完毕, newChildren 有多出来的节点
    } else {
        // 两者之间都未处理的节点
        const count = newEnd - j + 1
        const source = new Array(count)
        source.fill(-1)
        const newKeyMap = {}
        for (let i = j; i <= newEnd; i++) {
            newKeyMap[newChildren[i].key] = i
        }
        let lastIdx = j
        let shouldMoved = false
        let patched = 0
        for (let i = j; i <= oldEnd; i++) {
            const oldVNode = oldChildren[i]
            if (patched >= count) {
                // 剩下的肯定不存在于 newChildren上
                unmount(oldVNode)
                continue
            }
            const newIdx = newKeyMap[oldVNode.key]
            if (typeof newIdx !== 'undefined') {
                source[newIdx - j] = i
                patch(oldVNode, newChildren[newIdx], container)
                patched++
                if (newIdx < lastIdx) {
                    // 不是递增趋势
                    shouldMoved = true
                } else {
                    lastIdx = newIdx
                }
            } else {
                // 不在新节点上
                unmount(oldVNode)
            }
        }

        if (shouldMoved) {
            // 进行移动操作
        }
    }
}

11.3. 如何移动元素

上述的还遗留实际需要 move 的情景,进行以下处理:

  • 构建 source 的最长子序列索引 seq (这个序列的节点都不需要移动)
  • seqsource 的尾部开始进行比对
    • 如果两者的尾部不重合(即 seq 的值 不是 source 处理过的的最后一个索引),则将 source 对应的该节点插入到尾部前面
    • 如果重合, 两者都缩短 1 , 继续比对
    • 其目的是, 移动或新增不在 seq 的节点,此时需要移动的节点数最少
function patchKeyChildren(n1, n2, container) {
    const newChildren = n2.children
    const oldChildren = n1.children
    let j = 0;
    let newEnd = newChildren.length - 1;
    let oldEnd = oldChildren.length - 1;
    // 比较前置
    // 比较后置
    if (j > newEnd) {
        // newChildren 处理完毕, oldChildren 有多出来的节点
    } else if (j > oldEnd) {
        // oldChilren 处理完毕, newChildren 有多出来的节点
    } else {
        // 两者之间都未处理的节点
        const count = newEnd - j + 1
        const source = new Array(count)
        source.fill(-1)
        const newKeyMap = {}
        for (let i = j; i <= newEnd; i++) {
            newKeyMap[newChildren[i].key] = i
        }
        let lastIdx = j
        let shouldMoved = false
        let patched = 0
        for (let i = j; i <= oldEnd; i++) {
            const oldVNode = oldChildren[i]
            if (patched >= count) {
                // 剩下的肯定不存在于 newChildren上
                unmount(oldVNode)
                continue
            }
            const newIdx = newKeyMap[oldVNode.key]
            if (typeof newIdx !== 'undefined') {
                source[newIdx - j] = i
                patch(oldVNode, newChildren[newIdx], container)
                patched++
                if (newIdx < lastIdx) {
                    // 不是递增趋势
                    shouldMoved = true
                } else {
                    lastIdx = newIdx
                }
            } else {
                // 不在新节点上
                unmount(oldVNode)
            }
        }

        if (shouldMoved) {
            // 进行移动操作
            let seq = getSequence(source)
            let handledIdx = source.length - 1;
            let sEnd = seq.length - 1
            for (; handledIdx >= 0; handledIdx--) {
                const anchor = newChildren[handledIdx + j + 1]?.el
                const newVNode = newChildren[handledIdx + j]
                if (source[handledIdx] === -1) {
                    patch(null, newVNode, container, anchor)
                } else if (seq[sEnd] === handledIdx) {
                    // 重合部分
                    sEnd--
                } else {
                    insert(newVNode.el, container, anchor)
                }
            }
        }
    }
}


11.4. 总结

核心逻辑

  • 处理新旧两端重合的部分, 该部分不用移动
  • 如果第一步处理后, newChildren 都处理完了, 那么剩余的 oldChildren 是需要删除的
  • 如果第一步处理后, oldChildren 都处理完了, 那么剩余的 newChildren 都是新增的节点
  • 接下来比较两者中间的部分
    • 构建 source 用来保存 newChildren 中在 oldChildren 可寻找到的复用的节点的索引
    • 移除 oldChildren 中不再 source 的节点
    • 如果 source 的值不是递增趋势, 说明有节点需要移动
    • 移动节点时进行下面操作
      • 构建最大递增子序列 seq , 该序列的节点不需要进行移动
      • 从尾部比较 seq 的值和 source 尾部索引, 如果重合就 -1, 不重合,那么将该 索引对应的节点插到尾部
function patchKeyChildren(n1, n2, container) {
    const oldChildren = n1.children
    const newChildren = n2.children
    let j = 0;
    let newEnd = newChildren.length - 1;
    let oldEnd = oldChildren.length - 1;
    // 比较前置
    let newVNode = newChildren[j]
    let oldVNode = oldChildren[j]
    while (newVNode.key === oldVNode.key) {
        patch(oldVNode, newVNode, container)
        j++
        newVNode = newChildren[j]
        oldVNode = oldChildren[j]
    }
    // 比较后置
    newVNode = newChildren[newEnd]
    oldVNode = oldChildren[oldEnd]
    while (newVNode.key === oldVNode.key) {
        patch(oldVNode, newVNode, container)
        newEnd--
        oldEnd--
        newVNode = newChildren[newEnd]
        oldVNode = oldChildren[oldEnd]
    }
    if (j > oldEnd) {
        // oldChilren 处理完
        while (j <= newEnd) {
            patch(null, newChildren[j++], container, newChildren[newEnd + 1]?.el)
        }
    } else if (j > newEnd) {
        // newChildren 处理完
        while (j <= oldEnd) {
            unmount(oldChildren[j++])
        }
    } else {
        const newStart = j
        const oldStart = j
        const count = newEnd - newStart + 1
        const source = new Array(count)
        source.fill(-1)

        const newKeyMap = {}
        for (let i = newStart; i <= newEnd; i++) {
            newKeyMap[newChildren[i].key] = i
        }
        let lastIdx = 0
        let moved = false
        let patched = 0
        for (let i = oldStart; i <= oldEnd; i++) {
            const oldVNode = oldChildren[i]
            if (patched >= count) {
                unmount(oldVNode)
                continue
            }
            const k = newKeyMap[oldVNode.key]
            if (typeof k === 'undefined') {
                unmount(oldVNode)
            } else {
                source[k - newStart] = i
                patched++
                patch(oldVNode, newChildren[k], container)
                if (i < lastIdx) {
                    moved = true
                } else {
                    lastIdx = i
                }
            }
        }
        if (moved) {
            const seq = getSequence(source)
            let sEnd = seq.length - 1
            for (let i = source.length - 1; i >= 0; i--) {
                const pos = i + newStart
                const anchor = newChildren[pos + 1]?.el
                const newVNode = newChildren[pos]
                if (source[i] === -1) {
                    patch(null, newVNode, container, anchor)
                } else if (source[i] === seq[sEnd]) {
                    // 重合
                    sEnd--
                } else {
                    insert(newVNode.el, container, anchor)
                }
            }
        }
    }
}

12. TODO 组件的实现原理

13. TODO 异步组件与函数式组件

14. TODO 内建组件和模块

15. TODO 编译器核心技术概览

16. TODO 解析器

17. TODO 编译优化

18. TODO 同构渲染

日期: 2022-09-09

Validate