读书笔记——Vue.js设计与实现

首先,向vue.js霍春阳前辈献上感谢。博客内容主要总结于“Vue.js设计与实现”,代码块实现来自我的仿写,可能存在bug。欢迎大家一起讨论学习,博客内容持续更新。

第1章 权衡的艺术

  1. 框架设计里到处都体现了权衡的艺术。
  2. 命令式框架关注过程(例如,jQuery )。
  3. 声明式框架关注结果(例如, Vue.js)。
  4. Vue.js对过程进行了封装,内部自身实现是命令式的,外部用户使用是声明式的
  5. 声明式代码的性能不优于命令式代码的性能(即,声明式 <= 命令式)。
  6. 命令式代码的更新性能消耗 = 直接修改的性能消耗
  7. 声明式代码的更新性能消耗 = 直接修改的性能消耗 + 找出差异的性能消耗
  8. 要做出的关于可维护性性能之间的权衡,在保持可维护性的同时让性能损失最小化。
  9. 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,因此,如果我们能够最小化找出差异的性能消耗,就可以让声明式代码的性能无限接近命令式代码的性能。而所谓的虚拟DOM,就是为了最小化找出差异这一步的性能消耗而出现的。
  10. 我们很难写出绝对优化的命令式代码,尤其是当应用程序的规模很大的时候,即使你写出了极致优化的代码,也一定耗费了巨大的精力,这时的投入产出比其实并不高。
  11. 编译时,可以分析用户提供的内容,看看哪些内容未来可能会改变,哪些内容永远不会改变,这样我们就可以在编译的时候提取这些信息,然后将其传递给 Render 函数,Render 函数得到这些信息之后,就可以做进一步的优化了。
  12. 运行时,更加灵活。
  13. Vue.js 3 仍然保持了运行时 + 编译时的架构,在保持灵活性的基础上能够尽可能地去优化。

第2章 框架设计的核心要素

  1. 提供友好的警告信息至关重要。
  2. 开发工具插件支持。
  3. 控制框架代码的体积,有一些代码需要出现在开发阶段,不需要出现在生产阶段(例如,警告提示)。
  4. 框架要做到良好的 Tree-Shaking。Tree-Shaking 是一种排除 dead code 的机制,框架中会内建多种能力,例如 Vue.js 内建的组件等。对于用户可能用不到的能力,我们可以利用 Tree-Shaking 机制使最终打包的代码体积最小化。另外,Tree-Shaking 本身基于 ESM,并且 JavaScript 是一门动态语言,通过纯静态分析的手段进行 Tree-Shaking 难度较大,因此大部分工具能够识别 /*#__PURE__*/ 注释,在编写框架代码时,我们可以利用/*#__PURE__*/ 来辅助构建工具进行 Tree-Shaking。
  5. 针对不同的需求场景,提供不同的产物。
  6. 特性开关。有时用户明确知道自己仅会使用组合式 API,而不会使用选项对象式 API,这时用户可以通过特性开关关闭对应的特性,这样在打包的时候,用于实现关闭功能的代码将会被Tree-Shaking 机制排除。
  7. 用户自定义统一的异常处理。框架的错误处理做得好坏直接决定了用户应用程序的健壮性,同时还决定了用户开发应用时处理错误的心智负担。框架需要为用户提供统一的错误处理接口,这样用户可以通过注册自定义的错误处理函数来处理全部的框架异常。
  8. TypeScript的良好支持。

