说说 VNode 节点(Vue.js 实现) - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a Javascript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
Javascript 权威指南第 5 版
Closure: The Definitive Guide
answershuto
V2EX    Javascript

说说 VNode 节点(Vue.js 实现)

  •  
  •   answershuto 2017-09-10 21:36:26 +08:00 8766 次点击
    这是一个创建于 3008 天前的主题,其中的信息可能已经有所发展或是发生改变。

    写在前面

    因为对 Vue.js 很感兴趣,而且平时工作的技术栈也是 Vue.js ,这几个月花了些时间研究学习了一下 Vue.js 源码,并做了总结与输出。 文章的原地址:https://github.com/answershuto/learnVue。 在学习过程中,为 Vue 加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习 Vue 源码的小伙伴有所帮助。 可能会有理解存在偏差的地方,欢迎提 issue 指出,共同学习,共同进步。

    抽象 Dom 树

    在刀耕火种的年代,我们需要在各个事件方法中直接操作 Dom 来达到修改视图的目的。但是当应用一大就会变得难以维护。

    那我们是不是可以把真实 Dom 树抽象成一棵以 Javascript 对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实 Dom 重绘到页面上呢?于是虚拟 Dom 出现了,它是真实 Dom 的一层抽象,用属性描述真实 Dom 的各个特性。当它发生变化的时候,就会去修改视图。

    但是这样的 Javascript 操作 Dom 进行重绘整个视图层是相当消耗性能的,我们是不是可以每次只更新它的修改呢?所以 Vue.js 将 Dom 抽象成一个以 Javascript 对象为节点的虚拟 Dom 树,以 VNode 节点模拟真实 Dom,可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实 Dom,只需要操作 Javascript 对象,大大提升了性能。修改以后经过 diff 算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样做减少了很多不需要的 Dom 操作,大大提高了性能。

    Vue 就使用了这样的抽象节点 VNode,它是对真实 Dom 的一层抽象,而不依赖某个平台,它可以是浏览器平台,也可以是 weex,甚至是 node 平台也可以对这样一棵抽象 Dom 树进行创建删除修改等操作,这也为前后端同构提供了可能。

    VNode 基类

    先来看一下 Vue.js 源码中对 VNode 类的定义。

    export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*当前节点的标签名*/ this.tag = tag /*当前节点对应的对象,包含了具体的一些数据信息,是一个 VNodeData 类型,可以参考 VNodeData 类型中的数据信息*/ this.data = data /*当前节点的子节点,是一个数组*/ this.children = children /*当前节点的文本*/ this.text = text /*当前虚拟节点对应的真实 dom 节点*/ this.elm = elm /*当前节点的名字空间*/ this.ns = undefined /*编译作用域*/ this.cOntext= context /*函数化组件作用域*/ this.functiOnalContext= undefined /*节点的 key 属性,被当作节点的标志,用以优化*/ this.key = data && data.key /*组件的 option 选项*/ this.compOnentOptions= componentOptions /*当前节点对应的组件的实例*/ this.compOnentInstance= undefined /*当前节点的父节点*/ this.parent = undefined /*简而言之就是是否为原生 HTML 或只是普通文本,innerHTML 的时候为 true,textContent 的时候为 false*/ this.raw = false /*静态节点标志*/ this.isStatic = false /*是否作为跟节点插入*/ this.isRootInsert = true /*是否为注释节点*/ this.isComment = false /*是否为克隆节点*/ this.isClOned= false /*是否有 v-once 指令*/ this.isOnce= false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next https://github.com/answershuto/learnVue*/ get child (): Component | void { return this.componentInstance } } 

    这是一个最基础的 VNode 节点,作为其他派生 VNode 类的基类,里面定义了下面这些数据。

    tag: 当前节点的标签名

    data: 当前节点对应的对象,包含了具体的一些数据信息,是一个 VNodeData 类型,可以参考 VNodeData 类型中的数据信息

    children: 当前节点的子节点,是一个数组

    text: 当前节点的文本

    elm: 当前虚拟节点对应的真实 dom 节点

    ns: 当前节点的名字空间

    context: 当前节点的编译作用域

    functionalContext: 函数化组件作用域

    key: 节点的 key 属性,被当作节点的标志,用以优化

    componentOptions: 组件的 option 选项

    componentInstance: 当前节点对应的组件的实例

    parent: 当前节点的父节点

    raw: 简而言之就是是否为原生 HTML 或只是普通文本,innerHTML 的时候为 true,textContent 的时候为 false

    isStatic: 是否为静态节点

    isRootInsert: 是否作为跟节点插入

    isComment: 是否为注释节点

    isCloned: 是否为克隆节点

    isOnce: 是否有 v-once 指令


    打个比方,比如说我现在有这么一个 VNode 树

    { tag: 'div' data: { class: 'test' }, children: [ { tag: 'span', data: { class: 'demo' } text: 'hello,VNode' } ] } 

    渲染之后的结果就是这样的

    <div class="test"> <span class="demo">hello,VNode</span> </div> 

    生成一个新的 VNode 的方法

    下面这些方法都是一些常用的构造 VNode 的方法。

    createEmptyVNode 创建一个空 VNode 节点

    /*创建一个空 VNode 节点*/ export const createEmptyVNode = () => { const node = new VNode() node.text = '' node.isComment = true return node } 

    createTextVNode 创建一个文本节点

    /*创建一个文本节点*/ export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val)) } 

    createComponent 创建一个组件节点

     // plain options object: turn it into a constructor https://github.com/answershuto/learnVue if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // if at this stage it's not a constructor or an async component factory, // reject. /*Github:https://github.com/answershuto*/ /*如果在该阶段 Ctor 依然不是一个构造函数或者是一个异步组件工厂则直接返回*/ if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component /*处理异步组件*/ if (isUndef(Ctor.cid)) { Ctor = resolveAsyncComponent(Ctor, baseCtor, context) if (Ctor === undefined) { // return nothing if this is indeed an async component // wait for the callback to trigger parent update. /*如果这是一个异步组件则会不会返回任何东西( undifiened ),直接 return 掉,等待回调函数去触发父组件更新。s*/ return } } // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) data = data || {} // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners onst listeners = data.on // replace with listeners with .native modifier data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners data = {} } // merge component management hooks onto the placeholder node mergeHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children } ) return vnode } 

    cloneVNode 克隆一个 VNode 节点

    export function cloneVNode (vnode: VNode): VNode { const clOned= new VNode( vnode.tag, vnode.data, vnode.children, vnode.text, vnode.elm, vnode.context, vnode.componentOptions ) cloned.ns = vnode.ns cloned.isStatic = vnode.isStatic cloned.key = vnode.key cloned.isClOned= true return cloned } 

    createElement

    // wrapper function for providing a more flexible interface // without getting yelled at by flow export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode { /*兼容不传 data 的情况*/ if (Array.isArray(data) || isPrimitive(data)) { normalizatiOnType= children children = data data = undefined } /*如果 alwaysNormalize 为 true,则 normalizationType 标记为 ALWAYS_NORMALIZE*/ if (isTrue(alwaysNormalize)) { normalizatiOnType= ALWAYS_NORMALIZE } /*Github:https://github.com/answershuto*/ /*创建虚拟节点*/ return _createElement(context, tag, data, children, normalizationType) } /*创建虚拟节点*/ export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode { /* 如果 data 未定义( undefined 或者 null )或者是 data 的__ob__已经定义(代表已经被 observed,上面绑定了 Oberver 对象), https://cn.vuejs.org/v2/guide/render-function.html#约束 那么创建一个空节点 */ 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 ) return createEmptyVNode() } /*如果 tag 不存在也是创建一个空节点*/ if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // support single function children as default scoped slot /*默认默认作用域插槽*/ if (Array.isArray(children) && typeof children[0] === 'function') { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizatiOnType=== ALWAYS_NORMALIZE) { children = normalizeChildren(children) } else if (normalizatiOnType=== SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { let Ctor /*获取 tag 的名字空间*/ ns = config.getTagNamespace(tag) /*判断是否是保留的标签*/ if (config.isReservedTag(tag)) { // platform built-in elements /*如果是保留的标签则创建一个相应节点*/ vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component /*从 vm 实例的 option 的 components 中寻找该 tag,存在则就是一个组件,创建相应节点,Ctor 为组件的构造类*/ vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children /*未知的元素,在运行时检查,因为父组件可能在序列化子组件的时候分配一个名字空间*/ vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor /*tag 不是字符串的时候则是组件的构造类*/ vnode = createComponent(tag, data, context, children) } if (isDef(vnode)) { /*如果有名字空间,则递归所有子节点应用该名字空间*/ if (ns) applyNS(vnode, ns) return vnode } else { /*如果 vnode 没有成功创建则创建空节点*/ return createEmptyVNode() } } 

    createElement 用来创建一个虚拟节点。当 data 上已经绑定__ob__的时候,代表该对象已经被 Oberver 过了,所以创建一个空节点。tag 不存在的时候同样创建一个空节点。当 tag 不是一个 String 类型的时候代表 tag 是一个组件的构造类,直接用 new VNode 创建。当 tag 是 String 类型的时候,如果是保留标签,则用 new VNode 创建一个 VNode 实例,如果在 vm 的 option 的 components 找得到该 tag,代表这是一个组件,否则统一用 new VNode 创建。

    7 条回复    2017-09-11 10:49:50 +08:00
    dcoder
        1
    dcoder  
       2017-09-11 01:18:24 +08:00
    支持一个
    codermagefox
        2
    codermagefox  
       2017-09-11 09:36:26 +08:00
    上次已经 Star 过了,这次只能顶一下了.
    sansansan333
        3
    sansansan333  
       2017-09-11 10:02:56 +08:00
    回看见'几个月'这三个字我就放心了,不然我觉得自己像一个傻子
    answershuto
        4
    answershuto  
    OP
       2017-09-11 10:25:03 +08:00
    @codermagefox 哈哈谢谢
    answershuto
        5
    answershuto  
    OP
       2017-09-11 10:25:30 +08:00
    @sansansan333 时间花下去了,总会有收获的哈~
    answershuto
        6
    answershuto  
    OP
       2017-09-11 10:25:49 +08:00
    @dcoder 3q
    66beta
        7
    66beta  
       2017-09-11 10:49:50 +08:00
    毫不留情地 star 了
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1921 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 16:12 PVG 00:12 LAX 08:12 JFK 11:12
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86