阅读 49

Vue源码解析

1. Vue整体架构与源码调试

1.1 源码目录结构

src

├─compiler 编译相关
├─core Vue 核心库
├─platforms 平台相关代码
├─server SSR,服务端渲染
├─sfc .vue 文件编译为 js 对象
└─shared 公共的代码

1.2 Vue调试

  • 打包工具 Rollup
    • Vue.js 源码的打包工具使用的是 Rollup,比 Webpack 轻量
    • Webpack 把所有文件当做模块,Rollup 只处理 js 文件更适合在 Vue.js 这样的库中使用
    • Rollup 打包不会生成冗余的代码
  • 安装依赖
npm i
复制代码
  • 设置 sourcemap
    • package.json 文件中的 dev 脚本中添加参数 --sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
复制代码
  • 执行 dev
    • npm run dev 执行打包,用的是 rollup,-w 参数是监听文件的变化,文件变化自动重新打包
  • 调试
    • examples 的示例中引入的 vue.min.js 改为 vue.js
    • 打开 Chrome 的调试工具中的 source

1.3 不同版本构建

官方不同构建版本解释

  • 完整版:同时包含编译器和运行时的版本。

  • 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大、效率低。

  • 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码,体积小、效率高。基本上就是除去编译器的代码。

  • UMD:UMD 版本通用的模块版本,支持多种模块方式。 vue.js 默认文件就是运行时 + 编译器的UMD 版本

  • CommonJS(cjs):CommonJS 版本用来配合老的打包工具比如 Browserifywebpack 1

  • ES Module:从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件,为现代打包工具提供的版本。

    • ESM 格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包。
    • ES6 模块与 CommonJS 模块的差异

如何选用:

  • 推荐使用运行时版本,因为运行时版本相比完整版体积要小大约 30%
  • 基于 Vue-CLI 创建的项目默认使用的是 vue.runtime.esm.js
    • 通过查看 webpack 的配置文件
vue inspect > output.js
复制代码

注意*.vue 文件中的模板是在构建时预编译的,最终打包后的结果不需要编译器,只需要运行时版本即可

2. Vue初始化过程

2.1 入口文件

我们首先来看下执行构建的命令

npm run dev # "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev" 
# --environment TARGET:web-full-dev 设置环境变量 TARGET
复制代码

所以我们来看下script/config.js 的执行过程

// 判断环境变量是否有 TARGET
// 如果有的话 使用 genConfig() 生成 rollup 配置文件 
if (process.env.TARGET) { 
  module.exports = genConfig(process.env.TARGET) 
} else { 
  // 否则获取全部配置 
  exports.getBuild = genConfig 
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig) 
}
复制代码
  • 作用:生成 rollup 构建的配置文件

  • 使用环境变量 TARGET = web-full-dev

  • 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 
}
复制代码

由上代码得出如下结论:

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

2.2 Vue导出的四个模块

  • src/platforms/web/entry-runtime-with-compiler.js
    • web 平台相关的入口
    • 重写了平台相关的 $mount() 方法
    • 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
  • src/platforms/web/runtime/index.js
    • web 平台相关
    • 注册和平台相关的全局指令:v-model、v-show
    • 注册和平台相关的全局组件: v-transition、v-transition-group
    • 全局方法:
      • __patch__:把虚拟 DOM 转换成真实 DOM
      • $mount:挂载方法
  • src/core/index.js
    • 与平台无关
    • 设置了 Vue 的静态方法,initGlobalAPI(Vue)
  • src/core/instance/index.js
    • 与平台无关
    • 定义了构造函数,调用了 this._init(options) 方法
    • 给 Vue 中混入了常用的实例成员

接下来我们以以下代码为例,来具体分析下vue执行过程

const vm = new Vue({ 
  el: '#app', 
  template: '<h3>Hello template</h3>', 
  render (h) { 
  	return h('h4', 'Hello render') 
  } 
})
复制代码

2.3 vue初始化过程

我们首先来分析下src/platforms/web/entry-runtime-with-compiler.js

  • src/platforms/web/entry-runtime-with-compiler.js
    • web 平台相关的入口
    • 重写了平台相关的 $mount() 方法
    • 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
