最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 这天,我被面试官刁难了......

    正文概述 掘金(阿镇@吃橙子)   2021-07-07   505

    这天我被面试官刁难了,面试官灵魂四连击:

    1. 你对虚拟 DOM 原理的理解?
    2. Vue 的 diff 算法有了解过吗?
    3. 抽象语法树是什么,能介绍一下吗?
    4. 从 new 一个 Vue 对象开始,Vue 的内部运行机制是什么样的?

    知耻而后勇,我们静下心来好好在学习一下 vue 吧?从哪里入手呢?我们以源码的角度看下从new 一个 Vue 实例开始,Vue 内部发生了什么?

    1. Vue 初始化和挂载

    这天,我被面试官刁难了......

    let vm = new Vue({ el: '#app' })
    

    从Vue 的构造函数开始入手:

    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)
    }
    

    再看下 this._init(options)

    Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        vm._uid = uid++
    
        let startTag, endTag
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          startTag = `vue-perf-start:${vm._uid}`
          endTag = `vue-perf-end:${vm._uid}`
          mark(startTag)
        }
        //一个避免 vm 实例被观察的标志
        vm._isVue = true
        //内部组件的options._isComponent是true,mian.js里面new Vue()时为false
        if (options && options._isComponent) {
          //初始化内部组件
          initInternalComponent(vm, options)
        } else {
        	//合并options选项
        	//resolveConstructorOptions函数是获取当前实例中的构造函数的options选项以及它所有的父级的构造函数的options
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        /* istanbul ignore else */
        //为Vue实例的_renderProxy属性赋值
        if (process.env.NODE_ENV !== 'production') {
          initProxy(vm)
        } else {
          vm._renderProxy = vm
        }
        // expose real self
        vm._self = vm
        initLifecycle(vm)
        initEvents(vm)
        initRender(vm)
        callHook(vm, 'beforeCreate')
        initInjections(vm)
        initState(vm)
        initProvide(vm) 
        callHook(vm, 'created')
    
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          vm._name = formatComponentName(vm, false)
          mark(endTag)
          measure(`vue ${vm._name} init`, startTag, endTag)
        }
        //如果实例化Vue时传递了el选项,则自动执行$mount进行挂载
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    

    从生命周期来看, _init 主要完成下列工作:

    • 初始化生命周期、事件、 render
    • 调用 beforeCreate 钩子函数
    • 初始化 props、methods、data、computed 与 watch ,并且对 options 中的数据进行"响应式化"(双向绑定)以及完成依赖收集
    • 调用 created 钩子函数
    • 挂载组件

    2. 模板编译 compile

    这天,我被面试官刁难了......

    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
      }
    })
    

    这里 baseCompile 函数可以划分为三个阶段:

    • parse 解析阶段。用正则等方式解析 template 中的指令、class、style等数据,形成 ast 。
    • optimize 优化阶段。标记 static 静态节点。在新旧节点比较变更时,diff 算法会直接跳过静态节点,这里优化了 patch 的性能。
    • generate 代码生成阶段。将 ast 转化成 render function 字符串,得到结果是 render 的字符串以及 staticRenderFns 字符串。

    三个模块具体实现对应的源码:

    /*解析阶段*/
    src/compiler/parser/index.js
    /*优化阶段*/
    src/compiler/optimizer.js
    /*代码生成阶段*/
    src/compiler/codegen/index.js
    

    3. Watcher 到更新视图

    这天,我被面试官刁难了...... Watcher 对象会调用 updateComponent 方法来实现更新视图:

    updateComponent = () => {
        vm._update(vm._render(), hydrating)
    }
    

    updateComponent 就执行一句话,_render 函数会返回一个新的 VNode 节点,传入 _update 中与旧的 VNode 对象进行对比,经过一个 patch 的过程得到两个 VNode 节点的差异,最后我们只需要将这些差异的对应 DOM 进行修改即可。

    3.1. VNode

    这里 VNode 是一种全新的性能优化解决方案,它可以理解为用 JS 的计算性能来换取操作 DOM 所消耗的性能。 最直观的思路就是我们以数据驱动的思想去开发,我们只需要关注数据操作,而不是去操作真实 DOM。 我们可以用 JS 模拟出一个 DOM 节点,这里简称 VNode。当数据发生变化时,我们对比变化前后的 VNode,通过 diff 算法 计算出需要更新的地方,最后一起更新视图。另外 VNode 的存在也使得 Vue 不在依赖浏览器环境,使其有了服务端渲染的能力。

    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?
        asyncFactory: Function | void; // async component factory function
        asyncMeta: Object | void;
        isAsyncPlaceholder: boolean;
        ssrContext: Object | void;
    
        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.functionalContext = 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
        }
        get child(): Component | void {
            return this.componentInstance
        }
    }
    

    3.2. Patch

    前面说过 _update 会将新旧两个 VNode 进行一次 patch 的过程,得到两 VNode 之间最小差异,然后将这些差异渲染到视图。其实仔细想想就三种操作:

    1. 创建节点
    2. 删除节点
    3. 更新节点

    这天,我被面试官刁难了......

    diff 算法相当的高效。它是一种通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n)。上图代表新旧的 VNode 进行 patch 的过程,他们只是在同层级的 VNode 之间进行比较得到变化(第二张图中相同颜色的方块代表互相进行比较的 VNode 节点),然后修改变化的视图,所以十分高效。在 patch 的过程中,如果两个 VNode 被认为是同一个 VNode (sameVnode),才会进行深度的比较,得出最小差异,否则直接删除旧有 DOM 节点,创建新的 DOM 节点。 这天,我被面试官刁难了...... 如上图所示,新、老VNode 节点的左右头尾两侧都有一个变量标记,在遍历中这几个变量都向中间靠拢。当 oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 时结束循环。在遍历中,如果存在 key ,并且满足 sameVnode ,复用该 DOM 节点,否则创建一个新的 DOM 节点。这里,oldStartVnode、oldEndVnode 与 newStartVnode、newEndVnode 两两比较一共有 4 种比较方法:

    1. 当新老 VNode 节点的 start 或者 end 满足 sameVnode 时,也就是 sameVnode(oldStartVnode, newStartVnode) 或者 sameVnode(oldEndVnode, newEndVnode) ,直接将该 VNode 节点进行 patchVnode 即可。
    2. 如果 oldStartVnode 与 newEndVnode 满足 sameVnode ,即 sameVnode(oldStartVnode, newEndVnode) ,这时候说明 oldStartVnode 已经跑到了 oldEndVnode 后面去了,进行 patchVnode 的同时还需要将真实 DOM 节点移动到 oldEndVnode 的后面。
    3. 如果 oldEndVnode 与 newStartVnode 满足 sameVnode ,即 sameVnode(oldEndVnode, newStartVnode)。这说明 oldEndVnode 跑到了 oldStartVnode 的前面,进行 patchVnode 的同时真实的 DOM 节点移动到了 oldStartVnode 的前面。
    4. 通过 createKeyToOldIdx 会得到一个 oldKeyToIdx ,里面存放了一个 key 为旧的 VNode , value 为对应 index 序列的哈希表。从这个哈希表中可以找到是否有与 newStartVnode 一致 key 的旧的 VNode 节点,如果同时满足 sameVnode , patchVnode 的同时会将这个真实 DOM(elmToMove) 移动到 oldStartVnode 对应的真实 DOM 的前面

    当结束时,如果oldStartIdx > oldEndIdx ,这个时候老的 VNode 节点已经遍历完了,但是新的节点还没有。说明了新的 VNode 节点实际上比老的 VNode 节点多,也就是比真实 DOM 多,需要将剩下的(也就是新增的) VNode 节点插入到真实 DOM 节点中去,此时调用 addVnodes (批量调用 createElm 的接口将这些节点加入到真实 DOM 中去)。同理,当 newStartIdx > newEndIdx 时,新的 VNode 节点已经遍历完了,但是老的节点还有剩余,说明真实 DOM 节点多余了,需要从文档中删除,这时候调用 removeVnodes 将这些多余的真实 DOM 删除。

    4. 映射到真实DOM

    虚拟 DOM 提供一些钩子函数,分别在不同的时期会进行调用。 这里没有对 attr、class、props、events、style 以及 transition (过渡状态)的 DOM 属性进行操作的描述。下一步有机会补充。

    const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] /*构建 cbs 回调函数*/
    for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = []
        for (j = 0; j < modules.length; ++j) {
            if (isDef(modules[j][hooks[i]])) {
                cbs[hooks[i]].push(modules[j][hooks[i]])
            }
        }
    }
    

    起源地下载网 » 这天,我被面试官刁难了......

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元