Vue-js-源码剖析-响应式原理(19)

Vue源码解析-响应式原理

欢迎访问个人网站:blog.5coder.cn

课程目标

  • Vue.js的静态成员和实例成员初始化过程
    • vue.use()vue.set()、vue.extened()等这些全局成员的创建过程
    • vm. e l 、 ‘ v m . el、`vm. elvm.data、vm.$on、vm.$mount`等这些实例成员的创建过程
  • 首次渲染的过程
    • 创建完vue实例,并把数据传递给vue之后,vue内部是如何把数据渲染到页面的,后续在分析源码过程中都是基于这个过程的
  • 数据响应式原理(核心)

准备工作

Vue源码的获取

  • 项目地址:https://github.com/vuejs/vue
  • Fork一份到自己仓库,克隆到本地,可以自己写注释提交到github
  • 为什么分析Vue2.6
    • 到目前为止Vue3.0的正式版本还没有发布
    • 新版本发布后,现有项目不会升级到3.0,2.x患有很长一段过渡期
    • 3.0项目地址:https://github.com/vuejs/vue-next

源码目录结构

vue
    ├─dist  打包之后的结果,包含不同版本
    ├─examples  示例
    ├─flow  
    ├─packages
    ├─scripts
    ├─src
        ├─compiler  编译相关(把模板转换成render函数,render函数会创建虚拟DOM)
        ├─core  Vue 核心库
        	├─components  定义vue自带的keep-alive组件
        	├─global-api  定义vue中的静态方法,包含vue.component()、vue.filter()、vue.extend()等
        	├─instance  创建vue示例的位置,定义vue的构造函数以及vue的初始化、生命周期的响应函数
        	├─observer  响应式机制实现的位置
        	├─utils  公共成员
        	├─vdom  虚拟DOM,vue中的虚拟DOM重写了snabbdom,增加了组件的形式
        ├─platforms 平台相关代码
        	├─web  web平台下相关代码
                ├─compiler
                ├─runtime
                ├─server
                ├─util
                ├─entry-compiler.js  打包入口文件
                ├─entry-runtime.js  打包入口文件
                ├─entry-runtime-with-compiler.js  打包入口文件
                ├─entry-server-basic-renderer.js  打包入口文件
                ├─entry-server-renderer.js  打包入口文件
        	├─weex  week平台下相关代码(week是vue基于移动端下开发的框架)
        ├─server SSR,服务端渲染
        ├─sfc .vue 文件编译为 js 对象(Single File Component单文件组件)
        └─shared 公共的代码

了解Flow

  • 官网:https://flow.org/
  • JavaScript的静态类型检查器
  • Flow的静态类型检车错误是通过静态类型推断实现的
    • 文件开头通过// @flow 或者/* @flow */声明

调试设置

打包
  • 打包工具Rollup

    • vue.js源码的打包工具使用的是Rollup,比webpack清凉
    • Webpack把所有的文件当做模块,Rollup只处理js文件,更适合在Vue.js这样的库中使用
    • Rollup打包不会生成冗余的代码
  • 安装依赖

    yarn
    
  • 设置sourcemap

    • package.json文件中的script的dev脚本中添加参数--sourcemap

      "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev"
      
  • 执行dev

    • 执行打包前先删除dist目录,rollup会自动生成dist目录

    • yarn dev执行打包,用的是rollup,-w参数是坚挺稳健的变化,文件变化自动重新打包

    • 结果

      Vue-js-源码剖析-响应式原理(19)_第1张图片

调试
  • examples的实例中引入的是vue.min.js,将其改为vue.js

  • 打开Chrome的调试工具中的source

    Vue-js-源码剖析-响应式原理(19)_第2张图片

Vue的不同构件版本

  • 运行yarn build重新打包所有文件

  • 官方文档 - 对不同构建版本的解释

  • dist\README.md

    UMD CommonJS ES Module
    Full vue.js vue.common.js vue.esm.js
    Runtime-only vue.runtime.js vue.common.min.js vue.esm.min.js
    Full(Production) vue.min.js
    Runtime-only(Production) vue.runtime.min.js
术语
  • 完整版:同时包含编译器运行时的版本
  • 编译器:用来将模板字符串编译成为JavaScript渲染函数的代码,体积大、效率低
  • 运行时:用来创建Vue实例、渲染并处理虚拟DOM等的代码,体积小、效率高。基本上就是除去编译器的代码
  • UMD:UMD版本通用的模块版本,支持多种模块方式。vue.js默认文件就是运行时+编译器的UMD版本
  • CommonJS(cjs):CommonJS版本用来配合老的打包工具比如Browserify或webpack 1
  • ES Module:从2.6开始Vue会提供两个ES Module(ESM)构建稳健,为现代打包工具提供的版本
    • ESM格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“Tree-Shaking”并将用不到的代码排除最终的包
    • ES6模块与CommonJS模块的差异
Runtime + Compiler VS Runtime-only
DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Titletitle>

head>
<body>
<div id="app">
  Hello World
div>

<script src="../../dist/vue.js">script>
<script>
  // Compiler
  // 需要编译器,把template转换成render函数
  const vm = new Vue({
    el: '#app',
    template: '

{{ msg }}

'
, data: { msg: "Hello Vue" } })
script> body> html>

Vue-js-源码剖析-响应式原理(19)_第3张图片

将vue版本改为vue.runtime.js,发现浏览器报错,提示更改为render函数或者用compiler-included build。

Vue-js-源码剖析-响应式原理(19)_第4张图片

更改template如下:

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Titletitle>

head>
<body>
<div id="app">
  Hello World
div>

<script src="../../dist/vue.runtime.js">script>
<script>
  // Compiler
  // 需要编译器,把template转换成render函数
  const vm = new Vue({
    el: '#app',
    // template: '

{{ msg }}

',
render(h) { return h('h1', this.msg) }, data: { msg: "Hello Vue" } })
script> body> html>

Vue-js-源码剖析-响应式原理(19)_第5张图片

