美文网首页
vue 源码运行流程解析

vue 源码运行流程解析

作者: 拉面的无聊时光 | 来源:发表于2019-02-15 22:39 被阅读0次

    框架只是工具,了解框架底层原理才能在框架基础上开发出更好的程序。 vue作为前端基础框架之一,更应该清楚其底层运行原理

    mvvm模式

    vue的mvvm模式如图所示 mvvm.png

    内部基础对象

    • Observer: 用来深度遍历 model对象 ,对内部的每一个key用Object.defineProperty 函数对gettersetter 进行劫持

    • Dep: 初始化vue实例时,Observer劫持model 每个key的gettersetter函数时,会为每一个key值都创建Dep对象,Dep对象内部有数组去存储watcher。

    • Wathcer : 当model数据变动时,触发对应的key的Dep.notify(), Dep.notity触发dep内部所有watcher的watcher.update()事件

    watcher 实例主要有三种情况
    1. 主watcher, 每个vue实例都有一个主watcher,负责页面渲染
    2. computed-watcher:computed选项生成的watcher,负责触发computed函数
    3. watch-watcher:watch选项生成的watcher,负责触发自定义的watch函数
    

    vue在mounted阶段 ,

    • 数据如下所示
    //name属性对应一个dep对象,dep内部一个数组保存着收集的 wather
    name -- {dep : [ watcher ] } 
    
    //age属性对应一个dep对象,dep内部一个数组保存着收集的 wather
    age  -- {dep : [ watcher ] } 
    
    • 数据驱动页面 的事件流程
    1.name 值变更 
    2.name的setter 函数执行
    3.name对应的dep触发dep.notify()
    4.dep内部所有watcher 触发wather.update() 
    // water.update()触发 1.页面更新  2.computed选项属性更新  3.wath选项属性更新
    

    在vue初始化阶段,有一些重要的事情处理

    vue初始化阶段

    1.补全vue实例的相关属性,生命周期属性,Event事件注册,渲染函数相关初始化,处理option里面的数据等等。

    //...省略
    initLifecycle(vm); //给vue实例补充生命周期相关属性如,_isMounted,_isDestroyed等
    initEvents(vm); 
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    
    //重点: ininState这里处理了option里面的
    //1. data属性(劫持getter,setter),
          //目前还未收集watche,此时model是如下
    
          name -- {dep : [   ] } 
          age -- {dep : [   ] } 
    
    //2. computed属性(生成watcher),
    //3. watch属性(生成watcher)
    initState(vm);
    
    initProvide(vm); //  resolve provide after data/props
    callHook(vm, 'created');
    //...省略
    
    1. 生成渲染函数 :源码地址
    // 以下代码是我个人便于理解而写的,代码和源码不同,但是逻辑流程是一样的
    var render 
    if(option.render){
      render = option.render
    }else if(option.template ){
       render = compile(option.template)
    }else if(option.el) {
       rennder = compile(doucment.getElementById(option.el).outerHTML)
    }else {
         throw new Error("...")
    }
    

    以上代码会将html模板转化为渲染函数即

    <div id="app">
        <h2>{{name}}</h2>
         <h4>it is {{age}}</h4>
     </div>
    //转化为
    let _render = function () {
        with(this){
            return 
            _c('div',{attrs:{"id":"app"}},
                [
                    _c('h2',[_v(_s(name))]),
                    _c('h4',[_v("it is "+_s(age))])
                ]
            )
        }
    }
    vm.$option.render = render
    //_c :返回vnode(虚拟dom节点)的函数
    //_v: 返回vnode(虚拟文本节点)的函数
    //_s: String()
    转化过程就是:
    1.html转astdom树
    2.根据astdom树拼接 渲染函数字符串
    3. new Function(`渲染函数字符串`)
    所以说不写模板标签,直接写option里面写渲染函数性能会提高很多。但代价就是开发效率低,不够直观
    

    3.生成主watcher ,代码如下

    updateComponent = function () {
        vm._update(vm._render(), hydrating);
    };
    
    new Watcher(vm, updateComponent, noop, {
        before: function before() {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate');
            }
        }
    }, true /* isRenderWatcher */); 
    
    
    hydrating = false;
    vm._isMounted = true;
    callHook(vm, 'mounted');
    

    new Watcher时第四个参数里面没有lazy属性 ,所以此watcher 会在构造函数内部执行watcher.getter(),也就是传入第二个参数updateComponent

    看下watcher.getter执行流程

    1.watcher.getter()
            ⬇
    2.vm._update(vm._render(),hydrating) 
            ⬇
                1. vm._render()
                          ⬇
                2.vm.$option.render.call(vm,vm.$createElement) 
                //也就是以vm为上下文执行渲染函数(此函数在第二步中动态生成)           
                          ⬇
                3.执行 function () {
                            with(this){
                                return
                                  _c('div',{attrs:{"id":"app"}},
                                    [
                                      ```执行这里时,触发name的getter函数```
                                      _c('h2',[_v(_s(name))]), 
                                      ```执行这里时,触发age的getter函数```
                                      _c('h4',[_v("it is "+_s(age))]) 
                                    ] 
                                  )
                             }
                        }
                           ⬇
                4.执行name的get()和age的get()
                      function defineReactive$$1(obj,   key,val,) {
                        var dep = new Dep();
                        Object.defineProperty(obj, key, {
                          get: function reactiveGetter() {
                                `````````执行这里收集watcher`````
                                 if (Dep.target) {dep.subs.push(Dep.target);}
                                 return val
                               },
                           set: function reactiveSetter(newVal) {
                                  if (newVal === val ) {return}
                                  val = newVal;
                                  dep.notify();
                               }
                        });
                      }
                                 ⬇
                 5.执行完get后 此时model是如下
                        ``` name -- {dep : [ watcher  ] } ```
                        ``` age -- {dep : [  watcher ] } ```
                                ⬇
                 6 执行完vm._render后返回vnode,此时watcher收集完毕
            ⬇
    3.vm._update(vnode,hydrating)
            ⬇
    4.vm._update内部执行__patch__ ,
        if (!prevVnode) {
            //第一次渲染:__patch__根据vnode生成真实dom插入到$el中
            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
        } else {
            // 更新渲染:__patch__,使用diff算法比较prevVnode和vnode,最小化修正真实dom
            vm.$el = vm.__patch__(prevVnode, vnode);
        }      
    

    以上代码执行完成,vue的初始化就已完成。

    在mounted阶段
    model此时数据如下:
    
    name -- {dep : [ watcher ] } 
    age  -- {dep : [ watcher ] } 
    
    此时当 name的值变更时执行以下函数:
    
    name 的setter() 执行
         ⬇
    dep.notify()
         ⬇
    watcher.update()
         ⬇
    watcher.getter()
         ⬇
    vm._update(vm._render(),hydrating) ) 
    
    vm._update()执行流程 。上面已经详细的写过了,这里就不写了
    
    
    在destroy阶段
    • 移除vue实例上的所有watcher
    • 移除dom上所有事件绑定
    • 。。。
    vue组件
    • 创建时间点: __patch__函数内部创建
    • 运行流程:和上面写的一样
      最外层的vue实例也是vue组件,区别是:最外层的vue组件(vue实例)new的时候需要提供el属性, 内部vue组件不需要el属性,因为内层vue组件知道自己真实dom插在哪里。
    总结

    vue运行总体流程大概就是这样,vue源码还有好多细节比如:

    • diff算法
    • 防止dep里面多次收集到同一个watcher
    • 异步模式下,多个属性更改,收集watcher到一个队列,在下一帧调用一遍队列即可,不必反复触发同一个watcher.
    • computed属性的watcher生成和收集
    • watch属性的watcher生成和收集

    相关文章

      网友评论

          本文标题:vue 源码运行流程解析

          本文链接:https://www.haomeiwen.com/subject/sudjeqtx.html