【Vue2.x源码系列03】数据驱动渲染(Render、Update)

上一篇文章我们介绍了 Vue2模版编译原理,这一章我们的目标是弄清楚模版 template和响应式数据是如何渲染成最终的DOM。数据更新驱动视图变化这部分后期会单独讲解

我们先看一下模版和响应式数据是如何渲染成最终DOM 的流程

Vue初始化

new Vue发生了什么

Vue入口构造函数

highlighter- reasonml

function Vue(options) {
  this._init(options) // options就是用户的选项
  ...
}

initMixin(Vue) // 在Vue原型上扩展初始化相关的方法,_init、$mount 等
initLifeCycle(Vue) // 在Vue原型上扩展渲染相关的方法,_render、_c、_v、_s、_update 等

export default Vue

initMixin、initLifeCycle方法

highlighter- reasonml

export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this
    vm.$options = options // 将用户的选项挂载到实例上

    // 初始化数据
    initState(vm)

    if (options.el) {
      vm.$mount(options.el) 
    }
  }

  Vue.prototype.$mount = function (el) {
    const vm = this
    el = document.querySelector(el)
    let ops = vm.$options

    // 这里需要对模板进行编译
    const render = compileToFunction(template)
    ops.render = render

    // 实例挂载
    mountComponent(vm, el) 
  }
}

export function initLifeCycle(Vue) {
  Vue.prototype._render = function () {} // 渲染方法
  Vue.prototype._c = function () {} // 创建节点虚拟节点
  Vue.prototype._v = function () {} // 创建文本虚拟节点
  Vue.prototype._s = function () {} // 处理变量
  Vue.prototype._update = function () {} // 初始化元素 和 更新元素
}

在 initMixin 方法中,我们重点关注 compileToFunction模版编译 和 mountComponent实例挂载 2个方法。我们已经在上一篇文章详细介绍过 compileToFunction 编译过程,接下来我们就把重心放在 mountComponent 方法上,它会用到在 initLifeCycle 方法给Vue原型上扩展的方法,在 render 和 update章节会做详细讲解

实例挂载

mountComponent 方法主要是 实例化了一个渲染 watcher,updateComponent 作为回调会立即执行一次。watcher 还有一个其他作用,就是当响应式数据发生变化时,也会通过内部的 update方法执行updateComponent 回调。

现在我们先无需了解 watcher 的内部实现及其原理,后面会作详细介绍

vm._render 方法会创建一个虚拟DOM(即以 VNode节点作为基础的树),vm._update 方法则是把这个虚拟DOM 渲染成一个真实的 DOM 并渲染出来

highlighter- reasonml

export function mountComponent(vm, el) {
  // 这里的el 是通过querySelector获取的
  vm.$el = el

  const updateComponent = () => {
    // 1.调用render方法创建虚拟DOM,即以 VNode节点作为基础的树
    const vnode = vm._render() // 内部调用 vm.$options.render()

    // 2.根据虚拟DOM 产生真实DOM,插入到el元素中
    vm._update(vnode)
  }

  // 实例化一个渲染watcher,true用于标识是一个渲染watche
  const watcher = new Watcher(vm, updateComponent, true)
}

接下来我们会重点分析最核心的 2 个方法:vm._render 和 vm._update

render

我们需要在Vue原型上扩展 _render 方法

highlighter- vim

Vue.prototype._render = function () {
  // 当渲染的时候会去实例中取值,我们就可以将属性和视图绑定在一起
  const vm = this
  return vm.$options.render.call(vm) // 模版编译后生成的render方法
}

在之前的 Vue $mount过程中,我们已通过 compileToFunction方法将模版template 编译成 render方法,其返回一个 虚拟DOM。template转化成render函数的结果如下

highlighter- xquery

<div id="app" style="color: red; background: yellow">
   hello {{name}} world
   <span></span>
</div>

ƒ anonymous(
) {
  with(this){
    return _c('div',{id:"app",style:{"color":"red","background":"yellow"}},
              _v("hello"+_s(name)+"world"),
              _c('span',null))
  }
}

render 方法内部使用了 _c、_v、_s 方法,我们也需要在Vue原型上扩展它们

  • _c: 创建节点虚拟节点(VNode)
  • _v: 创建文本虚拟节点(VNode)
  • _s: 处理变量

highlighter- ada

// _c('div',{},...children)
// _c('div',{id:"app",style:{"color":"red"," background":"yellow"}},_v("hello"+_s(name)+"world"),_c('span',null))
Vue.prototype._c = function () {
  return createElementVNode(this, ...arguments)
}

// _v(text)
Vue.prototype._v = function () {
  return createTextVNode(this, ...arguments)
}

Vue.prototype._s = function (value) {
  if (typeof value !== 'object') return value
  return JSON.stringify(value)
}

接下来我们看一下 createElementVNode 和 createTextVNode 是如何创建 VNode 的

createElement

每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个虚拟树结构,用于描述真实的DOM树结构,即我们的虚拟DOM

highlighter- actionscript

// h()  _c() 创建元素的虚拟节点 VNode
export function createElementVNode(vm, tag, data, ...children) {
  if (data == null) {
    data = {}
  }
  let key = data.key
  if (key) {
    delete data.key
  }
  return vnode(vm, tag, key, data, children)
}