// 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
// resolve template/el and convert to render function
// 把 template/el 转换成 render 函数
if (!options.render) {
  // 把 template/el 转换成 render 函数
  let template = options.template
}
// 调用 mount 方法,渲染 DOM
  return mount.call(this, el, hydrating)
复制代码

由以上代码我们可以得出如下结论:

    1. el 不能是 body 或者 html 标签
    1. 如果没有 render,把 template 转换成 render 函数
    1. 如果有 render 方法,直接调用 mount 挂载 DOM

2.4 Vue构造函数在哪

我们来分析下以下问题:

Vue 的构造函数在哪?
Vue 实例的成员/Vue 的静态成员从哪里来的?

我们通过浏览器来调试下代码 我们在$mount方法中打一个断点,查看右边的调用堆栈,我们就可以找到Vue构造函数在哪里

  • src/platforms/web/runtime/index.js
    • web 平台相关
    • 注册和平台相关的全局指令:v-model、v-show
    • 注册和平台相关的全局组件: v-transition、v-transition-group
    • 全局方法:
      • __patch__:把虚拟 DOM 转换成真实 DOM
      • $mount:挂载方法
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
复制代码

2.5 静态方法

  • src/core/index.js
    • 与平台无关
    • 设置了 Vue 的静态方法,initGlobalAPI(Vue)
// src/core/index.js
// 挂载vue的api,如filter/directives/set/nextTick/observable/keep-alive等等
initGlobalAPI(Vue)
复制代码
// src/core/global-api/index.js
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick

// components/directives/filters
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})

// 设置 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)
复制代码

这里说一下我们会用到的Vue.extend()方法

// Vue 构造函数
const Super = this

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
)

// 把Super中的成员拷贝到Sub上
// ...
return Sub
复制代码

由代码可知,Vue.extend()方法返回了一个组件的构造函数

2.6 成员实例化

  • src/core/instance/index.js
    • 与平台无关
    • 定义了构造函数,调用了 this._init(options) 方法
    • 给 Vue 中混入了常用的实例成员
      • initMixin(Vue) 注册 vm 的 _init() 方法,初始化 vm
      • stateMixin(Vue) 注册 vm 的 $data/$props/$set/$delete/$watch
      • eventsMixin(Vue) $on/$once/$off/$emit(发布订阅模式)
      • lifecycleMixin(Vue) 初始化生命周期相关的混入方法_update(将Vnode转为真实DOM)/$forceUpdate/$destroy
      • renderMixin(Vue) 混入render $nextTick/_render
// 此处不用 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)
复制代码
  • initMixin
    • 将用户传入的 options 与 Vue的 $options 合并
    • 设置渲染时的代理对象
    • initLifecycle(vm) $children/$parent/$root/$refs
    • initEvents(vm) vm 的事件监听初始化, 父组件绑定在当前组件上的事件
    • initRender(vm) vm 的编译render初始化 $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    • callHook(vm, 'beforeCreate')
    • initInjections(vm) 把 inject 的成员注入到 vm 上
    • initState(vm) 初始化 vm 的 _props/methods/_data/computed/watch,注册到vue实例,转为响应式
    • initProvide(vm)
    • callHook(vm, 'created')
