【前端面试必看】深度解读Vue2和Vue3的响应式

目录

前言

一、响应式的基本原理

1. 数据劫持

2. 收集依赖

3. 派发更新

二、观察者模式

三、Vue2的响应式原理

1. 数据劫持(Observer处理)

2. Vue2中的Watcher 和 Dep

3. 依赖收集

4. 派发更新

四、Vue3的响应式原理

1. 数据劫持

2. 依赖收集 

3. 派发更新

4. 拾遗

4.1 Proxy和Reflect

4.2 结合生命周期看Vue2的初始化

总结


前言

现在的前端面试越来越卷了有没有,面试造火箭,工作打螺丝。面试的时候不深度分析下Vue的响应式原理,就约等于你不会这玩意儿。那今天咱就来分析下Vue2和Vue3的响应式。


一、响应式的基本原理

Vue的数据绑定,将template中添加的变量,与定义的属性关联起来,当属性值发生变化时,使用该属性的页面位置,自动发生变化,而不需要开发者去操作dom。
在这个过程中,响应式的作用是:

1. 将template中的变量与定义的属性关联起来

2. 当属性发生变化时,通知对应的页面/组件重新渲染

 要达成上述目标,那就需要做如下工作:

1. 数据劫持

数据劫持的目标是,对于vue组件中定义的属性(vue2中在data中定义的,vue3使用reactive等方法声明的),vue需要对其进行劫持,当其被访问和设置的时候,vue需要实时得知,并做出反应。

我们知道,在vue2中,是使用Object.defineProperty实现,vue3是借助了ES6的Proxy API。

2. 收集依赖

收集依赖的目标是,vue需要记录,某个属性,是否被使用,它的变化,需要引起哪些变化。比如触发computed、watch中对应位置的回调,以及触发某个页面组件的重新渲染。

3. 派发更新

当属性发生变化后,需要根据记录的依赖,一一通知对应的方法做出反应。比如执行computed、watch中对应位置的回调,对应页面组件的重新渲染。

二、观察者模式

在Vue依赖收集和派发更新的过程中,一个属性发生变化,多处处理方法需要做出响应,且必须做出响应。天然适合观察者模式。

观察者模式中有两种角色,观察目标、观察者,简单的实现逻辑如下

// 观察者
class Observer {
  constructor(fn) {
     this.update = fn;
  }
}
// 观察目标
class Subject {
   constructor() {
      this.observers = [];
   }
   addObserver(observe) {
      this.observers.push(observe);
   }
   removeObserver(observe) {
      const index = this.observers.findIndex(v => v == observe);
      if (index >= 0) {
        this.observers.splice(index, 1);
      }
   }
   notify(context) {
      this.observers.forEach(v => {
         v.update(context);
      });
   }
}

const sub1 = new Subject();
const sub2 = new Subject();
const obs1 = new Observer((context) => {
  console.log("obs1监听,收到值变化", context);
})
const obs2 = new Observer((context) => {
  console.log("obs2监听,收到值变化", context);
})
sub1.addObserver(obs1);
sub1.addObserver(obs2);
sub2.addObserver(obs2);
sub1.notify("狗子,你变了");
sub2.notify("是的,我变了");

执行结果如下: 

观察者有各自的回调函数,观察目标维护一个观察者列表,当其调用notify方法发布通知时,顺次执行观察者列表中所有观察者的回调函数,通知的内容以参数形式传给观察者回调。

可以看到,一个观察目标可以有多个观察者,一个观察者也可以去观察多个观察目标。

放到响应式过程中来说,vue组件的属性是观察目标,视图、计算属性、侦听器,是观察者

三、Vue2的响应式原理

三个核心 Observer(数据观测者)  Watcher(视图更新者)  Dep(依赖收集器)

1. 数据劫持(Observer处理)

Vue2的数据劫持,使用Object.defineProperty

Object.defineProperty(data,key,{
    enumerable:true,
    configurable:true,
    get(){
      console.log("试图访问"+key+"属性")
      return val
    },
    set(newValue){
      console.log("试图修改"+key+"属性")
      if(val === newValue) return
      val = newValue
    }
})