// _v() 创建文本虚拟节点
export function createTextVNode(vm, text) {
  return vnode(vm, undefined, undefined, undefined, undefined, text)
}

// 虚拟节点
function vnode(vm, tag, key, data, children, text) {
  return {
    vm,
    tag,
    key,
    data,
    children,
    text,
    // ....
  }
}

VNode 和 AST一样吗?
我们的 VNode 描述的是 DOM元素
AST 做的是语法层面的转化,它描述的是语法本身 ,可以描述 js css html

虚拟DOM

DOM是很慢的,其元素非常庞大,当我们频繁的去做 DOM更新,会产生一定的性能问题,我们可以直观感受一下div元素包含的海量属性

在Javascript对象中,Virtual DOM 表现为一个 Object对象。并且最少包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别。

实际上它只是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上

vue中 VNode结构如下

highlighter- arduino

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?

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions
  ) {
    /*当前节点的标签名*/
    this.tag = tag
    /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
    this.data = data
    /*当前节点的子节点,是一个数组*/
    this.children = children
    /*当前节点的文本*/
    this.text = text
    /*当前虚拟节点对应的真实dom节点*/
    this.elm = elm
    /*当前节点的名字空间*/
    this.ns = undefined
    /*编译作用域*/
    this.context = context
    /*函数化组件作用域*/
    this.functionalContext = undefined
    /*节点的key属性,被当作节点的标志,用以优化*/
    this.key = data && data.key
    /*组件的option选项*/
    this.componentOptions = componentOptions
    /*当前节点对应的组件的实例*/
    this.componentInstance = undefined
    /*当前节点的父节点*/
    this.parent = undefined
    /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
    this.raw = false
    /*静态节点标志*/
    this.isStatic = false
    /*是否作为跟节点插入*/
    this.isRootInsert = true
    /*是否为注释节点*/
    this.isComment = false
    /*是否为克隆节点*/
    this.isCloned = false
    /*是否有v-once指令*/
    this.isOnce = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next https://github.com/answershuto/learnVue*/
  get child (): Component | void {
    return this.componentInstance
  }
}

虚拟DOM的优点😍😍😍

  1. 提升效率。操作 DOM的代价是昂贵的,使用 diff算法,可以减少 JavaScript操作真实DOM 带来的性能消耗

通过 Virtual DOM 改变真正的 DOM并不比直接操作 DOM效率更高。恰恰相反,Virtual DOM 仍需要调用 DOM API 去操作 DOM,并且还会额外占用内存。but!!!我们可以通过 diff算法,找到需要更新的最小单位,最大限度地减少DOM操作。而且在大量频繁数据更新后,并不会立即重流重绘,而是批量操作真实的 DOM,最大限度的减少DOM操作,从而提升性能

  1. 跨平台。抽象了原本的渲染过程,提供了一个中间抽象层(runtime-dom/src/nodeOps),使我们可以在不接触真实DOM 的情况下操作 DOM,实现了跨平台的能力。而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,也可以是近期很火热的小程序。

runtime-dom/src/nodeOps 这里存放常见 DOM操作API,不同运行时(浏览器、小程序......)提供的具体实现不一样,最终将操作方法传递到 runtime-core中,所以 runtime-core不需要关心平台相关代码

update

vm._update 的作用就是把 VNode 渲染成真实的DOM

vm._update 被调用的时机有 2 个,一个是首次渲染,一个是数据更新的时候。我们暂时先不考虑数据更新部分

highlighter- vim

Vue.prototype._update = function (vnode) {
  // 将vnode转化成真实dom
  const vm = this
  const el = vm.$el
  // patch既有初始化元素的功能 ,又有更新元素的功能
  vm.$el = patch(el, vnode)
}

vm._update 核心就是调用 patch 方法,parentElm 就是 oldVNode 的父元素,即我们的 body 节点,通过 createElm 递归创建一个完整的 DOM树 并 插入到 body 节点中,然后删除老节点

highlighter- javascript

// 利用vnode创建真实元素
function createElm(vnode) {
  let { tag, data, children, text } = vnode
  if (typeof tag === 'string') {
    // 标签
    vnode.el = document.createElement(tag) // 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
    patchProps(vnode.el, data)
    children.forEach(child => {
      vnode.el.appendChild(createElm(child))
    })
  } else {
    vnode.el = document.createTextNode(text)
  }
  return vnode.el
}

// 对比属性打补丁
function patchProps(el, props) {
  for (let key in props) {
    if (key === 'style') {
      // { color: 'red', "background": 'yellow' }
      for (let styleName in props.style) {
        console.log(styleName, props.style[styleName])
        el.style[styleName] = props.style[styleName]
      }
    } else {
      el.setAttribute(key, props[key])
    }
  }
}

// patch既有初始化元素的功能 ,又有更新元素的功能
function patch(oldVNode, vnode) {
  // 写的是初渲染流程
  const isRealElement = oldVNode.nodeType
  if (isRealElement) {
    const elm = oldVNode // 获取真实元素
    const parentElm = elm.parentNode // 拿到父元素
    let newElm = createElm(vnode)

    parentElm.insertBefore(newElm, elm.nextSibling)
    parentElm.removeChild(elm) // 删除老节点

    return newElm
  } else {
    // diff算法,暂时先不考虑
  }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

supeerzdj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值