共计 10335 个字符,预计需要花费 26 分钟才能阅读完成。
前言
作为一个 vue 爱好学习者,也加入了源码解读的学习阵营,对一个 vue 和 react 框架都用过的前端小白来说,还是更喜欢写 vue 的语法,现在也很主流,一直想研究一下 vue 框架背后的实现机制,对 api 掌握、数据驱动、数据更新、以及组件等有个更全面的认识、而不仅仅局限于会用它,现在就当做记录一下自己的理解,会持续更新~
1:Vue 的本质:
其实就是一个用 Function 实现的 Class,通过它的原型 prototype 以及它本身扩展的一系列的方法和属性,所以一般我们会在 main.js 中会先 new Vue 一个实例对象出来,否则会报错 warn(‘Vue is a constructor and should be called with the new keyword’)
在 src/core/instance/index.js 代码如下
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’
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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
2:核心思想:数据驱动
所谓的数据驱动,是指视图是由数据驱动生成的,对视图的修改,不再直接操作 DOM,而是通过修改数据。我们所关心的只是数据的修改,DOM 变成了数据的映射。
3:初始化主要干的几件事情
合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher
4:new Vue 的时候做的事情
做了一层 initState() 的方法,给设置了 data 属性,会执行 getData() 方法,这里会先对 data 进行判断,是不是一个 function,代码如下
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initData (vm: Component) {
let data = vm.$options.data
// 这里判断 data 是不是一个 function
data = vm._data = typeof data === ‘function’
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
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
)
}
注意:在循环遍历对象属性时,会对 props 和 data 进行一层判断,二者不能重名,因为最后都会挂载到 vm 对象上,然后对 vm 对象进行一层 proxy 代理,下面的代码很重要
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
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)) {
// 会报 props 和 data 重名一样的警告
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)
}
}
// 将 vm 对象用_data 进行代理,收集和触发更新依赖
proxy(vm, `_data`, key)
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
这里的 proxy, 通过 Object.defineProtery 可以做到给原型去做代理,get() 方法收集依赖、set() 方法去触发更新,所以比如在 mounted() 时,例如打印一个 console.log(this.messags) 和 console.log(this._data.message) 是一样的结果,实际上访问的就是 vm._data.message
接着 el 设置了之后,进行 mount 函数处理,即 mount 钩子函数
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
5:Vue.prototype.$mount 函数中,说明的几个点
Vue 不能挂载到 body 或 html 这样的根节点上,一般都用 div 嵌套包括起来,会被覆盖,Vue2.0 版本中,所有的 vue 组件渲染最终都需要 rendr 方法,不论写的是 el 或者 template 属性,最终都会转换陈 render 方法,即 ” 在线编译的过程 ”
// 原型上添加 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// 若 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
// 如果是已经 render() 的话,不必再 compile()
if (!options.render) {
let template = options.template
if (template) {
…..
}
}
// 如果是 template 模板,需要进行 compile 解析
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) {
mark(‘compile’)
}
}
// 最后会创建 DOM 元素,在这里内容进行覆盖,这也是为什么外层一般要有一个父级 div 包裹它,而不是写在 body 或 html 上,实际上 template 会走一个 compileToFunctions 的过程
function getOuterHTML (el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement(‘div’)
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
_render():Vue 实例的一个私有方法,它用来把实例渲染成一个虚拟 Node, 用一个原生的 JS 对象去描述一个 DOM 节点,会比创建一个 DOM 的代价要小很多,这里和 react 的思想是一样的
onstructor (
tag?: string, // vNode 的标签,例如 div、p 等标签
data?: VNodeData, // vNode 上的的 data 值,包括其所有的 class、attribute 属性、style 属性已经绑定的时间
children?: ?Array<VNode>, // vNode 上的子节点
text?: string, // 文本
elm?: Node, // vNode 上对应的真实 dom 元素
context?: Component, //vdom 的上下文
componentOptions?: VNodeComponentOptions
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.functionalContext = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
上面是 VNode 的初始化,然后 Vue 它是通过 createElement 方法创建的 VNode
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
// 注意:这里会先进行一层判断,进行属性值前移,该方法可以借鉴在实际项目中
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
// _createElement() 是它的私有方法,创建成一个 VNode, 每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree
return _createElement(context, tag, data, children, normalizationType)
}
6:Vue.prototype._update(重要重要)
目的是为了把 vNode 转换为真实的 DOM,_update 会再首次渲染和数据更新的时候去调用,核心方法其实是其中的_patch() 方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
// 创建一个新的 vNode
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
// 和之前的 vNode, 进行 diff,将需要更新的 dom 操作和已经 patch 的 vNode 大道需要更新的 vNode, 完成真实的 dom 操作
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent’s updated hook.
}
看一下_patch 里面做了什么
// 定义了生命周期,这些钩子函数
const hooks = [‘create’, ‘activate’, ‘update’, ‘remove’, ‘destroy’]
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const {modules, nodeOps} = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// …
// oldVnode: 旧的 VNode 节点 or DOM 对象
// vnode: 执行了_render() 之后范湖的 VNode 的节点
// hydrating:是否是服务端渲染,因为 patch 是和平台相关的,在 Web 和 Weex 环境下,把 VNode 映射到平台 DOM 的方法也是不同 (有它自己的 nodeOps 和 modules)
// removeOnly: 给 transition-group 用的
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
// 创建新的节点
createElm(vnode, insertedVnodeQueue)
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
// oldVNode 和 vnode 进行 diff,并对 oldVnode 打 patch
patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (process.env.NODE_ENV !== ‘production’) {
warn(
‘The client-side rendered virtual DOM tree is not matching ‘ +
‘server-rendered content. This is likely caused by incorrect ‘ +
‘HTML markup, for example nesting block-level elements inside ‘ +
‘<p>, or missing <tbody>. Bailing hydration and performing ‘ +
‘full client-side render.’
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
// createElm 的作用:通过传入的 VNode 去创建真是的 DOM 元素,并插图到它的父节点中,
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
…
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes(parentElm, [oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
// 执行所有 created 的钩子并把 vnodepush 到 insertedVnodeQueue 中
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
其中对 oldVNode 和 vnode 类型判断中有一个 sameVnode 方法,这个方法很重要,是 oldVNode 和 vnode 需要进行 diff 和 patch 的前提
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
注意:insert() 方法把 DOM 插入到父节点中,进行了递归调用,子元素会优先调用 insert,所以整个 vnode 树节点的插入顺序是先子后父
insert(parentElm, vnode.elm, refElm)
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) {
if (ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else {
nodeOps.appendChild(parent, elm)
}
}
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
所以在 patch 的过程中,会有这个问题抛出来
if (isDef(tag)) {
if (process.env.NODE_ENV !== ‘production’) {
if (data && data.pre) {
creatingElmInVPre++
}
// 忘记注册组件的时候,会经常遇到如下报错,这个刚开始的时候遇到的情况很多
if (isUnknownElement(vnode, creatingElmInVPre)) {
warn(
‘Unknown custom element: <‘ + tag + ‘> – did you ‘ +
‘register the component correctly? For recursive components, ‘ +
‘make sure to provide the “name” option.’,
vnode.context
)
}
}
…..
}
可以看到最终返回的是一个 patch() 方法,赋值给 vm.__patch__() 方法
在 createElm 过程中,可以看到如果 vnode 节点不包含 tag 的话,它有可能是一个注释或者纯文本节点,可以直接插入到父元素中,递归创建一个完整的 DOM 并插入到 body 中。
总结: 对数据渲染的过程有了更深的一层理解,从 new Vue() 开始,创建了一个 vue 是对象,会先进行 init 初始化——>$mount()——>compile(若已经是 render 则该过程不需要)——>render——> 创建 VNode——>patch 过程——> 生成真实的 DOM