目前就职于新浪微博

0%

特别声明:本文未经许可禁止转载

提出问题

国庆逢中秋长假,回家休假,我妈吐槽家里小米智能电视,该智能的地方不智能(比如开机直接看电视),不该智能的地方超智能(比如随时随地提醒你开通小米影视会员、屏保结束就显示超亮眼的广告,做广告比做产品做的都用心),真的是,需要它的地方它不行,不需要的地方比谁都聪明。我也怀疑,我花钱买了一个小米电视,买来之后,这电视仍旧不是我家的,而是小米的,小米想放广告就放广告,想看电视直播?没门。白马非马,智能电视非电视。

问题调查以及解决

问题一:看不了电视直播

小米电视应用商店里面,是搜不到电视直播app的,经过一番搜索,使用U盘,安装成功,问题解决。

问题二:开机不能直接进电视直播

对于年轻人来说,开机显示小米桌面,可能无所谓。但是对于年纪稍大的人来说,就没有那么方便了。年纪稍大的人,本来能用手机就不错了,现在想看电视,都还要使用遥控器左右选择,进入“我的”,进入”我的应用“,选择电视直播的app,偶尔还要将烦人的购买影视会员弹窗取消掉,实在麻烦。

我买的小米电视(由于它老显示广告,所以我觉得目前它不是我家的电视,所以称它为我买的小米电视),内置的系统是Android 6.0,如果想Root后让电视直播app支持自启动,会比较麻烦,并且Root后,系统可能会不稳定,不适合我妈这种小白。经过一番调研,我放弃了Root的想法。

小米电视只允许它内置的应用自启动,其他应用不行,自启动的应用有:小米桌面,小爱同学(遥控器语音控制)等等。小米桌面是Android的主屏幕,小爱同学是使用的Android无障碍。那么,想让电视直播app自启动,就有了一点转机。

方案一:替换小米桌面。将小米桌面直接替换成其他的支持自定义的TV桌面,减少进入电视直播app的操作步骤。经过调研,此方案可行,也有人实施了,但是是不可逆的,因为小米电视桌面被设死了,无法通过系统设置更改主屏幕,需要把小米电视桌面直接替换掉,方案欠佳。

方案二:仿照小爱同学,走无障碍的路。自己写一个无障碍app(我命名为我家辅助)并启用,则小米电视开机时,我家辅助启动,再在我家辅助中,启动电视直播app的Activity,则实现了小米电视启动直接进入电视直播,方案完美。

问题延伸

使用Android的无障碍,走的是Android官方的路,不怕小米电视封杀。用无障碍实现这个开机进入电视直播的功能,实在是小事一桩。无障碍如果继续扩展,则还可以用于去广告,比如干掉烦人的小米影视会员弹窗。

特别声明:本文未经许可禁止转载

开头

  • 不能免俗,我也要做Vue源码解析了
  • 本文基于Vue 2.5.18-beta.0版本进行分析
  • 源码讲解的第一行附了源码路径,可供查看,我会在我认为重要的地方加注释
  • 如果有疑问,可以留言或者关注我公众号并发送消息
  • 本文通俗易懂,不人云亦云,应该都能看得懂

Vue Compiler:编译Vue模版

  • 先来看一下,开发者开发时的Vue代码结构
    1
    2
    3
    4
    5
    <template>
    <div>
    <span>Hello World</span>
    </div>
    </template>
  • 以上代码,经过Vue compiler处理之后,template会变成
    1
    _c("div", [_c("span", [_vm._v("Hello World")])])
  • 其中,_c就是createElement,定义在
    1
    2
    // vue/src/core/instance/render.js
    vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  • 看一下Vue compiler入口,可猜出该部分的功能
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // vue/src/compiler/index.js
    export const createCompiler = createCompilerCreator(function baseCompile (
    template: string,
    options: CompilerOptions
    ): CompiledResult {
    const ast = parse(template.trim(), options)
    if (options.optimize !== false) {
    optimize(ast, options)
    }
    const code = generate(ast, options)
    return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
    }
    })
  • 输入是模版string,输出是render函数,而这个render函数,将会被Vue-loader使用,将其声明到options中,后面Vue.prototype._render()函数中使用到到options.render(xx)函数,就是由此而来。另外Vue compiler还对某些Vue特性做了处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options // 1. 该render函数就是Vue compiler编译输出的

    if (_parentVnode) {
    vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
    // 2、生成vnode,供后面做VDOM DIFF时使用
    vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {

    handleError(e, vm, `render`)
    // return error render result,
    // or previous vnode to prevent render error causing blank component
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
    try {
    vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
    } catch (e) {
    handleError(e, vm, `renderError`)
    vnode = vm._vnode
    }
    } else {
    vnode = vm._vnode
    }
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
    if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
    warn(
    'Multiple root nodes returned from render function. Render function ' +
    'should return a single root node.',
    vm
    )
    }
    vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
    }

