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

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

内容简介: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

至此响应式过程完成。

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


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

查看所有标签

猜你喜欢:

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

Eric Meyer on CSS

Eric Meyer on CSS

Eric Meyer / New Riders Press / 2002-7-8 / USD 55.00

There are several other books on the market that serve as in-depth technical guides or reference books for CSS. None, however, take a more hands-on approach and use practical examples to teach readers......一起来看看 《Eric Meyer on CSS》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具