vue-2.7源码解读之初始化流程和响应式实现

vue2.7.16源码解读

vue2的源码调试比较容易,先将代码依赖安装好,然后执行构建开发命令,这要调试时看到的代码和原文件代码差不多。

js 复制代码
pnpm install

// 构建dev环境
pnpm dev
// 这时会在dist目录下创建一个vue.js的文件

接着找到examples 文件目录,随便找一个示例文件,修改index.html 中引用vue的路径,改成我们刚刚生产的vue.js。本地启动这个index.html就可以断点调试了。

html 复制代码
  <body>
    <div id="app"></div>
    <script src="../../../dist/vue.js"></script>
    <script src="app.js"></script>
  </body>

vue初始化流程

可以按照我下面梳理的流程,找到对应位置打上断点看一下执行流程,能快速理清楚运行过程,加深理解。

vue执行入口文件、对应源码目录src/core/instatnce/index.ts

js 复制代码
// 在外面使用new Vue调用的就是这个方法
function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 初始化vue,这里的_init方法是来自initMixin挂在原型上的
  this._init(options)
}

// 挂在_init方法
initMixin(Vue)

// 在vue原型上挂在$data、$props、$set、$delete、$watch方法
stateMixin(Vue)

// 在vue原型上挂在$on、$once、$off、$emit方法
eventsMixin(Vue)

// 在vue原型上挂在_update、$forceUpdate、$destory方法
lifecycleMixin(Vue)

// 在vue原型上挂在$nextTick、render方法
renderMixin(Vue)

export default Vue as unknown as GlobalAPI

初始化的核心流程在_init方法中,下面看一下源码内容。源码目录src/core/instance/init.ts

js 复制代码
export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    vm._uid = uid++
    let startTag, endTag
    vm._isVue = true
    vm.__v_skip = true
    vm._scope = new EffectScope(true /* detached */)
    vm._scope.parent = undefined
    vm._scope._vm = true
    if (options && options._isComponent) {
      initInternalComponent(vm, options as any)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // 将vm实例挂在到self属性后期可以访问
    vm._self = vm
    // 初始化属性如$root、_watcher等属性,并不是执行生命周期方法
    initLifecycle(vm)
    // 初始化listeners属性值
    initEvents(vm)
    // 初始化插槽,给$attrs、$listeners添加响应式
    initRender(vm)
    // beforeCreate 生命周期
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    // 给注入的inject添加响应式
    initInjections(vm) // resolve injections before data/props
    
    // 按照下面顺序进行执行
    // 先初始化props
    // 初始化setup,在2.7中setup是介于beforeCreate和created之间的
    // 初始化methods
    // 初始化data
    // 初始化computed
    // 初始化watch
    initState(vm)
    // 给provide添加响应式
    // 这里的数据来源于data、props和inject,因此要等data、props、inject初始化完之后
    initProvide(vm) // resolve provide after data/props
    // create生命周期
    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)
    }
    
    // new Vue是参数是不是配置el,有的话就进行挂在
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
js 复制代码
// 上面说的initState源码
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)
  }
}

上面流程走完一般会进入到$mount方法中,将元素挂在到dom上。目录地址src/platforms/web/runtime/index.ts

js 复制代码
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  // 更新组件
  return mountComponent(this, el, hydrating)
}
js 复制代码
// 这段代码有删减,去掉了dev环境判断
// 目录地址 src/core/instance/lifecycle.ts

export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  vm.$el = el
  // beforeMount生命周期
  callHook(vm, 'beforeMount')

  let updateComponent = () => {
    // 在watcher中注册的更新渲染方法,最后会进_update方法
    vm._update(vm._render(), hydrating)
  }
  const watcherOptions: WatcherOptions = {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }
  // 创建一个渲染watcher
  // 我们在watcher的构造函数中设置了这个,因为watcher的初始补丁可能会调用$forceUpdate(例如在子组件的挂载钩子中),
  // 这依赖于vm._watcher已经被定义
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  hydrating = false

  // 刷新"pre"watcher队列
  // 刷新"pre"watcher队列的原因是,在组件setup函数中创建的watcher,它们的flush属性为"pre",
  // 这意味着它们应该在组件渲染之前运行。
  // 但是,由于组件渲染是异步的,watcher的运行也可能是异步的,这可能导致watcher在组件渲染之后运行。
  // 为了确保watcher在组件渲染之前运行,我们需要刷新"pre"watcher队列。
  // flush buffer for flush: "pre" watchers queued in setup()
  const preWatchers = vm._preWatchers
  if (preWatchers) {
    for (let i = 0; i < preWatchers.length; i++) {
      preWatchers[i].run()
    }
  }

  // 手动挂载的实例,调用自身的mounted钩子
  // mounted钩子在inserted钩子中调用,用于渲染创建的子组件
  if (vm.$vnode == null) {
    vm._isMounted = true
    // mounted生命周期
    callHook(vm, 'mounted')
  }
  return vm
}
js 复制代码
// 目录地址 src/core/instance/lifecycle.ts
// 这个是执行lifecycleMixin的时候挂载的_update方法,前面第一个代码片段有介绍
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // 当不存在上一个Vnode虚拟节点,执行初始化,直接渲染
    // __patch__方法走的就时vnode-diff,更新节点到dom对应的流程,后面单独详情介绍
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 更新节点时走的流程
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    let wrapper: Component | undefined = vm
    while (
      wrapper &&
      wrapper.$vnode &&
      wrapper.$parent &&
      wrapper.$vnode === wrapper.$parent._vnode
    ) {
      wrapper.$parent.$el = wrapper.$el
      wrapper = wrapper.$parent
    }
  }

到这里页面已经更新出对应的UI,初始化流程基本结束了

vue中响应式如何实现

我们知道在vue中data、props、computed、provided、inject这类数据有响应式,我们就data这种数据的响应式看一下源码中是如何实现的。在上面初始化流程中看到initData是处理data相关的数据。

js 复制代码
// 路径地址:src/core/instance/state.ts
function initData(vm: Component) {
  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) {
    data = {}
  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 (__DEV__) {
      // methods和data不能用相同的名称
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    // props和data不能有相同的名称
    if (props && hasOwn(props, key)) {
      __DEV__ &&
        warn(
          `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        )
    } else if (!isReserved(key)) { // 检查是不是以$或_开头的
      // 代理实例中_data属性,
      // 将vm上的key属性进行操作拦截定义,后面响应式触发会进入到对应拦截
      proxy(vm, `_data`, key)
    }
  }
  // 添加响应式拦截
  const ob = observe(data)
  ob && ob.vmCount++
}
js 复制代码
// 路径地址:src/core/instance/state.ts
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
  }
  // 我们日常写的this.xx 或者this.xx = 123 这样的读写操作,就会触发这里的拦截
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
js 复制代码
// 路径地址:src/core/observer/index.ts
export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {
  // 已经添加过响应式直接返回
  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) &&
    !(value instanceof VNode)
  ) {
    // 创建一个Observer的实例,进行拦截数据的操作
    return new Observer(value, shallow, ssrMockReactivity)
  }
}
js 复制代码
// 路径地址:src/core/observer/index.ts
export class Observer {
  dep: Dep
  vmCount: number // number of vms that have this object as root $data

  constructor(public value: any, public shallow = false, public mock = false) {
    this.dep = mock ? mockDep : new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (isArray(value)) {
      if (!mock) {
        if (hasProto) {
          // 改写原型中的修改数组的7个方法
          ;(value as any).__proto__ = arrayMethods
        } 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 {
      // 遍历对象上所有的key
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        // 给每一个key添加响应式拦截
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }
js 复制代码
// 路径地址:src/core/observer/index.ts
// 使用Object.defineProperty拦截对象,重写数据原型方法的操作都在这个方法里面
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
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if (
    (!getter || setter) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key]
  }
  // 基础类型的数据会返回undefined,数组和对象类型会递归操作
  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
  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
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
      // 修改数据通知对应的watcher进行视图修改
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        dep.notify()
      }
    }
  })

  return dep
}
js 复制代码
 observeArray(value: any[]) {
   // 数组类型的递归便利绑定响应式
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }

上面这些代码大部门都是建立依赖关系、绑定watcher更新逻辑,以便于修改数据触发对应的watcher进行更新。下面就来看一下修改数据代码是怎么实现的。

js 复制代码
export function proxy(target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  // 当我们修改this上的属性值时,会触发这个set方法
  // 在给当前的this[sourceKey][key] = val 赋值操作会进一步触发对应的set拦截
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
js 复制代码
// 上面代码的赋值操作,会触发这里的set操作
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean,
  observeEvenIfShallow = false
) {
  ...
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      ...
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        // 兼容vue3中的语法
        value.value = newVal
        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
}
js 复制代码
// 路径: src/core/observer/dep.ts  
// Dep类中的通知方法
  notify(info?: DebuggerEventExtraInfo) {
    const subs = this.subs.filter(s => s) as DepTarget[]
    if (__DEV__ && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      const sub = subs[i]
      if (__DEV__ && info) {
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      // 依次触发对应的watcher的update方法
      sub.update()
    }
  }
  
// 路径: src/core/observer/watcher.ts   
// watcher类中的更新方法
    update() {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 放入更新队列中
      queueWatcher(this)
    }
  }

// 路径: src/core/observer/scheduler.ts

export function queueWatcher(watcher: Watcher) {
  const id = watcher.id
  if (has[id] != null) {
    return
  }

  if (watcher === Dep.target && watcher.noRecurse) {
    return
  }

  has[id] = true
  // 表示当前是否正在刷新队列
  // 第一会把待更新的watcher放入到队列中
  if (!flushing) {
    queue.push(watcher)
  } else {
    // if already flushing, splice the watcher based on its id
    // if already past its id, it will be run next immediately.
    let i = queue.length - 1
    while (i > index && queue[i].id > watcher.id) {
      i--
    }
    queue.splice(i + 1, 0, watcher)
  }
  // 表示当前是否正在等待刷新队列
  if (!waiting) {
    waiting = true

    if (__DEV__ && !config.async) {
      flushSchedulerQueue()
      return
    }
    // 采用微任务的方式更新
    nextTick(flushSchedulerQueue)
  }
}

// 路径:src/core/util/next-tick.ts
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  // 把更新的方法放入到callbacks中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
// nextTick回调方法
// 从callbacks中取出watcher更新方法,依次执行
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

上面的流程走完,数据的更新对应的更新流程就操作完了,接下来会触发前面初始化流程中说的_update方法会执行,然后模版中依赖的数据会依次触发get拦截获取到最新的数据,渲染到页面。

下面看一下debugger流程

先直接进入initState中

针对数据使用object.defineProperty进行拦截操作

修改数据,先触发vue实例上的拦截

再触发具体数据的拦截

触发通知方法,遍历对应订阅的watcher,执行更新方法,后面会走到初始化时的_update方法,在处理temlate的vnode,最后执行渲染流程

以上就是对vue2.7的初始化和响应式实现进行的梳理和对应代码阅读记录,接下来将完成对vdom-diff流程和vue的模版编译梳理。

相关推荐
莫空00001 分钟前
深入理解JavaScript属性描述符:从数据属性到存取器属性
前端·面试
guojl2 分钟前
深度剖析Kafka读写机制
前端
FogLetter2 分钟前
图片懒加载:让网页飞起来的魔法技巧 ✨
前端·javascript·css
Mxuan3 分钟前
vscode webview 插件开发(精装篇)
前端
Mxuan4 分钟前
vscode webview 插件开发(交付篇)
前端
Mxuan5 分钟前
vscode 插件与 electron 应用跳转网页进行登录的实践
前端
拾光拾趣录6 分钟前
JavaScript 加载对浏览器渲染的影响
前端·javascript·浏览器
Codebee6 分钟前
OneCode图表配置速查手册
大数据·前端·数据可视化
然我6 分钟前
React 开发通关指南:用 HTML 的思维写 JS🚀🚀
前端·react.js·html
Mxuan8 分钟前
vscode webview 插件开发(毛坯篇)
前端