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


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

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

    Vue初始化

    new Vue发生了什么

    Vue入口构造函数

    javascript
    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方法

    javascript
    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 并渲染出来

    javascript
    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._rendervm._update

    render

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

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

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

    javascript
    
    
    "app" style="color: red; background: yellow"> hello {{name}} world
    ƒ 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: 处理变量
    javascript
    // _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

    javascript
    // 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结构如下

    javascript
    export default class VNode {
      tag: string | void;
      data: VNodeData | void;
      children: ?Array;
      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,
        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 个,一个是首次渲染,一个是数据更新的时候。我们暂时先不考虑数据更新部分

    javascript
    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 节点中,然后删除老节点

    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算法,暂时先不考虑
      }
    }

    参考文档

    什么是虚拟DOM?


    __EOF__

  • 本文作者: 柏成
  • 本文链接: https://www.cnblogs.com/burc/p/17254661.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    【COMP305 LEC6 LEC 7】
    盘点 10 个 GitHub 上的前端高仿项目
    【Vue3】穿梭框 -- 思路与实现分析
    一个纯Python构建的Web应用框架
    vs中集成vcpkg
    win server 2012 r2 部署 netcore 站点 500.19
    重新认识Java
    Weblogic SSRF 漏洞复现
    vue 向 docx模板中填充数据生成目标docx 文件
    IPv6环境下的网络安全观察报告总结
  • 原文地址:https://www.cnblogs.com/burc/p/17254661.html