- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
Vue2.0源码学习(1) - 数据和模板的渲染
上述updateComponent方法调用是运行了一个函数:
// src\core\instance\lifecycle.js
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
其中会先运行vm._render函数,那么vm._render函数又是从哪里定义的呢?我们回到src\core\instance\index.js,这里一开始会运行renderMixin方法,而renderMixin函数具体定义在src\core\instance\render.js中,去到函数中可以清晰看到,我们心心念念的vm._render就定义在此处了。
// src\core\instance\render.js
export function renderMixin (Vue: Class<Component>) {
...
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
...
let vnode
try {
// 暂时只关注这部分代码
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
...
}
...
return vnode
}
}
render函数中其他的先不做详细解读,先把目光聚焦在render.call(vm._renderProxy, vm.$createElement)。
render函数可以在配置中自己写,也可以是生成的,在上节Vue实例挂载中有具体分析过$mount是如何生成render函数的大概流程,忘了的可以回顾一下,往下会以自写的render去跑一遍代码流程。
剖析点①:
vm._renderProxy:在_init函数中定义。
Vue.prototype._init = function (options?: Object) {
...
if (process.env.NODE_ENV !== 'production') { //非生产环境
initProxy(vm)
} else {
vm._renderProxy = vm
}
...
}
代码中可知生产环境其实就是vm或者说this本身,而非生产环境时则是在initProxy函数中定义vm._renderProxy。我们往下看看定义在src\core\instance\proxy.js里的initProxy方法。
// src\core\instance\proxy.js
let initProxy
...
const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy)
// 浏览器是否支持proxy
if (hasProxy) {
const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')
config.keyCodes = new Proxy(config.keyCodes, {
set (target, key, value) {
if (isBuiltInModifier(key)) {
warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
return false
} else {
target[key] = value
return true
}
}
})
}
const hasHandler = {
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
}
}
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
export { initProxy }
initProxy方法首先用hasProxy判断浏览器是否支持proxy,不支持情况下vm._renderProxy就是vm,支持的情况则是用Proxy对vm做一个数据劫持。
vm.$createElement:把render函数转换成Vnode,它的定义需要回到我们开始说的vue._init方法,其中运行了initRender方法,而vm.$createElement就定义在initRender函数中,下面我们看看initRender函数。
// src\core\instance\render.js
export function initRender (vm: Component) {
...
// bind the createElement fn to this instance
// (将createElement fn绑定到此实例)
// so that we get proper render context inside it.
// (以便在其中获得适当的渲染上下文)
// args order: tag, data, children, normalizationType, alwaysNormalize(参数标注)
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
...
}
其中vm._c是生产render所调用的,而vm.$createElement是自写render调用的,下面我们自写个render来做调试实验。
new Vue({
el:'#app',
render(createElement){
return createElement('div',{
attrs:{
id:"app1"
}
},this.msg)
},
data(){
return{
msg:"niccc"
}
}
})
还记得前面的Vue.prototype._render方法不,里面有段代码render.call(vm._renderProxy, vm.$createElement),上面代码块中的render函数中的createElement参数,其实就是vm.$createElement方法。
注:官方render函数文档 https://cn.vuejs.org/v2/api/#render
不知道各位是否还记得之前分析的render函数,其中的vm._c和vm.$createElement都是调用了createElement函数。下面我们来看看该函数
//src\core\vdom\create-element.js
const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
//判断data参数是否为空,空时自动补全
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
createElement 方法实际上是对 _createElement 方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElement,由于函数体较大,我们进行分段解读。
//src\core\vdom\create-element.js
// part 1
export function _createElement (
context: Component, //上下文环境,一般就是vm
tag?: string | Class<Component> | Function | Object, //标签(element)
data?: VNodeData, //VNode数据,VnodeData类型,详见flow\vnode.js
children?: any, //Vnode子节点
normalizationType?: number //子节点规范类型
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
// warn against non-primitive key
if (process.env.NODE_ENV !== 'production' &&
isDef(data) && isDef(data.key) && !isPrimitive(data.key)
) {
if (!__WEEX__ || !('@binding' in data.key)) {
warn(
'Avoid using non-primitive value as key, ' +
'use string/number value instead.',
context
)
}
}
...
}
我们先分析一下入参:
context:上下文环境,一般就是vm;
tag:标签;
data:VNode数据,VnodeData类型,详见flow\vnode.js;
children:Vnode子节点;
normalizationType:子节点规范类型;
往下看其实就是对data数据的判断,看是否需要跑createEmptyVNode函数,即创建注释函数。下面我们继续看part 2。
//src\core\vdom\create-element.js
// part 2
export function _createElement (
context: Component, //上下文环境,一般就是vm
tag?: string | Class<Component> | Function | Object, //标签(element)
data?: VNodeData, //VNode数据,VnodeData类型,详见flow\vnode.js
children?: any, //Vnode子节点
normalizationType?: number //子节点规范类型
): VNode | Array<VNode> {
...
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
...
}
children最终形态是Vnode的节点,但是入参中的children却是any类型,所以我们需要对它进行转换,normalizeChildren和simpleNormalizeChildren就是做了这项任务。下面我们看看这两个函数
// src\core\vdom\helpers\normalize-children.js
// 其中simpleNormalizeChildren是拍扁数组,成为一维素组
export function simpleNormalizeChildren (children: any) {
for (let i = 0; i < children.length; i++) {
if (Array.isArray(children[i])) {
return Array.prototype.concat.apply([], children)
}
}
return children
}
simpleNormalizeChildren方法其实就是为了拍扁为一维数组,具体是什么场景下进行的,后续再研究过来填坑。
// src\core\vdom\helpers\normalize-children.js
export function normalizeChildren (children: any): ?Array<VNode> {
return isPrimitive(children) //判断是否为基础类型
? [createTextVNode(children)] //是,创建一个vnode节点
: Array.isArray(children) //否,判断是否为数组
? normalizeArrayChildren(children) //详见下文
: undefined
}
normalizeChildren其实最终也是返回一个一维数组,它分了三个情况:
①:isPrimitive判断是基础类型,返回一个用一维数组包裹着的文本Vnode;
②:Array.isArray判断是数组类型,调用normalizeArrayChildren函数;
③:啥都都不是,返回undefined;
然后我们看看第②个情况中,normalizeArrayChildren做了什么。
// src\core\vdom\helpers\normalize-children.js
function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {
const res = []
let i, c, lastIndex, last
for (i = 0; i < children.length; i++) {
c = children[i]
if (isUndef(c) || typeof c === 'boolean') continue
lastIndex = res.length - 1
last = res[lastIndex]
// 注①,如果是数组类型,则继续调用normalizeArrayChildren递归,递归后于父数组apply为一个一维数组。
if (Array.isArray(c)) {
if (c.length > 0) {
// 递归chilrend
c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)
// merge adjacent text nodes(合并相邻的文本节点)
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
c.shift()
}
res.push.apply(res, c)
}
// 注②,如果是基础类型,则创建文本Vnode然后push到res数组中
} else if (isPrimitive(c)) {
if (isTextNode(last)) {
// merge adjacent text nodes
// this is necessary for SSR hydration because text nodes are
// essentially merged when rendered to HTML strings
res[lastIndex] = createTextVNode(last.text + c)
} else if (c !== '') {
// convert primitive to vnode
res.push(createTextVNode(c))
}
// 注③,再否则其实就是一个正常的Vnode,然后回对一些v-for做一些处理,然后也是push到res数组中
} else {
if (isTextNode(c) && isTextNode(last)) {
// merge adjacent text nodes
res[lastIndex] = createTextVNode(last.text + c.text)
} else {
// default key for nested array children (likely generated by v-for)
if (isTrue(children._isVList) &&
isDef(c.tag) &&
isUndef(c.key) &&
isDef(nestedIndex)) {
c.key = `__vlist${nestedIndex}_${i}__`
}
res.push(c)
}
}
}
return res
}
normalizeArrayChildren做的其实就是利用递归把多维数组apply合并成一维数组。
注①:如果是数组类型,则继续调用normalizeArrayChildren递归,递归后于父数组apply为一个一维数组。
注②:如果是基础类型,则创建文本Vnode然后push到res数组中;
注③:再否则其实就是一个正常的Vnode,然后回对一些v-for做一些处理,然后也是push到res数组中;
//src\core\vdom\create-element.js
// part 3
export function _createElement (
context: Component, //上下文环境,一般就是vm
tag?: string | Class<Component> | Function | Object, //标签(element)
data?: VNodeData, //VNode数据,VnodeData类型,详见flow\vnode.js
children?: any, //Vnode子节点
normalizationType?: number //子节点规范类型
): VNode | Array<VNode> {
...
let vnode, ns
//这个tag就是'string'
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
//是否HTML原生标签
if (config.isReservedTag(tag)) {
// platform built-in elements
vnode = new VNode(
//config.parsePlatformTagName方法其实就是传什么返回什么
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// unknown or unlisted namespaced elements
// check at runtime because it may get assigned a namespace when its
// parent normalizes children
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
//组件形式后面再说
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
首先会判断tag的类型,tag可以是字符串,也可以是组件,组件我们往后再细说,这里先关注String类型。config.isReservedTag判断是否HTML原生标签,(该方法定义在src\platforms\web\util\element.js),config.parsePlatformTagName方法其实就是传什么返回什么,然后new Vnode创建一个Vnode实例。
这个最终生成好的vnode,会经过几个方法return到_render函数。
①:return到createElement函数;
②:return到vm.$createElement;
③:return到vue._render中的vnode = render.call(vm._renderProxy, vm.$createElement)。
自此_render函数就告一段落了,我们接下来开始分析vm._update(vm._render(), hydrating)的vm._update函数。
_update是实例的一个私有方法,主要是吧Vnode渲染成真实的dom。调用的情况有两种,一是首次渲染,二是数据更新的时候。_update方法定义在src\core\instance\lifecycle.js,接下来我们看看具体的方法。
// src\core\instance\lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
...
// 主要方法patch
vm.$el = vm.__patch__(prevVnode, vnode)
...
}
}
其中核心其实就是vm.__patch__函数,其定义在 src\platforms\web\runtime\index.js
// src\platforms\web\runtime\index.js
import { patch } from './patch'
// 判断是否浏览器环境
Vue.prototype.__patch__ = inBrowser ? patch : noop
__patch__有个三元表达式,inBrowser,用于判断是否为浏览器环境,是则为patch方法,否则就是noop空函数(服务器渲染时用到的)。下面我们看看patch方法,patch方法定义在src\platforms\web\runtime\patch.js
// 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)
export const patch: Function = createPatchFunction({ nodeOps, modules })
// createPatchFunction方法创建了许多辅助函数,最后返回patch函数(函数柯里化)
可以看出,patch方法其实就是createPatchFunction方法调用后的return值,createPatchFunction入参分别是nodeOps和modules。
nodeOps其实就是封装一些dom原生操作的方法,有兴趣的可以到src\platforms\web\runtime\node-ops.js看看;
modules是baseModules和platformModules的合集,主要是一些模块的钩子函数,主要用于生成dom,这里暂时不多赘述。
我们把关注点放到createPatchFunction函数上,由于createPatchFunction函数比较复杂,下面会分段分析。
// src\core\vdom\patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
}
}
createPatchFunction很复杂,看源码是可以看出它其中定义了很多的辅助函数,然后最终返回了一个patch函数,这个也就是patch的最终方法,为什么要这么处理呢?其实这里是运用了函数柯里化的技巧,主要目的是让其有更高的灵活度和可复用性。接下来我们来详细看patch方法。
// src\core\vdom\patch.js
return function patch (oldVnode, vnode, hydrating, removeOnly) {
...
// oldVnode有值所以进入 else 阶段
if (isUndef(oldVnode)) {
...
} else {
const isRealElement = isDef(oldVnode.nodeType)
// isRealElement为true,进入else阶段
if (!isRealElement && sameVnode(oldVnode, vnode)) {
...
} else {
// isRealElement为true
if (isRealElement) {
...
// either not server-rendered, or hydration failed.
// create an empty node and replace it
// emptyNodeAt函数作用是把真实dom转换成虚拟dom
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm //提取真实dom对象(div#app)
const parentElm = nodeOps.parentNode(oldElm) //真实dom的父级(body)
// create new node
// 下面会单独分析
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 : parent Elm,
nodeOps.nextSibling(oldElm)
)
...
}
}
//插入钩子函数,后续再细看
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}
因为patch的高复用性,因此patch函数有多个条件判断语句,建议调试看着代码去理解patch。
现在我们以vue-cli默认模板来去分析,其中省略了部分没运行到的代码,上述代码注释中有注释解析,这边就不多赘述了,就简单说一下入参。oldVnode其实就是真实dom对象;vnode是虚拟dom;hydrating和removeOnly其实就是false。
一套流程下来去到了createElm函数,它是定义在createPatchFunction中的,下面我们来具体分析一下这个函数。
// src\core\vdom\patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
...
// 标注①
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
...
// 标注②
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode)
setScope(vnode)
/* istanbul ignore if */
if (__WEEX__) {
...
} else {
// 标注③
createChildren(vnode, children, insertedVnodeQueue)
if (isDef(data)) {
// 创建一些钩子,后续再研究
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 标注④
insert(parentElm, vnode.elm, refElm)
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text)
insert(parentElm, vnode.elm, refElm)
} else {
vnode.elm = nodeOps.createTextNode(vnode.text)
insert(parentElm, vnode.elm, refElm)
}
}
createElm的作用其实就是通过vnode创建真实dom并插入到父节点中。
下面我们来分析上述代码块中标注的代码:
①:这里会尝试创建一个组件,当然我们现在创建的是页面,返回的是undefined,后续讲解组件化的时候会再详细聊createComponent函数;
②:vnode.ns为undefined,因此运行的是nodeOps.createElement(tag, vnode),createElement其实就是就是调用了原生的document.createElement方法去创建一个dom,源码在src\platforms\web\runtime\node-ops.js中;
③:children是否有子节点,有的话创建子节点;createChildren函数其实其实就是递归createElm方法,把子节点插入进来;
④:insert其作用就是根据判断插入dom(insertBefore/appendChild),用到的地方很多,是插入真实dom的主要方法;
自此,数据和模板是如何渲染到dom的过程我们已经分析完毕,当然中间还有很多细节的东西我们没有去探讨,我们首先把思维流程架构起来,然后再慢慢发散分支,这样会更有助我们去把源码读懂,下面再附上一张数据驱动流程图。
初学者 android 问题。好的,我已经成功写入文件。例如。 //获取文件名 String filename = getResources().getString(R.string.filename
我已经将相同的图像保存到/data/data/mypackage/img/中,现在我想显示这个全屏,我曾尝试使用 ACTION_VIEW 来显示 android 标准程序,但它不是从/data/dat
我正在使用Xcode 9,Swift 4。 我正在尝试使用以下代码从URL在ImageView中显示图像: func getImageFromUrl(sourceUrl: String) -> UII
我的 Ubuntu 安装 genymotion 有问题。主要是我无法调试我的数据库,因为通过 eclipse 中的 DBMS 和 shell 中的 adb 我无法查看/data/文件夹的内容。没有显示
我正在尝试用 PHP 发布一些 JSON 数据。但是出了点问题。 这是我的 html -- {% for x in sets %}
我观察到两种方法的结果不同。为什么是这样?我知道 lm 上发生了什么,但无法弄清楚 tslm 上发生了什么。 > library(forecast) > set.seed(2) > tts lm(t
我不确定为什么会这样!我有一个由 spring data elasticsearch 和 spring data jpa 使用的类,但是当我尝试运行我的应用程序时出现错误。 Error creatin
在 this vega 图表,如果我下载并转换 flare-dependencies.json使用以下 jq 到 csv命令, jq -r '(map(keys) | add | unique) as
我正在提交一个项目,我必须在其中创建一个带有表的 mysql 数据库。一切都在我这边进行,所以我只想检查如何将我所有的压缩文件发送给使用不同计算机的人。基本上,我如何为另一台计算机创建我的数据库文件,
我有一个应用程序可以将文本文件写入内部存储。我想仔细看看我的电脑。 我运行了 Toast.makeText 来显示路径,它说:/数据/数据/我的包 但是当我转到 Android Studio 的 An
我喜欢使用 Genymotion 模拟器以如此出色的速度加载 Android。它有非常好的速度,但仍然有一些不稳定的性能。 如何从 Eclipse 中的文件资源管理器访问 Genymotion 模拟器
我需要更改 Silverlight 中文本框的格式。数据通过 MVVM 绑定(bind)。 例如,有一个 int 属性,我将 1 添加到 setter 中的值并调用 OnPropertyChanged
我想向 Youtube Data API 提出请求,但我不需要访问任何用户信息。我只想浏览公共(public)视频并根据搜索词显示视频。 我可以在未经授权的情况下这样做吗? 最佳答案 YouTube
我已经设置了一个 Twilio 应用程序,我想向人们发送更新,但我不想回复单个文本。我只是想让他们在有问题时打电话。我一切正常,但我想在发送文本时显示传入文本,以确保我不会错过任何问题。我正在使用 p
我有一个带有表单的网站(目前它是纯 HTML,但我们正在切换到 JQuery)。流程是这样的: 接受用户的输入 --- 5 个整数 通过 REST 调用网络服务 在服务器端运行一些计算...并生成一个
假设我们有一个名为 configuration.js 的文件,当我们查看内部时,我们会看到: 'use strict'; var profile = { "project": "%Projec
这部分是对 Previous Question 的扩展我的: 我现在可以从我的 CI Controller 成功返回 JSON 数据,它返回: {"results":[{"id":"1","Sourc
有什么有效的方法可以删除 ios 中 CBL 的所有文档存储?我对此有疑问,或者,如果有人知道如何从本质上使该应用程序像刚刚安装一样,那也会非常有帮助。我们正在努力确保我们的注销实际上将应用程序设置为
我有一个 Rails 应用程序,它与其他 Rails 应用程序通信以进行数据插入。我使用 jQuery $.post 方法进行数据插入。对于插入,我的其他 Rails 应用程序显示 200 OK。但在
我正在为服务于发布请求的 API 调用运行单元测试。我正在传递请求正文,并且必须将响应作为帐户数据返回。但我只收到断言错误 注意:数据是从 Azure 中获取的 spec.js const accou
我是一名优秀的程序员,十分优秀!