Vue Instance第一次渲染(总计有12个重要注释)

  • 一切要从new Vue(options)开始
    1
    2
    3
    4
    5
    6
    var app = new Vue({
    el: '#app',
    data: {
    message: 'Hello Vue!'
    }
    })
  • 其实在new Vue(options)之前,需要先考虑Vue Constructor的初始化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // vue/src/core/instance/index.js
    function Vue (options) {
    if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
    ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
    }
    this._init(options) // 1. new Vue(options)时执行
    // _init方法即Vue.prototype._init
    }
    // 2. 以下方法调用,就是在初始化Vue Constructor,从方法名不难猜出其功能
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)

    export default Vue
  • this._init(options)方法中,初始化Vue Instance,我们来看看_init方法的定义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    // vue/src/core/instance/init.js
    Vue.prototype._init = function (options?: Object) {
    // 3. 你可能奇怪javascript怎么会有类型声明,这是prop-types在编译时用到的,
    // 编译结束之后,就是纯碎的javascript代码了
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    startTag = `vue-perf-start:${vm._uid}`
    endTag = `vue-perf-end:${vm._uid}`
    mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
    } else {
    vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
    )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
    } else {
    vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 4、下面的以init开头的方法,都是根据options,给vm即this塞必要的属性方便开发时使用
    // 以call开头的方法,都是在触发Vue的声明周期
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    vm._name = formatComponentName(vm, false)
    mark(endTag)
    measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
    vm.$mount(vm.$options.el) // 5、$mount方法,就是mountComponent方法,定义在 // vue/src/core/instance/lifecycle.js中
    }
    }
  • 执行$mount方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    // vue/src/core/instance/lifecycle.js
    export function mountComponent (
    vm: Component, // 6、vm即this、即Vue Instance
    el: ?Element, // 7、new Vue(options)时传入的el
    hydrating?: boolean
    ): Component {
    vm.$el = el
    if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
    /* istanbul ignore if */
    if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
    vm.$options.el || el) {
    warn(
    'You are using the runtime-only build of Vue where the template ' +
    'compiler is not available. Either pre-compile the templates into ' +
    'render functions, or use the compiler-included build.',
    vm
    )
    } else {
    warn(
    'Failed to mount component: template or render function not defined.',
    vm
    )
    }
    }
    }
    callHook(vm, 'beforeMount')

    let updateComponent
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
    const name = vm._name
    const id = vm._uid
    const startTag = `vue-perf-start:${id}`
    const endTag = `vue-perf-end:${id}`

    mark(startTag)
    const vnode = vm._render()
    mark(endTag)
    measure(`vue ${name} render`, startTag, endTag)

    mark(startTag)
    vm._update(vnode, hydrating)
    mark(endTag)
    measure(`vue ${name} patch`, startTag, endTag)
    }
    } else {
    // 8、定义updateComponent方法传给Watcher,
    // 使用Watcher去watch options中定义的data,一旦data中的数据发生变动,则触发updateComponent
    updateComponent = () => {
    vm._update(vm._render(), hydrating)
    // 9、_render方法即Vue.prototype._render,定义在 // vue/src/core/instance/render.js,
    // 该方法调用了options中的render方法生成了vnode而vnode是VDOM的基础之一,而options中的render方法,则是在Vue Compile阶段
    // 根据开发者定义的Vue Template生成的
    // 10、_update方法即Vue.prototype._update,定义在 // vue/src/core/instance/lifecycle.js,
    该方法主要是执行patch方法,而patch方法第一个参数是preVnode,第二个参数是vnode。
    }
    }

    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined

    // 11、new Watcher(xxx)时,将会在Watcher Constructor中调用传入的updateComponent方法,
    因此,当new Watcher(xxx)执行完毕,DOM操作已经执行完成,页面已经渲染出来了
    new Watcher(vm, updateComponent, noop, {
    before () {
    if (vm._isMounted) {
    callHook(vm, 'beforeUpdate')
    }
    }
    }, true /* isRenderWatcher */)
    hydrating = false

    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted') // 12、第一次渲染完成,由此可以得出结论,Vue是以组件为单位进行watch的
    // 那么,如果合理的对Vue代码进行封装,封装成组件,则有可能有效的减少VDOM DIFF时的节点数量
    }
    return vm
    }

Vue VDOM:通过比对VDOM,计算出所需要的真实DOM操作(总计19处注释)

