vue相干的源码剖析最初一篇,模板编译和组件化
模板编译
模板编译的次要目标是将模板 (template) 转换为渲染函数 (render)
<div> <h1@click="handler">title</h1> <p>somecontent</p></div>
渲染函数 render
render(h){ return h('div',[ h('h1',{on{click:this.handler}},'title'), h('p','somecontent') ])}
模板编译的作用:
- Vue 2.x 应用 VNode 形容视图以及各种交互,用户本人编写 VNode 比较复杂
- 用户只须要编写相似 HTML 的代码 - Vue 模板,通过编译器将模板转换为返回 VNode 的render 函数
- .vue 文件会被 webpack 在构建的过程中转换成 render 函数
带编译器版本的 Vue.js 中,应用 template 或 el 的形式设置模板
例如:
<div id="app"> <h1>Vue<span>模板编译过程</span></h1> <p>{{ msg }}</p> <comp @myclick="handler"></comp> </div> <script src="../../dist/vue.js"></script> <script> Vue.component('comp', { template: '<div>I am a comp</div>' }) const vm = new Vue({ el: '#app', data: { msg: 'Hello compiler' }, methods: { handler () { console.log('test') } } }) console.log(vm.$options.render) </script>
编译出:
(function anonymous(){ with(this) { return _c( "div", { attrs: { id: "app" } }, [ _m(0), _v(""), _c("p", [_v(_s(msg))]), _v(""), _c("comp", { on: { myclick: handler } }), ], 1 ); } });
这里this指向了vm,也就是this._c ,vm._c
_c 是 createElement() 办法,定义的地位 instance/render.js 中
咱们通过我上一篇晓得 vm._c(生成虚构dom)是对编译生成的 render 进行渲染的办法,vm.$createElement(生成虚构dom)是对手写render 函数进行渲染的办法.
其余相干的渲染函数(_结尾的办法定义),在 instance/render-helps/index.js 中。
这里安利一个小工具:Vue Template Explorer
Vue 2.6 把模板编译成 render 函数的工具
Vue 3.0 beta 把模板编译成 render 函数的工具
模板编译的过程,次要是三点
- 解析
- 优化
- 生成
编译模板的入口:
src\platforms\web\entry-runtime-with-compiler.js
在调用$mount 挂载时 会调用compileToFunctions 把template转换成render函数(外部调用_c()生成虚构dom)。
上面就是调试compileToFunctions来看下生成渲染函数的过程.
- compileToFunctions: src\compiler\to-function.js
- complie(template, options):src\compiler\create-compiler.j
- baseCompile(template.trim(), finalOptions):src\compiler\index.js
parse:解析器将模板解析为形象语树 AST,只有将模板解析成 AST 后,能力基于它做优化或者生成代码字符串。
src\compiler\index.js
这里再提供一个工具能够查看转换进去的失去的 AST tree
vue AST 树 js对象
v-if/v-for 结构化指令只能在编译阶段解决,如果咱们要在 render 函数解决条件或循环只能应用js 中的 if 和 for
Vue.component('comp', { data:(){ return {msg:'mycomp'} }, render(h){ if(this.msg){ return h('div',this.msg) } return h('div','bar') } })
优化 - optimize
- 优化形象语法树,检测子节点中是否是纯动态节点
一旦检测到纯动态节点,例如:
- hello整体是动态节点
永远不会更改的节点
- 晋升为常量,从新渲染的时候不在从新创立节点
- 在 patch 的时候间接跳过动态子树
生成 - generate
把形象语法树生成字符串模式的 js 代码
AST 形象语法树很大,咱们不在这里做过多学习,次要学习理念.
组件化机制
- 组件化能够让咱们不便的把页面拆分成多个可重用的组件
- 组件是独立的,零碎内可重用,组件之间能够嵌套
- 有了组件能够像搭积木一样开发网页
从源码的角度来剖析 Vue 组件外部如何工作
- 组件实例的创立过程是从上而下
- 组件实例的挂载过程是从下而上
全局组件的定义
Vue.component('comp', { template: '<div>I am a comp</div>' })
Vue.component() 入口
创立组件的构造函数,挂载到 Vue 实例的 vm.options.component.componentName =Ctor
//src\core\global-api\index.js//注册Vue.directive()、Vue.component()、Vue.filter()initAssetRegisters(Vue)//src\core\global-api\assets.jsif(type==='component'&&isPlainObject(definition)){ definition.name=definition.name||id definition=this.options._base.extend(definition)}……//全局注册,存储资源并赋值// this.options['components']['comp']=Ctorthis.options[type+'s'][id]=definitionVue.options._base=VueVue.extend()
组件构造函数的创立
export function initExtend (Vue: GlobalAPI) { /** * Each instance constructor, including Vue, has a unique * cid. This enables us to create wrapped "child * constructors" for prototypal inheriance and cache them. */ Vue.cid = 0 let cid = 1 /** * Class inheritance */ Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} // Vue 构造函数 const Super = this const SuperId = Super.cid // 从缓存中加载组件的构造函数 const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { // 如果是开发环境验证组件的名称 validateComponentName(name) } const Sub = function VueComponent (options) { // 调用 _init() 初始化 this._init(options) } // 原型继承自 Vue Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ // 合并 options Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super // For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // allow further extension/mixin/plugin usage Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use // create asset registers, so extended classes // can have their private assets too. ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // enable recursive self-lookup // 把组件结构构造函数保留到 Ctor.options.components.comp = Ctor if (name) { Sub.options.components[name] = Sub } // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // cache constructor // 把组件的构造函数缓存到 options._Ctor cachedCtors[SuperId] = Sub return Sub }}
组件的构造函数原型继承自vue,也执行了init办法,也就会调用$mount()办法,也会生成一个渲染watcher,也就验证了之前说的一个组件对应一个渲染watcher,这个组件构造函数被缓存到 options._Ctor。
前面生成vNode时,会依据Ctor创立组件的VNode.
组件创立和挂载
组件 VNode 的创立过程
- 创立根组件,首次 _render() 时,会失去整棵树的 VNode 构造
- 整体流程:new Vue() --> $mount() --> vm._render() --> createElement() --> createComponent()
- 创立组件的 VNode,初始化组件的 hook 钩子函数
生成组件的vnode时初始化了installComponentHooks钩子
这里调用了createComponentInstanceForVnode
这样咱们就能够串通了,生成虚构dom vnode时检测是组件createComponent 的时候,注册init钩子,而后前面调用init钩子时,
调用 createComponentInstanceForVnode 实例化组件,
newvnode.componentOptions.Ctor(options)
实例化完了调用:
child.$mount(hydrating?vnode.elm:undefined,hydrating)
调用组件对象的$mount(),生成渲染watcher,并把组件挂载到页面
组件实例的创立和挂载过程
Vue._update() --> patch() --> createElm() --> createComponent()
在patch的createElm时
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { // 调用 init() 办法,创立和挂载组件实例 // init() 的过程中创立好了组件的实在 DOM,挂载到了 vnode.elm 上 i(vnode, false /* hydrating */) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { // 调用钩子函数(VNode的钩子函数初始化属性/事件/款式等,组件的钩子函数) initComponent(vnode, insertedVnodeQueue) // 把组件对应的 DOM 插入到父元素中 insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
在这里咱们看到执行了init,init() 的过程中创立好了组件的实在 DOM,挂载到了 vnode.elm 上. 挂载到界面上,vnode.elm时对界面实在dom的援用.
这样联合咱们三篇,就能够看到vue的加载运行了,其实次要是后面两篇,
最初一篇只是在后面两篇,根底上加了组件在vNode中的渲染机会.
也就是说生成虚构dom时,判断是组件的状况时,把对应地位的vNode传递,而后调用组件继承自vue原型的init办法初始化,而后再调用$mount办法挂载到对应vNode.elm实在dom上.
好了,记得有点含糊.
有工夫再改下把.
本文内容借鉴于拉钩大前端训练营