vue 的模板引擎与渲染机制

模板引擎

vue 中如何解析模板,指令如何处理

问题:

模板是什么
render函数
render函数与 vdom

模板是什么

<div id='app'>
  <div>
    <input v-model="title"/>
    <button v-on:click="add">submit</button>
  </div>
  <ul>
    <li v-for="item in list">
      {{item}}
    </li>
  </ul>
</div>

这是一个模板

那么模板是什么呢?

  1. 本质是字符串,是以字符串存在的,只不过像html
  2. 有逻辑,比如判断,循环这些,如v-if,v-for等,怎么会有逻辑呢,之前写html就没逻辑
  3. 与html格式很像,但有很大区别。首先html在语法上是不认识v-if,v-for这些的。第二个是html是静态的,没有逻辑,vue是动态的,有逻辑的。它们只是格式很像
  4. 但是最终模板还是要转换为html来显示的。

那么它是怎么做到的呢?

首先模板最终必须转换成js代码,因为:

  • 模板有逻辑,有v-if,v-for。必须用js才能实现(图灵完备的语言)
  • 模板要转化为html渲染页面,必须用js才能实现

因此,模板最终要转换成一个js函数(render函数,也就是渲染函数)

首先了解下with

var obj = {
  name: 'zhangsan',
  age: 20,
  getAddress: function(){
    alert('beijing');
  }
}

// 不用with
function fn(){
  alert(obj.name);
  alert(obj.age);
  obj.getAddress();
}
fn();

// 使用width
function fn1(){
  with(obj){
    alert(name);
    alert(age);
    getAddress();
  }
}
fn1();

在实际开发中,尽量不要使用with。

fn是我们正常的使用。

fn1使用with的情况。

两个是同样的效果,用with的里面,都不写是谁的属性,是谁统一的,用with包起来。
这个可读性可能没那么强。

render函数

template 模板 通过 Compile 编译 得到 render函数。

compile 编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。

compile 编译过程,现阶段可以不需要完全掌握,只需要了解 解析的大致流程即可。

简单的例子

我们看最简单的一段模板

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
		<script src="../lib/vue-2.4.0.js"></script>
	</head>
	<body>
		<div id="app">
		  <p>{{price}}</p>
		</div>

		<script type="text/javascript">
			var vm = new Vue({
				el: '#app',
				data: {
					price:100
				}
			})
		</script>
	</body>
</html>

把模板摘出来

<div id="app">
	<p>{{price}}</p>
</div>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="./vue-2.5.13.js"></script>
</head>
<body>
    <div id="app">
        <p>{{price}}</p>
    </div>

    <script>
	/**
	* _c : 创建dom标签
	* _v : 创建文本节点
	* _s : toString
	*/
        var vm = new Vue({
            el: '#app',
            data: {
                price: 100
            }
        })
		
		// 这个模板最终生成的函数是下面这个
        // 以下是手写的 render 函数
        function render() {
            with(this) {  // this 就是 vm
                return _c(
                    'div',
                    {
                        attrs: {'id': 'app'}
                    },
                    [
                        _c('p', [_v(_s(price))])
                    ]
                )
            }
        }
		
		// 把函数翻译一下,也就是不用 with
        function render1() {
            return vm._c(
                'div',
                {
                    attrs: {'id': 'app'}
                },
                [
                    vm._c('p', [vm._v(vm._s(vm.price))])
                ]
            )
        }

    </script>
</body>
</html>

这个this就是vm这个实例,这个_c就是vm._c,用来创建dom标签的。

第一个参数是个div。

第二个参数是个对象,对象里面有属性。

第三个参数是个数组,数组里面只有一个元素,这个_c肯定也是vm._c。

这个_c第一个参数是p,第二个参数是个数组,里面也是一个数组,_v(_s(price)),这里面的price肯定是vm.price,就是data.price。

然后前面的_s就是vm._s,就是toString函数。

_v也是vm._v,用来创建文本节点的。

总结:

模板中所有信息都包含在render函数中。

this即vm

price即this.price即vm.price即data.price。

_c即this._c即vm._c

vm

todo-list demo的render函数

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <!-- 模板 -->
  <div id="app">
    <input v-model='title'/>
    <button v-on:click='add'>submit</button>
    <ul v-for='item in list'>
      {{item}}
    </ul>
  </div>
    
  <!-- 源码 -->
  <script src="./vue-2.6.10.js"></script>
  <script>
    var data = {
      title: '',
      list: []
    }
    // 初始化 vue 实例
    var vm = new Vue({
      el: '#app',
      data: data,
      methods: {
        add: function(){
          this.list.push(this.title);
          this.title = ''
        }
      }
    })
  </script>