VDOM其实是一个定义,是为了与真实DOM相区分而起的名字,不要被吓到,说白了,其实就是用JavaScript的Object(没错就是Vnode)为节点,构成的一棵树,每个节点上有parent、children以及其他信息,这棵树尽可能的与真实的DOM树相匹配,在数据被更新时,生成新VDOM Tree,与旧的VDOM Tree相比对,通过DIFF算法,比对出,需要怎么样操作DOM,才能正确更新

  • 既然Vnode是基础,先看一下Vnode的数据结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    // vue/src/core/vdom/vnode.js
    export default class VNode {
    tag: string | void; // 1、如vue-compoment-keep-alive
    data: VNodeData | void; // 2、patch运行时用到的数据,比如hooks,keepAlive
    children: ?Array<VNode>;
    text: string | void;
    elm: Node | void; // 3、真实的DOM节点
    ns: string | void;
    context: Component | void; // rendered in this component's scope
    key: string | number | void;
    componentOptions: VNodeComponentOptions | void; // 4、组件的options
    componentInstance: Component | void; // component instance // 5、组件new完之后的Vue Instance
    parent: VNode | void; // component placeholder node

    // strictly internal
    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?
    asyncFactory: Function | void; // async component factory function
    asyncMeta: Object | void;
    isAsyncPlaceholder: boolean;
    ssrContext: Object | void;
    fnContext: Component | void; // real context vm for functional nodes
    fnOptions: ?ComponentOptions; // for SSR caching
    devtoolsMeta: ?Object; // used to store functional render context for devtools
    fnScopeId: ?string; // functional scope id support

    constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
    ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
    }

    // DEPRECATED: alias for componentInstance for backwards compat.
    /* istanbul ignore next */
    get child (): Component | void {
    return this.componentInstance
    }
    }
  • 先解释一下Patch,Patch是Vue VDOM部分中的一个函数,该函数就是Vue.prototype.__patch__,而该函数其实就是在做DIFF,输入是oldVnode、vnode,输出是DIFF后,需要对真实DOM节点所做对DOM操作,比如createElemennt、insertBefore、appendChild等等,而驱动该函数运行的,是Watcher,而Watcher观察的,是data。因此,开发者只需要改变Vue options中的data,就能完成想要的更新

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    // vue/src/core/vdom/patch.js
    function patch (oldVnode, vnode, hydrating, removeOnly) {
    // 6、输入参数是oldVnode、vnode,就是对oldVnode和vnode做DIFF
    if (isUndef(vnode)) {
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
    } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
    // patch existing root node

    // 7、开始做DIFF
    patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
    if (isRealElement) {
    // mounting to a real element
    // check if this is server-rendered content and if we can perform
    // a successful hydration.
    if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    oldVnode.removeAttribute(SSR_ATTR)
    hydrating = true
    }
    if (isTrue(hydrating)) {
    if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
    invokeInsertHook(vnode, insertedVnodeQueue, true)
    return oldVnode
    } else if (process.env.NODE_ENV !== 'production') {
    warn(
    'The client-side rendered virtual DOM tree is not matching ' +
    'server-rendered content. This is likely caused by incorrect ' +
    'HTML markup, for example nesting block-level elements inside ' +
    '<p>, or missing <tbody>. Bailing hydration and performing ' +
    'full client-side render.'
    )
    }
    }
    // either not server-rendered, or hydration failed.
    // create an empty node and replace it
    oldVnode = emptyNodeAt(oldVnode)
    }

    // replacing existing element
    const oldElm = oldVnode.elm
    const parentElm = nodeOps.parentNode(oldElm)

    // create new node
    createElm(
    vnode,
    insertedVnodeQueue,
    // extremely rare edge case: do not insert if old element is in a
    // leaving transition. Only happens when combining transition +
    // keep-alive + HOCs. (#4590)
    oldElm._leaveCb ? null : parentElm,
    nodeOps.nextSibling(oldElm)
    )

    // update parent placeholder node element, recursively
    if (isDef(vnode.parent)) {
    let ancestor = vnode.parent
    const patchable = isPatchable(vnode)
    while (ancestor) {
    for (let i = 0; i < cbs.destroy.length; ++i) {
    cbs.destroy[i](ancestor)
    }
    ancestor.elm = vnode.elm
    if (patchable) {
    for (let i = 0; i < cbs.create.length; ++i) {
    cbs.create[i](emptyNode, ancestor)
    }
    // #6513
    // invoke insert hooks that may have been merged by create hooks.
    // e.g. for directives that uses the "inserted" hook.
    const insert = ancestor.data.hook.insert
    if (insert.merged) {
    // start at index 1 to avoid re-invoking component mounted hook
    for (let i = 1; i < insert.fns.length; i++) {
    insert.fns[i]()
    }
    }
    } else {
    registerRef(ancestor)
    }
    ancestor = ancestor.parent
    }
    }

    // destroy old node
    if (isDef(parentElm)) {
    removeVnodes(parentElm, [oldVnode], 0, 0)
    } else if (isDef(oldVnode.tag)) {
    invokeDestroyHook(oldVnode)
    }
    }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
    }
  • 我们来看一下patchVnode函数。整个VDOM树,每个节点都有parent和children,而patchVnode其实在做的是:比对oldVnode’s children与vnode’s children

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
    ) {
    if (oldVnode === vnode) {
    return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
    hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
    vnode.isAsyncPlaceholder = true
    }
    return
    }

    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
    // 8、如果children都存在,则比对children
    if (isDef(oldCh) && isDef(ch)) {
    if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) { // 9、如果只有vnode上存在children,则这些children都需被add
    if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(ch)
    }
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) { // 10、如果只有oldVnode上存在children,则这些children都需被删除
    removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
    nodeOps.setTextContent(elm, '') // 11、针对text 节点做单独处理
    }
    } else if (oldVnode.text !== vnode.text) { // 12、针对text节点做单独处理
    nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
    }
  • 在看updateChildren之前,先看一个函数sameVnode,该函数的用途是,判断节点a和节点b的真实DOM节点是否一致

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function sameVnode (a, b) {
    return (
    a.key === b.key && ( // 13、这里会先判断key,因此设置一个合适的key的重要性不言而喻。
    // 当前后两次Patch更新时,如果key不变,那么就不会产生不必要的DOM操作
    (
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    !childrenIgnored(a) && !childrenIgnored(b) &&
    sameInputType(a, b)
    ) || (
    isTrue(a.isAsyncPlaceholder) &&
    a.asyncFactory === b.asyncFactory &&
    isUndef(b.asyncFactory.error)
    )
    )
    )
    }
  • 再看一下updateChildren函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    // vue/src/core/vdom/patch.js
    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

    // removeOnly is a special flag used only by <transition-group>
    // to ensure removed elements stay in correct relative positions
    // during leaving transitions
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
    }
    // 14、假设old children是[A, B, C, D]
    // new children是[B, C, D]
    // 那么,根据下面的循环,DIFF后的DOM操作是 removeChild(A)
    // 其实,该函数的最终目的是:在尽量减少DOM操作的前提下,找出old children中能继续使用的真实DOM节点,
    // 并保证new children中的节点能够正确的更新
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
    oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
    oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    oldStartVnode = oldCh[++oldStartIdx]
    newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
    patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    oldEndVnode = oldCh[--oldEndIdx]
    newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
    patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
    // 15、产生DOM操作insertBefore
    canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
    oldStartVnode = oldCh[++oldStartIdx]
    newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
    patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
    oldEndVnode = oldCh[--oldEndIdx]
    newStartVnode = newCh[++newStartIdx]
    } else {
    if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    // 16、如果定义了key,则时间复杂度为O(1),如果没有定义key,则时间复杂度为O(n)
    idxInOld = isDef(newStartVnode.key)
    ? oldKeyToIdx[newStartVnode.key]
    : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    if (isUndef(idxInOld)) { // New element
    // 17、产生DOM操作appendChild or insertBefore
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    } else {
    vnodeToMove = oldCh[idxInOld]
    if (sameVnode(vnodeToMove, newStartVnode)) {
    patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
    oldCh[idxInOld] = undefined
    canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    } else {
    // same key but different element. treat as new element
    createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
    }
    }
    newStartVnode = newCh[++newStartIdx]
    }
    }
    if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    // 18、产生DOM操作
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
    // 19、产生DOM操作
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
    }
  • 总结一下整个过程

    • 在Vue.prototype._init()中,Watcher将会watch options data等关键位置
    • 开发者代码中更新了options data
    • Watcher watch到改变,触发updateComponent
    • 调用options中的render函数,生成新vnode
    • 紧接着调用patch,与oldVnode做对比,输出是:需要对真实DOM所做的操作
    • 更新完成

