vue是怎么初始化数据并挂载的?
vue初始化流程----模板渲染(挂载)
-
$mount()
方法工程源码:
src/platforms/web/entry-runtime-with-compiler.js
- 如果传入的 el 是字符串就查找 DOM 元素, 如果有就返回, 没有就警告并返回一个新创建的 div
- 如果传入的 el 不是字符串就直接返回 el (可以测试传入真实 DOM 元素 box)
- 然后调用了
mount()
方法并返回
Vue.prototype.$mount = function (el) { // 根据用户传入的 el 属性获取节点 el = el && query(el) let vm = this; //把节点放在 vm.$el 上方便后面使用 vm.$el = el; let options = vm.$options; let template /** * 编译权重: * 优先看有没有 render 函数, 如果有就直接用 * 如果没有 render 函数就看有没有 template 模板 * 如果都没有就直接获取 el 的 outerHTML 作为渲染模板 */ if (!options.render) { if (!options.template) { template = el.outerHTML } else { template = vm.$options.template } } if (template) { //用 template 生成 render 函数 let render = compileToFunctions(template) options.render = render } //调用 mount 方法开始渲染页面。 return mount(this, el) }
- 上面代码主要实现了 Vue 渲染过程中很重要的一步: 生成 render 函数
- 如果我们使用的 template 进行编写 HTML 代码, Vue 内部会把模板编译成 Vue 可识别的 render 函数, 如果有写 render 则省去编译过程
- 总结: 直接写 render 函数比在 template 中写代码的编译效率更高
query
方法function query (el) { // 传入了 `#box` 字符串 if (typeof el === 'string') { var selected = document.querySelector(el); if (!selected) { warn( 'Cannot find element: ' + el ); return document.createElement('div') } return selected } else { return el } }
-
mountComponent()
工程源码:
src/core/instance/lifecycle.js
- 开始准备挂载真实 DOM, 触发
beforeMount
钩子 - 创建渲染 Watcher, 渲染 Watcher 内部调用了
updateComponent
方法 - 真实 DOM 渲染完毕后触发
mounted
钩子
function mountComponent (vm, el) { // 渲染之前调用 beforeMount 生命周期 callHook(vm, 'beforeMount') // 定义一个更新渲染函数 (用来获取虚拟 DOM 后渲染真实 DOM) let updateComponent = () => { // 整个渲染周期最关键的一行 vm._update(vm._render()) } // 生成一个渲染 Watcher 每次页面依赖的数据更新后会调用 updateComponent 进行渲染 new Watcher(vm, updateComponent, () => {},{ before () { callHook(vm, 'beforeUpdate') } }, true) // 渲染真实 dom 结束后调用 mounted 生命周期 callHook(vm, 'mounted') }
- 开始准备挂载真实 DOM, 触发
-
Watcher()
工程源码:
src/core/observer/watcher.js
- Vue 初次渲染时
Watcher
内部调用了updateComponent
方法 (数据添加依赖我们后面说)
export class Watcher { constructor(vm,expOrFn,cb,options) { if (typeof expOrFn === 'function') { // 将 updateComponent 方法赋值给 getter this.getter = expOrFn } this.get(); } get() { pushTarget(this) let value // 使用 call 调用 updateComponent 方法 value = this.getter.call(this.vm, this.vm); popTarget() return value } }
- Vue 初次渲染时
-
触发
updateComponent()
方法, 内部的vm.update(vm._render())
就会执行工程源码:
src/core/instance/render.js
先看
_render()
方法- 取出
render()
函数 - 调用
render()
函数, 获取虚拟 DOM 后返回
Vue.prototype._render = function () { let vm = this // 取出 render 函数 let render = vm.$options.render; // 调用 render 函数得到虚拟 DOM return render.call(vm) }
- 取出
-
vm.update()
工程源码:
src/core/instance/lifecycle.js
- 获取旧的虚拟 DOM
- 如果没有说明首次渲染, 调用
patch()
传入根元素#box
渲染 - 如果有就说明是数据更新, 调用
patch()
传入旧的虚拟 DOM 和新的虚拟 DOM 进行 diff 对比更新
Vue.prototype._update = function (vnode) { let vm = this // 获取到上一次的虚拟 DOM 用于 diff 对比 const prevVnode = vm._vnode if (!prevVnode) { // 没有上次的虚拟 DOM, 说明是首次渲染 vm.$el = patch(vm.$el, vnode) } else { // 有虚拟 DOM 说明是数据更新驱动视图更新 vm.$el = patch(prevVnode, vnode) } // 保留虚拟 DOM vm._vnode = vnode }
-
patch()
工程源码:
src/core/vdom/patch.js
- 首次渲染会直接创建真实 DOM 并返回
- 如果有子元素会递归创建子元素
- 根据虚拟 DOM 记录的标签名 / 注释 / 文本节点来创建真实 DOM 元素
return function patch(el, vnode, hydrating, removeOnly) { // 判断有没有旧的虚拟 DOM 如果没有就进入 if if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) } // ... 省略其他不重要的代码 ... return vnode.elm } function createElm ( vnode, // 虚拟dom insertedVnodeQueue, parentElm, // 父节点 ) { // 查看元素 tag 是不是组件, 如果是组件就 return 不走这里, 去创建组件 if (createComponent(vnode, insertedVnodeQueue, parentElm)) { return } const data = vnode.data // 获取 data 数据 const children = vnode.children // 获取子元素 const tag = vnode.tag // 获取标签名 if (isDef(tag)) { // 创建真实 DOM vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) // 如果有子节点就递归创建子节点 createChildren(vnode, children, 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) } } function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { for (let i = 0; i < children.length; ++i) { // 创建子节点 createElm(children[i], insertedVnodeQueue, vnode.elm) } } }
总结
- 直接写
render
函数比在template
中写代码编译效率更高 render()
函数是用来创建虚拟 DOM 的_update
中调用的patch()
函数才是真正将虚拟 DOM 转成真实 DOM 的方法