05、模拟Vue数据响应式(万文知识回顾)

简介: 05、模拟Vue数据响应式(万文知识回顾)

文章目录


涉及知识体系

1、 预备知识

1.1、Object.defineProperty

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

应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 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 99
obj.a = 100 // 无法更改
obj.b = 666 // 可以更改
console.log(obj.a, obj.b); // 3 666
console.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 值(数值,对象,函数等)。 默认为 undefined
  • writable
    当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。 默认为 false

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

  • get
    属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined
  • set
    属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined

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

var obj = {}
var temp = null
Object.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 = 99
console.log(obj.a)

技术参考MDN

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、常用的闭包

  • 将函数作为另一个函数的返回值
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        function fn1(){
            var a = 2
            function fn2(){
                a++
                console.log(a)
            }
            return fn2
        }
        var f = fn1() // 没有输出
        f() // 3
        f() // 4
        fn1() // 没有输出
        fn1()() // 3 
    </script>
</body>
</html>
  • 将函数作为实参传递给另一个函数调用
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 = null
Object.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 = 99
console.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 = 10
console.log(obj.a.m.n)
// 全局位置
new Watcher(obj, 'a.m.n', (val)=>{
    console.log('※※※※※', val)
})
obj.a.m.n = 88
console.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<arr.length; ++i){
            // 逐项观察observe
            observe(arr[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.prototype
const 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<l; i++ ){
            subs[i].update();
        }
    }
}

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<arr.length; ++i){
            // 逐项观察observe
            observe(arr[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 !---------------------------

对数据响应式的新理解

详情请看】从模仿开始理解Vue2的数据响应式

相关文章
|
5天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
6天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
6天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
5天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
5天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
JavaScript 前端开发
模拟Vue数据的双向绑定
Vue的数据双向绑定功能一直为人称道, Vue数据的双向数据绑定主要依赖了Object.defineProperty,这里尝试用最简单的代码, 实现数据的双向绑定Demo MVVM ViewModel基本实现原理 Gi...
938 0
|
6天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
7天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
7天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
12天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发