Vue Watcher:相当于大管家,运行时,触发各处的调用(总计18处注释)

  • 上面多次提到了Watcher,Watcher在Vue中的作用有很多,比如

    • 上面提到的options data,更新options data触发视图更新
    • computed特性,更新options data后,更新computed的属性绑定的视图
    • Vue中手动watch特性
    • Vuex的实现,也是借助于Watcher
  • 整个Watcher其实就是观察者模式,只不过,register是全自动的,不需要开发者关心,我们将重点关注是如何实现全自动register

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    // vue/src/core/observer/watcher.js
    export default class Watcher {
    vm: Component; // 1、Vue实例
    expression: string;
    cb: Function;
    id: number;
    deep: boolean;
    user: boolean;
    lazy: boolean;
    sync: boolean;
    dirty: boolean;
    active: boolean;
    deps: Array<Dep>;
    newDeps: Array<Dep>;
    depIds: SimpleSet;
    newDepIds: SimpleSet;
    before: ?Function;
    getter: Function; // 2、当监听到变动是,需要执行到函数
    value: any;

    constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean // 3、mountComponent函数执行时,会new Watcher(xxx)
    ) {
    this.vm = vm
    if (isRenderWatcher) {
    vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
    this.deep = !!options.deep
    this.user = !!options.user
    this.lazy = !!options.lazy
    this.sync = !!options.sync
    this.before = options.before
    } else {
    this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString()
    : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
    this.getter = expOrFn // 4、expOrFn即mountComponent函数中到updateComponent函数
    } else {
    this.getter = parsePath(expOrFn)
    if (!this.getter) {
    this.getter = noop
    process.env.NODE_ENV !== 'production' && warn(
    `Failed watching path: "${expOrFn}" ` +
    'Watcher only accepts simple dot-delimited paths. ' +
    'For full control, use a function instead.',
    vm
    )
    }
    }
    this.value = this.lazy
    ? undefined
    : this.get() // 5、在Watcher构造函数中执行get()函数
    }

    /**
    * Evaluate the getter, and re-collect dependencies.
    */
    get () {
    pushTarget(this) // 6、此处是关键,将会使得 Dep.target === this
    let value
    const vm = this.vm
    try {
    value = this.getter.call(vm, vm)
    // 6、第一次执行updateComponent函数,
    // updateComponent会依次调用Vue.prototype._render()生成VDOM树、
    // Vue.prototype._update()进行patch、patch()、产生DOM操作
    } catch (e) {
    if (this.user) {
    handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
    throw e
    }
    } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
    traverse(value)
    }
    popTarget()
    this.cleanupDeps()
    }
    return value
    }

    /**
    * Add a dependency to this directive.
    */
    addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
    dep.addSub(this) // 7、向Dep实例中添加 this ,
    // 当被defineReactive后的字段(比如data中当字段)被更新,
    // Dep类的notify()函数会被调用,紧接着会调用 this.update()
    }
    }
    }

    /**
    * Clean up for dependency collection.
    */
    cleanupDeps () {
    let i = this.deps.length
    while (i--) {
    const dep = this.deps[i]
    if (!this.newDepIds.has(dep.id)) {
    dep.removeSub(this)
    }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
    }

    /**
    * Subscriber interface.
    * Will be called when a dependency changes.
    */
    update () {
    /* istanbul ignore else */
    if (this.lazy) {
    this.dirty = true
    } else if (this.sync) {
    this.run()
    } else {
    queueWatcher(this) // 8、将Watcher加入队列,
    // 这是Vue异步渲染的原理,
    // 队列中的Watcher将会在下一个tick中被调用this.run() 函数,
    // this.run() 最终会调用 this.getter() 函数,
    // 如果 this 是Vue组件实例对应的Watcher,
    // 那么,就会触发updateComponent被调用
    }
    }

    /**
    * Scheduler job interface.
    * Will be called by the scheduler.
    */
    run () {
    if (this.active) {
    const value = this.get()
    if (
    value !== this.value ||
    // Deep watchers and watchers on Object/Arrays should fire even
    // when the value is the same, because the value may
    // have mutated.
    isObject(value) ||
    this.deep
    ) {
    // set new value
    const oldValue = this.value
    this.value = value
    if (this.user) {
    try {
    this.cb.call(this.vm, value, oldValue)
    } catch (e) {
    handleError(e, this.vm, `callback for watcher "${this.expression}"`)
    }
    } else {
    this.cb.call(this.vm, value, oldValue)
    }
    }
    }
    }

    /**
    * Evaluate the value of the watcher.
    * This only gets called for lazy watchers.
    */
    evaluate () {
    this.value = this.get()
    this.dirty = false
    }

    /**
    * Depend on all deps collected by this watcher.
    */
    depend () {
    let i = this.deps.length
    while (i--) {
    this.deps[i].depend()
    }
    }

    /**
    * Remove self from all dependencies' subscriber list.
    */
    teardown () {
    if (this.active) {
    // remove self from vm's watcher list
    // this is a somewhat expensive operation so we skip it
    // if the vm is being destroyed.
    if (!this.vm._isBeingDestroyed) {
    remove(this.vm._watchers, this)
    }
    let i = this.deps.length
    while (i--) {
    this.deps[i].removeSub(this)
    }
    this.active = false
    }
    }
    }
  • 另一个很重要的类是:Dep

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    // vue/src/core/observer/dep.js
    export default class Dep {
    static target: ?Watcher; // 9、这是关键变量:Vue用该全局变量,
    // 来保存当前正在watch的Watcher实例是哪个,这是依赖收集的关键
    id: number;
    subs: Array<Watcher>; // 10、该属性表示所有正在watch该Dep实例的Watcher。
    // 一个Dep实例对应了一个data中的一个字段,
    // 当该字段被更新,将会触发Watcher运行this.run()

    constructor () {
    this.id = uid++
    this.subs = []
    }

    addSub (sub: Watcher) {
    this.subs.push(sub)
    }

    removeSub (sub: Watcher) {
    remove(this.subs, sub)
    }

    depend () {
    if (Dep.target) {
    Dep.target.addDep(this) // 11、该方法会调用Dep类的addSub方法,
    // 最终的结果是,将Dep.target保存的Watcher实例加入到Dep的subs数组中,
    // 后续更新字段触发Dep类的notify时,运行该Watcher实例
    }
    }

    notify () { // 12、当被defineReactive后的字段更新后,notify会被执行
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update() //13、执行Watcher实例中的update()
    }
    }
    }

    // the current target watcher being evaluated.
    // this is globally unique because there could be only one
    // watcher being evaluated at any time.
    Dep.target = null // 14、该机制奏效的依据是:同一时刻,
    // 只会有一个Watcher在运行,而绝大多数情况下Watcher对应的就是Vue组件实例。
    // Vue的Patch更新机制,是以Vue组件为单位的
    const targetStack = []

    export function pushTarget (target: ?Watcher) {
    targetStack.push(target)
    Dep.target = target
    }

    export function popTarget () {
    targetStack.pop()
    Dep.target = targetStack[targetStack.length - 1]
    }
  • 接下来就是关键方法:defineReactive

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    export function defineReactive (
    obj: Object,
    key: string,
    val: any,
    customSetter?: ?Function,
    shallow?: boolean
    ) {
    const dep = new Dep() // 15、一个字段key,对应着一个Dep实例

    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
    return
    }

    // cater for pre-defined getter/setters
    const getter = property && property.get
    const setter = property && property.set
    if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
    }

    let childOb = !shallow && observe(val)
    // 16、Vue的关键点,使用了Object.defineProperty,
    // 借助get/set机制,在get时,自动收集依赖,在set时自动触发更新
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) { // 17、如果当前有Watcher正在执行,
    // 说明该字段key需要进行依赖收集,那么,收集依赖
    dep.depend()
    if (childOb) {
    childOb.dep.depend()
    if (Array.isArray(value)) {
    dependArray(value)
    }
    }
    }
    return value
    },
    set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
    return
    }
    /* eslint-enable no-self-compare */
    if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()
    }
    // #7981: for accessor properties without setter
    if (getter && !setter) return
    if (setter) {
    setter.call(obj, newVal)
    } else {
    val = newVal
    }
    childOb = !shallow && observe(newVal)
    dep.notify() // 18、如果该字段key被更新,运行Watcher
    }
    })
    }

