2. 对象的响应式原理

初始化数据和实现对象的响应式原理 (接上一章)

初始化数据

  1. 在dist目录下新建1.index.html文件, 内容如下

    <!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 src="vue.js"></script>
        <script>
            // 新建一个vue实例
            const vm = new Vue({
                data() {
                    return {
                        name: 'jerry',
                        age: '杨'
                    }
                }
            })
        </script>
    </body>
    </html>
    
    // 重点
    1. 引入vue
    2. 创建vue实例, 里面有个data属性
    
  2. 新建Vue构造函数,并进行数据的初始化

    在src/index.js里面

    // 新建vue构造函数, 里面执行_init方法(初始化方法), 现在还没有
    function Vue(options) {
        this._init(options)
    }
    
    

    在Vue的原型上创建初始化方法, 初始化状态

    // 获取到用户选项之后, 初始化状态
    Vue.prototype._init = function(options) {
        // 获取vue实例, 这里的this指向vue实例
        const vm = this
        // 获取用户选项, 方便后续获取参数, 很多地方都是挂载到vue上面的
        vm.$options = options
        // 初始化状态, 也就是data里面的数据, vue的实例暂时长这样
        // const vm = new Vue({
        //     data() {
        //         return {
        //             name: 'jerry',
        //             age: '杨'
        //         }
        //     }
        // })
        initState(vm)
    }
    
    

    新建初始化状态方法, 是一个总的方法, 里面还有其他东西需要初始化, 如: computed, watcher

    // 如果选项有data的话, 初始化data
    function initState(vm) {
        const opts = vm.$options
        if(opts.data) {
            initData(vm)
        }
    }
    

    初始化data的方法

    function initData(vm) {
        console.log('初始化数据', vm.$options)
    }
    

    此时在页面能打印vm.$options的值, 说明初始化完成

  3. 文件分离, 上面的代码完成之后, src/index.js 里面的内容

    // Vue 类是通过构造函数来实现的
    // 如果通过 class来实现, 里面的类和方法就会有很多, 不利于维护
    // 1. 新建一个Vue构造函数, 默认导出, 这样就有了全局 Vue 
    // 2. Vue中执行一个初始化方法, 参数是用户的选项
    // 3. 在Vue的原型上添加这个方法, (注意: 添加的这个方法在引入vue的时候就执行了, 而不是在new Vue()的时候执行的)
    
    
    function Vue(options) {
        this._init(options)
    }
    
    Vue.prototype._init = function(options) {
        // 获取vue实例, 这里的this指向vue实例
        const vm = this
        // 获取用户选项, 方便后续获取参数, 很多地方都是挂载到vue上面的
        vm.$options = options
        // 初始化状态, 也就是data里面的数据, vue的实例暂时长这样
        // const vm = new Vue({
        //     data() {
        //         return {
        //             name: 'jerry',
        //             age: '杨'
        //         }
        //     }
        // })
        initState(vm)
    }
    
    function initState(vm) {
        const opts = vm.$options
        if(opts.data) {
            initData(vm)
        }
    }
    
    function initData(vm) {
        console.log('初始化数据', vm.$options)
    }
    
    export default Vue
    

    很明显, 不同的功能应放在不同的文件里面

    新建init.js文件, 将初始化的内容放在里面

    新建state.js, 将初始化状态的内容放在里面

    由于init.js里面没有Vue, 所以考虑用function 将Vue的构造函数作为参数传递, 然后在src/index.js里面引用就行了, 实际上vue就是用这种方法扩展vue的功能的, 后续有很多地方也会用到

    文件分离之后, 各个文件代码如下

    src/index.js

    // Vue 类是通过构造函数来实现的
    // 如果通过 class来实现, 里面的类和方法就会有很多, 不利于维护
    // 1. 新建一个Vue构造函数, 默认导出, 这样就有了全局 Vue 
    // 2. Vue中执行一个初始化方法, 参数是用户的选项
    // 3. 在Vue的原型上添加这个方法, (注意: 添加的这个方法在引入vue的时候就执行了, 而不是在new Vue()的时候执行的)
    
    import { initMixin } from "./init"
    
    
    
    function Vue(options) {
        this._init(options)
    }
    
    initMixin(Vue)
    
    export default Vue
    

    同级目录下, init.js

    import { initState } from "./state"
    
    
    export function initMixin(Vue) {
    
        Vue.prototype._init = function(options) {
            // 获取vue实例, 这里的this指向vue实例
            const vm = this
            // 获取用户选项, 方便后续获取参数, 很多地方都是挂载到vue上面的
            vm.$options = options
            // 初始化状态, 也就是data里面的数据, vue的实例暂时长这样
            // const vm = new Vue({
            //     data() {
            //         return {
            //             name: 'jerry',
            //             age: '杨'
            //         }
            //     }
            // })
            initState(vm)
        }
        
       
    }
    
    

    同级目录下, state.js

    
    
    export function initState(vm) {
        const opts = vm.$options
        if(opts.data) {
            initData(vm)
        }
    }
    
    function initData(vm) {
        console.log('初始化数据', vm.$options)
    }
    
  4. 数据初始化的基本工作完毕, 开始具体实现