在vue组件初始化时,会对data中的属性进行深度遍历,通过这种方式给每个属性添加getter和setter,当数据被访问或改变时,vue都能第一时间获知。

特别地,对于数组的操作,push、splice、unshift等,都可以给数组添加新的数组项,vue直接重写了这些方法,给新添加的数组项内的属性,也添加响应式。

这种方案存在一些缺陷:

新增属性、删除属性、利用数组下标修改值、对数组.length的修改、Map、WeakMap、Set、WeakSet这些数据结构不支持。因为这些操作,getter和setter监听不到,故而无法做出响应式处理。

2. Vue2中的Watcher 和 Dep

Watcher有三类,渲染watcher,computed的watcher,watch的watcher。在执行过程中,computed的watcher先执行,watch的watcher次之,渲染watcher最后。对应着,先运算值,再根据值的变化进行一轮用户自定义的处理,此时本轮值运算已经结束,有了结果,最后通知render函数将结果渲染到页面上。

一个组件,对应一个渲染watcher,根据用户自定义代码,可以有若干个computed watcher和watch watcher。

一个组件有多个属性,一个computed可能与多个属性相关,所以一个watcher,可能与多个属性相关,也就对应多个dep。

Dep用于收集某个属性对应的watcher,根据上面对watcher的表述可以看出,一个属性可能有多个watcher。

watcher中也会维护哪些属性的变化会触发自身反应,一个watcher可能对应多个dep。

3. 依赖收集

我们来看看渲染watcher依赖收集过程,computed和watch后续再讨论。

渲染watcher的依赖收集发生在什么时候?

function Vue (options) {
	// ....
  this._init(options)
}
 
Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // *** 省略部分逻辑
  // initState中对我们传入对data进行了数据劫持
  initState(vm)
  // 调用$mount开始挂载 
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

我们new Vue时调用了_init方法。

initState进行了数据劫持,此时生命周期处于created后。执行$mount方法。


//$mount调用的实际是mountComponent方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 在此处,query方法寻找组件要挂载到的dom节点。
  // 如果el是字符串,则用querySelector找,找不到就新建一个div返回。
  // 如果el是dom,则直接返回
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
 
//mountComponent方法
function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el;
  // 在此处,将template转为render函数,vm._render()
  callHook(vm, "beforeMount");
  let updateComponent;
  // ...
	updateComponent = () => {
      vm._update(vm._render(), hydrating);
    };
	// 
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, "beforeUpdate");
        }
      },
    },
    true /* isRenderWatcher */
  );
  hydrating = false;
 
  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, "mounted");
  }
  return vm;
}

在 beforeMount生命周期前,根据template生成了render函数,然后将render函数包装在updateComponent方法中。

创建组件的渲染watcher,updateComponent方法被作为入参传入。

 创建渲染watcher实例后,就到了mounted生命周期,那么依赖收集就应该在实例化渲染watcher的过程中

接下来看看Watcher的源码


export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // ...
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()

    this.deps = []
  }
 
  
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  addDep(dep) {
    this.deps.push(dep);
    dep.addSub(this);
  }
}

看构造方法,入参有组件实例vm、expOrFn(在渲染watcher实例中实际就是render函数)等。

Watcher实例化时执行构造方法,将render方法expOrFn赋值给this.getter

如果不是懒加载,执行this.get()方法。

get方法中

首先执行pushTarget方法

Dep.target = null
const targetStack = []
 
function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

全局有一个targetStack的数组,当pushTarget调用时,将当前的watcher实例入栈,并将Dep.target设置为当前watcher实例。

继续看watcher的get方法,执行了这一句

value = this.getter.call(vm, vm)

我们知道,此时this.getter是组件的render函数,这里执行了render函数,用于生成虚拟dom,里面的属性会替换为其真实的值,就需要访问对应的属性值,从而触发属性数据劫持时监听的get方法。依赖收集,就在此处发生。

popTarget的作用是targetStack出栈,将Dep.target设置为栈顶元素对应的watcher

下一步就去数据劫持的defineReactive方法,看get做了什么


function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // getter和settter是做什么的?
  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
    }
  })
}