从产品角度进行总结

Vue的成功,有两个亮点可以学习:

  • 优化开发者开发体验,拉拢开发者,具体的表现有
    • 用户只需要写类似html的Vue template模版,入手容易
    • 全自动watch,同时又不降低性能
  • 技术创新
    • 使用DIFF算法进行Patch更新
    • 全自动Watch的设计
    • 跨平台支持,横向扩大开发者队伍

特别声明:本文未经许可禁止转载

开头

  • VS Code是微软在Electron的基础上使用TypeScript开发的
  • 本文是基于VS Code 1.36.0版本的基础上分析的
  • 本文重点解析VS Code工程中,我认为值得学习的地方
  • 如有问题,可以留言

Chromium

要讲Electron,必须先说ChromiumChromium使用了多进程架构,分为Browser ProcessRender ProcessRender Process使用BlinkV8Blink用于计算布局,V8用于运行JavaScript代码,真正渲染到屏幕上一个一个的像素,是在Browser Process完成的,Browser ProcessRender Process通过IPC进程通信(使用Mojo),Browser Process可以保证安全(用于渲染到屏幕,管理Cookie、Storage、网络请求等),而Render Process是在沙箱里面运行的。

Electron

ElectronChromuim基础上,给Browser ProcessRender Process都加进了Node Environment,这样,带来了Node开发者,带来了丰富的NPM包,并且,不论是在Browser Process还是Render Process,都能直接调用Node API,从而获得Native能力。同时,Electron还给Browser ProcessRender Process加进了Electron API,为开发者提供Browser ProcessRender Process的IPC通信API,以及提供一些必要的功能。
以下用主进程表示Browser Process,用渲染进程表示Render Process

