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 版本用来配合老的打包工具比如Browserify
或webpack 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
可以构建成不同平台下使用的库,目前有weex
和web
,还有服务器端渲染的库
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)
由以上代码我们可以得出如下结论:
-
- el 不能是 body 或者 html 标签
-
- 如果没有 render,把 template 转换成 render 函数
-
- 如果有 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
- initMixin(Vue) 注册 vm 的
// 此处不用 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 初始化过程调试
最后,我们来调试一遍初始化过程:
-
- 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
-
- src/core/index.js
- initGlobalAPI(Vue)
- config
components/filter/directives/set/nextTick/observable/keep-alive
等等
-
- src/platforms/web/runtime/index.js
- web 平台相关
- 注册和平台相关的全局指令:v-model、v-show
- 注册和平台相关的全局组件: v-transition、v-transition-group
- 全局方法:
- patch:把虚拟 DOM 转换成真实 DOM
- $mount:挂载方法
-
- src/platforms/web/entry-runtime-with-compiler.js
- 重写平台相关的 $mount() 方法
- 注册 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
3. 首次渲染过程
- 首次渲染过程从
instance/index.js
中调用this._init()
,我们来分析下它都做了什么
// 调用$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
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
}
}
src/platforms/web/runtime/index.js
中定义了$mount
方法
return mountComponent(this, el, hydrating)
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')
}
总结:
-
- instance/index.js
this._init()
vm.$mount(vm.$options.el)
-
- src/platforms/web/entry-runtime-with-compiler.js
- 重写$mount
- 如果没有render函数,就编译模版
- 把 template 转换成 render 函数
-
- src/platforms/web/runtime/index.js
- 定义$mount
- 重新获取el 因为运行时版本不会执行src/platforms/web/entry-runtime-with-compiler.js文件
- return mountComponent(this, el, hydrating)
-
- src/core/instance/lifecycle.js
- beforeMount
- 定义更新组件方法updateComponent
- vm._render() 调用用户传入的render或生成的render,生成虚拟dom
- vm._update() 将虚拟dom转为真实dom
- new Watcher()中watcher.get()执行组件方法updateComponent
- mounted
4. 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
}
}
})
- 初始化这个
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()
}
}
- 拿到计算属性对应的
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()
}
}
- 执行
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.firstName
和 this.lastName
都是响应式对象,这里会触发它们的 getter
,它们会把自身持有的 dep
添加到当前正在计算的 watcher
中,这个时候 Dep.target
就是这个 computed watcher
。
最后通过 return this.value
拿到计算属性对应的值。
- 计算属性依赖数据更改
那么对于计算属性这样的
computed watcher
,它实际上是有 2 种模式,lazy
和active
。如果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)
}
- 重新计算
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渲染效率。等到所有的微任务都被执行完毕之后,就开始进行页面的渲染。