vue2源码学习-数据驱动

20 篇文章 0 订阅
8 篇文章 0 订阅

数据驱动

前言

  1. 跟这黄轶大神学习的 vue,做一些简单记录。
  2. 这部分对应原文中的数据驱动,整体流程只涉及 vue 初始化渲染到页面上,不包括编译过程、vnode diff,响应式,
  3. 以下均为自己的理解,不对之处请指正
  4. 本文地址及自己添加的注释在这里

new Vue

init 之前

init 之前是做了很多初始化的工作的。比较重要的:

vue/src/platforms/web/runtime/index.js

  1. directives (v-model v-show) 平台定义的组件(transion transion-group)放到 options 里

extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);

  1. 给 vue 一个__patch__方法, 用于后续 vnode 更新, 属于核心方法
    Vue.prototype.__patch__ = inBrowser ? patch : noop;
  2. 暴露api $mount,用于后续挂载节点到页面,属于核心流程
    Vue.prototype.$mount = ...

vue/src/core/index.js

这里主要是 initGlobalAPI(Vue)方法,主要做了以下三件事

  1. 暴露一些 api,比如 use mixin extend filter directive,这些我们开发中都会用到
  2. 初始化 options['components'|'directive'|'filter'],这里会记录该 vue 组件中注册的所有组件、指令和过滤器,作用类似于供 vue 判断某个组件是不是在 vue 里注册了,没有注册的话就报错
  3. 把 vue 自带的组件如keep-alive 注册到 vue 里

vue/src/core/instance/index.js

这里也是在挂载并暴露 apidata $props $set $del $watch $on #once $off $emit update $forceupdate $destory

init vue/src/core/instance/init.js

init主要也是在做两件事

  1. 初始化
  2. 调用两个生命周期钩子 beforeCreate create
    这里对初始化过程做进一步描述

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

这里其实就是对用户传入的 optionsextend 的 options mixin 的 options进行一次合并,对于三者同一属性的优先级处理是有一个策略的,详细在vue/src/core/util/options.js

initLifecycle vue/src/core/instance/lifecycle.js

  1. $parent $children 记录两个vm之前的父子关系
  2. $root 记录根组件

initRender vue/src/core/instance/render.js

  1. 绑定_c() $createElement()方法 用于后续生成vnode,核心流程(_c 是内部调用,$createElement 是用户手写render时调用)
  2. 暴露 api $slots $attrs $listeners 属性、方法

其他

初始化 inject 、props methods data computed watcher provide

$mount

$mount 之前 vue/src/platforms/web/entry-runtime-with-compiler.js

$mount 的功能是把 vue 渲染的元素挂载到页面上,这里有两种情况

  1. 用户手写 render,不需要编译,直接走$mount 方法,
  2. template 语法,需要编译,此时需要先编译,在走$mount 方法
    编译的结果就是往 options 上挂载 render 、staticRenderFns 方法
// vue/src/platforms/web/entry-runtime-with-compiler.js
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;

$mount vue/src/core/instance/lifecycle.js fn mountComponent

new Watcher(
  vm,
  updateComponent,
  noop,
  {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, "beforeUpdate");
      }
    },
  },
  true /* isRenderWatcher */
);

这里其实初始化了一个渲染 watcher,在 watcher 里,此处触发一次updateComponent函数,此函数核心逻辑就是

vm._update(vm._render(), hydrating);
  1. vm._render()是生成vnode节点
  2. vm_update()是把vnode节点变成 html 元素并挂载到页面上

render vue/src/core/instance/render.js

render 的核心逻辑是

vnode = render.call(vm._renderProxy, vm.$createElement);

其中 render 有两种来源

  1. vue 编译而来,这涉及很复杂的编译过程,放在后面再细化
  2. 用户手写,比如,你可能见过这样的代码
new Vue({
  ...
  render: h => h(App)
}).$mount('#app')

从这里我们可以看到 h 其实就是 vm.$createElement

关于 render 的语法可以参考
现在来看看下用户手写render函数逻辑,即 vm.$createElement(app)的逻辑

//vue/src/core/instance/render.js
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);

$createElement 也就是在执行 createElement