为方便后文理解,先讲一下VS Code初始化过程

为方便起见,文件名不加后缀,比如src/main实际为src/main.js,而src/vs/code/electron-main/main实际为src/vs/code/electron-main/main.ts

  • Electron根据根目录下package.json文件中的main字段,在主进程加载src/main,处理本地语言配置以及process.env
  • 加载src/vs/code/electron-main/main,实例化CodeMain类,调用该类中的main()方法,创建主进程中外层的InstantiationService,并实例化CodeApplication类,调用该类中的startup()方法

    InstantiationService用于实例化其他类,使得其他类在主进程或者渲染进程中,在保持单例的同时又能很方便的作为构造器参数传入,这个类是VS Code工程中实现依赖注入的重要部分

  • CodeApplication类的startup()方法中,再次创建InstantiationService,该InstantiationService是外层InstantiationServicechild,并且如果某个类的实例在当前窗口的InstantiationService中找不到时,会去外层的InstantiationService中查找,然后实例化各个Service类,并最终在src/vs/code/electron-main/window中调用new BrowserWindow(options),打开窗口,携带处理完毕的配置参数加载渲染进程的代码src/vs/code/electron-browser/workbench/workbench
  • 加载src/vs/workbench/electron-browser/main,实例化渲染进程各个Service类放入serviceCollection,然后用serviceCollection去实例化渲染进程的InstantiationService
  • 加载后续代码,用TypeScript操作DOM,计算Layout,生成页面

用Service划分各个功能的界线

VS Code中有许多Service,有的位于主进程,有的位于渲染进程,有的只在主进程使用,有的只在渲染进程使用,有的在主进程中定义逻辑,在渲染进程中通过Electron提供的IPC建立Proxy使用(对于Service使用者来说无感知),Service位于src/vs/platform目录,主要有IInstantiationService,IEnvironmentService,IFileService,ILayoutService,INotificationService,IOpenerService,IStorageService,IWindowsService,IWindowsMainService,IWorkspacesService,IWorkspacesMainService

