1. 为什么我们把属性定义在 data,props,methods 等参数里, 却能通过 this 对象直接访问呢.
原理:
因为 vue 内部做了代理. 假如我们用 this 去访问某个属性, vue 会自动去 data,props,methods 等参数对象里面去查找. 所以我们开发时会发现, props 里面定义过的属性, data 不能再定义了, 会抛出警告. methods 也一样.
用过 Vue 都知道, Vue 本身是一个构造函数, 所以我们的用法是直接 new Vue(). 下面我们用代码模拟一下 Vue 内部的代理
- (部分代码来源: vue 项目下 src/core/instance/state.JS)
- // 定义一个空函数
- function noop() {}
- // 定义一个公用的属性描述对象
- const sharedPropertyDefinition = {
- enumerable: true,
- configurable: true,
- get: noop,
- set: noop
- }
- /**
- * 定义代理函数
- * @target 当前对象
- * @sourceKey 传入的是来源, 也就是代理对象的名称
- * @key 要访问的属性
- */
- function proxy(target, sourceKey, key) {
- sharedPropertyDefinition.get = function proxyGetter() {
- // 示例: 如果你在 data 中访问 this.name, 那么此时返回的是 this['_data']['name']
- // target[key] => target[source][key]
- return target[sourceKey][key];
- }
- sharedPropertyDefinition.set = function proxySetter(val) {
- target[sourceKey][key] = val;
- }
- Object.defineProperty(target, key, sharedPropertyDefinition);
- }
- // 构造函数
- function MyVue(options) {
- this._data = options.data || {};
- this._props = options.props || {};
- this._methods = options.methods || {};
- this.init(options);
- }
- MyVue.prototype.init = function(options) {
- initData(this, options.data);
- initProps(this, options.props);
- iniMethods(this, options.methods);
- }
- // 相关方法
- function initData(vm, dataObj) {
- Object.keys(dataObj).forEach(key => proxy(vm, '_data', key));
- }
- function initProps(vm, propsObj) {
- Object.keys(propsObj).forEach(key => proxy(vm, '_props', key));
- }
- function iniMethods(vm, methodsObj) {
- Object.keys(methodsObj).forEach(key => proxy(vm, '_methods', key));
- }
这里的代码主要是示例, 并没有判断属性是否重复.
测试代码:
- let myVm = new MyVue({
- data: {
- name: 'JK',
- age: 25
- },
- props: {
- sex: 'man'
- },
- methods: {
- about() {
- console.log(`my Name is ${this.name}, age is ${this.age}, sex is ${this.sex}`);
- }
- }
- });
- myVm.name // 'JK'
- myVm.age // 25
- myVm.sex // 'man'
- myVm.about() // my Name is JK, age is 25, sex is man
- myVm.age = 24;
具体 Vue 内部的处理是比较复杂的, 会判断很多边界情况. 例如 data 返回一个函数时需要单独处理, 例如 props 传入具有 default 和 type 属性的对象等等.
2. 如何实现一个简易的数据响应式系统
Vue 的数据响应式实现是依赖 Object.defineProperty 这个 API 的, 这也是它不支持 IE8 且无法 hack 的原因.
据说 Vue3.0 改用了 ES6 的 ```Proxy``, 并使用 TypeScript 编写. 很是期待.
vue 改变 data 之后做了什么? 如果要说完整的一套流程, 那是很多的, 涉及到 watcher,render 渲染函数, VNode,Dom diff 等等.
响应式系统本身是基于观察者模式的, 也可以说是发布 / 订阅模式. 发布 / 订阅模式, 就好比是你去找中介租房子. 而观察者模式呢, 就好比你直接去城中村找房东租房子. 发布 / 订阅模式比观察者模式多了个调度中心 (中介).
我这里只是先说一下怎么收集依赖, 修改了值是怎么通知的思路.
- // 假如有一个对象是 data
- let data = {
- x: 1,
- y: 2
- }
- // 我们把这个对象变成响应式的
- for(const key in data) {
- Object.defineProperty(data, key, {
- get() {
- console.log(` 我获取了 data 的 ${key}`);
- return data[key]
- },
- set(val) {
- console.log(` 我设置了 data 的 ${key} 为 ${val}`);
- data[key] = val;
- }
- })
- }
- // 定义一个 watch 函数, 作用是拿到改变某个值时对应的处理函数
- // Target 是全局变量, 用于存储对应的函数
- let Target = null
- function $watch (exp, fn) {
- // 将 Target 的值设置为 fn
- Target = fn;
- // 读取字段值, 触发 get 函数
- data[exp];
- }
- // dep 在 get 和 set 被闭包引用, 不会被回收
- // 每一个 key 都有一个属于自己的 dep
- for(const key in data) {
- const dep = [];
- // 优化死循环
- let val = data[key];
- Object.defineProperty(data, key, {
- get() {
- console.log(` 我获取了 data 的 ${key}`);
- // 收集依赖
- dep.push(Target);
- return val;
- },
- set(newVal) {
- console.log(` 我设置了 data 的 ${key} 为 ${newVal}`);
- if (val === newVal) {
- return ;
- }
- val = newVal;
- // 触发依赖
- dep.forEach(fn => fn());
- }
- })
- }
- // 监听数据变化
- $watch('x', () => console.log('x 被修改')); // 输出 '我获取了 data 的 x'
- data.x = 3; // 输出 '我设置了 data 的 x 为 3', x 被修改
- let data = {
- x: 1,
- y: 2
- }
- // Target 是全局变量, 用于存储对应的函数
- let Target = null
- function $watch (exp, fn) {
- // 将 Target 的值设置为 fn
- Target = fn;
- // 如果 exp 是函数, 直接执行该函数
- if (typeof exp === 'function') {
- exp();
- return;
- }
- // 读取字段值, 触发 get 函数
- data[exp];
- }
- // dep 在 get 和 set 被闭包引用, 不会被回收
- // 每一个 key 都有一个属于自己的 dep
- for(const key in data) {
- const dep = [];
- // 优化死循环
- let val = data[key];
- Object.defineProperty(data, key, {
- get() {
- console.log(` 我获取了 data 的 ${key}`);
- // 收集依赖
- dep.push(Target);
- return val;
- },
- set(newVal) {
- console.log(` 我设置了 data 的 ${key} 为 ${newVal}`);
- if (val === newVal) {
- return ;
- }
- val = newVal;
- // 触发依赖
- dep.forEach(fn => fn());
- }
- })
- }
- // 测试代码
- function render () {
- return document.write(`x:${data.x}; y:${data.y}`)
- }
- $watch(render, render);
来源: https://juejin.im/post/5bfa1b4ef265da61327efb8b