Vue源码分析-响应式原理

2021-10-26 15:19:51 浏览数 (2)

vuejs 的响应式就是通过数据劫持对每个 data 属性添加一个 Dep 对象,该 Dep 对象维护一个 Watcher 数组,data 发生改变时,通知所有的 Watcher 回调,每个组件都有一个默认的渲染 Watcher,它的回调就是 vm._update(vm._render())方法,_render 内部调用 render 方法生成的当前组件的 VNode 对象,vm._update 内部调用__patch__进行 VNode 对比,并返回新的 el,vue 响应式的整体逻辑就是如此,下面我们详细了解一下:

1. 数据劫持 依赖搜集

先看 new Vue 执行的核心逻辑_init 方法里的核心代码:

代码语言:javascript复制
Vue.prototype._init = function (options?: Object) {
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, "beforeCreate");
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, "created");
    // 如果有el参数直接主动挂载,没有的话需要开发者手动挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
};
复制代码

其中,跟响应式相关的就是 initState 方法,他初始化了 props、data、watch、computed 等属性,查看 initState 方法,在初始化 data 时对整个 data 调用了 observe 方法,observe 方法主要通过递归调用为每个属性调用 defineReactive 方法,响应式的核心也在这个方法中:

代码语言:javascript复制
export function defineReactive (
  obj: Object,
  key: string,
  val: any
) {
  const dep = new Dep()

  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
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
复制代码

可以看到对每个属性使用 get 方法收集依赖,set 方法派发更新,其中 dep.depend()就是将当前的渲染 Watcher 加入到 dep 中,dep.notify()通知所有 Watcher 对象进行更新回调。

2. Watcher 是什么?

vuejs 中 Watcher 共分为 3 中:

  1. 渲染 Watcher,每个组件都有一个,在挂载组件 mountComponent 时 new 出来的 Watcher 对象; $mount 方法其实最终会调用 mountComponent,看下 mountComponent 方法:
代码语言:javascript复制
export function mountComponent(
  vm: Component,
  el: ?Element,
): Component {
  vm.$el = el;
  callHook(vm, "beforeMount");
  let updateComponent = () => {
    vm._update(vm._render(), hydrating);
  };
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, "beforeUpdate");
        }
      },
    },
    true /* isRenderWatcher */
  );
  // 第一次调用,call mounted hook
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, "mounted");
  }
  return vm;
}
复制代码

可以看到 mountComponent 的核心就是 new 了一个 Watcher 对象并将vm._update(vm._render(), hydrating);作为他的回调执行,也就是说当当前组件的 data 发生改变,将会触发 vm._update 方法。

  1. userWatcher:通过 options.watcher 或者$watcher 方法定义的,这个比较简单,new Watcher 构造函数里会执行被监听的 data 的 getter 方法,可以将当前的 userWatcher 加入到该 data 的 dep 中,哪怕没有模版用到该 data,该 watcher 也会生效,因为已经收集过 watcher 对象了。
  2. computedWatcher:每个 computed 都会新建一个 Watcher(dirty = lazy = true)对象,第一次调用 computed 时在 computed 的 get 方法中通过调用该 Watcher 的 evalute 方法(dirty = false)然后执行 watcher.depend 方法,使得 computed 中依赖的响应式 data 的 Dep 既发布到该 computedWatcher,也发布到 renderWatcher 中,这样 computed 中依赖的响应式 data 改变就可以触发 renderWatcher 的回调更新 dom,不改变的话因为 watcher.dirty = false,不再去重新计算,从而实现数据缓存。

3. Render Watcher 的回调

上面说了当 data 发生改变时,会触发当前 render watcher 的回调,从上面代码可以看出 render watcher 的回调是执行了vm._update(vm._render())语句,下面分析一下该语句:

代码语言:javascript复制
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    if (!prevVnode) {
      // 初次渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
      // 更新
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
};
复制代码

_update 方法主要就是调用的patch方法,patch方法就是对比两个 VNode 并同时更新 dom,这个放到 vdom 的 diff 算法中详说。_update 方法需要传入当前组件的 VNode 对象,该对象就是通过_render 方法生成,看下_render 方法:

代码语言:javascript复制
Vue.prototype._render = function (): VNode {
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
    }
    return vnode
  }
}
复制代码

其实核心调用了 render 方法和一些其他的处理。这里还有一点需要注意的是,render watcher 的回调并不是立即执行的, 会加入一个异步队列,Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替,如下代码:。

代码语言:javascript复制
update () {
    if (this.lazy) {
        this.dirty = true
    } else if (this.sync) {
        this.run()
    } else {
        queueWatcher(this)
    }
}
复制代码

data 发生改变时,dep 通知 watcher 进行 update,此时并不是立即执行,而是调用 queueWatcher 将该 watcher 放到一个异步队列中,nextTick 方法其实就是将一个回调方法放入这一轮回调的最后。

代码语言:javascript复制
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
}
复制代码

以上就是 vue 实现响应式的整个逻辑了。

0 人点赞