使用vue-cli创建项目时查看vue构件版本

在使用vue create projectName创建的项目中,查看vue的构建版本。由于Vue对webpack.config.js进行了深度封装,所以在目录中无法看到其配置文件,但是Vue提供了命令行来查看配置文件。

vue inspect  # 直接输出到控制台
vue inspect > output.js  # 将执行vue inspect命令后的结果输出到output.js文件中

Vue-js-源码剖析-响应式原理(19)_第6张图片

output.js不是一个有效的webpack配置文件,不能拿来直接使用。

可以看到在resolve中的alias中,vue-cli使用了vue.runtime.esm.js(运行时版本,模块化为ES Module)作为构建版本,vue$中的$符号为精确匹配,在使用时直接使用import Vue from vue

runtime+compiler与runtime对比(ast:抽象语法树),由下面过程可见runtime-only性能更高。

  • runtime+compiler
    • template -> ast -> render -> vdom ->UI
  • runtime-only
    • render -> vdom -> UI

Vue-js-源码剖析-响应式原理(19)_第7张图片

以上内容来自coderwhy哔哩哔哩动画第96节视频

寻找入口文件

  • 查看dist/vue.js的构建过程

执行构建

yarn dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
# --environment TARGET:web-full-dev 设置环境变量TARGET
  • script/config.js的执行过程

    • 作用:生成rollup构建的配置文件
    • 使用环境变量TARGET=web-full-dev
    // 判断环境变量中是否有TARGET
    // 如果有的话,使用genConfig()生成rullup配置文件
    if (process.env.TARGET) {
        module.exports = genConfig(process.env.TARGET)
    } else {
        // 否则获取全部配置
        exports.getBuild = getConfig
        exports.getAllBuilds = () => Objet.keys(builds).map(genconfig)
    }
    
  • genConfig(name)

    • 根据环境变量TARGET获取配置信息
    • builds[name]获取生成配置的信息
    // Runtime+compiler development build (Browser)
    'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.js'),
        format: 'umd',
        env: 'development',
        alias: { he: './entity-decoder'},
        banner
    },
    
  • resolve()

    • 获取入口和出口文件的绝对路径
    const aliases = require('./alias')
    const resolve = p => {
        // 根据路径中的前半部分去alias中找别名
        const base = p.split('/')[0]
        if (aliases[base]) {
            return path.resolve(aliases[base], p.splice(base.length + 1))
        } else {
            return path.resolve(__dirname, '../', p)
        }
    }
    

结果

  • src/platforms/web/entry-runtime-with-compiler.js构建成dist/vue.js,如果设置--sourcemap,则会生成vue.js.map文件
  • src/platform文件夹下是Vue可以构建成不同平台下使用的库,目前有weexweb,还有服务端渲染SSR的库

从入口开始

  • src/platform/web/entry-runtime-with-compiler.js

通过查看源码解决下面问题

  • 观察以下代码,通过阅读源码,回答在页面上输出的结果
const vm = new Vue({    el: '#app',    template: '

Hello Template

'
, render(h) { return h('h4', 'Hello Render') }})
  • 阅读源码记录

    • el不能是body或者html标签
    • 如果没有render,把template转换成render函数
    • 如果有render方法,直接调用mount挂载DOM
    // 1. el 不能是 body 或者 htmlif (el === document.body || el === document.documentElement) {  process.env.NODE_ENV !== 'production' && warn(    `Do not mount Vue to  or  - mount to normal elementsinstead.`  )  return this}const options = this.$optionsif (!options.render) {// 2. 把 template/el 转换成 render 函数……}// 3. 调用 mount 方法,挂载 DOMreturn mount.call(this, el, hydrating)
    
    • 调试代码
      • 调试的方法
    const vm = new Vue({    el: '#app',    template: '

    Hello template

    '
    , render (h) { return h('h4', 'Hello render') }})

    Vue-js-源码剖析-响应式原理(19)_第8张图片

Vue的构造函数在哪里

Vue的构造函数在哪?

Vue实例的成员/Vue的静态成员从哪里来的?

  • src/platform/web/entry-runtime-with-compiler.js中引用了’./runtime/index

  • src/platform/web/runtime/index.js

    • 设置Vue.config
    • 设置平台相关的指令和组件
      • 指令v-model、v-show
      • 组件transition、transition-group
    • 设置平台相关的__patch__方法(打补丁方法,对比新旧的VNode)
    • 设置$mount方法,挂载DOM
    / install platform runtime directives & components// 注册跟平台相关指令和组件extend(Vue.options.directives, platformDirectives)  // 注册指令v-model、v-showextend(Vue.options.components, platformComponents)  // 注册组件v-transition、v-TransitionGroup// install platform patch function// 如果为浏览器环境则返回patch,否则返回noop空函数Vue.prototype.__patch__ = inBrowser ? patch : noop// public mount methodVue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && inBrowser ? query(el) : undefined  return mountComponent(this, el, hydrating)  // 渲染DOM}
    
    • src/platform/web/runtime/index.js中引用了’core/index
    • src/core/index.js
      • 定义了Vue的静态方法
      • initGlobalAPI(Vue)
    • src/core/index.js中引用了’./instance/index
    • src/core/instance/index.js
      • 定义了Vue的构造函数
    / 此处不用class的原因是因为方便后续给Vue实例混入实例成员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')  }  // 调用_init()方法  this._init(options)}// 注册vm的_init()方法,初始化vminitMixin(Vue)// 注册vm的$data/$props/$set/$delete/$watchstateMixin(Vue)// 初始化事件相关方法// $on/$once/$off/$emiteventsMixin(Vue)// 初始化生命周期相关的混入方法// _update/$forceUpdate/$destroylifecycleMixin(Vue)// 混入render// $nextTick/_renderrenderMixin(Vue)
    

四个导出Vue的模块

  • src/platform/web/entry-runtime-with-compiler.js(核心作用:增加了编译的功能)
    • web平台相关的入口
    • 重写了平台相关的$mount()方法
    • 除了使用$mount方法可以将模板字符串转换成render()函数,还定义了Vue.compile()方法可以将模板字符串转换成render()函数
    • 注册了Vue.compile()方法,传递了一个HTML字符串返回render函数
  • src/platform/web/runtime/index.js
    • web平台相关
    • 注册和平台相关的全局指令:v-modelv-show
    • 注册和平台相关的全局组件:v-transitionv-transition-group
    • 全局方法:
      • __patch__:把虚拟DOM转换成真实DOM
      • $mount:挂载方法,把DOM渲染到界面上
  • src/core/index.js
    • 与平台无关
    • 设置了Vue的静态方法,initGlobalAPI(Vue)
  • src/core/instance/index.js
    • 与平台无关
    • 定义了Vue构造函数,调用了this._init(options)方法
    • 给Vue中混入了常用的实例成员

Vue-js-源码剖析-响应式原理(19)_第9张图片

Vue的初始化

src/core/global-api/index.js

  • 初始化Vue的静态方法
/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // 初始化Vue.config对象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  // 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  // 静态方法 set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 让一个对象可响应
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
 // 初始化vue.options对象,并给其扩展
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
  // 设置 keep-alive组件
  extend(Vue.options.components, builtInComponents)

  // 注册Vue.use()用来注册插件
  initUse(Vue)
  // 注册Vue.mixin()实现混入
  initMixin(Vue)
  // 注册Vue.extend()基于传入的options返回一个组件的构造函数
  initExtend(Vue)
  // 注册Vue.directive()、Vue.component()、Vue.filter()
  initAssetRegisters(Vue)
}
  • src/core/global-api/use.js
/* @flow */import { toArray } from '../util/index'export function initUse (Vue: GlobalAPI) {  Vue.use = function (plugin: Function | Object) {    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))    if (installedPlugins.indexOf(plugin) > -1) {      return this    }    // additional parameters    // 把数组中的第一个元素(plugin)去除,后面的是install方法或plugin的参数的参数    const args = toArray(arguments, 1)    args.unshift(this)    // 把this(Vue)插入第一个元素的位置    if (typeof plugin.install === 'function') {      plugin.install.apply(plugin, args)    } else if (typeof plugin === 'function') {      plugin.apply(null, args)    }    installedPlugins.push(plugin)    return this  }}
  • src/core/global-api/mixin.js
/* @flow */import { mergeOptions } from '../util/index'export function initMixin (Vue: GlobalAPI) {  Vue.mixin = function (mixin: Object) {    this.options = mergeOptions(this.options, mixin)    return this  }}
  • src/core/global-api/extend.js
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // Vue的构造函数
    const Super = this
    const SuperId = Super.cid
    // 从缓存中加载组件的构造函数
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      // 如果是开发环境验证组件的名称
      validateComponentName(name)
    }

    // 组件对应的构造函数
    const Sub = function VueComponent (options) {
      // 调用——init()初始化
      this._init(options)
    }
    // 原型继承自Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 合并options
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}
  • src/core/global-api/extend.js
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  // 遍历ASSET_TYPE数组,为Vue定义响应方法
  // ASSET_TYPE包括了directive、component、filter
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        // Vue.component('comp', { template: '' })
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // 把组件配置转换为组件的构造函数
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        // 全局注册,存储资源并赋值
        // this.options['components']['comp'] = definition
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

Vue-js-源码剖析-响应式原理(19)_第10张图片

src/core/instance/index.js

  • 定义Vue的构造函数
  • 初始化Vue的实例成员
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

// Vue构造函数,此处不用class的原因是因为方便后续给Vue实例混入实例成员
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')
  }
  // 调用_init()方法
  this._init(options)
}
// 注册vm的_init()方法,初始化vm
initMixin(Vue)
// 注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入render
// $nextTick/_render
renderMixin(Vue)

