参考答案:
我们都听过知其然知其所以然这句话
那么不知道大家是否思考过new Vue()
这个过程中究竟做了些什么?
过程中是如何完成数据的绑定,又是如何将数据渲染到视图的等等
首先找到vue
的构造函数
源码位置:src\core\instance\index.js
1function Vue (options) { 2 if (process.env.NODE_ENV !== 'production' && 3 !(this instanceof Vue) 4 ) { 5 warn('Vue is a constructor and should be called with the `new` keyword') 6 } 7 this._init(options) 8}
options
是用户传递过来的配置项,如data、methods
等常用的方法
vue
构建函数调用_init
方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法
1initMixin(Vue); // 定义 _init 2stateMixin(Vue); // 定义 $set $get $delete $watch 等 3eventsMixin(Vue); // 定义事件 $on $once $off $emit 4lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy 5renderMixin(Vue); // 定义 _render 返回虚拟dom
首先可以看initMixin
方法,发现该方法在Vue
原型上定义了_init
方法
源码位置:src\core\instance\init.js
1Vue.prototype._init = function (options?: Object) { 2 const vm: Component = this 3 // a uid 4 vm._uid = uid++ 5 let startTag, endTag 6 /* istanbul ignore if */ 7 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 8 startTag = `vue-perf-start:${vm._uid}` 9 endTag = `vue-perf-end:${vm._uid}` 10 mark(startTag) 11 } 12 13 // a flag to avoid this being observed 14 vm._isVue = true 15 // merge options 16 // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法 17 if (options && options._isComponent) { 18 // optimize internal component instantiation 19 // since dynamic options merging is pretty slow, and none of the 20 // internal component options needs special treatment. 21 initInternalComponent(vm, options) 22 } else { // 合并vue属性 23 vm.$options = mergeOptions( 24 resolveConstructorOptions(vm.constructor), 25 options || {}, 26 vm 27 ) 28 } 29 /* istanbul ignore else */ 30 if (process.env.NODE_ENV !== 'production') { 31 // 初始化proxy拦截器 32 initProxy(vm) 33 } else { 34 vm._renderProxy = vm 35 } 36 // expose real self 37 vm._self = vm 38 // 初始化组件生命周期标志位 39 initLifecycle(vm) 40 // 初始化组件事件侦听 41 initEvents(vm) 42 // 初始化渲染方法 43 initRender(vm) 44 callHook(vm, 'beforeCreate') 45 // 初始化依赖注入内容,在初始化data、props之前 46 initInjections(vm) // resolve injections before data/props 47 // 初始化props/data/method/watch/methods 48 initState(vm) 49 initProvide(vm) // resolve provide after data/props 50 callHook(vm, 'created') 51 52 /* istanbul ignore if */ 53 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 54 vm._name = formatComponentName(vm, false) 55 mark(endTag) 56 measure(`vue ${vm._name} init`, startTag, endTag) 57 } 58 // 挂载元素 59 if (vm.$options.el) { 60 vm.$mount(vm.$options.el) 61 } 62 }
仔细阅读上面的代码,我们得到以下结论:
在调用beforeCreate
之前,数据初始化并未完成,像data
、props
这些属性无法访问到
到了created
的时候,数据已经初始化完成,能够访问data
、props
这些属性,但这时候并未完成dom
的挂载,因此无法访问到dom
元素
挂载方法是调用vm.$mount
方法
initState
方法是完成props/data/method/watch/methods
的初始化
源码位置:src\core\instance\state.js
1export function initState (vm: Component) { 2 // 初始化组件的watcher列表 3 vm._watchers = [] 4 const opts = vm.$options 5 // 初始化props 6 if (opts.props) initProps(vm, opts.props) 7 // 初始化methods方法 8 if (opts.methods) initMethods(vm, opts.methods) 9 if (opts.data) { 10 // 初始化data 11 initData(vm) 12 } else { 13 observe(vm._data = {}, true /* asRootData */) 14 } 15 if (opts.computed) initComputed(vm, opts.computed) 16 if (opts.watch && opts.watch !== nativeWatch) { 17 initWatch(vm, opts.watch) 18 } 19}
我们和这里主要看初始化data
的方法为initData
,它与initState
在同一文件上
1function initData (vm: Component) { 2 let data = vm.$options.data 3 // 获取到组件上的data 4 data = vm._data = typeof data === 'function' 5 ? getData(data, vm) 6 : data || {} 7 if (!isPlainObject(data)) { 8 data = {} 9 process.env.NODE_ENV !== 'production' && warn( 10 'data functions should return an object:\n' + 11 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', 12 vm 13 ) 14 } 15 // proxy data on instance 16 const keys = Object.keys(data) 17 const props = vm.$options.props 18 const methods = vm.$options.methods 19 let i = keys.length 20 while (i--) { 21 const key = keys[i] 22 if (process.env.NODE_ENV !== 'production') { 23 // 属性名不能与方法名重复 24 if (methods && hasOwn(methods, key)) { 25 warn( 26 `Method "${key}" has already been defined as a data property.`, 27 vm 28 ) 29 } 30 } 31 // 属性名不能与state名称重复 32 if (props && hasOwn(props, key)) { 33 process.env.NODE_ENV !== 'production' && warn( 34 `The data property "${key}" is already declared as a prop. ` + 35 `Use prop default value instead.`, 36 vm 37 ) 38 } else if (!isReserved(key)) { // 验证key值的合法性 39 // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据 40 proxy(vm, `_data`, key) 41 } 42 } 43 // observe data 44 // 响应式监听data是数据的变化 45 observe(data, true /* asRootData */) 46}
仔细阅读上面的代码,我们可以得到以下结论:
初始化顺序:props
、methods
、data
data
定义的时候可选择函数形式或者对象形式(组件只能为函数形式)
关于数据响应式在这就不展开详细说明
上文提到挂载方法是调用vm.$mount
方法
源码位置:
1Vue.prototype.$mount = function ( 2 el?: string | Element, 3 hydrating?: boolean 4): Component { 5 // 获取或查询元素 6 el = el && query(el) 7 8 /* istanbul ignore if */ 9 // vue 不允许直接挂载到body或页面文档上 10 if (el === document.body || el === document.documentElement) { 11 process.env.NODE_ENV !== 'production' && warn( 12 `Do not mount Vue to <html> or <body> - mount to normal elements instead.` 13 ) 14 return this 15 } 16 17 const options = this.$options 18 // resolve template/el and convert to render function 19 if (!options.render) { 20 let template = options.template 21 // 存在template模板,解析vue模板文件 22 if (template) { 23 if (typeof template === 'string') { 24 if (template.charAt(0) === '#') { 25 template = idToTemplate(template) 26 /* istanbul ignore if */ 27 if (process.env.NODE_ENV !== 'production' && !template) { 28 warn( 29 `Template element not found or is empty: ${options.template}`, 30 this 31 ) 32 } 33 } 34 } else if (template.nodeType) { 35 template = template.innerHTML 36 } else { 37 if (process.env.NODE_ENV !== 'production') { 38 warn('invalid template option:' + template, this) 39 } 40 return this 41 } 42 } else if (el) { 43 // 通过选择器获取元素内容 44 template = getOuterHTML(el) 45 } 46 if (template) { 47 /* istanbul ignore if */ 48 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 49 mark('compile') 50 } 51 /** 52 * 1.将temmplate解析ast tree 53 * 2.将ast tree转换成render语法字符串 54 * 3.生成render方法 55 */ 56 const { render, staticRenderFns } = compileToFunctions(template, { 57 outputSourceRange: process.env.NODE_ENV !== 'production', 58 shouldDecodeNewlines, 59 shouldDecodeNewlinesForHref, 60 delimiters: options.delimiters, 61 comments: options.comments 62 }, this) 63 options.render = render 64 options.staticRenderFns = staticRenderFns 65 66 /* istanbul ignore if */ 67 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 68 mark('compile end') 69 measure(`vue ${this._name} compile`, 'compile', 'compile end') 70 } 71 } 72 } 73 return mount.call(this, el, hydrating) 74}
阅读上面代码,我们能得到以下结论:
不要将根元素放到body
或者html
上
可以在对象中定义template/render
或者直接使用template
、el
表示元素选择器
最终都会解析成render
函数,调用compileToFunctions
,会将template
解析成render
函数
对template
的解析步骤大致分为以下几步:
将html
文档片段解析成ast
描述符
将ast
描述符解析成字符串
生成render
函数
生成render
函数,挂载到vm
上后,会再次调用mount
方法
源码位置:src\platforms\web\runtime\index.js
1// public mount method 2Vue.prototype.$mount = function ( 3 el?: string | Element, 4 hydrating?: boolean 5): Component { 6 el = el && inBrowser ? query(el) : undefined 7 // 渲染组件 8 return mountComponent(this, el, hydrating) 9}
调用mountComponent
渲染组件
1export function mountComponent ( 2 vm: Component, 3 el: ?Element, 4 hydrating?: boolean 5): Component { 6 vm.$el = el 7 // 如果没有获取解析的render函数,则会抛出警告 8 // render是解析模板文件生成的 9 if (!vm.$options.render) { 10 vm.$options.render = createEmptyVNode 11 if (process.env.NODE_ENV !== 'production') { 12 /* istanbul ignore if */ 13 if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || 14 vm.$options.el || el) { 15 warn( 16 'You are using the runtime-only build of Vue where the template ' + 17 'compiler is not available. Either pre-compile the templates into ' + 18 'render functions, or use the compiler-included build.', 19 vm 20 ) 21 } else { 22 // 没有获取到vue的模板文件 23 warn( 24 'Failed to mount component: template or render function not defined.', 25 vm 26 ) 27 } 28 } 29 } 30 // 执行beforeMount钩子 31 callHook(vm, 'beforeMount') 32 33 let updateComponent 34 /* istanbul ignore if */ 35 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 36 updateComponent = () => { 37 const name = vm._name 38 const id = vm._uid 39 const startTag = `vue-perf-start:${id}` 40 const endTag = `vue-perf-end:${id}` 41 42 mark(startTag) 43 const vnode = vm._render() 44 mark(endTag) 45 measure(`vue ${name} render`, startTag, endTag) 46 47 mark(startTag) 48 vm._update(vnode, hydrating) 49 mark(endTag) 50 measure(`vue ${name} patch`, startTag, endTag) 51 } 52 } else { 53 // 定义更新函数 54 updateComponent = () => { 55 // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render 56 vm._update(vm._render(), hydrating) 57 } 58 } 59 // we set this to vm._watcher inside the watcher's constructor 60 // since the watcher's initial patch may call $forceUpdate (e.g. inside child 61 // component's mounted hook), which relies on vm._watcher being already defined 62 // 监听当前组件状态,当有数据变化时,更新组件 63 new Watcher(vm, updateComponent, noop, { 64 before () { 65 if (vm._isMounted && !vm._isDestroyed) { 66 // 数据更新引发的组件更新 67 callHook(vm, 'beforeUpdate') 68 } 69 } 70 }, true /* isRenderWatcher */) 71 hydrating = false 72 73 // manually mounted instance, call mounted on self 74 // mounted is called for render-created child components in its inserted hook 75 if (vm.$vnode == null) { 76 vm._isMounted = true 77 callHook(vm, 'mounted') 78 } 79 return vm 80}
阅读上面代码,我们得到以下结论:
boforeCreate
钩子updateComponent
渲染页面视图的方法beforeUpdate
生命钩子updateComponent
方法主要执行在vue
初始化时声明的render
,update
方法
render
的作用主要是生成vnode
源码位置:src\core\instance\render.js
1// 定义vue 原型上的render方法 2Vue.prototype._render = function (): VNode { 3 const vm: Component = this 4 // render函数来自于组件的option 5 const { render, _parentVnode } = vm.$options 6 7 if (_parentVnode) { 8 vm.$scopedSlots = normalizeScopedSlots( 9 _parentVnode.data.scopedSlots, 10 vm.$slots, 11 vm.$scopedSlots 12 ) 13 } 14 15 // set parent vnode. this allows render functions to have access 16 // to the data on the placeholder node. 17 vm.$vnode = _parentVnode 18 // render self 19 let vnode 20 try { 21 // There's no need to maintain a stack because all render fns are called 22 // separately from one another. Nested component's render fns are called 23 // when parent component is patched. 24 currentRenderingInstance = vm 25 // 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode 26 vnode = render.call(vm._renderProxy, vm.$createElement) 27 } catch (e) { 28 handleError(e, vm, `render`) 29 // return error render result, 30 // or previous vnode to prevent render error causing blank component 31 /* istanbul ignore else */ 32 if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { 33 try { 34 vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) 35 } catch (e) { 36 handleError(e, vm, `renderError`) 37 vnode = vm._vnode 38 } 39 } else { 40 vnode = vm._vnode 41 } 42 } finally { 43 currentRenderingInstance = null 44 } 45 // if the returned array contains only a single node, allow it 46 if (Array.isArray(vnode) && vnode.length === 1) { 47 vnode = vnode[0] 48 } 49 // return empty vnode in case the render function errored out 50 if (!(vnode instanceof VNode)) { 51 if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { 52 warn( 53 'Multiple root nodes returned from render function. Render function ' + 54 'should return a single root node.', 55 vm 56 ) 57 } 58 vnode = createEmptyVNode() 59 } 60 // set parent 61 vnode.parent = _parentVnode 62 return vnode 63}
_update
主要功能是调用patch
,将vnode
转换为真实DOM
,并且更新到页面中
源码位置:src\core\instance\lifecycle.js
1Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { 2 const vm: Component = this 3 const prevEl = vm.$el 4 const prevVnode = vm._vnode 5 // 设置当前激活的作用域 6 const restoreActiveInstance = setActiveInstance(vm) 7 vm._vnode = vnode 8 // Vue.prototype.__patch__ is injected in entry points 9 // based on the rendering backend used. 10 if (!prevVnode) { 11 // initial render 12 // 执行具体的挂载逻辑 13 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) 14 } else { 15 // updates 16 vm.$el = vm.__patch__(prevVnode, vnode) 17 } 18 restoreActiveInstance() 19 // update __vue__ reference 20 if (prevEl) { 21 prevEl.__vue__ = null 22 } 23 if (vm.$el) { 24 vm.$el.__vue__ = vm 25 } 26 // if parent is an HOC, update its $el as well 27 if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { 28 vm.$parent.$el = vm.$el 29 } 30 // updated hook is called by the scheduler to ensure that children are 31 // updated in a parent's updated hook. 32 }
new Vue
的时候调用会调用_init
方法
$set
、 $get
、$delete
、$watch
等方法$on
、$off
、$emit
、$off
等事件_update
、$forceUpdate
、$destroy
生命周期调用$mount
进行页面的挂载
挂载的时候主要是通过mountComponent
方法
定义updateComponent
更新函数
执行render
生成虚拟DOM
_update
将虚拟DOM
生成真实DOM
结构,并且渲染到页面中
最近更新时间:2024-08-10