frontend9 / fe9-library

九部知识库
1.94k stars 138 forks source link

从源码解释VUE响应式原理-响应式的整体流程 #116

Open summercloud opened 6 years ago

summercloud commented 6 years ago

前言

vue官方对响应式原理的解释:深入响应式原理

上一节讲了VUE中依赖收集和依赖触发的原理,然鹅对响应式的整体流程我们还是有很多疑问:

为了回答以上的几个问题,我们不得不梳理一波VUE响应式的整体流程


从实例初始化阶段开始说起

vue源码的 instance/init.js 中是初始化的入口,其中初始化中除了初始化的几个步骤以外,在最后有这样一段 代码:

if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

在初始化结束后,调用$mount()方法将组件挂载挂载至给定的元素vm.$options.el中。

关于$mount的定义在两处可以看到:platforms/web/runtime/index.js、platforms/web/entry-runtime-with-compiler.js

其中runtime/index.js的代码如下:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  // 划重点!!!
  return mountComponent(this, el, hydrating)
}

runtime/index.js是运行时vue的入口,其中定义的$mount()方法是运行时vue的$mount功能,其中主要调用了mountComponent()函数完成挂载。 entry-runtime-with-compiler.js是完整的vue的入口,在运行时vue的$mount基础上加入了编译模版的能力。

编译模版,为挂载提供渲染函数

entry-runtime-with-compiler.js中定义了$mount(),在运行时$mount()的基础上添加了模版编译。代码如下:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  //检查挂载点是不是<body>元素或者<html>元素
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // 判断渲染函数不存在时
  if (!options.render) {
    ...//构建渲染函数
  }

  //调用运行时vue的$mount()函数,
  return mount.call(this, el, hydrating)
}

entry-runtime-with-compiler.js中的$mount()函数主要做了三件事:

  1. 判断挂载点是不是元素或者元素,因为挂载点会被自身模版替代掉,因此挂载点不能为元素或者元素;
  2. 判断渲染函数是否存在,如果渲染函数不存在,则构建渲染函数;
  3. 调用运行时vue的$mount()函数,即runtime/index.js中的$mount();

    创建渲染函数

    上述第二步,若渲染函数不存在时,构建渲染函数,代码如下:

    let template = options.template
    //如果template存在,则通过template获取真正的【模版】
    if (template) {
      //template是字符串
      if (typeof template === 'string') {
        //template第一个字符是#,则将该字符串作为id选择器获取对应元素作为【模版】
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          ... //省略
        }
        //如果template是元素节点,则将template的innerHTML作为【模版】
      } else if (template.nodeType) {
        template = template.innerHTML
        //若template无效,则显示提示
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
      //若template不存在,则将el元素的outerHTML作为【模版】
    } else if (el) {
      template = getOuterHTML(el)
    }
    //此时template中是最终的【模版】,下面根据【模版】生成rander函数
    if (template) {
      ... //省略
      // 划重点!!!
      // 使用compileToFunctions函数将【模版】template,编译成为渲染函数。
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    
      ... //省略
    }           

    创建渲染函数阶段主要做了两件事:

  4. 得到【模版】字符串:
    • 如果template存在,且template是字符串以#开头,则将该字符串作为id选择器获取对应元素作为【模版】
    • 如果template是元素节点,则将template的innerHTML作为【模版】
    • 如果tempalte是无效字符串,则显示warning
    • 若template不存在,则将el元素的outerHTML作为【模版】
  5. 根据【模版】字符串生成渲染函数render()
    • 生成的options.render,在挂载组件的mountComponent函数中用到

      实现挂载的mountComponent()函数

      上一步确保渲染函数render()存在后,就进入到了这正的挂载阶段。前面讲到挂载函数主要在mountComponent()中完成。

mountComponent()函数的定义在src/core/instance/lifecycle.js文件中。代码如下:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  //如果render不存在
  if (!vm.$options.render) {
    //为render赋初始值,并打印warning提示信息
    vm.$options.render = createEmptyVNode
    ... //省略
    }
  }
  //触发beforeMount钩子
  callHook(vm, 'beforeMount')
  // 开始挂载
  let updateComponent
  /* istanbul ignore if */
  // 定义并初始化updateComponent函数
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      // 调用_render函数生成vnode虚拟节点
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      // 以虚拟节点vnode作为参数调用_update函数,生成真正的DOM
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      //调用_render函数生成vnode虚拟节点;以虚拟节点vnode作为参数调用_update函数,生成真正的DOM
      vm._update(vm._render(), hydrating)
    }
  }

