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的模版编译梳理。

相关推荐
灵感__idea5 小时前
JavaScript高级程序设计(第5版):好的编程就是掌控感
前端·javascript·程序员
烛阴6 小时前
Mix
前端·webgl
代码续发6 小时前
前端组件梳理
前端
试图让你心动7 小时前
原生input添加删除图标类似vue里面移入显示删除[jquery]
前端·vue.js·jquery
_Kayo_7 小时前
VUE2 学习笔记6 vue数据监测原理
vue.js·笔记·学习
陈不知代码7 小时前
uniapp创建vue3+ts+pinia+sass项目
前端·uni-app·sass
小王码农记7 小时前
sass中@mixin与 @include
前端·sass
陈琦鹏7 小时前
轻松管理 WebSocket 连接!easy-websocket-client
前端·vue.js·websocket
hui函数8 小时前
掌握JavaScript函数封装与作用域
前端·javascript
行板Andante8 小时前
前端设计中如何在鼠标悬浮时同步修改块内样式
前端