</body>
</html>

然后通过这个例子,看看vue的render函数是什么样子的,在源码搜索code.render,然后打印出来。

这是与上面模板对应的 render 函数。

with(this) {
	return _c('div', {
			attrs: {
				"id": "app"
			}
		},
		[
			_c('input', {
				directives: [{ // 当title发生变化,会赋值给value,从而响应到input
					name: "model",
					rawName: "v-model",
					value: (title),
					expression: "title"
				}],
				domProps: {
					"value": (title)
				},
				on: { // v-model相关  input 里面的数据改变,事件监听到,会赋值给 title
					"input": function($event) {
						if ($event.target.composing) return;
						title = $event.target.value
					}
				}
			}),
			_v(" "),
			_c('button', {
					on: {
						// 绑定 click 事件
						"click": add
					}
				},
				[_v("submit")]
			),
			_v(" "),
			_l(
				// v-for相关
				(list),
				function(item) {
					return _c('ul', [_v("\n " + _s(item) + "\n ")])
				}
			)
		], 2)
}

这就是这个 demo 所对应的 render 函数。

在创建input的时候第二个参数有个directives。叫做指令,指令名字是model。value是title,也就是vm.title。

后面_v(''),_v表示创建文本节点,主要是input和button有个换行,如果不换行,就不会有 _v去创建一个空的文本节点。

_l 返回的是一个数组,针对list返回li的标签。

现在可以根据 todo-list demo 的 render 函数,回顾一下 render 函数中:

  • v-model 是怎么实现的?
  • v-on 是怎么实现的?
  • v-for 是怎么实现的?

渲染-vue的模板如何被渲染成html

以上已经解决了模板中“逻辑”的问题(v-for v-if)

还剩下模板生成 html 的问题

另外,vm._c 是什么?render 函数返回了什么?

先复习一下 vdom 的知识

这里 vm._c 跟 snabbdom 里面的h()函数非常相似。

vm._c 返回 vnode。那么 render 函数返回的也是 vnode。

可以这样理解:

  • vm._c 其实就相当于 snabbdom 中的 h 函数
  • render 函数执行之后,返回的是 vnode

vue的模板如何被渲染成html

vm._update(vnode) {
  const prevVnode = vm._vnode;
  vm._vnode = vnode;
  if(!prevVnode){
    vm.$el = vm._patch_(vm.$el, vnode); // 第一次没有值
  } else {
    vm.$el = vm._patch_(prevVnode, vnode); // 有值的情况
  }
}

function updateComponent(){
  // vm._render即上面的render函数,返回vnode
  vm._update(vm._render())
}

render 函数和 vdom

  • updateComponent 中实现了 vdom 的 patch
  • 页面首次渲染执行 updateComponent
  • data 中每次修改属性,执行 updateComponent

问题解答

模板是什么?

模板:字符串,有逻辑,嵌入 JS 变量……

模板必须转换为 JS 代码(有逻辑、渲染 html、JS 变量)

render 函数执行是返回 vnode

updateComponent

  • updateComponent 中实现了 vdom 的 patch
  • 页面首次渲染执行 updateComponent
  • data 中每次修改属性,执行 updateComponent

补充:vue的整个实现流程简易说明

  • 第一步:解析模板成 render 函数
  • 第二步:响应式开始监听
  • 第三步:首次渲染,显示页面,且绑定依赖
  • 第四步:data 属性变化,触发 rerender

1. 把模板解析为 render 函数

  • 运用 with
  • 模板中的所有信息都被 render 函数包含
  • 模板中用到的 data 中的属性,都变成了 JS 变量
  • 模板中的 v-model v-for v-on 都变成了 JS 逻辑
  • render 函数返回 vnode

2. 响应式开始监听

  • Object.defineProperty
  • 将data 的属性代理到 vm 上

3. 首次渲染,显示页面且绑定依赖

  • 初次渲染,执行 updateComponent, 执行 vm._render()
  • 执行 render 函数,会访问到 data 下的数据
  • 会被响应式的 get 方法监听到
  • 执行 updateComponent,会走到 vdom 的 patch 方法
  • patch 将 vnode 渲染成 DOM,初次渲染完成

4. data 属性变化,触发 rerender

  • 修改属性,被响应式的 set 监听到
  • set 中执行 updateComponent
  • updateComponent 重新执行 vm._render()
  • 生成的 vnode 和 prevVnode ,通过 patch 进行对比
  • 渲染到 html 中
posted @ 2020-03-28 15:47  Chrislinlin  阅读(4736)  评论(0编辑  收藏  举报