从源码解析vue的响应式原理-响应式的整体流程

栏目: JavaScript · 发布时间: 5年前

内容简介:vue官方对响应式原理的解释:深入响应式原理上一节讲了VUE中为了回答以上的几个问题,我们不得不梳理一波

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

从源码解析vue的响应式原理-响应式的整体流程

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

  • VUE是何时进行依赖收集的?
  • 依赖触发了以后又是怎么进行页面响应式变化的?
  • watcher对象到底起到了什么作用?

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

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

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

if (vm.$options.el) {
	vm.$mount(vm.$options.el)
}
复制代码

在初始化结束后,调用 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功能,其中主要调用了mountComponent()函数完成挂载。 entry-runtime-with-compiler.js是完整的vue的入口,在运行时vue的$mount基础上加入了编译模版的能力。

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

entry-runtime-with-compiler.js中定义了 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();

创建渲染函数

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

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

      ... //省略
    }			
复制代码

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

  1. 得到【模版】字符串:
    • 如果template存在,且template是字符串以#开头,则将该字符串作为id选择器获取对应元素作为【模版】
    • 如果template是元素节点,则将template的innerHTML作为【模版】
    • 如果tempalte是无效字符串,则显示warning
    • 若template不存在,则将el元素的outerHTML作为【模版】
  2. 根据【模版】字符串生成渲染函数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构造函数中做了如下几件事:

  1. 将组件的渲染函数的观察者存入_watcher,将所有的观察者存入_watchers中
  2. 保存before函数,在数据变化之后、触发更新之前调用
  3. 定义一系列实例属性
  4. 兼容被观测数据,当被观测数据是function时,直接将其作为getter; 当被观测数据不是function时通过parsePath解析其真正的返回值,被观测数据是 'obj.name'时,通过parsePath拿到真正的obj.name的返回值
  5. 除计算属性的观察者以外的所有观察者调用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)
      }
    }
  }
复制代码
  • 在addDep中添加依赖,并避免对一个数据多次求值时,其观察者被重复收集。
  • newDepIds避免一次求值的过程中重复收集依赖
  • depIds 属性避免多次求值中重复收集依赖

响应式的整体流程

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

<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

至此响应式过程完成。

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

信息学奥林匹克教程·提高篇

信息学奥林匹克教程·提高篇

吴耀斌 / 湖南师范大学出版社 / 2003-1 / 24.00元

《信息学奥林匹克教程》(提高篇)既有各个算法设计基本思路的讲解及对求解问题的分析,注重了算法引导分析与不同算法的比较,又给出了具体的编程思路与参考程序,程序采用信息学竞赛流行的Turbo Pascal7.0语言编写,并注重结构化与可读性。一起来看看 《信息学奥林匹克教程·提高篇》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

SHA 加密
SHA 加密

SHA 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具