依赖注入Dependency Injection

关于依赖注入的整体介绍,VS Code wiki已经讲的很清楚了:

The code is organized around services of which most are defined in the platform layer. Services get to its clients via constructor injection.
A service definition is two parts: (1) the interface of a service, and (2) a service identifier - the latter is required because TypeScript doesn’t use nominal but structural typing. A service identifier is a decoration (as proposed for ES7) and should have the same name as the service interface.
Declaring a service dependency happens by adding a corresponding decoration to a constructor argument. In the snippet below @IModelService is the service identifier decoration and IModelService is the (optional) type annotation for this argument. When a dependency is optional, use the @optional decoration otherwise the instantiation service throws an error.

1
2
3
4
5
6
7
8
class Client {
constructor(
@IModelService modelService: IModelService,
@optional(IEditorService) editorService: IEditorService
) {
// use services
}
}

Use the instantiation service to create instances for service consumers, like so instantiationService.createInstance(Client). Usually, this is done for you when being registered as a contribution, like a Viewlet or Language.

下面从代码角度说明一下:

  • 使用decoration(注解)将依赖以变量的形式存到Class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // src/vs/platform/instantiation/common/instantiation.ts
    export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {

    if (_util.serviceIds.has(serviceId)) {
    return _util.serviceIds.get(serviceId)!;
    }
    //根据TypeScript的规定,实现注解函数
    const id = <any>function (target: Function, key: string, index: number): any {
    if (arguments.length !== 3) {
    throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
    }
    storeServiceDependency(id, target, index, false);
    };

    id.toString = () => serviceId;

    _util.serviceIds.set(serviceId, id);
    return id;
    }

    function storeServiceDependency(id: Function, target: Function, index: number, optional: boolean): void {
    // 在运行时,将注解保存到target(Class),方便之后计算graph
    if (target[_util.DI_TARGET] === target) {
    target[_util.DI_DEPENDENCIES].push({ id, index, optional });
    } else {
    target[_util.DI_DEPENDENCIES] = [{ id, index, optional }];
    target[_util.DI_TARGET] = target;
    }
    }
  • 根据已有信息计算依赖,构造有向图
  • 找出出度为0的节点,并从这些节点开始,用instantiationService.createInstance(Client)初始化实例
    graph LR;
      Class-A-->Dependence-Class-B;
      Dependence-Class-B-->Dependence-Class-C;
      Class-A-->Dependence-Class-D;
      Dependence-Class-D-->Dependence-Class-E;
      Dependence-Class-D-->Dependence-Class-F;
    

    其中,Class-A为当前需要实例化的类,graph生成完毕之后,根据规则,先实例化Dependence-Class-C、Dependence-Class-E、Dependence-Class-F,再实例化Dependence-Class-B、Dependence-Class-D,最后才实例化Class-A

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    // src/vs/platform/instantiation/common/instantiationService.ts
    private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
    type Triple = { id: ServiceIdentifier<any>, desc: SyncDescriptor<any>, _trace: Trace };
    // 有向图,保存出度和入度
    const graph = new Graph<Triple>(data => data.id.toString());

    function throwCycleError() {
    const err = new Error('[createInstance] cyclic dependency between services');
    err.message = graph.toString();
    throw err;
    }

    let count = 0;
    const stack = [{ id, desc, _trace }];
    while (stack.length) {
    const item = stack.pop()!;
    graph.lookupOrInsertNode(item);

    // TODO@joh use the graph to find a cycle
    // a weak heuristic for cycle checks
    if (count++ > 100) {
    throwCycleError();
    }

    // check all dependencies for existence and if they need to be created first
    let dependencies = _util.getServiceDependencies(item.desc.ctor);
    for (let dependency of dependencies) {

    let instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
    if (!instanceOrDesc && !dependency.optional) {
    console.warn(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`);
    }

    if (instanceOrDesc instanceof SyncDescriptor) {
    const d = { id: dependency.id, desc: instanceOrDesc, _trace: item._trace.branch(dependency.id, true) };
    // 从item节点指向d节点
    graph.insertEdge(item, d);
    stack.push(d);
    }
    }
    }

    while (true) {
    // 找出出度为0的节点
    let roots = graph.roots();

    // if there is no more roots but still
    // nodes in the graph we have a cycle
    if (roots.length === 0) {
    if (!graph.isEmpty()) {
    throwCycleError();
    }
    break;
    }

    for (let { data } of roots) {
    // create instance and overwrite the service collections
    const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation, data._trace);
    this._setServiceInstance(data.id, instance);
    graph.removeNode(data);
    }
    }

    return <T>this._getServiceInstanceOrDescriptor(id);
    }
  • 值得说明的是,实例化是支持懒加载的,懒加载使用代理模式,懒加载的实现原理如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    private _createServiceInstance<T>(ctor: any, args: any[] = [], _supportsDelayedInstantiation: boolean, _trace: Trace): T {
    if (!_supportsDelayedInstantiation || !_canUseProxy) {
    // eager instantiation or no support JS proxies (e.g. IE11)
    return this._createInstance(ctor, args, _trace);

    } else {
    // Return a proxy object that's backed by an idle value. That
    // strategy is to instantiate services in our idle time or when actually
    // needed but not when injected into a consumer
    const idle = new IdleValue(() => this._createInstance<T>(ctor, args, _trace));
    return <T>new Proxy(Object.create(null), {
    get(_target: T, prop: PropertyKey): any {
    return idle.getValue()[prop];
    },
    set(_target: T, p: PropertyKey, value: any): boolean {
    idle.getValue()[p] = value;
    return true;
    }
    });
    }
    }

Part

打开VS Code并新建一个窗口(默认配置下),可以将窗口分成几大部分:

  • TitleBarPart,位于顶部
  • ActivityBarPart,位于最左侧,大部分由Icon构成
  • SideBarPart,紧贴ActiviyBarPart右侧
  • EditorPart,编辑器
  • PanelPart,位于编辑器下面,由Terminal等构成
  • StatusBarPart,位于最下面,显示状态、分支等
    可见,VS Code视图由Part构成。Part是VS Code工程中的一个基础类,定义了许多抽象方法,其中,protected createContentArea(parent: HTMLElement, options?: object): HTMLElement | null方法,使用TypeScript操作DOM来用来定义视图

Part之用TypeScript操作DOM

src/vs/base/browser/ui目录下,定义了许多基础的组件,比如SelectBox,用dom.append(container, $('.option-text'));形式和CSS,定义界面。

Command机制

Command可以说是VS Code定义的另一个非常好用的概念。他可以让用户通过Shift+Command+P选择Command然后执行,并且赋予了VS Code Extension扩展Command的能力。Command支持插件进程和VS Code进程相互调用。

Extension(插件)机制

软件开发中的开闭原则:开放扩展,关闭修改。Extension便是开闭原则的一个很好的实现。Chrome有插件,Cocos有插件,Hexo有插件,Webpack有插件,Gulp有插件,VS Code也有插件

VS Code内置插件在extension目录下,内置插件分成两种,一种是本地内置插件,另一种是打包是从Extension Markets下载的内置插件,插件开发文档点这。从插件大类来看,也可以分成两种,一种是Normal Extension,可以使用VS Code API,另一种是Debugger Extension,用于运行Debug Adapter。

Gulp编译打包

Gulp官方介绍如下:

  • Automation - gulp is a toolkit that helps you automate painful or time-consuming tasks in your development workflow.
  • Platform-agnostic - Integrations are built into all major IDEs and people are using gulp with PHP, .NET, Node.js, Java, and other platforms.
  • Strong Ecosystem - Use npm modules to do anything you want + over 2000 curated plugins for streaming file transformations
  • Simple - By providing only a minimal API surface, gulp is easy to learn and simple to use

VS Code打包脚本位于build目录下,在执行gulp watch之后,gulp会首先加载根目录的gulpfile.js文件,进而加载build目录下一系列gulp.*.js文件,build/gulp.*.js文件中定义了许多gulp task,各个task可以相互依赖。如果想运行VS Code,可以参考[官方文档](https://github.com/microsoft/VS Code/wiki/How-to-Contribute)。

VS Code调试架构

VS Code可以调试javascriptpythonphpc各种语言,而实现这些调试等基础就是DAP协议,官方对DAP的图示如下:

dap

VS Code 定义了一种抽象的协议即DAP,并实现了一种通用的调试UI,VS Code使用该协议与各种语言的调试进程通信,但是,各种语言不会实现DAP协议,因此,需要一个Adapter,即Debug Adapter(DA),DA运行在一个单独的进程里面,与调试进程通信。
如果你想调试某种语言,首先,需要先实现该语言的Debug Adapter并以Debugger Extension的形式,安装到VS Code上,关于如何实现,你可以查看官方文档。当然,大部分语言的Debug Adapter都已经被实现,你可以直接使用。

引用

https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle
https://www.typescriptlang.org/docs/handbook/decorators.html
https://github.com/microsoft/vscode/wiki/Source-Code-Organization
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
https://code.visualstudio.com/api/extension-guides/debugger-extension

怎样选择一个开源项目去学习

  • 文档要丰富,包括user guide,document api,从clone到运行环境配置,debug
  • 有测试用例,如果一个项目没有测试用例,那么这个项目最好不要去看
  • blog post丰富
  • 有需求,能用得上,如果用不上,很快就忘记了

怎样学习一个开源项目

  • 首先,网速要快。由于众所周知的原因,我们需要一个工具,跨过山和大海,推荐clashx/clash
  • 详细阅读项目文档,design document等,如果是英文,最好直接自己阅读英文,而不是看别人已经翻译好的
  • 不要使用windows,选择mac或者linux
  • 配置好运行环境,能够debug,能够使用代码跳转,mac上推荐vscode
  • 从sample开始,然后是测试用例,一点一点分解
  • 如果改项目支持插件开发,则一定要去完整的看一遍插件开发流程,并尝试开发一个插件