// vm即Vue
const vm: Component = this
// a uid 唯一标识
vm._uid = uid++
// 如果是 Vue 实例不需要被 observe
vm._isVue = true
// 将用户传入的 options 与 Vue的 $options 合并
if (options && options._isComponent) { // 如果是组件
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}
// 设置渲染时的代理对象
if (process.env.NODE_ENV !== 'production') {
  // 该方法判断是否有Proxy,如果有,用Proxy,否则使用vm._renderProxy = vm
  initProxy(vm)
} else {
  vm._renderProxy = 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,注册到vue实例,转为响应式
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// created 生命钩子的回调
callHook(vm, 'created')
复制代码

stateMixin:

  • 不允许给"$data"和"$props"赋值
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
// 不允许给"$data"和"$props"赋值
if (process.env.NODE_ENV !== 'production') {
  dataDef.set = function () {
    warn(
      'Avoid replacing instance root $data. ' +
      'Use nested data properties instead.',
      this
    )
  }
  propsDef.set = function () {
    warn(`$props is readonly.`, this)
  }
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)

// 不建议使用 _ or $ 开头
if ((key in vm) && isReserved(key)) {
  warn(
    `Method "${key}" conflicts with an existing Vue instance method. ` +
    `Avoid defining component methods that start with _ or $.`
  )
}
复制代码

2.7 初始化过程调试

最后,我们来调试一遍初始化过程:

    1. instance/index.js,注册实例成员
    • initMixin(Vue) _init
    • stateMixin(Vue) $data/$props/$set/$delete/$watch
    • eventsMixin(Vue) $on/$once/$off/$emit
    • lifecycleMixin(Vue) _update(将Vnode转为真实DOM)/$forceUpdate/$destroy
    • renderMixin(Vue) $nextTick/_render
    1. src/core/index.js
    • initGlobalAPI(Vue)
      • config
      • components/filter/directives/set/nextTick/observable/keep-alive等等
    1. src/platforms/web/runtime/index.js
    • web 平台相关
    • 注册和平台相关的全局指令:v-model、v-show
    • 注册和平台相关的全局组件: v-transition、v-transition-group
    • 全局方法:
      • patch:把虚拟 DOM 转换成真实 DOM
      • $mount:挂载方法
    1. src/platforms/web/entry-runtime-with-compiler.js
    • 重写平台相关的 $mount() 方法
    • 注册 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数

3. 首次渲染过程

  1. 首次渲染过程从instance/index.js中调用this._init(),我们来分析下它都做了什么
// 调用$mount
if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
复制代码
  1. src/platforms/web/entry-runtime-with-compiler.js中重写了$mount方法
// 如果没有render函数,就编译模版
if (!options.render) {
  // ... 生成template
  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
  }
}
复制代码
  1. src/platforms/web/runtime/index.js中定义了$mount方法
return mountComponent(this, el, hydrating)
复制代码
  1. src/core/instance/lifecycle.js中定义了mountComponent方法
callHook(vm, 'beforeMount')
let updateComponent
// 更新组件
// vm._render() 调用用户传入的render或生成的render,生成虚拟dom
// vm._update() 将虚拟dom转为真实dom
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

// 在new Wtacher()中执行了 updateComponent
new Watcher(vm, updateComponent, noop, {...})

if (vm.$vnode == null) {
  vm._isMounted = true
  callHook(vm, 'mounted')
}
复制代码

总结:

    1. instance/index.js
    • this._init()
      • vm.$mount(vm.$options.el)
    1. src/platforms/web/entry-runtime-with-compiler.js
    • 重写$mount
      • 如果没有render函数,就编译模版
      • 把 template 转换成 render 函数
    1. src/platforms/web/runtime/index.js
    • 定义$mount
      • 重新获取el 因为运行时版本不会执行src/platforms/web/entry-runtime-with-compiler.js文件
      • return mountComponent(this, el, hydrating)
    1. src/core/instance/lifecycle.js
    • beforeMount
    • 定义更新组件方法updateComponent
      • vm._render() 调用用户传入的render或生成的render,生成虚拟dom
      • vm._update() 将虚拟dom转为真实dom
    • new Watcher()中watcher.get()执行组件方法updateComponent
    • mounted

4. MVVM

Vue MVVM源码解析

5. 实例API

5.1 watch

  • 没有静态方法,因为 $watch 方法中要使用 Vue 的实例
  • Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
  • 创建顺序:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
  • vm.$watch 返回一个取消观察函数,用来停止触发回调∶
