从响应式原理一文了解到Vue最终是通过updateComponent方法来更新视图,updateComponent函数是调用 vm._update(vm._render(), hydrating)方法,_render内部是调用 vm.$options.render 方法 ;那我们就来了解下update函数和render函数到底做了什么。
render
我们在用vue-cli的时候main.js通常会有这么一段代码
// Vue实例化得参数就是 vm.$options
new Vue({
router,
i18n,
render: h => h(App)
}).$mount('#app');
复制代码
通过这里可以看出 render 是一个函数 他接受一个参数h,h也是一个函数,那h究竟是什么?h的本质其实就是createElement函数
createElement
作用
返回要创建的dom元素的描述,不是一个真实的 DOM 元素,它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也就是vnode,由vnode组成的映射真实dom的树我们称为‘虚拟DOM’。
总结:createElement 函数的作用是返回虚拟节点。
参数 (3 个)
createElement (renderElement: String | Component, definition: Object, children: String | Array)
/*
* params 参数名自定义的 跟源码有差入
renderElement : 必填项 一个 HTML 标签名、组件选项对象,或者resolve 了上述任何一种的一个 async 函数。
definition: 可填项 与模板中 attribute 对应的 数据对象。
children: 可填项 子虚拟节点 (VNodes),也是由 `createElement()` 构建而成,也可以使用字符串来生成“文本虚拟节点”。
*/
复制代码
使用场景
// 什么情况我们可以使用render函数去替换模板?
// 假设有如下场景 根据等级 level 动态生成 h: level 的模板内容
<div>
// level 1 : h1 、 level2 : h2 、 level3 : h3
<h1>
// 内容部分
{{content}}
</h1>
</div>
// --------------------------------分割线--------------------------------------
// 如果用模板template生成,页面就是以下这个样子 TestTemplate.vue
<template>
<h1 v-if='level === 1'>
<slot></slot>
</h1>
<h2 v-eles-if='level === 2'>
<slot></slot>
</h2>
<h3 v-eles-if='level === 3'>
<slot></slot>
</h3>
......
</template>
// 此时我们可以通过 以下代码生成上述场景
// <TestTemplate :level = '1'> 你好呀 </TestTemplate>
// 但是当等级过多的时候这个代码就很难看,很多余且重复书写了 <slot></slot>
// -------------------------------分割线------------------------------------
// 此时我们可以通过render方法来实现上述场景
<script>
Vue.component('test-template', {
render: function (createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
</script>
// 到这里会有一个疑问 ,就是createElement第二个参数是`数据对象definition`, 而这里传的是子节点数组,应该是第三个参数, 为什么会忽略第二个参数
// 解答:在createElement函数内部会进行第二个参数类型的判断,如果是数组或者非对象,就会给第二个参数换到第三个参数上去,第二个参数会给undefined。
// 源码位置: src\core\vdom\create-element.js
复制代码
深入数据对象(也就是createElement第二个参数到底可以定义啥东西)
// 官网示例
{
// 与 `v-bind:class` 的 API 相同,
// 接受一个字符串、对象或字符串和对象组成的数组
'class': {
foo: true,
bar: false
},
// 与 `v-bind:style` 的 API 相同,
// 接受一个字符串、对象,或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 普通的 HTML attribute
attrs: {
id: 'foo'
},
// 组件 prop
props: {
myProp: 'bar'
},
// DOM property
domProps: {
innerHTML: 'baz'
},
// 事件监听器在 `on` 内,
// 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
// 需要在处理函数中手动检查 keyCode。
on: {
click: this.clickHandler
},
// 仅用于组件,用于监听原生事件,而不是组件内部使用
// `vm.$emit` 触发的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
// 赋值,因为 Vue 已经自动为你进行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽的格式为
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'name-of-slot',
// 其它特殊顶层 property
key: 'myKey',
ref: 'myRef',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
refInFor: true
}
复制代码
render作用
render 函数作用就是使用Javascript创建模板,她的重点是createElement参数
在render函数中如何使用v-if、v-for、v-model、事件、描述符、插槽、jsx等可以去官网了解下。
-------------------------------官网传送门-----------------------------------
update
update顾名思义就是起到更新的作用,我们通过render已经得到vnode虚拟节点,那这个update方法他的作用肯定是将vnode组成的树也就是虚拟dom更新成真实dom的,那它内部是怎么实现的呢。
// _update 定义
// src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
// 初始渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 数据更新渲染
vm.$el = vm.__patch__(prevVnode, vnode)
}
// 省略部分代码
}
复制代码
可以看到
vm._update
是调用的vm.__patch__
方法,这时候查询__patch__
方法可以看到这方法在不同的平台,web 和 weex 上的定义是不一样的,在 web 平台中它的定义在 src\platforms\web\runtime\index.js 中
// __patch__ 方法定义
// src\platforms\web\runtime\index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 在不是浏览器环境下返回了个空函数,浏览器环境下调用patch方法
复制代码
// patch 方法定义
// src\platforms\web\runtime\patch.js
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)
// patch 实际是 createPatchFunction 函数 接受一个对象
// 对象传递俩参数 nodeOps :一些对dom的操作, modules应该是对不同平台的处理(eg:weex/web)
export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码
patch
// src\core\vdom\patch.js
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
// ********省略辅助函数代码
// 核心代码就是返回一个patch函数
// 这个函数可以更新真实dom
/*
*params
oldVnode 旧节点
vnode render函数返回的虚拟节点
hydrating 是否服务器端渲染
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)) { // 旧节点未定义
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 直接创建
} else {
// 判断是否是真实dom, 第一次渲染时传入vm.$el是真实节点, 所以为true
const isRealElement = isDef(oldVnode.nodeType) //nodeType:1 元素、3:文本,8:注释
// 如果不是真实节点,且新旧节点相同(通过比较两个节点的 key、tag、注释节点、数据信息是否相等来判断两个 Node 节点是否是相同节点,对 input 标签做了一个单独的判断,为了兼容不同浏览器。)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// vnode的diff算法
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 第一次渲染会进else,因为vm.$el不为空,传入的是真实节点。
if (isRealElement) {
// 服务器渲染
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)
// 创建新节点
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)
)
// 省略部分代码
// 移除旧节点
if (isDef(parentElm)) { // 第一次渲染时parentElm节点不为空,所以要移除。
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
}
复制代码
初次渲染时断点截图,执行完createElm还未执行removeVnodes
patch 作用
通过观察patch函数可以得知,他是通过对比oldVnode和vnode差异,映射到真实dom上,具体diff算法比较复杂,我就不深究了,感兴趣的jym可以了解一下。
总结
学习到这里,再结合上篇文章 vue源码阅读之响应式原理 ,我们可以了解到
data
是如何触发view
更新的
-
defineReactive
中通过Object.defineProperty
使 data数据响应式; -
Dep
在getter
中作依赖收集,在setter
中派发更新; -
通过
dep.notify()
通知Watcher
更新,最终调用vm._update(vm._render(), hydrating)
更新 UI; -
view
触发data
更新就是通过事件方法触发data
的setter
,从而实现双向绑定。