Vue核心 — Vue2响应式原理和核心源码解析(核心中的核心)

一、前置知识

1、Vue 核心概念

Vue 是什么?

Vue 是一款用于构建用户界面的 JavaScript 框架 。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式 的、组件化的编程模型,帮助你高效地开发用户界面。

Vue 核心特点是什么?

响应式数据绑定:

Vue.js 实现了数据的双向绑定,即当数据发生变化时,视图会自动更新,反之亦然。这使得开发者可以更轻松地管理和更新数据,同时保持视图与数据的同步。

组件化开发:

Vue.js 将页面拆分为多个独立的组件 ,每个组件负责自己的视图和逻辑。这种组件化的开发方式使得代码更加模块化可维护性更高,也有利于团队协作。

虚拟 Dom:

Vue.js 使用虚拟 DOM 技术,将页面的 DOM 结构表示为 JavaScript 对象,通过比较新旧虚拟 DOM 树的差异,最小化 DOM 操作,从而提高页面的性能和效率。

指令:

Vue.js 提供了丰富的指令、用于在模板中添加特定的功能或行为。指令使得开发者可以更便捷地操作 DOM 元素,实现动态数据绑定、条件渲染等功能。

插件系统:

Vue.js 提供了灵活的插件系统,允许开发者根据项目需求扩展 Vue 的功能

二、Vue2 的响应式原理

1、理解什么是响应式数据?

什么不是响应式数据?

数据和视图(dom属性值)相互独立、互相并不影响、即数据发生变化视图并不发生变化、视图发送变化数据并不发生变化、想要实现双向绑定、得在一方的值发生变化时去修改另一方的值。

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>测试响应式</title>
</head>
<body>
  <input type="text">
  <button>value++</button>
  <script>
    // 设置 value 初始值
    let value = 1
    // 读取输入框 dom 元素
    const ipt = document.querySelector('input')
    // 按钮 dom 元素
    const btn = document.querySelector('button')

    // 设置 ipt 的 value 值为 value
    ipt.value = value

    // 为输入框绑定输入事件
    ipt.addEventListener('input', (event) => {
      console.log('iptValue: ', event.target.value);
      console.log('value ', value);;
      // 重新为 value 赋值
      // value = event.target.value
    })
    
    // 为按钮绑定点击事件
    btn.addEventListener('click', (event) => {
      value++
      // 重新为 ipt.value 赋值
      // ipt.value = ipt.value
      console.log('iptValue: ',ipt.value)
      console.log('value ', value);
    })
  </script>
</body>

</html>

什么是响应式数据?

数据发生变化、视图绑定该数据会自动更新、反之亦然。详细说明、例如页面上的表单元素通过v-model: value 绑定 data 方法里返回的对象属性值 value、当 value 值发送变化时视图会自动更新、在页面上修改表单元素时(修改value值)、data 方法里返回的对象属性值 value 也会同步变化。

为什么数据发送变化视图也会更新呢、底层源码是如何实现的?

如上图所示、这就是响应式对象发送变化时视图发送变化的机制。让我们一步一步的来剖析。

2、Vue 初始化做了什么?(重点关注状态初始化)

javascript 复制代码
new Vue({
  render: h => h(App)
}).$mount('#app')

上面代码大家都很熟悉、简单来讲就是通过new Vue({render: h => h(App)}) 创建一个 Vue 实例、render: h => h(App) 就是一个指令,告诉 Vue 使用 createElement 函数来创建 App 组件的虚拟 DOM 对象 ,然后将实例通过其内置的 $mount 方法挂载到 id 为 app 根节点上。

从 new 操作符、咱们可以看出 Vue 其实就是一个构造函数、没啥特别的、传入的参数就是一个对象、源码中我们叫做 options(选项)。

让我们来看看构造函数做了什么?

javascript 复制代码
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
import type { GlobalAPI } from 'types/global-api'

function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

//@ts-expect-error Vue has function type
initMixin(Vue)
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)

export default Vue as unknown as GlobalAPI