第3章 Vue.js 3 的设计思路

  1. Vue.js 3 是一个声明式的 UI 框架,意思是说用户在使用 Vue.js 3 开发页面时是声明式地描述 UI 的。

  2. 现在我们已经了解了什么是虚拟 DOM,它其实就是用 JavaScript 对象来描述真实的DOM 结构。

  3. 渲染器的作用就是把虚拟 DOM 渲染为真实 DOM

  4. 实现一个简单的渲染器:

    // 函数声明
    function render(vnode, root) {
        const el = document.createElement(vnode.tag)
        for (const key in vnode.props) {
            if (Object.hasOwnProperty.call(vnode.props, key)) {
                const element = vnode.props[key]
                if (key.startsWith("on")) {
                    el.addEventListener(key.match(/^on([a-zA-Z]+)$/)[1].toLocaleLowerCase(), element)
                }
                el[key] = element
            }
        }
        if (Array.isArray(vnode.children)) {
            vnode.children.forEach(element => {
                render(element, el)
            })
        } else {
            const textNode = document.createTextNode(vnode.children)
            el.appendChild(textNode)
        }
        root.appendChild(el)
    }
    function init() {
        const vnode = {
            tag: "div",
            props: {
                style: "color: green"
            },
            children: [
                {
                    tag: "h1",
                    children: "看下面"
                },
                {
                    tag: "span",
                    props: {
                        onClick: () => alert("出现弹框"),
                        style: "cursor: pointer"
                    },
                    children: "点我试试"
                }
            ]
        }
        const bodyDom = document.querySelector("body")
        render(vnode, bodyDom)
    }
    
    // 初始化应用
    init()
    
  5. 一句话总结:组件就是一组 DOM 元素的封装。

  6. 组件要渲染的内容,就是虚拟 DOM。组件的返回值也是虚拟 DOM,它代表组件要渲染的内容。

  7. 我们可以使用一个对象来代表组件,该对象有一个函数,叫作 render,其返回值代表组件要渲染的内容,也就是虚拟 DOM

  8. 我们可以使用一个函数来代表组件,函数返回值是虚拟 DOM,即组件要渲染的内容

  9. 无论是手写虚拟 DOM(渲染函数)还是使用模板,都属于声明式地描述 UI,并且Vue.js 同时支持这两种描述 UI 的方式。

  10. 编译器的作用其实就是将模板编译为渲染函数。对于编译器来说,模板就是一个普通的字符串,它会分析该字符串并生成一个功能与之相同的渲染函数。

  11. 所以,无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM渲染为真实DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程。

  12. 渲染函数的返回值是虚拟 DOM渲染函数的生成虚拟 DOM

  13. 我们了解到编译器和渲染器之间是存在信息交流的,它们互相配合使得性能进一步提升,而它们之间交流的媒介就是虚拟 DOM 对象。

  14. Vue.js 采用模板的方式来描述 UI,但它同样支持使用虚拟 DOM (渲染函数)来描述 UI。虚拟 DOM 要比模板更加灵活,但模板要比虚拟 DOM 更加直观。

  15. 渲染器的作用是,把虚拟 DOM 对象渲染为真实 DOM 元素。它的工作原理是,递归地遍历虚拟 DOM 对象,并调用原生 DOM API 来完成真实 DOM 的创建。渲染器的精髓在于后续的更新,它会通过 Diff 算法找出变更点,并且只会更新需要更新的内容。

  16. 组件其实就是一组虚拟 DOM 元素的封装,它可以是一个返回虚拟 DOM 的函数,也可以是一个对象,但这个对象下必须要有一个函数用来产出组件要渲染的虚拟 DOM。