实现对象的响应式原理

  1. 初始化数据的具体方法

    // state.js 文件
    // 初始化数据的具体方法
    function initData(vm) {
        let data = vm.$options.data
        data = typeof data === 'function' ? data.call(vm) : data
    	
        // why
        vm._data = data
    
        // 进行数据劫持, 关键方法, 放在另一个文件里面, 新建 observe/index.js
        observe(data)
    
    }
    
    // 注意, 这里为什么将 vm._data = data,
    通过 observe方法, 给data里面的给个属性添加上了get和set, 但是通过vm并不能观察, 此时vm上只有一个$options用来记录用户的选项, 添加了 _data之后, 就可以通过 vm._data 来观察各个属性是否添加上了set和get
    
  2. 实现observe方法, 新建observe/index.js文件, 内容:

    // 初次里面传过来的是一个对象, 后续可能是null, 或数据, 只需要对对象进行劫持, 数组不需要
    // 就是传说中的Object.defineProperty方法, 不过就提的实现是在一个类中, 这里就是判断 + 返回一个class
    // observe 方法 就是对对象进行监控, 添加set和get, 很多地方都有用到
    export function observe(data) {
        if(typeof data !== 'object' || data === null) {
            return 
        }
    
        // 这里的劫持使用的是一个类, 因为里面需要给被劫持的对象添加很多属性, 
        return new Observer(data)
    
    }
    
    // 这里主要是进行类型的判断, 还有是否已经被劫持(暂时没写), 因为很多地方都需要判断, 就将判断条件写在一个方法里面, 后续的具体实现是在 new Observer类里面
    
  3. 实现 Observer 类

    class Observer {
        constructor(data) {
            // 对对象的属性劫持放在walk方法中, 对数组的暂时没写
            this.walk(data)
        }
        // 遍历对象, 给每一个属性添加响应式, defineReactive不在class中, 因为其他地方也可能用到, 写了外面
        walk(data) {
            Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
        }
    }
    
  4. 实现关键方法: defineReactive

    export function defineReactive(target, key, value) {
        Object.defineProperty(target, key, {
            get() {
                console.log('get')
                return value
            },
            set(newVlaue) {
                if(value === newValue) return
                console.log('set')
                value = newVlaue
            }
        })
    }
    
  5. 测试, 在1.index.html文件中打印vm._data, 发现对象里面的属性有被添加get 和 set属性,设置和取值时能触发 set 和 get

     const vm = new Vue({
         data() {
             return {
                 name: 'jerry',
                 age: '杨'
             }
         }
     })
    
    console.log('vm:', vm._data.name)
    vm._data.age = 123
    
  6. 修改实例, 如下, 对象嵌套, 发现问题

     const vm = new Vue({
         data() {
             return {
                 name: 'jerry',
                 eat: {
                     food: 'bababa'   
                 }
             }
         }
     })
    
     console.log('vm:', vm._data)
    
    // 发现eat 里面的 food 并没有被劫持, 没有get 和 set
    
    还有一个问题, 在设置值的时候, 设置一个新的 对象
    vm._data.name = {firstname: 123}
    console.log('vm:', vm._data)
    
    发现 name下的 firstname 并没有被劫持
    

    ​ 需要在 observe/index.js 稍作修改

    class Observer {
      constructor(data) {
          // 对对象的属性劫持放在walk方法中, 对数组的暂时没写
          this.walk(data)
      }
      // 遍历对象, 给每一个属性添加响应式, defineReactive不在class中, 因为其他地方也可能用到, 写了外面
      walk(data) {
          Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
      }
    }
    
    export function defineReactive(target, key, value) {
      // 在这里对值进行再次劫持, 里面有object的判断, 不是object会直接return
    observe(value)
      // ....
      Object.defineProperty(target, key, {
          get() {
              console.log('get')
              return value
          },
          set(newValue) {
              if(value === newValue) return
              console.log('set')
              observe(newValue)
              value = newValue
          }
      })
    }
    
  7. 现在我们访问属性的时候使用的时vm._data.name, 而vue可以直接使用vm.name, 这中间需要在做一层代理, 最外层的data, 需要使用vm.__data.xxx来代理vm.xxx,

    // 就是在访问 vm.name的时候, 实际访问的是vm._data.name
    // 需要对state.js里面的方法修改
    // 初始化数据的具体方法
    function initData(vm) {
        let data = vm.$options.data
        data = typeof data === 'function' ? data.call(vm) : data
    
        vm._data = data
    
        // 进行数据劫持, 关键方法, 放在另一个文件里面, 新建 observe/index.js
        observe(data)
    
        // 设置代理, 这个代理只有最外面这一层
        // 希望访问 vm.name 而不是 vm._data.name, 使用vm 来代理 vm._data
        // 在vm上取值时, 实际上是在vm._data上取值
        // 设置值时, 实际上是在vm._data上设置值
        // 每一个属性都需要代理
        for(let key in data) {
            proxy(vm, '_data', key)
        }
    
    
    }
    
    // 属性代理 vm._data.name  => vm.name
    function proxy(vm, target, key) {
        Object.defineProperty(vm, key, {
            get() {
                return vm[target][key]
            },
            set(newValue) {
                vm[target][key] = newValue
            }
        })
    }
    
    // 重点是proxy方法
    在vm上定义name属性, 获取name的时候, 去vm._data里面获取name, 也会走name的get方法
    set name的时候, 实际去设置 vm._data.name, 走name的set方法
    
  8. 至此基本实现了对象的响应式原理, 注意还没涉及到视图

    各个文件代码如下:

    1.index.html

    import { initState } from "./state"
    
    
    export function initMixin(Vue) {
    
        Vue.prototype._init = function(options) {
            // 获取vue实例, 这里的this指向vue实例
            const vm = this
            // 获取用户选项, 方便后续获取参数, 很多地方都是挂载到vue上面的
            vm.$options = options
            // 初始化状态, 也就是data里面的数据, vue的实例暂时长这样
            // const vm = new Vue({
            //     data() {
            //         return {
            //             name: 'jerry',
            //             age: '杨'
            //         }
            //     }
            // })
            initState(vm)
        }
        
       
    }
    
    

    state.js

    import { observe } from "./observe"
    
    
    export function initState(vm) {
        const opts = vm.$options
        if(opts.data) {
            initData(vm)
        }
    }
    
    // 初始化数据的具体方法
    function initData(vm) {
        let data = vm.$options.data
        data = typeof data === 'function' ? data.call(vm) : data
    
        vm._data = data
    
        // 进行数据劫持, 关键方法, 放在另一个文件里面, 新建 observe/index.js
        observe(data)
        
         // 设置代理, 这个代理只有最外面这一层
        // 希望访问 vm.name 而不是 vm._data.name, 使用vm 来代理 vm._data
        // 在vm上取值时, 实际上是在vm._data上取值
        // 设置值时, 实际上是在vm._data上设置值
        // 每一个属性都需要代理
        for(let key in data) {
            proxy(vm, '_data', key)
        }
    
    }
    
    / 属性代理 vm._data.name  => vm.name
    function proxy(vm, target, key) {
        Object.defineProperty(vm, key, {
            get() {
                return vm[target][key]
            },
            set(newValue) {
                vm[target][key] = newValue
            }
        })
    }
    

    observe/index.js

    
    // 初次里面传过来的是一个对象, 后续可能是null, 或数据, 只需要对对象进行劫持, 数组不需要
    // 就是传说中的Object.defineProperty方法, 不过就提的实现是在一个类中, 这里就是判断 + 返回一个class
    // observe 方法 就是对对象进行监控, 添加set和get, 很多地方都有用到
    export function observe(data) {
        if(typeof data !== 'object' || data === null) {
            return 
        }
    
        // 这里的劫持使用的是一个类, 因为里面需要给被劫持的对象添加很多属性, 
        return new Observer(data)
    
    }
    
    
    class Observer {
        constructor(data) {
            // 对对象的属性劫持放在walk方法中, 对数组的暂时没写
            this.walk(data)
        }
        // 遍历对象, 给每一个属性添加响应式, defineReactive不在class中, 因为其他地方也可能用到, 写了外面
        walk(data) {
            Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
        }
    }
    
    export function defineReactive(target, key, value) {
        // 再次劫持
        observe(value)
        // 再次劫持
        Object.defineProperty(target, key, {
            get() {
                console.log('get')
                return value
            },
            set(newValue) {
                if(value === newValue) return
                console.log('set')
                value = newValue
            }
        })
    }
    
posted @ 2022-06-22 16:34  littlelittleship  阅读(22)  评论(0编辑  收藏  举报