2. 对象的响应式原理
初始化数据和实现对象的响应式原理 (接上一章)
初始化数据
-
在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属性
-
新建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的值, 说明初始化完成
-
文件分离, 上面的代码完成之后, 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) }
-
数据初始化的基本工作完毕, 开始具体实现
实现对象的响应式原理
-
初始化数据的具体方法
// 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
-
实现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类里面
-
实现 Observer 类
class Observer { constructor(data) { // 对对象的属性劫持放在walk方法中, 对数组的暂时没写 this.walk(data) } // 遍历对象, 给每一个属性添加响应式, defineReactive不在class中, 因为其他地方也可能用到, 写了外面 walk(data) { Object.keys(data).forEach(key => defineReactive(data, key, data[key])) } }
-
实现关键方法: 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 } }) }
-
测试, 在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
-
修改实例, 如下, 对象嵌套, 发现问题
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 } }) }
-
现在我们访问属性的时候使用的时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方法
-
至此基本实现了对象的响应式原理, 注意还没涉及到视图
各个文件代码如下:
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 } }) }