模拟Vue数据响应式(万文知识回顾)
发布日期:2021-05-10 03:28:57 浏览次数:20 分类:精选文章

本文共 11168 字,大约阅读时间需要 37 分钟。

文章目录


涉及知识体系

在这里插入图片描述

1、 预备知识

1.1、Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

应当直接在 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

  • 用处—> 可以设置一些额外隐藏的属性
  • 测试源码
var obj = {   }Object.defineProperty(obj, 'a',{       value:3,    // 是否可以被写    writable:false,    // 是否可以被枚举    enumerable:true})Object.defineProperty(obj, 'b', {       value: 99,    writable:true,    enumerable:false})Object.defineProperty(obj, 'c',{       value:1,    enumerable:true})console.log(obj.a, obj.b); // 3 99obj.a = 100 // 无法更改obj.b = 666 // 可以更改console.log(obj.a, obj.b); // 3 666console.log('enumerable')for(var i in obj){       console.log(i)  // a c}

语法

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符数据描述符是一个具有值的属性,该值可以是可写的,也可以是不可写的。存取描述符是由 getter 函数和 setter 函数所描述的属性。一个描述符只能是这两者其中之一;不能同时是两者。

这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用 Object.defineProperty() 定义属性时的默认值):

  • configurable

    当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false

  • enumerable

    当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。 默认为 false

数据描述符还具有以下可选键值:

  • value

    该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为

  • writable

    当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被改变。 默认为 false

存取描述符还具有以下可选键值:

  • get

    属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为

  • set

    属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为

注意,getter与setter需要变量中转

var obj = {   }var temp = nullObject.defineProperty(obj, 'a',{       // 数据劫持   getter    get(){           console.log('获取obj.a')        return temp    },    // setter    set(a){           console.log('尝试设置a属性 = ',a)        temp = a            }})console.log(obj)obj.a = 99console.log(obj.a)

技术参考

1.2 函数柯里化

通过函数调用继续返回函数的方式,实现多次接收参数,最后统一处理的函数处理形式

附方便理解的代码

function sum(a){       return(b)=>{           return (c)=>{               return a+b+c        }    }}console.log(sum(1)(2)(3))

1.3 闭包

1.3.1、如何产生闭包(closure)

  • 当一个函数的内部(子)函数引用了嵌套的外部(父)函数的变量或者函数时,就产生了闭包

1.3.2、闭包是什么?

  • 使用chrome调试查看
      1. 理解一: 闭包就是函数内部的嵌套函数
      2. 理解二:包含被引用变量或者函数的对象
    • 注意: 闭包存在于嵌套的内部函数中

1.3.3、产生闭包的条件

    1. 函数嵌套

    2. 内部函数引用了外部函数的数据(变量或者函数)

    3. 执行外部函数,执行内部函数的定义

1.3.4、在chrome中的表现

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LqSYULzJ-1618128834529)(.\pics\image-20210213170315426.png)]


1.3.5、常用的闭包

  • 将函数作为另一个函数的返回值

        
    Document
  • 将函数作为实参传递给另一个函数调用

function showDelay(msg,time){       setTimeout(function(){           console.log(msg)    },time)}showDelay('hello',1000)

1.3.6、闭包的作用

    1. 使函数内部的变量在函数执行完之后,仍然存活在内存中(延长局部变量的生命周期)
    2. 让函数外部可以操作函数内部的局部变量

1.3.7、闭包的生命周期

  • 产生: 在嵌套内部函数定义执行完时就产生了(并不是在调用时)
  • 死亡:在嵌套内部函数成为垃圾对象时

1.3.8、闭包的应用

1.3.8.1 定义js模块

js模块:

  • 具有特定功能的js文件
  • 将所有的数据和功能都封装在一个函数内部(私有的)
  • 只向外暴露一个包含n个方法的对象或者函数
  • 模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能
  • 模块的定义方法1 --> 利用封闭函数封装私有属性,利用全局变量函数暴露接口---->使用的时候:通过全局对象来访问内部方法
(function () {           let a = 1;        function test() {             console.log(++a);        }        // 浏览器的全局变量        window.$ = function () {             return {               test:test,          };        };      })();      $().test() //2      console.log($().test,'\n-->', $().test())       /**       * 3       ƒ test() {          console.log(++a);        } "        -->" undefined      */
  • 模块的定义方法2 --> 利用闭包定义私有属性,通过return 一个对象,暴露模块API —> 使用的时候需要先new 一个对象, 在通过这个对象来访问私有属性

    function myModule(){         // 私有数据    var msg = 'hello XiaoMing'    // 私有方法    function big(){             console.log(msg.toUpperCase())    }    function small(){             console.log(msg.toLowerCase())    }    // 向外暴露对象    return {             big,        small    }}

1.3. 9、闭包的缺点

  • 9.1 函数执行完之后,函数内的局部变量没雨释放,占用内存时间会变长

  • 9.2 容易造成内存泄漏

  • 解决方法

    • 能不用闭包就不用

    • 及时释放 f=null

1.4、 webpack

  • 涉及依赖包
npm -i webpack@4 webpack-cli webpack-dev-server -D
  • webpack.config.js配置文件
const path = require('path');module.exports = {       // 模式 开发    mode: 'development',    entry: './src/index.js',    output: {           filename: 'bundle.js'    },devServer: {       // 静态文件根目录    contentBase: path.join(__dirname, "www"),    // 不压缩    compress: false,    port: 8080,    // 虚拟打包的路径,bundle.js文件没有真正的生成    publicPath: "/xuni/"}};

ps : 注意在package.json 中新增 "dev": "webpack-dev-server" ,后续通过npm run dev 启动测试工程

⚠以下部分为学习视频补充笔记,可能会看不懂

2、模拟数据响应式

2.1 利用Object.defineProperty定义隐藏属性

  • 使用getter以及setter实现简易数据劫持

样例如1.1, 方便学习,重复贴上

var obj = {   }var temp = nullObject.defineProperty(obj, 'a',{       // 数据劫持   getter    get(){           console.log('获取obj.a')        return temp    },    // setter    set(a){           console.log('尝试设置a属性 = ',a)        temp = a            }})console.log(obj)obj.a = 99console.log(obj.a)

2.2 构建目录文件

在这里插入图片描述

2.3 响应式原理回顾

在这里插入图片描述

2.4 模拟代码

index.js

import observe from './Observe';import Watcher from './Watcher';import Dep from './Dep'var obj = {       a:{           m: {               n: 5        }    },    b:{           c:'hello'    },    g: [2,3,44,55,66]}observe(obj)// obj.a = 10console.log(obj.a.m.n)// 全局位置new Watcher(obj, 'a.m.n', (val)=>{       console.log('※※※※※', val)})obj.a.m.n = 88console.log(obj.a.m.n)

observe函数

import Observer from './Observer'export default function Observe(value){       // 创建observe 函数    // 要求观测的对象必须是Object    if(typeof value != 'object'){           // do nothing        return ;    }    // 定义ob    var ob = null     if(typeof value.__ob !== 'undefined'){           ob = value.__ob    }else{           ob = new Observer(value)    }    return ob;}

Observer类

import def from './utils';import defineReactive from './defineReactive';import arrayMethods from './Array';import observe from './Observe';import Dep from './Dep';export default class Observer{       // 将一个正常的对象转换成可观测对象    // 创建类的时候要思考如何实例化    constructor(value){                   // 每个Observer的实例成员都有Dep的实例对象        this.dep = new Dep();        // __ob__ 一般为不可枚举        // 这里的this指向的是实例对象而不是类        def(value,'__ob__', this, false);                // console.log("我是Observer构造器",value)        if(Array.isArray(value)){               // 如果是数组则更改原型链            Object.setPrototypeOf(value,arrayMethods)            // 让这个数组变得observe            this.observeArray(value)        }else{               this.walk(value)        }               // console.log('构造器结束')     }    // 遍历    walk(value){           for(let k in value){               defineReactive(value, k)        }    }    // 数组的特殊遍历    observeArray(arr){           for(let i=0; i

defineReactive.js

import observe from './Observe';import Dep from './Dep';// 构造闭包环境export default function defineReactive(data, key, val){       // console.log("我是defineReactive",key)    // 定义管理依赖成员    const dep = new Dep()    if( arguments.length === 2){           // console.log("arguments=",arguments)        val = data[key]    }    // 让子元素进行observe, 至此形成了递归,     // 这个递归不是函数自己调用自己,而是多个函数、类循环调用    let childOb = observe(val)    Object.defineProperty(data, key,{           // 可枚举        enumerator : true,                // 可以被配置 ,比如说可以被delete        configurable : true,                // 数据劫持   getter        get(){               // console.log('获取'+ key + '属性')            // 如果现在处于依赖的收集阶段            // 即有watcher被劫持            if(Dep.target){                   dep.depend()                if(childOb){                       childOb.dep.depend()                }            }            return val        },        // setter        set(newVal){               // console.log(`尝试设置${key}属性 = `,newVal)            if(val === newVal){                   return             }            val = newVal                // 当设置了新值的时候,这个新值也要被observe            childOb = observe(childOb)            // 发布订阅模式            // 依赖管理员通知观察者,观察数据是否发生变化, 如果变化了,则保存新数据            dep.notify()        }})}

Array.prototype的七大方法重写

import def from './utils';// 得到Array.prototypeconst arrayPrototype = Array.prototype// 以Array.prototype 为原型创建一个arrayMethods对象const arrayMethods = Object.create(arrayPrototype) const methodsNeedChange = [    'push',    'pop',    'shift',    'unshift',    'splice',    'sort',    'reverse']methodsNeedChange.forEach(methodName =>{       // 备份原来的方法    // 因为push, pop 等7个函数的功能不能被剥夺    const original = arrayPrototype[methodName]    // 把类数组对象转换成数组    const args = [...arguments]    // 把数组身上的__ob__取出来, __ob__已经被添加了    // 因为数组肯定不是最高层,     // 定义新的方法    def(arrayMethods, methodName, function(){                   const ob = this.__ob__        // 有三种方法,push\unshift\shift能够插入新项,现在把插入的新项也要变成observe的        const result = original.apply(this,arguments)        let inserted = []        // console.log('arguments',arguments);        switch(methodName){               case 'push':            case 'unshift':                inserted = args;                break;            case 'splice':                // splice(下标,数量, 插入的新项)                inserted = args.slice(2);                break;        }        // 判断有没有要插入的项,把新插入的项变成响应式        if(inserted){               ob.observeArray(inserted)        }                ob.dep.notify();        // console.log('aaaa')        return result    },false)})export default arrayMethods;

utils.js

export default function def(obj, key, value, enumerable){       // console.log('开始定义属性',key)    Object.defineProperty(obj,key, {           value,        enumerable,        writable:true,        configurable:true    })}

重点Dep类

var uid = 0;export default class Dep{       constructor(){           // console.log('我是Dep的构造器');        this.id = uid ++;        // 用数组来存储自己的订阅者 subscribes        this.subs = [];    }    // 添加订阅    addSub(sub){           this.subs.push(sub)        console.log(this.subs)    }    // 移除订阅    rmSub(){           // do nothing    }    // 添加依赖即是Watcher    depend(){           // Dep.target 是我们自己指定的一个全局位置        if(Dep.target){               this.addSub(Dep.target)        }    }    // 通知Watcher更新数据    notify(){           // console.log("我是notitfy");        // 浅克隆一份        const subs = this.subs.slice()        // 遍历所有对象,看看是否需要更新数据        for(let i=0 ,l=subs.length; i

Watcher类

import def from './utils';import defineReactive from './defineReactive';import arrayMethods from './Array';import observe from './Observe';import Dep from './Dep';export default class Observer{       // 将一个正常的对象转换成可观测对象    // 创建类的时候要思考如何实例化    constructor(value){                   // 每个Observer的实例成员都有Dep的实例对象        this.dep = new Dep();        // __ob__ 一般为不可枚举        // 这里的this指向的是实例对象而不是类        def(value,'__ob__', this, false);                // console.log("我是Observer构造器",value)        if(Array.isArray(value)){               // 如果是数组则更改原型链            Object.setPrototypeOf(value,arrayMethods)            // 让这个数组变得observe            this.observeArray(value)        }else{               this.walk(value)        }               // console.log('构造器结束')     }    // 遍历    walk(value){           for(let k in value){               defineReactive(value, k)        }    }    // 数组的特殊遍历    observeArray(arr){           for(let i=0; i

3、Dep类与Watcher类

把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例成员中都有一个Dep的实例;

Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件。

依赖就是Watcher。

只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。

Dep使用发布订阅模式

当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

代码实现的巧妙之处

Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在

getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中

4、附中介者模式3S入门精髓

  • 使用中介者模式之前
  • 在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述

----------------------------------The end !---------------------------
上一篇:蓝桥省赛前晚复习数学知识
下一篇:HTTP之神秘的CORS

发表评论

最新留言

能坚持,总会有不一样的收获!
[***.219.124.196]2025年05月04日 21时01分14秒