IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 说说Vue的核心特点(一,数据驱动) -> 正文阅读

[JavaScript知识库]说说Vue的核心特点(一,数据驱动)

? ? ? ? Vue.js的核心思想之一就是数据驱动。所谓数据驱动就是视图是依靠数据驱动实现的,我们对视图的修改不会直接去操作DOM,而是通过操作数据,Vue帮我们修改视图。相比于传统的jQuery的原生DOM开发,大大简化了代码量,而且用户体验也更加友好,不需要花费大量精力去维护。尤其是在业务量十分复杂的时候,只关心数据的改变,会让代码的逻辑更加清晰,也更易于维护。

下面我们基于Vue的源码来分析,数据是如何渲染成最终DOM的

首先我们来看看new Vue 的时候发生了什么(建议在vue官网下载一份源码来学习)

????????我们先从入口文件开始看起,“src/core/instance/index.js”, 这个文件非常简单,其实就是调用了一个初始化函数(this._init(options)),再找到“src/core/instance/init.js” ,其中该初始化函数主要做了以下几件事,合并配置、初始化生命周期(initLifecycle(vm))、初始化事件中心(initEvents(vm))、初始化渲染(initRender(vm))、初始化data(initState(vm))、初始化props(initProvide(cm))、computed、watcher。之后做了最重要的一件事,就是将vm挂载值容器上了(vm.$mount(vm.$options.el)),下面我们接着分析Vue的挂载过程。

Vue中我们是使用$mount方法挂载vm实例的,这个方法在多个文件中都有定义,因为$mount方法的实现是与平台,构建方式挂钩的,所以对于不同平台的vue实现,同样也有着不同的$mount方法,这里我们主要针对compiler版本的进行分析,具体文件在“src/platform/web/entry-runtime-with-compiler.js” ,这段代码主要是对Vue原型上的$mount进行了重写,首先,他对el进行了响应的限制,不能是body、html这样的根节点。然后是一段很关键的逻辑,如果我们没有自己定义render方法,那么它会将el或者template转换为render方法,不管我们是用来单文件的形式(.vue),还是写了el或者template属性,最终都会转换为render方法,这个过程是Vue的一个在线编译的过程,这个编译是通过complierToFuntions,感兴趣的可以去看看。(这里我提一嘴,vue属于是一种编译时+运行时的一种框架),最后调用了原型上的$mount挂载,该方法支持传入两个参数,第一个参数为el,表示挂载的容器元素,可以使字符串,也可以是DOM对象,如果是字符串,会在浏览器环境下转换为DOM对象。第二个参数是与服务端渲染有关的(hydrating),最终$mount回去调用一个,mountCompont方法,该方法定义在“src/core/instance/lifecycle.js” ,在该方法中,主要定义了一个watcher,会调用调用了上面定义的updateComponent方法,在这个方法中,先调用了_render()方法生成虚拟Node,最终调用vm._update方法更新(生成)DOM。这里定义的watcher有两个作用,第一个作用就是初始化执行回调函数,?第二个作用就是监测vm中的数据变化,从而调用回调函数更新DOM。最后判断了一下当前实例是否已经挂载了,挂载过了则会调用响应的钩子(beforeUpdate),没挂载也会调用响应的钩子(mounted),这里的vm.$node,表示的是父虚拟节点,当为null时表示没有挂载。

然后让我们知道了,最终核心的方法是_render(),以及_update()

? ? ? ? 我们先来看看_render方法,该方法是实例上的一个私有方法,用来将实例转化为虚拟Node,定义在文件“src/core/instance/render.js”中。嗯...一般情况下我们都不会手写render方法,一般都是我们写好template模板,然后在挂载之前编译成render,而编译的这个过程较为复杂,在这不展开细说,可能有些人也不理解render方法是个什么样的东西,可以看看下列代码:

<-- 模板 -->
<div id="app">
  {{ message }}
</div>

?编译成render

render: function (createElement) {
  return createElement('div', {
     attrs: {
        id: 'app'
      },
  }, this.message)
}

会到_render方法中,render方法的调用,根据这行代码

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