很明显、核心在于调用了方法 this._init(options) 这里开始进行 Vue 实例的初始化工作

options 就是传入的虚拟 dom 对象。

那么 _init() 方法是从哪里来的呢? _init() 方法内部干了什么?

核心关注 initMixin(Vue)

_init()这个方法就是 initMixin(Vue) 在 vue 实例的原型上挂载的。

javascript 复制代码
export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to mark this as a Vue instance without having to do instanceof
    // check
    vm._isVue = true
    // avoid instances from being observed
    vm.__v_skip = true
    // effect scope
    vm._scope = new EffectScope(true /* detached */)
    // #13134 edge case where a child component is manually created during the
    // render of a parent component
    vm._scope.parent = undefined
    vm._scope._vm = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options as any)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

让我们来关注几个核心点

(1) Vue 组件实例的选项 (vm.$options)初始化。

如果是内部组件 :通过 initInternalComponent 函数 优化初始化

如果是非内部组件 :通过 mergeOptions函数 合并传入的 options 对象和 当前 Vue 实例成为最终的 $options 对象

javascript 复制代码
if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options as any)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }

(2) 生命周期、事件、渲染初始化:

javascript 复制代码
// 生命周期初始化
initLifecycle(vm)
// 事件初始化
initEvents(vm)
// 渲染初始化
initRender(vm)

(3) 状态初始化(核心重点):

javascript 复制代码
initState(vm)
initState 干了什么?
javascript 复制代码
export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)

  // Composition API
  initSetup(vm)

  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    const ob = observe((vm._data = {}))
    ob && ob.vmCount++
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

可以看到依次初始化了

props > setup > methods > data > computed > watch

想要弄清楚 Vue2 的响应式原理 重点得关注初始化 data 上

javascript 复制代码
if (opts.data) {
  initData(vm)
} else {
  const ob = observe((vm._data = {}))
  ob && ob.vmCount++
}

如果组件定义了 data ,则直接调用 initData(vm) 来初始化;如果没有定义 data ,则创建一个空对象,并将其转换为响应式对象

javascript 复制代码
function initData(vm: Component) {
  let data: any = vm.$options.data;
  // 获取组件配置中的 data 选项
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
  // 如果 data 是一个函数,则通过 getData 函数获取其返回值,否则直接使用 data 或者默认为空对象 {}

  if (!isPlainObject(data)) {
    // 如果 data 不是一个纯对象,给出开发者警告(开发环境下)
    data = {};
    __DEV__ &&
      warn(
        'data functions should return an object:\n' +
          'https://v2.vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      );
  }

  // proxy data on instance
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  // 遍历 data 的键,将每个键设置为 vm 实例的代理属性(如果不是保留键)

  while (i--) {
    const key = keys[i];
    if (__DEV__) {
      // 在开发环境下,检查是否有同名方法已经定义为数据属性
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm);
      }
    }
    if (props && hasOwn(props, key)) {
      // 如果 data 的属性已经被声明为 prop,给出警告
      __DEV__ &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        );
    } else if (!isReserved(key)) {
      // 如果属性不是保留属性,则将其代理到 vm 实例上
      proxy(vm, `_data`, key);
    }
  }

  // observe data
  // 观察数据,确保数据变化时可以通知相关的依赖
  const ob = observe(data);
  if (ob) {
    // 如果成功创建了观察对象,则增加其引用计数
    ob.vmCount++;
  }

可以看到无论如何都会执行 observe(data) 、就是其让data数据变成响应式数据的。

observe 干了什么?
javascript 复制代码
/**
 * 尝试为一个值创建观察者实例,
 * 如果成功观察,则返回新的观察者实例,
 * 如果值已经有观察者实例,则返回现有的观察者。
 *
 * @param value 需要观察的值。
 * @param shallow 是否执行浅层观察。
 * @param ssrMockReactivity 是否在服务端渲染时模拟响应性。
 * @returns 如果成功观察到,则返回 Observer 实例,否则返回 void。
 */
