本篇讲述了vue实现初始化渲染过程的源码流程,因此不可避免的涉及到大量代码(事实上已经尽量剔除非关键代码),如果您对vue响应式原理还不是太了解,请先阅读vue响应式原理一文,本篇文章将按照初始化阶段->收集需要更新的组件阶段-->dom-diff更新组件dom阶段(只涉及初渲染对比)进行展开,同时跳过模版编译阶段,只关注最核心流程;模版编译的主要作用是构建模版引擎以及多端代码构建,由于vue中采用的是正则提取的方式解析模版,而不是采用更通用的有限状态机模式。因此建议您避免在模版编译阶段耗费过多时间。
1. 初始化阶段
new Vue传入option选项,之后会调用init方法,在这个方法里首先会把vue混入的选项与option选项进行合并,然后进行一系列的初始化操作,如初始化生命周期,初始化render函数 初始化数据,以及调用钩子函数等,其中最重要的是通过Object.defineProperty实现代理模式。
import {initMixin} from './init';
function Vue(options) {
this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
初始化data,以及把data的属性直接挂载到vm上方便读取
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key];
},
set(newValue){
vm[source][key] = newValue;
}
});
}
function initData(vm){
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? data.call(vm) : data;
for(let key in data){ // 将_data上的属性全部代理给vm实例
proxy(vm,'_data',key)
}
observe(data);
}
2. 收集需要更新的组件阶段
通过代理模式与观察者模式 在对象属性读取以及修改时收集依赖项以及通知依赖项
class Observer { // 观测值
constructor(value){
this.walk(value);
}
walk(data){ // 让对象上的所有属性依次进行观测
let keys = Object.keys(data);
for(let i = 0; i < keys.length; i++){
let key = keys[i];
let value = data[key];
defineReactive(data,key,value);
}
}
}
class Dep{
constructor(){
this.id = id++;
this.subs = [];
}
depend(){
if(Dep.target){
Dep.target.addDep(this);// 让watcher,去存放dep
}
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
addSub(watcher){
this.subs.push(watcher);
}
}
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
if(Dep.target){ // 如果取值时有watcher
dep.depend(); // 让watcher保存dep,并且让dep 保存watcher
}
return value
},
set(newValue) {
if (newValue == value) return;
observe(newValue);
value = newValue;
dep.notify(); // 通知渲染watcher去更新
}
});
export function observe(data) {
if(typeof data !== 'object' || data == null){
return;
}
return new Observer(data);
}
实现观察者watcher
let id = 0;
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm;
this.exprOrFn = exprOrFn;
if (typeof exprOrFn == 'function') {
this.getter = exprOrFn;
}
this.cb = cb;
this.options = options;
this.id = id++;
this.get();
}
get() {
this.getter();
}
}
export default Watcher;
3. 生成render函数阶段
初始化完成之后会检测当前实例是否为根组件,如果为根组件则调用vm.$mount(vm.$options.el)进行模版编译操作,最终的目标是生成虚拟dom,提供给dom-diff进行比对,更新dom.
vm.$mount是一个高阶函数,它首先会检测用户是否传入了render函数,如果用户没有传入,它会用来调用compileToFunctions使用数据和模版编译render函数,render函数的作用是生产虚拟dom。
react和vue都使用到了dom-diff ,之所以使用dom-diff是因为无法做到收集dom元素级的依赖,react中没有进行依赖收集,vue中收集依赖到组件级,这是因为收集dom级的依赖,会导致内存和计算性能大量浪费,因此他们都需要使用dom-diff来对比组件中变化了的dom元素,从而实现最小量更新
事实上虚拟dom并不比真实dom快,只是在解决前端手动操作dom的繁琐时 找到的性能相对较高的实现方法。参见 www.zhihu.com/question/31…
import {mountComponent} from './lifecycle'
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options;
el = document.querySelector(el);
// 如果没有render方法
if (!options.render) {
let template = options.template;
// 如果没有模板但是有el
if (!template && el) {
template = el.outerHTML;
}
const render= compileToFunctions(template);
options.render = render;
}
mountComponent(vm,el);
}
function gen(node) {
if (node.type == 1) {
return generate(node);
} else {
let text = node.text
if(!defaultTagRE.test(text)){
return `_v(${JSON.stringify(text)})`
}
let lastIndex = defaultTagRE.lastIndex = 0
let tokens = [];
let match,index;
while (match = defaultTagRE.exec(text)) {
index = match.index;
if(index > lastIndex){
tokens.push(JSON.stringify(text.slice(lastIndex,index)));
}
tokens.push(`_s(${match[1].trim()})`)
lastIndex = index + match[0].length;
}
if(lastIndex < text.length){
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join('+')})`;
}
}
function getChildren(el) { // 生成儿子节点
const children = el.children;
if (children) {
return `${children.map(c=>gen(c)).join(',')}`
} else {
return false;
}
}
function genProps(attrs){ // 生成属性
let str = '';
for(let i = 0; i<attrs.length; i++){
let attr = attrs[i];
if(attr.name === 'style'){
let obj = {}
attr.value.split(';').forEach(item=>{
let [key,value] = item.split(':');
obj[key] = value;
})
attr.value = obj;
}
str += `${attr.name}:${JSON.stringify(attr.value)},`;
}
return `{${str.slice(0,-1)}}`;
}
function generate(el) {
let children = getChildren(el);
let code = `_c('${el.tag}',${
el.attrs.length?`${genProps(el.attrs)}`:'undefined'
}${
children? `,${children}`:''
})`;
return code;
}
生成render函数
export function compileToFunctions(template) {
parseHTML(template);
let code = generate(root);
let render = `with(this){return ${code}}`;
let renderFn = new Function(render);
return renderFn
}
生成虚拟dom
import {createTextNode,createElement} from './vdom/create-element'
export function renderMixin(Vue){
Vue.prototype._v = function (text) { // 创建文本
return createTextNode(text);
}
Vue.prototype._c = function () { // 创建元素
return createElement(...arguments);
}
Vue.prototype._s = function (val) {
return val == null? '' : (typeof val === 'object'?JSON.stringify(val):val);
}
Vue.prototype._render = function () {
const vm = this;
const {render} = vm.$options;
let vnode = render.call(vm);
return vnode;
}
}
至此,我们实现了数据属性与组件更新逻辑的绑定,有了生成虚拟dom的render方法,我们来看下当如何把虚拟dom挂载到页面上
4. dom-diff更新组件dom阶段
mountCompoent函数也是一个高阶函数,通过patch方法 进行dom-diff比对,更新组件的dom节点
Vue.prototype._update = function(vnode) {
const el = this.$el
this.$el = patch(el,vnode)
}
function patch(oldVnode,vnode) { // oldVnode->el,vnode->render函数返回值
const isRealElement = oldVnode.nodeType
if(isRealElement) {
const oldEle = oldVnode // el
const parentEle = oldEle.parentNode
const el = createEle(vnode)
parentEle.insertBefore(el,oldVnode)
parentEle.removeChild(oldVnode)
return el
}else {
// diff算法
}
}
// 真正的渲染函数
function createEle(vnode) {
// 如果是数字类型转化为字符串
// 字符串类型直接就是文本节点
if (vnode.tag == null) {
return document.createTextNode(vnode.text);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
});
}
// 子数组进行递归操作
vnode.children.forEach((child) => dom.appendChild(createEle(child)));
return dom;
}
<!-- ```
回看`mountCompoent`方法
---
```js
function mountComponent(vm,el) {
el = document.querySelector(el)
vm.$el = el
const updateComponent = ()=>{
vm._update(vm._render())
}
new Watcher(vm,updateComponent,()=>{},true)
}