?其实是调用了$createElement方法,所以我们需要跳到createElement方法中继续分析(这里的vm.createElement其实就是定义在一开始初始化渲染的函数中(vm.initRender(options),而除了定义了这个方法,还定义了一个vm._c方法,他是在将模板编译成render的时候调用的,而$createElement是用来解析用户手写render的,两个方法支持的参数都一样,最终都是去调用了createElement方法,返回一个虚拟Node),然后则是根据虚拟Node最终调用_update方法。

在分析最后的createElement和_update之前我们可以先来了解一下虚拟DOM的概念,如果已经知道,可以直接跳过

????????Virtual DOM这个名词相比大家在一开始接触Vue的时候就已经听过很多遍了,那么既然我们以及有真实的DOM,为什么还需要整个虚拟DOM出来,我们可以试着去浏览器中执行一下一下代码:

var div = document.createElement("div")
let str = ""
for(let key in div) {
    console.log(str += key + " ")
}

可以发现,打印出了特别多的属性,真实的DOM是十分庞大的,因为浏览器把DOM设计的十分复杂。当我们频繁更新真实DOM的时候,就有很大的性能问题。那么虚拟DOM则是用js对象去描述DOM节点,所以它比创建一个真实的DOM要轻松的多。在Vue.js中虚拟DOM是用一个VNode的class去描述的(“src/core/vdom/vnode.js”),但是这个类中其实还是比较复杂的,实际上Vue.js中是借鉴了一个第三方库snabbdom,然后添加了Vue.js中的特色的一些东西。而虚拟DOM的出现就是为了映射真实的DOM结构,不需要 类似DOM身上的那些API,所以更加轻量简单。

? ? ? ? 了解完虚拟DOM之后我们会到之前的地方,通过createElement方法创建虚拟DOM,该方法定义在“src/core/vdom/create-element.js”,嗯... 该方法其实是对_createElement方法的封装维护,使传入的参数更加灵活,处理完参数之后,就去调用了_createElement,该方法就在上面方法的下面,_createElement主要的两个流程就是,规范化children、创建VNode,该方法有五个参数,分别是context(VNode上下文环境,Component类型)、tag(标签名,可以使字符串,也可以是Component类型)、data(VNode的数据,VNodeData类型数据),children(子节点)、normalizationType(子节点规范的类型,主要是参考render是用户手写的还是生成的),可以看到,只有第四个参数children是没有做任何类型限制的,所以_createElement第一个首要的任务就是,规范子节点的数据类型,这里根据normalizationType的不同,调用了两个方法,分别是,normalizeChildren(children)、simpleNormalizeChildren(children),他们定义在“src/core/vdom/helpers/normalzie-children.js”中。simpleNormalizeChildren方法调用场景为render函数是编译生成的,normalizeChildren方法的调用场景有两种,一个是render方法是有用户自己手写的,另一个场景是编译slot、v-for这种会产生嵌套数组的时候。该方法接受两个参数,第一个参数是children,表示要规范的子节点,第二个参数为nextedIndex,表示嵌套的索引,由于单个子节点的类型可能是基本类型,也可能是一个数组,也可能是一个列表,所以当为数组的时候就递归调用该方法,为基本类型的时候就调用createTextNode方法转换为VNode类型,当为列表且还存在嵌套的时候则根据嵌套索引更新Key;值得注意的是,这里的三总情况都做了一个操作,就是如果存在两个连续的text节点,会对其做合并操作。经过以上children规范之后,children就变成了一个VNode的Array了,接下来,就是通过createElement创建生成VNode了。

? ? ? ? 回到createElement函数,规范完之后,对tag进行判断,如果是个字符串,则继续判断是否是内置的标签节点,如果是,则直接创建一个普通的VNode,如果不是内置的,而是一个已注册的组件名,则通过createComponent创建组件类型的VNode,否则创建一个未知标签的VNode;那如果tag不是一个字符串,而是一个Component类型的话,就直接通过createComponent创建组件类型的VNode,最后返回创建完成的VNode,去到_update渲染成真实DOM挂载到页面

????????_update()方法是实例的一个私有方法,被调用的时机有两个,一个是首次渲染的时候,一个是数据更新的时候;这次我们只看首次渲染的时候,该方法定义在“src/core/instance/lifecycle.js”,该方法核心就是调用了vm.__patch__方法,嗯.....这个方法也存在不同平台的差异,我们只分析web版本的,定义在“src/platforms/web/runtime/index.js”中,可以注意到,是否为服务端渲染也会影响__patch__方法的执行,因为服务端渲染没有真实的浏览器DOM环境,所以也不需要将虚拟DOM转为真实的DOM,在浏览器端,最终指向了patch方法,该方法定义在“src/platforms/web/runtime/patch.js”,该方法最终是调用了另一个方法的返回值(createFunction,定义在“src/core/vdom/patch.js”),这个方法最终返回了一个方法给到patch调用;其中调用createFunction的时候传入了两个参数,一个是nodeOps(封装了一些DOM方法),另一个参数是modules(定义了一些模块钩子的实现);接着我们来分析下其内部做了什么事,主要看看返回的patch方法,该方法接受四个参数,oldNode表示旧的VNode节点,可以为null,或者是一个DOM对象vnode表示通过_render渲染出来的VNode节点,hydrating表示是否为服务端渲染,removeOnly是用来给transtion-group使用的,这里不做拓展;其实这里主要的参数就是前两个;这里由于逻辑分支太复杂,我们用文章上面的一段render作为例子:

var app = new Vue({
  el: '#app',
  render: function (createElement) {
    return createElement('div', {
      attrs: {
        id: 'app'
      },
    }, this.message)
  },
  data: {
    message: 'Hello Vue!'
  }
})

此时的四个参数就分别为,第一个参数就是传入的vm.$el的一个DOM对象,表示的就是index.html模板中的<div id="app"></div>,第二个参数则是上面render函数的返回值,最后两个参数都为false,确定参数之后我们来看看patch的几个关键逻辑,如下:

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)) {
      oldVnode.removeAttribute(SSR_ATTR)
      hydrating = true
    }
    if (isTrue(hydrating)) {
      if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
        invokeInsertHook(vnode, insertedVnodeQueue, true)
        return oldVnode
      } else if (process.env.NODE_ENV !== 'production') {
        warn(
          'The client-side rendered virtual DOM tree is not matching ' +
          'server-rendered content. This is likely caused by incorrect ' +
          'HTML markup, for example nesting block-level elements inside ' +
          '<p>, or missing <tbody>. Bailing hydration and performing ' +
          'full client-side render.'
        )
      }
    }      
    // either not server-rendered, or hydration failed.
    // create an empty node and replace it
    oldVnode = emptyNodeAt(oldVnode)
  }

  // replacing existing element
  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)
  )
}