第4章 响应系统的作用与实现

  1. 副作用函数指的是会产生副作用的函数。副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用。
  2. effect 函数的执行会直接或间接影响其他函数的执行,这时我们说 effect 函数产生了副作用。
  3. const obj = { text: 'hello world' }
    function effect() {
      document.body.innerText = obj.text
    }
    
    观察上方代码块,我们可以发现两点线索:
    (1)当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作。
    (2)当修改 obj.text 的值时,会触发字段 obj.text 的设置操作。
  4. 如果我们能拦截一个对象的读取设置操作,事情就变得简单了。当读取字段 obj.text 时,我们可以把副作用函数 effect 存储到一个“桶”里。当设置 obj.text 时,再把副作用函数 effect 从“桶”里取出并执行。
  5. 在 ES2015之前,只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象 Proxy 来实现,这也是 Vue.js 3 所采用的方式。
  6. 一个响应系统的工作流程如下:
    (1)当读取操作发生时,将副作用函数收集到“桶”中。
    (2)当设置操作发生时,从“桶”中取出副作用函数并执行。
  7. “桶”的数据结构,使用了 WeakMap、Map 和 Set :
    (1)WeakMap 由 target --> Map 构成。
    (2)Map 由 key --> Set 构成。
    其中 WeakMap 的键是原始对象 target,WeakMap 的值是一个 Map 实例,而 Map 的键是原始对象 target 的 key,Map 的值是一个由副作用函数组成的 Set。
  8. Set 数据结构所存储的副作用函数集合称为 key 的依赖集合。
  9. WeakMap 对 key 是弱引用不影响垃圾回收器的工作。所以 WeakMap 经常用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息,例如上面的场景中,如果 target 对象没有任何引用了,说明用户侧不再需要它了,这时垃圾回收器会完成回收任务。但如果使用 Map 来代替 WeakMap,那么即使用户侧的代码对 target 没有任何引用,这个 target 也不会被回收,最终可能导致内存溢出。
  10. // 设计一个完善的响应系统
    // 全局常量变量声明及初始化
    let activeEffect = null
    const bucket = new WeakMap()
    
    // 函数声明
    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) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        effects && effects.forEach(fn => fn())
    }
    function effect(fn) {
        activeEffect = fn
        fn()
    }
    function getProxy(data) {
        return new Proxy(data, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
    }
    function init() {
        const proxyData = getProxy({ text: "hello world." })
        effect(() => {
            document.body.innerText = proxyData.text
        })
        setTimeout(() => {
            proxyData.text = "你好世界。"
        }, 3000)
    }
    
    // 初始化应用
    init()
    
  11. 分支切换与 cleanup ,分支切换可能会产生遗留的副作用函数,遗留的副作用函数会导致不必要的更新。解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。
  12. 当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用函数。
  13. 语言规范:在调用 forEach 遍历 Set 集合时,如果一个值已经被访问过了,但该值被删除并重新添加到集合,如果此时 forEach 遍历没有结束,那么该值会重新被访问。
  14. // 分支切换与cleanup
    // 全局常量变量声明及初始化
    let activeEffect = null
    const bucket = new WeakMap()
    
    // 函数声明
    function cleanup(effectFn) {
        for (let i = 0; i < effectFn.deps.length; i++) {
            const deps = effectFn.deps[i]
            deps.delete(effectFn)
        }
        effectFn.deps.length = 0
    }
    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) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const effectsToRun = new Set(effects)
        effectsToRun.forEach(effectFn => effectFn())
    }
    function effect(fn) {
        const effectFn = () => {
            cleanup(effectFn)
            activeEffect = effectFn
            fn()
        }
        effectFn.deps = []
        effectFn()
    }
    function getProxy(data) {
        return new Proxy(data, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
    }
    function init() {
        const proxyData = getProxy({ text: "hello world." })
        effect(() => {
            document.body.innerText = proxyData.text
        })
        setTimeout(() => {
            proxyData.text = "你好世界。"
        }, 3000)
    }
    
    // 初始化应用
    init()
    
  15. // 嵌套的effect与effect栈,避免无限递归循环(例如,n++)
    // 全局常量变量声明及初始化
    let activeEffect = null
    const effectStack = []
    const bucket = new WeakMap()
    
    // 函数声明
    function cleanup(effectFn) {
        for (let i = 0; i < effectFn.deps.length; i++) {
            const deps = effectFn.deps[i]
            deps.delete(effectFn)
        }
        effectFn.deps.length = 0
    }
    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) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const effectsToRun = new Set()
        effects && effects.forEach(effectFn => {
            if (effectFn !== activeEffect) {
                effectsToRun.add(effectFn)
            }
        })
        effectsToRun.forEach(effectFn => effectFn())
    }
    function effect(fn) {
        const effectFn = () => {
            cleanup(effectFn)
            activeEffect = effectFn
            effectStack.push(effectFn)
            fn()
            effectStack.pop()
            activeEffect = effectStack[effectStack.length - 1]
        }
        effectFn.deps = []
        effectFn()
    }
    function getProxy(data) {
        return new Proxy(data, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
    }
    function init() {
        const proxyData = getProxy({ foo: 1, bar: 1 })
        effect(function effectFn1() {
            console.log('effectFn1 执行')
            effect(function effectFn2() {
                console.log('effectFn2 执行')
                proxyData.bar
            })
            proxyData.foo
        })
        setTimeout(() => {
            proxyData.foo++;
        }, 1000)
        setTimeout(() => {
            proxyData.bar++;
        }, 3000)
    }
    
    // 初始化应用
    init()
    
  16. 所谓可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式
  17. // 005 第四章完结
    // 全局常量变量声明及初始化
    let activeEffect = null
    const effectStack = []
    const bucket = new WeakMap()
    const jobQueue = new Set()
    const p1 = Promise.resolve()
    let isFlushing = false
    
    // 函数声明
    function getProxy(data) {
        return new Proxy(data, {
            get(target, key) {
                track(target, key)
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal
                trigger(target, key)
            }
        })
    }
    function effect(fn, options = {}) {
        const effectFn = () => {
            cleanup(effectFn)
            activeEffect = effectFn
            effectStack.push(effectFn)
            const res = fn()
            effectStack.pop()
            activeEffect = effectStack[effectStack.length - 1]
            return res
        }
        effectFn.options = options
        effectFn.deps = []
        if (!options.lazy) {
            effectFn()
        }
        return effectFn
    }
    function effectEliminateTransition(getter) {
        effect(() => {
            getter()
        }, {
            scheduler(fn) {
                jobQueue.add(fn)
                flushJob()
            }
        })
    }
    function computed(getter) {
        let value
        let dirty = true
        const effectFn = effect(getter, {
            lazy: true,
            scheduler() {
                if (!dirty) {
                    dirty = true
                    trigger(obj, 'value')
                }
            }
        })
        const obj = {
            get value() {
                if (dirty) {
                    value = effectFn()
                    dirty = false
                }
                track(obj, 'value')
                return value
            }
        }
        return obj
    }
    function watch(source, cb, options = {}) {
        let getter
        if (typeof source === 'function') {
            getter = source
        } else {
            getter = () => traverse(source)
        }
        let oldValue, newValue
        let cleanup
        function onInvalidate(fn) {
            cleanup = fn
        }
        const job = () => {
            newValue = effectFn()
            if (cleanup) {
                cleanup()
            }
            cb(newValue, oldValue, onInvalidate)
            oldValue = newValue
        }
        const effectFn = effect(
            () => getter(),
            {
                lazy: true,
                scheduler: () => {
                    if (options.flush === 'post') {
                        const p2 = Promise.resolve()
                        p2.then(job)
                    } else {
                        job()
                    }
                }
            }
        )
        if (options.immediate) {
            job()
        } else {
            oldValue = effectFn()
        }
    }
    function cleanup(effectFn) {
        for (let i = 0; i < effectFn.deps.length; i++) {
            const deps = effectFn.deps[i]
            deps.delete(effectFn)
        }
        effectFn.deps.length = 0
    }
    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) {
        const depsMap = bucket.get(target)
        if (!depsMap) return
        const effects = depsMap.get(key)
        const effectsToRun = new Set()
        effects && effects.forEach(effectFn => {
            if (effectFn !== activeEffect) {
                effectsToRun.add(effectFn)
            }
        })
        effectsToRun.forEach(effectFn => {
            if (effectFn.options.scheduler) {
                effectFn.options.scheduler(effectFn)
            } else {
                effectFn()
            }
        })
    }
    function flushJob() {
        if (isFlushing) return
        isFlushing = true
        p1.then(() => {
            jobQueue.forEach(job => job())
        }).finally(() => {
            isFlushing = false
        })
    }
    function traverse(value, seen = new Set()) {
        if (typeof value !== 'object' || value === null || seen.has(value)) return
        seen.add(value)
        for (const k in value) {
            traverse(value[k], seen)
        }
        return value
    }
    function init() {
        const proxyData = getProxy({ foo: 1, bar: 2 })
        watch(proxyData, () => {
            console.log("proxyData变化了")
        }, { immediate: true })
        setTimeout(() => {
            proxyData.foo++
        }, 2000);
    }
    
    // 初始化应用
    init()
    
  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值