Tips: 代码中的getter和settter是做什么的?

用于记录原生的get和set方法。vue会重写get和set方法,在方法中要先执行原生的get/set逻辑,进行值的获取和设置,再去做vue的特性处理。

defineReactive是给一个属性做数据劫持,首先创建了一个Dep实例。

当触发get时,如果Dep.target(当前激活的Watcher)存在,则执行dep.depend(),这句代码完成了依赖收集。

我们需要看一下Dep的源码

class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  // ......
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }
  notify() {
    var subs = this.subs.slice();
    //通知所有绑定 Watcher。调用watcher的update()
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

dep.depend会去找当前Watcher,如果存在,则调用watcher的addDep方法,将自身传入。watcher中收集到关联的依赖项。 在Watcher的addDep中,又调用了dep.addSub,将watcher自身传入,完成了互相收集。

4. 派发更新

派发更新的动作,在数据劫持设置的set方法中执行。

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
 
  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,
    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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

实际上核心就是最后一句,dep.notify()

Dep的源码中可以看到,notify方法会将dep实例中收集的所有的watcher遍历一遍,并执行watcher的update方法。

update方法会依照一定规则去执行对应watcher的回调(渲染watcher对应的就是render函数)。

接下来就是生成新vnode,新旧vnode比较,然后最小化更新到页面。

四、Vue3的响应式原理

Vue3的响应式核心思想和Vue2是一致的,数据劫持+观察者模式。

1. 数据劫持

上面提到,Vue2进行数据劫持的方案存在缺陷。

新增属性、删除属性、利用数组下标修改值、对数组.length的修改、Map、WeakMap、Set、WeakSet这些数据结构不支持。因为这些操作,getter和setter监听不到,故而无法做出响应式处理。

比如delete this.xxx这种写法,无法被get、set监听到。

Vue3出现的背景是主流浏览器已经原生支持了ES6语法,以IE为代表的老浏览器的市场份额越来越低。

所以Vue3采用了ES6 Proxy API对对象进行数据劫持。上面提到的无法被监听的操作,用proxy都有对应的方法可以识别监听到。

Vue3全面抛弃了IE浏览器,微软浏览器必须得Edge浏览器才能支持Vue3。

在数据劫持方面,Vue2和Vue3还有一处显著区别

Vue2的数据劫持是在初始化时,一次性递归遍历了所有的属性,分别进行数据劫持。

Vue3中,初始化时只对第一层的属性进行劫持,当第一层某个属性被访问到时,才会给该属性的下一层进行数据劫持。

好处是提升了性能,定义了但无用的属性不会被添加响应式,初始化数据劫持不需要递归很深。 

2. 依赖收集 

Vue3使用effect代替了Vue2中的Watcher。

我们还是从组件初始化开始,看一个渲染effect的工作过程。

 // 初始化组件
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 第一步: 创建component 实例   */
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    /* 第二步 : TODO:初始化 初始化组件,建立proxy , 根据字符窜模版得到 */
    setupComponent(instance)
    /* 第三步:建立一个渲染effect,执行effect */
    setupRenderEffect(
      instance,     // 组件实例
      initialVNode, //vnode  
      container,    // 容器元素
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )   
  }

可以看到,初始化组件时,首先创建组件实例,然后使用setupComponent方法,建立proxy,进行数据劫持。接着,调用setupRenderEffect,创建一个渲染effect并执行。

const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 创建一个渲染 effect */
    instance.update = effect(function componentEffect() {
      //... 后面会讲这块源码
    },{ scheduler: queueJob })
  }

其实质,就是调用effect方法,创建一个effect,并赋值给组件实例的update属性,作为渲染更新视图用。

effect方法接受两个参数,第一个是个方法,里面是effect被触发后需要处理的逻辑。第二个是一些配置。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options)
  /* 如果不是懒加载 立即执行 effect函数 */
  if (!options.lazy) {
    effect()
  }
  return effect
}

effect方法,最终创建出来的effect,还是一个方法。

如果配置中没有设置lazy,创建effect后,立即执行它一次。

