学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理

Vue.js 源码剖析 - 响应式原理 准备工作 Vue源码获取

这里主要分析 Vue 2.6版本的源码,使用Vue 3.0版本来开发项目还需要一段时间的过渡
  • 项目地址:
    • Vue 2.6 https://github.com/vuejs/vue
    • Vue 3.x https://github.com/vuejs/vue-next
  • Fork一份到自己仓库,克隆到本地,这样可以自己写注释提交,如果直接从github clone太慢,也可以先导入gitee,再从gitee clone到本地
  • 查看Vue源码的目录结构
    src
    compiler 编译相关
    core Vue 核心库
    platforms 平台相关代码(web、weex)
    server SSR 服务端渲染
    sfc 单文件组件 .vue 文件编译为 js 对象
    shared 公共代码
Flow
【学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理】Vue 2.6 版本中使用了Flow,用于代码的静态类型检查
Vue 3.0+ 版本已使用TypeScript进行开发,因此不再需要Flow
  • Flow是JavaScript的静态类型检查器
  • Flow的静态类型检查是通过静态类型推断来实现的
    • 安装Flow以后,在要进行静态类型检查的文件头中通过 // @flow/* @flow */ 注释的方式来声明启用
    • 对于变量类型的指定,与TypeScript类似
      /* @flow */ function hello (s: string): string { return `hello ${s}` } hello(1) // Error

打包与调试
Vue 2.6中使用Rollup来打包
  • 打包工具Rollup
    • Rollup比webpack轻量
    • Rollup只处理js文件,更适合用来打包工具库
    • Rollup打包不会生成冗余的代码
  • 调试
    • 执行yarn安装依赖(有yarn.lock文件)
    • package.json文件dev script中添加 --sourcemap 参数来开启sourcemap,以便调试过程中查看代码
Vue不同构建版本的区别
执行yarn build可以构建所有版本的打包文件
UMD CommonJS ES Module
Full vue.js vue.common.js vue.esm.js
Runtime-only vue.runtime.js vue.runtime.common.js vue.runtime.esm.js
Full(production) vue.min.js
Runtime-only(production) vue.runtime.min.js
不同版本之间的区别
  • 完整版:同时包含编译器和运行时的版本
    • 编译器:用来将模板字符串编译为JavaScript渲染(render)函数的代码,体积大、效率低
    • 运行时:用来创建Vue实例,渲染并处理虚拟DOM等的代码,体积小、效率高,即去除编译器之后的代码
    • 简单来说,运行时版本不包含编译器的代码,无法直接使用template模板字符串,需要自行使用render函数
    • 通过Vue Cli创建的项目,使用的是vue.runtime.esm.js版本
  • 运行时版:只包含运行时的版本
  • UMD:通用模块版本,支持多种模块方式。vue.js默认就是运行时+编译器的UMD版本
  • CommonJS:CommonJS模块规范的版本,用来兼容老的打包工具,例如browserfy、webpack 1等
  • ES Module:从2.6版本开始,Vue会提供两个esm构建文件,是为现代打包工具提供的版本
    • esm格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行tree-shaking,精简调未被用到的代码