因为我们传入的oldVNode是一个DOM容器,所以这里isRealElement判断为true,然后就通过emptyNodeAt方法将oldVNode转化为VNode对象,然后调用createEle方法,该方法十分重要,让我们来看看:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // This vnode was used in a previous render!
    // now it's used as a new node, overwriting its elm would cause
    // potential patch errors down the road when it's used as an insertion
    // reference node. Instead, we clone the node on-demand before creating
    // associated DOM element for it.
    vnode = ownerArray[index] = cloneVNode(vnode)
  }

  vnode.isRootInsert = !nested // for transition enter check
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  if (isDef(tag)) {
    if (process.env.NODE_ENV !== 'production') {
      if (data && data.pre) {
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }

    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)
    setScope(vnode)

    /* istanbul ignore if */
    if (__WEEX__) {
      // ...
    } else {
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      insert(parentElm, vnode.elm, refElm)
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
      creatingElmInVPre--
    }
  } 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)
  }
}

该方法的作用就是通过虚拟DOM创建为真实的DOM插入到父节点中;这createComponent是尝试创建子组件,这里不做拓展,在当前我们这个例子的情况下,它的返回值为false;接下来判断VNode是否包含tag,如果包含,则需要对其做简单的合法性校验,然后再去调用平台的DOM方法创建一个占位元素,然后调用createChildren方法创建子节点,该方法比较简单,就是递归调用createEle创建节点(这里就是利用了深度搜索),这里在递归遍历的时候,会把父容器的DOM节点的占位元符传入;接着执行invokeCreateHooks执行所有的create钩子,将VNode? push到insertedVnodeQueue中;最终调用insert方法将DOM节点插入到父节点中(就是当初的第一个参数vm.$el),因为是递归调用,所以子节点会最先插入,所以顺序是先子后父;insert方法定义在“src/core/vdom/patch.js”,insert方法很简单,就是通过一些辅助方法将DOM插入到父节点中,而这些辅助方法也就是调用了原生DOM函数(定义在“src/platforms/web/runtime/node-ops.js”),绕了那么多到这,是不是突然豁然开朗了,本质上还是调用了原生方法动态的创建DOM;在这个例子中子节点就是一个文本节点,父节点就是id为app的div,实际就是递归创建了一颗完整的DOM树插入到了Body中;

最终附上Vue技术揭秘中的图片

?

?以上就是首屏数据驱动的过程了,想看更详细的讲解可以到数据驱动

摘要:数据驱动 | Vue.js 技术揭秘

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-08-19 18:55:10  更:2022-08-19 18:55:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 10:09:07-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码