vue源码解读---模板渲染&响应式

220 阅读6分钟

目标:

  • 如何阅读VUE源码
  • 弄清楚模板和数据如何渲染成最终的 DOM
  • 深入响应式原理

如何阅读VUE源码

vue源码的目录结构

scripts            # 构建配置相关
src
├── compiler     # 编译相关 把模板解析成 ast 语法树,ast 语法树优化,代码生成等
├── core         # 核心代码内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM等
├── platforms    # 不同平台的支持 
├── server       # 服务端渲染
├── sfc          # .vue 文件解析
├── shared       # 共享代码

Vue.js 的源码利用了 Flow 做了静态类型检查 TypeScript vs Flow - 掘金

Vue.js 源码是基于 Rollup 构建的, 它的构建相关配置都在 scripts 目录下

Runtime Only VS Runtime + Compiler

当我们的代码执行 import Vue from 'vue' 的时候,就是从src/platforms/web/entry-runtime-with-compiler.js 这个入口来初始化Vue的

Vue本质上就是一个用 Function 实现的 Class,然后在它的原型 prototype 以及它本身上扩展了一系列的方法和属性

// 用 Function 实现的类,我们只能通过 new Vue 去实例化它
// src/core/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

// xxxMixin 的函数调用,并把 Vue 当参数传入,它们的功能都是给 Vue 的 prototype 上扩展一些方法
// Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

生命周期

模板渲染

New Vue 发生了什么

export function initMixin(Vue: Class < Component > ) {
  // 合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等
  Vue.prototype._init = function(options ? : Object) {
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      // 把 Vue 构造函数的 options 和用户传入的 options 做一层合并,到 vm.$options 上
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // 把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化 props、data、methods、watch、computed 等属性
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    // 组件初始化的时候是不传 el 的,因此组件是自己接管了 $mount 的过程
    if (vm.$options.el) {
      // $mount 这个方法的实现是和平台、构建方式都相关的
      vm.$mount(vm.$options.el)
    }
  }
}

Vue 实例挂载的实现

Virtual DOM

  • Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom 的实现,然后加入了一些 Vue.js 特色的东西
  • 核心定义就几个关键属性,标签名、数据、子节点、键值等
  • 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法

$mount 这个方法的实现是和平台、构建方式都相关的

export function mountComponent(
  vm: Component,
  el: ? Element,
  hydrating ? : boolean ): Component {
  // ...
  // 执行beforeMount钩子函数
  // 因为先执行 在调用child,所以先父后子
  callHook(vm, 'beforeMount')

  let updateComponent
  updateComponent = () => {
    // vm._render 渲染出来vnode
    // 调用 vm._update 更新 DOM 一次真实的渲染
    vm._update(vm._render(), hydrating)
  }
  // 在组件 mount 的过程中,会实例化一个渲染的 Watcher 去监听 vm 上的数据变化重新渲染
  // 实例化的过程中,在它的构造函数里会判断 isRenderWatcher,接着把当前 watcher 的实例赋值给 vm._watcher
  // 定义在 src/core/observer/watcher.js 中 观察者模式
  new Watcher(vm, updateComponent, noop, {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */ )

  // vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例,是我们通过外部 new Vue 初始化过程
  if (vm.$vnode == null) {
    vm._isMounted = true
    // _update调用完 才会执行到这里 所以先子后父
    callHook(vm, 'mounted')
  }
  return vm }

createElement

  • createElement 创建 VNode 的过程
  • 每个 VNode 有 childrenchildren 每个元素也是一个 VNode,这样就形成了一个 VNode Tree

如果是组件会调用createComponent

export function createComponent(
  Ctor: Class < Component > | Function | Object | void,
  data: ? VNodeData,
  context : Component,
  children: ? Array < VNode > ,
  tag ? : string ): VNode | Array < VNode > | void {
  if (isUndef(Ctor)) {
    return
  }

  // baseCtor 实际上就是 Vue
  const baseCtor = context.$options._base

  // 构造子类构造函数
  // Vue.extend 函数的定义 : src/core/global-api/extend.js
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }

  // install component management hooks onto the placeholder node
  // 安装组件的钩子
  installComponentHooks(data)

  // 通过 new VNode 实例化一个 vnode 并返回 注意组件的vnode没有children
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context, {
      Ctor,
      propsData,
      listeners,
      tag,
      children
    },
    asyncFactory
  )

  return vnode
}
  
  // 构造组件构造函数
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  
  // 组件上挂载的钩子函数
  init(vnode: VNodeWithData, hydrating: boolean): ? boolean {
    const child = vnode.componentInstance =
      createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
    // hydrating 为 true 一般是服务端渲染的情况,我们只考虑客户端渲染,所以这里 $mount 相当于执行 child.$mount(undefined, false)
    // 它最终会调用 mountComponent 方法,进而执行 vm._render() 方法
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
  
  
 export function createComponentInstanceForVnode(vnode: any, parent: any): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode, // 占位节点
    parent
  }
  // 子组件的实例化实际上就是在这个时机执行的
  // 执行实例的 _init 方法
  return new vnode.componentOptions.Ctor(options)
}