export default Vue
  • initMixin(Vue)----(src/core/instance/init.js)

    • 初始化_init()方法

      /* @flow */
      
      import config from '../config'
      import { initProxy } from './proxy'
      import { initState } from './state'
      import { initRender } from './render'
      import { initEvents } from './events'
      import { mark, measure } from '../util/perf'
      import { initLifecycle, callHook } from './lifecycle'
      import { initProvide, initInjections } from './inject'
      import { extend, mergeOptions, formatComponentName } from '../util/index'
      
      let uid = 0
      
      export function initMixin (Vue: Class<Component>) {
        // 给vue的原型挂载init方法
        // 合并options / 初始化操作
        Vue.prototype._init = function (options?: Object) {
          const vm: Component = this
          // a uid
          vm._uid = uid++
      
          let startTag, endTag
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            startTag = `vue-perf-start:${vm._uid}`
            endTag = `vue-perf-end:${vm._uid}`
            mark(startTag)
          }
      
          // a flag to avoid this being observed
          // 如果是Vue实例则不需要被observe
          vm._isVue = true
          // merge options
          // 合并options
          if (options && options._isComponent) {
            // optimize internal component instantiation
            // since dynamic options merging is pretty slow, and none of the
            // internal component options needs special treatment.
            initInternalComponent(vm, options)
          } else {
            vm.$options = mergeOptions(
              resolveConstructorOptions(vm.constructor),
              options || {},
              vm
            )
          }
          /* istanbul ignore else */
          if (process.env.NODE_ENV !== 'production') {
            initProxy(vm)
          } else {
            vm._renderProxy = vm
          }
          // expose real self
          vm._self = vm
          // Vm
          // vm的生命周期相关变量初始化
          // $children/$parent/$root/$refs
          initLifecycle(vm)
          
          // vm的事件监听初始化,父组件绑定在当前组件的事件
          initEvents(vm)
          
          // vm的编译render初始化
          // $slots/$scopedSlots_c/$createElement/$attrs/$listeners
          initRender(vm)
          
          // beforeCreate生命钩子的回调
          callHook(vm, 'beforeCreate')
          
          // 把inject的成员注入到vm上
          initInjections(vm) // resolve injections before data/props
          
          // 初始化vm的_props/methods/_data/computed/watch
          initState(vm)
          
          // 初始化provide
          initProvide(vm) // resolve provide after data/props
          
          // create生命钩子的回调
          callHook(vm, 'created')
      
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            vm._name = formatComponentName(vm, false)
            mark(endTag)
            measure(`vue ${vm._name} init`, startTag, endTag)
          }
      
          if (vm.$options.el) {
            vm.$mount(vm.$options.el)
          }
        }
      }
      
      export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
        const opts = vm.$options = Object.create(vm.constructor.options)
        // doing this because it's faster than dynamic enumeration.
        const parentVnode = options._parentVnode
        opts.parent = options.parent
        opts._parentVnode = parentVnode
      
        const vnodeComponentOptions = parentVnode.componentOptions
        opts.propsData = vnodeComponentOptions.propsData
        opts._parentListeners = vnodeComponentOptions.listeners
        opts._renderChildren = vnodeComponentOptions.children
        opts._componentTag = vnodeComponentOptions.tag
      
        if (options.render) {
          opts.render = options.render
          opts.staticRenderFns = options.staticRenderFns
        }
      }
      
      export function resolveConstructorOptions (Ctor: Class<Component>) {
        let options = Ctor.options
        if (Ctor.super) {
          const superOptions = resolveConstructorOptions(Ctor.super)
          const cachedSuperOptions = Ctor.superOptions
          if (superOptions !== cachedSuperOptions) {
            // super option changed,
            // need to resolve new options.
            Ctor.superOptions = superOptions
            // check if there are any late-modified/attached options (#4976)
            const modifiedOptions = resolveModifiedOptions(Ctor)
            // update base extend options
            if (modifiedOptions) {
              extend(Ctor.extendOptions, modifiedOptions)
            }
            options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
            if (options.name) {
              options.components[options.name] = Ctor
            }
          }
        }
        return options
      }
      
      function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
        let modified
        const latest = Ctor.options
        const sealed = Ctor.sealedOptions
        for (const key in latest) {
          if (latest[key] !== sealed[key]) {
            if (!modified) modified = {}
            modified[key] = latest[key]
          }
        }
        return modified
      }
      
      
  • stateMixin(Vue)

    Vue-js-源码剖析-响应式原理(19)_第11张图片

  • eventsMixin(Vue)

    Vue-js-源码剖析-响应式原理(19)_第12张图片

  • lifecycleMixin(Vue)

Vue-js-源码剖析-响应式原理(19)_第13张图片

  • renderMixin(Vue)

Vue-js-源码剖析-响应式原理(19)_第14张图片

首次渲染过程

  • Vue初始化完毕,开始真正的执行
  • 调用new Vue()之前,已经初始化完毕
  • 通过调试代码,记录首次渲染过程

Vue-js-源码剖析-响应式原理(19)_第15张图片

数据响应式原理

参考之前的文章:模拟Vue.js响应式原理,文章中自己模拟了响应式原理,实现了简易版的响应式机制,其中的思想与方法与Vue.js源码吻合,可对照查看。

通过查看源码解决下面问题

  • vm.msg = { count: 0 },重新给属性赋值,是否是响应式的?
  • vm.arr[0] = 4,给数组元素赋值,视图是否会更新?
  • vm.arr.length = 0, 修改数组的length,视图是否会更新?
  • vm.arr.push(4),视图是否会更新?

响应式处理的入口

整个响应式处理的过程是比较复杂的,下面我们先从

  • src/core/instance/init.js

    • initState(vm)vm状态的初始化
    • 初始化了_data、_props、methods
  • src/core/instance/state.js

    // 数据的初始化
    if (opts.data) {
        initData(vm)
    } else {
        observe(vm._data = {}, true /* asRootData */)
    }
    
  • initData(vm) vm数据的初始化

    function initData (vm: Component) {
      let data = vm.$options.data
      // 初始化_data,组件中data是函数,调用函数返回结果
      // 否则直接返回data
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      // 获取data中的所有属性
      const keys = Object.keys(data)
      // 获取props / methods
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      // 判断data上的成员是否和props/methods重名
      while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        if (props && hasOwn(props, key)) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else if (!isReserved(key)) {
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      // 响应式处理
      observe(data, true /* asRootData */)
    }
    
  • src/core/observer/index.js

    • observe(value, asRootData)

    • 负责为每一个Object类型的value创建一个observer实例

      export function observe (value: any, asRootData: ?boolean): Observer | void {  // 判断 value 是否是对象  if (!isObject(value) || value instanceof VNode) {    return  }  let ob: Observer | void  // 如果 value 有 __ob__(observer对象) 属性 结束  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {    ob = value.__ob__  } else if (    shouldObserve &&    !isServerRendering() &&    (Array.isArray(value) || isPlainObject(value)) &&    Object.isExtensible(value) &&    !value._isVue  ) {    // 创建一个 Observer 对象    ob = new Observer(value)  }  if (asRootData && ob) {    ob.vmCount++  }  return ob}
      

Observer

  • src/core/observer/index.js

    • 对对象做响应化处理

    • 对数组做响应化处理

      export class Observer {  // 观测对象  value: any;  // 依赖对象  dep: Dep;  // 实例计数器  vmCount: number; // number of vms that have this object as root $data  constructor (value: any) {    this.value = value    this.dep = new Dep()    // 初始化实例的 vmCount 为0    this.vmCount = 0    // 将实例挂载到观察对象的 __ob__ 属性    def(value, '__ob__', this)    // 数组的响应式处理    if (Array.isArray(value)) {      if (hasProto) {        protoAugment(value, arrayMethods)      } else {        copyAugment(value, arrayMethods, arrayKeys)      }      // 为数组中的每一个对象创建一个 observer 实例      this.observeArray(value)    } else {      // 遍历对象中的每一个属性,转换成 setter/getter      this.walk(value)    }  }  /**   * Walk through all properties and convert them into   * getter/setters. This method should only be called when   * value type is Object.   */  walk (obj: Object) {    // 获取观察对象的每一个属性    const keys = Object.keys(obj)    // 遍历每一个属性,设置为响应式数据    for (let i = 0; i < keys.length; i++) {      defineReactive(obj, keys[i])    }  }  /**   * Observe a list of Array items.   */  observeArray (items: Array) {    for (let i = 0, l = items.length; i < l; i++) {      observe(items[i])    }  }}
      
    • wakl(obj)

      • 遍历obj的所有属性,为每一个属性调用defineReactive()方法,设置getter/setter

defineReactive()

  • src/core/observer/index.js
  • defineReactive(obj, key, val, customSetter, shallow)
    • 为一个对象定义一个响应式的属性,每一个属性对应一个dep对象
    • 如果该属性的值是对象,继续调用observe
    • 如果给属性赋新值,继续调用observe
    • 如果数据更新发送通知

对象响应式处理

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 1.为每一个属性,创建依赖对象实例
  const dep = new Dep()
  // 获取obj的属性描述符
  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) && arguments.length === 2) {
    val = obj[key]
  }
  // 2.判断是否递归观察子对象,并将子对象属性都转换成getter/setter,返回子观察对象
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 如果预定义的getter存在,则value等于getter调用的返回值
      // 否则直接赋予属性值
      const value = getter ? getter.call(obj) : val
      // 如果存在当前依赖目标,即watcher对象,则建立依赖
      if (Dep.target) {
        // dep()添加相互的依赖
        // 一个组件对应一个watcher对象
        // 一个watcher会对应多个dep(要观察的属性很多)
        // 我们可以手动创建多个watcher监听一个属性的变化,一个dep可以对应多个watcher
        dep.depend()
        // 如果子观察对象目标存在,建立子对象的依赖关系
        if (childOb) {
          childOb.dep.depend()
          // 如果属性是数组,则特殊处理收集数组对象依赖
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      // 返回属性值
      return value
    },
    set: function reactiveSetter (newVal) {
      // 如果预定义的getter存在则value等于getter调用的返回值
      // 否则直接赋予属性值
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 如果新值等于旧值或者新旧值为NaN则不执行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // 如果没有setter直接返回
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      // 如果预定义setter存在则调用,否则直接更新新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 3.如果新值是对象,观察子对象并返回子对象的observer对象
      childOb = !shallow && observe(newVal)
      // 4.派发更新(发布更改通知)
      dep.notify()
    }
  })
}

