vue实现的简单过程
new Vue() -> init -> $mount -> complie -> render -> vnode -> patch -> dom
- 往vue实例上添加方法。new Vue时直接执行 _init方法,初始化data props methods 等参数
- 执行$mount操作: 执行render函数 -> 调用createElement -> createElement中return new vnode实例 -》 返回深层次vnode树
- 执行update方法:如果是第一次,递归调用createElm生成真是节点,直接插入整个dom;如果更新,那么要进行diff算法比较,patch方法判断哪些vnode才需要插入
一、初始化参数,定义函数
- 首先往实例上添加了render、update等方法
- 实例化过程执行_init()方法,往实例上添加属性、初始化data ,将data绑定到实例vm上,创建observer实例,全局监听data,为每个属性创建一个dep实例,Object.defineProperty 添加get 方法, set方法(研究响应式原理会重点分析)
function Vue (options) {
this._init(options) // 实例化后直接执行
}
// 往vue实例上添加方法
initMixin(Vue) // 添加 _init方法
// stateMixin(Vue) // stateMixin.js 中,添加 $set $watch 方法等
// eventsMixin(Vue) // events.js 中,添加 $on $once $emit 方法等
renderMixin(Vue) // 添加 _render方法 $nextTick方法
lifecycleMixin(Vue) // 添加 _update $forceUpdate 方法等
// 添加 _init方法
function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this
vm.$options = options
vm._self = vm
initRender(vm) // 往实例上添加属性,如 $createElement
initState(vm) // 初始化 props, method, data, watch
vm.$mount(vm.$option.el) //挂载元素
}
}
//添加 _render方法
function renderMixin (Vue) {
Vue.prototype._render = function () {
const vm = this
const { render } = vm.$options
let vnode = render(vm.$createElement) // new Vue 中的 render 执行
console.log(32323, vnode)
return vnode // 返回 vnode
}
}
// 添加 _update
function lifecycleMixin (Vue) {
// 主要用来将 _render 返回的 vnode ,运用 diff 算法对比,适当的添加到 dom 中
Vue.prototype._update = function (vnode) {
// 这里将 _render 返回的 vnode 插入到 dom中
// 如果第一次,则直接插入整个 vnode ;如果是更新,那么要进行 diff 算法比较,判断哪些 vnode 才需要插入,这里先不研究
const vm = this
const prevVnode = vm._vnode;//之前旧的vnode
if (!prevVnode) { // 直接插入 vnode
createElm(vnode, vm.$el)
} else { // diff 算法比较
// vm.patch()
}
}
}
// 往实例上添加属性,如 $createElement
function initRender (vm) {
vm.$createElement = createElement
}
//初始化 props,method,data,watch
function initState (vm) {
let data = vm._data = vm.$options.data
if (data) {
// 之后会将data中的属性绑定到实例上
// observer dep ...
}
}
二、挂载元素
1. 获取render函数
挂载元素阶段会执行render函数, 生成深层次的vnode树,render函数从何而来的呢?获得render通常有两种方式:
第一种就是,手写render,创建vue实例的时候,不用template的方式写,直接手写render
第二种就是,将template转换成render函数,我们平时写的实际上是一个字符串
// main.js @vue/cli 4.5.0
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
// main.js
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})
@vue/cli 4.5.0 版本创建的项目中,已经直接写成了render函数的形式
那么,是怎么转换成render函数的呢?这里不做深究,只知道有两种方式可以做到即可:
- webpack @vue/cli 创建的项目,template写在 .vue文件中,可以借助vue-loader转换
- 用正则去匹配字符串,找出一个个的标识符,不断往后移动,生成 ast 树,像babel等工具都有类似的
vue源码中有这个 compile 的工具函数 compileToFunctions ,之后会去分析这个函数,转换之后的形式是
// 有些地方会把 createElement 写为 _c ,或者 h ,实际上都是同一个函数 createElement
render (createElement) {
return createElement('div', { attr: { id: 'container' } }, [
createElement('p', { class: 'nav' }, [
createElement('span', {key: 'index'}, 'aaa,'),
createElement('span', 'bbb')
]),
createElement('a', { attr: { href: 'www.baidu.com' } }, '百度')
])
}
顺便提一下Vue 开发有两种方式:
Runtime Only 和 Runtime+Compiler
可以理解为,
- Runtime+Compiler: Compiler 就是带有compileToFunctions 函数的功能,输入template字符串直接能转成 render,所以Runtime+Compiler属于完整版,支持你随便、任何条件下写各种 template
- Runtime Only : Vue中没有 Compiler 功能,也就是说没有compileToFunctions 函数去将 template 转成 render ,那么只能寄托于:
- 直接手写 render ,越过转换的步骤
- 利用 webpack loader ,在开发电脑上帮你转换 .vue 文件,打包后生成的一堆 js 文件中,全部变为了 render ,不含有 template 字符串了。
获取到了render函数之后,接下来就是执行render函数了
2. 执行render函数,返回vnode
render函数里面调用了createElement函数,最终返回带层级的vnode树
// 定义 $mount 挂载方法,实际上是在其他文件定义的,因为和跨平台有关,所以可能存在不同的 $mount
Vue.prototype.$mount = function (el) {
const vm = this
vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body
const options = vm.$options;
if (!options.render) {
// 如果不存在 render ,那么就需要 compileToFunctions 编译 render了,这里我们刚好跳过了。。。
// vm.options.render = compileToFunctions(vm.options.template)
}
// 继续
mountComponent(vm) // 在 lifecycle.js 中定义的
}
// lifecycle.js
function mountComponent (vm) {
// callHook(vm, 'beforeMount') // 生命周期
let updateComponent = function () {
vm._update(vm._render()) // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
}
updateComponent()
}
// 判断 tag ,创建不同的 vnode
function createElement (tag, data, children) {
if (typeof data !== 'object') {
children = data
data = undefined
}
let vnode
if (typeof tag === 'string') { // 认为是标签
vnode = new VNode(tag, data, children)
} else { // 认为是子组件
// vnode = createComponent(Ctor, data, context, children, tag);
}
return vnode
}
// VNode 类
class VNode {
constructor (tag, data, children) {
this.tag = tag
this.data = data
this.children = children
}
}
三. 执行_update方法,将vnode渲染到dom中
如果第一次,递归调用 createElm 生成真实节点并插入到 dom 中;如果是更新,那么要进行 diff 算法比较(研究diff算法时再看)
// lifecycle.js
function mountComponent (vm) {
// callHook(vm, 'beforeMount') // 生命周期
let updateComponent = function () {
vm._update(vm._render()) // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
}
updateComponent()
}
// 添加 _update
function lifecycleMixin (Vue) {
// 主要用来将 _render 返回的 vnode ,运用 diff 算法对比,适当的添加到 dom 中
Vue.prototype._update = function (vnode) {
// 这里将 _render 返回的 vnode 插入到 dom中
// 如果第一次,则直接插入整个 vnode ;如果是更新,那么要进行 diff 算法比较,判断哪些 vnode 才需要插入,这里先不研究
const vm = this
const prevVnode = vm._vnode;//之前旧的vnode
if (!prevVnode) { // 直接插入 vnode
createElm(vnode, vm.$el)
} else { // diff 算法比较
// vm.patch()
}
}
}
// patch.js
function createElm (vnode, el) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
console.log(32, vnode)
if (tag) { // 因为下面 createChildren 递归时,children 也会作为 vnode 参数传进来,children 可能是字符串时就不存在 .tag (比如span标签内的纯文本,也是span的children)
vnode.elm = document.createElement(tag) // 创建了真实节点
createChildren(vnode, children) // 遍历children,递归调用 createElm ,不断插入节点
insert(el, vnode.elm) // 插入该节点
}
}
function createChildren (vnode, children) {
if (Array.isArray(children)) {
for(let i = 0;i < children.length;i++) {
createElm(children[i], vnode.elm) // 传入父节点
}
} else { // children 是纯文本,比如span标签内的文字
insert(vnode.elm, document.createTextNode(children))
}
}
function insert (parentEl, elm) {
parentEl.appendChild(elm)
}
简化代码
function Vue (options) {
this._init(options) // 实例化后直接执行
}
// 往vue实例上添加方法
initMixin(Vue) // 添加 _init方法
renderMixin(Vue) // 添加 _render方法 $nextTick方法
lifecycleMixin(Vue) // 添加 _update $forceUpdate 方法等
// 定义 $mount 挂载方法,实际上是在其他文件定义的,因为和跨平台有关,所以可能存在不同的 $mount
Vue.prototype.$mount = function (el) {
const vm = this
vm.$el = typeof el === 'string' ? document.querySelector(el) : document.body
const options = vm.$options;
if (!options.render) {
// 如果不存在 render ,那么就需要 compileToFunctions 编译 render了,这里我们刚好跳过了。。。
// vm.options.render = compileToFunctions(vm.options.template)
}
// 继续
mountComponent(vm) // 在 lifecycle.js 中定义的
}
// 添加 _init方法
function initMixin(Vue) {
Vue.prototype._init = function (options) {
const vm = this
vm.$options = options
vm._self = vm
initRender(vm) // 往实例上添加属性,如 $createElement
initState(vm) // 初始化 props, method, data, watch
vm.$mount(vm.$option.el) //挂载元素
}
}
// lifecycle.js
function mountComponent (vm) {
// callHook(vm, 'beforeMount') // 生命周期
let updateComponent = function () {
vm._update(vm._render()) // 执行 _render()获得 vnode,再执行 _update 渲染到 dom 中
}
updateComponent()
}
// 往实例上添加属性,如 $createElement
function initRender (vm) {
vm.$createElement = createElement
}
//初始化 props,method,data,watch
function initState (vm) {
let data = vm._data = vm.$options.data
if (data) {
// 之后会将data中的属性绑定到实例上
// observer dep ...
}
}
//添加 _render方法
function renderMixin (Vue) {
Vue.prototype._render = function () {
const vm = this
const { render } = vm.$options
let vnode = render(vm.$createElement) // new Vue 中的 render 执行
console.log(32323, vnode)
return vnode // 返回 vnode
}
}
// 判断 tag ,创建不同的 vnode
function createElement (tag, data, children) {
if (typeof data !== 'object') {
children = data
data = undefined
}
let vnode
if (typeof tag === 'string') { // 认为是标签
vnode = new VNode(tag, data, children)
} else { // 认为是子组件
// vnode = createComponent(Ctor, data, context, children, tag);
}
return vnode
}
// VNode 类
class VNode {
constructor (tag, data, children) {
this.tag = tag
this.data = data
this.children = children
}
}
// 添加 _update
function lifecycleMixin (Vue) {
// 主要用来将 _render 返回的 vnode ,运用 diff 算法对比,适当的添加到 dom 中
Vue.prototype._update = function (vnode) {
// 这里将 _render 返回的 vnode 插入到 dom中
// 如果第一次,则直接插入整个 vnode ;如果是更新,那么要进行 diff 算法比较,判断哪些 vnode 才需要插入,这里先不研究
const vm = this
const prevVnode = vm._vnode;//之前旧的vnode
if (!prevVnode) { // 直接插入 vnode
createElm(vnode, vm.$el)
} else { // diff 算法比较
// vm.patch()
}
}
}
// patch.js
function createElm (vnode, el) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
console.log(32, vnode)
if (tag) { // 因为下面 createChildren 递归时,children 也会作为 vnode 参数传进来,children 可能是字符串时就不存在 .tag (比如span标签内的纯文本,也是span的children)
vnode.elm = document.createElement(tag) // 创建了真实节点
createChildren(vnode, children) // 遍历children,递归调用 createElm ,不断插入节点
insert(el, vnode.elm) // 插入该节点
}
}
function createChildren (vnode, children) {
if (Array.isArray(children)) {
for(let i = 0;i < children.length;i++) {
createElm(children[i], vnode.elm) // 传入父节点
}
} else { // children 是纯文本,比如span标签内的文字
insert(vnode.elm, document.createTextNode(children))
}
}
function insert (parentEl, elm) {
parentEl.appendChild(elm)
}