响应式原理 - 学习vue源码系列4.2

简介: 响应式原理 - 学习vue源码系列4.2

响应式原理 - 学习vue源码系列4.2


决定跟着黄轶老师的 vue2 源码课程好好学习下vue2的源码,学习过程中,尽量输出自己的所得,提高学习效率,水平有限,不对的话请指正~

vue 的源码clone 到本地,切换到分支2.6

Introduction

前面说了,vue 是怎么实现初始化数据渲染和组件化的,但当数据变动的时候,dom 又是怎么更新的呢。

先看个 demo

数据变更:

<div id="app">hello</div>

假设希望点击之后能将hello变成hello world的话,寻常怎么操作。

// 1.修改数据
const text = "hello world";
// 2.获取dom,监听事件
document.querySelector("#app").onclick = () => {
  // 3.手动操作dom重新渲染
  app.innerHTML = text;
};

如果需要多次修改的话,就不得不每次都手动操作 dom,重新渲染。

而 vue,去掉了手动操作dom,根据数据变化自动操作 dom。

<div id="app" @click="changeMsg">{{ message }}</div>
<script src="/Users/zhm/mygit/vue/dist/vue.js"></script>
<script>
  var app = new Vue({
    el: "#app",
    data: {
      message: "Hello Vue!",
    },
    methods: {
      changeMsg() {
        // 这边只是 改变了数据,其余的由vue本身去更新dom,重新渲染
        this.message = "Hello World!";
      },
    },
  });
</script>

响应式对象

其实Object.defineProperty应该都听过了,嗯,主要就是用这个属性。

普通获取对象属性,设置对象属性都可以用这个,但是比较麻烦,所以一般分场景使用。

/** 普通创建属性的方式: **/
var obj = { a: 1 };
/** defineProperty创建、获取和设置属性的方式: **/
var obj = {};
// !注意需要另设一个变量,存储属性的值。
let value = 1;
Object.defineProperty(obj, "a", {
  get() {
    console.log("get");
    return value;
  },
  set(newValue) {
    console.log("set");
    value = newValue;
  },
});
/* 当然获取属性和设置的话,是一样的 */
// 获取
console.log(obj.a);
// 设置
obj.a = 2;

网络异常,图片无法展示
|

Object.defineProperty的核心就是getset,因为是函数,所以能做很多事。

所谓的劫持,每次获取/设置属性的时候,都会执行get/set函数,既然是函数,自然能做一些别的事情。

所谓的响应式对象,当对象的某属性有get/set,就称为响应式对象。

简版的响应式Vue

先写个简版的响应式Vue,大致有个印象,真实的源码处理的情景比较多,看懂简版的,再看源码,不容易迷路。。。

主要做了以下几件事:

  • vm._data指向options.data
  • vm._data上面的属性都代理到vm
  • vm._data.__ob__指向Observer实例,而这个实例的value是data的响应式
const vm = new Vue({ data: { a: 1 } });
console.dir(vm);
function Vue(options) {
  this.vm = this;
  this.$options = options;
  /* this._init() initState()开始 */
  initData(this);
  /* this._init() initState()结束 */
}
function initData(vm) {
  let data = vm.$options.data;
  vm._data = data;
  // 将data上面的属性直接挂在vm上
  Object.keys(data).forEach((key) => {
    /* proxy(vm, "_data", key) 开始 */
    Object.defineProperty(vm, key, {
      enumerable: true,
      configurable: true,
      get() {
        return vm._data[key];
      },
      set(newValue) {
        vm.data[key] = newValue;
      },
    });
    /* proxy(vm, "_data", key) 结束 */
  });
  /* observe(data) 开始 */
  data.__ob__ = data.__ob__ || new Observer(data);
  /* observe(data) 结束 */
}
function Observer(data) {
  this.value = data;
  // this.dep = new Dep();
  data.__ob__ = this;
  Object.keys(data).forEach((key) => {
    defineReactive(data, key);
  });
}
// defineReactive将 obj.x 这种定义属性的方式  变成Object.defineProperty(obj, 'x' , {get(){}}
function defineReactive(data, key) {
  // const dep = new Dep();
  let value = data[key];
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // dep.depend();
      return value;
    },
    set: function reactiveSetter(newValue) {
      // dep.notify();
      value = newValue;
    },
  });
}

网络异常,图片无法展示
|

initState

Vue 的构造函数,开始就是this._init(..),而Vue.prototype._init = function(){initState(this)}

这里的initState就是让Vue实例变成响应式对象的关键,这个方法就是对options进行各种初始化的操作,而本文的重点是对data/props的处理。

// src/core/instance/state.js
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    initData(vm);
  } else {
    observe((vm._data = {}), true /* asRootData */);
  }
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

props 的处理

先看initProps,主要其实就是

  • 遍历options.props
  • 每个 prop 变成响应式(set/get),且每个 prop,也同步到vm._props.xx
  • 通过proxyvm._props.xxx 的访问代理到 vm.xxx
// src/core/instance/state.js
function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {};
  const props = (vm._props = {});
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = (vm.$options._propKeys = []);
  const isRoot = !vm.$parent;
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false);
  }
  for (const key in propsOptions) {
    keys.push(key);
    const value = validateProp(key, propsOptions, propsData, vm);
    defineReactive(props, key, value);
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key);
    }
  }
  toggleObserving(true);
}

data 的处理

data 的初始化也做了两件事:

  • 遍历data对象,每一个vm._data.xx通过proxy代理到vm._data
  • 调用observe观察data变化,将其也变成响应式
// src/core/instance/state.js
function initData(vm: Component) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  if (!isPlainObject(data)) {
    data = {};
  }
  // proxy data on instance
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    if (!isReserved(key)) {
      proxy(vm, `_data`, key);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

proxy 代理

初始化 data 和 props 一个关键操作,就是让他们变成响应式,然后代理到vm上。

代理?其实就是就是一个中介。本身只是一个线索,会连接到真实的资源。

举个例子,看下面的obj,有个data属性,现在想obj.a也可以访问到 a 属性,怎么办?

此时obj.a就是一个中介,其真实连接的资源是obj.data.a

const obj = { data: { a: 1 } };

其实还是利用上面的Object.defineProperty

Object.defineProperty(obj, "a", {
  get() {
    // 这里的obj.data.a就相当于存储变量
    return obj.data.a;
  },
  set(newValue) {
    obj.data.a = newValue;
  },
});
// 1
console.log(obj.a);

现在看源码里,对于proxy的实现:

// src/core/instance/state.js
// const noop = function empty(){}
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
};
// target相当于obj,sourceKey相当于data,key相当于a
// proxy就是可以让vm.x 代理到 vm.data.x
export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}

observe:给增加一个 Observer 实例

observe 的功能就是用来监测数据的变化,给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。

// src/core/observer/index.js
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob: Observer | void;
  if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob;
}

Observer 类:给对象的每个属性添加 getter 和 setter

Observer 是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新:

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data
  constructor(value: any) {
    this.value = value;
    // 实例化 Dep 对象
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, "__ob__", this);
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk(obj: Object) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }
  /**
   * Observe a list of Array items.
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

Observer 的构造函数逻辑很简单:

  • 实例化 Dep 对象,
  • 通过执行 def 函数把自身实例添加到数据对象 value__ob__ 属性上

def就是给一个对象,定义一个属性,设置一个属性值,默认此属性是不可遍历的。

// def的定义
// src/core/util/lang.js
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
  });
}

def相当于Object.defineProperty的简单封装。

defineReactive:给对象的某个属性动态添加 getter 和 setter

defineReactive 的功能就是给对象动态添加 gettersetter,让对象成为响应式对象:

// src/core/observer/index.js
/**
 * Define a reactive property on an Object.
 */
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 初始化 Dep 对象的实例
  const dep = new Dep();
  // 拿到 obj 的属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }
  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    },
  });
}

defineReactive 函数:

  • 最开始初始化 Dep 对象的实例,
  • 接着拿到 obj 的属性描述符,
  • 然后对子对象递归调用 observe方法

这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象, 这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 gettersetter

最后利用 Object.defineProperty 去给 obj 的属性 key 添加 gettersetter

总结

响应式对象,核心就是利用 Object.defineProperty 给数据添加了 gettersetter。 这样在访问数据以及写数据的时候能自动执行一些逻辑:

  • getter 做的事情是依赖收集
  • setter 做的事情是派发更新

引用

目录
相关文章
|
3天前
|
资源调度 JavaScript 前端开发
Vue的路由管理:VueRouter的配置和使用
【4月更文挑战第24天】VueRouter是Vue.js的官方路由管理器,用于在单页面应用中管理URL路径与组件的映射。通过安装并引入VueRouter,设置路由规则和创建router实例,可以实现不同路径下显示不同组件。主要组件包括:`&lt;router-link&gt;`用于创建导航链接,`&lt;router-view&gt;`负责渲染当前路由对应的组件。此外,VueRouter还支持编程式导航和各种高级特性,如嵌套路由、路由参数和守卫,以应对复杂路由场景。
|
1天前
|
监控 JavaScript
Vue中的数据变化监控与响应——深入理解Watchers
Vue中的数据变化监控与响应——深入理解Watchers
|
1天前
|
JavaScript 安全 前端开发
Vue 项目中的权限管理:让页面也学会说“你无权访问!
Vue 项目中的权限管理:让页面也学会说“你无权访问!
10 3
|
1天前
|
JavaScript 前端开发 开发者
Vue的神奇解锁:冒险的开始
Vue的神奇解锁:冒险的开始
5 1
|
2天前
|
JavaScript 前端开发
【vue】iview如何把input输入框和点击输入框之后的边框去掉
【vue】iview如何把input输入框和点击输入框之后的边框去掉
8 0
|
2天前
|
JavaScript
【vue实战】父子组件互相传值
【vue实战】父子组件互相传值
8 1
|
2天前
|
JavaScript
vue2_引入Ant design vue
vue2_引入Ant design vue
8 0
|
2天前
|
JavaScript
vue知识点
vue知识点
10 4
|
3天前
|
存储 JavaScript 前端开发
【Vue】绝了!这生命周期流程真...
【Vue】绝了!这生命周期流程真...
|
3天前
|
JavaScript 索引
【vue】框架搭建
【vue】框架搭建
7 1