createElement vue/src/core/vdom/create-element.js

关键流程

  1. 处理 children
  2. 生成 vnode 并返回
    1. 如果是组件 就 createComponent
    2. 其他情况就直接 new Vnode
...
if (normalizationType === ALWAYS_NORMALIZE) {
  children = normalizeChildren(children);
} else if (normalizationType === SIMPLE_NORMALIZE) {
  children = simpleNormalizeChildren(children);
}

...
if (组件) {
  vnode = createComponent(Ctor, data, context, children, tag);
} else {
  vnode = new VNode(tag, data, children, undefined, undefined, context);
}

处理 children vue/src/core/vdom/helpers/normalize-children.js

为啥要处理 children 呢?我理解是为了做新旧 vnode diff 的时候更快,也就是做一个优化,主要有以下两个优化点

  1. 相邻的两个 vnode 节点都是文本,那就可以合并成一个 vnode 节点
// merge adjacent text nodes
if (isTextNode(c[0]) && isTextNode(last)) {
  res[lastIndex] = createTextVNode(last.text + (c[0]: any).text);
  c.shift();
}
  1. 考虑这种情况
new Vue({
  render: h => h(
    'div'
    [
      [
        'a',
        'b',
      ],
      'c'
    ]
  )
})

'a b c’渲染到页面上应该是平级的,如果保留着这种结构的话会增大 diff 的难度。我理解以下两种情况会出现上面这样的结构

  1. 用户手写 render
  2. v-for(暂未验证)

_update vue/src/core/instance/lifecycle.js

render 之后已经得到了 vnode,剩下的就是把 vnode 变成 html 元素放到页面上,核心逻辑就是调用了__patch方法

// fn  _update
...
if (!prevVnode) {
  // initial render
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
  // updates
  vm.$el = vm.__patch__(prevVnode, vnode);
}
...

patch vue/src/core/vdom/patch.js

简单场景下(即 初始渲染vm.__patch__(vm.$el, vnode, hydrating, false)主要流程为:

  1. $el 元素当做一个空 vnode
  2. 获取改 vnode 的父 element,相邻 element
  3. createElm()
  4. removeVnodes([oldVnode], 0, 0)清除 oldvnode,如果 oldvnode 有任何 remove hook 的话要执行一次。在这步执行之前页面是有 oldvnode、vnode 两个元素的
  5. 执行invokeInsertHook(insertedVnodeQueue 记录了所有要 insert 的 vnode,这里执行 vnode 的 insert hook)

oldVnode = emptyNodeAt(oldVnode);

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

if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0);
}

invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);

createElm vue/src/core/vdom/patch.js

// 如果事组件的话执行执行createComponent,到此结束
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return;
  }

if (isDef(tag)) {
vnode.elm = vnode.ns
  ? nodeOps.createElementNS(vnode.ns, tag)
  : nodeOps.createElement(tag, vnode);


  createChildren(vnode, children, insertedVnodeQueue);
  if (isDef(data)) {
    invokeCreateHooks(vnode, insertedVnodeQueue);
  }
  insert(parentElm, vnode.elm, refElm);

}else if (isTrue(vnode.isComment)) {
  vnode.elm = nodeOps.createComment(vnode.text);
  insert(parentElm, vnode.elm, refElm);
} else {
  vnode.elm = nodeOps.createTextNode(vnode.text);
  insert(parentElm, vnode.elm, refElm);
}
  1. vnode 是组件的话走组件相关的逻辑,执行结束(暂时不看)
  2. 如果是注释节点或者本文节点的话直接生成内容挂载到 vnode.elm 上,然后插入到页面上(页面展示出 vnode 元素)
  3. 如果是普通的tag,如 div
    1. 生成 html 元素挂载到 vnode.elm 上
    2. createChildren()
    3. insert(),挂载到页面上

注意:

  1. createChildren 的逻辑很简单,就是把遍历 vnode 的 children,挨个执行 createElm(),所以是 children 先 insert 到 vnode 上,vnode 才 insert 到它的 vnode 上(这里也就是 document.body)
  2. createElm 执行完成之后页面上是有 oldvnode vnode 两个节点的 html 元素的
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值