update

Vue 的 _update 是实例的一个私有方法,它的作用是把 VNode 渲染成真实的 DOM,它被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候

渲染的时候会先判断vnode上是否有init钩子 如果有就会调用该钩子,否则使用封装的原生操作dom的方法渲染为真实dom

举个例子:

import Vue from 'vue'
import App from './App.vue'

new Vue({
  el: "#app",
  render: h => h(App),
})

// new Vue({
//   el: '#app',
//   render(createElement) {
//     return createElement('div', {
//       attrs: {
//         id: '#app2'
//       }
//     }, [this.message, createElement('h1', {
//       attrs: {
//         id: 'app3'
//       }
//     }, this.test)])
//   },
//   data: {
//     message: 'Hello Vue2!',
//     test: '商业化技术'
//   }
// })

// initial render
// hydrating 表示是否是服务端渲染;
// removeOnly 是给 transition-group 用的 
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)

总结

响应式原理

所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据

initData:

image.png

// 定义一个响应式对象,给对象动态添加 getter 和 setter
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter ? : ? Function,
  shallow ? : boolean ) {
  // 实例化一个 Dep 的实例 dep是getter依赖收集的核心 它的定义在 src/core/observer/dep.js 中
  const dep = new Dep()

  val = obj[key]

  // 对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,
  // 这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。
  let childOb = !shallow && observe(val)
  // 给 obj 的属性 key 添加 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val      if (Dep.target) {
        // 依赖收集
        // 当前watcher会收集所依赖的dep
        // 数据的dep也会收集变更时需要通知的watcher
        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      // 如果 shallow 为 false 的情况,会对新设置的值变成一个响应式对象
      childOb = !shallow && observe(newVal)
      // 通知所有的订阅者
      dep.notify()
    }
  })
}

dep

export default class Dep {
  static target: ? Watcher; // 全局唯一 Watcher
  id: number;
  subs: Array < Watcher > ; // 订阅这个数据变化的Watcher 的数组

  constructor() {
    this.id = uid++
    this.subs = []
  }

  addSub(sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub(sub: Watcher) {
    remove(this.subs, sub)
  }

  depend() {
    if (Dep.target) {
      // addDep 在watcher中定义
      Dep.target.addDep(this)
    }
  }

  notify() {
    const subs = this.subs.slice()
    // 通知所有的订阅者
    // 遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcher 的 update 方法
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

watcher

export default class Watcher {
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options ? : ? Object,
    isRenderWatcher ? : boolean  ) {
    this.getter = expOrFn    this.value = this.lazy ?
      undefined // 计算属性走这里
      :
      this.get()
  }

  // 和依赖收集相关的原型方法
  get() {
    // 把 this 赋值为当前的渲 染 watcher 并压栈(为了恢复用)
    pushTarget(this)
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    // 递归去访问 value,触发它所有子项的 getter
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    // 依赖清空(将newdeps赋值给deps,清空newdeps,其实清空的是没有用的依赖)
    this.cleanupDeps()
    return value
  }

  // 和依赖收集相关的原型方法
  addDep(dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) { // 保证同一数据不会被添加多次
        // 执行 dep.addSub(this),那么就会执行 this.subs.push(sub)
        // 把当前的 watcher 订阅到这个数据持有的 dep 的 subs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备
        dep.addSub(this)
      }
    }
  }

  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // 同步watcher
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  run() {
    // 这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程,但它和首次渲染有所不同
    const value = this.get()
    this.cb.call(this.vm, value, oldValue)
  }

  evaluate() {
    // 真正访问到computed的getter
    this.value = this.get()
    this.dirty = false
  }

  depend() {
    // deps中是computed watcher 所依赖的响应式数据
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

initComputed:

image.png

const computedWatcherOptions = { lazy: true }

function initComputed(vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  // 对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数
  for (const key in computed) {
    const userDef = computed[key]
    const getter = userDef

    // 为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher
    // 因为 const computedWatcherOptions = { lazy: true }
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    )

    defineComputed(vm, key, userDef)
  }
}

export function defineComputed(
  target: any,
  key: string,
  userDef: Object | Function ) {
  sharedPropertyDefinition.get = createComputedGetter(key)
  sharedPropertyDefinition.set = noop
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 将计算watcher放在数据的dep.subs中
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 将渲染watcher放在数据的dep.subs中
      // 因为computer没有自己的dep,更新的时候不能通知渲染watcher更新
      // 需要依赖的数据通知渲染watcher
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

这块的逻辑比较绕,简单来说就是:

computed依赖的响应式数据收集了computed watcher和渲染watcher

computed依赖的响应式数据变更时会通知这两个watcher更新,computed watcher会直接返回,渲染watcher会执行,然后触发computed的get,开始计算computed的值

总结: