Vue

50.Vue组件挂载的过程分析

2022-02-12  本文已影响0人  wo不是黄蓉

上一篇文章理了一下vue初始化的过程,本章来理一下vue的挂载过程。上一篇初始化相关内容->看这里
同样还是从入口文件开始:

//入口
function Vue(options) {
    //进行一些初始化操作
  this._init(options);
}

1.初始化 src\core\instance\init.js
可以看到在初始化完成之后会调用mount方法进行挂载,接下来我们看看mount中做了哪些事情?
$mount是挂载到vm上的方法,因此我们去Vue的原型上找这个方法,发现是在entry-runtime-with-compiler.js中进行声明的,接下来看看mount中做了什么事情。

export function initMixin(Vue: Class<Component>) {
  //在initMinxin里面定义Vue的原型方法_init
  Vue.prototype._init = function (options?: Object) {
    // expose real self
    vm._self = vm; //把vm放在_self属性上暴露出去
    //一些初始化的工作
    callHook(vm, "created");
      //调用mount方法进行挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}

2.编译阶段 src\platforms\web\entry-runtime-with-compiler.js

Vue.prototype.$mount = function (
  el ? : string | Element,
  hydrating ? : boolean
): Component {
  el = el && query(el)
 //a.判断el是否是body或者html标签
  if (el === document.body || el === document.documentElement) { 
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  const options = this.$options
  // resolve template/el and convert to render function
//b.判断是否有render,render函数的优先级大于一切
  if (!options.render) { 
    let template = options.template
    if (template) {
 //获取模板的操作。。。省略
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      const {
        render,
        staticRenderFns
      } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this) //c.把模板语法变编译render函数,把编译好的东西挂载到render函数上
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }

  //d.进行挂载
  return mount.call(this, el, hydrating)
}

3.编译阶段->将template字符串编译成ast src\compiler\to-function.js
判断有模板的合法性之后,compileToFunctions这个函数将模板字符串编译成渲染render函数

compileToFunctions进去可以看到这个函数其实是由createCompileToFunctionFn创建的来的

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    // compile--重点来了
    const compiled = compile(template, options)
    return (cache[key] = res)
  }
}

src\compiler\create-compiler.js

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
        //编译模板,将编译结果返回
      const compiled = baseCompile(template.trim(), finalOptions)
     
      return compiled
    }
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

4.生成vnode src\compiler\index.js

export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  //生成ast
  const ast = parse(template.trim(), options);
  //没有配置优化项
  if (options.optimize !== false) {
    //自动进行优化,标记静态节点等
    optimize(ast, options);
  }
  //将抽象语法树编译为render函数->目前只是_c写出需要创建什么
  //返回内容是一个with函数with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_c('child',{attrs:{\"test\":test}})],1)}
  const code = generate(ast, options);
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  };
});

src\core\vdom\patch.js

export function parse(
  template: string,
  options: CompilerOptions
): ASTElement | void {
  const stack = [];

  //核心代码
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    //遇到开始标签使用start进行解析
    start(tag, attrs, unary, start, end) {
      //遇到开始标签,就创建一个ast对象
      let element: ASTElement = createASTElement(tag, attrs, currentParent);
    },

    end(tag, start, end) {
      closeElement(element);
    },

    chars(text: string, start: number, end: number) {},
    comment(text: string, start, end) {
    },
  });
  //将重构的ast返回
  return root;
}

5.挂载阶段 src\compiler\index.js
const code = generate(ast, options);->最终的返回结果code就是_c('div',{attrs:{"id":"app"}},[_c('child',{attrs:{"test":test}})],1)

最后到了mount.call(this, el, hydrating)进行挂载的地方

调用mountComponent()方法-->开始创建Vnode

第一次的时候prevNode为空,patch的时候oldVnode为空,创建一个空的vnode节点,接下来根据最新的vnode创建node节点
然后根据vnode.componentInstance来判断是否需要创建子组件?
如果需要走createComponent方法进行初始化子组件,否则进行原生标签创建。

export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  //挂载组件
  vm.$el = el;
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
  }
    //组件挂载前调用的钩子函数
  callHook(vm, "beforeMount");
  let updateComponent;
    //用户$mount时,定义updateComponent
    updateComponent = () => {
      vm._update(vm._render(), hydrating);
    };
  // 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
  //在执行组件实例时,每一个实例都有一个watcher与之对应,如果一个组件的数据发生改变,我们只会调用改变的组件里面的渲染函数和更新函数,合理的切割组件粒度
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, "beforeUpdate");
        }
      },
    },
    true /* isRenderWatcher */
  );
  //hydrating:watcher api使用的,用来判断是否加载最新的内容
  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");
  }
  return vm;
}

createComponent创建组件,怎样判断是否是一个子组件呢?看下面代码,注释中其实已经告诉我们答案了

src\core\vdom\patch.js

在此判断是否需要创建子组件

  function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data;
    if (isDef(i)) {
      //如果vnode是一个子组件,子组件实例已经创建并且已经挂载了,子组件也设置了是否是vnode的标识符。
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        //初始化组件,从子组件到父组件
        initComponent(vnode, insertedVnodeQueue);
        //插入html内容
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true;
      }
    }
  }

Q:怎么判断需要创建组件呢?
A:isDef(vnode.componentInstance)

Q:怎么走到创建元素的?
A:构建完vnode之后,需要根据vnode构建出node节点

Q:patch的时候为什么要创建元素?
A:patch的过程就是将vnode转成真实node的过程,在patch的过程通过vnode反映出最新节点的变化情况,方便对真实节点进行操作。

在mount的时候发现有需要创建的子组件,此时再重新开始对子组件进行初始化,创建子组件。
挂载时可以看到会执行两次的vm.$mount(vm.$options.el);第一次是创建vue的时候,第二次是进行子组件挂载的时候。子组件挂载的时机是在mount阶段的,此时el元素是没有值的,在mount.call(this, el, hydrating)el为undefined

在挂载子组件的时候会创建一个watcher,此时watcher是用来监听数据的改变,将通知发送到对应的组件

挂载完成后,开始进行初始化组件工作initComponent(vnode, insertedVnodeQueue);

6.插入元素&渲染阶段
组件创建完成后,将子组件插入到父组件中,在src\core\vdom\patch.js->createComponent可以看出来。

在createElm insert()有子组件的时候,判断是否需要创建子组件需要return,返回结果就是一个vnode,然后又通过createElement进行创建子组件节点。

挂载的顺序时先父后子,插入的顺序时先子后父,调用完insert之后,会发现界面上已经出现了child。
PS:对源码的理解可能还不是很深入,有问题欢迎进行指证与我进行讨论,共同学习,加油鸭!

最后附上我的demo:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <child :test="test"></child>
    </div>
  </body>

  <script>
    //定义全局组件
    let child = Vue.component("child", {
      template: "<div>{{test}}</div>",
      props: ["test"],
    });
    // let componentA = new Vue({

    // })
    const vm = new Vue({
      el: "#app",
      // component: child,
      data() {
        return {
          test: "child",
        };
      },
    });
  </script>
</html>

上一篇 下一篇

猜你喜欢

热点阅读