创建effect的实际逻辑,在createReactiveEffect中。

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T, /**回调函数 */
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    try {
        enableTracking()
        effectStack.push(effect) //往effect数组中里放入当前 effect
        activeEffect = effect //TODO: effect 赋值给当前的 activeEffect
        return fn(...args) //TODO:    fn 为effect传进来 componentEffect
      } finally {
        effectStack.pop() //完成依赖收集后从effect数组删掉这个 effect
        resetTracking() 
        /* 将activeEffect还原到之前的effect */
        activeEffect = effectStack[effectStack.length - 1]
    }
  } as ReactiveEffect
  /* 配置一下初始化参数 */
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] /* TODO:用于收集相关依赖 */
  effect.options = options
  return effect
}

该方法定义了一个effect变量,它是一个方法,同时还给它设置了一些额外的参数,其中effect.deps数组就是用于收集相关依赖的。 

上一段代码中,effect方法执行时,实际执行了这段代码中effect中的逻辑。

它做的事情也很简单,先将当前effect放入effectStack中,并将activeEffect指向当前effect。

之后,就执行了fn,往上层层溯源,它其实就是setupRenderEffect方法中,effect方法的第一个入参componentEffect

执行结束后,将当前effect出栈,activeEffect指向effectStack中的栈顶元素。

回顾上面的代码,实际作用是创建了一个渲染effect,没有别的额外工作。那么,依赖收集相关的逻辑,应该就在componentEffect方法中。

function componentEffect() {
    if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, a, parent } = instance
        /* TODO: 触发instance.render函数,形成树结构 */
        const subTree = (instance.subTree = renderComponentRoot(instance))
        if (bm) {
          //触发 beforeMount声明周期钩子
          invokeArrayFns(bm)
        }
        patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
        )
        /* 触发声明周期 mounted钩子 */
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        instance.isMounted = true
      } else {
        // 更新组件逻辑
        // ......
      }
}

介绍下前提,在setupComponent中,根据template生成了render函数。在componentEffect中,通过renderComponentRoot方法,执行了render函数,此时,会访问页面展示使用到的属性,并触发其get方法,并完成依赖收集。

当这一步完成后,触发beforeMount生命周期回调。然后调用patch方法进行页面渲染,之后触发mounted生命周期回调。

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: object, key: string | symbol, receiver: object) {
    const res = Reflect.get(target, key, receiver)
    /* 浅逻辑 */
    if (shallow) {
      !isReadonly && track(target, TrackOpTypes.GET, key)
      return res
    }
    /* 数据绑定 */
    !isReadonly && track(target, TrackOpTypes.GET, key)
    return isObject(res)
      ? isReadonly
        ?
          /* 只读属性 */
          readonly(res)
          /*  */
        : reactive(res)
      : res
  }
}

get方法由createGetter创建。它接受两个参数,是否只读,是否浅逻辑。

根据代码,可以看出两个信息

1. track方法可以对当前属性进行依赖收集,如果当前属性是对象,也是对对象本身进行依赖收集,而不会递归对其子属性进行处理。

2. vue3初始化时只对第一层属性进行数据劫持,当某个对象属性的get被触发时,且非只读和浅逻辑,则对该对象进行数据劫持。

这样可以优化性能,初次渲染更快,而且有一些定义了但没用的属性值,不会进行深层次的响应式处理

/* target 对象本身 ,key属性值  type 为 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  /* 当打印或者获取属性的时候 console.log(this.a) 是没有activeEffect的 当前返回值为0  */
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    /*  target -map-> depsMap  */
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    /* key : dep dep观察者 */
    depsMap.set(key, (dep = new Set()))
  }
   /* 当前activeEffect */
  if (!dep.has(activeEffect)) {
    /* dep添加 activeEffect */
    dep.add(activeEffect)
    /* 每个 activeEffect的deps 存放当前的dep */
    activeEffect.deps.push(dep)
  }
}

再来看看track方法,核心代码是最下面的四句。

 /* 当前activeEffect */
  if (!dep.has(activeEffect)) {
    /* dep添加 activeEffect */
    dep.add(activeEffect)
    /* 每个 activeEffect的deps 存放当前的dep */
    activeEffect.deps.push(dep)
  }

dep指的是当前属性的依赖集合(关联的effect集合),effect中也会维护deps,表示当前effect关联的属性集合。

activeEffect指的是当前激活的effect,上面有讲。

track中的其他代码,都是为了找到当前属性的依赖集合,也就是dep。

这里使用两个Map,一个Set来存储响应式对象、属性、依赖集合之间的关系。

targetMap  键名是proxy,被劫持的响应式对象,值是depsMap,当前proxy中所有属性的依赖集合的集合。

depsMap  键名是key,也就是属性名,值是dep,当前属性的依赖集合。

dep是一个Set,里面的值是一个个的依赖(effect),用Set是为了去重。

至此,依赖收集工作就完成了。

3. 派发更新

和vue2一样,当数据发生变化,触发了属性的set方法,set方法中会派发更新。

/* 根据value值的改变,从effect和computer拿出对应的callback ,然后依次执行 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  /* 获取depssMap */
  const depsMap = targetMap.get(target)
  /* 没有经过依赖收集的 ,直接返回 */
  if (!depsMap) {
    return
  }
  const effects = new Set<ReactiveEffect>()        /* effect钩子队列 */
  const computedRunners = new Set<ReactiveEffect>() /* 计算属性队列 */
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || !shouldTrack) {
          if (effect.options.computed) { /* 处理computed逻辑 */
            computedRunners.add(effect)  /* 储存对应的dep */
          } else {
            effects.add(effect)  /* 储存对应的dep */
          }
        }
      })
    }
  }

  add(depsMap.get(key))

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) { /* 放进 scheduler 调度*/
      effect.options.scheduler(effect)
    } else {
      effect() /* 不存在调度情况,直接执行effect */
    }
  }

  //TODO: 必须首先运行计算属性的更新,以便计算的getter
  //在任何依赖于它们的正常更新effect运行之前,都可能失效。

  computedRunners.forEach(run) /* 依次执行computedRunners 回调*/
  effects.forEach(run) /* 依次执行 effect 回调( TODO: 里面包括渲染effect )*/
}

定义effects存储涉及的effect,computedRunner存储computed产生的effect。

从targetMap中找到depsMap,再从depsMap中根据key获取当前属性的依赖集合,depsMap.get(key)。

使用add方法对集合中的依赖分类,分别存到effects和computedRunner中。然后分别遍历并执行effect方法。

其中渲染effect方法,会触发页面重新渲染。

4. 拾遗

4.1 Proxy和Reflect

在Proxy get的handler中,首先调用Reflect.get,这个调用是用来做什么的呢?

答案:为了获取到相应处理,原生的处理结果。

我们用Proxy拦截get,是为了在get的基础上,增加收集依赖的操作,但首先要保证其原生的能力,也就是获取到相应属性的值,并return出去。

Proxy和Reflect有一一对应的捕获器,Proxy.get可以捕获get操作,Reflect.get可以获取相应属性的值。其他捕获器也是一样的道理。

4.2 结合生命周期看Vue2的初始化

A、创建vue实例,事件循环,定义生命周期方法

触发beforeCreated,此时数据未初始化,也无法获取到props和data

B、数据劫持、创建inject

触发created,此时已经可以访问到props和data,可以在此进行接口调用等操作

C、调用vm.$mounte()方法

判断是否有template,如果没有,则用el的innerHTML作为template。

将template转为render函数

触发beforeMount

D、创建渲染Watcher,在构造函数中,调用了传入的render函数

创建虚拟dom,在此会访问data中数据,触发其getter,完成依赖收集工作

将虚拟dom渲染为真实页面

创建vm.$el(创建真实dom),并用其代替el,也就是el指定的挂载位置

触发mounted

Vue3和Vue2的生命周期过程大差不差,区别在beforeMount的触发时机

Vue3是先执行render函数,收集依赖,完成后再触发beforeMount。

Vue2是先触发beforeMount,再实例化渲染watcher,在执行构造函数时执行render函数,收集依赖。


总结

Vue2和Vue3的响应式原理就告一段落啦,欢迎小伙伴们多多交流沟通。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值