var unwatch= vm.Swatch('a',(newwal,oldVal)=>{)
//之后取消观察
unwatch()
复制代码

1. 计算属性watcher

我们以fullname为例,来分析下计算属性watcher

var vm = new Vue({
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})
复制代码
  1. 初始化这个 computed watcher 实例

这里 computed watcher 会并不会立刻求值,同时持有一个 dep 实例

constructor (
  vm: Component,
  expOrFn: string | Function,
  cb: Function,
  options?: ?Object,
  isRenderWatcher?: boolean
) {
  // ...
  if (this.computed) {
    this.value = undefined
    this.dep = new Dep()
  } else {
    this.value = this.get()
  }
}  
复制代码
  1. 拿到计算属性对应的 watcher,执行 watcher.depend()

render 函数执行访问到 this.fullName 的时候,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend()
这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化

depend () {
  if (this.dep && Dep.target) {
    this.dep.depend()
  }
}
复制代码
  1. 执行 watcher.evaluate() 求值
evaluate () {
  if (this.dirty) {
    this.value = this.get()
    this.dirty = false
  }
  return this.value
}
复制代码

evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get() 求值,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我们这个例子就是执行了 return this.firstName + ' ' + this.lastName

特别注意:由于 this.firstNamethis.lastName 都是响应式对象,这里会触发它们的 getter,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher

最后通过 return this.value 拿到计算属性对应的值。

  1. 计算属性依赖数据更改

那么对于计算属性这样的 computed watcher,它实际上是有 2 种模式,lazyactive。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行

一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法

if (this.computed) {
  // A computed property watcher has two modes: lazy and activated.
  // 默认情况下,它初始化为惰性,只有在至少一个订阅服务器依赖它时才会激活,
  // 订阅服务器通常是另一个计算属性或组件的呈现函数
  if (this.dep.subs.length === 0) {
    // 在惰性模式下,除非有必要,否则我们不希望执行计算,因此我们只需将观察者标记为dirty。	   // 实际的计算是在访问computed属性时即时在this.evaluate()中执行的
    this.dirty = true
  } else {
    // In activated mode, we want to proactively perform the computation
    // but only notify our subscribers when the value has indeed changed.
    this.getAndInvoke(() => {
      this.dep.notify()
    })
  }
} else if (this.sync) {
  this.run()
} else {
  queueWatcher(this)
}
复制代码
  1. 重新计算

getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。

this.getAndInvoke(() => {
  this.dep.notify()
})

getAndInvoke (cb: Function) {
  const value = this.get()
  if (
    value !== this.value ||
    // 深度观察者和对象/数组上的观察者即使值相同也应该被触发,因为值可能发生了变化
    isObject(value) ||
    this.deep
  ) {
    // set new value
    const oldValue = this.value
    this.value = value
    this.dirty = false
    if (this.user) {
      try {
        cb.call(this.vm, value, oldValue)
      } catch (e) {
        handleError(e, this.vm, `callback for watcher "${this.expression}"`)
      }
    } else {
      cb.call(this.vm, value, oldValue)
    }
  }
}
复制代码

计算属性依赖的值发生变化/计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化

2. vm.$watch()

src\core\instance\state.js

  • 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()
  }
}
复制代码

3. immediate实现

Vue.prototype.$watch = function(expOrFn, cb, options){
  const vm = this 
  options = options || {}
  const watcher = new Matcher(vm, expOrFn, cb, options)
  if(options.immediate){
  	cb.call(vm, watcher.value)
  }
  return function unwatchFn(){
  	watcher.teardown()
  }
}
复制代码

执行new watcher后,代码会判断用户是否使用了immediate参数,如果使用了,则立即执行一次 cb。最后,返回一个函数 unwatchFn。它的作用是取消观察数据。
当用户执行这个函数时,实际上是执行了watcher.teardown()来取消观察数据,其本质是把 watcher 实例从当前正在观察的状态的依赖列表中移除。

teardown(){
  let i = this.deps.length 
  while(i--){
	this.deps[i].removeSub(this)
  }
}

removeSub(sub){
  const index = this.subs.indexOf(sub)
  if(index > -1){
  	return this.subs.splice(index,1)
  }
}
复制代码

4. expOrFn支持函数

export default class Watcher{
  constructor (vm,expOrFn, cb) {
    this.vm = vm
    // expOrFn参数支持画数
    if(typeof expOrFn === 'function'){
    this.getter = expOrFn
  } else {
    this.getter = parsePath(expOrFn)
    this.c b=cb
  	this.value = this.get()
  }
  ...
}

复制代码

当expOrFn是函数时,会发生很神奇的事情。它不只可以动态返回数据,其中读取的所有数据也都会被watcher观察。当expOrFn是字符串类型的keypath时,watcher会读取这个keypath所指向的数据并观察这两个数据的变化

5. deep实现

export default class watcher{
  constructor(vm,expOrFn,cb, options){
    // 新增
    if(options){
      this.deep =!loptions.deep
    }else{
      this.deep =false 
    }
    this.deps=[]
    this.depIds= new Set()
    this.getter = parsePath(expOrFn)
    this.cb= cb
    this.value= this.get()
  }
  get(){
    window.target= this
    let value = this.getter.call(vm, vm)
    // 新增
    if(this.deep){
      traverse(value)
    }
    window.target = undefined 
    return value
  }
  ...
}
复制代码

在如果用户使用了deep 参数,则在 window.target =undefined 之前调用 traverse来处理 deep的逻辑。 这里非常强调的一点是,一定要在window.target =undefined之前去触发子值的收集依赖逻辑,这样才能保证子集收集的依赖是当前这个watcher。如果在window.target=undefined 之后去触发收集依赖的逻辑,那么其实当前的watcher并不会被收集到子值的依赖列表中,也就无法实现 deep的功能。

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}
复制代码