数组的响应式处理

  • Observer的构造函数中

    // 数组的响应式处理
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 为数组中的每一个对象创建一个 observer 实例
      this.observeArray(value)
    } else {
      // 遍历对象中的每一个属性,转换成 setter/getter
      this.walk(value)
    }
    
    
    // helpers
    
    /**
     * Augment a target Object or Array by intercepting
     * the prototype chain using __proto__
     */
    function protoAugment (target, src: Object) {
      /* eslint-disable no-proto */
      target.__proto__ = src
      /* eslint-enable no-proto */
    }
    
    /**
     * Augment a target Object or Array by defining
     * hidden properties.
     */
    /* istanbul ignore next */
    function copyAugment (target: Object, src: Object, keys: Array<string>) {
      for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
      }
    }
    
  • 处理数组修改数据的方法

    • src/core/observer/array.js

      /*
       * not type checking this file because flow doesn't play well with
       * dynamically accessing methods on Array prototype
       */
      
      import { def } from '../util/index'
      
      const arrayProto = Array.prototype
      // 使用数组的原型创建一个新的对象(克隆数组的原型)
      export const arrayMethods = Object.create(arrayProto)
      // 修改数组元素的方法
      const methodsToPatch = [
        'push',
        'pop',
        'shift',
        'unshift',
        'splice',
        'sort',
        'reverse'
      ]
      
      /**
       * Intercept mutating methods and emit events
       */
      methodsToPatch.forEach(function (method) {
        // cache original method
        // 保存数组原方法
        const original = arrayProto[method]
        // 调用Object.defineProperty() 重新定义修改数组的方法
        def(arrayMethods, method, function mutator (...args) {
          // 执行数组的原始方法
          const result = original.apply(this, args)
          // 获取数组对象的ob对象
          const ob = this.__ob__
          let inserted
          switch (method) {
            case 'push':
            case 'unshift':
              inserted = args
              break
            case 'splice':
              inserted = args.slice(2)
              break
          }
          // 对插入的新元素,重新遍历数组元素设置为响应式数据
          if (inserted) ob.observeArray(inserted)
          // notify change
          // 调用了修改数组的方法,调用数组的ob对象发送通知
          ob.dep.notify()
          return result
        })
      })
      
      
    • def方法

      /**
       * Define a property.
       */
      export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
        Object.defineProperty(obj, key, {
          value: val,
          enumerable: !!enumerable,
          writable: true,
          configurable: true
        })
      }
      

Dep类

  • src/core/observer/dep.js
  • 依赖对象
  • 记录watcher对象
  • depend() ---- watcher记录对应的dep
  • 发布通知
  1. defineReactive()中的getter中创建dep对象,并判断Dep.target是否有值(一会再来看看有什么时候有值得),调用dep.depend()
  2. dep.depend()内部调用Dep.target.addDep(this),也就是watcheraddDep()方法,它内部最后调用dep.addSub(this),把watcher对象,添加到dep.subs.push(watcher)中,也就是把订阅者添加到depsubs数组中,当数据变化的时候调用watcher对象的update()方法
  3. 什么时候设置的Dep.target?通过简单的案例调试观察。调用mountComponent()方法的时候,创建了渲染watcher对象,执行watcher中的get()方法
  4. get()方法内部调用pushTarget(this),把当前Dep.target = watcher,同时把当前watcher入栈,因为有父子组件嵌套的时候,先把父组件对应的watcher入栈,再去处理子组件的watcher,子组件的处理完毕后,再把父组件对应的watcher出栈,继续操作
  5. Dep.target用来存放目前正在使用的watcher。全局唯一,并且一次也只能有一个watcher被使用
/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0
// dep 是个可观察对象,可以有多个指令订阅它
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  // 静态属性,watcher 对象
  static target: ?Watcher;
  // dep 实例 Id
  id: number;
  // dep 实例对应的 watcher 对象/订阅者数组
  subs: Array<Watcher>;

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

  // 添加新的订阅者 watcher 对象
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  // 移除订阅者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  // 将观察对象和 watcher 建立依赖
  depend () {
    if (Dep.target) {
      // 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
      Dep.target.addDep(this)
    }
  }

  // 发布通知
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    // 调用每个订阅者的update方法实现更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// Dep.target 用来存放目前正在使用的watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
// 入栈并将当前 watcher 赋值给 Dep.target
// 父子组件嵌套的时候先把父组件对应的 watcher 入栈,
// 再去处理子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,继续操作
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  // 出栈操作
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher类

  • Watcher分为三种,Comouted Watcher、用户Watcher(侦听器)、渲染Watcher

  • 渲染Watcher的创建时机

    • src/core/instance/lifecycle.js

      export function mountComponent (
        vm: Component,
        el: ?Element,
        hydrating?: boolean
      ): Component {
        vm.$el = el
        if (!vm.$options.render) {
          vm.$options.render = createEmptyVNode
          if (process.env.NODE_ENV !== 'production') {
            /* istanbul ignore if */
            if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
              vm.$options.el || el) {
              warn(
                'You are using the runtime-only build of Vue where the template ' +
                'compiler is not available. Either pre-compile the templates into ' +
                'render functions, or use the compiler-included build.',
                vm
              )
            } else {
              warn(
                'Failed to mount component: template or render function not defined.',
                vm
              )
            }
          }
        }
        callHook(vm, 'beforeMount')
      
        let updateComponent
        /* istanbul ignore if */
        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)
            const vnode = vm._render()
            mark(endTag)
            measure(`vue ${name} render`, startTag, endTag)
      
            mark(startTag)
            vm._update(vnode, hydrating)
            mark(endTag)
            measure(`vue ${name} patch`, startTag, endTag)
          }
        } else {
          updateComponent = () => {
            vm._update(vm._render(), hydrating)
          }
        }
      	// 创建渲染Watcher,exOrFn为updateComponent
        // we set this to vm._watcher inside the watcher's constructor
        // since the watcher's initial patch may call $forceUpdate (e.g. inside child
        // component's mounted hook), which relies on vm._watcher being already defined
        new Watcher(vm, updateComponent, noop, {
          before () {
            if (vm._isMounted && !vm._isDestroyed) {
              callHook(vm, 'beforeUpdate')
            }
          }
        }, true /* isRenderWatcher */)
        hydrating = false
      
        // manually mounted instance, call mounted on self
        // mounted is called for render-created child components in its inserted hook
        if (vm.$vnode == null) {
          vm._isMounted = true
          callHook(vm, 'mounted')
        }
        return vm
      }
      
  • 渲染watcher创建的位置:lifecycle.js的mountComponent函数中

  • Watcher的构造函数初始化,处理exOrFn(渲染watcher和侦听器处理不同,渲染watcherupdateComponent,对比新旧vdom并渲染到页面上)

  • 调用this.get(),他里面调用pushTarget(),然后this.getter.call(vm,vm)(对于渲染watcher调用updateComponent),如果是用户watcher会回去属性的值(触发get操作)

  • 当数据更新时,dep中调用notify()方法,notify()中调用watcherupdate()方法

  • update()中调用queueWatcher()

  • queueWatcher()是一个核心方法,去除重复操作,调用flushSchedulerQueue()刷新队列并执行watcher

  • flushSchedulerQueue()中对watcher排序,遍历所有watcher,如果有before,触发生命周期的钩子函数beforeUpdate,执行wacher.run(),它内部调用this.get(),然后调用this.cb()(渲染wachercbnoop,侦听器的function

    1. 组件更新的顺序是从父组件到子组件(因为先创建父组件,后创建子组件)
    2. 组件的用户watcher在渲染watcher之前运行(因为用户watcher(initState)是在渲染watcher(mountComponent)之前创建的)
    3. 如果一个组件在父组件执行之前被销毁,那他应该被跳过
  • 整个流程结束

调试响应式数据执行过程

  • 数组响应式处理的核心过程和数组收集依赖的过程

  • 当数组的数据改变的时候watcher的执行过程

    DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>observetitle>
    head>
    <body>
      <div id="app">
        {{ arr }}
      div>
    
      <script src="../../dist/vue.js">script>
      <script>
        const vm = new Vue({
          el: '#app',
          data: {
            arr: [2, 3, 5]
          }
        })
    
        // vm.arr.push(8)
        // vm.arr[0] = 100
        // vm.arr.length = 0
        
      script>
    body>
    html>
    

回答以下问题

  • 检测变化的注意事项

    methods: {
      handler() {
        this.obj.count = 555
        this.arr[0] = 1
        this.arr.length = 0
        this.arr.push(4)
      }
    }
    
  • 转换成响应式数据

    methods: {
      handler() {
        this.$set(this.obj, 'count', 555)
        this.$set(this.arr, 0, 1)
        this.arr.splice(0)
      }
    }
    

数据响应式原理总结

Vue-js-源码剖析-响应式原理(19)_第16张图片

动态添加一个响应式属性

当我们给一个响应式对象,动态增加一个对象,这个属性是否为响应式?

示例代码

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>settitle>
head>
<body>
  <div id="app">
    {{ obj.title }}
    <hr>
    {{ obj.name }}
    <hr>
    {{ arr }}
  div>

  <script src="../../dist/vue.js">script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        obj: {
          title: 'Hello Vue'
        },
        arr: [1, 2, 3]
      }
    })
  script>
body>
html>