mountComponent主要做了三件事:

  1. 如果render不存在,为render赋初始值,并打印warning信息
  2. 触发beforeMount
  3. 定义并初始化updateComponent函数:

    • 调用_render函数生成vnode虚拟节点
    • 虚拟节点vnode作为参数调用_update函数,生成真正的DOM

      Watcher类

      watcher类的定义在core/observer/watcher.js中,代码如下:

      
      export default class Watcher {
      ... //
      // 构造函数
      constructor (
      vm: Component,
      expOrFn: string | Function,
      cb: Function,
      options?: ?Object,
      isRenderWatcher?: boolean
      ) {
      this.vm = vm
      if (isRenderWatcher) {
      // 将渲染函数的观察者存入_watcher
      vm._watcher = this
      }
      //将所有观察者push到_watchers列表
      vm._watchers.push(this)
      // options
      if (options) {
      // 是否深度观测
      this.deep = !!options.deep
      // 是否为开发者定义的watcher(渲染函数观察者、计算属性观察者属于内部定义的watcher)
      this.user = !!options.user
      // 是否为计算属性的观察者
      this.computed = !!options.computed
      this.sync = !!options.sync
      //在数据变化之后、触发更新之前调用
      this.before = options.before
      } else {
      this.deep = this.user = this.computed = this.sync = false
      }
      // 定义一系列实例属性
      this.cb = cb
      this.id = ++uid // uid for batching
      this.active = true
      this.dirty = this.computed // for computed watchers
      this.deps = []
      this.newDeps = []
      // depIds 和 newDepIds 用书避免重复收集依赖
      this.depIds = new Set()
      this.newDepIds = new Set()
      this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
      // parse expression for getter
      // 兼容被观测数据,当被观测数据是function时,直接将其作为getter
      // 当被观测数据不是function时通过parsePath解析其真正的返回值
      if (typeof expOrFn === 'function') {
      this.getter = expOrFn
      } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
      this.getter = function () {}
      process.env.NODE_ENV !== 'production' && warn(
        `Failed watching path: "${expOrFn}" ` +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      )
      }
      }
      if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
      } else {
      // 除计算属性的观察者以外的所有观察者调用this.get()方法
      this.value = this.get()
      }
      }

    // get方法 get () { ... } // 添加依赖 addDep (dep: Dep) { ... } // 移除废弃观察者;清空newDepIds 属性和 newDeps 属性的值 cleanupDeps () { ... } // 当依赖变化时,触发更新 update () { ... } // 数据变化函数的入口 run () { ... } // 真正进行数据变化的函数 getAndInvoke (cb: Function) { ... } // evaluate () { ... } // depend () { ... }

    // teardown () { ... } }

    
    #### watcher构造函数
    由以上代码可见,在watcher构造函数中做了如下几件事:
  4. 将组件的渲染函数的观察者存入_watcher,将所有的观察者存入_watchers中
  5. 保存before函数,在数据变化之后、触发更新之前调用
  6. 定义一系列实例属性
  7. 兼容被观测数据,当被观测数据是function时,直接将其作为getter; 当被观测数据不是function时通过parsePath解析其真正的返回值,被观测数据是 'obj.name'时,通过parsePath拿到真正的obj.name的返回值
  8. 除计算属性的观察者以外的所有观察者调用this.get()方法

get()中收集依赖

get中的代码如下:

get () {
    // 将观察者对象保存至Dep.target中(Dep.target在上一章提到过)
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      //调用getter方法,获得被观察目标的值
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      ...
    }
    return value
  }

get()函数中主要做了如下几件事:

  1. 调用pushTarget()方法,将观察者对象保存至Dep.target中,其中Dep.target在上一章提到过
  2. 调用defineReactive中的get实现依赖收集、返回正确值
  3. 上一章讲过,defineReactive中调用dep.depend(),dep.depend()中调用Dep.target.addDep()进行依赖收集

addDep添加依赖

  // 添加依赖
  addDep (dep: Dep) {
    const id = dep.id
    // newDepIds避免本次get中重复收集依赖
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      // 避免多次求值中重复收集依赖,每次求值之后newDepIds会被清空,因此需要depIds来判断。newDepIds中清空
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

响应式的整体流程

根据上一章和本章的讲解,总结一下响应式的整体流程: 假设有模版:

<div id="test">
  {{str}}
</div>
  1. 调用$mount()函数进入到挂载阶段
  2. 检查是否有render()函数,根据上述模版创建render()函数
  3. 调用了mountComponent()函数完成挂载,并在mountComponen()中定义并初始化updateComponent()
  4. 为渲染函数添加观察者,在观察者中对渲染函数求值
  5. 在求值的过程中触发数据对象str的get,在str的get中收集str的观察者到数据的dep中
  6. 修改str的值时,触发str的set,在set中调用数据的dep的notify触发响应
  7. notify中对每一个观察者调用update方法
  8. 在run中调用getAndInvoke函数,进行数据变化。 在getAndInvoke函数中调用回调函数
  9. 对于渲染函数的观察者来说getAndInvoke就相当于执行updateComponent函数
  10. 在updateComponent函数中调用_render函数生成vnode虚拟节点,以虚拟节点vnode作为参数调用_update函数,生成真正的DOM

至此响应式过程完成。

参考文章:揭开数据响应系统的面纱

nanhupatar commented 6 years ago

get