export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {
  // 检查值是否已经有 __ob__ 属性,并且该属性是 Observer 的实例
  if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    return value.__ob__; // 返回现有的观察者实例
  }

  // 创建新的观察者实例的条件判断
  if (
    shouldObserve && // 全局标志,确定是否进行观察
    (ssrMockReactivity || !isServerRendering()) && // 确保在非服务端渲染时或者模拟响应性时启用响应性
    (isArray(value) || isPlainObject(value)) && // 值必须是数组或普通对象
    Object.isExtensible(value) && // 值必须是可扩展的(即未被密封)
    !value.__v_skip /* ReactiveFlags.SKIP */ && // 值不能被标记为跳过观察
    !isRef(value) && // 值不能是 ref 对象
    !(value instanceof VNode) // 值不能是 Vue 虚拟节点
  ) {
    // 创建一个新的 Observer 实例来观察该值
    return new Observer(value, shallow, ssrMockReactivity);
  }

  // 如果不满足观察条件,则返回 void
}

初始化传进来的 vm._data 满足上述条件、所以会执行

return new Observer(value, shallow, ssrMockReactivity);

Observer 里干了什么?

核心: 是将传入的 data 对象(或数组)转换为响应式对象(或响应式数组)

javascript 复制代码
/**
 * Observer 类被附加到每个被观察的对象上。
 * 一旦附加,Observer 将目标对象的属性键转换为 getter 和 setter,
 * 用于收集依赖和分发更新。
 */
export class Observer {
  dep: Dep; // 依赖管理对象
  vmCount: number; // 使用该对象作为根 $data 的 vm 数量

  constructor(public value: any, public shallow = false, public mock = false) {
    // 初始化 Observer 实例
    this.dep = mock ? mockDep : new Dep(); // 创建依赖管理对象 Dep
    this.vmCount = 0; // 记录有多少个 vm 实例使用该对象作为根 $data

    // 在值 value 上定义 __ob__ 属性,指向当前 Observer 实例
    def(value, '__ob__', this);

    // 如果值是数组
    if (isArray(value)) {
      if (!mock) {
        if (hasProto) {
          /* eslint-disable no-proto */
          // 如果支持原型链修改,则将数组的原型指向 arrayMethods
          ;(value as any).__proto__ = arrayMethods;
          /* eslint-enable no-proto */
        } else {
          // 否则,逐个定义数组的方法
          for (let i = 0, l = arrayKeys.length; i < l; i++) {
            const key = arrayKeys[i];
            def(value, key, arrayMethods[key]);
          }
        }
      }

      // 如果不是浅层观察,则递归观察数组中的每一项
      if (!shallow) {
        this.observeArray(value);
      }
    } else { // 如果值是普通对象
      // 遍历对象的所有属性,转换为 getter/setter
      const keys = Object.keys(value);
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i];
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock);
      }
    }
  }

  /**
   * 观察数组中的每一项。
   */
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock);
    }
  }
}

def(value, key, arrayMethods[key]):将数组数据变成响应式数据核心方法。

defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock):将对象数据转化成响应式数据核心方法。

defineReactive 干了什么?

将一个对象的属性变成响应式,即当属性被访问或修改时能触发相应的依赖收集和通知更新操作。核心方法就是 Object.defineProperty()

javascript 复制代码
/**
 * 在对象上定义一个响应式属性。
 * @param obj 要定义属性的对象。
 * @param key 要定义的属性的名称。
 * @param val 可选,属性的初始值。
 * @param customSetter 可选,自定义的 setter 函数。
 * @param shallow 可选,是否进行浅层观察。
 * @param mock 可选,是否模拟对象。
 * @param observeEvenIfShallow 默认为 false,即使是浅层观察也进行观察。
 */
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean,
  observeEvenIfShallow = false
) {
  const dep = new Dep(); // 创建一个依赖对象

  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return; // 如果属性已存在且不可配置,直接返回
  }

  // 处理预定义的 getter 和 setter
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && (val === NO_INITIAL_VALUE || arguments.length === 2)) {
    val = obj[key]; // 获取属性的初始值
  }

  // 观察子对象,决定是否进行深层观察
  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock);
  
  // 定义属性的 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val; // 获取属性值
      if (Dep.target) {
        if (__DEV__) {
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          }); // 进行依赖收集
        } else {
          dep.depend();
        }
        if (childOb) {
          childOb.dep.depend(); // 子对象依赖收集
          if (isArray(value)) {
            dependArray(value); // 数组依赖收集
          }
        }
      }
      return isRef(value) && !shallow ? value.value : value; // 如果是 ref 类型且不是浅层观察,返回其值
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val; // 获取属性当前值
      if (!hasChanged(value, newVal)) {
        return; // 新旧值相同则直接返回
      }
      if (__DEV__ && customSetter) {
        customSetter(); // 在开发环境下调用自定义的 setter 函数
      }
      if (setter) {
        setter.call(obj, newVal); // 使用属性的原始 setter 函数
      } else if (getter) {
        // 如果属性是 accessor 类型但没有 setter
        return;
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal; // 处理 ref 类型属性的赋值
        return;
      } else {
        val = newVal; // 更新属性值
      }
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock); // 更新子对象的观察状态
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        }); // 发送属性变更通知
      } else {
        dep.notify();
      }
    }
  });

  return dep; // 返回依赖对象
}
Object.defineProperty() 干了什么
首先得知道 Object.defineProperty() 是什么?
Obeject.defineProperty() 概念

Obeject.defineProperty 是一个用于定义或修改对象属性的方法 。它允许你精确地控制属性的特性,包括可写性、可枚举性、可配置性以及访问器方法(getter 和 setter)。

使用语法
javascript 复制代码
Object.defineProperty(obj, 'age',descriptor);
  • obj: 要在其上定义属性的对象。
  • prop: 要定义或修改的属性的名称或 Symbol。
  • descriptor: 描述符对象,定义了要定义或修改的属性的特性。
描述符对象(descriptor)
  1. value : 设置属性的值(仅适用于数据属性)。默认为 undefined不能与 getset 同时使用。

  2. writable : 值是否可写。默认为 false。如果为 true,则属性的值可以被赋值运算符改变。

  3. enumerable : 属性是否可以被枚举。默认为 false 。如果为 true ,则属性可以在 for...in 循环和 **Object.keys**方法中被枚举。

  4. configurable : 属性是否可以被删除或修改特性。默认为 false 。如果为 true ,则可以使用 Object.defineProperty 修改该属性的特性,或者使用 **delete**删除该属性。

  5. get : 属性的 getter 函数,当访问该属性时调用。不能与 value 或 **writable**同时使用。

  6. set : 属性的 setter 函数,当属性被赋值时调用。不能与 value 或 **writable**同时使用。

注意事项
  • 在非严格模式下,如果尝试写入一个不可写属性,赋值操作将会静默失败。
  • 不能同时在同一个属性描述符对象中使用 valuegetset
  • 一旦将属性设置为不可配置 (configurable : false),则不能再修改其特性,也不能删除该属性。
代码示例
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>测试代码执行顺序</title>
</head>
<body>
  <script>
    // 定义数据属性
    let objOne = {};
    Object.defineProperty(objOne, 'myProperty', {
      value: 42,
      writable: true,
      enumerable: true,
      configurable: true
    });
    console.log(objOne.myProperty); // 输出: 42
    objOne.myProperty = 50;
    console.log(objOne.myProperty); // 输出: 50

    // 定义访问器属性 
    let objTwo = {
      _myProperty: 0
    };
    Object.defineProperty(objTwo, 'myProperty', {
      get: function() {
        console.log('获取值');
        return this._myProperty;
      },
      set: function(value) {
        if(value ===  this._myProperty) {
          return
        }
        console.log('设置值');
        this._myProperty = value;
      },
      enumerable: true,
      configurable: true
    });
    // 设置属性值 myProperty 就是在调用 set 方法
    objTwo.myProperty = 10;
    // 访问属性值 myProperty 就是在调用 get 方法
    console.log(objTwo.myProperty); // 输出: 10
  </script>