寻找入口文件
通过查看构建过程,来寻找对应构建版本的入口文件位置
  • 以vue.js版本的构建为例,通过rollup进行构建,指定了配置文件scripts/config.js,并设置了环境变量TARGET:web-full-dev
    "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
  • 进一步查看 scripts/config.js 配置文件
    module.exports导出的内容来自genConfig()函数,并接收了环境变量TARGET作为参数
    // scripts/config.js if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }

    genConfig函数组装生成了config配置对象,入口文件配置input: opts.entry,配置项的值opts.entry来自builds[name]
    // scripts/config.js function genConfig (name) { const opts = builds[name] const config = { input: opts.entry, external: opts.external, plugins: [ flow(), alias(Object.assign({}, aliases, opts.alias)) ].concat(opts.plugins || []), output: { file: opts.dest, format: opts.format, banner: opts.banner, name: opts.moduleName || 'Vue' }, onwarn: (msg, warn) => { if (!/Circular/.test(msg)) { warn(msg) } } } ... }

    通过传入环境变量TARGET的值,可以找到web-full-dev相应的配置,入口文件是web/entry-runtime-with-compiler.js
    const builds = { // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify 'web-runtime-cjs-dev': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.dev.js'), format: 'cjs', env: 'development', banner }, 'web-runtime-cjs-prod': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.prod.js'), format: 'cjs', env: 'production', banner }, // Runtime+compiler CommonJS build (CommonJS) 'web-full-cjs-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.common.dev.js'), format: 'cjs', env: 'development', alias: { he: './entity-decoder' }, banner }, // 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 }, ... }

使用VS Code查看Vue源码的两个问题
使用VSCode查看Vue源码时通常会碰到两个问题
  • 对于flow的语法显示异常报错
    • 修改VSCode设置,在setting.json中增加"javascript.validate.enable": false配置
  • 对于使用了泛型的后续代码,丢失高亮
    • 通过安装Babel JavaScript插件解决
一切从入口开始
入口文件 entry-runtime-with-compiler.js 中,为Vue.prototype.$mount指定了函数实现
  • el可以是DOM元素,或者选择器字符串
  • el不能是body或html
  • 选项中有render,则直接调用mount挂载DOM
  • 选项中如果没有render,判断是否有template,没有template则将el.outerHTML作为template,并尝试将template转换成render函数
    Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // 判断el是否是 DOM 元素 // 如果el是字符串,则当成选择器来查询相应的DOM元素,查询不到则创建一个div并返回 el = el && query(el)/* istanbul ignore if */ // 判断el是否是body或者html // Vue不允许直接挂载在body或html标签下 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue toor - mount to normal elements instead.` ) return this }const options = this.$options // resolve template/el and convert to render function // 判断选项中是否包含render if (!options.render) { // 如果没有render,判断是否有template let template = options.template if (template) { ... } else if (el) { // 如果没有template,则获取el的outerHTML作为template template = getOuterHTML(el) } if (template) { ...// 将template转换成render函数 const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns... } } // 调用 mount 挂载 DOM return mount.call(this, el, hydrating) }

tips:$mount()函数在什么位置被调用?通过浏览器调试工具的Call Stack视图,可以简单且清晰的查看一个函数被哪个位置的上层代码所调用
Vue初始化 初始化相关的几个主要文件
  • src/platforms/web/entry-runtime-with-compiler.js
    • 重写了平台相关的$mount()方法
    • 注册了Vue.compile()方法,将HTML字符串转换成render函数
  • src/platforms/web/runtime/index.js
    • 注册了全局指令:v-model、v-show
    • 注册了全局组件:v-transition、v-transition-group
    • 为Vue原型添加了全局方法
      • _patch_:把虚拟DOM转换成真实DOM(snabbdom的patch函数)
      • $mount: 挂载方法
  • src/core/index.js
    • 调用initGlobalAPI(Vue)设置了Vue的全局静态方法
  • src/core/instance/index.js - Vue构造函数所在位置
    • 定义了Vue构造函数,调用了this._init(options)方法
    • 给Vue中混入了常用的实例成员和方法
静态成员
通过 initGlobalAPI() 函数,实现了Vue静态成员的初始化过程
  • Vue.config
  • Vue.util
    • 暴露了util对象,util中的工具方法不视作全局API的一部分,应当避免依赖它们
  • Vue.set()
    • 用来添加响应式属性
  • Vue.delete()
    • 用来删除响应式属性
  • Vue.nextTick()
    • 在下次 DOM 更新循环结束之后执行延迟回调
  • Vue.observable()
    • 用来将对象转换成响应式数据
  • Vue.options
    • Vue.options.components 保存全局组件
      • Vue.options.components.KeepAlive 注册了内置的keep-alive组件到全局Vue.options.components
    • Vue.options.directives 保存全局指令
    • Vue.options.filters 保存全局过滤器
    • Vue.options._base 保存Vue构造函数
  • Vue.use()
    • 用来注册插件
  • Vue.mixin()
    • 用来实现混入
  • Vue.extend(options)
    • 使用基础 Vue 构造器,创建一个子组件,参数是包含选项的对象
  • Vue.component()
    • 用来注册或获取全局组件
  • Vue.directive()
    • 用来注册或获取全局指令
  • Vue.filter()
    • 用来注册或获取全局过滤器
  • Vue.compile()
    • 将一个模板字符串编译成 render 函数
    • 这个静态成员方法是在入口js文件中添加的
// src/core/global-api/index.js 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. // 增加静态成员util对象 // util中的工具方法不视作全局API的一部分,应当避免依赖它们 Vue.util = { warn, extend, mergeOptions, defineReactive } // 增加静态方法 set/delete/nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick // 2.6 explicit observable API // 增加 observable 方法,用来将对象转换成响应式数据 Vue.observable = (obj: T): T => { observe(obj) return obj } // 初始化 options 对象 Vue.options = Object.create(null) // ASSET_TYPES // 'component', // 'directive', // 'filter' // 为 Vue.options 初始化components/directives/filters // 分别保存全局的组件/指令/过滤器 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.options._base = Vue // 注册内置组件 keep-alive 到全局 components extend(Vue.options.components, builtInComponents) // 注册 Vue.use() 用来注册插件 initUse(Vue) // 注册 Vue.mixin() 实现混入 initMixin(Vue) // 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数 initExtend(Vue) // 注册 Vue.component()/Vue.directive()/Vue.filter() initAssetRegisters(Vue) }

实例成员
src/core/instance/index.js 中初始化了绝大部分实例成员属性和方法
  • property
    • vm.$data
    • vm.$props
    • ...
  • 方法 / 数据
    • vm.$watch()
      • $watch() 没有对应的全局静态方法,因为需要用到实例对象vm
        Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, // 可以是函数,也可以是对象 options?: Object ): Function { // 获取 vue 实例 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) if (options.immediate) { // 判断 immediate 选项是 true,则立即执行 cb 回调函数 // 不确定 cb 是否能正常执行,使用 try catch 进行异常处理 try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } // 返回 unwatch 方法 return function unwatchFn () { watcher.teardown() } }

    • vm.$set()
      • 同Vue.set()
    • vm.$delete()
      • 同Vue.delete()
  • 方法 / 事件
    • vm.$on()
      • 监听自定义事件
    • vm.$once()
      • 监听自定义事件,只触发一次
    • vm.$off()
      • 取消自定义事件的监听
    • vm.$emit()
      • 触发自定义事件
  • 方法 / 生命周期
    • vm.$mount()
      • 挂载DOM元素
      • runtime/index.js中添加,在入口js中重写
    • vm.$forceUpdate()
      • 强制重新渲染
    • vm.$nextTick()
      • 将回调延迟到下次 DOM 更新循环之后执行
    • vm.$destory()
      • 完全销毁一个实例
  • 其他
    • vm._init()
      • Vue实例初始化方法
      • 在Vue构造函数中调用了该初始化方法
    • vm._update()
      • 会调用vm._patch_方法更新 DOM 元素
    • vm._render()
      • 会调用用户初始化时选项传入的render函数(或者template转换成的render函数)
    • vm._patch_()
      • 用于把虚拟DOM转换成真实DOM
      • runtime/index.js中添加了该方法
// src/code/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) } // 注册 vm 的 _init() 方法,同时初始化 vm initMixin(Vue) // 注册 vm 的 $data/$props/$set()/$delete()/$watch() stateMixin(Vue) // 注册 vm 事件相关的成员及方法 // $on()/$off()/$once()/$emit() eventsMixin(Vue) // 注册 vm 生命周期相关的成员及方法 // _update()/$forceUpdate()/$destory() lifecycleMixin(Vue) // $nextTick()/_render() renderMixin(Vue) export default Vue

_init()
Vue的构造函数中会调用Vue实例的_init()方法来完成一些实例相关的初始化工作,并触发beforeCreatecreated生命周期钩子函数
// src/core/instance/init.js Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid // 设置实例的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 // 给实例对象添加_isVue属性,避免被转换成响应式对象 vm._isVue = true // merge options // 合并构造函数中的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 // 初始化生命周期相关的属性 // $children/$parent/$root/$refs initLifecycle(vm) // 初始化事件监听,父组件绑定在当前组件上的事件 initEvents(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 // 触发 created 生命周期钩子函数 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) { // 如果提供了挂载的 DOM 元素 el // 调用$mount() 挂载 DOM元素 vm.$mount(vm.$options.el) } }

initState()
初始化 vm 的_props/methods/_data/computed/watch
// src/core/instance/state.js export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options // 将 props 中成员转换成响应式的数据,并注入到 Vue 实例 vm 中 if (opts.props) initProps(vm, opts.props) // 将 methods 中的方法注入到 Vue 的实例 vm 中 // 校验 methods 中的方法名与 props 中的属性是否重名 // 校验 methods 中的方法是否以 _ 或 $ 开头 if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { // 将 data 中的属性转换成响应式的数据,并注入到 Vue 实例 vm 中 // 校验 data 中的属性是否在 props 与 methods 中已经存在 initData(vm) } else { // 如果没有提供 data 则初始化一个响应式的空对象 observe(vm._data = https://www.it610.com/article/{}, true /* asRootData */) } // 初始化 computed if (opts.computed) initComputed(vm, opts.computed) // 初始化 watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }

首次渲染过程
学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理
文章图片
image-20201216233101117 数据响应式原理
Vue的响应式原理是基于观察者模式来实现的
响应式处理入口
Vue构造函数中调用了vm._init()
_init()函数中调用了initState()
initState()函数中如果传入的data有值,则调用initData(),并在最后调用了observe()
observe()函数会创建并返回一个Observer的实例,并将data转换成响应式数据,是响应式处理的入口
// src/core/observer/index.js /** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void 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++ } // 返回 Observer 实例 return ob }

Observer
Observer是响应式处理的核心类,用来对数组或对象做响应式的处理
在它的构造函数中初始化依赖对象,并将传入的对象的所有属性转换成响应式的getter/setter,如果传入的是数组,则会遍历数组的每一个元素,并调用observe() 创建observer实例
/** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */ export class Observer { // 观察对象 value: any; // 依赖对象 dep: Dep; // 实例计数器 vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = https://www.it610.com/article/value this.dep = new Dep() // 初始化实例计数器 this.vmCount = 0 // 使用 Object.defineProperty 将实例挂载到观察对象的 __ob__ 属性 def(value,'__ob__', this) if (Array.isArray(value)) { // 数组的响应式处理 if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } // 为数组每一个对象创建一个 observer 实例 this.observeArray(value) } else { // 如果 value 是一个对象 // 遍历对象所有属性并转换成 getter/setter 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++) { // 转换成 getter/setter defineReactive(obj, keys[i]) } } /** * Observe a list of Array items. */ observeArray (items: Array) { // 遍历数组所有元素,调用 observe for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }

defineReactive
用来定义一个对象的响应式的属性,即使用Object.defineProperty来设置对象属性的 getter/setter
/** * Define a reactive property on an Object. */ export function defineReactive ( // 目标对象 obj: Object, // 目标属性 key: string, // 属性值 val: any, // 自定义 setter 方法 customSetter?: ?Function, // 是否深度观察 // 为 false 时如果 val 是对象,也将转换成响应式 shallow?: boolean ) { // 创建依赖对象实例,用于收集依赖 const dep = new Dep() // 获取目标对象 obj 的目标属性 key 的属性描述符对象 const property = Object.getOwnPropertyDescriptor(obj, key) // 如果属性 key 存在,且属性描述符 configurable === false // 则该属性无法通过 Object.defineProperty来重新定义 if (property && property.configurable === false) { return } // cater for pre-defined getter/setters // 获取用于预定义的 getter 与 setter const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { // 如果调用时只传入了2个参数(即没传入val),且没有预定义的getter,则直接通过 obj[key] 获取 val val = obj[key] } // 判断是否深度观察,并将子对象属性全部转换成 getter/setter,返回子观察对象 let childOb = !shallow && observe(val) // 使用 Object.defineProperty 定义 obj 对象 key 属性的 getter 与 setter Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { // 如果存在预定义的 getter 则 value 等于 getter 调用的返回值 // 否则 value 赋值为属性值 val const value = https://www.it610.com/article/getter ? getter.call(obj) : val if (Dep.target) { // 当前存在依赖目标则建立依赖 dep.depend() if (childOb) { // 如果子观察目标存在,则建立子依赖 childOb.dep.depend() if (Array.isArray(value)) { // 如果属性是数组,则处理数组元素的依赖收集 // 调用数组元素 e.__ob__.dep.depend() dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { // 如果存在预定义的 getter 则 value 等于 getter 调用的返回值 // 否则 value 赋值为属性值 val const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ // 判断新值旧值是否相等 // (newVal !== newVal && value !== value) 是对 NaN 这个特殊值的判断处理(NaN !== NaN) if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !=='production' && customSetter) { customSetter() } // #7981: for accessor properties without setter // 有预定义 getter 但没有 setter 直接返回 if (getter && !setter) return if (setter) { // 有预定义 setter 则调用 setter setter.call(obj, newVal) } else { // 否则直接更新新值 val = newVal } // 判断是否深度观察,并将新赋值的子对象属性全部转换成 getter/setter,返回子观察对象 childOb = !shallow && observe(newVal) // 触发依赖对象的 notify() 派发通知所有依赖更新 dep.notify() } }) }

依赖收集
依赖收集由Dep对象来完成
每个需要收集依赖的对象属性,都会创建一个相应的dep实例,并收集watchers保存到其subs数组中
对象响应式属性的依赖收集,主要是getter中的这部分代码
if (Dep.target) { // 当前存在依赖目标则建立依赖 dep.depend() if (childOb) { // 如果子观察目标存在,则建立子依赖 childOb.dep.depend() if (Array.isArray(value)) { // 如果属性是数组,则处理数组元素的依赖收集 // 调用数组元素 e.__ob__.dep.depend() dependArray(value) } } }

这里有两个问题
  • Dep.target 是何时赋值的?
    在mountComponent()调用时,Watcher被实例化
    Watcher构造函数中调用了实例方法get(),并通过pushTarget() 将Watcher实例赋值给Dep.target
  • dep.depend() 的依赖收集进行了什么操作?
    dep.depend()会调用Dep.target.addDep()方法,并调用dep.addSub()方法,将Watcher实例添加到观察者列表subs中
    Watcher中会维护dep数组与dep.id集合,当调用addDep()方法时,会先判断dep.id是否已经在集合中,从而避免重复收集依赖
数组
数组的成员无法像对象属性一样通过Object.defineProperty()去设置 getter/setter 来监视变化,因此数组的响应式需要进行特殊的处理,通过对一系列会影响数组成员数量的原型方法进行修补,添加依赖收集与更新派发,来完成响应式处理
影响数组的待修补方法arrayMethods
  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse
// Observer 构造函数中的处理 if (Array.isArray(value)) { // 数组的响应式处理 if (hasProto) { // 如果支持原型, 替换原型指向 __prototype__ 为修补后的方法 protoAugment(value, arrayMethods) } else { // 如果不支持原型,通过 Object.defineProperty 为数组重新定义修补后的方法 copyAugment(value, arrayMethods, arrayKeys) } // 为数组每一个对象创建一个 observer 实例 this.observeArray(value) }

// src/core/observer/array.js 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) { // 先执行数组原型上原始的处理函数并将结果保存到 result 中 const result = original.apply(this, args) const ob = this.__ob__ // 是否新增 let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 如果新增, 调用observe()将新增的成员转化成响应式 if (inserted) ob.observeArray(inserted) // notify change // 调用依赖的 notify() 方法派发更新,通知观察者 Watcher 执行相应的更新操作 ob.dep.notify() // 返回结果 return result }) })

通过查看数组响应式处理的源码我们可以发现,除了通过修补过的七个原型方法来修改数组内容外,其他方式修改数组将不能触发响应式更新,例如通过数组下标来修改数组成员array[0] = xxx,或者修改数组长度array.length = 1
Watcher
Vue中的Watcher有三种
  • Computed Watcher
    • Computed Watcher是在Vue构造函数初始化调用_init() -> initState() -> initComputed() 中创建的
  • 用户Watcher(侦听器)
    • 用户Watcher是在Vue构造函数初始化调用_init() -> initState() -> initWatch() 中创建的(晚于Computed Watcher)
  • 渲染Watcher
    • 渲染Watcher是在Vue初始化调用_init() -> vm.$mount() -> mountComponent()的时候创建的(晚于用户Watcher)
      // src/core/instance/lifecycle.js// 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 // 渲染 Watcher 的创建 // updateComponent 方法用于调用 render 函数并最终通过 patch 更新 DOM // isRenderWatcher 标记参数为 true new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)

  • Watcher的实现
    /** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array; newDeps: Array; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { // 如果是渲染 watcher,记录到 vm._watcher vm._watcher = this } // 记录 watcher 实例到 vm._watchers 数组中 vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { // 渲染 watcher 不传 options this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers // watcher 相关 dep 依赖对象 this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter // expOrFn 的值是函数或字符串 if (typeof expOrFn === 'function') { // 是函数时,直接赋给 getter this.getter = expOrFn } else { // 是字符串时,是侦听器中监听的属性名,例如 watch: { 'person.name': function() {}} // parsePath('person.name') 返回一个获取 person.name 的值的函数 this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop 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 ) } } // 渲染 watcher 的 lazy 是 false, 会立即执行 get() // 计算属性 watcher 的lazy 是 true,在 render 时才会获取值 this.value = https://www.it610.com/article/this.lazy ? undefined : this.get() }/** * Evaluate the getter, and re-collect dependencies. */ get () { // 组件 watcher 入栈 // 用于处理父子组件嵌套的情况 pushTarget(this) let value const vm = this.vm try { // 执行传入的 expOrFn // 渲染 Watcher 传入的是 updateComponent 函数,会调用 render 函数并最终通过 patch 更新 DOM value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher"${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } // 组件 watcher 实例出栈 popTarget() // 清空依赖对象相关的内容 this.cleanupDeps() } return value } ... }

总结
响应式处理过程总结
学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理
文章图片
image-20201228185240923
  • 整个响应式处理过程是从Vue初始化_init()开始的
    • initState() 初始化vue实例的状态,并调用initData()初始化data属性
    • initData() 将data属性注入vm实例,并调用observe()方法将data中的属性转换成响应式的
    • observe() 是响应式处理的入口
  • observe(value)
    • 判断value是否是对象,如果不是对象直接返回
    • 判断value对象是否有__ob__属性,如果有直接返回(认为已进行过响应式转换)
    • 创建并返回observer对象
    学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理
    文章图片
    image-20201228185835411
  • Observer
    • 为value对象(通过Object.defineProperty)定义不可枚举的__ob__属性,用来记录当前的observer对象
    • 区分是数组还是对象,并进行相应的响应式处理
    学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理
    文章图片
    image-20201228185941768
  • defineReactive
    • 为每一个对象属性创建dep对象来收集依赖
    • 如果当前属性值是对象,调用observe将其转换成响应式
    • 为对象属性定义getter与setter
    • getter
      • 通过dep收集依赖
      • 返回属性值
    • setter
      • 保存新值
      • 调用observe()将新值转换成响应式
      • 调用dep.notify()派发更新通知给watcher,调用update()更新内容到DOM
    学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理
    文章图片
    image-20201228190416199
  • 依赖收集
    • 在watcher对象的get()方法中调用pushTarget
      • 将Dep.target赋值为当前watcher实例
      • 将watcher实例入栈,用来处理父子组件嵌套的情况
    • 访问data中的成员的时候,即defineReactive为属性定义的getter中收集依赖
    • 将属性对应的watcher添加到dep的subs数组中
    • 如果有子观察对象childOb,给子观察对象收集依赖
    学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理
    文章图片
    image-20201228190600536
  • Watcher
    • 数据触发响应式更新时,dep.notify()派发更新调用watcher的update()方法
    • queueWatcher()判断watcher是否被处理,如果没有的话添加queue队列中,并调用flushSchedulerQueue()
    • flushSchedulerQueue()
      • 触发beforeUpdate钩子函数
      • 调用watcher.run()
        • run() -> get() -> getter() -> updateComponent()
      • 清空上一次的依赖
      • 触发actived钩子函数
      • 触发updated钩子函数
    学习笔记(十五)Vue.js源码剖析|学习笔记(十五)Vue.js源码剖析 - 响应式原理
    文章图片
    QQ截图20201228205942
全局API
为一个响应式对象动态添加一个属性,该属性是否是响应式的?不是
为一个响应式对象动态添加一个响应式属性,可以使用Vue.set()vm.$set()来实现
Vue.set()
用于向一个响应式对象添加一个属性,并确保这个新属性也是响应式的,且触发视图更新
注意:对象不能是Vue实例vm,或者Vue实例的根数据对象vm.$data
  • 示例
    // object Vue.set(object, 'name', 'hello') // 或 vm.$set(object, 'name', 'hello')// array Vue.set(array, 0, 'world') // 或 vm.$set(array, 0, 'world')

  • 定义位置
    core/global-api/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 | 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 方法已经在Vue初始化时中完成了响应式补丁处理 (array.js) target.splice(key, 1, val) return val } // 如果 key 在目标对象 target 中存在,且不是原型上的成员,则直接赋值(已经是响应式的) if (key in target && !(key in Object.prototype)) { target[key] = val return val } // 获取目标对象 target 的 __ob__ 属性 const ob = (target: any).__ob__ // 判断 target 是否是 Vue 实例,或者是否是 $data (vmCount === 1) 并抛出异常 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 } // 判断 target 是否为响应式对象 (ob是否存在) // 如果是普通对象则不做响应式处理直接返回 if (!ob) { target[key] = val return val } // 调用 defineReactive 为目标对象添加响应式属性 key 值为 val defineReactive(ob.value, key, val) // 发送通知更新视图 ob.dep.notify() return val }

Vue.delete()
用于删除对象的属性,如果对象是响应式的,确保删除能触发视图更新
主要用于避开Vue不能检测到属性被删除的限制,但是很少会使用到
注意:对象不能是Vue实例vm,或者Vue实例的根数据对象vm.$data
  • 示例
    Vue.delete(object, 'name') // 或 vm.$delete(object, 'name')

  • 定义位置
    core/global-api/index.js
  • 源码解析
    /** * Delete a property and trigger change if necessary. */ export function del (target: Array | 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)}`) } // 判断目标对象 target 是否是数组,且参数 key 是否是合法的数组索引 if (Array.isArray(target) && isValidArrayIndex(key)) { // 通过 splice 删除 key 位置的元素 // 数组的 splice 方法已经在Vue初始化时中完成了响应式补丁处理 (array.js) target.splice(key, 1) return } // 获取目标对象 target 的 __ob__ 属性 const ob = (target: any).__ob__ // 判断 target 是否是 Vue 实例,或者是否是 $data (vmCount === 1) 并抛出异常 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 // 如果不包含则直接返回 if (!hasOwn(target, key)) { return } // 删除目标对象 target 的属性 key delete target[key] // 判断 target 是否为响应式对象 (ob是否存在) // 如果是普通对象则直接返回 if (!ob) { return } // 发送通知更新视图 ob.dep.notify() }

Vue.nextTick()
Vue更新DOM是批量异步执行的,当通过响应式方式触发DOM更新但没有完成时,无法立即获取更新后的DOM
在修改数据后立即使用nextTick()方法可以在下次DOM更新循环结束后,执行延迟回调,从而获得更新后的DOM
  • 示例
    Vue.nextTick(function(){}) // 或 vm.$nextTick(function(){})

  • 定义位置
    • 实例方法
      core/instance/render.js -> core/util/next-tick.js
    • 静态方法
      core/global-api/index.js -> core/util/next-tick.js
  • 源码解析
    export function nextTick (cb?: Function, ctx?: Object) { // 声明 _resolve 用来保存 cb 未定义时返回新创建的 Promise 的 resolve let _resolve // 将回调函数 cb 加上 try catch 异常处理存入 callbacks 数组 callbacks.push(() => { if (cb) { // 如果 cb 有定义,则执行回调 try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { // 如果 _resolve 有定义,执行_resolve _resolve(ctx) } }) if (!pending) { pending = true // nextTick() 的核心 // 尝试在本次事件循环之后执行 flushCallbacks // 如果支持 Promise 则优先尝试使用 Promsie.then() 的方式执行微任务 // 否则非IE浏览器环境判断是否支持 MutationObserver 并使用 MutationObserver 来执行微任务 // 尝试使用 setImmediate 来执行宏任务(仅IE浏览器支持,但性能好于 setTimeout) // 最后尝试使用 setTimeout 来执行宏任务 timerFunc() } // $flow-disable-line // cb 未定义且支持 Promise 则返回一个新的 Promise,并将 resolve 保存到 _resolve 备用 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }

    // timerFunc()// 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 = https://www.it610.com/article/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) } }

    推荐阅读