这里我们先判断 val 的类型,如果它不是Array和object,或者已经被冻结,那么直接返回,什么都不干。
然后拿到 val 的dep.id,用这个id来保证不会重复收集依赖。
如果是数组,则循环数组,将数组中的每一项递归调用_traverse
如果是Object类型的数据,则循环Object中的所有key,然后执行一次读取操作,再递归子值∶
wihle(l--)_traverse(val[keys[1],seen)
其中val[keys[i]]会触发 getr,也就是说会触发收集依赖的操作,这时window.target 还没有被清空,会将当前的 watcher收集进去。这也是前面我强调的一定要在window.target=undefined 这个语句之前触发收集依赖的原因。 而_traverse 函数其实是一个递归操作,所以这个value的子值也会触发同样的逻辑,这样就可以实现通过 deep 参数来监听所有子值的变化。

6. computed与watch的区别

计算属性computed更多是作为缓存功能的观察者,它可以将一个或者多个data的属性进行复杂的计算生成一个新的值,提供给渲染函数使用,当依赖的属性变化时,computed不会立即重新计算生成新的值,而是先标记为脏数据,当下次computed被获取时候,才会进行重新计算并返回。
而监听器watch并不具备缓存性,监听器watch提供一个监听函数,当监听的属性发生变化时,会立即执行该函数

5.2 set

vm.$set的具体实现其实是在 observer中抛出的set方法。

数组处理

export function set(target, key, val){
  if(Array.isArray(target) && isValidArrayIndex(key)){
    target.length = Math.max(target.length,key)
    target.splice(key,1,val)
    return val
  }
}
复制代码

如果 target是数组并且 key是一个有效的索引值,就先设置length 属性。这样如果我们传递的索引值大于当前数组的length,就需要让target的length等于索引值。
接下来,通过 splice方法把val设置到target中的指定位置(参数中提供的索引值的位置)。当我们使用splice方法把val设置到target中的时候,数组拦截器会侦测到target发生了变化,并且会自动帮助我们把这个新增的 val转换成响应式的。最后,返回 val即可。

export function set(target, key, val){
  if(Array.isArray(target) && isValidArrayIndex(key)){
    target.length = Math.max(target.length,key)
    target.splice(key,1,val)
    return val
  }
  // 新增
  if(key in target && !(key in Oobject.prototype)){
    target[key]= val 
    return val 
  }
}
复制代码

如果key已经存在于target中,所以其实这个key已经被侦测了变化。也就是说,这种情况属于修改数据,直接用key和val改数据就好了。修改数据的动作会被Vuejs侦测到,所以数据发生变化后,会自动向依赖发送通知。

export function set(target, key, val){
  if(Array.isArray(target) && isValidArrayIndex(key)){
    target.length = Math.max(target.length,key)
    target.splice(key,1,val)
    return val
  }
  // 新增
  if(key in target && !(key in Oobject.prototype)){
    target[key]= val 
    return val 
  }
  // 新增
  const ob= target.__o__
  if(target._isVue || (ob 8& ob.vmCount)){
  	return val 
  }
  if(!ob){
  	target[key]= val return val
  }
  defineReactive(ob.value,key,val)
  ob.dep.notify()
  return val
}
复制代码

在上面的代码中,我们最先做的事情是获取 target的__ob__属性。
然后要处理文档中所说的"target不能是Vuejs实例或Vuejs实例的根数据对象"的情况。实现这个功能并不难,只需要使用target.isVue来判断 target是不是Vuejs实例,使用ob.vmcount来判断它是不是根数据对象即可。
接下来,我们处理target不是响应式的情况。如果target身上没有ob_属性,说明它并不是响应式的,并不需要做什么特殊处理,只需要通过 key和val在target上设置就行了。
如果前面的所有判断条件都不满足,那么说明用户是在响应式数据上新增了一个属性,这种情况下需要追踪这个新增属性的变化,即使用 defineReactive将新增属性转换成 gettr/stter 的形式即可。
最后,向 target的依赖触发变化通知,并返回val。

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 进行了响应化的处理
    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 直接返回
  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
}
复制代码

5.3 delete

export function del(target, key){
  if(Array.isArray(target) && isValidArrayIndex(key)){
    target.splice(key,1)
    return
  }
  const o b= target.ob__
  if(target.isVue || (ob 88 ob.vmCount)){return}
  if(!hasOwn(target, key)){
      return
  }
  delete target[key]
  //如果ob不存在,则直接终止程序
  if(!ob){
  	return ob.dep.notify()
  }
}
复制代码

5.4 nextTick

定义位置:src\core\instance\render.js

  • 手动调用 vm.$nextTick()
  • 在 Watcher 的 queueWatcher 中执行 nextTick()
  • src\core\util\next-tick.js
export let isUsingMicroTask = false
// callbacks存放所有的回调函数 也就是dom更新之后我们希望执行的回调函数
const callbacks = []
// pending可以理解为上锁 也可以理解为挂起 这里的意思是不上锁
let pending = false

// 会执行所有的回调函数
function flushCallbacks () {
  pending = false
  // 之所以要slice复制一份出来是因为有的cb执行过程中又会往callbacks中加入内容
  // 比如$nextTick的回调函数里又有$nextTick
  // 这些是应该放入到下一个轮次的nextTick去执行的,
  // 所以拷贝一份当前的,遍历执行完当前的即可,避免无休止的执行下去
  const copies = callbacks.slice(0)
  // 清空回调函数 因为全部都拿出来执行了
  callbacks.length = 0
  // 执行所有的回调函数
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

// 使用哪种异步函数去执行:MutationObserver,setImmediate还是setTimeout
let timerFunc

const p = Promise.resolve()
timerFunc = () => {
  p.then(flushCallbacks)
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 把 cb 加上异常处理存入 callbacks 数组中,等待dom更新之后执行
  callbacks.push(() => {
    if (cb) {
      try {
        // 调用 cb()
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 如果pending为true,就表明本轮事件循环中已经执行过timerFunc了
  if (!pending) {
    // 上锁
    pending = true
    // 执行异步函数,在异步函数中执行所有的回调
    timerFunc()
  }
 
  if (!cb && typeof Promise !== 'undefined') {
    // 返回 promise 对象
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
复制代码

首先,当我们第一次在自己的代码中调用nextTick,就执行了把回调函数推入callbacks,然后调用异步函数timerFunc的过程,这时候设置了pending = true,也就是已经上锁了。所以后面我们再调用nextTick,都是执行到callbacks.push(func),把异步函数推到callbacks里面就停止了,不再调用异步函数了。毕竟异步函数等到执行时机一到,就会把callbacks里面的函数全部执行完毕,所以没有必要调用多次。
pending一打开(false),就像是在说:我把timerFunc放入异步队列啦!你们赶紧把回调函数放进来给他到时候执行!pending一锁上(true),就表示来了来了!正在放入回调函数!最后timerFunc在微任务中一执行,就把所有回调函数都执行了。
等到执行完所有的回调函数了,又要把pending给打开,如果不打开的话他就一直锁着,一直傻傻的在存回调函数,那你都没把nextTickHandler再放入异步队列,给他存这么多回调有啥用嘛。
所以说最后打开pending是为了让我们在宏任务(如setTimeout)中调用nextTick的时候能顺利调用到timerFunc,才能够执行回调函数。
nextTick的主要思路就是:我们有可能会在同步任务中多次改变DOM。那么在所有同步任务执行完毕之后,就说明数据修改已经结束了,改变DOM的函数我都执行过了,已经得到了最终要渲染的DOM数据,所以这个时候可放心更新DOM了。因此nextTick的回调函数都是在microtask中执行的。这样就可以尽量避免重复的修改渲染某个DOM元素,另一方面也能够将DOM操作聚集,减少渲染的次数,提升DOM渲染效率。等到所有的微任务都被执行完毕之后,就开始进行页面的渲染。