</body>
</html>

源码中的 Object.defineProperty()干了什么?

javascript 复制代码
javascript
Object.defineProperty(obj, key, {
  enumerable: true,        // 可枚举
  configurable: true,      // 可配置
  get: function reactiveGetter() {
    const value = getter ? getter.call(obj) : val;  // 获取属性值
    if (Dep.target) {  // 如果存在正在依赖此属性的 Watcher
      if (__DEV__) {
        // 在开发模式下进行依赖收集
        dep.depend({
          target: obj,
          type: TrackOpTypes.GET,
          key
        });
      } else {
        dep.depend();  // 正常情况下进行依赖收集
      }
      if (childOb) {
        childOb.dep.depend();  // 如果存在子响应式对象,也进行依赖收集
        if (isArray(value)) {
          dependArray(value);  // 如果值是数组,还需依赖收集数组的元素
        }
      }
    }
    // 如果值是 Ref 对象且不是浅层响应式,则返回其实际值;否则返回原始值
    return isRef(value) && !shallow ? value.value : value;
  },
  set: function reactiveSetter(newVal) {
    const value = getter ? getter.call(obj) : val;  // 获取当前属性值
    if (!hasChanged(value, newVal)) {
      return;  // 如果新旧值相同,则不进行更新
    }
    if (__DEV__ && customSetter) {
      customSetter();  // 在开发模式下,如果定义了自定义 setter,则调用它
    }
    if (setter) {
      setter.call(obj, newVal);  // 如果定义了 setter 函数,则调用它设置新值
    } else if (getter) {
      // 如果只定义了 getter 函数但未定义 setter,不执行任何操作(适用于只读属性)
      return;
    } else if (!shallow && isRef(value) && !isRef(newVal)) {
      value.value = newVal;  // 如果属性值是 Ref 对象且不是浅层响应式,则设置其值
      return;
    } else {
      val = newVal;  // 否则直接更新属性的值
    }
    // 更新子响应式对象的依赖关系
    childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock);
    if (__DEV__) {
      // 在开发模式下,通知依赖此属性的 Watcher 进行更新
      dep.notify({
        type: TriggerOpTypes.SET,
        target: obj,
        key,
        newValue: newVal,
        oldValue: value
      });
    } else {
      dep.notify();  // 正常情况下通知依赖进行更新
    }
  }
});
get和set核心就是调用dep的两个方法depend()和notify()

dep核心就是调用Watcher的两个方法get()和update()

javascript 复制代码
// 定义一个 Dep 类,用于管理依赖和通知更新(演示非源码)

export default class Dep {
  constructor() {
    this.subs = []; // subs 数组用来存储 Watcher 对象
  }

  // 添加 Watcher 对象到 subs 数组
  addSub(sub) {
    this.subs.push(sub);
  }

  // 从 subs 数组移除指定的 Watcher 对象
  removeSub(sub) {
    remove(this.subs, sub); // 使用 remove 函数移除
  }

  // 当前 Dep 对象收集依赖
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this); // 将当前 Dep 对象添加到 Watcher 的依赖列表中
    }
  }

  // 通知所有依赖于该 Dep 对象的 Watcher 执行更新操作
  notify() {
    // 遍历 subs 数组,调用每个 Watcher 的 update 方法
    const subs = this.subs.slice(); // 使用 slice() 创建副本,避免在遍历过程中被修改
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update(); // 调用 Watcher 的 update 方法
    }
  }
}

Dep.target = null; // 静态属性,用来存储当前正在执行的 Watcher 对象
const targetStack = []; // Watcher 栈,用于处理嵌套依赖

// 将指定的 Watcher 对象推入 Watcher 栈中
export function pushTarget(target) {
  if (Dep.target) targetStack.push(Dep.target);
  Dep.target = target; // 将当前 Watcher 对象赋值给 Dep.target
}

