阅读视图

发现新文章,点击刷新页面。

函数柯里化(curry)是什么?🤔

什么是函数柯里化?

函数柯里化是一种将多参数函数转换为一系列单参数函数的技术。简单来说,柯里化后的函数不会立即求值,而是每次接受一个参数,并返回一个新函数来接收剩余参数,直到所有参数都被提供,最终返回结果。

基本示例

让我们通过一个简单的例子来理解柯里化:

// 普通函数
function add(a, b, c) {
    return a + b + c;
}

// 柯里化版本
function curriedAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}

// 使用方式对比
console.log(add(1, 2, 3)); // 6
console.log(curriedAdd(1)(2)(3)); // 6

实现通用的柯里化函数

手动为每个函数编写柯里化版本显然不现实,我们可以创建一个通用的柯里化工具函数: 思路就是创建一个自动柯里化函数可以接收一个函数作为参数,然后返回一个它的柯里化后的函数。

//自动柯里化函数,接收一个函数的参数
const autoCurryFn = function(fn){
    //边界判断
    //是否是函数
    if(typeof fn !== 'function'){
        throw new Error('传进来的参数必须是一个函数')
    }

    //返回一个新的函数,接收新的参数,这里用gras剩余参数收集
    return function curryFn(...args){
        //如果收集的参数个数少于原fn函数的参数个数,则返回这个新函数继续收集
        if(args.length < fn.length){
            return function(...newGras){
                return curryFn(...args,...newGrgs)
            }
        }else{
            //如果收集的参数大于等于原函数的参数就可以执行原函数,并返回对应结果
            return fn(...args)
        }
    }
}

柯里化的实际应用场景

1. 参数复用

柯里化非常适合创建可复用的函数模板:

// 创建特定前缀的日志函数
function createLogger(prefix) {
    return function(message) {
        console.log('[' + prefix + '] ' + message);
    };
}

const infoLogger = createLogger('INFO');
const errorLogger = createLogger('ERROR');

infoLogger('系统启动成功'); // [INFO] 系统启动成功
errorLogger('数据库连接失败'); // [ERROR] 数据库连接失败

2. 延迟执行

柯里化允许我们分步提供参数,这在事件处理等场景中特别有用:

// 事件处理器工厂
function createEventHandler(eventType, element) {
    return function(handler) {
        element.addEventListener(eventType, handler);
    };
}

// 为特定元素创建点击事件处理器
const createClickHandler = createEventHandler('click', document.getElementById('myButton'));

// 稍后添加具体的处理逻辑
createClickHandler(function(event) {
    console.log('按钮被点击了!');
});

总结

函数柯里化其实就是将多参数函数转换为单参数函数序列,为我们提供了更灵活的函数组合方式和更高的代码复用性。

《手撕类Vue2的响应式核心思想:我的学习心路历程》

一、前言:什么是响应式?

响应式的核心思想很简单:当数据发生变化时,所有依赖于这个数据的函数(例如视图渲染函数、计算函数等)会自动重新执行,从而保持数据与依赖它的事物同步。在Vue2中,这是通过Object.defineProperty实现的。今天,我们就顺着这个思路,用原生JavaScript手写一个简易的响应式Demo。

实例效果.gif

二、基础版本:手动触发更新

我们先实现一个最基础的版本。点击按钮,让obj.age增加,并同步更新页面。

<div id="age">18</div>
<button onclick="changeObj()">年龄++</button>
// 1. 定义数据对象
const obj = {
    age: 18
};

// 2. 获取DOM元素
const ageEl = document.querySelector('#age');

// 3. 定义更新DOM的函数
const updateDom = function() {
    ageEl.textContent = obj.age.toString();
    console.log('页面已更新: ', ageEl.textContent);
}

// 4. 定义改变数据的函数
const changeObj = () => {
    obj.age++; // 修改数据
    updateDom(); // 手动更新DOM
}

效果: 点击按钮,年龄增加,页面显示同步更新。

image.png

这确实复现了“响应式”的效果,但存在一个明显问题:我们需要手动调用updateDom。如果依赖obj.age的函数很多,比如有dependFn1dependFn2dependFn3,那么changeObj函数会变得非常冗长和难以维护。

const updateDom = function() {
    age.textContent = obj.age.toString()
    console.log(age.textContent);
}

const dependFn1 = ()=>{
    console.log(obj.age);
}

const dependFn2 = ()=>{
    console.log(obj.age);
}

const dependFn3 = ()=>{
    console.log(obj.age);
}

const changeObj = ()=>{
    console.log(obj.age);
    obj.age++
    updateDom()//更新dom
    dependFn1()
    dependFn2()
    dependFn3()
}

image.png

三、优化一:统一管理依赖函数

为了解决上述问题,我们可以把所有依赖函数收集到一个数组中,当数据变化时,统一遍历执行。

//创建依赖函数数组
let fnArrays = []

//创建触发依赖函数的函数
const dispatchFns = ()=>{
  fnArrays.forEach(fn => fn())
}

//收集依赖函数的函数
const gatherFns = (fn)=>{
  fnArrays.push(fn)
}

gatherFns(dependFn1)
gatherFns(dependFn2)
gatherFns(dependFn3)

const changeObj = ()=>{
  console.log(obj.age);
  obj.age++
  updateDom()//更新dom
  dispatchFns()
}

这样,我们只需要调用一次dispatchFns(),所有依赖函数都会执行。但这种方式依然不够灵活,如果多个数据对象拥有各自的依赖函数,管理起来会非常混乱。

image.png

四、优化二:引入Depend类

更好的做法是为每个响应式数据创建一个独立的依赖管理器。我们定义一个Depend类来专门负责收集和触发依赖函数。

//这时候得创建一个Depend(依赖)类
  class Depend {
      constructor(){
          this.dependFns = new Set()
      }

      //收集依赖函数
      gatherFns(fn){
          if(fn && typeof fn === 'function'){
              this.dependFns.add(fn)
          }
      }

      //统一调用依赖函数
      dispatchFns(){
          this.dependFns.forEach(fn => {
              fn()
          })
      }
  }

  const depend1 = new Depend()

  depend1.gatherFns(dependFn1)
  depend1.gatherFns(dependFn2)
  depend1.gatherFns(dependFn3)

  const changeObj = ()=>{
      console.log(obj.age);
      obj.age++
      updateDom()//更新dom
      depend1.dispatchFns()
  }

image.png

现在,每个响应式数据都有自己的Depend实例,依赖管理更加清晰。但我们仍然需要在修改数据后手动调用dispatchFns()

五、核心实现:自动依赖收集和触发

要实现真正的自动响应式,我们需要在获取数据时自动收集依赖,在修改数据时自动触发更新。这可以通过Object.defineProperty拦截数据的读取和修改操作来实现。

//创建一个myReactive的函数,接收一个对象,进行对它的属性劫持
function myReactive(obj){
    Object.keys(obj).forEach(key => {
        const instance = new Depend() //创建一个实例对象
        let value = obj[key] //获取对应属性的值,保留等下要用

        //加上属性劫持
        Object.defineProperty(obj,key,{
            get : function(){
                return value
            },
            set : function(newValue){
                value = newValue
                //通知对应的实例执行所有依赖函数
                instance.dispatchFns()
            }
        })
    })
}

image.png

现在是解决了对象属性改变时,可以劫持使用set方法进行调用依赖函数,但是收集函数就成了一个难题,因为实例是在内部创建的,形成了闭包,外界拿不到内部的实例,所以只有一个方法了,就是设置一个全局活跃函数,当调用依赖函数时,会触发获取对象属性,触发get方法,就可以在get方法调用内部的instance可以获取外部的全局活跃函数,将其加入实例中的Set数组中!

关键思路:设置"全局活跃函数"

我们需要一个方法来标记当前正在执行的函数,这样在读取数据时,就能知道是哪个函数依赖了这个数据。

//设置活跃函数
let activeFn = null
//加入依赖的函数
const addDependFn = (fn)=>{
    activeFn = fn
    fn()
    activeFn = null
}

addDependFn(dependFn1)
addDependFn(dependFn2)
addDependFn(dependFn3)

//创建一个myReactive的函数,接收一个对象,进行对它的属性劫持
function myReactive(obj){
    Object.keys(obj).forEach(key => {
        const instance = new Depend() //创建一个实例对象
        let value = obj[key] //获取对应属性的值,保留等下要用

        //加上属性劫持
        Object.defineProperty(obj,key,{
            get : function(){
                instance.gatherFns(activeFn)
                return value
            },
            set : function(newValue){
                value = newValue
                //通知对应的实例执行所有依赖函数
                instance.dispatchFns()
            }
        })
    })
}

image.png

其实到这里就已经实现了简单的响应式啦!哈哈哈, 完整demo代码如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手撕类Vue2响应式demo</title>
</head>
<body>
    <div id="age">18</div>

    <button onclick="changeObj()">年龄++</button>
    <script>
        // obj对象
        const obj = {
            age : 18
        }

        //使用原生拿到元素
        const age = document.querySelector('#age')

        const updateDom = function() {
            age.textContent = obj.age.toString()
            console.log(age.textContent);
        }

        //模拟依赖函数1
        const dependFn1 = ()=>{
            console.log(obj.age);
        }

        //模拟依赖函数2
        const dependFn2 = ()=>{
            console.log(obj.age);
        }

        //模拟依赖函数3
        const dependFn3 = ()=>{
            console.log(obj.age);
        }

        //这时候得创建一个类
        class Depend {
            constructor(){
                this.dependFns = new Set()
            }

            //收集依赖函数
            gatherFns(fn){
                if(fn && typeof fn === 'function'){
                    this.dependFns.add(fn)
                }
            }

            //统一调用依赖函数
            dispatchFns(){
                this.dependFns.forEach(fn => {
                    fn()
                })
            }
        }

        const changeObj = ()=>{
            console.log(obj.age);
            obj.age++
        }

        //设置活跃函数
        let activeFn = null
        //加入依赖的函数
        const addDependFn = (fn)=>{
            activeFn = fn
            fn()
            activeFn = null
        }

        //手动执行一遍函数
        addDependFn(dependFn1)
        addDependFn(dependFn2)
        addDependFn(dependFn3)

        //创建一个myReactive的函数,接收一个对象,进行对它的属性劫持
        function myReactive(obj){
            Object.keys(obj).forEach(key => {
                const instance = new Depend() //创建一个实例对象
                let value = obj[key] //获取对应属性的值,保留等下要用

                //加上属性劫持
                Object.defineProperty(obj,key,{
                    get : function(){
                        instance.gatherFns(activeFn)
                        return value
                    },
                    set : function(newValue){
                        value = newValue
                        //通知对应的实例执行所有依赖函数
                        instance.dispatchFns()
                    }
                })
            })
        }

        myReactive(obj) //传入对象,添加劫持
        addDependFn(updateDom) //添加活跃函数
    </script>
</body>
</html>

六、最终优化

但是到现在,其实它还有很多可以优化的地方,比如可能一个对象多次传入myReactive函数,会创建多个实例,还有实例是在myReactive函数里面创建的,属于闭包,可能出现内存泄漏,所以我们可以采用WeakMap集合来收集对象,这样子的话,就可以在myReactive函数开始那里判断一下对象是否创建过。然后每一个对象对应着一个(属性-实例)Map表,这样子的话,就可以方便的拿到实例了,就不会有强引用来泄漏内存了。

这里是优化,就不赘述了,直接上代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>手撕类Vue2响应式demo</title>
</head>
<body>
    <div id="age">18</div>

    <button onclick="changeObj()">年龄++</button>
    <script>
        // obj对象
        const obj = {
            age : 18
        }

        //使用原生拿到元素
        const age = document.querySelector('#age')

        const updateDom = function() {
            age.textContent = obj.age.toString()
            console.log(age.textContent);
        }

        //模拟依赖函数1
        const dependFn1 = ()=>{
            console.log(obj.age);
        }

        //模拟依赖函数2
        const dependFn2 = ()=>{
            console.log(obj.age);
        }

        //模拟依赖函数3
        const dependFn3 = ()=>{
            console.log(obj.age);
        }

        //这时候得创建一个类
        class Depend {
            constructor(){
                this.dependFns = new Set()
            }

            //收集依赖函数
            gatherFns(fn){
                if(fn && typeof fn === 'function'){
                    this.dependFns.add(fn)
                }
            }

            //统一调用依赖函数
            dispatchFns(){
                this.dependFns.forEach(fn => {
                    fn()
                })
            }
        }

        const changeObj = ()=>{
            console.log(obj.age);
            obj.age++
        }

        //设置活跃函数
        let activeFn = null
        //加入依赖的函数
        const addDependFn = (fn)=>{
            activeFn = fn
            fn()
            activeFn = null
        }

        //手动执行一遍函数
        addDependFn(dependFn1)
        addDependFn(dependFn2)
        addDependFn(dependFn3)

        const targetMap = new WeakMap()//创建一个集合,将响应式对象放进去

        //查找依赖实例的函数
        const getDepend = (obj,key)=>{
            let target = targetMap.get(obj)

            if(!target){
                //创建
                const propertyDependMap = new Map() //创建一张属性和对应依赖实例的对照表
                targetMap.set(obj,propertyDependMap) //将对象和对应的属性和对应依赖实例的对照表关联,可以通过对象找到对照表
                target = targetMap.get(obj)
            }

            let depend = target.get(key)
            if(!depend){
                //创建
                const instance = new Depend()
                target.set(key,instance) //将映射关系写好
                depend = instance
            }

            return depend
        }

        //创建一个myReactive的函数,接收一个对象,进行对它的属性劫持
        function myReactive(obj){
            Object.keys(obj).forEach(key => {
                let value = obj[key] //获取对应属性的值,保留等下要用

                //加上属性劫持
                Object.defineProperty(obj,key,{
                    get : function(){
                        getDepend(obj,key).gatherFns(activeFn)
                        return value
                    },
                    set : function(newValue){
                        value = newValue
                        //通知对应的实例执行所有依赖函数
                        getDepend(obj,key).dispatchFns()
                    }
                })
            })
        }

        myReactive(obj)
        addDependFn(updateDom)
    </script>
</body>
</html>

image.png

七、总结

响应式其实就是3个重要的点

  1. 依赖收集:通过Object.defineProperty的getter拦截属性访问,收集当前正在执行的函数作为依赖
  2. 依赖触发:通过setter拦截属性修改,自动通知所有依赖函数更新
  3. 依赖管理:使用Depend类管理每个属性的依赖关系,使用WeakMapMap建立对象-属性-依赖的映射关系

这个简易实现包含了Vue2响应式系统的核心思想,但是vue封装的更加复杂!这个只是我的个人总结,如果能帮到你的话,就更好啦!如果觉得写的好的话,可以给个一键三连哈哈哈哈哈哈哈哈

❌