前言
本文主要记录 Vue 的响应式原理以及数据首次渲染流程,和数据更新后的渲染流程
本文依赖 Vue 版本 2.6.11, 为了方便理解,文章源码有删减
一、找出 Vue 构造函数
问题:
import Vue from 'vue'
, 引入的是vue dist
目录下哪个 js 文件?😏😏
vue 是通过 webpack
进行打包,可通过命令 vue inspect > config.js
将打包配置输出的 config.js
文件中
在
config.js
文件中可以看到,真正引入的是 vue.runtime.esm.js
文件, 我们接着看下 vue 源码中是如何打包出该文件的。
在
package.json
文件中可以看出 npm run build
命令执行的是 scripts/build.js
文件,在 build.js
文件中,主要代码是
let builds = require('./config').getAllBuilds()
build(builds)
function build (builds) {
let built = 0
const total = builds.length
const next = () => {
//...
}
next()
}
在 scripts/config.js
文件中, 可以找出打包成不同文件的配置信息
可以看出
vue.runtime.esm.js
文件的打包入口文件是 web/entry-runtime.js
。在 vue 中声明了很多别名
// src/alias.js
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
所以解析出来的打包入口的完整路径是 src/platforms/web/entry-runtime.js
然后在该文件中,并没有找到 Vue 构造函数,而是从别处引入再导出
import Vue from './runtime/index'
export default Vue
通过在下面几个文件中不停跳转
- src/platforms/web/runtime/index.js
- src/core/index.js
- src/core/instance/index.js
终于在 src/core/instance/index.js
找到了 Vue 构造函数
function Vue (options) {
this._init(options)
}
// ...
export default Vue
我们从内向外,看看每个文件的具体工作
1、src/core/instance/index.js
主要工作是:导入的每个
xxxMixin
都是为了扩展 Vue.prototype, 在原型上增加属性和方法
- initMixin: 扩展
_init
方法 - stateMixin:扩展
$data
、$props
、$set
、$delete
、$watch
方法 - eventsMixin:扩展
$on
、$once
、$off
、$emit
方法 - lifecycleMixin:扩展
_update
、$forceUpdate
、$destroy
方法 - renderMixin:扩展
$nextTick
、_render
方法
2、src/core/index.js
主要工作是:
initGlobalAPI
初始化全局api, 以及扩展原型上的属性
3、src/platforms/web/runtime/index.js
主要工作是:安装特定于平台的 utils
、运行时指令和组件以及定义公用的 __patch__
、 $mount
方法 。
二、new Vue 实例初始化
我们从一个简单的 vue 代码开始
<template>
<div id="app">
<div>{{name}}</div>
<div>{{hobby}}</div>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
name: '张三',
hobby: ['music', 'game', 'coding']
}
}
}
</script>
不出意外,页面会如下显示
我们具体看下,数据首次渲染如何实现的吧
2.1 数据首次渲染
2.1.1 数据监测
从核心文件 src/core/instance/index.js 开始分析
- **this._init(options)**调用方法 _init,前面分析得知,该方法定义在 initMixin 中
- initState(vm) , 状态初始化
- initData(vm)
- observe(data, true)
export function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) { // 非对象和 VNode 不进行监测
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__ // 已经监测过的直接返回,__ob__ 标识是否监测过
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) // 进行监测
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
- new Observer(value) ,数据监测
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() // 1、为对象、数组增加一个 dep 实例
this.vmCount = 0
def(value, '__ob__', this) // 2、增加 __ob__ 标识
if (Array.isArray(value)) {
if (hasProto) { // 3、是否支持使用 __proto__
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value) // 4、对数组(eg: 对象数组)中的每一项进行 observe
} else {
this.walk(value)
}
}
walk (obj: Object) { // 5、遍历 Object
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
需要特别注意的是:在 constructor
中,执行 this.dep = new Dep()
,为对象、数组创建一个 dep 实例,而在👇 介绍的 defineReactive
中,是给对象中的每个属性创建一个 dep 实例。
(ps: 关于 Dep 对象,稍后介绍)
那为什么要给对象、数组创建一个 dep 实例?
当我们执行 arr.push
或者使用 $set
给对象增加新属性时,就是通过自身的 dep
实例来实现页面从新渲染,后续介绍
- defineReactive(obj, keys[i]),对象进行观测
export function defineReactive (obj,key,val,) {
const dep = new Dep() // 1、为每个属性创建一个 dep 实例
// ...
let childOb = observe(val) // 2、val 可能是数组、对象,也需要监测
Object.defineProperty(obj, key, { // 3、使用 Object.defineProperty 数据劫持
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = val
/* if (Dep.target) { // 3.1、依赖收集,后续介绍
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
} */
return value
},
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
childOb = observe(newVal) // 3.2、新设置的 newVal 可能是数组、对象,也需要监测
// dep.notify() // 数据更新,触发更新,后续介绍
}
})
}
- arrayMethods ,数组进行观测,通过重写数组方法
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [ // 需要重新的数组方法,共 7 个
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args) // 1、通过调用默认的方法获取返回值
const ob = this.__ob__ // 2、this.__ob__ 可以访问的 Observer 实例
let inserted
switch (method) { // 3、push、unshift、splice 三个方法会往数组中增加新值,可能是对象,需要监测
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// ob.dep.notify() // 数据更新,触发更新,后续介绍
return result
})
})
2.1.2 模版编译及渲染
继续回到 _init
方法中,initState(vm)
之后,会执行 $mount
,我们看着整个流程
-
vm.options.el),$mount 方法定义在 src/platforms/web/runtime/index.js 文件中
-
mountComponent(this, el), mountComponent 方法定义在 src/core/instance/lifecycle.js 文件中
export function mountComponent (vm, el) {
vm.$el = el
let updateComponent = () => { //
vm._update(vm._render())
}
// 1、初始化,创建一个 Watcher 实例,(ps: 可以记着渲染 watcher)
new Watcher(vm, updateComponent, noop, {}, true /* isRenderWatcher */)
return vm
}
- new Watcher()
export default class Watcher {
constructor (vm, expOrFn, cb, options,isRenderWatcher) {
this.vm = vm
this.cb = cb
this.id = ++uid // 1、给Watcher 实例设置 id
if (typeof expOrFn === 'function') { // 2、定义 getter
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.get() // 3、get() => 执行 getter()
}
get () {
// 4、Dep.target = this, 将 Dep.target 设置成当前 Watcher 实例, 也就是渲染watcher
pushTarget(this)
let value = this.getter.call(vm, vm) // 5、执行 getter -> expOrFn
return value
}
}
从👆分析得出,new Watcher
初始化时,会默认执行 get()
-> getter()
-> expOrFn
-> updateComponent
-> vm._update(vm._render())
, 我们继续看下 vm._update(vm._render())
的执行过程吧
- vm._update(vm._render()),_render 定义在 renderMixin中,_update 定义在 lifecycleMixin 中
- vm._render()
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render } = vm.$options
let vnode = render.call(vm)
return vnode
}
_render() 方法内部执行 $options.render 方法,生成 vnode, 当 render
中使用了实例中的属性时,在 render
执行过程中,会进行取值,触发这些属性的 getter
, 从何实现依赖收集(ps: 这部分后续介绍)
我们需要先搞清楚的是 render
方法没定义,从何而来?
根据 Vue 文档 中定义:
el
提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标, Vue 生成的 DOM 会替换掉挂载元素 如果render
函数和template
property 都不存在,挂载 DOM 元素的 HTML 会被提取出来用作模板,此时,必须使用 Runtime + Compiler 构建的 Vue 库
前面分析知道,Vue 默认使用的是 vue.runtime.esm.js
文件,并不支持 Compiler
,而 Compiler
则提供了将 template
转换成 render
的能力
import Vue from 'vue'
new Vue({
data: {
msg: '233'
},
template: '<div>{{msg}}</div>'
}).$mount('#app')
此时控制台会报错
当使用 Runtime + Compiler
构建的 Vue 库时,会重写 $mount
方法(参考 src/platforms/web/entry-runtime-with-compiler.js 文件)
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el, hydrating): Component {
el = el && query(el)
const options = this.$options
if (!options.render) { // 1、判断 render 是否存在
let template = options.template // 2、判断 template 是否存在
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {
// 3、当 template 不存在,将 el 对应的挂载 DOM 元素的 OuterHTML 提取出来用作 template
template = getOuterHTML(el)
}
if (template) {
// 4、compileToFunctions 将 template 转换成 render, => 模版编译
const { render } = compileToFunctions(template, {}, this)
options.render = render
}
}
return mount.call(this, el, hydrating) // 5、调用之前定义的 render 方法
}
默认 .vue 文件中的 template
处理是通过 vue-loader
来进行处理,靠的是 vue-template-compiler
模块生成 render
const VueTemplateCompiler = require('vue-template-compiler');
const {render} = VueTemplateCompiler.compile("<div id="hello">{{msg}}</div>");
console.log(render.toString())
小总结:render 方法如何生成?
1、非 Compiler
构建版本, 提供 render
函数
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App),
}).$mount('#app')
2、Compiler
构建版本,按照 render
> template
> OuterHTML
优先级生成 render
函数
- vm._update(vnode)
Vue.prototype._update = function (vnode) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// 1、首次渲染
vm.$el = vm.__patch__(vm.$el, vnode)
} else {
// 2、更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
- vm.patch(vm.$el, vnode), 首次渲染,该方法定义在 src/platforms/web/runtime/index.js, 通过引用路径,最终的 patch 方法定义在 src/core/vdom/patch.js文件中
- patch
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 1、更新数据,patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 2、首次渲染,replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
//...
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
return vnode.elm
}
}
当首次渲染时,会使用传入的 VNode
生成新的 DOM
, 去替换掉旧的 DOM
至此,页面就成功渲染了
2.1.3 依赖收集
当 render
中使用了实例中的属性时,在 render
执行过程中,会进行取值,触发这些属性的 getter
, 从何实现依赖收集
依赖收集是通过 Dep
对象来实现的,在数据劫持阶段,给每个属性创建一个 dep
实例,在属性取值时,会触发 dep.depend()
export function defineReactive (obj,key,val,) {
const dep = new Dep() // 1、为每个属性创建一个 dep 实例
// ...
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = val
if (Dep.target) {
dep.depend() // 2、进行依赖收集
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// ...
}
})
}
dep.depend()
执行的整个流程图如下
在挂载阶段 Dep.target
存储这渲染watcher, 执行 dep.depend()
后,属性的 dep
中就会存储这个渲染 watcher
, 同时渲染watcher 中也会存储该 dep
,二者双向绑定
2.2 数据更新渲染
还是从一个简单的 vue 代码开始
<template>
<div id="app">
<div>{{name}}</div>
<div>{{hobby}}</div>
</div>
</template>
<script>
export default {
name: 'App',
data () {
return {
name: '张三',
hobby: ['music', 'game', 'coding']
}
},
mounted (){
setTimeout(()=> {
this.name = 'zhangsan',
this.hobby.push('running')
}, 2000)
}
}
</script>
2s 之后。页面如下展示
我们来看下当数据更新到页面渲染的整个流程。👆数据更新可分成对象属性更新 和 数组更新
2.2.1 对象属性更新
执行 this.name = 'zhangsan'
, 会触发属性 name
的 setter
方法
export function defineReactive (obj,key,val,) {
const dep = new Dep()
// ...
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
//...
},
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify() // 数据更新
}
})
}
dep.notify()
执行的整个流程图如下
当执行 dep.notify()
时,会遍历 dep
中存储的所有 watcher
, 执行其 update
方法,该方法会将当前 watcher
加入队列,并开启异步执行队列实现更新,也就是执行 getter
,最终会执行 vm._update(vm._render())
,再到 patch
方法,进行 VNode Diff
实现页面更新
2.2.2 数组更新
执行 this.hobby.push('running')
, 会执行我们重写的 push
方法
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__ // 2、this.__ob__ 可以访问的 Observer 实例
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify() // 数据更新,触发更新,后续介绍
return result
})
})
在 push
方法中,会执行 ob.dep.notify()
, 但需要声明的是,此时的 dep 是在数组 ['music', 'game', 'coding']
创建的 dep 实例,后续的更新操作和对象属性的更新相同,不再啰嗦
三、总结
本文从源码的角度,介绍了 Vue 响应式原理以及数据更新流程,大致的流程图如下
如有错误之处,欢迎指出
参考链接