// 从 Watcher 栈中弹出最后一个 Watcher 对象
export function popTarget() {
  Dep.target = targetStack.pop(); // 恢复 Watcher 栈的上一个 Watcher 对象
}
javascript 复制代码
export default class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm; // Vue 实例
    this.getter = expOrFn; // 数据获取函数
    this.cb = cb; // 回调函数,数据变化时触发
    this.value = this.get(); // 初始化时执行 getter 函数,获取当前数据的值
  }

  // 更新 Watcher 的回调函数
  update() {
    const oldValue = this.value;
    this.value = this.get(); // 重新获取数据的值
    this.cb.call(this.vm, this.value, oldValue); // 执行回调函数,通知数据变化
  }

  // 评估 getter 函数,建立依赖关系
  get() {
    pushTarget(this); // 将当前 Watcher 对象推入 Watcher 栈中
    const vm = this.vm;
    let value;
    try {
      value = this.getter.call(vm, vm); // 执行 getter 函数,获取当前数据的值
    } catch (e) {
      throw e;
    } finally {
      popTarget(); // 从 Watcher 栈中弹出最后一个 Watcher 对象
    }
    return value;
  }
}
get方法核心 - 收集依赖 dep.depend()
  • 当访问(get)响应式对象的属性时,Vue.js 会收集当前正在执行的 Watcher 对象作为依赖 。这个过程是通过 dep.depend() 实现的,其中 dep 是依赖对象(Dep 实例)。
  • Watcher 对象可以理解为观察者 ,它负责响应式数据与视图之间的绑定关系。当数据变化时,与之相关的 Watcher 将被通知,从而更新视图。
set方法核心 - 通知依赖更新dep.notify()
  • 当设置(set)响应式对象的属性时,Vue.js 会调用 dep.notify() 来通知所有依赖于该属性的 Watcher 进行更新。
  • 这意味着所有观察此数据的视图组件将会重新渲染以反映数据变化。

总结

模板中绑定data数据发送变化时为什么视图会同步更新呢?

这时候这张图就更高理解了。

  1. 响应式对象(响应式原理)

    • 当你将一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 会遍历这个对象的属性,并使用 Object.defineProperty 或 **Proxy**将每个属性转换为 getter 和 setter、引用data数据实际上是访问数据对象属性的 get 方法、修改数据实际上是在调用数据对象属性的set方法。
    • 这样一来,当属性被访问或修改时,Vue 能够捕捉到这些操作,并触发相应的依赖更新。
  2. 依赖追踪与 Watcher

    • Vue 内部维护了一个依赖收集的系统。每个响应式属性都会有一个关联的 Watcher 对象。
    • 当属性被访问时,Watcher 会将当前组件实例与这个属性建立关联(依赖追踪),确保在属性变化时能够通知相关的 Watcher 执行更新操作。
  3. 虚拟 DOM 及更新优化(虚拟dom和diff算法)

    • Vue 使用虚拟 DOM 来提高渲染效率。当数据发生变化时,Vue 会生成新的虚拟 DOM,并通过比对算法找出变化的部分,然后更新到实际 DOM 中,而不是直接操作实际 DOM。
  4. 异步更新队列

    • Vue 在更新数据时是异步执行的,多个数据的变化会被合并成一个更新操作,以提高性能并避免不必要的计算和 DOM 操作。
  5. 渲染 Watcher

    • 每个组件实例都有一个渲染 Watcher,它是 Vue 在实例化过程中自动创建的。这个 Watcher 负责将组件的 render 函数渲染成虚拟 DOM,并在数据变化时重新渲染组件。

(4) hook 的调用(从源码上理解 beforeCreate 和 created 调用时机)

javascript 复制代码
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

这里也很好理解了生命周期hook 中的 beforeCreate 和 created 的调用时机。

beforeCreate 在状态初始化前、这时状态数据的肯定是不能用的

created 在状态初始化完成后调用。

3、总结响应式原理

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
沈梦研5 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb5 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角5 小时前
CSS 颜色
前端·css
轻口味6 小时前
Vue.js 组件之间的通信模式
vue.js
浪浪山小白兔6 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter