数据驱动
前言
- 跟这黄轶大神学习的 vue,做一些简单记录。
- 这部分对应原文中的数据驱动,整体流程只涉及 vue 初始化渲染到页面上,不包括编译过程、vnode diff,响应式,
- 以下均为自己的理解,不对之处请指正
- 本文地址及自己添加的注释在这里
init 之前
init 之前是做了很多初始化的工作的。比较重要的:
vue/src/platforms/web/runtime/index.js
- 把
directives (v-model v-show) 平台定义的组件(transion transion-group)放到 options 里
extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);
- 给 vue 一个
__patch__ 方法, 用于后续 vnode 更新, 属于核心方法 Vue.prototype.__patch__ = inBrowser ? patch : noop; - 暴露
api $mount ,用于后续挂载节点到页面,属于核心流程 Vue.prototype.$mount = ...
vue/src/core/index.js
这里主要是 initGlobalAPI(Vue) 方法,主要做了以下三件事
- 暴露一些
api ,比如 use mixin extend filter directive ,这些我们开发中都会用到 - 初始化
options['components'|'directive'|'filter'] ,这里会记录该 vue 组件中注册的所有组件、指令和过滤器,作用类似于供 vue 判断某个组件是不是在 vue 里注册了,没有注册的话就报错 - 把 vue 自带的组件如
keep-alive 注册到 vue 里
vue/src/core/instance/index.js
这里也是在挂载并暴露 apidata $props $set $del $watch $on #once $off $emit update $forceupdate $destory …
init vue/src/core/instance/init.js
init 主要也是在做两件事
- 初始化
- 调用两个生命周期钩子
beforeCreate create 这里对初始化过程做进一步描述
options 初始化
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
这里其实就是对用户传入的 options 、extend 的 options mixin 的 options 进行一次合并,对于三者同一属性的优先级处理是有一个策略的,详细在vue/src/core/util/options.js
initLifecycle vue/src/core/instance/lifecycle.js
$parent $children 记录两个vm 之前的父子关系$root 记录根组件
initRender vue/src/core/instance/render.js
- 绑定
_c() $createElement() 方法 用于后续生成vnode ,核心流程(_c 是内部调用,$createElement 是用户手写render 时调用) - 暴露 api
$slots $attrs $listeners 属性、方法
其他
初始化 inject 、props methods data computed watcher provide
$mount
$mount 之前 vue/src/platforms/web/entry-runtime-with-compiler.js
$mount 的功能是把 vue 渲染的元素挂载到页面上,这里有两种情况
- 用户手写
render ,不需要编译,直接走$mount 方法, - template 语法,需要编译,此时需要先编译,在走$mount 方法
编译的结果就是往 options 上挂载 render 、staticRenderFns 方法
// vue/src/platforms/web/entry-runtime-with-compiler.js
const { render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: process.env.NODE_ENV !== "production",
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments,
},
this
);
options.render = render;
options.staticRenderFns = staticRenderFns;
$mount vue/src/core/instance/lifecycle.js fn mountComponent
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, "beforeUpdate");
}
},
},
true /* isRenderWatcher */
);
这里其实初始化了一个渲染 watcher ,在 watcher 里,此处触发一次updateComponent 函数,此函数核心逻辑就是
vm._update(vm._render(), hydrating);
vm._render() 是生成vnode 节点vm_update() 是把vnode 节点变成 html 元素并挂载到页面上
render vue/src/core/instance/render.js
render 的核心逻辑是
vnode = render.call(vm._renderProxy, vm.$createElement);
其中 render 有两种来源
vue 编译而来,这涉及很复杂的编译过程,放在后面再细化- 用户手写,比如,你可能见过这样的代码
new Vue({
...
render: h => h(App)
}).$mount('#app')
从这里我们可以看到 h 其实就是 vm.$createElement
关于 render 的语法可以参考 现在来看看下用户手写render 函数逻辑,即 vm.$createElement(app) 的逻辑
//vue/src/core/instance/render.js
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);
$createElement 也就是在执行 createElement
createElement vue/src/core/vdom/create-element.js
关键流程
- 处理
children - 生成
vnode 并返回
- 如果是组件 就
createComponent - 其他情况就直接
new Vnode
...
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children);
}
...
if (组件) {
vnode = createComponent(Ctor, data, context, children, tag);
} else {
vnode = new VNode(tag, data, children, undefined, undefined, context);
}
处理 children vue/src/core/vdom/helpers/normalize-children.js
为啥要处理 children 呢?我理解是为了做新旧 vnode diff 的时候更快,也就是做一个优化,主要有以下两个优化点
- 相邻的两个 vnode 节点都是文本,那就可以合并成一个 vnode 节点
// merge adjacent text nodes
if (isTextNode(c[0]) && isTextNode(last)) {
res[lastIndex] = createTextVNode(last.text + (c[0]: any).text);
c.shift();
}
- 考虑这种情况
new Vue({
render: h => h(
'div'
[
[
'a',
'b',
],
'c'
]
)
})
'a b c’渲染到页面上应该是平级的,如果保留着这种结构的话会增大 diff 的难度。我理解以下两种情况会出现上面这样的结构
- 用户手写 render
- v-for(暂未验证)
_update vue/src/core/instance/lifecycle.js
render 之后已经得到了 vnode,剩下的就是把 vnode 变成 html 元素放到页面上,核心逻辑就是调用了__patch 方法
// fn _update
...
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
...
patch vue/src/core/vdom/patch.js
简单场景下(即 初始渲染vm.__patch__(vm.$el, vnode, hydrating, false )主要流程为:
- 把
$el 元素当做一个空 vnode - 获取改 vnode 的父 element,相邻 element
createElm() removeVnodes([oldVnode], 0, 0) 清除 oldvnode,如果 oldvnode 有任何 remove hook 的话要执行一次。在这步执行之前页面是有 oldvnode、vnode 两个元素的- 执行
invokeInsertHook (insertedVnodeQueue 记录了所有要 insert 的 vnode,这里执行 vnode 的 insert hook)
oldVnode = emptyNodeAt(oldVnode);
const oldElm = oldVnode.elm;
const parentElm = nodeOps.parentNode(oldElm);
// 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 : parentElm,
nodeOps.nextSibling(oldElm)
);
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0);
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
createElm vue/src/core/vdom/patch.js
// 如果事组件的话执行执行createComponent,到此结束
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
if (isDef(tag)) {
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}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);
}
- vnode 是组件的话走组件相关的逻辑,执行结束(暂时不看)
- 如果是注释节点或者本文节点的话直接生成内容挂载到 vnode.elm 上,然后插入到页面上(页面展示出 vnode 元素)
- 如果是普通的
tag ,如 div
- 生成 html 元素挂载到 vnode.elm 上
createChildren() insert() ,挂载到页面上
注意:
createChildren 的逻辑很简单,就是把遍历 vnode 的 children,挨个执行 createElm(),所以是 children 先 insert 到 vnode 上,vnode 才 insert 到它的 vnode 上(这里也就是 document.body)- createElm 执行完成之后页面上是有 oldvnode vnode 两个节点的 html 元素的
|