打开浏览器开发者模式,分别键入如下内容:

  • vm.obj.name = 'abc'

    Vue-js-源码剖析-响应式原理(19)_第17张图片

    可以发现,动态给obj增加name属性,视图并未更新,说明此时name属性不是响应式的

  • vm.$set(vm.obj, 'name', 'zhangsan')

    Vue-js-源码剖析-响应式原理(19)_第18张图片

    可以使用vm.$set(vm.obj, 'name', 'zhangsan')来给响应式对象动态添加响应式属性(或者使用Vue.set())。

    使用vm. s e t ( ) 方法改变数组的第一个元素的值: ‘ v m . set()方法改变数组的第一个元素的值:`vm. set()方法改变数组的第一个元素的值:vm.set(vm.arr, 0, 100)`。vm.$set()官方文档

    Vue-js-源码剖析-响应式原理(19)_第19张图片

    Vue-js-源码剖析-响应式原理(19)_第20张图片

    不能给Vue实例或者Vue实例的跟数组对象动态的添加响应式属性。

    Vue-js-源码剖析-响应式原理(19)_第21张图片

实例方法/数据

vm.$set

定义位置

  • Vue.set()

    • global-api/index.js
    // 静态方法 set/delete/nextTick
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = nextTick
    
  • vm.$set()

    • instance/index.js
    // 注册vm的$data/$props/$set/$delete/$watch
    // instance/state.js
    stateMixin(Vue)
    
    // instance/state.js
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    

源码

  • set()方法

    • observer/index.js
    /**
     * Set a property on an object. Adds the new property and
     * triggers change notification if the property doesn't
     * already exist.
     */
    export function set (target: Array<any> | Object, key: any, val: any): any {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 判断target是否是数组,key值是否是合法的索引
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        // 通过splice对key位置的元素进行替换
        // splice在array.js进行了响应化的处理,此处的splice已经不是原生的splice方法
        target.splice(key, 1, val)
        return val
      }
      // 如果key在对象中已经存在,直接赋值
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      // 获取target中的observer对象
      const ob = (target: any).__ob__
      // 如果target是Vue实例或者$data直接返回,如果是$data的话其observe对象中的vmCount为1,否则为0
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
        )
        return val
      }
      // 如果ob不存在,target不是响应式对象,此时直接赋值即可
      if (!ob) {
        target[key] = val
        return val
      }
      // 把key设置为响应式属性
      defineReactive(ob.value, key, val)
      // 发送通知
      ob.dep.notify()
      return val
    }
    

调试

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>settitle>
head>
<body>
  <div id="app">
    {{ obj.title }}
    <hr>
    {{ obj.name }}
    <hr>
    {{ arr }}
  div>

  <script src="../../dist/vue.js">script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        obj: {
          title: 'Hello Vue'
        },
        arr: [1, 2, 3]
      }
    })
  script>
body>
html>

回顾defineReactive中的childOb,给每一个响应式对象设置一个ob

调用$set的时候,会获取ob对象,并通过ob.dep.notify()发送通知

vm.$delete

  • 功能

    删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开Vue不能检测到属性被删除的限制,但是你应该很少会使用它。

    注意:目标不能是一个Vue实例或者Vue实例的跟数据对象

  • 示例

    vm.$delete(vm.obj, 'title')
    

定义位置

  • Vue.delete()

    • global-api/index.js
    // 静态方法 set/delete/nextTick
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = nextTick
    
  • vm.$delete()

    • instance/index.js
    // 注册 vm 的 $data/$props/$set/$delete/$watch
    stateMixin(Vue)
    
    // instance/state.js
    Vue.prototype.$set = set
    Vue.prototype.$delete = del
    

源码

  • src/core/observer/index.js

    /**
     * Delete a property and trigger change if necessary.
     */
    export function del (target: Array<any> | Object, key: any) {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 判断是否是数组,以及key是否合法
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 如果是数组通过splice删除
        // splice做过响应式处理
        target.splice(key, 1)
        return
      }
      // 获取target的ob对象
      const ob = (target: any).__ob__
      // target如果是Vue实例或者$data对象,直接返回
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.'
        )
        return
      }
      // 如果target对象没有key属性直接返回,判断依据是:key是否直接属于target属性,而不是继承来的
      // 如果是继承来的或者没有这个属性,直接返回
      if (!hasOwn(target, key)) {
        return
      }
      // 删除属性
      delete target[key]
      if (!ob) {
        return
      }
      // 通过ob发送通知
      ob.dep.notify()
    }
    

vm.$watch

vm.$watch(expOrFn, callback, [options]),官方文档

  • 功能

    观察Vue实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更负责的表达式,用一个函数取代

  • 参数

    • expOrFn:要监视的$data中的属性,可以是表达式或函数
    • callback:数据变化后执行的函数
      • 函数:回调函数
      • 对象:具有handler属性(字符串或者函数),如果该属性为字符串则methods中相应的定义
    • options:可选的选项
      • deep:布尔类型,深度监听
      • immediate:布尔类型,是否立即执行一次回调函数
  • 示例1

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>watcher</title>
    </head>
    <body>
      <div id="app">
        {{ user.fullName }}
      </div>
    
      <script src="../../dist/vue.js"></script>
      <script>
        const vm = new Vue({
          el: '#app',
          data: {
            user: {
              firstName: '诸葛',
              lastName: '亮',
              fullName: ''
            }
          }
        })
    
        vm.$watch('user',
          function (newValue, oldValue) {
            this.user.fullName = newValue.firstName + ' ' + newValue.lastName
          }
        )
      </script>
    </body>
    </html>
    

    打开浏览器,发现并没有立即显示诸葛亮,此时需要添加第三个参数options,内容为:{immediate: true},其意味着立即执行。再次刷新页面,发现页面显示了诸葛亮。

    当我们需要监听vm. d a t a . u s e r . f i r s t N a m e 时,发现后续可能还要监听 v m . data.user.firstName时,发现后续可能还要监听vm. data.user.firstName时,发现后续可能还要监听vm.data.user.lastName,所以此时写多个watch是非常不方便的,此时在options中添加:deep: true,即为深度监听。不但监听user对象的变化,而且监听其内部属性的变化。此时修改firstName,发现视图也会更新。

  • 示例2

    const vm = new Vue({
      el: '#app',
      data: {
        a: '1',
        b: '2',
        msg: 'Hello Vue',
        user: {
          firstName: '诸葛',
          lastName: '亮'
        }
      }
    })
    
    // expOrFn是表达式
    vm.$watch('msg', function (newVal, oldVal) {
      congole.log(newVal)
    })
    vm.$watch('user.firstName', function (newVal, oldVal) {
      congole.log(newVal)
    })
    
    // expOrFn是函数
    vm.$watch(function () {
      return this.a + this.b
    }, function (newVal, oldVal) {
      console.log(newVal)
    })
    
    // deep是true,此时比较消耗性能
    vm.$watch('user', function (newVal, oldVal) {
      console.log(newVal)
    }, {
      deep: true
    })
    
    // immediate是true
    vm.$watch('msg', function (newVal, oldVal) {
      console.log(newVal)
    }, {immediate: true})
    

三种类型的Watcher对象

  • 没有静态方法,因为$watch方法中要使用vue的实例
  • Watcher分三种:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher
  • 创建顺序:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher
  • vm.$watch()
    • src/core/instance/state.js

源码

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  // 获取 Vue 实例 this
  const vm: Component = this
  if (isPlainObject(cb)) {
    // 判断如果 cb 是对象执行 createWatcher
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  // 标记为用户 watcher
  options.user = true
  // 创建用户 watcher 对象
  const watcher = new Watcher(vm, expOrFn, cb, options)
  // 判断 immediate 如果为 true
  if (options.immediate) {
    // 立即执行一次 cb 回调,并且把当前值传入
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  // 返回取消监听的方法
  return function unwatchFn () {
    watcher.teardown()
  }
}

调试

  • 查看watcher的创建顺序

  • 测试代码

    • DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>watchertitle>
      head>
      <body>
        <div id="app">
          {{ reversedMessage }}
          <hr>
          {{ user.fullName }}
        div>
      
        <script src="../../dist/vue.js">script>
        <script>
          const vm = new Vue({
            el: '#app',
            data: {
              message: 'Hello Vue',
              user: {
                firstName: '诸葛',
                lastName: '亮',
                fullName: ''
              }
            },
            computed: {
              reversedMessage: function () {
                return this.message.split('').reverse().join('')
              }
            },
            watch: {
              // 'user.firstName': function (newValue, oldValue) {
              //   this.user.fullName = this.user.firstName + ' ' + this.user.lastName
              // },
              // 'user.lastName': function (newValue, oldValue) {
              //   this.user.fullName = this.user.firstName + ' ' + this.user.lastName
              // },
              'user': {
                handler: function (newValue, oldValue) {
                  this.user.fullName = this.user.firstName + ' ' + this.user.lastName
                },
                deep: true,
                immediate: true
              }
            }
          })
        script>
      body>
      html>
      

      将断点打在src/core/observer/watcher.js中的watcher构造函数中

    • 计算属性watcher

      Vue-js-源码剖析-响应式原理(19)_第22张图片

    • 用户watcher(侦听器)

      Vue-js-源码剖析-响应式原理(19)_第23张图片

      Vue-js-源码剖析-响应式原理(19)_第24张图片

    • 渲染watcher

      Vue-js-源码剖析-响应式原理(19)_第25张图片

      Vue-js-源码剖析-响应式原理(19)_第26张图片

      • 查看渲染watcher的执行过程
        • 当数据更新,defineReactiveset方法中调用dep.notify()
        • 调用watcherupdate()
        • 调用ququeWatcher(),把watcher存入队列,如果已经存在,不重复添加
        • 循环调用flushSchedulerQueue()
          • 通过nextTick(),在消息循环结束之前时候调用flushShedulerQueue()
        • 调用watcher.run()
          • 调用watcher.get()获取最新值
          • 如果是渲染wacher结束
          • 如果 用户watcher,调用this.cb()

异步更新队列-nextTick()

  • Vue更新DOM是异步执行的,批量的
    • 在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个办法,获取更新后的DOM
  • vm.$nextTick(function() { /* 操作DOM */ } / Vue.$nextTick(function () {})

vm.$nextTick()代码演示

DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>nextTicktitle>
head>
<body>
  <div id="app">
    <p id="p" ref="p1">{{ msg }}p>
    {{ name }}<br>
    {{ title }}<br>
  div>
  <script src="../../dist/vue.js">script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello nextTick',
        name: 'Vue.js',
        title: 'Title'
      },
      mounted() {
        this.msg = 'Hello World'
        this.name = 'Hello snabbdom'
        this.title = 'Vue.js'
  
        Vue.nextTick(() => {
          console.log(this.$refs.p1.textContent)
        })
      }
    })

    
  script>
body>
html>

vm.$nextTick()代码演示

定义位置

  • src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {
  return nextTick(fn. this)
}

源码

  • 手动调用vm.$nextTick()

  • WatcherqueueWatcher中执行nextTick()

  • src/core/util/next-tick.js

  • $nextTick()实例方法

    Vue-js-源码剖析-响应式原理(19)_第27张图片

  • $nextTick()静态方法

    Vue-js-源码剖析-响应式原理(19)_第28张图片

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 把 cb 加上异常处理存入 callbacks 数组中
  callbacks.push(() => {
    if (cb) {
      try {
        // 调用 cb()
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 调用
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    // 返回 promise 对象
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
  • timerFunc()
/* @flow */
/* globals MutationObserver */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 把 cb 加上异常处理存入 callbacks 数组中
  callbacks.push(() => {
    if (cb) {
      try {
        // 调用 cb()
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    // 调用
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    // 返回 promise 对象
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

你可能感兴趣的:(大前端【进阶】之路,vue,vue响应式原理,observer,watcher,dep)