You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/** * Runtime helper for rendering v-for lists. * 运行时渲染 v-for 列表的帮助函数,循环遍历 val 值,依次为每一项执行 render 方法生成 VNode,最终返回一个 VNode 数组 */exportfunctionrenderList(val: any,render: (val: any,keyOrIndex: string|number,index?: number)=>VNode): ?Array<VNode>{letret: ?Array<VNode>, i, l, keys,keyif(Array.isArray(val)||typeofval==='string'){// val 为数组或者字符串ret=newArray(val.length)for(i=0,l=val.length;i<l;i++){ret[i]=render(val[i],i)}}elseif(typeofval==='number'){// val 为一个数值,则遍历 0 - val 的所有数字
ret =newArray(val)for(i=0;i<val;i++){ret[i]=render(i+1,i)}}elseif(isObject(val)){// val 为一个对象,遍历对象if(hasSymbol&&val[Symbol.iterator]){// val 为一个可迭代对象ret=[]constiterator: Iterator<any> = val[Symbol.iterator]()
let result = iterator.next()
while (!result.done) {ret.push(render(result.value,ret.length))result=iterator.next()}}else{// val 为一个普通对象keys=Object.keys(val)ret=newArray(keys.length)for(i=0,l=keys.length;i<l;i++){
key =keys[i]ret[i]=render(val[key],key,i)}}}
if (!isDef(ret)) {ret=[]}
// 返回 VNode 数组
(ret: any)._isVList = true
return ret
}
/** * Runtime helper for rendering static trees. * 运行时负责生成静态树的 VNode 的帮助程序,完成了以下两件事 * 1、执行 staticRenderFns 数组中指定下标的渲染函数,生成静态树的 VNode 并缓存,下次在渲染时从缓存中直接读取(isInFor 必须为 true) * 2、为静态树的 VNode 打静态标记 * @param { number} index 表示当前静态节点的渲染函数在 staticRenderFns 数组中的下标索引 * @param { boolean} isInFor 表示当前静态节点是否被包裹在含有 v-for 指令的节点内部 */
export functionrenderStatic(index: number,isInFor: boolean): VNode|Array<VNode>{// 缓存,静态节点第二次被渲染时就从缓存中直接获取已缓存的 VNodeconst cached =this._staticTrees||(this._staticTrees=[])lettree=cached[index]// if has already-rendered static tree and not inside v-for,// we can reuse the same tree.// 如果当前静态树已经被渲染过一次(即有缓存)而且没有被包裹在 v-for 指令所在节点的内部,则直接返回缓存的 VNodeif(tree&&!isInFor){returntree}// 执行 staticRenderFns 数组中指定元素(静态树的渲染函数)生成该静态树的 VNode,并缓存// otherwise, render a fresh tree.tree=cached[index]=this.$options.staticRenderFns[index].call(this._renderProxy,null,this// for render fns generated for functional component templates)// 静态标记,为静态树的 VNode 打标记,即添加 { isStatic: true, key: `__static__${index}`, isOnce: false }markStatic(tree,`__static__${index}`,false)returntree}
_c
/src/core/instance/render.js
/** * 定义 _c,它是 createElement 的一个柯里化方法 * @param {*} a 标签名 * @param {*} b 属性的 JSON 字符串 * @param {*} c 子节点数组 * @param {*} d 节点的规范化类型 * @returns VNode or Array<VNode> */vm._c=(a,b,c,d)=>createElement(vm,a,b,c,d,false)
createElement
/src/core/vdom/create-element.js
/** * 生成组件或普通标签的 vnode,一个包装函数,不用管 * wrapper function for providing a more flexible interface * without getting yelled at by flow */
export functioncreateElement(context: Component,tag: any,data: any,children: any,normalizationType: any,alwaysNormalize: boolean): VNode|Array<VNode>{if(Array.isArray(data)||isPrimitive(data)){normalizationType=childrenchildren=datadata=undefined}if(isTrue(alwaysNormalize)){normalizationType=ALWAYS_NORMALIZE}// 执行 _createElement 方法创建组件的 VNodereturn_createElement(context,tag,data,children,normalizationType)}
_createElement
/src/core/vdom/create-element.js
/** * 生成 vnode, * 1、平台保留标签和未知元素执行 new Vnode() 生成 vnode * 2、组件执行 createComponent 生成 vnode * 2.1 函数式组件执行自己的 render 函数生成 VNode * 2.2 普通组件则实例化一个 VNode,并且在其 data.hook 对象上设置 4 个方法,在组件的 patch 阶段会被调用, * 从而进入子组件的实例化、挂载阶段,直至完成渲染 * @param {*} context 上下文 * @param {*} tag 标签 * @param {*} data 属性 JSON 字符串 * @param {*} children 子节点数组 * @param {*} normalizationType 节点规范化类型 * @returns VNode or Array<VNode> */
export function_createElement(context: Component,tag?: string|Class<Component>|Function|Object,data?: VNodeData,children?: any,normalizationType?: number): VNode|Array<VNode>{if(isDef(data)&&isDef((data: any).__ob__)){// 属性不能是一个响应式对象process.env.NODE_ENV!=='production'&&warn(`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n`+'Always create fresh vnode data objects in each render!',context)// 如果属性是一个响应式对象,则返回一个空节点的 VNodereturncreateEmptyVNode()}// object syntax in v-bindif(isDef(data)&&isDef(data.is)){
tag =data.is}if(!tag){// 动态组件的 is 属性是一个假值时 tag 为 false,则返回一个空节点的 VNode// in case of component :is set to falsy valuereturncreateEmptyVNode()}// 检测唯一键 key,只能是字符串或者数字// warn against non-primitive keyif(process.env.NODE_ENV!=='production'&&isDef(data)&&isDef(data.key)&&!isPrimitive(data.key)){if(!__WEEX__||!('@binding'indata.key)){warn('Avoid using non-primitive value as key, '+'use string/number value instead.',context)}}// 子节点数组中只有一个函数时,将它当作默认插槽,然后清空子节点列表// support single function children as default scoped slotif(Array.isArray(children)&&typeofchildren[0]==='function'){data=data||{}data.scopedSlots={default: children[0]}children.length=0}// 将子元素进行标准化处理if(normalizationType===ALWAYS_NORMALIZE){children=normalizeChildren(children)}elseif(normalizationType===SIMPLE_NORMALIZE){children=simpleNormalizeChildren(children)}/** * 这里开始才是重点,前面的都不需要关注,基本上是一些异常处理或者优化等 */letvnode,nsif(typeoftag==='string'){// 标签是字符串时,该标签有三种可能:// 1、平台保留标签// 2、自定义组件// 3、不知名标签letCtor// 命名空间ns=(context.$vnode&&context.$vnode.ns)||config.getTagNamespace(tag)if(config.isReservedTag(tag)){// tag 是平台原生标签// platform built-in elementsif(process.env.NODE_ENV!=='production'&&isDef(data)&&isDef(data.nativeOn)){// v-on 指令的 .native 只在组件上生效warn(`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,context)}// 实例化一个 VNodevnode=newVNode(config.parsePlatformTagName(tag),data,children,undefined,undefined,context)}elseif((!data||!data.pre)&&isDef(Ctor=resolveAsset(context.$options,'components',tag))){// tag 是一个自定义组件// 在 this.$options.components 对象中找到指定标签名称的组件构造函数// 创建组件的 VNode,函数式组件直接执行其 render 函数生成 VNode,// 普通组件则实例化一个 VNode,并且在其 data.hook 对象上设置了 4 个方法,在组件的 patch 阶段会被调用,// 从而进入子组件的实例化、挂载阶段,直至完成渲染// componentvnode=createComponent(Ctor,data,context,children,tag)}else{// 不知名的一个标签,但也生成 VNode,因为考虑到在运行时可能会给一个合适的名字空间// unknown or unlisted namespaced elements// check at runtime because it may get assigned a namespace when its// parent normalizes childrenvnode=newVNode(tag,data,children,undefined,undefined,context)}}else{// tag 为非字符串,比如可能是一个组件的配置对象或者是一个组件的构造函数// direct component options / constructorvnode=createComponent(tag,data,context,children)}// 返回组件的 VNodeif(Array.isArray(vnode)){returnvnode}elseif(isDef(vnode)){if(isDef(ns))applyNS(vnode,ns)if(isDef(data))registerDeepBindings(data)returnvnode}else{returncreateEmptyVNode()}}
createComponent
/src/core/vdom/create-component.js
/** * 创建组件的 VNode, * 1、函数式组件通过执行其 render 方法生成组件的 VNode * 2、普通组件通过 new VNode() 生成其 VNode,但是普通组件有一个重要操作是在 data.hook 对象上设置了四个钩子函数, * 分别是 init、prepatch、insert、destroy,在组件的 patch 阶段会被调用, * 比如 init 方法,调用时会进入子组件实例的创建挂载阶段,直到完成渲染 * @param {*} Ctor 组件构造函数 * @param {*} data 属性组成的 JSON 字符串 * @param {*} context 上下文 * @param {*} children 子节点数组 * @param {*} tag 标签名 * @returns VNode or Array<VNode> */
export functioncreateComponent(Ctor: Class<Component>|Function|Object|void,data: ?VNodeData,context: Component,children: ?Array<VNode>,tag?: string): VNode|Array<VNode>|void{// 组件构造函数不存在,直接结束if(isUndef(Ctor)){return}// Vue.extendconstbaseCtor=context.$options._base// 当 Ctor 为配置对象时,通过 Vue.extend 将其转为构造函数// plain options object: turn it into a constructorif(isObject(Ctor)){
Ctor =baseCtor.extend(Ctor)}// 如果到这个为止,Ctor 仍然不是一个函数,则表示这是一个无效的组件定义// if at this stage it's not a constructor or an async component factory,// reject.if(typeofCtor!=='function'){if(process.env.NODE_ENV!== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // 异步组件 // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor) if (Ctor === undefined) { // 为异步组件返回一个占位符节点,组件被渲染为注释节点,但保留了节点的所有原始信息,这些信息将用于异步服务器渲染 和 hydration return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } // 节点的属性 JSON 字符串 data = data || {} // 这里其实就是组件做选项合并的地方,即编译器将组件编译为渲染函数,渲染时执行 render 函数,然后执行其中的 _c,就会走到这里了 // 解析构造函数选项,并合基类选项,以防止在组件构造函数创建后应用全局混入 // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // 将组件的 v-model 的信息(值和回调)转换为 data.attrs 对象的属性、值和 data.on 对象上的事件、回调 // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // 提取 props 数据,得到 propsData 对象,propsData[key] = val // 以组件 props 配置中的属性为 key,父组件中对应的数据为 value // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // 函数式组件 // functional component if (isTrue(Ctor.options.functional)) { /** * 执行函数式组件的 render 函数生成组件的 VNode,做了以下 3 件事: * 1、设置组件的 props 对象 * 2、设置函数式组件的渲染上下文,传递给函数式组件的 render 函数 * 3、调用函数式组件的 render 函数生成 vnode */ return createFunctionalComponent(Ctor, propsData, data, context, children) } // 获取事件监听器对象 data.on,因为这些监听器需要作为子组件监听器处理,而不是 DOM 监听器 // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // 将带有 .native 修饰符的事件对象赋值给 data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // 如果是抽象组件,则值保留 props、listeners 和 slot // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } /** * 在组件的 data 对象上设置 hook 对象, * hook 对象增加四个属性,init、prepatch、insert、destroy, * 负责组件的创建、更新、销毁,这些方法在组件的 patch 阶段会被调用 * install component management hooks onto the placeholder node */ installComponentHooks(data) const name = Ctor.options.name || tag // 实例化组件的 VNode,对于普通组件的标签名会比较特殊,vue-component-${cid}-${name} const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// Weex specific: invoke recycle-list optimized @render function for
// extracting cell-slot template.
// https://github.com/Hanks10100/weex-native-directive/tree/master/component
/* istanbul ignore if */
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
resolveConstructorOptions
/src/core/instance/init.js
/** * 从构造函数上解析配置项 */exportfunctionresolveConstructorOptions(Ctor: Class<Component>){// 从实例构造函数上获取选项let options =Ctor.optionsif(Ctor.super){const superOptions =resolveConstructorOptions(Ctor.super)// 缓存constcachedSuperOptions=Ctor.superOptionsif(superOptions!==cachedSuperOptions){// 说明基类的配置项发生了更改// super option changed,// need to resolve new options.
Ctor.superOptions=superOptions// check if there are any late-modified/attached options (#4976)// 找到更改的选项constmodifiedOptions=resolveModifiedOptions(Ctor)// update base extend optionsif(modifiedOptions){// 将更改的选项和 extend 选项合并extend(Ctor.extendOptions,modifiedOptions)}// 将新的选项赋值给 optionsoptions=Ctor.options=mergeOptions(superOptions,Ctor.extendOptions)if(options.name){options.components[options.name]=Ctor}}}returnoptions}
/** * 将组件的 v-model 的信息(值和回调)转换为 data.attrs 对象的属性、值和 data.on 对象上的事件、回调 * transform component v-model info (value and callback) into * prop and event handler respectively. */functiontransformModel(options,data: any){// model 的属性和事件,默认为 value 和 inputconstprop=(options.model&&options.model.prop)||'value'constevent=(options.model&&options.model.event)||'input'// 在 data.attrs 对象上存储 v-model 的值;(data.attrs||(data.attrs={}))[prop]=data.model.value// 在 data.on 对象上存储 v-model 的事件conston=data.on||(data.on={})// 已存在的事件回调函数constexisting=on[event]// v-model 中事件对应的回调函数constcallback=data.model.callback// 合并回调函数if(isDef(existing)){if(Array.isArray(existing)
? existing.indexOf(callback)===-1
: existing!==callback){on[event]=[callback].concat(existing)}}else{on[event]=callback}}
extractPropsFromVNodeData
/src/core/vdom/helpers/extract-props.js
/** * <comp :msg="hello vue"></comp> * * 提取 props,得到 res[key] = val * * 以 props 配置中的属性为 key,父组件中对应的的数据为 value * 当父组件中数据更新时,触发响应式更新,重新执行 render,生成新的 vnode,又走到这里 * 这样子组件中相应的数据就会被更新 */exportfunctionextractPropsFromVNodeData(data: VNodeData,// { msg: 'hello vue' }Ctor: Class<Component>,// 组件构造函数tag?: string// 组件标签名): ?Object{// 组件的 props 选项,{ props: { msg: { type: String, default: xx } } }// 这里只提取原始值,验证和默认值在子组件中处理// we are only extracting raw values here.// validation and default values are handled in the child// component itself.constpropOptions=Ctor.options.propsif(isUndef(propOptions)){// 未定义 props 直接返回return}// 以组件 props 配置中的属性为 key,父组件传递下来的值为 value// 当父组件中数据更新时,触发响应式更新,重新执行 render,生成新的 vnode,又走到这里// 这样子组件中相应的数据就会被更新constres={}const{ attrs, props }=dataif(isDef(attrs)||isDef(props)){// 遍历 propsOptionsfor(constkeyinpropOptions){// 将小驼峰形式的 key 转换为 连字符 形式constaltKey=hyphenate(key)// 提示,如果声明的 props 为小驼峰形式(testProps),但由于 html 不区分大小写,所以在 html 模版中应该使用 test-props 代替 testPropsif(process.env.NODE_ENV!=='production'){constkeyInLowerCase=key.toLowerCase()if(key!==keyInLowerCase&&attrs&&hasOwn(attrs,keyInLowerCase)){tip(`Prop "${keyInLowerCase}" is passed to component `+`${formatComponentName(tag||Ctor)}, but the declared prop name is`+` "${key}". `+`Note that HTML attributes are case-insensitive and camelCased `+`props need to use their kebab-case equivalents when using in-DOM `+`templates. You should probably use "${altKey}" instead of "${key}".`)}}checkProp(res,props,key,altKey,true)||checkProp(res,attrs,key,altKey,false)}}returnres}
Vue 源码解读(11)—— render helper
当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注、点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
封面
前言
上一篇文章 Vue 源码解读(10)—— 编译器 之 生成渲染函数 最后讲到组件更新时,需要先执行编译器生成的渲染函数得到组件的 vnode。
渲染函数之所以能生成 vnode 是通过其中的
_c、_l、、_v、_s
等方法实现的。比如:普通的节点被编译成了可执行 _c 函数
v-for 节点被编译成了可执行的 _l 函数
...
但是到目前为止我们都不清楚这些方法的原理,它们是如何生成 vnode 的?只知道它们是 Vue 实例方法,今天我们就从源码中找答案。
目标
在 Vue 编译器的基础上,进一步深入理解一个组件是如何通过这些运行时的工具方法(render helper)生成 VNode 的
源码解读
入口
我们知道这些方法是 Vue 实例方法,按照之前对源码的了解,实例方法一般都放在
/src/core/instance
目录下。其实之前在 Vue 源码解读(6)—— 实例方法 阅读中见到过render helper
,在文章的最后。installRenderHelpers
_o = markOnce
markStatic
markStaticNode
_l = renderList
_m = renderStatic
_c
createElement
_createElement
createComponent
resolveConstructorOptions
resolveModifiedOptions
transformModel
extractPropsFromVNodeData
checkProp
createFunctionalComponent
installComponentHooks
componentVNodeHooks
createComponentInstanceForVnode
总结
面试官 问:一个组件是如何变成 VNode?
答:
组件实例初始化,最后执行 $mount 进入挂载阶段
如果是只包含运行时的 vue.js,只直接进入挂载阶段,因为这时候的组件已经变成了渲染函数,编译过程通过模块打包器 + vue-loader + vue-template-compiler 完成的
如果没有使用预编译,则必须使用全量的 vue.js
挂载时如果发现组件配置项上没有 render 选项,则进入编译阶段
将模版字符串编译成 AST 语法树,其实就是一个普通的 JS 对象
然后优化 AST,遍历 AST 对象,标记每一个节点是否为静态静态;然后再进一步标记出静态根节点,在组件后续更新时会跳过这些静态节点的更新,以提高性能
接下来从 AST 生成渲染函数,生成的渲染函数有两部分组成:
负责生成动态节点 VNode 的 render 函数
还有一个 staticRenderFns 数组,里面每一个元素都是一个生成静态节点 VNode 的函数,这些函数会作为 render 函数的组成部分,负责生成静态节点的 VNode
接下来将渲染函数放到组件的配置对象上,进入挂载阶段,即执行 mountComponent 方法
最终负责渲染组件和更新组件的是一个叫 updateComponent 方法,该方法每次执行前首先需要执行 vm._render 函数,该函数负责执行编译器生成的 render,得到组件的 VNode
将一个组件生成 VNode 的具体工作是由 render 函数中的
_c、_o、_l、_m
等方法完成的,这些方法都被挂载到 Vue 实例上面,负责在运行时生成组件 VNode_c,负责生成组件或 HTML 元素的 VNode,_c 是所有 render helper 方法中最复杂,也是最核心的一个方法,其它的 _xx 都是它的组成部分
接收标签、属性 JSON 字符串、子节点数组、节点规范化类型作为参数
如果标签是平台保留标签或者一个未知的元素,则直接
new VNode(标签信息)
得到 VNode如果标签是一个组件,则执行 createComponent 方法生成 VNode
函数式组件执行自己的 render 函数生成 VNode
普通组件则实例化一个 VNode,并且在在 data.hook 对象上设置 4 个方法,在组件的 patch 阶段会被调用,从而进入子组件的实例化、挂载阶段,然后进行编译生成渲染函数,直至完成渲染
当然生成 VNode 之前会进行一些配置处理比如:
子组件选项合并,合并全局配置项到组件配置项上
处理自定义组件的 v-model
处理组件的 props,提取组件的 props 数据,以组件的 props 配置中的属性为 key,父组件中对应的数据为 value 生成一个 propsData 对象;当组件更新时生成新的 VNode,又会进行这一步,这就是 props 响应式的原理
处理其它数据,比如监听器
安装内置的 init、prepatch、insert、destroy 钩子到 data.hooks 对象上,组件 patch 阶段会用到这些钩子方法
_l,运行时渲染 v-for 列表的帮助函数,循环遍历 val 值,依次为每一项执行 render 方法生成 VNode,最终返回一个 VNode 数组
_m,负责生成静态节点的 VNode,即执行 staticRenderFns 数组中指定下标的函数
简单总结 render helper 的作用就是:在 Vue 实例上挂载一些运行时的工具方法,这些方法用在编译器生成的渲染函数中,用于生成组件的 VNode。
好了,到这里,一个组件从初始化开始到最终怎么变成 VNode 就讲完了,最后剩下的就是 patch 阶段了,下一篇文章将讲述如何将组件的 VNode 渲染到页面上。
链接
感谢各位的:关注、点赞、收藏和评论,我们下期见。
当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注、 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
The text was updated successfully, but these errors were encountered: