adodo0829 / blog

搭建知识体系
29 stars 4 forks source link

new Vue 的过程梳理 #70

Open adodo0829 opened 4 years ago

adodo0829 commented 4 years ago

new Vue 的过程

在我们new一个Vue对象的时候, 现在我们就从源码看看经过了哪些流程

1.Vue 构造函数

new Vue(options)这一步调用了实例方法_init

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)) {
    warn('Vue should be called with the `new` keyword')
  }
  // 初始化
  this._init(options)
}

2.Vue 实例方法_init(options)

Vue.prototype._init = function (options?: Object) {
    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-init:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    /*一个防止vm实例自身被观察的标志位*/
    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(
        // 解析constructor上的options属性的
        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
    /*初始化生命周期*/
    initLifecycle(vm)
    /*初始化事件*/
    initEvents(vm)
    /*初始化render*/
    initRender(vm)
    /*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
    callHook(vm, 'beforeCreate')
    // 初始化 inject, 在数据/Props之前注入 
    initInjections(vm) 
    /*初始化props,methods,data,computed与watch*/
    initState(vm)
    // 初始化 provide
    // provide 选项应该是一个对象或返回一个对象的函数。
    // 该对象包含可注入其子孙的属性,用于组件之间通信
    initProvide(vm)
    /*调用created钩子函数并且触发created钩子事件*/
    callHook(vm, 'created')

    /* istanbul ignore if */
    // 性能监控
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      /*格式化组件名*/
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      /*挂载组件*/
      vm.$mount(vm.$options.el) // runtime 不能编译模板 runtime-with-compiler template
      // 注意 new Vue(options).$mount()为手动挂载
      // 在项目中可用于延时挂载
      //(例如在挂载之前要进行一些其他操作,判断, 加入路由插件等等)之后要手动挂载上。
    }
  }

2.挂载组件 调用vm.$mount方法

#### 初始化每个环节干了啥?
初始化处理了我们平时写在.vue文件下script中导出的对象{}中的各个属性;
也就是我们的组件实例...

- 1.合并options参数

mergeOptions方法: 递归合并父子的 options 选项

- 2.initProxy: 初始化代理操作(开发环境下)
```js
function initProxy(vm) {
      if (hasProxy) {
        var options = vm.$options;
        var handlers = options.render && options.render._withStripped
          ? getHandler  
          : hasHandler; 
        // 实例化代理对象,代理操作, 在开发环境下给出 warning 提示
        vm._renderProxy = new Proxy(vm, handlers);
      } else {
        // 如果不能代理直接赋值
        vm._renderProxy = vm;
      }
    };
  }

//更新组件事件 function updateComponentListeners(vm, //虚拟dom listeners, //新的数据队列 oldListeners //旧的事件数据队列 ) { target = vm; //更新数据源 并且为新的值 添加函数 旧的值删除函数等功能 updateListeners(listeners, oldListeners || {}, add, remove$1, vm); target = undefined; }

- 5.initRender: 初始化渲染节点
```js
function initRender(vm) {
    vm._vnode = null; // the root of the child tree 上一个 vonde
    vm._staticTrees = null; // v-once cached trees v-once缓存的树
    var options = vm.$options; //获取参数
    var parentVnode = vm.$vnode = options._parentVnode; // the placeholder node in parent tree 父树中的占位符节点
    var renderContext = parentVnode && parentVnode.context; // this 上下文
    //判断children 有没有分发式插槽 并且过滤掉空的插槽,并且收集插槽
    vm.$slots = resolveSlots(options._renderChildren, renderContext);
    vm.$scopedSlots = emptyObject;
    //将createElement fn绑定到这个实例
    //这样我们就得到了合适的渲染上下文。
    //内部版本由模板编译的呈现函数使用

    //创建虚拟dom的数据结构
    vm._c = function (a, b, c, d) {
      return createElement(
        vm, //vm  new Vue 实例化的对象
        a, //有可能是vonde或者指令
        b,
        c,
        d,
        false
      );
    };
    // 用户自己编写的渲染功能。
    vm.$createElement = function (a, b, c, d) {
      return createElement(vm, a, b, c, d, true);
    };
    var parentData = parentVnode && parentVnode.data; //获取父vnode
    // ...
}
// 1.初始化的$mount 方法
// ***********************************************
Vue.prototype.$mount = function (el,hydrating) {
    el = el && inBrowser ? query(el) : undefined;
    // 挂载组件方法执行
    return mountComponent(this,el,hydrating)
  }

// 2.真正执行挂载 DOM 的是 runtime with compiler版本的$mount 方法
// 此时 Vue.prototype.$mount 会被重写
// ***********************************************
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el) // 根节点挂载元素

  // 1.组件挂载节点不能是 html 和 body
  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
  if (!options.render) {
    let template = options.template

    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }

    if (template) {
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      // 将模板转换成 render函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating) // 还是会执行 mountComponent
}

// 3.挂载组件方法: 核心是 _update 更新vnode
// ***********************************************
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      // 省略....做了一些提示
    }
  }
  callHook(vm, 'beforeMount') // 挂载前调用

  let updateComponent // 更新方法

  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      // 省略...这里做了一些标记
    }
  } else {
    // Watcher对象会通过调用updateComponent方法来更新视图
    updateComponent = () => {
      // _render函数会返回一个新的Vnode节点,传入_update中与旧的VNode对象进行对比,
      // 经过一个patch的过程得到两个VNode节点的差异,最后将这些差异渲染到真实环境形成视图
      vm._update(vm._render(), hydrating)
    }
  }

  new Watcher(vm, updateComponent, noop, { // 添加一个渲染watcher实例
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        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')
  }
  return vm // 返回组件实例
}

// 4.patch过程, 新旧 vnode 的比较, 并生成真实 dom
// 核心是diff算法: 通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式
// ***********************************************
/*createPatchFunction的返回值,一个patch函数*/
// createElm: 创建真实元素并插入
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    /*vnode不存在则直接调用销毁钩子*/
    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
      /*oldVnode未定义的时候,其实也就是root节点,创建一个新的节点*/
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      /*标记旧的VNode是否有nodeType*/
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        /*是同一个节点的时候直接修改现有的节点*/
        patchVnode(oldVnode, vnode, insertedVnodeQueue, 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)) {
            /*当旧的VNode是服务端渲染的元素,hydrating记为true*/
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            /*需要合并到真实DOM上*/
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              /*调用insert钩子*/
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'xxx'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          /*如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它*/
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        /*取代现有元素*/
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        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)
        )

        if (isDef(vnode.parent)) {
          // component root element replaced.
          // update parent placeholder node element, recursively
          /*组件根节点被替换,遍历更新父节点element*/
          let ancestor = vnode.parent
          while (ancestor) {
            ancestor.elm = vnode.elm
            ancestor = ancestor.parent
          }
          if (isPatchable(vnode)) {
            /*调用create回调*/
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, vnode.parent)
            }
          }
        }

        if (isDef(parentElm)) {
          /*移除老节点*/
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          /*调用destroy钩子*/
          invokeDestroyHook(oldVnode)
        }
      }
    }
    /*调用insert钩子*/
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

// 5.diff 过程: patchVnode方法
// TODO:
// ***********************************************
// 1.判断同一个VNode(sameVnode),则会进行深度的比较,得出最小差异,
// 否则直接删除旧有DOM节点,创建新的DOM节点。

总结

1.new Vue之后,对生命周期,事件中心,data,props等进行了初始化

2.手动调用$mount函数对$el节点进行渲染。 渲染的过程中,Vue只认render函数,那么就会先将template转换为render函数(mountComponent函数)。

3.调用render函数生成vnode。怎么生成? 主要是利用createELement函数,先将vnode的children处理为一维数组,然后通过判断tag来生成vnode。

4.生成vnode之后,就是通过patch函数转换为dom节点,渲染在视图上。vnode转换为dom节点 vnode是如何转换为dom节点的: 在vnode中,有个elm用来保存对应的dom节点,因此不用再额外生成dom(除了文本节点以外. 只是需要不深度遍历children,将children中的dom节点添加到父节点中,即完成dom树的构建